This commit is contained in:
Mikhail Kilin
2026-01-30 15:07:13 +03:00
parent 126c7482af
commit 4deb0fbe00
32 changed files with 1049 additions and 697 deletions

View File

@@ -1,3 +1,5 @@
use crate::app::App;
use crate::utils::{format_date, format_timestamp_with_tz, get_day};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -5,8 +7,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::app::App;
use crate::utils::{format_timestamp_with_tz, format_date, get_day};
use tdlib_rs::enums::TextEntityType;
use tdlib_rs::types::TextEntity;
@@ -58,9 +58,16 @@ impl CharStyle {
}
/// Преобразует текст с entities в вектор стилизованных Span (owned)
fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Color) -> Vec<Span<'static>> {
fn format_text_with_entities(
text: &str,
entities: &[TextEntity],
base_color: Color,
) -> Vec<Span<'static>> {
if entities.is_empty() {
return vec![Span::styled(text.to_string(), Style::default().fg(base_color))];
return vec![Span::styled(
text.to_string(),
Style::default().fg(base_color),
)];
}
// Создаём массив стилей для каждого символа
@@ -82,9 +89,13 @@ fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Co
char_styles[i].code = true
}
TextEntityType::Spoiler => char_styles[i].spoiler = true,
TextEntityType::Url | TextEntityType::TextUrl(_) | TextEntityType::EmailAddress
TextEntityType::Url
| TextEntityType::TextUrl(_)
| TextEntityType::EmailAddress
| TextEntityType::PhoneNumber => char_styles[i].url = true,
TextEntityType::Mention | TextEntityType::MentionName(_) => char_styles[i].mention = true,
TextEntityType::Mention | TextEntityType::MentionName(_) => {
char_styles[i].mention = true
}
_ => {}
}
}
@@ -144,7 +155,12 @@ fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool {
}
/// Рендерит текст инпута с блочным курсором
fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: Color) -> Line<'static> {
fn render_input_with_cursor(
prefix: &str,
text: &str,
cursor_pos: usize,
color: Color,
) -> Line<'static> {
let chars: Vec<char> = text.chars().collect();
let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
@@ -186,10 +202,7 @@ struct WrappedLine {
/// Возвращает строки с информацией о позициях для корректного применения entities
fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
if max_width == 0 {
return vec![WrappedLine {
text: text.to_string(),
start_offset: 0,
}];
return vec![WrappedLine { text: text.to_string(), start_offset: 0 }];
}
let mut result = Vec::new();
@@ -263,10 +276,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
}
if result.is_empty() {
result.push(WrappedLine {
text: String::new(),
start_offset: 0,
});
result.push(WrappedLine { text: String::new(), start_offset: 0 });
}
result
@@ -368,24 +378,28 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
};
// Chat header с typing status
let typing_action = app.td_client.typing_status.as_ref().map(|(_, action, _)| action.clone());
let typing_action = app
.td_client
.typing_status
.as_ref()
.map(|(_, action, _)| action.clone());
let header_line = if let Some(action) = typing_action {
// Показываем typing status: "👤 Имя @username печатает..."
let mut spans = vec![
Span::styled(
format!("👤 {}", chat.title),
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
),
];
let mut spans = vec![Span::styled(
format!("👤 {}", chat.title),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)];
if let Some(username) = &chat.username {
spans.push(Span::styled(
format!(" {}", username),
Style::default().fg(Color::Gray),
));
spans
.push(Span::styled(format!(" {}", username), Style::default().fg(Color::Gray)));
}
spans.push(Span::styled(
format!(" {}", action),
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC),
));
Line::from(spans)
} else {
@@ -396,17 +410,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
};
Line::from(Span::styled(
header_text,
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
};
let header = Paragraph::new(header_line)
.block(Block::default().borders(Borders::ALL));
let header = Paragraph::new(header_line).block(Block::default().borders(Borders::ALL));
f.render_widget(header, message_chunks[0]);
// Pinned bar (если есть закреплённое сообщение)
if let Some(pinned_msg) = &app.td_client.current_pinned_message {
let pinned_preview: String = pinned_msg.content.chars().take(40).collect();
let ellipsis = if pinned_msg.content.chars().count() > 40 { "..." } else { "" };
let ellipsis = if pinned_msg.content.chars().count() > 40 {
"..."
} else {
""
};
let pinned_datetime = crate::utils::format_datetime(pinned_msg.date);
let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis);
let pinned_hint = "Ctrl+P";
@@ -421,8 +440,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
Span::raw(" ".repeat(padding)),
Span::styled(pinned_hint, Style::default().fg(Color::Gray)),
]);
let pinned_bar = Paragraph::new(pinned_line)
.style(Style::default().bg(Color::Rgb(40, 20, 40)));
let pinned_bar =
Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
f.render_widget(pinned_bar, message_chunks[1]);
}
@@ -484,9 +503,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
}
let sender_style = if msg.is_outgoing {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
};
if msg.is_outgoing {
@@ -540,16 +563,21 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
]));
} else {
// Forward слева для входящих
lines.push(Line::from(vec![
Span::styled(forward_line, Style::default().fg(Color::Magenta)),
]));
lines.push(Line::from(vec![Span::styled(
forward_line,
Style::default().fg(Color::Magenta),
)]));
}
}
// Отображаем reply если есть
if let Some(reply) = &msg.reply_to {
let reply_text: String = reply.text.chars().take(40).collect();
let ellipsis = if reply.text.chars().count() > 40 { "..." } else { "" };
let ellipsis = if reply.text.chars().count() > 40 {
"..."
} else {
""
};
let reply_line = format!("{}: {}{}", reply.sender_name, reply_text, ellipsis);
let reply_len = reply_line.chars().count();
@@ -562,9 +590,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
]));
} else {
// Reply слева для входящих
lines.push(Line::from(vec![
Span::styled(reply_line, Style::default().fg(Color::Cyan)),
]));
lines.push(Line::from(vec![Span::styled(
reply_line,
Style::default().fg(Color::Cyan),
)]));
}
}
@@ -593,11 +622,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
);
// Форматируем текст с entities
let formatted_spans = format_text_with_entities(
&wrapped.text,
&line_entities,
msg_color,
);
let formatted_spans =
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
if is_last_line {
// Последняя строка — добавляем time_mark
@@ -605,17 +631,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
let padding = content_width.saturating_sub(full_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if is_selected {
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)));
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
}
line_spans.extend(formatted_spans);
line_spans.push(Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)));
line_spans.push(Span::styled(
format!(" {}", time_mark),
Style::default().fg(Color::Gray),
));
lines.push(Line::from(line_spans));
} else {
// Промежуточные строки — просто текст справа
let padding = content_width.saturating_sub(line_len + marker_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if i == 0 && is_selected {
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)));
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
}
line_spans.extend(formatted_spans);
lines.push(Line::from(line_spans));
@@ -643,19 +682,24 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
);
// Форматируем текст с entities
let formatted_spans = format_text_with_entities(
&wrapped.text,
&line_entities,
msg_color,
);
let formatted_spans =
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
if i == 0 {
// Первая строка — с временем и маркером выбора
let mut line_spans = vec![];
if is_selected {
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)));
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
}
line_spans.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
line_spans.push(Span::styled(
format!(" {}", time_str),
Style::default().fg(Color::Gray),
));
line_spans.push(Span::raw(" "));
line_spans.extend(formatted_spans);
lines.push(Line::from(line_spans));
@@ -694,9 +738,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
};
let style = if reaction.is_chosen {
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_chosen))
Style::default()
.fg(app.config.parse_color(&app.config.colors.reaction_chosen))
} else {
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_other))
Style::default()
.fg(app.config.parse_color(&app.config.colors.reaction_other))
};
reaction_spans.push(Span::styled(reaction_text, style));
@@ -723,10 +769,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
}
if lines.is_empty() {
lines.push(Line::from(Span::styled(
"Нет сообщений",
Style::default().fg(Color::Gray),
)));
lines.push(Line::from(Span::styled("Нет сообщений", Style::default().fg(Color::Gray))));
}
// Вычисляем скролл с учётом пользовательского offset
@@ -769,10 +812,15 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Input box с wrap для длинного текста и блочным курсором
let (input_line, input_title) = if app.is_forwarding() {
// Режим пересылки - показываем превью сообщения
let forward_preview = app.get_forwarding_message()
let forward_preview = app
.get_forwarding_message()
.map(|m| {
let text_preview: String = m.content.chars().take(40).collect();
let ellipsis = if m.content.chars().count() > 40 { "..." } else { "" };
let ellipsis = if m.content.chars().count() > 40 {
"..."
} else {
""
};
format!("{}{}", text_preview, ellipsis)
})
.unwrap_or_else(|| "↪ ...".to_string());
@@ -782,8 +830,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} else if app.is_selecting_message() {
// Режим выбора сообщения - подсказка зависит от возможностей
let selected_msg = app.get_selected_message();
let can_edit = selected_msg.map(|m| m.can_be_edited && m.is_outgoing).unwrap_or(false);
let can_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false);
let can_edit = selected_msg
.map(|m| m.can_be_edited && m.is_outgoing)
.unwrap_or(false);
let can_delete = selected_msg
.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users)
.unwrap_or(false);
let hint = match (can_edit, can_delete) {
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc",
@@ -791,7 +843,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
(false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc",
(false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc",
};
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
(
Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))),
" Выбор сообщения ",
)
} else if app.is_editing() {
// Режим редактирования
if app.message_input.is_empty() {
@@ -804,16 +859,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
(line, " Редактирование (Esc отмена) ")
} else {
// Текст с курсором
let line = render_input_with_cursor("", &app.message_input, app.cursor_position, Color::Magenta);
let line = render_input_with_cursor(
"",
&app.message_input,
app.cursor_position,
Color::Magenta,
);
(line, " Редактирование (Esc отмена) ")
}
} else if app.is_replying() {
// Режим ответа на сообщение
let reply_preview = app.get_replying_to_message()
let reply_preview = app
.get_replying_to_message()
.map(|m| {
let sender = if m.is_outgoing { "Вы" } else { &m.sender_name };
let sender = if m.is_outgoing {
"Вы"
} else {
&m.sender_name
};
let text_preview: String = m.content.chars().take(30).collect();
let ellipsis = if m.content.chars().count() > 30 { "..." } else { "" };
let ellipsis = if m.content.chars().count() > 30 {
"..."
} else {
""
};
format!("{}: {}{}", sender, text_preview, ellipsis)
})
.unwrap_or_else(|| "...".to_string());
@@ -829,7 +898,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
} else {
let short_preview: String = reply_preview.chars().take(15).collect();
let prefix = format!("{} > ", short_preview);
let line = render_input_with_cursor(&prefix, &app.message_input, app.cursor_position, Color::Yellow);
let line = render_input_with_cursor(
&prefix,
&app.message_input,
app.cursor_position,
Color::Yellow,
);
(line, " Ответ (Esc отмена) ")
}
} else {
@@ -844,7 +918,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
(line, "")
} else {
// Текст с курсором
let line = render_input_with_cursor("> ", &app.message_input, app.cursor_position, Color::Yellow);
let line = render_input_with_cursor(
"> ",
&app.message_input,
app.cursor_position,
Color::Yellow,
);
(line, "")
}
};
@@ -860,7 +939,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
Block::default()
.borders(Borders::ALL)
.title(input_title)
.title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD))
.title_style(
Style::default()
.fg(title_color)
.add_modifier(Modifier::BOLD),
)
};
let input = Paragraph::new(input_line)
@@ -882,7 +965,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Модалка выбора реакции
if app.is_reaction_picker_mode() {
render_reaction_picker_modal(f, area, &app.available_reactions, app.selected_reaction_index);
render_reaction_picker_modal(
f,
area,
&app.available_reactions,
app.selected_reaction_index,
);
}
}
@@ -899,8 +987,12 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
// Search input
let total = app.message_search_results.len();
let current = if total > 0 { app.selected_search_result_index + 1 } else { 0 };
let current = if total > 0 {
app.selected_search_result_index + 1
} else {
0
};
let input_line = if app.message_search_query.is_empty() {
Line::from(vec![
Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
@@ -915,15 +1007,18 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)),
])
};
let search_input = Paragraph::new(input_line)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.title(" Поиск по сообщениям ")
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
);
let search_input = Paragraph::new(input_line).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.title(" Поиск по сообщениям ")
.title_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
);
f.render_widget(search_input, chunks[0]);
// Search results
@@ -948,14 +1043,29 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
// Маркер выбора, имя и дата
let marker = if is_selected { "" } else { " " };
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan };
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() };
let sender_color = if msg.is_outgoing {
Color::Green
} else {
Color::Cyan
};
let sender_name = if msg.is_outgoing {
"Вы".to_string()
} else {
msg.sender_name.clone()
};
lines.push(Line::from(vec![
Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(
marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!("{} ", sender_name),
Style::default().fg(sender_color).add_modifier(Modifier::BOLD),
Style::default()
.fg(sender_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!("({})", crate::utils::format_datetime(msg.date)),
@@ -964,7 +1074,11 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
]));
// Текст сообщения (с переносом)
let msg_color = if is_selected { Color::Yellow } else { Color::White };
let msg_color = if is_selected {
Color::Yellow
} else {
Color::White
};
let max_width = content_width.saturating_sub(4);
let wrapped = wrap_text_with_offsets(&msg.content, max_width);
let wrapped_count = wrapped.len();
@@ -998,20 +1112,35 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.border_style(Style::default().fg(Color::Yellow)),
)
.scroll((scroll_offset, 0));
f.render_widget(results_widget, chunks[1]);
// Help bar
let help_line = Line::from(vec![
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(
" ↑↓ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("навигация"),
Span::raw(" "),
Span::styled(" n/N ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(
" n/N ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("след./пред."),
Span::raw(" "),
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(
" Enter ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("перейти"),
Span::raw(" "),
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -1021,7 +1150,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.border_style(Style::default().fg(Color::Yellow)),
)
.alignment(Alignment::Center);
f.render_widget(help, chunks[2]);
@@ -1046,9 +1175,13 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta))
.border_style(Style::default().fg(Color::Magenta)),
)
.style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD));
.style(
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
);
f.render_widget(header, chunks[0]);
// Pinned messages list
@@ -1057,7 +1190,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
for (idx, msg) in app.pinned_messages.iter().enumerate() {
let is_selected = idx == app.selected_pinned_index;
// Пустая строка между сообщениями
if idx > 0 {
lines.push(Line::from(""));
@@ -1065,14 +1198,29 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
// Маркер выбора и имя отправителя
let marker = if is_selected { "" } else { " " };
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan };
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() };
let sender_color = if msg.is_outgoing {
Color::Green
} else {
Color::Cyan
};
let sender_name = if msg.is_outgoing {
"Вы".to_string()
} else {
msg.sender_name.clone()
};
lines.push(Line::from(vec![
Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(
marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!("{} ", sender_name),
Style::default().fg(sender_color).add_modifier(Modifier::BOLD),
Style::default()
.fg(sender_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!("({})", crate::utils::format_datetime(msg.date)),
@@ -1081,12 +1229,17 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
]));
// Текст сообщения (с переносом)
let msg_color = if is_selected { Color::Yellow } else { Color::White };
let msg_color = if is_selected {
Color::Yellow
} else {
Color::White
};
let max_width = content_width.saturating_sub(4);
let wrapped = wrap_text_with_offsets(&msg.content, max_width);
let wrapped_count = wrapped.len();
for wrapped_line in wrapped.into_iter().take(3) { // Максимум 3 строки на сообщение
for wrapped_line in wrapped.into_iter().take(3) {
// Максимум 3 строки на сообщение
lines.push(Line::from(vec![
Span::raw(" "), // Отступ
Span::styled(wrapped_line.text, Style::default().fg(msg_color)),
@@ -1121,17 +1274,27 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta))
.border_style(Style::default().fg(Color::Magenta)),
)
.scroll((scroll_offset, 0));
f.render_widget(messages_widget, chunks[1]);
// Help bar
let help_line = Line::from(vec![
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
Span::styled(
" ↑↓ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw("навигация"),
Span::raw(" "),
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(
" Enter ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("перейти"),
Span::raw(" "),
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -1141,7 +1304,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta))
.border_style(Style::default().fg(Color::Magenta)),
)
.alignment(Alignment::Center);
f.render_widget(help, chunks[2]);
@@ -1169,11 +1332,18 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
Line::from(""),
Line::from(Span::styled(
"Удалить сообщение?",
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::styled(" [y/Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(
" [y/Enter] ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("Да"),
Span::raw(" "),
Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
@@ -1194,9 +1364,13 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
f.render_widget(modal, modal_area);
}
/// Рендерит модалку выбора реакции
fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) {
fn render_reaction_picker_modal(
f: &mut Frame,
area: Rect,
available_reactions: &[String],
selected_index: usize,
) {
use ratatui::widgets::Clear;
// Размеры модалки (зависят от количества реакций)
@@ -1248,9 +1422,19 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
// Добавляем пустую строку и подсказку
text_lines.push(Line::from(""));
text_lines.push(Line::from(vec![
Span::styled(" [←/→/↑/↓] ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::styled(
" [←/→/↑/↓] ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw("Выбор "),
Span::styled(" [Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
Span::styled(
" [Enter] ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw("Добавить "),
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw("Отмена"),
@@ -1262,7 +1446,11 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.title(" Выбери реакцию ")
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
.title_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
)
.alignment(Alignment::Left);