345 lines
10 KiB
C++
345 lines
10 KiB
C++
#include "app.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <curses.h>
|
|
|
|
#include "app_ui.h"
|
|
#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::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<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, " | ");
|
|
}
|
|
|
|
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<int>(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<int>(line.size()) > width - 2) {
|
|
line.resize(static_cast<std::size_t>(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
|