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;
|
||||
|
||||
pub use chat_state::ChatState;
|
||||
pub use state::AppScreen;
|
||||
|
||||
use crate::tdlib::client::ChatInfo;
|
||||
@@ -10,6 +12,8 @@ pub struct App {
|
||||
pub config: crate::config::Config,
|
||||
pub screen: AppScreen,
|
||||
pub td_client: TdClient,
|
||||
/// Состояние чата - type-safe state machine (новое!)
|
||||
pub chat_state: ChatState,
|
||||
// Auth state
|
||||
pub phone_input: String,
|
||||
pub code_input: String,
|
||||
@@ -32,59 +36,9 @@ pub struct App {
|
||||
pub search_query: String,
|
||||
/// Флаг для оптимизации рендеринга - перерисовывать только при изменениях
|
||||
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 status (для throttling)
|
||||
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 {
|
||||
@@ -96,6 +50,7 @@ impl App {
|
||||
config,
|
||||
screen: AppScreen::Loading,
|
||||
td_client: TdClient::new(),
|
||||
chat_state: ChatState::Normal,
|
||||
phone_input: String::new(),
|
||||
code_input: String::new(),
|
||||
password_input: String::new(),
|
||||
@@ -112,28 +67,7 @@ impl App {
|
||||
is_searching: false,
|
||||
search_query: String::new(),
|
||||
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,
|
||||
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.cursor_position = 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;
|
||||
// Сбрасываем pinned режим
|
||||
self.is_pinned_mode = false;
|
||||
self.pinned_messages.clear();
|
||||
self.selected_pinned_index = 0;
|
||||
// Сбрасываем состояние чата в нормальный режим
|
||||
self.chat_state = ChatState::Normal;
|
||||
// Очищаем данные в TdClient
|
||||
self.td_client.current_chat_id = None;
|
||||
self.td_client.current_chat_messages.clear();
|
||||
self.td_client.typing_status = 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;
|
||||
}
|
||||
// Начинаем с последнего сообщения (индекс 0 = самое новое снизу)
|
||||
self.selected_message_index = Some(0);
|
||||
self.chat_state = ChatState::MessageSelection { selected_index: 0 };
|
||||
}
|
||||
|
||||
/// Выбрать предыдущее сообщение (вверх по списку = увеличить индекс)
|
||||
@@ -222,24 +146,25 @@ impl App {
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
self.selected_message_index = Some(
|
||||
self.selected_message_index
|
||||
.map(|i| (i + 1).min(total - 1))
|
||||
.unwrap_or(0),
|
||||
);
|
||||
if let ChatState::MessageSelection { selected_index } = &mut self.chat_state {
|
||||
*selected_index = (*selected_index + 1).min(total - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать следующее сообщение (вниз по списку = уменьшить индекс)
|
||||
pub fn select_next_message(&mut self) {
|
||||
self.selected_message_index = self
|
||||
.selected_message_index
|
||||
.map(|i| if i > 0 { Some(i - 1) } else { None })
|
||||
.flatten();
|
||||
if let ChatState::MessageSelection { selected_index } = &mut self.chat_state {
|
||||
if *selected_index > 0 {
|
||||
*selected_index -= 1;
|
||||
} else {
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить выбранное сообщение
|
||||
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();
|
||||
if total == 0 || idx >= total {
|
||||
return None;
|
||||
@@ -251,21 +176,33 @@ impl App {
|
||||
|
||||
/// Начать редактирование выбранного сообщения
|
||||
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| {
|
||||
if msg.can_be_edited && msg.is_outgoing {
|
||||
Some((msg.id, msg.content.clone()))
|
||||
Some((msg.id, msg.content.clone(), selected_idx.unwrap()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Затем присваиваем
|
||||
if let Some((id, content)) = msg_data {
|
||||
self.editing_message_id = Some(id);
|
||||
if let Some((id, content, idx)) = msg_data {
|
||||
self.cursor_position = content.chars().count();
|
||||
self.message_input = content;
|
||||
self.selected_message_index = None;
|
||||
self.chat_state = ChatState::Editing {
|
||||
message_id: id,
|
||||
selected_index: idx,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
false
|
||||
@@ -273,20 +210,19 @@ impl App {
|
||||
|
||||
/// Отменить редактирование
|
||||
pub fn cancel_editing(&mut self) {
|
||||
self.editing_message_id = None;
|
||||
self.selected_message_index = None;
|
||||
self.chat_state = ChatState::Normal;
|
||||
self.message_input.clear();
|
||||
self.cursor_position = 0;
|
||||
}
|
||||
|
||||
/// Проверить, находимся ли в режиме редактирования
|
||||
pub fn is_editing(&self) -> bool {
|
||||
self.editing_message_id.is_some()
|
||||
self.chat_state.is_editing()
|
||||
}
|
||||
|
||||
/// Проверить, находимся ли в режиме выбора сообщения
|
||||
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> {
|
||||
@@ -385,14 +321,15 @@ impl App {
|
||||
|
||||
/// Проверить, показывается ли модалка подтверждения удаления
|
||||
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 {
|
||||
if let Some(msg) = self.get_selected_message() {
|
||||
self.replying_to_message_id = Some(msg.id);
|
||||
self.selected_message_index = None;
|
||||
self.chat_state = ChatState::Reply {
|
||||
message_id: msg.id,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
false
|
||||
@@ -400,17 +337,17 @@ impl App {
|
||||
|
||||
/// Отменить режим ответа
|
||||
pub fn cancel_reply(&mut self) {
|
||||
self.replying_to_message_id = None;
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
|
||||
/// Проверить, находимся ли в режиме ответа
|
||||
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> {
|
||||
self.replying_to_message_id.and_then(|id| {
|
||||
self.chat_state.selected_message_id().and_then(|id| {
|
||||
self.td_client
|
||||
.current_chat_messages
|
||||
.iter()
|
||||
@@ -421,9 +358,10 @@ impl App {
|
||||
/// Начать режим пересылки выбранного сообщения
|
||||
pub fn start_forward_selected(&mut self) -> bool {
|
||||
if let Some(msg) = self.get_selected_message() {
|
||||
self.forwarding_message_id = Some(msg.id);
|
||||
self.selected_message_index = None;
|
||||
self.is_selecting_forward_chat = true;
|
||||
self.chat_state = ChatState::Forward {
|
||||
message_id: msg.id,
|
||||
selecting_chat: true,
|
||||
};
|
||||
// Сбрасываем выбор чата на первый
|
||||
self.chat_list_state.select(Some(0));
|
||||
return true;
|
||||
@@ -433,18 +371,20 @@ impl App {
|
||||
|
||||
/// Отменить режим пересылки
|
||||
pub fn cancel_forward(&mut self) {
|
||||
self.forwarding_message_id = None;
|
||||
self.is_selecting_forward_chat = false;
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
|
||||
/// Проверить, находимся ли в режиме выбора чата для пересылки
|
||||
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> {
|
||||
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
|
||||
.current_chat_messages
|
||||
.iter()
|
||||
@@ -456,44 +396,57 @@ impl App {
|
||||
|
||||
/// Проверка режима pinned
|
||||
pub fn is_pinned_mode(&self) -> bool {
|
||||
self.is_pinned_mode
|
||||
self.chat_state.is_pinned_mode()
|
||||
}
|
||||
|
||||
/// Войти в режим pinned (вызывается после загрузки pinned сообщений)
|
||||
pub fn enter_pinned_mode(&mut self, messages: Vec<crate::tdlib::client::MessageInfo>) {
|
||||
if !messages.is_empty() {
|
||||
self.pinned_messages = messages;
|
||||
self.selected_pinned_index = 0;
|
||||
self.is_pinned_mode = true;
|
||||
self.chat_state = ChatState::PinnedMessages {
|
||||
messages,
|
||||
selected_index: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Выйти из режима pinned
|
||||
pub fn exit_pinned_mode(&mut self) {
|
||||
self.is_pinned_mode = false;
|
||||
self.pinned_messages.clear();
|
||||
self.selected_pinned_index = 0;
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
|
||||
/// Выбрать предыдущий pinned (вверх = более старый)
|
||||
pub fn select_previous_pinned(&mut self) {
|
||||
if !self.pinned_messages.is_empty()
|
||||
&& self.selected_pinned_index < self.pinned_messages.len() - 1
|
||||
if let ChatState::PinnedMessages {
|
||||
selected_index,
|
||||
messages,
|
||||
} = &mut self.chat_state
|
||||
{
|
||||
self.selected_pinned_index += 1;
|
||||
if *selected_index + 1 < messages.len() {
|
||||
*selected_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать следующий pinned (вниз = более новый)
|
||||
pub fn select_next_pinned(&mut self) {
|
||||
if self.selected_pinned_index > 0 {
|
||||
self.selected_pinned_index -= 1;
|
||||
if let ChatState::PinnedMessages { selected_index, .. } = &mut self.chat_state {
|
||||
if *selected_index > 0 {
|
||||
*selected_index -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить текущее выбранное pinned сообщение
|
||||
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 для перехода в историю
|
||||
@@ -505,51 +458,66 @@ impl App {
|
||||
|
||||
/// Проверить, активен ли режим поиска по сообщениям
|
||||
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) {
|
||||
self.is_message_search_mode = true;
|
||||
self.message_search_query.clear();
|
||||
self.message_search_results.clear();
|
||||
self.selected_search_result_index = 0;
|
||||
self.chat_state = ChatState::SearchInChat {
|
||||
query: String::new(),
|
||||
results: Vec::new(),
|
||||
selected_index: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Выйти из режима поиска
|
||||
pub fn exit_message_search_mode(&mut self) {
|
||||
self.is_message_search_mode = false;
|
||||
self.message_search_query.clear();
|
||||
self.message_search_results.clear();
|
||||
self.selected_search_result_index = 0;
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
|
||||
/// Установить результаты поиска
|
||||
pub fn set_search_results(&mut self, results: Vec<crate::tdlib::client::MessageInfo>) {
|
||||
self.message_search_results = results;
|
||||
self.selected_search_result_index = 0;
|
||||
if let ChatState::SearchInChat { results: r, selected_index, .. } = &mut self.chat_state {
|
||||
*r = results;
|
||||
*selected_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать предыдущий результат (вверх)
|
||||
pub fn select_previous_search_result(&mut self) {
|
||||
if self.selected_search_result_index > 0 {
|
||||
self.selected_search_result_index -= 1;
|
||||
if let ChatState::SearchInChat { selected_index, .. } = &mut self.chat_state {
|
||||
if *selected_index > 0 {
|
||||
*selected_index -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать следующий результат (вниз)
|
||||
pub fn select_next_search_result(&mut self) {
|
||||
if !self.message_search_results.is_empty()
|
||||
&& self.selected_search_result_index < self.message_search_results.len() - 1
|
||||
if let ChatState::SearchInChat {
|
||||
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> {
|
||||
self.message_search_results
|
||||
.get(self.selected_search_result_index)
|
||||
if let ChatState::SearchInChat {
|
||||
results,
|
||||
selected_index,
|
||||
..
|
||||
} = &self.chat_state
|
||||
{
|
||||
results.get(*selected_index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить ID выбранного результата для перехода
|
||||
@@ -557,6 +525,40 @@ impl App {
|
||||
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 ===
|
||||
|
||||
/// Получить черновик для текущего чата
|
||||
@@ -581,62 +583,118 @@ impl App {
|
||||
|
||||
/// Проверить, активен ли режим профиля
|
||||
pub fn is_profile_mode(&self) -> bool {
|
||||
self.is_profile_mode
|
||||
self.chat_state.is_profile()
|
||||
}
|
||||
|
||||
/// Войти в режим профиля
|
||||
pub fn enter_profile_mode(&mut self) {
|
||||
self.is_profile_mode = true;
|
||||
self.selected_profile_action = 0;
|
||||
self.leave_group_confirmation_step = 0;
|
||||
pub fn enter_profile_mode(&mut self, info: crate::tdlib::ProfileInfo) {
|
||||
self.chat_state = ChatState::Profile {
|
||||
info,
|
||||
selected_action: 0,
|
||||
leave_group_confirmation_step: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Выйти из режима профиля
|
||||
pub fn exit_profile_mode(&mut self) {
|
||||
self.is_profile_mode = false;
|
||||
self.selected_profile_action = 0;
|
||||
self.leave_group_confirmation_step = 0;
|
||||
self.profile_info = None;
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
|
||||
/// Выбрать предыдущее действие
|
||||
pub fn select_previous_profile_action(&mut self) {
|
||||
if self.selected_profile_action > 0 {
|
||||
self.selected_profile_action -= 1;
|
||||
if let ChatState::Profile {
|
||||
selected_action, ..
|
||||
} = &mut self.chat_state
|
||||
{
|
||||
if *selected_action > 0 {
|
||||
*selected_action -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать следующее действие
|
||||
pub fn select_next_profile_action(&mut self, max_actions: usize) {
|
||||
if self.selected_profile_action < max_actions.saturating_sub(1) {
|
||||
self.selected_profile_action += 1;
|
||||
if let ChatState::Profile {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
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 ==========
|
||||
|
||||
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(
|
||||
@@ -644,36 +702,52 @@ impl App {
|
||||
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;
|
||||
self.chat_state = ChatState::ReactionPicker {
|
||||
message_id,
|
||||
available_reactions,
|
||||
selected_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;
|
||||
self.chat_state = ChatState::Normal;
|
||||
}
|
||||
|
||||
pub fn select_previous_reaction(&mut self) {
|
||||
if !self.available_reactions.is_empty() && self.selected_reaction_index > 0 {
|
||||
self.selected_reaction_index -= 1;
|
||||
if let ChatState::ReactionPicker { selected_index, .. } = &mut self.chat_state {
|
||||
if *selected_index > 0 {
|
||||
*selected_index -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next_reaction(&mut self) {
|
||||
if self.selected_reaction_index + 1 < self.available_reactions.len() {
|
||||
self.selected_reaction_index += 1;
|
||||
if let ChatState::ReactionPicker {
|
||||
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> {
|
||||
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> {
|
||||
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();
|
||||
}
|
||||
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);
|
||||
app.select_next_profile_action(max_actions);
|
||||
}
|
||||
}
|
||||
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 action_index = app.selected_profile_action;
|
||||
let action_index = app.get_selected_profile_action().unwrap_or(0);
|
||||
|
||||
if action_index < actions {
|
||||
// Определяем какое действие выбрано
|
||||
@@ -201,36 +201,42 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.message_search_query.pop();
|
||||
// Выполняем поиск при изменении запроса
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
if !app.message_search_query.is_empty() {
|
||||
// Удаляем символ из запроса
|
||||
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 !query.is_empty() {
|
||||
if let Ok(Ok(results)) = timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client.search_messages(chat_id, &query),
|
||||
)
|
||||
.await
|
||||
{
|
||||
app.set_search_results(results);
|
||||
}
|
||||
} else {
|
||||
app.set_search_results(Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char(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 Ok(Ok(results)) = timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client
|
||||
.search_messages(chat_id, &app.message_search_query),
|
||||
app.td_client.search_messages(chat_id, &query),
|
||||
)
|
||||
.await
|
||||
{
|
||||
app.set_search_results(results);
|
||||
}
|
||||
} else {
|
||||
app.set_search_results(Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.message_search_query.push(c);
|
||||
// Выполняем поиск при изменении запроса
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
if let Ok(Ok(results)) = timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client
|
||||
.search_messages(chat_id, &app.message_search_query),
|
||||
)
|
||||
.await
|
||||
{
|
||||
app.set_search_results(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,17 +293,30 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Переход на ряд выше (8 эмодзи в ряду)
|
||||
if app.selected_reaction_index >= 8 {
|
||||
app.selected_reaction_index = app.selected_reaction_index.saturating_sub(8);
|
||||
app.needs_redraw = true;
|
||||
if let crate::app::ChatState::ReactionPicker {
|
||||
selected_index,
|
||||
..
|
||||
} = &mut app.chat_state
|
||||
{
|
||||
if *selected_index >= 8 {
|
||||
*selected_index = selected_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;
|
||||
if let crate::app::ChatState::ReactionPicker {
|
||||
selected_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
@@ -351,7 +370,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
match key.code {
|
||||
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() {
|
||||
// Находим сообщение для проверки can_be_deleted_for_all_users
|
||||
let can_delete_for_all = app
|
||||
@@ -377,7 +396,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.td_client
|
||||
.current_chat_messages
|
||||
.retain(|m| m.id != msg_id);
|
||||
app.selected_message_index = None;
|
||||
// Сбрасываем состояние
|
||||
app.chat_state = crate::app::ChatState::Normal;
|
||||
}
|
||||
Ok(Err(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 => {
|
||||
// Отмена удаления
|
||||
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(chat) = filtered.get(i) {
|
||||
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() {
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
@@ -528,7 +549,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
// Редактирование начато
|
||||
} else {
|
||||
// Нельзя редактировать это сообщение
|
||||
app.selected_message_index = None;
|
||||
app.chat_state = crate::app::ChatState::Normal;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -538,11 +559,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
let text = app.message_input.clone();
|
||||
|
||||
if let Some(msg_id) = app.editing_message_id {
|
||||
// Режим редактирования
|
||||
app.message_input.clear();
|
||||
app.cursor_position = 0;
|
||||
app.editing_message_id = None;
|
||||
if let Some(msg_id) = app.chat_state.selected_message_id() {
|
||||
if app.is_editing() {
|
||||
// Режим редактирования
|
||||
app.message_input.clear();
|
||||
app.cursor_position = 0;
|
||||
app.chat_state = crate::app::ChatState::Normal;
|
||||
|
||||
match timeout(
|
||||
Duration::from_secs(5),
|
||||
@@ -570,9 +592,14 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.error_message = Some("Таймаут редактирования".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Обычная отправка (или 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 ДО отправки, пока сообщение точно доступно
|
||||
let reply_info = app.get_replying_to_message().map(|m| {
|
||||
crate::tdlib::client::ReplyInfo {
|
||||
@@ -583,7 +610,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
});
|
||||
app.message_input.clear();
|
||||
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;
|
||||
|
||||
// Отменяем typing status
|
||||
@@ -665,7 +695,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
if key.code == KeyCode::Esc {
|
||||
if app.is_selecting_message() {
|
||||
// Отменить выбор сообщения
|
||||
app.selected_message_index = None;
|
||||
app.chat_state = crate::app::ChatState::Normal;
|
||||
} else if app.is_editing() {
|
||||
// Отменить редактирование
|
||||
app.cancel_editing();
|
||||
@@ -709,7 +739,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
let can_delete =
|
||||
msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users;
|
||||
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
|
||||
{
|
||||
Ok(Ok(profile)) => {
|
||||
app.profile_info = Some(profile);
|
||||
app.enter_profile_mode();
|
||||
app.enter_profile_mode(profile);
|
||||
app.status_message = None;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
|
||||
@@ -322,7 +322,7 @@ fn adjust_entities_for_substring(
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Режим профиля
|
||||
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);
|
||||
}
|
||||
return;
|
||||
@@ -964,18 +964,31 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
// Модалка выбора реакции
|
||||
if app.is_reaction_picker_mode() {
|
||||
render_reaction_picker_modal(
|
||||
f,
|
||||
area,
|
||||
&app.available_reactions,
|
||||
app.selected_reaction_index,
|
||||
);
|
||||
if let crate::app::ChatState::ReactionPicker {
|
||||
available_reactions,
|
||||
selected_index,
|
||||
..
|
||||
} = &app.chat_state
|
||||
{
|
||||
render_reaction_picker_modal(f, area, available_reactions, *selected_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Рендерит режим поиска по сообщениям
|
||||
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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
@@ -986,14 +999,14 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.split(area);
|
||||
|
||||
// Search input
|
||||
let total = app.message_search_results.len();
|
||||
let total = results.len();
|
||||
let current = if total > 0 {
|
||||
app.selected_search_result_index + 1
|
||||
selected_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let input_line = if app.message_search_query.is_empty() {
|
||||
let input_line = if query.is_empty() {
|
||||
Line::from(vec![
|
||||
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 {
|
||||
Line::from(vec![
|
||||
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(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 mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
if app.message_search_results.is_empty() {
|
||||
if !app.message_search_query.is_empty() {
|
||||
if results.is_empty() {
|
||||
if !query.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Ничего не найдено",
|
||||
Style::default().fg(Color::Gray),
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
for (idx, msg) in app.message_search_results.iter().enumerate() {
|
||||
let is_selected = idx == app.selected_search_result_index;
|
||||
for (idx, msg) in results.iter().enumerate() {
|
||||
let is_selected = idx == selected_index;
|
||||
|
||||
// Пустая строка между результатами
|
||||
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 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 {
|
||||
(selected_line - visible_height / 2) as u16
|
||||
} 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) {
|
||||
// Извлекаем данные из 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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
@@ -1168,8 +1192,8 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.split(area);
|
||||
|
||||
// Header
|
||||
let total = app.pinned_messages.len();
|
||||
let current = app.selected_pinned_index + 1;
|
||||
let total = messages.len();
|
||||
let current = selected_index + 1;
|
||||
let header_text = format!("📌 ЗАКРЕПЛЁННЫЕ ({}/{})", current, total);
|
||||
let header = Paragraph::new(header_text)
|
||||
.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 mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
for (idx, msg) in app.pinned_messages.iter().enumerate() {
|
||||
let is_selected = idx == app.selected_pinned_index;
|
||||
for (idx, msg) in messages.iter().enumerate() {
|
||||
let is_selected = idx == selected_index;
|
||||
|
||||
// Пустая строка между сообщениями
|
||||
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 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 {
|
||||
(selected_line - visible_height / 2) as u16
|
||||
} else {
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
|
||||
let actions = get_available_actions(profile);
|
||||
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 style = if is_selected {
|
||||
Style::default()
|
||||
|
||||
Reference in New Issue
Block a user