#include "app.h" #include #include #include "app_ui.h" #include "util.h" namespace telegram_tui { namespace { std::string join_with_separator_local(const std::vector &parts, const char *separator) { std::string joined; for (const auto &part : parts) { if (part.empty()) { continue; } if (!joined.empty()) { joined += separator; } joined += part; } return joined; } std::string format_user_status(const json &status) { const std::string type = safe_string(status, "@type"); if (type == "userStatusOnline") { return "online"; } if (type == "userStatusOffline") { const std::int32_t was_online = safe_i32(status, "was_online"); return was_online > 0 ? ("last seen " + format_datetime(was_online)) : "offline"; } if (type == "userStatusRecently") { return "last seen recently"; } if (type == "userStatusLastWeek") { return "last seen within a week"; } if (type == "userStatusLastMonth") { return "last seen within a month"; } return {}; } std::string primary_username(const json &object) { const json usernames = object.value("usernames", json::object()); const json active_usernames = usernames.value("active_usernames", json::array()); if (active_usernames.is_array()) { for (const auto &username : active_usernames) { if (username.is_string()) { const std::string value = username.get(); if (!value.empty()) { return value; } } } } return safe_string(object, "username"); } } // namespace void App::update_user_status(std::int64_t user_id, const json &status) { UserInfo &info = users_[user_id]; info.id = user_id; info.status = format_user_status(status); } void App::upsert_user(const json &user) { if (user.is_null()) { return; } UserInfo &info = users_[safe_i64(user, "id")]; info.id = safe_i64(user, "id"); info.first_name = safe_string(user, "first_name"); info.last_name = safe_string(user, "last_name"); info.username = primary_username(user); info.status = format_user_status(user.value("status", json::object())); } void App::upsert_basic_group(const json &basic_group) { if (basic_group.is_null()) { return; } const std::int64_t basic_group_id = safe_i64(basic_group, "id"); const std::int32_t member_count = safe_i32(basic_group, "member_count"); for (auto &[chat_id, chat] : chats_) { if (chat.basic_group_id != basic_group_id) { continue; } chat.id = chat_id; chat.username.clear(); chat.member_count = member_count; chat.has_member_count = true; } } void App::upsert_supergroup(const json &supergroup) { if (supergroup.is_null()) { return; } const std::int64_t supergroup_id = safe_i64(supergroup, "id"); const std::int32_t member_count = safe_i32(supergroup, "member_count"); const bool is_channel = supergroup.value("is_channel", false); const std::string username = primary_username(supergroup); for (auto &[chat_id, chat] : chats_) { if (chat.supergroup_id != supergroup_id) { continue; } chat.id = chat_id; chat.is_channel = is_channel; chat.username = username; chat.member_count = member_count; chat.has_member_count = true; } } void App::upsert_chat(const json &chat_object) { if (chat_object.is_null()) { return; } const std::int64_t chat_id = safe_i64(chat_object, "id"); ChatInfo &chat = chats_[chat_id]; chat.id = chat_id; if (chat_object.contains("title")) { chat.title = safe_string(chat_object, "title"); } const json type = chat_object.value("type", json::object()); const std::string type_name = safe_string(type, "@type"); const std::int64_t previous_private_user_id = chat.private_user_id; const std::int64_t previous_basic_group_id = chat.basic_group_id; const std::int64_t previous_supergroup_id = chat.supergroup_id; chat.private_user_id = 0; chat.basic_group_id = 0; chat.supergroup_id = 0; chat.is_channel = false; if (type_name == "chatTypePrivate" || type_name == "chatTypeSecret") { chat.private_user_id = safe_i64(type, "user_id"); chat.username.clear(); chat.has_member_count = false; chat.has_online_member_count = false; chat.member_count = 0; chat.online_member_count = 0; } else if (type_name == "chatTypeBasicGroup") { chat.basic_group_id = safe_i64(type, "basic_group_id"); chat.username.clear(); } else if (type_name == "chatTypeSupergroup") { chat.supergroup_id = safe_i64(type, "supergroup_id"); chat.is_channel = type.value("is_channel", false); if (chat.supergroup_id != previous_supergroup_id) { chat.username.clear(); } } else { chat.username.clear(); } if (chat.private_user_id != previous_private_user_id || chat.basic_group_id != previous_basic_group_id || chat.supergroup_id != previous_supergroup_id) { chat.details_requested = false; } chat.unread_count = safe_i32(chat_object, "unread_count"); chat.last_read_inbox_message_id = safe_i64(chat_object, "last_read_inbox_message_id"); chat.last_read_outbox_message_id = safe_i64(chat_object, "last_read_outbox_message_id"); if (chat_object.contains("last_message")) { chat.last_message_preview = preview_message(chat_object.at("last_message")); } if (chat_object.contains("positions") && chat_object.at("positions").is_array()) { chat.in_main_list = false; chat.main_order = 0; for (const auto &position : chat_object.at("positions")) { apply_chat_position(chat, position); } } if (open_chat_id_ == chat.id) { request_chat_details(chat.id); } resort_chats(); } void App::apply_chat_position(ChatInfo &chat, const json &position) { if (!position.is_object()) { return; } const json chat_list = position.value("list", json::object()); if (safe_string(chat_list, "@type") != "chatListMain") { return; } chat.in_main_list = true; chat.main_order = safe_i64(position, "order"); } void App::resort_chats() { if (sorted_chat_ids_.empty()) { for (const auto &[chat_id, chat] : chats_) { if (chat.in_main_list) { sorted_chat_ids_.push_back(chat_id); } } } else { for (const auto &[chat_id, chat] : chats_) { if (!chat.in_main_list) { continue; } if (std::find(sorted_chat_ids_.begin(), sorted_chat_ids_.end(), chat_id) == sorted_chat_ids_.end()) { sorted_chat_ids_.push_back(chat_id); } } } std::stable_sort(sorted_chat_ids_.begin(), sorted_chat_ids_.end(), [&](std::int64_t lhs, std::int64_t rhs) { const auto left_it = chats_.find(lhs); const auto right_it = chats_.find(rhs); const bool left_has_order = left_it != chats_.end() && left_it->second.in_main_list && left_it->second.main_order > 0; const bool right_has_order = right_it != chats_.end() && right_it->second.in_main_list && right_it->second.main_order > 0; if (left_has_order != right_has_order) { return left_has_order; } if (left_has_order && right_has_order && left_it->second.main_order != right_it->second.main_order) { return left_it->second.main_order > right_it->second.main_order; } return false; }); if (selected_chat_index_ >= static_cast(sorted_chat_ids_.size())) { selected_chat_index_ = std::max(0, static_cast(sorted_chat_ids_.size()) - 1); } } std::string App::user_status_label(std::int64_t user_id) const { const auto user_it = users_.find(user_id); if (user_it == users_.end()) { return {}; } return user_it->second.status; } std::string App::format_open_chat_header(const ChatInfo &chat) const { std::vector parts; if (!chat.title.empty()) { parts.push_back(chat.title); } if (chat.private_user_id != 0) { const auto user_it = users_.find(chat.private_user_id); if (user_it != users_.end()) { if (!user_it->second.username.empty()) { parts.push_back("@" + user_it->second.username); } const std::string status = user_status_label(chat.private_user_id); if (!status.empty()) { parts.push_back(status); } parts.push_back("id:" + std::to_string(user_it->second.id)); } else { parts.push_back("id:" + std::to_string(chat.private_user_id)); } return join_with_separator_local(parts, " | "); } if (!chat.username.empty()) { parts.push_back("@" + chat.username); } if (chat.has_member_count) { parts.push_back(std::to_string(chat.member_count) + " " + (chat.is_channel ? "subscribers" : "members")); } if (!chat.is_channel && chat.has_online_member_count) { parts.push_back(std::to_string(chat.online_member_count) + " online"); } parts.push_back("id:" + std::to_string(chat.id)); return join_with_separator_local(parts, " | "); } void App::draw_chat_pane(int top, int height, int width) { const int visible_rows = std::max(1, height); int first_index = 0; if (selected_chat_index_ >= visible_rows) { first_index = selected_chat_index_ - visible_rows + 1; } for (int row = 0; row < visible_rows; ++row) { const int index = first_index + row; const int y = top + row; mvhline(y, 0, ' ', width); if (index >= static_cast(sorted_chat_ids_.size())) { continue; } const auto chat_id = sorted_chat_ids_[index]; const auto chat_it = chats_.find(chat_id); const bool selected = index == selected_chat_index_; const bool is_open = open_chat_id_ == chat_id; if (selected) { attron(A_BOLD | (focus_ == FocusPane::Chats ? A_REVERSE : A_NORMAL)); } if (is_open) { attron(COLOR_PAIR(kColorPairOpenChat)); } const bool has_chat = chat_it != chats_.end(); const ChatInfo *chat = has_chat ? &chat_it->second : nullptr; std::string line = (chat != nullptr && chat->unread_count > 0) ? "[U] " : "[ ] "; line += is_open ? ">" : " "; if (chat != nullptr) { line += chat->title; } else { line += "Chat " + std::to_string(chat_id); } if (chat != nullptr && chat->unread_count > 0) { line += " [" + std::to_string(chat->unread_count) + "]"; } if (chat != nullptr && !chat->last_message_preview.empty()) { line += " - " + chat->last_message_preview; } if (static_cast(line.size()) > width - 2) { line.resize(static_cast(width - 5)); line += "..."; } mvprintw(y, 1, "%s", line.c_str()); if (is_open) { attroff(COLOR_PAIR(kColorPairOpenChat)); } if (selected) { attroff(A_BOLD | (focus_ == FocusPane::Chats ? A_REVERSE : A_NORMAL)); } } } } // namespace telegram_tui