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:
@@ -191,73 +191,9 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
/// Рендерит список сообщений с группировкой по дате/отправителю и автоскроллом
|
||||||
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||||
// Режим профиля
|
let content_width = area.width.saturating_sub(2) as usize;
|
||||||
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;
|
|
||||||
|
|
||||||
// Messages с группировкой по дате и отправителю
|
// Messages с группировкой по дате и отправителю
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
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
|
// Вычисляем скролл с учётом пользовательского 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();
|
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)
|
let messages_widget = Paragraph::new(lines)
|
||||||
.block(Block::default().borders(Borders::ALL))
|
.block(Block::default().borders(Borders::ALL))
|
||||||
.scroll((scroll_offset, 0));
|
.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 (input_line, input_title) = if app.is_forwarding() {
|
||||||
// Режим пересылки - показываем превью сообщения
|
// Режим пересылки - показываем превью сообщения
|
||||||
let forward_preview = app
|
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)
|
let input = Paragraph::new(input_line)
|
||||||
.block(input_block)
|
.block(input_block)
|
||||||
.wrap(ratatui::widgets::Wrap { trim: false });
|
.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 {
|
} else {
|
||||||
let empty = Paragraph::new("Выберите чат")
|
let empty = Paragraph::new("Выберите чат")
|
||||||
.block(Block::default().borders(Borders::ALL))
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
|||||||
Reference in New Issue
Block a user