Resolve stale TODO paths

This commit is contained in:
Mikhail Kilin
2026-05-17 18:50:36 +03:00
parent 91e4f118f3
commit 2d4c1906d5
5 changed files with 92 additions and 533 deletions

View File

@@ -193,11 +193,11 @@ Target files:
Steps:
- [ ] Review every TODO in `src/`.
- [ ] Convert active TODOs into tests or tracked plan items.
- [ ] Remove stale TODOs whose behavior is already implemented.
- [ ] For pinned-message compatibility in `messages/operations.rs`, decide whether the fallback is still needed and document the decision in code or tests.
- [ ] Run `cargo test --all-features`.
- [x] Review every TODO in `src/`.
- [x] Convert active TODOs into tests or tracked plan items.
- [x] Remove stale TODOs whose behavior is already implemented.
- [x] For pinned-message compatibility in `messages/operations.rs`, decide whether the fallback is still needed and document the decision in code or tests.
- [x] Run `cargo test --all-features`.
Acceptance criteria:

View File

@@ -456,24 +456,3 @@ pub async fn handle_open_chat_keyboard_input<T: TdClientTrait>(app: &mut App<T>,
_ => {}
}
}
// TODO (Этап 4): Эти функции будут переписаны для модального просмотрщика
/*
#[cfg(feature = "images")]
fn collapse_photo<T: TdClientTrait>(app: &mut App<T>, msg_id: crate::types::MessageId) {
// Закомментировано - будет реализовано в Этапе 4
}
#[cfg(feature = "images")]
fn expand_photo<T: TdClientTrait>(app: &mut App<T>, msg_id: crate::types::MessageId, path: &str) {
// Закомментировано - будет реализовано в Этапе 4
}
*/
// TODO (Этап 4): Функция _download_and_expand будет переписана
/*
#[cfg(feature = "images")]
async fn _download_and_expand<T: TdClientTrait>(app: &mut App<T>, msg_id: crate::types::MessageId, file_id: i32) {
// Закомментировано - будет реализовано в Этапе 4
}
*/

View File

@@ -1,450 +0,0 @@
/// Модуль для обработки клавиш с использованием trait-based подхода
///
/// Позволяет каждому экрану/режиму определить свою логику обработки клавиш,
/// избегая огромных match блоков в одном месте.
use crate::app::App;
use crate::config::Command;
use crate::tdlib::{TdClient, TdClientTrait};
use crossterm::event::KeyEvent;
/// Результат обработки клавиши
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyResult {
/// Клавиша обработана, продолжить работу
Handled,
/// Клавиша обработана, нужна перерисовка UI
HandledNeedsRedraw,
/// Клавиша не обработана (fallback на глобальные команды)
NotHandled,
/// Выход из приложения
Quit,
}
impl KeyResult {
/// Проверяет нужна ли перерисовка
pub fn needs_redraw(&self) -> bool {
matches!(self, KeyResult::HandledNeedsRedraw)
}
/// Проверяет был ли запрос выхода
pub fn should_quit(&self) -> bool {
matches!(self, KeyResult::Quit)
}
}
/// Trait для обработки клавиш на конкретном экране/в режиме
///
/// # Examples
///
/// ```ignore
/// struct ChatListHandler;
///
/// impl KeyHandler for ChatListHandler {
/// fn handle_key(
/// &self,
/// app: &mut App,
/// key: KeyEvent,
/// command: Option<Command>,
/// ) -> KeyResult {
/// match command {
/// Some(Command::MoveUp) => {
/// app.move_chat_selection_up();
/// KeyResult::HandledNeedsRedraw
/// }
/// Some(Command::OpenChat) => {
/// // Open selected chat
/// KeyResult::HandledNeedsRedraw
/// }
/// _ => KeyResult::NotHandled,
/// }
/// }
/// }
/// ```
pub trait KeyHandler {
/// Обрабатывает нажатие клавиши
///
/// # Arguments
///
/// * `app` - Mutable reference на состояние приложения
/// * `key` - Событие клавиши от crossterm
/// * `command` - Опциональная команда из keybindings (если привязана)
///
/// # Returns
///
/// `KeyResult` - результат обработки (обработана/не обработана/выход)
fn handle_key(
&self,
app: &mut App,
key: KeyEvent,
command: Option<Command>,
) -> KeyResult;
/// Приоритет обработчика (для цепочки обработчиков)
///
/// Обработчики с более высоким приоритетом вызываются первыми.
/// По умолчанию 0.
fn priority(&self) -> i32 {
0
}
}
/// Глобальный обработчик клавиш (работает на всех экранах)
pub struct GlobalKeyHandler;
impl KeyHandler for GlobalKeyHandler {
fn handle_key(
&self,
app: &mut App,
_key: KeyEvent,
command: Option<Command>,
) -> KeyResult {
match command {
Some(Command::Quit) => KeyResult::Quit,
Some(Command::OpenSearch) if !app.is_searching() => {
// TODO: implement enter_search_mode or use existing method
KeyResult::HandledNeedsRedraw
}
Some(Command::Cancel) => {
// Cancel различных режимов
if app.is_searching() {
// TODO: implement exit_search_mode or use existing method
KeyResult::HandledNeedsRedraw
} else {
KeyResult::NotHandled
}
}
_ => KeyResult::NotHandled,
}
}
fn priority(&self) -> i32 {
-100 // Низкий приоритет - fallback для всех экранов
}
}
/// Обработчик для списка чатов
pub struct ChatListKeyHandler;
impl KeyHandler for ChatListKeyHandler {
fn handle_key(
&self,
app: &mut App,
_key: KeyEvent,
command: Option<Command>,
) -> KeyResult {
match command {
Some(Command::MoveUp) => {
// TODO: implement chat selection navigation
// app.chat_list_state is ListState, use .select()
KeyResult::HandledNeedsRedraw
}
Some(Command::MoveDown) => {
// TODO: implement chat selection navigation
KeyResult::HandledNeedsRedraw
}
Some(Command::OpenChat) => {
// Обработка открытия чата будет в async контексте
// Здесь только возвращаем что команда распознана
KeyResult::HandledNeedsRedraw
}
// Папки 1-9
Some(Command::SelectFolder1) => {
app.set_selected_folder_id(Some(1));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder2) => {
app.set_selected_folder_id(Some(2));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder3) => {
app.set_selected_folder_id(Some(3));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder4) => {
app.set_selected_folder_id(Some(4));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder5) => {
app.set_selected_folder_id(Some(5));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder6) => {
app.set_selected_folder_id(Some(6));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder7) => {
app.set_selected_folder_id(Some(7));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder8) => {
app.set_selected_folder_id(Some(8));
KeyResult::HandledNeedsRedraw
}
Some(Command::SelectFolder9) => {
app.set_selected_folder_id(Some(9));
KeyResult::HandledNeedsRedraw
}
_ => KeyResult::NotHandled,
}
}
fn priority(&self) -> i32 {
10 // Средний приоритет
}
}
/// Обработчик для просмотра сообщений
pub struct MessageViewKeyHandler;
impl KeyHandler for MessageViewKeyHandler {
fn handle_key(
&self,
app: &mut App,
_key: KeyEvent,
command: Option<Command>,
) -> KeyResult {
match command {
Some(Command::MoveUp) => {
if app.message_view_state().message_scroll_offset > 0 {
app.message_view_state().message_scroll_offset -= 1;
KeyResult::HandledNeedsRedraw
} else {
KeyResult::Handled
}
}
Some(Command::MoveDown) => {
app.message_view_state().message_scroll_offset += 1;
KeyResult::HandledNeedsRedraw
}
Some(Command::PageUp) => {
app.message_view_state().message_scroll_offset = app.message_view_state().message_scroll_offset.saturating_sub(10);
KeyResult::HandledNeedsRedraw
}
Some(Command::PageDown) => {
app.message_view_state().message_scroll_offset += 10;
KeyResult::HandledNeedsRedraw
}
Some(Command::OpenSearchInChat) => {
// Открыть поиск в чате
KeyResult::HandledNeedsRedraw
}
Some(Command::OpenProfile) => {
// Открыть профиль
KeyResult::HandledNeedsRedraw
}
_ => KeyResult::NotHandled,
}
}
fn priority(&self) -> i32 {
10 // Средний приоритет
}
}
/// Обработчик для режима выбора сообщения
pub struct MessageSelectionKeyHandler;
impl KeyHandler for MessageSelectionKeyHandler {
fn handle_key(
&self,
_app: &mut App,
_key: KeyEvent,
command: Option<Command>,
) -> KeyResult {
match command {
Some(Command::DeleteMessage) => {
// Показать модалку подтверждения удаления
KeyResult::HandledNeedsRedraw
}
Some(Command::ReplyMessage) => {
// Войти в режим ответа
KeyResult::HandledNeedsRedraw
}
Some(Command::ForwardMessage) => {
// Войти в режим пересылки
KeyResult::HandledNeedsRedraw
}
Some(Command::CopyMessage) => {
// Скопировать текст в буфер
KeyResult::HandledNeedsRedraw
}
Some(Command::ReactMessage) => {
// Открыть emoji picker
KeyResult::HandledNeedsRedraw
}
Some(Command::Cancel) => {
// Выйти из режима выбора
KeyResult::HandledNeedsRedraw
}
_ => KeyResult::NotHandled,
}
}
fn priority(&self) -> i32 {
20 // Высокий приоритет - режимы должны обрабатываться первыми
}
}
/// Цепочка обработчиков клавиш
///
/// Позволяет комбинировать несколько обработчиков в порядке приоритета.
pub struct KeyHandlerChain {
handlers: Vec<(i32, Box<dyn KeyHandler>)>,
}
impl KeyHandlerChain {
/// Создаёт новую цепочку
pub fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
/// Добавляет обработчик в цепочку
pub fn add<H: KeyHandler + 'static>(mut self, handler: H) -> Self {
let priority = handler.priority();
self.handlers.push((priority, Box::new(handler)));
// Сортируем по убыванию приоритета
self.handlers.sort_by(|a, b| b.0.cmp(&a.0));
self
}
/// Обрабатывает клавишу, вызывая обработчики по порядку
///
/// Останавливается на первом обработчике, который вернул Handled/HandledNeedsRedraw/Quit
pub fn handle(
&self,
app: &mut App,
key: KeyEvent,
command: Option<Command>,
) -> KeyResult {
for (_priority, handler) in &self.handlers {
let result = handler.handle_key(app, key, command);
if result != KeyResult::NotHandled {
return result;
}
}
KeyResult::NotHandled
}
}
impl Default for KeyHandlerChain {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyCode;
#[test]
fn test_key_result_needs_redraw() {
assert!(!KeyResult::Handled.needs_redraw());
assert!(KeyResult::HandledNeedsRedraw.needs_redraw());
assert!(!KeyResult::NotHandled.needs_redraw());
assert!(!KeyResult::Quit.needs_redraw());
}
#[test]
fn test_key_result_should_quit() {
assert!(!KeyResult::Handled.should_quit());
assert!(!KeyResult::HandledNeedsRedraw.should_quit());
assert!(!KeyResult::NotHandled.should_quit());
assert!(KeyResult::Quit.should_quit());
}
// TODO: Enable these tests after App trait integration
// #[test]
// fn test_global_handler_quit() {
// let handler = GlobalKeyHandler;
// let mut app = App::new_for_test();
//
// let result = handler.handle_key(
// &mut app,
// KeyEvent::from(KeyCode::Char('q')),
// Some(Command::Quit),
// );
//
// assert_eq!(result, KeyResult::Quit);
// }
// #[test]
// fn test_chat_list_handler_navigation() {
// let handler = ChatListKeyHandler;
// let mut app = App::new_for_test();
//
// // Test move up (should be handled even at top)
// let result = handler.handle_key(
// &mut app,
// KeyEvent::from(KeyCode::Up),
// Some(Command::MoveUp),
// );
//
// assert_eq!(result, KeyResult::Handled);
// }
// #[test]
// fn test_handler_chain() {
// let chain = KeyHandlerChain::new()
// .add(ChatListKeyHandler)
// .add(GlobalKeyHandler);
//
// let mut app = App::new_for_test();
//
// // ChatListHandler should handle MoveUp first
// let result = chain.handle(
// &mut app,
// KeyEvent::from(KeyCode::Up),
// Some(Command::MoveUp),
// );
//
// assert_eq!(result, KeyResult::Handled);
//
// // GlobalHandler should handle Quit
// let result = chain.handle(
// &mut app,
// KeyEvent::from(KeyCode::Char('q')),
// Some(Command::Quit),
// );
//
// assert_eq!(result, KeyResult::Quit);
// }
#[test]
fn test_handler_priority() {
let handler1 = ChatListKeyHandler;
let handler2 = MessageSelectionKeyHandler;
let handler3 = GlobalKeyHandler;
assert_eq!(handler1.priority(), 10);
assert_eq!(handler2.priority(), 20);
assert_eq!(handler3.priority(), -100);
// В цепочке должны быть отсортированы: MessageSelection > ChatList > Global
}
}

View File

@@ -280,15 +280,13 @@ impl MessageManager {
///
/// * `chat_id` - ID чата
///
/// # Note
/// # Compatibility
///
/// TODO: В tdlib-rs 1.8.29 поле `pinned_message_id` было удалено из `Chat`.
/// Нужно использовать `getChatPinnedMessage` или альтернативный способ.
/// Временно отключено, возвращает `None`.
/// The current `tdlib-rs` schema no longer exposes `Chat.pinned_message_id`, and the
/// generated wrapper does not provide `getChatPinnedMessage`. The pinned-message modal
/// uses `get_pinned_messages` with `SearchMessagesFilter::Pinned`; this method keeps the
/// legacy single-header state empty until TDLib exposes a direct top-pinned-message API.
pub async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {
// TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat.
// Нужно использовать getChatPinnedMessage или альтернативный способ.
// Временно отключено.
self.current_pinned_message = None;
}

View File

@@ -1,7 +1,7 @@
use crate::types::{ChatId, MessageId};
use tdlib_rs::enums::ReactionType;
use tdlib_rs::enums::{AvailableReactions, ReactionType};
use tdlib_rs::functions;
use tdlib_rs::types::ReactionTypeEmoji;
use tdlib_rs::types::{AvailableReaction, ReactionTypeEmoji};
/// Менеджер реакций на сообщения.
///
@@ -49,11 +49,6 @@ impl ReactionManager {
/// * `Ok(Vec<String>)` - Список доступных emoji реакций
/// * `Err(String)` - Ошибка получения
///
/// # Note
///
/// В tdlib-rs 1.8.29 структура AvailableReactions изменилась.
/// Временно возвращается стандартный набор из 12 популярных реакций.
///
/// # Examples
///
/// ```ignore
@@ -86,54 +81,15 @@ impl ReactionManager {
.await;
match reactions_result {
Ok(_available) => {
// TODO: В tdlib-rs 1.8.29 структура AvailableReactions изменилась
// Временно используем fallback на стандартные реакции
let emojis: Vec<String> = Vec::new();
// let emojis: Vec<String> = if let tdlib_rs::enums::AvailableReactions::AvailableReactions(ar) = available {
// ar.top_reactions.iter().filter_map(...).collect()
// } else {
// Vec::new()
// };
Ok(available) => {
let emojis = available_reaction_emojis(&available);
if emojis.is_empty() {
// Фолбек на стандартные реакции
Ok(vec![
"👍".to_string(),
"👎".to_string(),
"❤️".to_string(),
"🔥".to_string(),
"😊".to_string(),
"😢".to_string(),
"😮".to_string(),
"🎉".to_string(),
"🤔".to_string(),
"😡".to_string(),
"😎".to_string(),
"🤝".to_string(),
])
Ok(default_reaction_emojis())
} else {
Ok(emojis)
}
}
Err(_) => {
// В случае ошибки возвращаем стандартный набор
Ok(vec![
"👍".to_string(),
"👎".to_string(),
"❤️".to_string(),
"🔥".to_string(),
"😊".to_string(),
"😢".to_string(),
"😮".to_string(),
"🎉".to_string(),
"🤔".to_string(),
"😡".to_string(),
"😎".to_string(),
"🤝".to_string(),
])
}
Err(_) => Ok(default_reaction_emojis()),
}
}
@@ -196,3 +152,79 @@ impl ReactionManager {
}
}
}
fn default_reaction_emojis() -> Vec<String> {
vec![
"👍".to_string(),
"👎".to_string(),
"❤️".to_string(),
"🔥".to_string(),
"😊".to_string(),
"😢".to_string(),
"😮".to_string(),
"🎉".to_string(),
"🤔".to_string(),
"😡".to_string(),
"😎".to_string(),
"🤝".to_string(),
]
}
fn available_reaction_emojis(available: &AvailableReactions) -> Vec<String> {
let AvailableReactions::AvailableReactions(available) = available;
available
.top_reactions
.iter()
.chain(available.recent_reactions.iter())
.chain(available.popular_reactions.iter())
.filter_map(reaction_emoji)
.fold(Vec::new(), |mut emojis, emoji| {
if !emojis.contains(&emoji) {
emojis.push(emoji);
}
emojis
})
}
fn reaction_emoji(reaction: &AvailableReaction) -> Option<String> {
match &reaction.r#type {
ReactionType::Emoji(emoji) => Some(emoji.emoji.clone()),
ReactionType::CustomEmoji(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use tdlib_rs::types::{AvailableReaction, AvailableReactions as AvailableReactionsData};
fn emoji_reaction(emoji: &str) -> AvailableReaction {
AvailableReaction {
r#type: ReactionType::Emoji(ReactionTypeEmoji { emoji: emoji.to_string() }),
needs_premium: false,
}
}
#[test]
fn extracts_unique_emoji_reactions_in_display_order() {
let available = AvailableReactions::AvailableReactions(AvailableReactionsData {
top_reactions: vec![emoji_reaction("👍"), emoji_reaction("🔥")],
recent_reactions: vec![emoji_reaction("🔥"), emoji_reaction("❤️")],
popular_reactions: vec![emoji_reaction("🎉")],
allow_custom_emoji: false,
are_tags: false,
unavailability_reason: None,
});
assert_eq!(
available_reaction_emojis(&available),
vec![
"👍".to_string(),
"🔥".to_string(),
"❤️".to_string(),
"🎉".to_string(),
]
);
}
}