From 7939002dbaef3d49d9a191ce5a18eb0614b61cf4 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 14:11:25 +0100 Subject: [PATCH] Show own and peer devices in the DM member panel --- tools/chatgui/src/accountlist.cpp | 83 +++++++++++++++++++++++++++++-- tools/chatgui/src/accountlist.h | 7 ++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index ea56f88ac..e602e2451 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include #include #include "../../../lib/platform_compat.h" @@ -34,6 +36,7 @@ extern "C" { #include "../lib/socket_compat.h" #include "../lib/debug_config.h" #include "chat/chat_core.h" + #include "dm/dm_core.h" #include "routing_layer/topo_node.h" #include "member_files/protocol.h" #include "../../lib/mem.h" @@ -89,6 +92,15 @@ AccountList::AccountList(DbManager* db, QWidget *parent) m_header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);"); layout->addWidget(m_header); + m_dmDevices = new QTreeWidget(this); m_dmDevices->setObjectName("dmDevices"); + m_dmDevices->setColumnCount(2); m_dmDevices->setHeaderHidden(true); m_dmDevices->setRootIsDecorated(false); + m_dmDevices->setItemsExpandable(false); m_dmDevices->setIndentation(12); + m_dmDevices->header()->setSectionResizeMode(0, QHeaderView::Stretch); + m_dmDevices->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + m_dmDevices->hide(); layout->addWidget(m_dmDevices, 1); + m_dmTimer = new QTimer(this); m_dmTimer->setInterval(2000); + connect(m_dmTimer, &QTimer::timeout, this, &AccountList::refresh); + m_grouped = new GroupedMemberModel(m_model, this); m_listView->setModel(m_grouped); auto* delegate = new AccountDelegate(m_listView); @@ -222,14 +234,21 @@ AccountList::~AccountList() { DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "member detail panel removed remaining=%lld", (long long)g_detailTargets.size()); } -void AccountList::setChannel(const QString& channelId) { +void AccountList::setChannel(const QString& channelId, quint64 dmGeneration) { const QByteArray channel = channelId.toUtf8(); if (channel.size() >= int(sizeof(chat_member_list_req::ch_id)) || channel.contains('\0')) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member panel channel rejected bytes=%lld embedded_nul=%d", (long long)channel.size(), channel.contains('\0')); return; } - if (m_channelId != channelId) clearMemberDetail(); + if (!dmGeneration && channelId.startsWith("dm:") && m_db && m_db->isOpen()) + for (const auto& conv : m_db->getDmConversations()) + if (conv.convId == channelId.mid(3) && conv.mode == DM_MODE_GROUPS) { dmGeneration = conv.generation; break; } + if (m_channelId != channelId || m_dmGeneration != dmGeneration) { clearMemberDetail(); m_dmDevices->clear(); } m_channelId = channelId; + m_dmGeneration = channelId.startsWith("dm:") ? dmGeneration : 0; + m_dmDevices->setVisible(m_dmGeneration != 0); + m_listView->setVisible(!m_dmGeneration); m_detailPanel->setVisible(!m_dmGeneration); + if (m_dmGeneration && isVisible()) m_dmTimer->start(); else m_dmTimer->stop(); bool isOwner = false; if (m_db && m_db->isOpen()) { @@ -239,13 +258,67 @@ void AccountList::setChannel(const QString& channelId) { } } - m_model->setChannel(channelId, isOwner); + m_model->setChannel(channelId.startsWith("dm:") ? QString() : channelId, isOwner); + if (m_dmGeneration) refresh(); m_header->setText(isOwner ? QCoreApplication::translate("ChatGui", "Members 🔑") : QCoreApplication::translate("ChatGui", "Members")); m_header->setToolTip(isOwner ? QCoreApplication::translate("ChatGui", "Channel owner (has private key)") : QString()); } -void AccountList::refresh() { m_model->refresh(); } +void AccountList::refresh() { + if (!m_dmGeneration) { m_model->refresh(); return; } + if (!gui_bridge_is_uasync_ready()) return; + auto* req = static_cast(u_calloc(1, sizeof(dm_messages_req))); + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: request allocation failed"); return; } + req->inst = gui_bridge_get_inst(); req->generation = m_dmGeneration; + snprintf(req->conv_id, sizeof(req->conv_id), "%s", m_channelId.mid(3).toUtf8().constData()); + if (gui_bridge_post_uasync_owned_fn(dm_members_trampoline, req) < 0) { + DEBUG_WARN(DEBUG_CATEGORY_DM, "DM members: request rejected conv=%s", req->conv_id); u_free(req); + } +} + +void AccountList::onDmMembers(const uint8_t* data, int len) { + if (!data || len < 12 || !data[0] || data[0] > 63 || len < 11 + data[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: invalid header bytes=%d", len); return; + } + const int cl = data[0], count = mf_get16(data + 9 + cl); + const QString channel = "dm:" + QString::fromUtf8(reinterpret_cast(data + 1), cl); + if (!m_dmGeneration || m_channelId != channel || mf_get64(data + 1 + cl) != m_dmGeneration) return; + if (count > DM_TARGET_MAX || len != 11 + cl + count * DM_MEMBER_WIRE_SIZE) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: invalid count=%d bytes=%d", count, len); return; + } + QVector> members; QSet nodes; + for (int i = 0; i < count; i++) { + const auto* row = data + 11 + cl + i * DM_MEMBER_WIRE_SIZE; chat_member_display member{}; + if (row[0] > 1 || chat_core_deserialize_member(row + 1, &member) || !member.node_id || nodes.contains(member.node_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM members: invalid/duplicate row=%d", i); return; + } + nodes.insert(member.node_id); members.append({row[0], member}); + } + std::stable_sort(members.begin(), members.end(), [](const auto& a, const auto& b) { + if (a.first != b.first) return a.first < b.first; + if (a.second.is_self != b.second.is_self) return a.second.is_self > b.second.is_self; + if (a.second.online != b.second.online) return a.second.online > b.second.online; + return a.second.node_id < b.second.node_id; + }); + const quint64 selected = m_dmDevices->currentItem() ? m_dmDevices->currentItem()->data(0, Qt::UserRole).toULongLong() : 0; + m_dmDevices->clear(); QTreeWidgetItem* sections[2]; + for (int side = 0; side < 2; side++) { + sections[side] = new QTreeWidgetItem(m_dmDevices, {QCoreApplication::translate("ChatGui", side ? "Peer devices" : "My devices")}); + sections[side]->setFlags(Qt::ItemIsEnabled); QFont font = sections[side]->font(0); font.setBold(true); + sections[side]->setFont(0, font); sections[side]->setExpanded(true); + } + for (const auto& entry : members) { + const auto& member = entry.second; + auto* row = new QTreeWidgetItem(sections[entry.first], {QString::fromUtf8(member.name), + QCoreApplication::translate("ChatGui", member.online ? "online" : "offline")}); + row->setData(0, Qt::UserRole, QVariant::fromValue(quint64(member.node_id))); + row->setToolTip(0, QString("0x%1").arg(member.node_id, 16, 16, QChar('0'))); + if (member.is_self) { QFont font = row->font(0); font.setBold(true); row->setFont(0, font); } + if (member.node_id == selected) m_dmDevices->setCurrentItem(row); + } + DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM members: panel updated channel=%s count=%d", qPrintable(channel), count); +} void AccountList::onMemberClicked(const QModelIndex& index) { if (!index.isValid()) return; @@ -328,10 +401,12 @@ void AccountList::renameGroup(QString group) { void AccountList::showEvent(QShowEvent* event) { QWidget::showEvent(event); + if (m_dmGeneration) { m_dmTimer->start(); refresh(); } if (m_detailNodeId) { m_detailTimer->start(); requestMemberDetail(); } } void AccountList::hideEvent(QHideEvent* event) { + m_dmTimer->stop(); m_detailTimer->stop(); m_detailRequest = 0; m_detailHops.clear(); QWidget::hideEvent(event); } diff --git a/tools/chatgui/src/accountlist.h b/tools/chatgui/src/accountlist.h index a19a41245..64e5f4701 100644 --- a/tools/chatgui/src/accountlist.h +++ b/tools/chatgui/src/accountlist.h @@ -25,6 +25,7 @@ class GroupedMemberModel; class DbManager; class QSortFilterProxyModel; class QPushButton; +class QTreeWidget; struct DetailAddress { int family = 0; // 4/6, не AF_INET/AF_INET6 @@ -61,8 +62,9 @@ public: explicit AccountList(DbManager* db = nullptr, QWidget *parent = nullptr); // без db доступны снимки/действия, редактирование выключено ~AccountList(); - void setChannel(const QString& channelId); // ID до 63 UTF-8 байт без NUL; пустой очищает модель/карточку, owner определяется по ключу БД + void setChannel(const QString& channelId, quint64 dmGeneration = 0); // CHAT или dm:ID; GROUPS generation берётся из БД либо caller. void refresh(); // асинхронно запросить список, показанные строки сохраняются до ответа + void onDmMembers(const uint8_t* data, int len); // Полный CHAT_EVT_DM_MEMBERS; чужое/устаревшее поколение игнорируется. void onMemberDetailData(const uint8_t* data, int len); // целый codec-пакет, заимствован на время вызова; неверный/старый не меняет карточку /* GUI callbacks; данные заимствованы. Updated содержит полный CHAT_MEMBER_DISPLAY_SIZE, @@ -121,6 +123,9 @@ private: QString m_detailGroup; // hex devgroup в нижней панели; пусто при выборе отдельного устройства QTimer *m_detailTimer; // child, 1 с; запускается только для выбранной видимой карточки QString m_channelId; // захватывается меню/диалогом и перепроверяется после exec + QTreeWidget* m_dmDevices; // Два раздела текущего состава GROUPS; не снимок доставки сообщения. + QTimer* m_dmTimer; // Опрос текущего состава/online раз в 2 с только для видимой панели. + quint64 m_dmGeneration = 0; // 0 — обычная панель или DEVICE; ненулевое привязывает запросы к истории. quint64 m_detailNodeId = 0; qint64 m_detailLastSeen = 0, m_detailCreated = 0; // Unix seconds, 0=неизвестно