commit
This commit is contained in:
162
src/app/chat_state.rs
Normal file
162
src/app/chat_state.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// Chat state management - type-safe state machine for chat modes
|
||||||
|
|
||||||
|
use crate::tdlib::client::MessageInfo;
|
||||||
|
use crate::tdlib::ProfileInfo;
|
||||||
|
|
||||||
|
/// Состояния чата - взаимоисключающие режимы работы с чатом
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ChatState {
|
||||||
|
/// Обычный режим - просмотр сообщений, набор текста
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// Выбор сообщения для действия (edit/delete/reply/forward/reaction)
|
||||||
|
MessageSelection {
|
||||||
|
/// Индекс выбранного сообщения (снизу вверх, 0 = последнее)
|
||||||
|
selected_index: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Редактирование сообщения
|
||||||
|
Editing {
|
||||||
|
/// ID редактируемого сообщения
|
||||||
|
message_id: i64,
|
||||||
|
/// Индекс сообщения в списке
|
||||||
|
selected_index: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Ответ на сообщение (reply)
|
||||||
|
Reply {
|
||||||
|
/// ID сообщения, на которое отвечаем
|
||||||
|
message_id: i64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Пересылка сообщения (forward)
|
||||||
|
Forward {
|
||||||
|
/// ID сообщения для пересылки
|
||||||
|
message_id: i64,
|
||||||
|
/// Находимся в режиме выбора чата для пересылки
|
||||||
|
selecting_chat: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Подтверждение удаления сообщения
|
||||||
|
DeleteConfirmation {
|
||||||
|
/// ID сообщения для удаления
|
||||||
|
message_id: i64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Выбор реакции на сообщение
|
||||||
|
ReactionPicker {
|
||||||
|
/// ID сообщения для реакции
|
||||||
|
message_id: i64,
|
||||||
|
/// Список доступных реакций
|
||||||
|
available_reactions: Vec<String>,
|
||||||
|
/// Индекс выбранной реакции в picker
|
||||||
|
selected_index: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Просмотр профиля пользователя/чата
|
||||||
|
Profile {
|
||||||
|
/// Информация профиля
|
||||||
|
info: ProfileInfo,
|
||||||
|
/// Индекс выбранного действия
|
||||||
|
selected_action: usize,
|
||||||
|
/// Шаг подтверждения выхода из группы (0 = не показано, 1-2 = подтверждения)
|
||||||
|
leave_group_confirmation_step: u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Поиск по сообщениям в текущем чате
|
||||||
|
SearchInChat {
|
||||||
|
/// Поисковый запрос
|
||||||
|
query: String,
|
||||||
|
/// Результаты поиска
|
||||||
|
results: Vec<MessageInfo>,
|
||||||
|
/// Индекс выбранного результата
|
||||||
|
selected_index: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Просмотр закреплённых сообщений
|
||||||
|
PinnedMessages {
|
||||||
|
/// Список закреплённых сообщений
|
||||||
|
messages: Vec<MessageInfo>,
|
||||||
|
/// Индекс выбранного pinned сообщения
|
||||||
|
selected_index: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChatState {
|
||||||
|
fn default() -> Self {
|
||||||
|
ChatState::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatState {
|
||||||
|
/// Проверка: находимся в режиме выбора сообщения
|
||||||
|
pub fn is_message_selection(&self) -> bool {
|
||||||
|
matches!(self, ChatState::MessageSelection { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: находимся в режиме редактирования
|
||||||
|
pub fn is_editing(&self) -> bool {
|
||||||
|
matches!(self, ChatState::Editing { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: находимся в режиме ответа
|
||||||
|
pub fn is_reply(&self) -> bool {
|
||||||
|
matches!(self, ChatState::Reply { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: находимся в режиме пересылки
|
||||||
|
pub fn is_forward(&self) -> bool {
|
||||||
|
matches!(self, ChatState::Forward { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: показываем подтверждение удаления
|
||||||
|
pub fn is_delete_confirmation(&self) -> bool {
|
||||||
|
matches!(self, ChatState::DeleteConfirmation { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: показываем reaction picker
|
||||||
|
pub fn is_reaction_picker(&self) -> bool {
|
||||||
|
matches!(self, ChatState::ReactionPicker { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: показываем профиль
|
||||||
|
pub fn is_profile(&self) -> bool {
|
||||||
|
matches!(self, ChatState::Profile { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: находимся в режиме поиска по сообщениям
|
||||||
|
pub fn is_search_in_chat(&self) -> bool {
|
||||||
|
matches!(self, ChatState::SearchInChat { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: показываем pinned сообщения
|
||||||
|
pub fn is_pinned_mode(&self) -> bool {
|
||||||
|
matches!(self, ChatState::PinnedMessages { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверка: находимся в обычном режиме
|
||||||
|
pub fn is_normal(&self) -> bool {
|
||||||
|
matches!(self, ChatState::Normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает ID выбранного сообщения (если есть)
|
||||||
|
pub fn selected_message_id(&self) -> Option<i64> {
|
||||||
|
match self {
|
||||||
|
ChatState::Editing { message_id, .. } => Some(*message_id),
|
||||||
|
ChatState::Reply { message_id } => Some(*message_id),
|
||||||
|
ChatState::Forward { message_id, .. } => Some(*message_id),
|
||||||
|
ChatState::DeleteConfirmation { message_id } => Some(*message_id),
|
||||||
|
ChatState::ReactionPicker { message_id, .. } => Some(*message_id),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает индекс выбранного сообщения (если есть)
|
||||||
|
pub fn selected_message_index(&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
ChatState::MessageSelection { selected_index } => Some(*selected_index),
|
||||||
|
ChatState::Editing { selected_index, .. } => Some(*selected_index),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
430
src/app/mod.rs
430
src/app/mod.rs
@@ -1,5 +1,7 @@
|
|||||||
|
mod chat_state;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
|
pub use chat_state::ChatState;
|
||||||
pub use state::AppScreen;
|
pub use state::AppScreen;
|
||||||
|
|
||||||
use crate::tdlib::client::ChatInfo;
|
use crate::tdlib::client::ChatInfo;
|
||||||
@@ -10,6 +12,8 @@ pub struct App {
|
|||||||
pub config: crate::config::Config,
|
pub config: crate::config::Config,
|
||||||
pub screen: AppScreen,
|
pub screen: AppScreen,
|
||||||
pub td_client: TdClient,
|
pub td_client: TdClient,
|
||||||
|
/// Состояние чата - type-safe state machine (новое!)
|
||||||
|
pub chat_state: ChatState,
|
||||||
// Auth state
|
// Auth state
|
||||||
pub phone_input: String,
|
pub phone_input: String,
|
||||||
pub code_input: String,
|
pub code_input: String,
|
||||||
@@ -32,59 +36,9 @@ pub struct App {
|
|||||||
pub search_query: String,
|
pub search_query: String,
|
||||||
/// Флаг для оптимизации рендеринга - перерисовывать только при изменениях
|
/// Флаг для оптимизации рендеринга - перерисовывать только при изменениях
|
||||||
pub needs_redraw: bool,
|
pub needs_redraw: bool,
|
||||||
// Edit message state
|
|
||||||
/// ID сообщения, которое редактируется (None = режим отправки нового)
|
|
||||||
pub editing_message_id: Option<i64>,
|
|
||||||
/// Индекс выбранного сообщения для навигации (снизу вверх, 0 = последнее)
|
|
||||||
pub selected_message_index: Option<usize>,
|
|
||||||
// Delete confirmation
|
|
||||||
/// ID сообщения для подтверждения удаления (показывает модалку)
|
|
||||||
pub confirm_delete_message_id: Option<i64>,
|
|
||||||
// Reply state
|
|
||||||
/// ID сообщения, на которое отвечаем (None = обычная отправка)
|
|
||||||
pub replying_to_message_id: Option<i64>,
|
|
||||||
// Forward state
|
|
||||||
/// ID сообщения для пересылки
|
|
||||||
pub forwarding_message_id: Option<i64>,
|
|
||||||
/// Режим выбора чата для пересылки
|
|
||||||
pub is_selecting_forward_chat: bool,
|
|
||||||
// Typing indicator
|
// Typing indicator
|
||||||
/// Время последней отправки typing status (для throttling)
|
/// Время последней отправки typing status (для throttling)
|
||||||
pub last_typing_sent: Option<std::time::Instant>,
|
pub last_typing_sent: Option<std::time::Instant>,
|
||||||
// Pinned messages mode
|
|
||||||
/// Режим просмотра закреплённых сообщений
|
|
||||||
pub is_pinned_mode: bool,
|
|
||||||
/// Список закреплённых сообщений
|
|
||||||
pub pinned_messages: Vec<crate::tdlib::client::MessageInfo>,
|
|
||||||
/// Индекс выбранного pinned сообщения
|
|
||||||
pub selected_pinned_index: usize,
|
|
||||||
// Message search mode
|
|
||||||
/// Режим поиска по сообщениям
|
|
||||||
pub is_message_search_mode: bool,
|
|
||||||
/// Поисковый запрос
|
|
||||||
pub message_search_query: String,
|
|
||||||
/// Результаты поиска
|
|
||||||
pub message_search_results: Vec<crate::tdlib::client::MessageInfo>,
|
|
||||||
/// Индекс выбранного результата
|
|
||||||
pub selected_search_result_index: usize,
|
|
||||||
// Profile mode
|
|
||||||
/// Режим просмотра профиля
|
|
||||||
pub is_profile_mode: bool,
|
|
||||||
/// Индекс выбранного действия в профиле
|
|
||||||
pub selected_profile_action: usize,
|
|
||||||
/// Шаг подтверждения выхода из группы (0 = не показано, 1 = первое, 2 = второе)
|
|
||||||
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 {
|
impl App {
|
||||||
@@ -96,6 +50,7 @@ impl App {
|
|||||||
config,
|
config,
|
||||||
screen: AppScreen::Loading,
|
screen: AppScreen::Loading,
|
||||||
td_client: TdClient::new(),
|
td_client: TdClient::new(),
|
||||||
|
chat_state: ChatState::Normal,
|
||||||
phone_input: String::new(),
|
phone_input: String::new(),
|
||||||
code_input: String::new(),
|
code_input: String::new(),
|
||||||
password_input: String::new(),
|
password_input: String::new(),
|
||||||
@@ -112,28 +67,7 @@ impl App {
|
|||||||
is_searching: false,
|
is_searching: false,
|
||||||
search_query: String::new(),
|
search_query: String::new(),
|
||||||
needs_redraw: true,
|
needs_redraw: true,
|
||||||
editing_message_id: None,
|
|
||||||
selected_message_index: None,
|
|
||||||
confirm_delete_message_id: None,
|
|
||||||
replying_to_message_id: None,
|
|
||||||
forwarding_message_id: None,
|
|
||||||
is_selecting_forward_chat: false,
|
|
||||||
last_typing_sent: None,
|
last_typing_sent: None,
|
||||||
is_pinned_mode: false,
|
|
||||||
pinned_messages: Vec::new(),
|
|
||||||
selected_pinned_index: 0,
|
|
||||||
is_message_search_mode: false,
|
|
||||||
message_search_query: String::new(),
|
|
||||||
message_search_results: Vec::new(),
|
|
||||||
selected_search_result_index: 0,
|
|
||||||
is_profile_mode: false,
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,24 +121,14 @@ impl App {
|
|||||||
self.message_input.clear();
|
self.message_input.clear();
|
||||||
self.cursor_position = 0;
|
self.cursor_position = 0;
|
||||||
self.message_scroll_offset = 0;
|
self.message_scroll_offset = 0;
|
||||||
self.editing_message_id = None;
|
|
||||||
self.selected_message_index = None;
|
|
||||||
self.replying_to_message_id = None;
|
|
||||||
self.last_typing_sent = None;
|
self.last_typing_sent = None;
|
||||||
// Сбрасываем pinned режим
|
// Сбрасываем состояние чата в нормальный режим
|
||||||
self.is_pinned_mode = false;
|
self.chat_state = ChatState::Normal;
|
||||||
self.pinned_messages.clear();
|
|
||||||
self.selected_pinned_index = 0;
|
|
||||||
// Очищаем данные в TdClient
|
// Очищаем данные в TdClient
|
||||||
self.td_client.current_chat_id = None;
|
self.td_client.current_chat_id = None;
|
||||||
self.td_client.current_chat_messages.clear();
|
self.td_client.current_chat_messages.clear();
|
||||||
self.td_client.typing_status = None;
|
self.td_client.typing_status = None;
|
||||||
self.td_client.current_pinned_message = None;
|
self.td_client.current_pinned_message = None;
|
||||||
// Сбрасываем режим поиска
|
|
||||||
self.is_message_search_mode = false;
|
|
||||||
self.message_search_query.clear();
|
|
||||||
self.message_search_results.clear();
|
|
||||||
self.selected_search_result_index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Начать выбор сообщения для редактирования (при стрелке вверх в пустом инпуте)
|
/// Начать выбор сообщения для редактирования (при стрелке вверх в пустом инпуте)
|
||||||
@@ -213,7 +137,7 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Начинаем с последнего сообщения (индекс 0 = самое новое снизу)
|
// Начинаем с последнего сообщения (индекс 0 = самое новое снизу)
|
||||||
self.selected_message_index = Some(0);
|
self.chat_state = ChatState::MessageSelection { selected_index: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать предыдущее сообщение (вверх по списку = увеличить индекс)
|
/// Выбрать предыдущее сообщение (вверх по списку = увеличить индекс)
|
||||||
@@ -222,24 +146,25 @@ impl App {
|
|||||||
if total == 0 {
|
if total == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.selected_message_index = Some(
|
if let ChatState::MessageSelection { selected_index } = &mut self.chat_state {
|
||||||
self.selected_message_index
|
*selected_index = (*selected_index + 1).min(total - 1);
|
||||||
.map(|i| (i + 1).min(total - 1))
|
}
|
||||||
.unwrap_or(0),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать следующее сообщение (вниз по списку = уменьшить индекс)
|
/// Выбрать следующее сообщение (вниз по списку = уменьшить индекс)
|
||||||
pub fn select_next_message(&mut self) {
|
pub fn select_next_message(&mut self) {
|
||||||
self.selected_message_index = self
|
if let ChatState::MessageSelection { selected_index } = &mut self.chat_state {
|
||||||
.selected_message_index
|
if *selected_index > 0 {
|
||||||
.map(|i| if i > 0 { Some(i - 1) } else { None })
|
*selected_index -= 1;
|
||||||
.flatten();
|
} else {
|
||||||
|
self.chat_state = ChatState::Normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить выбранное сообщение
|
/// Получить выбранное сообщение
|
||||||
pub fn get_selected_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
pub fn get_selected_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||||
self.selected_message_index.and_then(|idx| {
|
self.chat_state.selected_message_index().and_then(|idx| {
|
||||||
let total = self.td_client.current_chat_messages.len();
|
let total = self.td_client.current_chat_messages.len();
|
||||||
if total == 0 || idx >= total {
|
if total == 0 || idx >= total {
|
||||||
return None;
|
return None;
|
||||||
@@ -251,21 +176,33 @@ impl App {
|
|||||||
|
|
||||||
/// Начать редактирование выбранного сообщения
|
/// Начать редактирование выбранного сообщения
|
||||||
pub fn start_editing_selected(&mut self) -> bool {
|
pub fn start_editing_selected(&mut self) -> bool {
|
||||||
|
// Получаем selected_index из текущего состояния
|
||||||
|
let selected_idx = match &self.chat_state {
|
||||||
|
ChatState::MessageSelection { selected_index } => Some(*selected_index),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if selected_idx.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Сначала извлекаем данные из сообщения
|
// Сначала извлекаем данные из сообщения
|
||||||
let msg_data = self.get_selected_message().and_then(|msg| {
|
let msg_data = self.get_selected_message().and_then(|msg| {
|
||||||
if msg.can_be_edited && msg.is_outgoing {
|
if msg.can_be_edited && msg.is_outgoing {
|
||||||
Some((msg.id, msg.content.clone()))
|
Some((msg.id, msg.content.clone(), selected_idx.unwrap()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Затем присваиваем
|
// Затем присваиваем
|
||||||
if let Some((id, content)) = msg_data {
|
if let Some((id, content, idx)) = msg_data {
|
||||||
self.editing_message_id = Some(id);
|
|
||||||
self.cursor_position = content.chars().count();
|
self.cursor_position = content.chars().count();
|
||||||
self.message_input = content;
|
self.message_input = content;
|
||||||
self.selected_message_index = None;
|
self.chat_state = ChatState::Editing {
|
||||||
|
message_id: id,
|
||||||
|
selected_index: idx,
|
||||||
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -273,20 +210,19 @@ impl App {
|
|||||||
|
|
||||||
/// Отменить редактирование
|
/// Отменить редактирование
|
||||||
pub fn cancel_editing(&mut self) {
|
pub fn cancel_editing(&mut self) {
|
||||||
self.editing_message_id = None;
|
self.chat_state = ChatState::Normal;
|
||||||
self.selected_message_index = None;
|
|
||||||
self.message_input.clear();
|
self.message_input.clear();
|
||||||
self.cursor_position = 0;
|
self.cursor_position = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверить, находимся ли в режиме редактирования
|
/// Проверить, находимся ли в режиме редактирования
|
||||||
pub fn is_editing(&self) -> bool {
|
pub fn is_editing(&self) -> bool {
|
||||||
self.editing_message_id.is_some()
|
self.chat_state.is_editing()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверить, находимся ли в режиме выбора сообщения
|
/// Проверить, находимся ли в режиме выбора сообщения
|
||||||
pub fn is_selecting_message(&self) -> bool {
|
pub fn is_selecting_message(&self) -> bool {
|
||||||
self.selected_message_index.is_some()
|
self.chat_state.is_message_selection()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected_chat_id(&self) -> Option<i64> {
|
pub fn get_selected_chat_id(&self) -> Option<i64> {
|
||||||
@@ -385,14 +321,15 @@ impl App {
|
|||||||
|
|
||||||
/// Проверить, показывается ли модалка подтверждения удаления
|
/// Проверить, показывается ли модалка подтверждения удаления
|
||||||
pub fn is_confirm_delete_shown(&self) -> bool {
|
pub fn is_confirm_delete_shown(&self) -> bool {
|
||||||
self.confirm_delete_message_id.is_some()
|
self.chat_state.is_delete_confirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Начать режим ответа на выбранное сообщение
|
/// Начать режим ответа на выбранное сообщение
|
||||||
pub fn start_reply_to_selected(&mut self) -> bool {
|
pub fn start_reply_to_selected(&mut self) -> bool {
|
||||||
if let Some(msg) = self.get_selected_message() {
|
if let Some(msg) = self.get_selected_message() {
|
||||||
self.replying_to_message_id = Some(msg.id);
|
self.chat_state = ChatState::Reply {
|
||||||
self.selected_message_index = None;
|
message_id: msg.id,
|
||||||
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -400,17 +337,17 @@ impl App {
|
|||||||
|
|
||||||
/// Отменить режим ответа
|
/// Отменить режим ответа
|
||||||
pub fn cancel_reply(&mut self) {
|
pub fn cancel_reply(&mut self) {
|
||||||
self.replying_to_message_id = None;
|
self.chat_state = ChatState::Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверить, находимся ли в режиме ответа
|
/// Проверить, находимся ли в режиме ответа
|
||||||
pub fn is_replying(&self) -> bool {
|
pub fn is_replying(&self) -> bool {
|
||||||
self.replying_to_message_id.is_some()
|
self.chat_state.is_reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить сообщение, на которое отвечаем
|
/// Получить сообщение, на которое отвечаем
|
||||||
pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||||
self.replying_to_message_id.and_then(|id| {
|
self.chat_state.selected_message_id().and_then(|id| {
|
||||||
self.td_client
|
self.td_client
|
||||||
.current_chat_messages
|
.current_chat_messages
|
||||||
.iter()
|
.iter()
|
||||||
@@ -421,9 +358,10 @@ impl App {
|
|||||||
/// Начать режим пересылки выбранного сообщения
|
/// Начать режим пересылки выбранного сообщения
|
||||||
pub fn start_forward_selected(&mut self) -> bool {
|
pub fn start_forward_selected(&mut self) -> bool {
|
||||||
if let Some(msg) = self.get_selected_message() {
|
if let Some(msg) = self.get_selected_message() {
|
||||||
self.forwarding_message_id = Some(msg.id);
|
self.chat_state = ChatState::Forward {
|
||||||
self.selected_message_index = None;
|
message_id: msg.id,
|
||||||
self.is_selecting_forward_chat = true;
|
selecting_chat: true,
|
||||||
|
};
|
||||||
// Сбрасываем выбор чата на первый
|
// Сбрасываем выбор чата на первый
|
||||||
self.chat_list_state.select(Some(0));
|
self.chat_list_state.select(Some(0));
|
||||||
return true;
|
return true;
|
||||||
@@ -433,18 +371,20 @@ impl App {
|
|||||||
|
|
||||||
/// Отменить режим пересылки
|
/// Отменить режим пересылки
|
||||||
pub fn cancel_forward(&mut self) {
|
pub fn cancel_forward(&mut self) {
|
||||||
self.forwarding_message_id = None;
|
self.chat_state = ChatState::Normal;
|
||||||
self.is_selecting_forward_chat = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверить, находимся ли в режиме выбора чата для пересылки
|
/// Проверить, находимся ли в режиме выбора чата для пересылки
|
||||||
pub fn is_forwarding(&self) -> bool {
|
pub fn is_forwarding(&self) -> bool {
|
||||||
self.is_selecting_forward_chat && self.forwarding_message_id.is_some()
|
self.chat_state.is_forward()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить сообщение для пересылки
|
/// Получить сообщение для пересылки
|
||||||
pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||||
self.forwarding_message_id.and_then(|id| {
|
if !self.chat_state.is_forward() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.chat_state.selected_message_id().and_then(|id| {
|
||||||
self.td_client
|
self.td_client
|
||||||
.current_chat_messages
|
.current_chat_messages
|
||||||
.iter()
|
.iter()
|
||||||
@@ -456,44 +396,57 @@ impl App {
|
|||||||
|
|
||||||
/// Проверка режима pinned
|
/// Проверка режима pinned
|
||||||
pub fn is_pinned_mode(&self) -> bool {
|
pub fn is_pinned_mode(&self) -> bool {
|
||||||
self.is_pinned_mode
|
self.chat_state.is_pinned_mode()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Войти в режим pinned (вызывается после загрузки pinned сообщений)
|
/// Войти в режим pinned (вызывается после загрузки pinned сообщений)
|
||||||
pub fn enter_pinned_mode(&mut self, messages: Vec<crate::tdlib::client::MessageInfo>) {
|
pub fn enter_pinned_mode(&mut self, messages: Vec<crate::tdlib::client::MessageInfo>) {
|
||||||
if !messages.is_empty() {
|
if !messages.is_empty() {
|
||||||
self.pinned_messages = messages;
|
self.chat_state = ChatState::PinnedMessages {
|
||||||
self.selected_pinned_index = 0;
|
messages,
|
||||||
self.is_pinned_mode = true;
|
selected_index: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выйти из режима pinned
|
/// Выйти из режима pinned
|
||||||
pub fn exit_pinned_mode(&mut self) {
|
pub fn exit_pinned_mode(&mut self) {
|
||||||
self.is_pinned_mode = false;
|
self.chat_state = ChatState::Normal;
|
||||||
self.pinned_messages.clear();
|
|
||||||
self.selected_pinned_index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать предыдущий pinned (вверх = более старый)
|
/// Выбрать предыдущий pinned (вверх = более старый)
|
||||||
pub fn select_previous_pinned(&mut self) {
|
pub fn select_previous_pinned(&mut self) {
|
||||||
if !self.pinned_messages.is_empty()
|
if let ChatState::PinnedMessages {
|
||||||
&& self.selected_pinned_index < self.pinned_messages.len() - 1
|
selected_index,
|
||||||
|
messages,
|
||||||
|
} = &mut self.chat_state
|
||||||
{
|
{
|
||||||
self.selected_pinned_index += 1;
|
if *selected_index + 1 < messages.len() {
|
||||||
|
*selected_index += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать следующий pinned (вниз = более новый)
|
/// Выбрать следующий pinned (вниз = более новый)
|
||||||
pub fn select_next_pinned(&mut self) {
|
pub fn select_next_pinned(&mut self) {
|
||||||
if self.selected_pinned_index > 0 {
|
if let ChatState::PinnedMessages { selected_index, .. } = &mut self.chat_state {
|
||||||
self.selected_pinned_index -= 1;
|
if *selected_index > 0 {
|
||||||
|
*selected_index -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить текущее выбранное pinned сообщение
|
/// Получить текущее выбранное pinned сообщение
|
||||||
pub fn get_selected_pinned(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
pub fn get_selected_pinned(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||||
self.pinned_messages.get(self.selected_pinned_index)
|
if let ChatState::PinnedMessages {
|
||||||
|
messages,
|
||||||
|
selected_index,
|
||||||
|
} = &self.chat_state
|
||||||
|
{
|
||||||
|
messages.get(*selected_index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить ID текущего pinned для перехода в историю
|
/// Получить ID текущего pinned для перехода в историю
|
||||||
@@ -505,51 +458,66 @@ impl App {
|
|||||||
|
|
||||||
/// Проверить, активен ли режим поиска по сообщениям
|
/// Проверить, активен ли режим поиска по сообщениям
|
||||||
pub fn is_message_search_mode(&self) -> bool {
|
pub fn is_message_search_mode(&self) -> bool {
|
||||||
self.is_message_search_mode
|
self.chat_state.is_search_in_chat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Войти в режим поиска по сообщениям
|
/// Войти в режим поиска по сообщениям
|
||||||
pub fn enter_message_search_mode(&mut self) {
|
pub fn enter_message_search_mode(&mut self) {
|
||||||
self.is_message_search_mode = true;
|
self.chat_state = ChatState::SearchInChat {
|
||||||
self.message_search_query.clear();
|
query: String::new(),
|
||||||
self.message_search_results.clear();
|
results: Vec::new(),
|
||||||
self.selected_search_result_index = 0;
|
selected_index: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выйти из режима поиска
|
/// Выйти из режима поиска
|
||||||
pub fn exit_message_search_mode(&mut self) {
|
pub fn exit_message_search_mode(&mut self) {
|
||||||
self.is_message_search_mode = false;
|
self.chat_state = ChatState::Normal;
|
||||||
self.message_search_query.clear();
|
|
||||||
self.message_search_results.clear();
|
|
||||||
self.selected_search_result_index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Установить результаты поиска
|
/// Установить результаты поиска
|
||||||
pub fn set_search_results(&mut self, results: Vec<crate::tdlib::client::MessageInfo>) {
|
pub fn set_search_results(&mut self, results: Vec<crate::tdlib::client::MessageInfo>) {
|
||||||
self.message_search_results = results;
|
if let ChatState::SearchInChat { results: r, selected_index, .. } = &mut self.chat_state {
|
||||||
self.selected_search_result_index = 0;
|
*r = results;
|
||||||
|
*selected_index = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать предыдущий результат (вверх)
|
/// Выбрать предыдущий результат (вверх)
|
||||||
pub fn select_previous_search_result(&mut self) {
|
pub fn select_previous_search_result(&mut self) {
|
||||||
if self.selected_search_result_index > 0 {
|
if let ChatState::SearchInChat { selected_index, .. } = &mut self.chat_state {
|
||||||
self.selected_search_result_index -= 1;
|
if *selected_index > 0 {
|
||||||
|
*selected_index -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать следующий результат (вниз)
|
/// Выбрать следующий результат (вниз)
|
||||||
pub fn select_next_search_result(&mut self) {
|
pub fn select_next_search_result(&mut self) {
|
||||||
if !self.message_search_results.is_empty()
|
if let ChatState::SearchInChat {
|
||||||
&& self.selected_search_result_index < self.message_search_results.len() - 1
|
selected_index,
|
||||||
|
results,
|
||||||
|
..
|
||||||
|
} = &mut self.chat_state
|
||||||
{
|
{
|
||||||
self.selected_search_result_index += 1;
|
if *selected_index + 1 < results.len() {
|
||||||
|
*selected_index += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить текущий выбранный результат
|
/// Получить текущий выбранный результат
|
||||||
pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||||
self.message_search_results
|
if let ChatState::SearchInChat {
|
||||||
.get(self.selected_search_result_index)
|
results,
|
||||||
|
selected_index,
|
||||||
|
..
|
||||||
|
} = &self.chat_state
|
||||||
|
{
|
||||||
|
results.get(*selected_index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить ID выбранного результата для перехода
|
/// Получить ID выбранного результата для перехода
|
||||||
@@ -557,6 +525,40 @@ impl App {
|
|||||||
self.get_selected_search_result().map(|m| m.id)
|
self.get_selected_search_result().map(|m| m.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Получить поисковый запрос из режима поиска
|
||||||
|
pub fn get_search_query(&self) -> Option<&str> {
|
||||||
|
if let ChatState::SearchInChat { query, .. } = &self.chat_state {
|
||||||
|
Some(query.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обновить поисковый запрос
|
||||||
|
pub fn update_search_query(&mut self, new_query: String) {
|
||||||
|
if let ChatState::SearchInChat { query, .. } = &mut self.chat_state {
|
||||||
|
*query = new_query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить индекс выбранного результата поиска
|
||||||
|
pub fn get_search_selected_index(&self) -> Option<usize> {
|
||||||
|
if let ChatState::SearchInChat { selected_index, .. } = &self.chat_state {
|
||||||
|
Some(*selected_index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить результаты поиска
|
||||||
|
pub fn get_search_results(&self) -> Option<&[crate::tdlib::client::MessageInfo]> {
|
||||||
|
if let ChatState::SearchInChat { results, .. } = &self.chat_state {
|
||||||
|
Some(results.as_slice())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Draft Management ===
|
// === Draft Management ===
|
||||||
|
|
||||||
/// Получить черновик для текущего чата
|
/// Получить черновик для текущего чата
|
||||||
@@ -581,62 +583,118 @@ impl App {
|
|||||||
|
|
||||||
/// Проверить, активен ли режим профиля
|
/// Проверить, активен ли режим профиля
|
||||||
pub fn is_profile_mode(&self) -> bool {
|
pub fn is_profile_mode(&self) -> bool {
|
||||||
self.is_profile_mode
|
self.chat_state.is_profile()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Войти в режим профиля
|
/// Войти в режим профиля
|
||||||
pub fn enter_profile_mode(&mut self) {
|
pub fn enter_profile_mode(&mut self, info: crate::tdlib::ProfileInfo) {
|
||||||
self.is_profile_mode = true;
|
self.chat_state = ChatState::Profile {
|
||||||
self.selected_profile_action = 0;
|
info,
|
||||||
self.leave_group_confirmation_step = 0;
|
selected_action: 0,
|
||||||
|
leave_group_confirmation_step: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выйти из режима профиля
|
/// Выйти из режима профиля
|
||||||
pub fn exit_profile_mode(&mut self) {
|
pub fn exit_profile_mode(&mut self) {
|
||||||
self.is_profile_mode = false;
|
self.chat_state = ChatState::Normal;
|
||||||
self.selected_profile_action = 0;
|
|
||||||
self.leave_group_confirmation_step = 0;
|
|
||||||
self.profile_info = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать предыдущее действие
|
/// Выбрать предыдущее действие
|
||||||
pub fn select_previous_profile_action(&mut self) {
|
pub fn select_previous_profile_action(&mut self) {
|
||||||
if self.selected_profile_action > 0 {
|
if let ChatState::Profile {
|
||||||
self.selected_profile_action -= 1;
|
selected_action, ..
|
||||||
|
} = &mut self.chat_state
|
||||||
|
{
|
||||||
|
if *selected_action > 0 {
|
||||||
|
*selected_action -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выбрать следующее действие
|
/// Выбрать следующее действие
|
||||||
pub fn select_next_profile_action(&mut self, max_actions: usize) {
|
pub fn select_next_profile_action(&mut self, max_actions: usize) {
|
||||||
if self.selected_profile_action < max_actions.saturating_sub(1) {
|
if let ChatState::Profile {
|
||||||
self.selected_profile_action += 1;
|
selected_action, ..
|
||||||
|
} = &mut self.chat_state
|
||||||
|
{
|
||||||
|
if *selected_action < max_actions.saturating_sub(1) {
|
||||||
|
*selected_action += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Показать первое подтверждение выхода из группы
|
/// Показать первое подтверждение выхода из группы
|
||||||
pub fn show_leave_group_confirmation(&mut self) {
|
pub fn show_leave_group_confirmation(&mut self) {
|
||||||
self.leave_group_confirmation_step = 1;
|
if let ChatState::Profile {
|
||||||
|
leave_group_confirmation_step,
|
||||||
|
..
|
||||||
|
} = &mut self.chat_state
|
||||||
|
{
|
||||||
|
*leave_group_confirmation_step = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Показать второе подтверждение выхода из группы
|
/// Показать второе подтверждение выхода из группы
|
||||||
pub fn show_leave_group_final_confirmation(&mut self) {
|
pub fn show_leave_group_final_confirmation(&mut self) {
|
||||||
self.leave_group_confirmation_step = 2;
|
if let ChatState::Profile {
|
||||||
|
leave_group_confirmation_step,
|
||||||
|
..
|
||||||
|
} = &mut self.chat_state
|
||||||
|
{
|
||||||
|
*leave_group_confirmation_step = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Отменить подтверждение выхода из группы
|
/// Отменить подтверждение выхода из группы
|
||||||
pub fn cancel_leave_group(&mut self) {
|
pub fn cancel_leave_group(&mut self) {
|
||||||
self.leave_group_confirmation_step = 0;
|
if let ChatState::Profile {
|
||||||
|
leave_group_confirmation_step,
|
||||||
|
..
|
||||||
|
} = &mut self.chat_state
|
||||||
|
{
|
||||||
|
*leave_group_confirmation_step = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Получить текущий шаг подтверждения
|
/// Получить текущий шаг подтверждения
|
||||||
pub fn get_leave_group_confirmation_step(&self) -> u8 {
|
pub fn get_leave_group_confirmation_step(&self) -> u8 {
|
||||||
self.leave_group_confirmation_step
|
if let ChatState::Profile {
|
||||||
|
leave_group_confirmation_step,
|
||||||
|
..
|
||||||
|
} = &self.chat_state
|
||||||
|
{
|
||||||
|
*leave_group_confirmation_step
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить информацию профиля
|
||||||
|
pub fn get_profile_info(&self) -> Option<&crate::tdlib::ProfileInfo> {
|
||||||
|
if let ChatState::Profile { info, .. } = &self.chat_state {
|
||||||
|
Some(info)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Получить индекс выбранного действия в профиле
|
||||||
|
pub fn get_selected_profile_action(&self) -> Option<usize> {
|
||||||
|
if let ChatState::Profile {
|
||||||
|
selected_action, ..
|
||||||
|
} = &self.chat_state
|
||||||
|
{
|
||||||
|
Some(*selected_action)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Reaction Picker ==========
|
// ========== Reaction Picker ==========
|
||||||
|
|
||||||
pub fn is_reaction_picker_mode(&self) -> bool {
|
pub fn is_reaction_picker_mode(&self) -> bool {
|
||||||
self.is_reaction_picker_mode
|
self.chat_state.is_reaction_picker()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enter_reaction_picker_mode(
|
pub fn enter_reaction_picker_mode(
|
||||||
@@ -644,36 +702,52 @@ impl App {
|
|||||||
message_id: i64,
|
message_id: i64,
|
||||||
available_reactions: Vec<String>,
|
available_reactions: Vec<String>,
|
||||||
) {
|
) {
|
||||||
self.is_reaction_picker_mode = true;
|
self.chat_state = ChatState::ReactionPicker {
|
||||||
self.selected_message_for_reaction = Some(message_id);
|
message_id,
|
||||||
self.available_reactions = available_reactions;
|
available_reactions,
|
||||||
self.selected_reaction_index = 0;
|
selected_index: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exit_reaction_picker_mode(&mut self) {
|
pub fn exit_reaction_picker_mode(&mut self) {
|
||||||
self.is_reaction_picker_mode = false;
|
self.chat_state = ChatState::Normal;
|
||||||
self.selected_message_for_reaction = None;
|
|
||||||
self.available_reactions.clear();
|
|
||||||
self.selected_reaction_index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_previous_reaction(&mut self) {
|
pub fn select_previous_reaction(&mut self) {
|
||||||
if !self.available_reactions.is_empty() && self.selected_reaction_index > 0 {
|
if let ChatState::ReactionPicker { selected_index, .. } = &mut self.chat_state {
|
||||||
self.selected_reaction_index -= 1;
|
if *selected_index > 0 {
|
||||||
|
*selected_index -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_next_reaction(&mut self) {
|
pub fn select_next_reaction(&mut self) {
|
||||||
if self.selected_reaction_index + 1 < self.available_reactions.len() {
|
if let ChatState::ReactionPicker {
|
||||||
self.selected_reaction_index += 1;
|
selected_index,
|
||||||
|
available_reactions,
|
||||||
|
..
|
||||||
|
} = &mut self.chat_state
|
||||||
|
{
|
||||||
|
if *selected_index + 1 < available_reactions.len() {
|
||||||
|
*selected_index += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected_reaction(&self) -> Option<&String> {
|
pub fn get_selected_reaction(&self) -> Option<&String> {
|
||||||
self.available_reactions.get(self.selected_reaction_index)
|
if let ChatState::ReactionPicker {
|
||||||
|
available_reactions,
|
||||||
|
selected_index,
|
||||||
|
..
|
||||||
|
} = &self.chat_state
|
||||||
|
{
|
||||||
|
available_reactions.get(*selected_index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected_message_for_reaction(&self) -> Option<i64> {
|
pub fn get_selected_message_for_reaction(&self) -> Option<i64> {
|
||||||
self.selected_message_for_reaction
|
self.chat_state.selected_message_id()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,16 +114,16 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
app.select_previous_profile_action();
|
app.select_previous_profile_action();
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
if let Some(profile) = &app.profile_info {
|
if let Some(profile) = app.get_profile_info() {
|
||||||
let max_actions = get_available_actions_count(profile);
|
let max_actions = get_available_actions_count(profile);
|
||||||
app.select_next_profile_action(max_actions);
|
app.select_next_profile_action(max_actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// Выполнить выбранное действие
|
// Выполнить выбранное действие
|
||||||
if let Some(profile) = &app.profile_info {
|
if let Some(profile) = app.get_profile_info() {
|
||||||
let actions = get_available_actions_count(profile);
|
let actions = get_available_actions_count(profile);
|
||||||
let action_index = app.selected_profile_action;
|
let action_index = app.get_selected_profile_action().unwrap_or(0);
|
||||||
|
|
||||||
if action_index < actions {
|
if action_index < actions {
|
||||||
// Определяем какое действие выбрано
|
// Определяем какое действие выбрано
|
||||||
@@ -201,14 +201,16 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
app.message_search_query.pop();
|
// Удаляем символ из запроса
|
||||||
|
if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) {
|
||||||
|
query.pop();
|
||||||
|
app.update_search_query(query.clone());
|
||||||
// Выполняем поиск при изменении запроса
|
// Выполняем поиск при изменении запроса
|
||||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
if !app.message_search_query.is_empty() {
|
if !query.is_empty() {
|
||||||
if let Ok(Ok(results)) = timeout(
|
if let Ok(Ok(results)) = timeout(
|
||||||
Duration::from_secs(3),
|
Duration::from_secs(3),
|
||||||
app.td_client
|
app.td_client.search_messages(chat_id, &query),
|
||||||
.search_messages(chat_id, &app.message_search_query),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -219,14 +221,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
app.message_search_query.push(c);
|
// Добавляем символ к запросу
|
||||||
|
if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) {
|
||||||
|
query.push(c);
|
||||||
|
app.update_search_query(query.clone());
|
||||||
// Выполняем поиск при изменении запроса
|
// Выполняем поиск при изменении запроса
|
||||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
if let Ok(Ok(results)) = timeout(
|
if let Ok(Ok(results)) = timeout(
|
||||||
Duration::from_secs(3),
|
Duration::from_secs(3),
|
||||||
app.td_client
|
app.td_client.search_messages(chat_id, &query),
|
||||||
.search_messages(chat_id, &app.message_search_query),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -234,6 +239,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -287,19 +293,32 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
// Переход на ряд выше (8 эмодзи в ряду)
|
// Переход на ряд выше (8 эмодзи в ряду)
|
||||||
if app.selected_reaction_index >= 8 {
|
if let crate::app::ChatState::ReactionPicker {
|
||||||
app.selected_reaction_index = app.selected_reaction_index.saturating_sub(8);
|
selected_index,
|
||||||
|
..
|
||||||
|
} = &mut app.chat_state
|
||||||
|
{
|
||||||
|
if *selected_index >= 8 {
|
||||||
|
*selected_index = selected_index.saturating_sub(8);
|
||||||
app.needs_redraw = true;
|
app.needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
// Переход на ряд ниже (8 эмодзи в ряду)
|
// Переход на ряд ниже (8 эмодзи в ряду)
|
||||||
let new_index = app.selected_reaction_index + 8;
|
if let crate::app::ChatState::ReactionPicker {
|
||||||
if new_index < app.available_reactions.len() {
|
selected_index,
|
||||||
app.selected_reaction_index = new_index;
|
available_reactions,
|
||||||
|
..
|
||||||
|
} = &mut app.chat_state
|
||||||
|
{
|
||||||
|
let new_index = *selected_index + 8;
|
||||||
|
if new_index < available_reactions.len() {
|
||||||
|
*selected_index = new_index;
|
||||||
app.needs_redraw = true;
|
app.needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// Добавить/убрать реакцию
|
// Добавить/убрать реакцию
|
||||||
if let Some(emoji) = app.get_selected_reaction().cloned() {
|
if let Some(emoji) = app.get_selected_reaction().cloned() {
|
||||||
@@ -351,7 +370,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => {
|
KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => {
|
||||||
// Подтверждение удаления
|
// Подтверждение удаления
|
||||||
if let Some(msg_id) = app.confirm_delete_message_id {
|
if let Some(msg_id) = app.chat_state.selected_message_id() {
|
||||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
// Находим сообщение для проверки can_be_deleted_for_all_users
|
// Находим сообщение для проверки can_be_deleted_for_all_users
|
||||||
let can_delete_for_all = app
|
let can_delete_for_all = app
|
||||||
@@ -377,7 +396,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
app.td_client
|
app.td_client
|
||||||
.current_chat_messages
|
.current_chat_messages
|
||||||
.retain(|m| m.id != msg_id);
|
.retain(|m| m.id != msg_id);
|
||||||
app.selected_message_index = None;
|
// Сбрасываем состояние
|
||||||
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
app.error_message = Some(e);
|
app.error_message = Some(e);
|
||||||
@@ -388,11 +408,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.confirm_delete_message_id = None;
|
// Закрываем модалку
|
||||||
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => {
|
KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => {
|
||||||
// Отмена удаления
|
// Отмена удаления
|
||||||
app.confirm_delete_message_id = None;
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -411,7 +432,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
if let Some(i) = app.chat_list_state.selected() {
|
if let Some(i) = app.chat_list_state.selected() {
|
||||||
if let Some(chat) = filtered.get(i) {
|
if let Some(chat) = filtered.get(i) {
|
||||||
let to_chat_id = chat.id;
|
let to_chat_id = chat.id;
|
||||||
if let Some(msg_id) = app.forwarding_message_id {
|
if let Some(msg_id) = app.chat_state.selected_message_id() {
|
||||||
if let Some(from_chat_id) = app.get_selected_chat_id() {
|
if let Some(from_chat_id) = app.get_selected_chat_id() {
|
||||||
match timeout(
|
match timeout(
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
@@ -528,7 +549,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
// Редактирование начато
|
// Редактирование начато
|
||||||
} else {
|
} else {
|
||||||
// Нельзя редактировать это сообщение
|
// Нельзя редактировать это сообщение
|
||||||
app.selected_message_index = None;
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -538,11 +559,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
let text = app.message_input.clone();
|
let text = app.message_input.clone();
|
||||||
|
|
||||||
if let Some(msg_id) = app.editing_message_id {
|
if let Some(msg_id) = app.chat_state.selected_message_id() {
|
||||||
|
if app.is_editing() {
|
||||||
// Режим редактирования
|
// Режим редактирования
|
||||||
app.message_input.clear();
|
app.message_input.clear();
|
||||||
app.cursor_position = 0;
|
app.cursor_position = 0;
|
||||||
app.editing_message_id = None;
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
|
|
||||||
match timeout(
|
match timeout(
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
@@ -570,9 +592,14 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
app.error_message = Some("Таймаут редактирования".to_string());
|
app.error_message = Some("Таймаут редактирования".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Обычная отправка (или reply)
|
// Обычная отправка (или reply)
|
||||||
let reply_to_id = app.replying_to_message_id;
|
let reply_to_id = if app.is_replying() {
|
||||||
|
app.chat_state.selected_message_id()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
// Создаём ReplyInfo ДО отправки, пока сообщение точно доступно
|
// Создаём ReplyInfo ДО отправки, пока сообщение точно доступно
|
||||||
let reply_info = app.get_replying_to_message().map(|m| {
|
let reply_info = app.get_replying_to_message().map(|m| {
|
||||||
crate::tdlib::client::ReplyInfo {
|
crate::tdlib::client::ReplyInfo {
|
||||||
@@ -583,7 +610,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
});
|
});
|
||||||
app.message_input.clear();
|
app.message_input.clear();
|
||||||
app.cursor_position = 0;
|
app.cursor_position = 0;
|
||||||
app.replying_to_message_id = None;
|
// Сбрасываем режим reply если он был активен
|
||||||
|
if app.is_replying() {
|
||||||
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
|
}
|
||||||
app.last_typing_sent = None;
|
app.last_typing_sent = None;
|
||||||
|
|
||||||
// Отменяем typing status
|
// Отменяем typing status
|
||||||
@@ -665,7 +695,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
if key.code == KeyCode::Esc {
|
if key.code == KeyCode::Esc {
|
||||||
if app.is_selecting_message() {
|
if app.is_selecting_message() {
|
||||||
// Отменить выбор сообщения
|
// Отменить выбор сообщения
|
||||||
app.selected_message_index = None;
|
app.chat_state = crate::app::ChatState::Normal;
|
||||||
} else if app.is_editing() {
|
} else if app.is_editing() {
|
||||||
// Отменить редактирование
|
// Отменить редактирование
|
||||||
app.cancel_editing();
|
app.cancel_editing();
|
||||||
@@ -709,7 +739,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
let can_delete =
|
let can_delete =
|
||||||
msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users;
|
msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users;
|
||||||
if can_delete {
|
if can_delete {
|
||||||
app.confirm_delete_message_id = Some(msg.id);
|
app.chat_state = crate::app::ChatState::DeleteConfirmation {
|
||||||
|
message_id: msg.id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -789,8 +821,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await
|
match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await
|
||||||
{
|
{
|
||||||
Ok(Ok(profile)) => {
|
Ok(Ok(profile)) => {
|
||||||
app.profile_info = Some(profile);
|
app.enter_profile_mode(profile);
|
||||||
app.enter_profile_mode();
|
|
||||||
app.status_message = None;
|
app.status_message = None;
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ fn adjust_entities_for_substring(
|
|||||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||||
// Режим профиля
|
// Режим профиля
|
||||||
if app.is_profile_mode() {
|
if app.is_profile_mode() {
|
||||||
if let Some(profile) = &app.profile_info {
|
if let Some(profile) = app.get_profile_info() {
|
||||||
crate::ui::profile::render(f, area, app, profile);
|
crate::ui::profile::render(f, area, app, profile);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -964,18 +964,31 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Модалка выбора реакции
|
// Модалка выбора реакции
|
||||||
if app.is_reaction_picker_mode() {
|
if let crate::app::ChatState::ReactionPicker {
|
||||||
render_reaction_picker_modal(
|
available_reactions,
|
||||||
f,
|
selected_index,
|
||||||
area,
|
..
|
||||||
&app.available_reactions,
|
} = &app.chat_state
|
||||||
app.selected_reaction_index,
|
{
|
||||||
);
|
render_reaction_picker_modal(f, area, available_reactions, *selected_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Рендерит режим поиска по сообщениям
|
/// Рендерит режим поиска по сообщениям
|
||||||
fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||||
|
// Извлекаем данные из ChatState
|
||||||
|
let (query, results, selected_index) =
|
||||||
|
if let crate::app::ChatState::SearchInChat {
|
||||||
|
query,
|
||||||
|
results,
|
||||||
|
selected_index,
|
||||||
|
} = &app.chat_state
|
||||||
|
{
|
||||||
|
(query.as_str(), results.as_slice(), *selected_index)
|
||||||
|
} else {
|
||||||
|
return; // Некорректное состояние, не рендерим
|
||||||
|
};
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
@@ -986,14 +999,14 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// Search input
|
// Search input
|
||||||
let total = app.message_search_results.len();
|
let total = results.len();
|
||||||
let current = if total > 0 {
|
let current = if total > 0 {
|
||||||
app.selected_search_result_index + 1
|
selected_index + 1
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_line = if app.message_search_query.is_empty() {
|
let input_line = if query.is_empty() {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
|
Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
|
||||||
Span::styled("█", Style::default().fg(Color::Yellow)),
|
Span::styled("█", Style::default().fg(Color::Yellow)),
|
||||||
@@ -1002,7 +1015,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
} else {
|
} else {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
|
Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
|
||||||
Span::styled(&app.message_search_query, Style::default().fg(Color::White)),
|
Span::styled(query, Style::default().fg(Color::White)),
|
||||||
Span::styled("█", Style::default().fg(Color::Yellow)),
|
Span::styled("█", Style::default().fg(Color::Yellow)),
|
||||||
Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)),
|
Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)),
|
||||||
])
|
])
|
||||||
@@ -1025,16 +1038,16 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let content_width = chunks[1].width.saturating_sub(2) as usize;
|
let content_width = chunks[1].width.saturating_sub(2) as usize;
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
|
||||||
if app.message_search_results.is_empty() {
|
if results.is_empty() {
|
||||||
if !app.message_search_query.is_empty() {
|
if !query.is_empty() {
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
"Ничего не найдено",
|
"Ничего не найдено",
|
||||||
Style::default().fg(Color::Gray),
|
Style::default().fg(Color::Gray),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (idx, msg) in app.message_search_results.iter().enumerate() {
|
for (idx, msg) in results.iter().enumerate() {
|
||||||
let is_selected = idx == app.selected_search_result_index;
|
let is_selected = idx == selected_index;
|
||||||
|
|
||||||
// Пустая строка между результатами
|
// Пустая строка между результатами
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
@@ -1101,7 +1114,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
// Скролл к выбранному результату
|
// Скролл к выбранному результату
|
||||||
let visible_height = chunks[1].height.saturating_sub(2) as usize;
|
let visible_height = chunks[1].height.saturating_sub(2) as usize;
|
||||||
let lines_per_result = 4;
|
let lines_per_result = 4;
|
||||||
let selected_line = app.selected_search_result_index * lines_per_result;
|
let selected_line = selected_index * lines_per_result;
|
||||||
let scroll_offset = if selected_line > visible_height / 2 {
|
let scroll_offset = if selected_line > visible_height / 2 {
|
||||||
(selected_line - visible_height / 2) as u16
|
(selected_line - visible_height / 2) as u16
|
||||||
} else {
|
} else {
|
||||||
@@ -1158,6 +1171,17 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
|
|
||||||
/// Рендерит режим просмотра закреплённых сообщений
|
/// Рендерит режим просмотра закреплённых сообщений
|
||||||
fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||||
|
// Извлекаем данные из ChatState
|
||||||
|
let (messages, selected_index) = if let crate::app::ChatState::PinnedMessages {
|
||||||
|
messages,
|
||||||
|
selected_index,
|
||||||
|
} = &app.chat_state
|
||||||
|
{
|
||||||
|
(messages.as_slice(), *selected_index)
|
||||||
|
} else {
|
||||||
|
return; // Некорректное состояние
|
||||||
|
};
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
@@ -1168,8 +1192,8 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let total = app.pinned_messages.len();
|
let total = messages.len();
|
||||||
let current = app.selected_pinned_index + 1;
|
let current = selected_index + 1;
|
||||||
let header_text = format!("📌 ЗАКРЕПЛЁННЫЕ ({}/{})", current, total);
|
let header_text = format!("📌 ЗАКРЕПЛЁННЫЕ ({}/{})", current, total);
|
||||||
let header = Paragraph::new(header_text)
|
let header = Paragraph::new(header_text)
|
||||||
.block(
|
.block(
|
||||||
@@ -1188,8 +1212,8 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
let content_width = chunks[1].width.saturating_sub(2) as usize;
|
let content_width = chunks[1].width.saturating_sub(2) as usize;
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
|
||||||
for (idx, msg) in app.pinned_messages.iter().enumerate() {
|
for (idx, msg) in messages.iter().enumerate() {
|
||||||
let is_selected = idx == app.selected_pinned_index;
|
let is_selected = idx == selected_index;
|
||||||
|
|
||||||
// Пустая строка между сообщениями
|
// Пустая строка между сообщениями
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
@@ -1263,7 +1287,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
// Скролл к выбранному сообщению
|
// Скролл к выбранному сообщению
|
||||||
let visible_height = chunks[1].height.saturating_sub(2) as usize;
|
let visible_height = chunks[1].height.saturating_sub(2) as usize;
|
||||||
let lines_per_msg = 5; // Примерно строк на сообщение
|
let lines_per_msg = 5; // Примерно строк на сообщение
|
||||||
let selected_line = app.selected_pinned_index * lines_per_msg;
|
let selected_line = selected_index * lines_per_msg;
|
||||||
let scroll_offset = if selected_line > visible_height / 2 {
|
let scroll_offset = if selected_line > visible_height / 2 {
|
||||||
(selected_line - visible_height / 2) as u16
|
(selected_line - visible_height / 2) as u16
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
|||||||
|
|
||||||
let actions = get_available_actions(profile);
|
let actions = get_available_actions(profile);
|
||||||
for (idx, action) in actions.iter().enumerate() {
|
for (idx, action) in actions.iter().enumerate() {
|
||||||
let is_selected = idx == app.selected_profile_action;
|
let is_selected = idx == app.get_selected_profile_action().unwrap_or(0);
|
||||||
let marker = if is_selected { "▶ " } else { " " };
|
let marker = if is_selected { "▶ " } else { " " };
|
||||||
let style = if is_selected {
|
let style = if is_selected {
|
||||||
Style::default()
|
Style::default()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tele_tui::app::{App, AppScreen};
|
use tele_tui::app::{App, AppScreen, ChatState};
|
||||||
use tele_tui::config::Config;
|
use tele_tui::config::Config;
|
||||||
use tele_tui::tdlib::client::AuthState;
|
use tele_tui::tdlib::client::AuthState;
|
||||||
use tele_tui::tdlib::{ChatInfo, MessageInfo};
|
use tele_tui::tdlib::{ChatInfo, MessageInfo};
|
||||||
@@ -21,17 +21,8 @@ pub struct TestAppBuilder {
|
|||||||
message_input: String,
|
message_input: String,
|
||||||
is_searching: bool,
|
is_searching: bool,
|
||||||
search_query: String,
|
search_query: String,
|
||||||
editing_message_id: Option<i64>,
|
chat_state: Option<ChatState>,
|
||||||
replying_to_message_id: Option<i64>,
|
|
||||||
is_reaction_picker_mode: bool,
|
|
||||||
is_profile_mode: bool,
|
|
||||||
confirm_delete_message_id: Option<i64>,
|
|
||||||
messages: HashMap<i64, Vec<MessageInfo>>,
|
messages: HashMap<i64, Vec<MessageInfo>>,
|
||||||
selected_message_index: Option<usize>,
|
|
||||||
message_search_mode: bool,
|
|
||||||
message_search_query: String,
|
|
||||||
forwarding_message_id: Option<i64>,
|
|
||||||
is_selecting_forward_chat: bool,
|
|
||||||
status_message: Option<String>,
|
status_message: Option<String>,
|
||||||
auth_state: Option<AuthState>,
|
auth_state: Option<AuthState>,
|
||||||
phone_input: Option<String>,
|
phone_input: Option<String>,
|
||||||
@@ -55,17 +46,8 @@ impl TestAppBuilder {
|
|||||||
message_input: String::new(),
|
message_input: String::new(),
|
||||||
is_searching: false,
|
is_searching: false,
|
||||||
search_query: String::new(),
|
search_query: String::new(),
|
||||||
editing_message_id: None,
|
chat_state: None,
|
||||||
replying_to_message_id: None,
|
|
||||||
is_reaction_picker_mode: false,
|
|
||||||
is_profile_mode: false,
|
|
||||||
confirm_delete_message_id: None,
|
|
||||||
messages: HashMap::new(),
|
messages: HashMap::new(),
|
||||||
selected_message_index: None,
|
|
||||||
message_search_mode: false,
|
|
||||||
message_search_query: String::new(),
|
|
||||||
forwarding_message_id: None,
|
|
||||||
is_selecting_forward_chat: false,
|
|
||||||
status_message: None,
|
status_message: None,
|
||||||
auth_state: None,
|
auth_state: None,
|
||||||
phone_input: None,
|
phone_input: None,
|
||||||
@@ -118,32 +100,43 @@ impl TestAppBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Режим редактирования сообщения
|
/// Режим редактирования сообщения
|
||||||
pub fn editing_message(mut self, message_id: i64) -> Self {
|
pub fn editing_message(mut self, message_id: i64, selected_index: usize) -> Self {
|
||||||
self.editing_message_id = Some(message_id);
|
self.chat_state = Some(ChatState::Editing {
|
||||||
|
message_id,
|
||||||
|
selected_index,
|
||||||
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Режим ответа на сообщение
|
/// Режим ответа на сообщение
|
||||||
pub fn replying_to(mut self, message_id: i64) -> Self {
|
pub fn replying_to(mut self, message_id: i64) -> Self {
|
||||||
self.replying_to_message_id = Some(message_id);
|
self.chat_state = Some(ChatState::Reply { message_id });
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Режим выбора реакции
|
/// Режим выбора реакции
|
||||||
pub fn reaction_picker(mut self) -> Self {
|
pub fn reaction_picker(mut self, message_id: i64, available_reactions: Vec<String>) -> Self {
|
||||||
self.is_reaction_picker_mode = true;
|
self.chat_state = Some(ChatState::ReactionPicker {
|
||||||
|
message_id,
|
||||||
|
available_reactions,
|
||||||
|
selected_index: 0,
|
||||||
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Режим профиля
|
/// Режим профиля
|
||||||
pub fn profile_mode(mut self) -> Self {
|
pub fn profile_mode(mut self, info: tele_tui::tdlib::ProfileInfo) -> Self {
|
||||||
self.is_profile_mode = true;
|
self.chat_state = Some(ChatState::Profile {
|
||||||
|
info,
|
||||||
|
selected_action: 0,
|
||||||
|
leave_group_confirmation_step: 0,
|
||||||
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Подтверждение удаления
|
/// Подтверждение удаления
|
||||||
pub fn delete_confirmation(mut self, message_id: i64) -> Self {
|
pub fn delete_confirmation(mut self, message_id: i64) -> Self {
|
||||||
self.confirm_delete_message_id = Some(message_id);
|
self.chat_state = Some(ChatState::DeleteConfirmation { message_id });
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,22 +159,27 @@ impl TestAppBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Установить выбранное сообщение (режим selection)
|
/// Установить выбранное сообщение (режим selection)
|
||||||
pub fn selecting_message(mut self, message_index: usize) -> Self {
|
pub fn selecting_message(mut self, selected_index: usize) -> Self {
|
||||||
self.selected_message_index = Some(message_index);
|
self.chat_state = Some(ChatState::MessageSelection { selected_index });
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Режим поиска по сообщениям в чате
|
/// Режим поиска по сообщениям в чате
|
||||||
pub fn message_search(mut self, query: &str) -> Self {
|
pub fn message_search(mut self, query: &str) -> Self {
|
||||||
self.message_search_mode = true;
|
self.chat_state = Some(ChatState::SearchInChat {
|
||||||
self.message_search_query = query.to_string();
|
query: query.to_string(),
|
||||||
|
results: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Режим пересылки сообщения
|
/// Режим пересылки сообщения
|
||||||
pub fn forward_mode(mut self, message_id: i64) -> Self {
|
pub fn forward_mode(mut self, message_id: i64) -> Self {
|
||||||
self.forwarding_message_id = Some(message_id);
|
self.chat_state = Some(ChatState::Forward {
|
||||||
self.is_selecting_forward_chat = true;
|
message_id,
|
||||||
|
selecting_chat: true,
|
||||||
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,16 +227,10 @@ impl TestAppBuilder {
|
|||||||
app.message_input = self.message_input;
|
app.message_input = self.message_input;
|
||||||
app.is_searching = self.is_searching;
|
app.is_searching = self.is_searching;
|
||||||
app.search_query = self.search_query;
|
app.search_query = self.search_query;
|
||||||
app.editing_message_id = self.editing_message_id;
|
// Применяем chat_state если он установлен
|
||||||
app.replying_to_message_id = self.replying_to_message_id;
|
if let Some(chat_state) = self.chat_state {
|
||||||
app.is_reaction_picker_mode = self.is_reaction_picker_mode;
|
app.chat_state = chat_state;
|
||||||
app.is_profile_mode = self.is_profile_mode;
|
}
|
||||||
app.confirm_delete_message_id = self.confirm_delete_message_id;
|
|
||||||
app.selected_message_index = self.selected_message_index;
|
|
||||||
app.is_message_search_mode = self.message_search_mode;
|
|
||||||
app.message_search_query = self.message_search_query;
|
|
||||||
app.forwarding_message_id = self.forwarding_message_id;
|
|
||||||
app.is_selecting_forward_chat = self.is_selecting_forward_chat;
|
|
||||||
|
|
||||||
// Применяем status_message
|
// Применяем status_message
|
||||||
if let Some(status) = self.status_message {
|
if let Some(status) = self.status_message {
|
||||||
@@ -325,11 +317,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_builder_editing_mode() {
|
fn test_builder_editing_mode() {
|
||||||
let app = TestAppBuilder::new()
|
let app = TestAppBuilder::new()
|
||||||
.editing_message(999)
|
.editing_message(999, 0)
|
||||||
.message_input("Edited text")
|
.message_input("Edited text")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(app.editing_message_id, Some(999));
|
assert!(app.is_editing());
|
||||||
|
assert_eq!(app.chat_state.selected_message_id(), Some(999));
|
||||||
assert_eq!(app.message_input, "Edited text");
|
assert_eq!(app.message_input, "Edited text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ fn snapshot_input_editing_mode() {
|
|||||||
.with_chat(chat)
|
.with_chat(chat)
|
||||||
.with_message(123, message)
|
.with_message(123, message)
|
||||||
.selected_chat(123)
|
.selected_chat(123)
|
||||||
.editing_message(1)
|
.editing_message(1, 0)
|
||||||
.message_input("Edited text here")
|
.message_input("Edited text here")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ fn snapshot_emoji_picker_default() {
|
|||||||
let chat = create_test_chat("Mom", 123);
|
let chat = create_test_chat("Mom", 123);
|
||||||
let message = TestMessageBuilder::new("React to this", 1).build();
|
let message = TestMessageBuilder::new("React to this", 1).build();
|
||||||
|
|
||||||
|
let reactions = vec!["👍".to_string(), "👎".to_string(), "❤️".to_string(), "🔥".to_string(), "😊".to_string(), "😢".to_string(), "😮".to_string(), "🎉".to_string()];
|
||||||
|
|
||||||
let app = TestAppBuilder::new()
|
let app = TestAppBuilder::new()
|
||||||
.with_chat(chat)
|
.with_chat(chat)
|
||||||
.with_message(123, message)
|
.with_message(123, message)
|
||||||
.selected_chat(123)
|
.selected_chat(123)
|
||||||
.reaction_picker()
|
.reaction_picker(1, reactions)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let buffer = render_to_buffer(80, 24, |f| {
|
let buffer = render_to_buffer(80, 24, |f| {
|
||||||
@@ -54,15 +56,19 @@ fn snapshot_emoji_picker_with_selection() {
|
|||||||
let chat = create_test_chat("Mom", 123);
|
let chat = create_test_chat("Mom", 123);
|
||||||
let message = TestMessageBuilder::new("React to this", 1).build();
|
let message = TestMessageBuilder::new("React to this", 1).build();
|
||||||
|
|
||||||
|
let reactions = vec!["👍".to_string(), "👎".to_string(), "❤️".to_string(), "🔥".to_string(), "😊".to_string(), "😢".to_string(), "😮".to_string(), "🎉".to_string()];
|
||||||
|
|
||||||
let mut app = TestAppBuilder::new()
|
let mut app = TestAppBuilder::new()
|
||||||
.with_chat(chat)
|
.with_chat(chat)
|
||||||
.with_message(123, message)
|
.with_message(123, message)
|
||||||
.selected_chat(123)
|
.selected_chat(123)
|
||||||
.reaction_picker()
|
.reaction_picker(1, reactions)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Выбираем 5-ю реакцию (индекс 4)
|
// Выбираем 5-ю реакцию (индекс 4)
|
||||||
app.selected_reaction_index = 4;
|
if let tele_tui::app::ChatState::ReactionPicker { selected_index, .. } = &mut app.chat_state {
|
||||||
|
*selected_index = 4;
|
||||||
|
}
|
||||||
|
|
||||||
let buffer = render_to_buffer(80, 24, |f| {
|
let buffer = render_to_buffer(80, 24, |f| {
|
||||||
tele_tui::ui::messages::render(f, f.area(), &app);
|
tele_tui::ui::messages::render(f, f.area(), &app);
|
||||||
@@ -77,14 +83,12 @@ fn snapshot_profile_personal_chat() {
|
|||||||
let chat = create_test_chat("Alice", 123);
|
let chat = create_test_chat("Alice", 123);
|
||||||
let profile = create_test_profile("Alice", 123);
|
let profile = create_test_profile("Alice", 123);
|
||||||
|
|
||||||
let mut app = TestAppBuilder::new()
|
let app = TestAppBuilder::new()
|
||||||
.with_chat(chat)
|
.with_chat(chat)
|
||||||
.selected_chat(123)
|
.selected_chat(123)
|
||||||
.profile_mode()
|
.profile_mode(profile)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
app.profile_info = Some(profile);
|
|
||||||
|
|
||||||
let buffer = render_to_buffer(80, 24, |f| {
|
let buffer = render_to_buffer(80, 24, |f| {
|
||||||
tele_tui::ui::messages::render(f, f.area(), &app);
|
tele_tui::ui::messages::render(f, f.area(), &app);
|
||||||
});
|
});
|
||||||
@@ -103,14 +107,12 @@ fn snapshot_profile_group_chat() {
|
|||||||
profile.member_count = Some(25);
|
profile.member_count = Some(25);
|
||||||
profile.description = Some("Work discussion group".to_string());
|
profile.description = Some("Work discussion group".to_string());
|
||||||
|
|
||||||
let mut app = TestAppBuilder::new()
|
let app = TestAppBuilder::new()
|
||||||
.with_chat(chat)
|
.with_chat(chat)
|
||||||
.selected_chat(456)
|
.selected_chat(456)
|
||||||
.profile_mode()
|
.profile_mode(profile)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
app.profile_info = Some(profile);
|
|
||||||
|
|
||||||
let buffer = render_to_buffer(80, 24, |f| {
|
let buffer = render_to_buffer(80, 24, |f| {
|
||||||
tele_tui::ui::messages::render(f, f.area(), &app);
|
tele_tui::ui::messages::render(f, f.area(), &app);
|
||||||
});
|
});
|
||||||
@@ -157,8 +159,10 @@ fn snapshot_search_in_chat() {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Устанавливаем результаты поиска
|
// Устанавливаем результаты поиска
|
||||||
app.message_search_results = vec![msg1, msg2];
|
if let tele_tui::app::ChatState::SearchInChat { results, selected_index, .. } = &mut app.chat_state {
|
||||||
app.selected_search_result_index = 0;
|
*results = vec![msg1, msg2];
|
||||||
|
*selected_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let buffer = render_to_buffer(80, 24, |f| {
|
let buffer = render_to_buffer(80, 24, |f| {
|
||||||
tele_tui::ui::messages::render(f, f.area(), &app);
|
tele_tui::ui::messages::render(f, f.area(), &app);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ expression: output
|
|||||||
│ ──────── 02.01.2022 ──────── │
|
│ ──────── 02.01.2022 ──────── │
|
||||||
│ │
|
│ │
|
||||||
│ Вы ──────────────── │
|
│ Вы ──────────────── │
|
||||||
│ Original message text (14:33 ✓✓) │
|
│ ▶ Original message text (14:33 ✓✓) │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ expression: output
|
|||||||
│User ──────────────── │
|
│User ──────────────── │
|
||||||
│ (14:33) React to this │
|
│ (14:33) React to this │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
|
||||||
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
|
│ │ 👍 👎 ❤️ 🔥 😊 😢 😮 🎉 │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ expression: output
|
|||||||
│User ──────────────── │
|
│User ──────────────── │
|
||||||
│ (14:33) React to this │
|
│ (14:33) React to this │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
|
||||||
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
|
│ │ 👍 👎 ❤️ 🔥 😊 😢 😮 🎉 │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
Reference in New Issue
Block a user