This commit is contained in:
2026-04-23 17:00:41 +03:00
commit ac28065d2a
17 changed files with 5401 additions and 0 deletions

269
src/app_chats.cpp Normal file
View File

@@ -0,0 +1,269 @@
#include "app.h"
#include <algorithm>
#include "util.h"
namespace telegram_tui {
namespace {
std::string join_with_separator_local(const std::vector<std::string>& 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<std::string>();
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<int>(sorted_chat_ids_.size())) {
selected_chat_index_ = std::max(0, static_cast<int>(sorted_chat_ids_.size()) - 1);
}
}
std::string App::format_open_chat_header(const ChatInfo& chat) const {
std::vector<std::string> 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, " | ");
}
} // namespace telegram_tui