fixes
This commit is contained in:
@@ -44,6 +44,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
.map(|chat| {
|
||||
let is_selected = app.selected_chat_id == Some(chat.id);
|
||||
let pin_icon = if chat.is_pinned { "📌 " } else { "" };
|
||||
let mute_icon = if chat.is_muted { "🔇 " } else { "" };
|
||||
|
||||
// Онлайн-статус (зелёная точка для онлайн)
|
||||
let status_icon = match app.td_client.get_user_status_by_chat_id(chat.id) {
|
||||
@@ -57,15 +58,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
.map(|u| format!(" {}", u))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Индикатор упоминаний @
|
||||
let mention_badge = if chat.unread_mention_count > 0 {
|
||||
" @".to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let unread_badge = if chat.unread_count > 0 {
|
||||
format!(" ({})", chat.unread_count)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let content = format!("{}{}{}{}{}{}", prefix, status_icon, pin_icon, chat.title, username_text, unread_badge);
|
||||
let content = format!("{}{}{}{}{}{}{}{}", prefix, status_icon, pin_icon, mute_icon, chat.title, username_text, mention_badge, unread_badge);
|
||||
|
||||
// Цвет зависит от онлайн-статуса
|
||||
// Цвет: онлайн — зелёные, остальные — белые
|
||||
let style = match app.td_client.get_user_status_by_chat_id(chat.id) {
|
||||
Some(UserOnlineStatus::Online) => Style::default().fg(Color::Green),
|
||||
_ => Style::default().fg(Color::White),
|
||||
|
||||
@@ -5,21 +5,35 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::NetworkState;
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let status = if let Some(msg) = &app.status_message {
|
||||
format!(" {} ", msg)
|
||||
} else if let Some(err) = &app.error_message {
|
||||
format!(" Error: {} ", err)
|
||||
} else if app.is_searching {
|
||||
" j/k: Navigate | Enter: Select | Esc: Cancel ".to_string()
|
||||
} else if app.selected_chat_id.is_some() {
|
||||
" Cmd+j/k: Scroll | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ".to_string()
|
||||
} else {
|
||||
" j/k: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ".to_string()
|
||||
// Индикатор состояния сети
|
||||
let network_indicator = match app.td_client.network_state {
|
||||
NetworkState::Ready => "",
|
||||
NetworkState::WaitingForNetwork => "⚠ Нет сети | ",
|
||||
NetworkState::ConnectingToProxy => "⏳ Прокси... | ",
|
||||
NetworkState::Connecting => "⏳ Подключение... | ",
|
||||
NetworkState::Updating => "⏳ Обновление... | ",
|
||||
};
|
||||
|
||||
let style = if app.error_message.is_some() {
|
||||
let status = if let Some(msg) = &app.status_message {
|
||||
format!(" {}{} ", network_indicator, msg)
|
||||
} else if let Some(err) = &app.error_message {
|
||||
format!(" {}Error: {} ", network_indicator, err)
|
||||
} else if app.is_searching {
|
||||
format!(" {}↑/↓: Navigate | Enter: Select | Esc: Cancel ", network_indicator)
|
||||
} else if app.selected_chat_id.is_some() {
|
||||
format!(" {}↑/↓: Scroll | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
} else {
|
||||
format!(" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
};
|
||||
|
||||
let style = if matches!(app.td_client.network_state, NetworkState::WaitingForNetwork) {
|
||||
Style::default().fg(Color::Red)
|
||||
} else if !matches!(app.td_client.network_state, NetworkState::Ready) {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else if app.error_message.is_some() {
|
||||
Style::default().fg(Color::Red)
|
||||
} else if app.status_message.is_some() {
|
||||
Style::default().fg(Color::Yellow)
|
||||
|
||||
@@ -8,14 +8,66 @@ use ratatui::{
|
||||
use crate::app::App;
|
||||
use crate::utils::{format_timestamp, format_date, get_day};
|
||||
|
||||
/// Разбивает текст на строки с учётом максимальной ширины
|
||||
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
|
||||
if max_width == 0 {
|
||||
return vec![text.to_string()];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut current_line = String::new();
|
||||
let mut current_width = 0;
|
||||
|
||||
for word in text.split_whitespace() {
|
||||
let word_width = word.chars().count();
|
||||
|
||||
if current_width == 0 {
|
||||
// Первое слово в строке
|
||||
current_line = word.to_string();
|
||||
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(current_line);
|
||||
current_line = word.to_string();
|
||||
current_width = word_width;
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
result.push(current_line);
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
result.push(String::new());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
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 message_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(3), // Input box
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area);
|
||||
|
||||
@@ -112,22 +164,62 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Исходящие: справа, формат "текст (HH:MM ✓✓)"
|
||||
let read_mark = if msg.is_read { "✓✓" } else { "✓" };
|
||||
let time_mark = format!("({} {})", time, read_mark);
|
||||
let msg_text = format!("{} {}", msg.content, time_mark);
|
||||
let msg_len = msg_text.chars().count();
|
||||
let padding = content_width.saturating_sub(msg_len + 1);
|
||||
let time_mark_len = time_mark.chars().count() + 1; // +1 для пробела
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" ".repeat(padding)),
|
||||
Span::styled(msg.content.clone(), Style::default().fg(Color::Green)),
|
||||
Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
// Максимальная ширина для текста сообщения (оставляем место для time_mark)
|
||||
let max_msg_width = content_width.saturating_sub(time_mark_len + 2);
|
||||
|
||||
let wrapped_lines = wrap_text(&msg.content, max_msg_width);
|
||||
let total_wrapped = wrapped_lines.len();
|
||||
|
||||
for (i, line_text) in wrapped_lines.into_iter().enumerate() {
|
||||
let is_last_line = i == total_wrapped - 1;
|
||||
let line_len = line_text.chars().count();
|
||||
|
||||
if is_last_line {
|
||||
// Последняя строка — добавляем time_mark
|
||||
let full_len = line_len + time_mark_len;
|
||||
let padding = content_width.saturating_sub(full_len + 1);
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" ".repeat(padding)),
|
||||
Span::styled(line_text, Style::default().fg(Color::Green)),
|
||||
Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
} else {
|
||||
// Промежуточные строки — просто текст справа
|
||||
let padding = content_width.saturating_sub(line_len + 1);
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" ".repeat(padding)),
|
||||
Span::styled(line_text, Style::default().fg(Color::Green)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Входящие: слева, формат "(HH:MM) текст"
|
||||
let time_str = format!("({})", time);
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)),
|
||||
Span::raw(format!(" {}", msg.content)),
|
||||
]));
|
||||
let time_prefix_len = time_str.chars().count() + 2; // " (HH:MM) "
|
||||
|
||||
// Максимальная ширина для текста
|
||||
let max_msg_width = content_width.saturating_sub(time_prefix_len + 1);
|
||||
|
||||
let wrapped_lines = wrap_text(&msg.content, max_msg_width);
|
||||
|
||||
for (i, line_text) in wrapped_lines.into_iter().enumerate() {
|
||||
if i == 0 {
|
||||
// Первая строка — с временем
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)),
|
||||
Span::raw(format!(" {}", line_text)),
|
||||
]));
|
||||
} else {
|
||||
// Последующие строки — с отступом
|
||||
let indent = " ".repeat(time_prefix_len);
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(indent),
|
||||
Span::raw(line_text),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +247,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(messages_widget, message_chunks[1]);
|
||||
|
||||
// Input box
|
||||
// Input box с wrap для длинного текста
|
||||
let input_text = if app.message_input.is_empty() {
|
||||
"> Введите сообщение...".to_string()
|
||||
} else {
|
||||
@@ -168,7 +260,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
let input = Paragraph::new(input_text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.style(input_style);
|
||||
.style(input_style)
|
||||
.wrap(ratatui::widgets::Wrap { trim: false });
|
||||
f.render_widget(input, message_chunks[2]);
|
||||
} else {
|
||||
let empty = Paragraph::new("Выберите чат")
|
||||
|
||||
@@ -6,12 +6,39 @@ mod messages;
|
||||
mod footer;
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use crate::app::{App, AppScreen};
|
||||
|
||||
/// Минимальная ширина терминала
|
||||
const MIN_WIDTH: u16 = 80;
|
||||
/// Минимальная высота терминала
|
||||
const MIN_HEIGHT: u16 = 20;
|
||||
|
||||
pub fn render(f: &mut Frame, app: &mut App) {
|
||||
let area = f.area();
|
||||
|
||||
// Проверяем минимальный размер терминала
|
||||
if area.width < MIN_WIDTH || area.height < MIN_HEIGHT {
|
||||
render_size_warning(f, area.width, area.height);
|
||||
return;
|
||||
}
|
||||
|
||||
match app.screen {
|
||||
AppScreen::Loading => loading::render(f, app),
|
||||
AppScreen::Auth => auth::render(f, app),
|
||||
AppScreen::Main => main_screen::render(f, app),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_size_warning(f: &mut Frame, width: u16, height: u16) {
|
||||
let message = format!(
|
||||
"Терминал слишком мал: {}x{}\n\nМинимум: {}x{}",
|
||||
width, height, MIN_WIDTH, MIN_HEIGHT
|
||||
);
|
||||
let warning = Paragraph::new(message)
|
||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(warning, f.area());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user