Encapsulate TDLib state mutations

This commit is contained in:
Mikhail Kilin
2026-05-17 18:25:18 +03:00
parent d8af6a76a1
commit 6b27cbece9
6 changed files with 237 additions and 147 deletions

View File

@@ -104,11 +104,11 @@ rg -n "current_chat_messages_mut|chats_mut|folders_mut|pending_user_ids_mut|user
Steps:
- [ ] Add focused methods on `TdClient` for common mutations: update chat, update message by id, queue pending user, update user cache, update folders.
- [ ] Replace raw `*_mut()` usage in helper/update modules with those methods.
- [ ] Keep raw mutable access private to `TdClient` implementation where it is still needed.
- [ ] Add or update tests around message updates, user-cache updates, and chat-list updates.
- [ ] Run `cargo test --all-features`.
- [x] Add focused methods on `TdClient` for common mutations: update chat, update message by id, queue pending user, update user cache, update folders.
- [x] Replace raw `*_mut()` usage in helper/update modules with those methods.
- [x] Keep raw mutable access private to `TdClient` implementation where it is still needed.
- [x] Add or update tests around message updates, user-cache updates, and chat-list updates.
- [x] Run `cargo test --all-features`.
Acceptance criteria:

View File

@@ -10,19 +10,12 @@ use tdlib_rs::enums::{Chat as TdChat, ChatList, ChatType};
use super::client::TdClient;
use super::types::ChatInfo;
/// Находит мутабельную ссылку на чат по ID.
pub fn find_chat_mut(client: &mut TdClient, chat_id: ChatId) -> Option<&mut ChatInfo> {
client.chats_mut().iter_mut().find(|c| c.id == chat_id)
}
/// Обновляет поле чата, если чат найден.
pub fn update_chat<F>(client: &mut TdClient, chat_id: ChatId, updater: F)
where
F: FnOnce(&mut ChatInfo),
{
if let Some(chat) = find_chat_mut(client, chat_id) {
updater(chat);
}
client.update_chat(chat_id, updater);
}
/// Добавляет новый чат или обновляет существующий
@@ -33,9 +26,7 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) {
// Пропускаем удалённые аккаунты
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
// Удаляем из списка если уже был добавлен
client
.chats_mut()
.retain(|c| c.id != ChatId::new(td_chat.id));
client.remove_chat(ChatId::new(td_chat.id));
return;
}
@@ -61,22 +52,23 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) {
ChatType::Private(private) => {
// Ограничиваем размер chat_user_ids
let chat_id = ChatId::new(td_chat.id);
if client.user_cache.chat_user_ids.len() >= MAX_CHAT_USER_IDS
&& !client.user_cache.chat_user_ids.contains_key(&chat_id)
{
// Удаляем случайную запись (первую найденную)
if let Some(&key) = client.user_cache.chat_user_ids.keys().next() {
client.user_cache.chat_user_ids.remove(&key);
}
}
let user_id = UserId::new(private.user_id);
client.user_cache.chat_user_ids.insert(chat_id, user_id);
// Проверяем, есть ли уже username в кэше (peek не обновляет LRU)
client
.user_cache
.user_usernames
.peek(&user_id)
.map(|u| format!("@{}", u))
client.update_user_cache(|cache| {
if cache.chat_user_ids.len() >= MAX_CHAT_USER_IDS
&& !cache.chat_user_ids.contains_key(&chat_id)
{
// Удаляем случайную запись (первую найденную)
if let Some(&key) = cache.chat_user_ids.keys().next() {
cache.chat_user_ids.remove(&key);
}
}
cache.chat_user_ids.insert(chat_id, user_id);
// Проверяем, есть ли уже username в кэше (peek не обновляет LRU)
cache
.user_usernames
.peek(&user_id)
.map(|u| format!("@{}", u))
})
}
_ => None,
};
@@ -110,44 +102,35 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) {
draft_text: None,
};
if let Some(existing) = find_chat_mut(client, ChatId::new(td_chat.id)) {
existing.title = chat_info.title;
existing.last_message = chat_info.last_message;
existing.last_message_date = chat_info.last_message_date;
existing.unread_count = chat_info.unread_count;
existing.unread_mention_count = chat_info.unread_mention_count;
existing.last_read_outbox_message_id = chat_info.last_read_outbox_message_id;
existing.folder_ids = chat_info.folder_ids;
existing.is_muted = chat_info.is_muted;
let chat_info_for_update = chat_info.clone();
let updated_existing = client.update_chat(ChatId::new(td_chat.id), |existing| {
existing.title = chat_info_for_update.title;
existing.last_message = chat_info_for_update.last_message;
existing.last_message_date = chat_info_for_update.last_message_date;
existing.unread_count = chat_info_for_update.unread_count;
existing.unread_mention_count = chat_info_for_update.unread_mention_count;
existing.last_read_outbox_message_id = chat_info_for_update.last_read_outbox_message_id;
existing.folder_ids = chat_info_for_update.folder_ids;
existing.is_muted = chat_info_for_update.is_muted;
// Обновляем username если он появился
if let Some(username) = chat_info.username {
if let Some(username) = chat_info_for_update.username {
existing.username = Some(username);
}
// Обновляем позицию только если она пришла
if main_position.is_some() {
existing.is_pinned = chat_info.is_pinned;
existing.order = chat_info.order;
existing.is_pinned = chat_info_for_update.is_pinned;
existing.order = chat_info_for_update.order;
}
} else {
client.chats_mut().push(chat_info);
});
if !updated_existing {
client.push_chat(chat_info);
// Ограничиваем количество чатов
if client.chats_mut().len() > MAX_CHATS {
// Удаляем чат с наименьшим order (наименее активный)
let Some(min_idx) = client
.chats()
.iter()
.enumerate()
.min_by_key(|(_, c)| c.order)
.map(|(i, _)| i)
else {
return; // Нет чатов для удаления (не должно произойти)
};
client.chats_mut().remove(min_idx);
}
client.trim_chats_to_max_by_order(MAX_CHATS);
}
// Сортируем чаты по order (TDLib order учитывает pinned и время)
client.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
client.sort_chats_by_order();
}

View File

@@ -105,7 +105,8 @@ impl TdClient {
self.notification_manager.set_enabled(config.enabled);
self.notification_manager
.set_only_mentions(config.only_mentions);
self.notification_manager.set_show_preview(config.show_preview);
self.notification_manager
.set_show_preview(config.show_preview);
self.notification_manager.set_timeout(config.timeout_ms);
self.notification_manager
.set_urgency(config.urgency.clone());
@@ -433,24 +434,117 @@ impl TdClient {
&self.chat_manager.chats
}
pub fn chats_mut(&mut self) -> &mut Vec<ChatInfo> {
&mut self.chat_manager.chats
pub fn update_chats<F, R>(&mut self, updater: F) -> R
where
F: FnOnce(&mut Vec<ChatInfo>) -> R,
{
updater(&mut self.chat_manager.chats)
}
pub fn update_chat<F>(&mut self, chat_id: ChatId, updater: F) -> bool
where
F: FnOnce(&mut ChatInfo),
{
let Some(chat) = self.chat_manager.chats.iter_mut().find(|c| c.id == chat_id) else {
return false;
};
updater(chat);
true
}
pub fn remove_chat(&mut self, chat_id: ChatId) {
self.chat_manager.chats.retain(|c| c.id != chat_id);
}
pub fn push_chat(&mut self, chat: ChatInfo) {
self.chat_manager.chats.push(chat);
}
pub fn trim_chats_to_max_by_order(&mut self, max_chats: usize) {
if self.chat_manager.chats.len() <= max_chats {
return;
}
let Some(min_idx) = self
.chat_manager
.chats
.iter()
.enumerate()
.min_by_key(|(_, chat)| chat.order)
.map(|(idx, _)| idx)
else {
return;
};
self.chat_manager.chats.remove(min_idx);
}
pub fn sort_chats_by_order(&mut self) {
self.chat_manager
.chats
.sort_by(|a, b| b.order.cmp(&a.order));
}
pub fn folders(&self) -> &[FolderInfo] {
&self.chat_manager.folders
}
pub fn folders_mut(&mut self) -> &mut Vec<FolderInfo> {
&mut self.chat_manager.folders
pub fn update_folders<F, R>(&mut self, updater: F) -> R
where
F: FnOnce(&mut Vec<FolderInfo>) -> R,
{
updater(&mut self.chat_manager.folders)
}
pub fn set_folders(&mut self, folders: Vec<FolderInfo>) {
self.chat_manager.folders = folders;
}
pub fn current_chat_messages(&self) -> &[MessageInfo] {
&self.message_manager.current_chat_messages
}
pub fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo> {
&mut self.message_manager.current_chat_messages
pub fn clear_current_chat_messages(&mut self) {
self.message_manager.current_chat_messages.clear();
}
pub fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>) {
self.message_manager.current_chat_messages = messages;
}
pub fn update_current_chat_messages<F, R>(&mut self, updater: F) -> R
where
F: FnOnce(&mut Vec<MessageInfo>) -> R,
{
updater(&mut self.message_manager.current_chat_messages)
}
pub fn update_current_chat_message<F>(&mut self, message_id: MessageId, updater: F) -> bool
where
F: FnOnce(&mut MessageInfo),
{
let Some(message) = self
.message_manager
.current_chat_messages
.iter_mut()
.find(|message| message.id() == message_id)
else {
return false;
};
updater(message);
true
}
pub fn replace_current_chat_message(
&mut self,
message_id: MessageId,
new_message: MessageInfo,
) -> bool {
self.update_current_chat_message(message_id, |message| {
*message = new_message;
})
}
pub fn current_chat_id(&self) -> Option<ChatId> {
@@ -498,8 +592,10 @@ impl TdClient {
&self.user_cache.pending_user_ids
}
pub fn pending_user_ids_mut(&mut self) -> &mut Vec<crate::types::UserId> {
&mut self.user_cache.pending_user_ids
pub fn queue_pending_user_id(&mut self, user_id: crate::types::UserId) {
if !self.user_cache.pending_user_ids.contains(&user_id) {
self.user_cache.pending_user_ids.push(user_id);
}
}
pub fn main_chat_list_position(&self) -> i32 {
@@ -515,8 +611,11 @@ impl TdClient {
&self.user_cache
}
pub fn user_cache_mut(&mut self) -> &mut UserCache {
&mut self.user_cache
pub fn update_user_cache<F, R>(&mut self, updater: F) -> R
where
F: FnOnce(&mut UserCache) -> R,
{
updater(&mut self.user_cache)
}
// ==================== Helper методы для упрощения обработки updates ====================
@@ -558,7 +657,7 @@ impl TdClient {
}
// Пересортируем по order
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
self.sort_chats_by_order();
}
Update::ChatReadInbox(update) => {
crate::tdlib::chat_helpers::update_chat(
@@ -600,11 +699,13 @@ impl TdClient {
);
// Если это текущий открытый чат — обновляем is_read у сообщений
if Some(ChatId::new(update.chat_id)) == self.current_chat_id() {
for msg in self.current_chat_messages_mut().iter_mut() {
if msg.is_outgoing() && msg.id() <= last_read_msg_id {
msg.state.is_read = true;
self.update_current_chat_messages(|messages| {
for msg in messages {
if msg.is_outgoing() && msg.id() <= last_read_msg_id {
msg.state.is_read = true;
}
}
}
});
}
}
Update::ChatPosition(update) => {
@@ -618,11 +719,13 @@ impl TdClient {
}
Update::ChatFolders(update) => {
// Обновляем список папок
*self.folders_mut() = update
.chat_folders
.into_iter()
.map(|f| FolderInfo { id: f.id, name: f.title })
.collect();
self.set_folders(
update
.chat_folders
.into_iter()
.map(|f| FolderInfo { id: f.id, name: f.title })
.collect(),
);
self.set_main_chat_list_position(update.main_chat_list_position);
}
Update::UserStatus(update) => {
@@ -635,9 +738,11 @@ impl TdClient {
UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth,
UserStatus::Empty => UserOnlineStatus::LongTimeAgo,
};
self.user_cache
.user_statuses
.insert(UserId::new(update.user_id), status);
self.update_user_cache(|cache| {
cache
.user_statuses
.insert(UserId::new(update.user_id), status);
});
}
Update::ConnectionState(update) => {
// Обновляем состояние сетевого соединения

View File

@@ -70,14 +70,14 @@ impl ChatClient for TdClient {
where
F: FnOnce(&mut Vec<ChatInfo>),
{
updater(self.chats_mut());
TdClient::update_chats(self, updater);
}
fn update_folders<F>(&mut self, updater: F)
where
F: FnOnce(&mut Vec<FolderInfo>),
{
updater(self.folders_mut());
TdClient::update_folders(self, updater);
}
}
@@ -204,18 +204,18 @@ impl MessageClient for TdClient {
}
fn clear_current_chat_messages(&mut self) {
self.current_chat_messages_mut().clear()
TdClient::clear_current_chat_messages(self)
}
fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>) {
*self.current_chat_messages_mut() = messages;
TdClient::set_current_chat_messages(self, messages);
}
fn update_current_chat_messages<F>(&mut self, updater: F)
where
F: FnOnce(&mut Vec<MessageInfo>),
{
updater(self.current_chat_messages_mut());
TdClient::update_current_chat_messages(self, updater);
}
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
@@ -253,7 +253,7 @@ impl UserClient for TdClient {
where
F: FnOnce(&mut UserCache),
{
updater(self.user_cache_mut());
TdClient::update_user_cache(self, updater);
}
async fn process_pending_user_ids(&mut self) {

View File

@@ -23,9 +23,7 @@ pub fn convert_message(client: &mut TdClient, message: &TdMessage, chat_id: Chat
.cloned()
.unwrap_or_else(|| {
// Добавляем в очередь для загрузки
if !client.pending_user_ids().contains(&user_id) {
client.pending_user_ids_mut().push(user_id);
}
client.queue_pending_user_id(user_id);
format!("User_{}", user_id.as_i64())
})
}
@@ -210,25 +208,27 @@ pub fn update_reply_info_from_loaded_messages(client: &mut TdClient) {
.collect();
// Обновляем reply_to для сообщений с неполными данными
for msg in client.current_chat_messages_mut().iter_mut() {
let Some(ref mut reply) = msg.interactions.reply_to else {
continue;
};
client.update_current_chat_messages(|messages| {
for msg in messages {
let Some(ref mut reply) = msg.interactions.reply_to else {
continue;
};
// Если sender_name = "..." или text пустой — пробуем заполнить
if reply.sender_name != "..." && !reply.text.is_empty() {
continue;
}
// Если sender_name = "..." или text пустой — пробуем заполнить
if reply.sender_name != "..." && !reply.text.is_empty() {
continue;
}
let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) else {
continue;
};
let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) else {
continue;
};
if reply.sender_name == "..." {
reply.sender_name = sender.clone();
if reply.sender_name == "..." {
reply.sender_name = sender.clone();
}
if reply.text.is_empty() {
reply.text = content.clone();
}
}
if reply.text.is_empty() {
reply.text = content.clone();
}
}
});
}

View File

@@ -54,17 +54,19 @@ pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessag
Some(idx) => {
// Сообщение уже есть - обновляем
if is_incoming {
client.current_chat_messages_mut()[idx] = msg_info;
client.replace_current_chat_message(msg_id, msg_info);
} else {
// Для исходящих: обновляем can_be_edited и другие поля,
// но сохраняем reply_to (добавленный при отправке)
let existing = &mut client.current_chat_messages_mut()[idx];
existing.state.can_be_edited = msg_info.state.can_be_edited;
existing.state.can_be_deleted_only_for_self =
msg_info.state.can_be_deleted_only_for_self;
existing.state.can_be_deleted_for_all_users =
msg_info.state.can_be_deleted_for_all_users;
existing.state.is_read = msg_info.state.is_read;
client.update_current_chat_messages(|messages| {
let existing = &mut messages[idx];
existing.state.can_be_edited = msg_info.state.can_be_edited;
existing.state.can_be_deleted_only_for_self =
msg_info.state.can_be_deleted_only_for_self;
existing.state.can_be_deleted_for_all_users =
msg_info.state.can_be_deleted_for_all_users;
existing.state.is_read = msg_info.state.is_read;
});
}
}
None => {
@@ -122,7 +124,7 @@ pub fn handle_chat_position_update(client: &mut TdClient, update: UpdateChatPosi
ChatList::Main => {
if update.position.order == 0 {
// Чат больше не в Main (перемещён в архив и т.д.)
client.chats_mut().retain(|c| c.id != chat_id);
client.remove_chat(chat_id);
} else {
// Обновляем позицию существующего чата
crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| {
@@ -131,7 +133,7 @@ pub fn handle_chat_position_update(client: &mut TdClient, update: UpdateChatPosi
});
}
// Пересортируем по order
client.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
client.sort_chats_by_order();
}
ChatList::Folder(folder) => {
// Обновляем folder_ids для чата
@@ -166,10 +168,10 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) {
// Удаляем чаты с этим пользователем из списка
let user_id = user.id;
// Clone chat_user_ids to avoid borrow conflict
let chat_user_ids = client.user_cache.chat_user_ids.clone();
client
.chats_mut()
.retain(|c| chat_user_ids.get(&c.id) != Some(&UserId::new(user_id)));
let chat_user_ids = client.user_cache().chat_user_ids.clone();
client.update_chats(|chats| {
chats.retain(|c| chat_user_ids.get(&c.id) != Some(&UserId::new(user_id)));
});
return;
}
@@ -179,10 +181,9 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) {
} else {
format!("{} {}", user.first_name, user.last_name)
};
client
.user_cache
.user_names
.insert(UserId::new(user.id), display_name);
client.update_user_cache(|cache| {
cache.user_names.insert(UserId::new(user.id), display_name);
});
// Сохраняем username если есть (с упрощённым извлечением через and_then)
if let Some(username) = user
@@ -190,17 +191,23 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) {
.as_ref()
.and_then(|u| u.active_usernames.first())
{
client
.user_cache
.user_usernames
.insert(UserId::new(user.id), username.to_string());
let affected_chat_ids = client.update_user_cache(|cache| {
cache
.user_usernames
.insert(UserId::new(user.id), username.to_string());
cache
.chat_user_ids
.iter()
.filter_map(|(&chat_id, &user_id)| {
(user_id == UserId::new(user.id)).then_some(chat_id)
})
.collect::<Vec<_>>()
});
// Обновляем username в чатах, связанных с этим пользователем
for (&chat_id, &user_id) in &client.user_cache.chat_user_ids.clone() {
if user_id == UserId::new(user.id) {
crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| {
chat.username = Some(format!("@{}", username));
});
}
for chat_id in affected_chat_ids {
crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| {
chat.username = Some(format!("@{}", username));
});
}
}
// LRU-кэш автоматически удаляет старые записи при вставке
@@ -218,16 +225,8 @@ pub fn handle_message_interaction_info_update(
return;
}
let Some(msg) = client
.current_chat_messages_mut()
.iter_mut()
.find(|m| m.id() == MessageId::new(update.message_id))
else {
return;
};
// Извлекаем реакции из interaction_info
msg.interactions.reactions = update
let reactions = update
.interaction_info
.as_ref()
.and_then(|info| info.reactions.as_ref())
@@ -250,6 +249,9 @@ pub fn handle_message_interaction_info_update(
.collect()
})
.unwrap_or_default();
client.update_current_chat_message(MessageId::new(update.message_id), |msg| {
msg.interactions.reactions = reactions;
});
}
/// Обрабатывает Update::MessageSendSucceeded - успешная отправка сообщения.
@@ -291,7 +293,7 @@ pub fn handle_message_send_succeeded_update(
}
// Заменяем старое сообщение на новое
client.current_chat_messages_mut()[idx] = new_msg;
client.replace_current_chat_message(old_id, new_msg);
}
/// Обрабатывает Update::ChatDraftMessage - обновление черновика сообщения в чате.