Show own and peer devices in the DM member panel

This commit is contained in:
evgeny committed 2026-11-03 14:11:25 +01:00
1 parent 21702d6c01
commit 7939002dba
2 files changed
+85 -5

No files matched your search

+79 -4
View File
@@ -26,6 +26,8 @@
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QSortFilterProxyModel>
#include <QTreeWidget>
#include <QHeaderView>
#include <algorithm>
#include <cstring>
#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<dm_messages_req*>(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<const char*>(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<QPair<int, chat_member_display>> members; QSet<quint64> 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);
}
+6 -1
View File
@@ -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=неизвестно