fixes
This commit is contained in:
@@ -75,8 +75,18 @@ pub struct App {
|
||||
pub leave_group_confirmation_step: u8,
|
||||
/// Информация профиля для отображения
|
||||
pub profile_info: Option<crate::tdlib::ProfileInfo>,
|
||||
// Reaction picker mode
|
||||
/// Режим выбора реакции
|
||||
pub is_reaction_picker_mode: bool,
|
||||
/// ID сообщения для добавления реакции
|
||||
pub selected_message_for_reaction: Option<i64>,
|
||||
/// Список доступных реакций
|
||||
pub available_reactions: Vec<String>,
|
||||
/// Индекс выбранной реакции в picker
|
||||
pub selected_reaction_index: usize,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
pub fn new() -> App {
|
||||
let mut state = ListState::default();
|
||||
@@ -119,6 +129,10 @@ impl App {
|
||||
selected_profile_action: 0,
|
||||
leave_group_confirmation_step: 0,
|
||||
profile_info: None,
|
||||
is_reaction_picker_mode: false,
|
||||
selected_message_for_reaction: None,
|
||||
available_reactions: Vec::new(),
|
||||
selected_reaction_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,4 +620,44 @@ impl App {
|
||||
pub fn get_leave_group_confirmation_step(&self) -> u8 {
|
||||
self.leave_group_confirmation_step
|
||||
}
|
||||
|
||||
// ========== Reaction Picker ==========
|
||||
|
||||
pub fn is_reaction_picker_mode(&self) -> bool {
|
||||
self.is_reaction_picker_mode
|
||||
}
|
||||
|
||||
pub fn enter_reaction_picker_mode(&mut self, message_id: i64, available_reactions: Vec<String>) {
|
||||
self.is_reaction_picker_mode = true;
|
||||
self.selected_message_for_reaction = Some(message_id);
|
||||
self.available_reactions = available_reactions;
|
||||
self.selected_reaction_index = 0;
|
||||
}
|
||||
|
||||
pub fn exit_reaction_picker_mode(&mut self) {
|
||||
self.is_reaction_picker_mode = false;
|
||||
self.selected_message_for_reaction = None;
|
||||
self.available_reactions.clear();
|
||||
self.selected_reaction_index = 0;
|
||||
}
|
||||
|
||||
pub fn select_previous_reaction(&mut self) {
|
||||
if !self.available_reactions.is_empty() && self.selected_reaction_index > 0 {
|
||||
self.selected_reaction_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next_reaction(&mut self) {
|
||||
if self.selected_reaction_index + 1 < self.available_reactions.len() {
|
||||
self.selected_reaction_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_reaction(&self) -> Option<&String> {
|
||||
self.available_reactions.get(self.selected_reaction_index)
|
||||
}
|
||||
|
||||
pub fn get_selected_message_for_reaction(&self) -> Option<i64> {
|
||||
self.selected_message_for_reaction
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,73 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка ввода в режиме выбора реакции
|
||||
if app.is_reaction_picker_mode() {
|
||||
match key.code {
|
||||
KeyCode::Left => {
|
||||
app.select_previous_reaction();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.select_next_reaction();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Переход на ряд выше (8 эмодзи в ряду)
|
||||
if app.selected_reaction_index >= 8 {
|
||||
app.selected_reaction_index = app.selected_reaction_index.saturating_sub(8);
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Переход на ряд ниже (8 эмодзи в ряду)
|
||||
let new_index = app.selected_reaction_index + 8;
|
||||
if new_index < app.available_reactions.len() {
|
||||
app.selected_reaction_index = new_index;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Добавить/убрать реакцию
|
||||
if let Some(emoji) = app.get_selected_reaction().cloned() {
|
||||
if let Some(message_id) = app.get_selected_message_for_reaction() {
|
||||
if let Some(chat_id) = app.selected_chat_id {
|
||||
app.status_message = Some("Отправка реакции...".to_string());
|
||||
app.needs_redraw = true;
|
||||
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
app.td_client.toggle_reaction(chat_id, message_id, emoji.clone())
|
||||
).await {
|
||||
Ok(Ok(_)) => {
|
||||
app.status_message = Some(format!("Реакция {} добавлена", emoji));
|
||||
app.exit_reaction_picker_mode();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
app.error_message = Some(format!("Ошибка: {}", e));
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
Err(_) => {
|
||||
app.error_message = Some("Таймаут отправки реакции".to_string());
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.exit_reaction_picker_mode();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Модалка подтверждения удаления
|
||||
if app.is_confirm_delete_shown() {
|
||||
match key.code {
|
||||
@@ -563,6 +630,58 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
// Начать режим пересылки
|
||||
app.start_forward_selected();
|
||||
}
|
||||
KeyCode::Char('y') | KeyCode::Char('н') => {
|
||||
// Копировать сообщение
|
||||
if let Some(msg) = app.get_selected_message() {
|
||||
let text = format_message_for_clipboard(msg);
|
||||
match copy_to_clipboard(&text) {
|
||||
Ok(_) => {
|
||||
app.status_message = Some("Сообщение скопировано".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(format!("Ошибка копирования: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') | KeyCode::Char('у') => {
|
||||
// Открыть emoji picker для добавления реакции
|
||||
if let Some(msg) = app.get_selected_message() {
|
||||
let chat_id = app.selected_chat_id.unwrap();
|
||||
let message_id = msg.id;
|
||||
|
||||
app.status_message = Some("Загрузка реакций...".to_string());
|
||||
app.needs_redraw = true;
|
||||
|
||||
// Запрашиваем доступные реакции
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
app.td_client.get_message_available_reactions(chat_id, message_id)
|
||||
).await {
|
||||
Ok(Ok(reactions)) => {
|
||||
if reactions.is_empty() {
|
||||
app.error_message = Some("Реакции недоступны для этого сообщения".to_string());
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
} else {
|
||||
app.enter_reaction_picker_mode(message_id, reactions);
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
app.error_message = Some(format!("Ошибка загрузки реакций: {}", e));
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
Err(_) => {
|
||||
app.error_message = Some("Таймаут загрузки реакций".to_string());
|
||||
app.status_message = None;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
@@ -756,3 +875,91 @@ fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// Копирует текст в системный буфер обмена
|
||||
fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||||
use arboard::Clipboard;
|
||||
|
||||
let mut clipboard = Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
|
||||
clipboard.set_text(text).map_err(|e| format!("Не удалось скопировать: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Форматирует сообщение для копирования с контекстом
|
||||
fn format_message_for_clipboard(msg: &crate::tdlib::client::MessageInfo) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
// Добавляем forward контекст если есть
|
||||
if let Some(forward) = &msg.forward_from {
|
||||
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
|
||||
}
|
||||
|
||||
// Добавляем reply контекст если есть
|
||||
if let Some(reply) = &msg.reply_to {
|
||||
result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text));
|
||||
}
|
||||
|
||||
// Добавляем основной текст с markdown форматированием
|
||||
result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities));
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Конвертирует текст с entities в markdown
|
||||
fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String {
|
||||
use tdlib_rs::enums::TextEntityType;
|
||||
|
||||
if entities.is_empty() {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
// Создаём вектор символов для работы с unicode
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut result = String::new();
|
||||
let mut i = 0;
|
||||
|
||||
while i < chars.len() {
|
||||
// Ищем entity, который начинается в текущей позиции
|
||||
let mut entity_found = false;
|
||||
|
||||
for entity in entities {
|
||||
if entity.offset as usize == i {
|
||||
entity_found = true;
|
||||
let end = (entity.offset + entity.length) as usize;
|
||||
let entity_text: String = chars[i..end.min(chars.len())].iter().collect();
|
||||
|
||||
// Применяем форматирование в зависимости от типа
|
||||
let formatted = match &entity.r#type {
|
||||
TextEntityType::Bold => format!("**{}**", entity_text),
|
||||
TextEntityType::Italic => format!("*{}*", entity_text),
|
||||
TextEntityType::Underline => format!("__{}__", entity_text),
|
||||
TextEntityType::Strikethrough => format!("~~{}~~", entity_text),
|
||||
TextEntityType::Code | TextEntityType::Pre | TextEntityType::PreCode(_) => {
|
||||
format!("`{}`", entity_text)
|
||||
}
|
||||
TextEntityType::TextUrl(url_info) => {
|
||||
format!("[{}]({})", entity_text, url_info.url)
|
||||
}
|
||||
TextEntityType::Url => format!("<{}>", entity_text),
|
||||
TextEntityType::Mention | TextEntityType::MentionName(_) => {
|
||||
format!("@{}", entity_text.trim_start_matches('@'))
|
||||
}
|
||||
TextEntityType::Spoiler => format!("||{}||", entity_text),
|
||||
_ => entity_text,
|
||||
};
|
||||
|
||||
result.push_str(&formatted);
|
||||
i = end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !entity_found {
|
||||
result.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -137,6 +137,17 @@ pub struct ForwardInfo {
|
||||
pub date: i32,
|
||||
}
|
||||
|
||||
/// Информация о реакции на сообщение
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReactionInfo {
|
||||
/// Эмодзи реакции (например, "👍")
|
||||
pub emoji: String,
|
||||
/// Количество людей, поставивших эту реакцию
|
||||
pub count: i32,
|
||||
/// Поставил ли текущий пользователь эту реакцию
|
||||
pub is_chosen: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageInfo {
|
||||
pub id: i64,
|
||||
@@ -159,6 +170,8 @@ pub struct MessageInfo {
|
||||
pub reply_to: Option<ReplyInfo>,
|
||||
/// Информация о forward (если сообщение переслано)
|
||||
pub forward_from: Option<ForwardInfo>,
|
||||
/// Реакции на сообщение
|
||||
pub reactions: Vec<ReactionInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -623,6 +636,37 @@ impl TdClient {
|
||||
});
|
||||
}
|
||||
}
|
||||
Update::MessageInteractionInfo(update) => {
|
||||
// Обновляем реакции в текущем открытом чате
|
||||
if Some(update.chat_id) == self.current_chat_id {
|
||||
if let Some(msg) = self.current_chat_messages.iter_mut().find(|m| m.id == update.message_id) {
|
||||
// Извлекаем реакции из interaction_info
|
||||
msg.reactions = update
|
||||
.interaction_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.reactions.as_ref())
|
||||
.map(|reactions| {
|
||||
reactions
|
||||
.reactions
|
||||
.iter()
|
||||
.filter_map(|reaction| {
|
||||
let emoji = match &reaction.r#type {
|
||||
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
|
||||
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None,
|
||||
};
|
||||
|
||||
Some(ReactionInfo {
|
||||
emoji,
|
||||
count: reaction.total_count,
|
||||
is_chosen: reaction.is_chosen,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -789,6 +833,9 @@ impl TdClient {
|
||||
// Извлекаем информацию о forward
|
||||
let forward_from = self.extract_forward_info(message);
|
||||
|
||||
// Извлекаем реакции
|
||||
let reactions = self.extract_reactions(message);
|
||||
|
||||
MessageInfo {
|
||||
id: message.id,
|
||||
sender_name,
|
||||
@@ -803,6 +850,7 @@ impl TdClient {
|
||||
can_be_deleted_for_all_users: message.can_be_deleted_for_all_users,
|
||||
reply_to,
|
||||
forward_from,
|
||||
reactions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,6 +907,34 @@ impl TdClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Извлекает информацию о реакциях из сообщения
|
||||
fn extract_reactions(&self, message: &TdMessage) -> Vec<ReactionInfo> {
|
||||
message
|
||||
.interaction_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.reactions.as_ref())
|
||||
.map(|reactions| {
|
||||
reactions
|
||||
.reactions
|
||||
.iter()
|
||||
.filter_map(|reaction| {
|
||||
// Извлекаем эмодзи из ReactionType
|
||||
let emoji = match &reaction.r#type {
|
||||
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
|
||||
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, // Пока игнорируем custom emoji
|
||||
};
|
||||
|
||||
Some(ReactionInfo {
|
||||
emoji,
|
||||
count: reaction.total_count,
|
||||
is_chosen: reaction.is_chosen,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Получает имя отправителя из MessageOrigin
|
||||
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
|
||||
use tdlib_rs::enums::MessageOrigin;
|
||||
@@ -1504,12 +1580,96 @@ impl TdClient {
|
||||
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
||||
reply_to: reply_info,
|
||||
forward_from: None,
|
||||
reactions: Vec::new(),
|
||||
})
|
||||
}
|
||||
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Получить доступные реакции для сообщения
|
||||
pub async fn get_message_available_reactions(
|
||||
&mut self,
|
||||
chat_id: i64,
|
||||
message_id: i64,
|
||||
) -> Result<Vec<String>, String> {
|
||||
use tdlib_rs::functions;
|
||||
|
||||
let result = functions::get_message_available_reactions(
|
||||
chat_id,
|
||||
message_id,
|
||||
8, // row_size - количество реакций в ряду
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(tdlib_rs::enums::AvailableReactions::AvailableReactions(reactions)) => {
|
||||
// Извлекаем эмодзи из доступных реакций
|
||||
// Используем top_reactions (самые популярные реакции)
|
||||
let mut emojis: Vec<String> = reactions
|
||||
.top_reactions
|
||||
.iter()
|
||||
.filter_map(|reaction| {
|
||||
if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type {
|
||||
Some(e.emoji.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Если top_reactions пустой, используем popular_reactions
|
||||
if emojis.is_empty() {
|
||||
emojis = reactions
|
||||
.popular_reactions
|
||||
.iter()
|
||||
.filter_map(|reaction| {
|
||||
if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type {
|
||||
Some(e.emoji.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok(emojis)
|
||||
}
|
||||
Err(e) => Err(format!("Ошибка получения реакций: {:?}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавить реакцию на сообщение (или убрать, если уже поставлена)
|
||||
pub async fn toggle_reaction(
|
||||
&mut self,
|
||||
chat_id: i64,
|
||||
message_id: i64,
|
||||
emoji: String,
|
||||
) -> Result<(), String> {
|
||||
use tdlib_rs::functions;
|
||||
use tdlib_rs::types::ReactionTypeEmoji;
|
||||
use tdlib_rs::enums::ReactionType;
|
||||
|
||||
let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji });
|
||||
|
||||
let result = functions::add_message_reaction(
|
||||
chat_id,
|
||||
message_id,
|
||||
reaction_type,
|
||||
false, // is_big - обычная реакция (не "большая" анимация)
|
||||
true, // update_recent_reactions - обновить список недавних реакций
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Ошибка добавления реакции: {:?}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Редактирование текстового сообщения с поддержкой Markdown
|
||||
/// Устанавливает черновик для чата через TDLib API
|
||||
pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> {
|
||||
@@ -1616,6 +1776,7 @@ impl TdClient {
|
||||
can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users,
|
||||
reply_to: None, // При редактировании reply сохраняется из оригинала
|
||||
forward_from: None, // При редактировании forward сохраняется из оригинала
|
||||
reactions: Vec::new(), // При редактировании реакции сохраняются из оригинала
|
||||
})
|
||||
}
|
||||
Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)),
|
||||
|
||||
@@ -668,6 +668,58 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Отображаем реакции под сообщением
|
||||
if !msg.reactions.is_empty() {
|
||||
let mut reaction_spans = vec![];
|
||||
|
||||
for reaction in &msg.reactions {
|
||||
if !reaction_spans.is_empty() {
|
||||
reaction_spans.push(Span::raw(" "));
|
||||
}
|
||||
|
||||
// Свои реакции в рамках [emoji], чужие просто emoji
|
||||
let reaction_text = if reaction.is_chosen {
|
||||
if reaction.count > 1 {
|
||||
format!("[{}] {}", reaction.emoji, reaction.count)
|
||||
} else {
|
||||
format!("[{}]", reaction.emoji)
|
||||
}
|
||||
} else {
|
||||
if reaction.count > 1 {
|
||||
format!("{} {}", reaction.emoji, reaction.count)
|
||||
} else {
|
||||
reaction.emoji.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let style = if reaction.is_chosen {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
};
|
||||
|
||||
reaction_spans.push(Span::styled(reaction_text, style));
|
||||
}
|
||||
|
||||
// Выравниваем реакции в зависимости от типа сообщения
|
||||
if msg.is_outgoing {
|
||||
// Реакции справа для исходящих
|
||||
let reactions_text: String = reaction_spans
|
||||
.iter()
|
||||
.map(|s| s.content.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let reactions_len = reactions_text.chars().count();
|
||||
let padding = content_width.saturating_sub(reactions_len + 1);
|
||||
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||
line_spans.extend(reaction_spans);
|
||||
lines.push(Line::from(line_spans));
|
||||
} else {
|
||||
// Реакции слева для входящих
|
||||
lines.push(Line::from(reaction_spans));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
@@ -734,10 +786,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
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 перслть · d удал. · Esc",
|
||||
(true, false) => "↑↓ · Enter ред. · r ответ · f переслть · Esc",
|
||||
(false, true) => "↑↓ · r ответ · f переслать · d удалить · Esc",
|
||||
(false, false) => "↑↓ · r ответить · f переслать · Esc",
|
||||
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc",
|
||||
(true, false) => "↑↓ · Enter ред. · r ответ · f переслть · y копир. · Esc",
|
||||
(false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc",
|
||||
(false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc",
|
||||
};
|
||||
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
|
||||
} else if app.is_editing() {
|
||||
@@ -827,6 +879,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
if app.is_confirm_delete_shown() {
|
||||
render_delete_confirm_modal(f, area);
|
||||
}
|
||||
|
||||
// Модалка выбора реакции
|
||||
if app.is_reaction_picker_mode() {
|
||||
render_reaction_picker_modal(f, area, &app.available_reactions, app.selected_reaction_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Рендерит режим поиска по сообщениям
|
||||
@@ -1136,3 +1193,78 @@ 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) {
|
||||
use ratatui::widgets::Clear;
|
||||
|
||||
// Размеры модалки (зависят от количества реакций)
|
||||
let emojis_per_row = 8;
|
||||
let rows = (available_reactions.len() + emojis_per_row - 1) / emojis_per_row;
|
||||
let modal_width = 50u16;
|
||||
let modal_height = (rows + 4) as u16; // +4 для заголовка, отступов и подсказки
|
||||
|
||||
// Центрируем модалку
|
||||
let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
|
||||
let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
|
||||
|
||||
let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height));
|
||||
|
||||
// Очищаем область под модалкой
|
||||
f.render_widget(Clear, modal_area);
|
||||
|
||||
// Формируем содержимое - сетка эмодзи
|
||||
let mut text_lines = vec![Line::from("")]; // Пустая строка сверху
|
||||
|
||||
for row in 0..rows {
|
||||
let mut row_spans = vec![Span::raw(" ")]; // Отступ слева
|
||||
|
||||
for col in 0..emojis_per_row {
|
||||
let idx = row * emojis_per_row + col;
|
||||
if idx >= available_reactions.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let emoji = &available_reactions[idx];
|
||||
let is_selected = idx == selected_index;
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
|
||||
row_spans.push(Span::styled(format!(" {} ", emoji), style));
|
||||
row_spans.push(Span::raw(" ")); // Пробел между эмодзи
|
||||
}
|
||||
|
||||
text_lines.push(Line::from(row_spans));
|
||||
}
|
||||
|
||||
// Добавляем пустую строку и подсказку
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from(vec![
|
||||
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::raw("Добавить "),
|
||||
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Отмена"),
|
||||
]));
|
||||
|
||||
let modal = Paragraph::new(text_lines)
|
||||
.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)),
|
||||
)
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
f.render_widget(modal, modal_area);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user