#include "app.h" #include #include #include "util.h" namespace telegram_tui { namespace { std::string format_download_progress(std::int64_t downloaded_size, std::int64_t size_bytes, bool is_downloaded) { if (is_downloaded) { return "100%"; } if (size_bytes > 0) { const auto downloaded = std::min(downloaded_size, size_bytes); return std::to_string(static_cast((downloaded * 100) / size_bytes)) + "% " + format_file_size(downloaded) + "/" + format_file_size(size_bytes); } return format_file_size(downloaded_size); } } // namespace bool App::process_updates() { bool changed = refresh_update_check_result(); while (true) { auto update = td_.receive(0.0); if (!update.has_value()) { break; } changed = true; handle_td_object(*update); } return changed; } void App::request_saved_animations(bool force) { if (saved_animations_loading_ && !force) { return; } saved_animations_loading_ = true; td_.send({ {"@type", "getSavedAnimations"}, {"@extra", "saved_animations"}, }); if (!saved_animation_menu_open_) { status_line_ = "Loading saved GIFs..."; } } void App::sync_saved_animations(const json &animations) { saved_animations_.clear(); if (!animations.is_array()) { saved_animations_loading_ = false; saved_animations_loaded_ = true; return; } for (const auto &item : animations) { if (!item.is_object()) { continue; } const json file = item.value("animation", json::object()); const std::int32_t file_id = safe_i32(file, "id"); if (file_id == 0) { continue; } SavedAnimationInfo animation; animation.file_id = file_id; animation.name = safe_string(item, "file_name"); if (animation.name.empty()) { animation.name = "animation"; } animation.mime_type = safe_string(item, "mime_type"); animation.size_bytes = std::max(safe_i64(file, "size"), safe_i64(file, "expected_size")); animation.downloaded_size = safe_i64(file.value("local", json::object()), "downloaded_size"); animation.duration = safe_i32(item, "duration"); animation.width = safe_i32(item, "width"); animation.height = safe_i32(item, "height"); animation.local_path = safe_string(file.value("local", json::object()), "path"); animation.is_downloading_active = file.value("local", json::object()).value("is_downloading_active", false); animation.can_be_downloaded = file.value("local", json::object()).value("can_be_downloaded", false); animation.can_be_deleted = file.value("local", json::object()).value("can_be_deleted", false); animation.is_downloaded = file.value("local", json::object()).value("is_downloading_completed", false); saved_animations_.push_back(std::move(animation)); } if (saved_animation_selection_index_ < 0) { saved_animation_selection_index_ = 0; } if (saved_animation_selection_index_ >= static_cast(saved_animations_.size())) { saved_animation_selection_index_ = std::max(0, static_cast(saved_animations_.size()) - 1); } saved_animations_loading_ = false; saved_animations_loaded_ = true; ensure_saved_animation_preview(); } void App::ensure_saved_animation_preview() { if (saved_animation_selection_index_ < 0 || saved_animation_selection_index_ >= static_cast(saved_animations_.size())) { return; } SavedAnimationInfo &animation = saved_animations_[static_cast(saved_animation_selection_index_)]; if (animation.file_id == 0 || animation.is_downloaded || animation.is_downloading_active || !animation.can_be_downloaded) { return; } td_.send({ {"@type", "downloadFile"}, {"file_id", animation.file_id}, {"priority", 1}, {"offset", 0}, {"limit", 0}, {"synchronous", false}, }); } void App::handle_td_object(const json &object) { const std::string type = safe_string(object, "@type"); if (type == "updateAuthorizationState") { authorization_state_ = object.value("authorization_state", json::object()); handle_authorization_state(); return; } if (type == "updateConnectionState") { status_line_ = "Connection: " + safe_string(object.at("state"), "@type"); return; } if (type == "updateChatOnlineMemberCount") { ChatInfo &chat = chats_[safe_i64(object, "chat_id")]; chat.id = safe_i64(object, "chat_id"); chat.online_member_count = safe_i32(object, "online_member_count"); chat.has_online_member_count = true; return; } if (type == "updateUser") { upsert_user(object.value("user", json::object())); return; } if (type == "updateUserStatus") { update_user_status(safe_i64(object, "user_id"), object.value("status", json::object())); return; } if (type == "updateBasicGroup" || type == "basicGroup") { upsert_basic_group(type == "basicGroup" ? object : object.value("basic_group", json::object())); return; } if (type == "updateSupergroup" || type == "supergroup") { upsert_supergroup( type == "supergroup" ? object : object.value("supergroup", json::object())); return; } if (type == "updateNewChat") { upsert_chat(object.value("chat", json::object())); return; } if (type == "updateChatTitle") { ChatInfo &chat = chats_[safe_i64(object, "chat_id")]; chat.id = safe_i64(object, "chat_id"); chat.title = safe_string(object, "title"); resort_chats(); return; } if (type == "updateChatPosition") { ChatInfo &chat = chats_[safe_i64(object, "chat_id")]; chat.id = safe_i64(object, "chat_id"); apply_chat_position(chat, object.value("position", json::object())); resort_chats(); return; } if (type == "updateChatLastMessage") { ChatInfo &chat = chats_[safe_i64(object, "chat_id")]; chat.id = safe_i64(object, "chat_id"); chat.last_message_preview = preview_message(object.value("last_message", json::object())); if (object.contains("positions") && object.at("positions").is_array()) { chat.in_main_list = false; chat.main_order = 0; for (const auto &position : object.at("positions")) { apply_chat_position(chat, position); } } resort_chats(); return; } if (type == "updateChatReadInbox") { ChatInfo &chat = chats_[safe_i64(object, "chat_id")]; chat.id = safe_i64(object, "chat_id"); chat.unread_count = safe_i32(object, "unread_count"); chat.last_read_inbox_message_id = safe_i64(object, "last_read_inbox_message_id"); return; } if (type == "updateChatReadOutbox") { ChatInfo &chat = chats_[safe_i64(object, "chat_id")]; chat.id = safe_i64(object, "chat_id"); chat.last_read_outbox_message_id = safe_i64(object, "last_read_outbox_message_id"); return; } if (type == "updateNewMessage") { const json message = object.value("message", json::object()); const std::int64_t chat_id = safe_i64(message, "chat_id"); if (chat_id == open_chat_id_ && message_scroll_ > 0) { ++message_scroll_; } const MessageInfo parsed = parse_message(message); append_message(chat_id, parsed); chats_[chat_id].last_message_preview = preview_message(message); if (chat_id == open_chat_id_ && !parsed.is_outgoing) { mark_message_as_read(chat_id, parsed.id); } return; } if (type == "updateMessageContent") { const std::int64_t chat_id = safe_i64(object, "chat_id"); const std::int64_t message_id = safe_i64(object, "message_id"); auto chat_it = chats_.find(chat_id); if (chat_it == chats_.end()) { return; } for (auto &message : chat_it->second.messages) { if (message.id == message_id) { const json content = object.value("new_content", json::object()); message.text = content_to_text(content, true); if (const auto attachment = parse_attachment(content); attachment.has_value()) { message.has_attachment = true; message.attachment = *attachment; } else { message.has_attachment = false; } break; } } return; } if (type == "updateDeleteMessages") { const std::int64_t chat_id = safe_i64(object, "chat_id"); auto chat_it = chats_.find(chat_id); if (chat_it == chats_.end()) { return; } std::set deleted; for (const auto &id : object.value("message_ids", json::array())) { if (id.is_number_integer()) { deleted.insert(id.get()); } } std::vector kept; kept.reserve(chat_it->second.messages.size()); for (const auto &message : chat_it->second.messages) { if (deleted.find(message.id) == deleted.end()) { kept.push_back(message); } } chat_it->second.messages = std::move(kept); return; } if (type == "updateFile") { update_attachment_file(object.value("file", json::object())); return; } if (type == "updateSavedAnimations") { request_saved_animations(true); return; } if (type == "updateOption" || type == "ok" || type == "userFullInfo" || type == "updateHavePendingNotifications" || type == "updateUnreadMessageCount" || type == "updateUnreadChatCount") { return; } if (type == "error") { const std::string extra = safe_string(object, "@extra"); const std::string message = safe_string(object, "message"); if (extra.rfind("history:", 0) == 0) { const auto chat_id = static_cast(std::stoll(extra.substr(8))); chats_[chat_id].history_loading = false; status_line_ = "History unavailable: " + message; return; } status_line_ = "TDLib error: " + message; return; } if (type == "user") { upsert_user(object); return; } if (type == "chat") { upsert_chat(object); return; } if (type == "chats") { const std::string extra = safe_string(object, "@extra"); if (extra == "main_chats") { sync_chat_ids_from_response(object); } return; } if (type == "messages") { const std::string extra = safe_string(object, "@extra"); if (extra.rfind("history:", 0) == 0) { const auto chat_id = static_cast(std::stoll(extra.substr(8))); merge_history(chat_id, object.value("messages", json::array())); } return; } if (type == "animations") { const std::string extra = safe_string(object, "@extra"); if (extra == "saved_animations") { sync_saved_animations(object.value("animations", json::array())); if (saved_animation_menu_open_) { status_line_ = saved_animations_.empty() ? "No saved GIFs." : "Saved GIFs."; } } return; } if (type == "updateMessageSendSucceeded") { const json message = object.value("message", json::object()); const std::int64_t chat_id = safe_i64(message, "chat_id"); remove_message(chat_id, safe_i64(object, "old_message_id")); append_message(chat_id, parse_message(message)); chats_[chat_id].last_message_preview = preview_message(message); status_line_ = "Message sent."; return; } const std::string extra = safe_string(object, "@extra"); if (extra == "getMe" && object.is_object()) { my_user_id_ = safe_i64(object, "id"); } } void App::request_more_chats() { td_.send({ {"@type", "getChats"}, {"chat_list", {{"@type", "chatListMain"}}}, {"limit", kChatPageSize}, {"@extra", "main_chats"}, }); status_line_ = "Loading chats..."; } bool App::request_chat_history(std::int64_t chat_id, bool force) { ChatInfo &chat = chats_[chat_id]; chat.id = chat_id; if (chat.history_loading && !force) { return false; } chat.history_loading = true; td_.send({ {"@type", "getChatHistory"}, {"chat_id", chat_id}, {"from_message_id", 0}, {"offset", 0}, {"limit", kHistoryBatchSize}, {"only_local", false}, {"@extra", "history:" + std::to_string(chat_id)}, }); return true; } bool App::request_open_chat_history(bool force) { const auto chat_id = open_chat_id(); if (!chat_id.has_value()) { return false; } request_chat_details(*chat_id); const bool requested = request_chat_history(*chat_id, force); if (requested) { status_line_ = force ? "Reloading chat history..." : "Loading chat history..."; } return requested; } void App::mark_chat_messages_as_read(std::int64_t chat_id) { auto chat_it = chats_.find(chat_id); if (chat_it == chats_.end()) { return; } std::vector unread_message_ids; unread_message_ids.reserve(chat_it->second.messages.size()); for (const auto &message : chat_it->second.messages) { if (message.is_outgoing || message.id == 0) { continue; } if (message.id <= chat_it->second.last_read_inbox_message_id) { continue; } unread_message_ids.push_back(message.id); } if (!unread_message_ids.empty()) { td_.send({ {"@type", "viewMessages"}, {"chat_id", chat_id}, {"message_ids", unread_message_ids}, {"source", {{"@type", "messageSourceChatHistory"}}}, {"force_read", true}, }); } td_.send({ {"@type", "toggleChatIsMarkedAsUnread"}, {"chat_id", chat_id}, {"is_marked_as_unread", false}, }); } void App::mark_message_as_read(std::int64_t chat_id, std::int64_t message_id) { if (chat_id == 0 || message_id == 0) { return; } td_.send({ {"@type", "viewMessages"}, {"chat_id", chat_id}, {"message_ids", json::array({message_id})}, {"source", {{"@type", "messageSourceChatHistory"}}}, {"force_read", true}, }); } void App::set_open_chat(std::int64_t chat_id) { if (chat_id == 0) { return; } const bool changed_chat = open_chat_id_ != chat_id; ChatInfo &chat = chats_[chat_id]; chat.id = chat_id; if (tdlib_open_chat_id_ != 0 && tdlib_open_chat_id_ != chat_id) { td_.send({ {"@type", "closeChat"}, {"chat_id", tdlib_open_chat_id_}, }); } open_chat_id_ = chat_id; message_scroll_ = 0; if (changed_chat) { message_attachment_message_id_ = 0; } if (tdlib_open_chat_id_ != chat_id) { td_.send({ {"@type", "openChat"}, {"chat_id", chat_id}, }); tdlib_open_chat_id_ = chat_id; } mark_chat_messages_as_read(chat_id); if (changed_chat) { request_chat_details(chat_id); const bool should_request_history = auto_reload_chat_history_ || !chat.history_loaded || chat.messages.empty(); const bool requested = should_request_history && request_chat_history(chat_id, auto_reload_chat_history_); if (requested) { status_line_ = auto_reload_chat_history_ && chat.history_loaded ? "Reloading chat history..." : "Loading chat history..."; } } } void App::sync_chat_ids_from_response(const json &response) { if (!response.contains("chat_ids") || !response.at("chat_ids").is_array()) { status_line_ = "Chat list response missing chat ids."; return; } std::vector chat_ids; for (const auto &entry : response.at("chat_ids")) { if (!entry.is_number_integer()) { continue; } const auto chat_id = entry.get(); chat_ids.push_back(chat_id); if (chats_.find(chat_id) == chats_.end()) { td_.send({ {"@type", "getChat"}, {"chat_id", chat_id}, }); } } if (!chat_ids.empty()) { sorted_chat_ids_ = std::move(chat_ids); if (selected_chat_index_ >= static_cast(sorted_chat_ids_.size())) { selected_chat_index_ = std::max(0, static_cast(sorted_chat_ids_.size()) - 1); } if (!open_chat_id().has_value()) { const auto chat_id = highlighted_chat_id(); if (chat_id.has_value()) { set_open_chat(*chat_id); } } request_open_chat_history(false); status_line_ = "Chats loaded."; return; } status_line_ = "No chats loaded."; } void App::append_message(std::int64_t chat_id, MessageInfo message) { ChatInfo &chat = chats_[chat_id]; chat.id = chat_id; auto existing = std::find_if(chat.messages.begin(), chat.messages.end(), [&](const MessageInfo &item) { return item.id == message.id; }); if (existing != chat.messages.end()) { *existing = std::move(message); } else { chat.messages.push_back(std::move(message)); } std::sort(chat.messages.begin(), chat.messages.end(), [](const MessageInfo &lhs, const MessageInfo &rhs) { if (lhs.date != rhs.date) { return lhs.date < rhs.date; } return lhs.id < rhs.id; }); if (kMaxMessagesPerChat > 0 && chat.messages.size() > kMaxMessagesPerChat) { chat.messages.erase(chat.messages.begin(), chat.messages.begin() + static_cast(chat.messages.size() - kMaxMessagesPerChat)); } } void App::remove_message(std::int64_t chat_id, std::int64_t message_id) { if (message_id == 0) { return; } auto chat_it = chats_.find(chat_id); if (chat_it == chats_.end()) { return; } auto &messages = chat_it->second.messages; messages.erase( std::remove_if(messages.begin(), messages.end(), [&](const MessageInfo &item) { return item.id == message_id; }), messages.end()); } void App::merge_history(std::int64_t chat_id, const json &messages) { ChatInfo &chat = chats_[chat_id]; chat.history_loading = false; chat.history_loaded = true; for (const auto &message : messages) { append_message(chat_id, parse_message(message)); } if (chat_id == open_chat_id_) { mark_chat_messages_as_read(chat_id); } status_line_ = "History loaded."; } void App::update_attachment_file(const json &file) { const std::int32_t file_id = safe_i32(file, "id"); if (file_id == 0) { return; } const std::int64_t size_bytes = std::max(safe_i64(file, "size"), safe_i64(file, "expected_size")); const json local = file.value("local", json::object()); const std::string local_path = safe_string(local, "path"); const std::int64_t downloaded_size = safe_i64(local, "downloaded_size"); const bool is_downloading_active = local.value("is_downloading_active", false); const bool is_downloaded = local.value("is_downloading_completed", false); const bool can_be_downloaded = local.value("can_be_downloaded", false); const bool can_be_deleted = local.value("can_be_deleted", false); for (auto &[chat_id, chat] : chats_) { (void)chat_id; for (auto &message : chat.messages) { if (!message.has_attachment || message.attachment.file_id != file_id) { continue; } if (size_bytes > 0) { message.attachment.size_bytes = size_bytes; } message.attachment.downloaded_size = downloaded_size; message.attachment.local_path = local_path; message.attachment.is_downloading_active = is_downloading_active; message.attachment.is_downloaded = is_downloaded; message.attachment.can_be_downloaded = can_be_downloaded; message.attachment.can_be_deleted = can_be_deleted; } } for (auto &animation : saved_animations_) { if (animation.file_id != file_id) { continue; } if (size_bytes > 0) { animation.size_bytes = size_bytes; } animation.downloaded_size = downloaded_size; animation.local_path = local_path; animation.is_downloading_active = is_downloading_active; animation.is_downloaded = is_downloaded; animation.can_be_downloaded = can_be_downloaded; animation.can_be_deleted = can_be_deleted; } if (pending_attachment_open_.has_value() && pending_attachment_open_->file_id == file_id) { pending_attachment_open_->size_bytes = size_bytes > 0 ? size_bytes : pending_attachment_open_->size_bytes; pending_attachment_open_->downloaded_size = downloaded_size; pending_attachment_open_->local_path = local_path; pending_attachment_open_->is_downloading_active = is_downloading_active; pending_attachment_open_->is_downloaded = is_downloaded; pending_attachment_open_->can_be_downloaded = can_be_downloaded; pending_attachment_open_->can_be_deleted = can_be_deleted; if (!is_downloaded && is_downloading_active) { status_line_ = "Downloading attachment to open " + format_download_progress(pending_attachment_open_->downloaded_size, pending_attachment_open_->size_bytes, pending_attachment_open_->is_downloaded); } if (is_downloaded && !local_path.empty()) { open_attachment(*pending_attachment_open_); pending_attachment_open_.reset(); } } if (pending_attachment_download_.has_value() && pending_attachment_download_->file_id == file_id) { pending_attachment_download_->size_bytes = size_bytes > 0 ? size_bytes : pending_attachment_download_->size_bytes; pending_attachment_download_->downloaded_size = downloaded_size; pending_attachment_download_->local_path = local_path; pending_attachment_download_->is_downloading_active = is_downloading_active; pending_attachment_download_->is_downloaded = is_downloaded; pending_attachment_download_->can_be_downloaded = can_be_downloaded; pending_attachment_download_->can_be_deleted = can_be_deleted; if (!is_downloaded && is_downloading_active) { status_line_ = "Downloading attachment to ~/Downloads " + format_download_progress( pending_attachment_download_->downloaded_size, pending_attachment_download_->size_bytes, pending_attachment_download_->is_downloaded); } if (is_downloaded && !local_path.empty()) { export_attachment_to_downloads(*pending_attachment_download_); pending_attachment_download_.reset(); } } } } // namespace telegram_tui