refactor: extract message list and input box rendering (ui/messages.rs)

Завершена Phase 5 - полная декомпозиция render():
- render_message_list() - список сообщений с автоскроллом (~100 строк)
- render_input_box() - input с режимами forward/select/edit/reply (~145 строк)

Результат:
- render() сокращена с ~390 до ~92 строк (76% ✂️)
- 4 извлечённые функции: header, pinned, message_list, input_box
- Каждая функция имеет чёткую ответственность

Файл: 879 → 905 строк (+26 doc-комментариев)
Phase 5 завершена: ui/messages.rs рефакторинг выполнен! 🎉

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-03 21:30:20 +03:00
parent 315395f1f2
commit 2dbbf1cb5b

View File

@@ -191,73 +191,9 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
result
}
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Режим профиля
if app.is_profile_mode() {
if let Some(profile) = app.get_profile_info() {
crate::ui::profile::render(f, area, app, profile);
}
return;
}
// Режим поиска по сообщениям
if app.is_message_search_mode() {
render_search_mode(f, area, app);
return;
}
// Режим просмотра закреплённых сообщений
if app.is_pinned_mode() {
render_pinned_mode(f, area, app);
return;
}
if let Some(chat) = app.get_selected_chat() {
// Вычисляем динамическую высоту инпута на основе длины текста
let input_width = area.width.saturating_sub(4) as usize; // -2 для рамок, -2 для "> "
let input_text_len = app.message_input.chars().count() + 2; // +2 для "> "
let input_lines = if input_width > 0 {
((input_text_len as f32 / input_width as f32).ceil() as u16).max(1)
} else {
1
};
// Минимум 3 строки (1 контент + 2 рамки), максимум 10
let input_height = (input_lines + 2).min(10).max(3);
// Проверяем, есть ли закреплённое сообщение
let has_pinned = app.td_client.current_pinned_message().is_some();
let message_chunks = if has_pinned {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Chat header
Constraint::Length(1), // Pinned bar
Constraint::Min(0), // Messages
Constraint::Length(input_height), // Input box (динамическая высота)
])
.split(area)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Chat header
Constraint::Length(0), // Pinned bar (hidden)
Constraint::Min(0), // Messages
Constraint::Length(input_height), // Input box (динамическая высота)
])
.split(area)
};
// Chat header с typing status
render_chat_header(f, message_chunks[0], app, chat);
// Pinned bar (если есть закреплённое сообщение)
render_pinned_bar(f, message_chunks[1], app);
// Ширина области сообщений (без рамок)
let content_width = message_chunks[2].width.saturating_sub(2) as usize;
/// Рендерит список сообщений с группировкой по дате/отправителю и автоскроллом
fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
let content_width = area.width.saturating_sub(2) as usize;
// Messages с группировкой по дате и отправителю
let mut lines: Vec<Line> = Vec::new();
@@ -316,7 +252,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
}
// Вычисляем скролл с учётом пользовательского offset
let visible_height = message_chunks[2].height.saturating_sub(2) as usize;
let visible_height = area.height.saturating_sub(2) as usize;
let total_lines = lines.len();
// Базовый скролл (показываем последние сообщения)
@@ -350,9 +286,11 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
let messages_widget = Paragraph::new(lines)
.block(Block::default().borders(Borders::ALL))
.scroll((scroll_offset, 0));
f.render_widget(messages_widget, message_chunks[2]);
f.render_widget(messages_widget, area);
}
// Input box с wrap для длинного текста и блочным курсором
/// Рендерит input box с поддержкой разных режимов (forward/select/edit/reply/normal)
fn render_input_box<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
let (input_line, input_title) = if app.is_forwarding() {
// Режим пересылки - показываем превью сообщения
let forward_preview = app
@@ -494,7 +432,79 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
let input = Paragraph::new(input_line)
.block(input_block)
.wrap(ratatui::widgets::Wrap { trim: false });
f.render_widget(input, message_chunks[3]);
f.render_widget(input, area);
}
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Режим профиля
if app.is_profile_mode() {
if let Some(profile) = app.get_profile_info() {
crate::ui::profile::render(f, area, app, profile);
}
return;
}
// Режим поиска по сообщениям
if app.is_message_search_mode() {
render_search_mode(f, area, app);
return;
}
// Режим просмотра закреплённых сообщений
if app.is_pinned_mode() {
render_pinned_mode(f, area, app);
return;
}
if let Some(chat) = app.get_selected_chat() {
// Вычисляем динамическую высоту инпута на основе длины текста
let input_width = area.width.saturating_sub(4) as usize; // -2 для рамок, -2 для "> "
let input_text_len = app.message_input.chars().count() + 2; // +2 для "> "
let input_lines = if input_width > 0 {
((input_text_len as f32 / input_width as f32).ceil() as u16).max(1)
} else {
1
};
// Минимум 3 строки (1 контент + 2 рамки), максимум 10
let input_height = (input_lines + 2).min(10).max(3);
// Проверяем, есть ли закреплённое сообщение
let has_pinned = app.td_client.current_pinned_message().is_some();
let message_chunks = if has_pinned {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Chat header
Constraint::Length(1), // Pinned bar
Constraint::Min(0), // Messages
Constraint::Length(input_height), // Input box (динамическая высота)
])
.split(area)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Chat header
Constraint::Length(0), // Pinned bar (hidden)
Constraint::Min(0), // Messages
Constraint::Length(input_height), // Input box (динамическая высота)
])
.split(area)
};
// Chat header с typing status
render_chat_header(f, message_chunks[0], app, chat);
// Pinned bar (если есть закреплённое сообщение)
render_pinned_bar(f, message_chunks[1], app);
// Messages с группировкой по дате и отправителю
render_message_list(f, message_chunks[2], app);
// Input box с wrap для длинного текста и блочным курсором
render_input_box(f, message_chunks[3], app);
} else {
let empty = Paragraph::new("Выберите чат")
.block(Block::default().borders(Borders::ALL))