287 lines
10 KiB
Rust
287 lines
10 KiB
Rust
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<WrappedLine> {
|
||
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<char> = 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<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
|
||
let content_width = area.width.saturating_sub(2) as usize;
|
||
let mut lines: Vec<Line> = Vec::new();
|
||
|
||
let selected_msg_id = app.get_selected_message().map(|m| m.id());
|
||
let mut selected_msg_line: Option<usize> = None;
|
||
|
||
#[cfg(feature = "images")]
|
||
let mut deferred_images: Vec<components::DeferredImageRender> = 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<T: TdClientTrait>(
|
||
f: &mut Frame,
|
||
area: Rect,
|
||
app: &mut App<T>,
|
||
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());
|
||
}
|