This commit is contained in:
Mikhail Kilin
2026-01-30 17:26:21 +03:00
parent a4cf6bac72
commit 433233d766
11 changed files with 603 additions and 315 deletions

View File

@@ -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()
}
}