use crate::app::methods::messages::MessageMethods; use crate::app::App; use crate::message_grouping::{group_messages, MessageGroup}; use crate::tdlib::TdClientTrait; use crate::ui::components; use ratatui::{ layout::Rect, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, Frame, }; /// Информация о строке после переноса: текст и позиция в оригинале. pub(crate) struct WrappedLine { pub text: String, } /// Разбивает текст на строки с учётом максимальной ширины. pub(crate) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec { if max_width == 0 { return vec![WrappedLine { text: text.to_string() }]; } let mut result = Vec::new(); let mut current_line = String::new(); let mut current_width = 0; let chars: Vec = text.chars().collect(); let mut word_start = 0; let mut in_word = false; for (i, ch) in chars.iter().enumerate() { if ch.is_whitespace() { if in_word { let word: String = chars[word_start..i].iter().collect(); let word_width = word.chars().count(); if current_width == 0 { current_line = word; current_width = word_width; } else if current_width + 1 + word_width <= max_width { current_line.push(' '); current_line.push_str(&word); current_width += 1 + word_width; } else { result.push(WrappedLine { text: current_line }); current_line = word; current_width = word_width; } in_word = false; } } else if !in_word { word_start = i; in_word = true; } } if in_word { let word: String = chars[word_start..].iter().collect(); let word_width = word.chars().count(); if current_width == 0 { current_line = word; } else if current_width + 1 + word_width <= max_width { current_line.push(' '); current_line.push_str(&word); } else { result.push(WrappedLine { text: current_line }); current_line = word; } } if !current_line.is_empty() { result.push(WrappedLine { text: current_line }); } if result.is_empty() { result.push(WrappedLine { text: String::new() }); } result } /// Рендерит список сообщений с группировкой по дате/отправителю и автоскроллом. pub(super) fn render_message_list(f: &mut Frame, area: Rect, app: &mut App) { let content_width = area.width.saturating_sub(2) as usize; let mut lines: Vec = Vec::new(); let selected_msg_id = app.get_selected_message().map(|m| m.id()); let mut selected_msg_line: Option = None; #[cfg(feature = "images")] let mut deferred_images: Vec = Vec::new(); let current_messages = app.td_client.current_chat_messages(); let grouped = group_messages(¤t_messages); let mut is_first_date = true; let mut is_first_sender = true; for group in grouped { match group { MessageGroup::DateSeparator(date) => { lines.extend(components::render_date_separator(date, content_width, is_first_date)); is_first_date = false; is_first_sender = true; } MessageGroup::SenderHeader { is_outgoing, sender_name } => { lines.extend(components::render_sender_header( is_outgoing, &sender_name, content_width, is_first_sender, )); is_first_sender = false; } MessageGroup::Message(msg) => { let is_selected = selected_msg_id == Some(msg.id()); if is_selected { selected_msg_line = Some(lines.len()); } let bubble_lines = components::render_message_bubble( msg, app.config(), content_width, selected_msg_id, app.playback_state.as_ref(), ); #[cfg(feature = "images")] if let Some(photo) = msg.photo_info() { if let crate::tdlib::PhotoDownloadState::Downloaded(path) = &photo.download_state { let inline_width = content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH); let img_height = components::calculate_image_height( photo.width, photo.height, inline_width, ); let img_width = inline_width as u16; let bubble_len = bubble_lines.len(); let placeholder_start = lines.len() + bubble_len - img_height as usize; deferred_images.push(components::DeferredImageRender { message_id: msg.id(), photo_path: path.clone(), line_offset: placeholder_start, x_offset: 0, width: img_width, height: img_height, }); } } lines.extend(bubble_lines); } MessageGroup::Album(album_messages) => { #[cfg(feature = "images")] { let is_selected = album_messages .iter() .any(|m| selected_msg_id == Some(m.id())); if is_selected { selected_msg_line = Some(lines.len()); } let (bubble_lines, album_deferred) = components::render_album_bubble( &album_messages, app.config(), content_width, selected_msg_id, ); for mut d in album_deferred { d.line_offset += lines.len(); deferred_images.push(d); } lines.extend(bubble_lines); } #[cfg(not(feature = "images"))] { for msg in &album_messages { let is_selected = selected_msg_id == Some(msg.id()); if is_selected { selected_msg_line = Some(lines.len()); } lines.extend(components::render_message_bubble( msg, app.config(), content_width, selected_msg_id, app.playback_state.as_ref(), )); } } } } } if lines.is_empty() { lines.push(Line::from(Span::styled("Нет сообщений", Style::default().fg(Color::Gray)))); } let visible_height = area.height.saturating_sub(2) as usize; let total_lines = lines.len(); let base_scroll = total_lines.saturating_sub(visible_height); let scroll_offset = if app.is_selecting_message() { if let Some(selected_line) = selected_msg_line { if selected_line < visible_height / 2 { 0 } else if selected_line > total_lines.saturating_sub(visible_height / 2) { base_scroll } else { selected_line.saturating_sub(visible_height / 2) } } else { base_scroll.saturating_sub(app.message_scroll_offset) } } else { base_scroll.saturating_sub(app.message_scroll_offset) } as u16; let messages_widget = Paragraph::new(lines) .block(Block::default().borders(Borders::ALL)) .scroll((scroll_offset, 0)); f.render_widget(messages_widget, area); #[cfg(feature = "images")] render_deferred_images(f, area, app, &deferred_images, visible_height, scroll_offset); } #[cfg(feature = "images")] fn render_deferred_images( f: &mut Frame, area: Rect, app: &mut App, deferred_images: &[components::DeferredImageRender], visible_height: usize, scroll_offset: u16, ) { use ratatui_image::StatefulImage; let should_render_images = app .last_image_render_time .map(|t| t.elapsed() > std::time::Duration::from_millis(66)) .unwrap_or(true); if deferred_images.is_empty() || !should_render_images { return; } let content_x = area.x + 1; let content_y = area.y + 1; for d in deferred_images { let y_in_content = d.line_offset as i32 - scroll_offset as i32; if y_in_content < 0 || y_in_content as usize >= visible_height { continue; } let img_y = content_y + y_in_content as u16; let remaining_height = (content_y + visible_height as u16).saturating_sub(img_y); if d.height > remaining_height { continue; } let img_rect = Rect::new(content_x + d.x_offset, img_y, d.width, d.height); if let Some(renderer) = &mut app.inline_image_renderer { let _ = renderer.load_image(d.message_id, &d.photo_path); if let Some(protocol) = renderer.get_protocol(&d.message_id) { f.render_stateful_widget(StatefulImage::default(), img_rect, protocol); } } } app.last_image_render_time = Some(std::time::Instant::now()); }