refactor: add modal/validation utils and partial App encapsulation

Quick wins refactoring (Variant 1):
- Created src/utils/modal_handler.rs (120+ lines)
  - 4 functions for modal handling (close, confirm, yes/no)
  - ModalAction enum for type-safe processing
  - English and Russian keyboard layout support
  - 4 unit tests
- Created src/utils/validation.rs (180+ lines)
  - 7 validation functions (empty, length, IDs, etc)
  - Covers all common validation patterns
  - 7 unit tests
- Partial App encapsulation:
  - Made config field private (readonly via app.config())
  - Added 30+ getter/setter methods
  - Updated ui/messages.rs to use config()
- Updated documentation:
  - REFACTORING_OPPORTUNITIES.md: #1 Complete, #5 Partial
  - CONTEXT.md: Added quick wins section

Tests: 563 passed, 0 failed

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-01 23:55:49 +03:00
parent e690acfb09
commit dff0897da4
7 changed files with 631 additions and 19 deletions

View File

@@ -44,18 +44,19 @@ use ratatui::widgets::ListState;
/// app.select_current_chat();
/// ```
pub struct App {
pub config: crate::config::Config,
// Core (config - readonly через getter)
config: crate::config::Config,
pub screen: AppScreen,
pub td_client: TdClient,
/// Состояние чата - type-safe state machine (новое!)
pub chat_state: ChatState,
// Auth state
// Auth state (используются часто в UI)
pub phone_input: String,
pub code_input: String,
pub password_input: String,
pub error_message: Option<String>,
pub status_message: Option<String>,
// Main app state
// Main app state (используются часто)
pub chats: Vec<ChatInfo>,
pub chat_list_state: ListState,
pub selected_chat_id: Option<ChatId>,
@@ -800,4 +801,193 @@ impl App {
pub fn get_selected_message_for_reaction(&self) -> Option<i64> {
self.chat_state.selected_message_id().map(|id| id.as_i64())
}
// ========== Getter/Setter методы для инкапсуляции ==========
// Config
pub fn config(&self) -> &crate::config::Config {
&self.config
}
// Screen
pub fn screen(&self) -> &AppScreen {
&self.screen
}
pub fn set_screen(&mut self, screen: AppScreen) {
self.screen = screen;
}
// Auth state
pub fn phone_input(&self) -> &str {
&self.phone_input
}
pub fn phone_input_mut(&mut self) -> &mut String {
&mut self.phone_input
}
pub fn set_phone_input(&mut self, input: String) {
self.phone_input = input;
}
pub fn code_input(&self) -> &str {
&self.code_input
}
pub fn code_input_mut(&mut self) -> &mut String {
&mut self.code_input
}
pub fn set_code_input(&mut self, input: String) {
self.code_input = input;
}
pub fn password_input(&self) -> &str {
&self.password_input
}
pub fn password_input_mut(&mut self) -> &mut String {
&mut self.password_input
}
pub fn set_password_input(&mut self, input: String) {
self.password_input = input;
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn set_error_message(&mut self, message: Option<String>) {
self.error_message = message;
}
pub fn status_message(&self) -> Option<&str> {
self.status_message.as_deref()
}
pub fn set_status_message(&mut self, message: Option<String>) {
self.status_message = message;
}
// Main app state
pub fn chats(&self) -> &[ChatInfo] {
&self.chats
}
pub fn chats_mut(&mut self) -> &mut Vec<ChatInfo> {
&mut self.chats
}
pub fn set_chats(&mut self, chats: Vec<ChatInfo>) {
self.chats = chats;
}
pub fn chat_list_state(&self) -> &ListState {
&self.chat_list_state
}
pub fn chat_list_state_mut(&mut self) -> &mut ListState {
&mut self.chat_list_state
}
pub fn selected_chat_id(&self) -> Option<ChatId> {
self.selected_chat_id
}
pub fn set_selected_chat_id(&mut self, id: Option<ChatId>) {
self.selected_chat_id = id;
}
pub fn message_input(&self) -> &str {
&self.message_input
}
pub fn message_input_mut(&mut self) -> &mut String {
&mut self.message_input
}
pub fn set_message_input(&mut self, input: String) {
self.message_input = input;
}
pub fn cursor_position(&self) -> usize {
self.cursor_position
}
pub fn set_cursor_position(&mut self, pos: usize) {
self.cursor_position = pos;
}
pub fn message_scroll_offset(&self) -> usize {
self.message_scroll_offset
}
pub fn set_message_scroll_offset(&mut self, offset: usize) {
self.message_scroll_offset = offset;
}
pub fn selected_folder_id(&self) -> Option<i32> {
self.selected_folder_id
}
pub fn set_selected_folder_id(&mut self, id: Option<i32>) {
self.selected_folder_id = id;
}
pub fn is_loading(&self) -> bool {
self.is_loading
}
pub fn set_loading(&mut self, loading: bool) {
self.is_loading = loading;
}
// Search state
pub fn is_searching(&self) -> bool {
self.is_searching
}
pub fn set_searching(&mut self, searching: bool) {
self.is_searching = searching;
}
pub fn search_query(&self) -> &str {
&self.search_query
}
pub fn search_query_mut(&mut self) -> &mut String {
&mut self.search_query
}
pub fn set_search_query(&mut self, query: String) {
self.search_query = query;
}
// Redraw flag
pub fn needs_redraw(&self) -> bool {
self.needs_redraw
}
pub fn set_needs_redraw(&mut self, redraw: bool) {
self.needs_redraw = redraw;
}
pub fn mark_for_redraw(&mut self) {
self.needs_redraw = true;
}
// Typing indicator
pub fn last_typing_sent(&self) -> Option<std::time::Instant> {
self.last_typing_sent
}
pub fn set_last_typing_sent(&mut self, time: Option<std::time::Instant>) {
self.last_typing_sent = time;
}
pub fn update_last_typing_sent(&mut self) {
self.last_typing_sent = Some(std::time::Instant::now());
}
}