Browse Source

chatgui: member detail panel redesign — compact format with relative time, colored online dot, 5s auto-refresh

topo_upd
Evgeny 2 months ago
parent
commit
373a2e4687
  1. 135
      tools/chatgui/src/accountlist.cpp
  2. 24
      tools/chatgui/src/accountlist.h

135
tools/chatgui/src/accountlist.cpp

@ -9,12 +9,7 @@
#include <QFont> #include <QFont>
#include "../../../lib/platform_compat.h" #include "../../../lib/platform_compat.h"
static QString shortenKey(const QByteArray& key) { QString AccountList::addrStr(const void* addr, int family) {
if (key.size() < 8) return key.toHex(' ').toUpper();
return key.left(4).toHex(' ').toUpper() + ".." + key.right(4).toHex(' ').toUpper();
}
static QString addrStr(const void* addr, int family) {
if (family == 4) { if (family == 4) {
const uint8_t* a = (const uint8_t*)addr; const uint8_t* a = (const uint8_t*)addr;
return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]); return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]);
@ -30,11 +25,14 @@ static QString addrStr(const void* addr, int family) {
return "?"; return "?";
} }
static QString protoStr(int protocol) { QString AccountList::relativeTimeStr(qint64 timestamp) {
switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; case 3: return "UDP+TCP"; default: return QString::number(protocol); } if (timestamp <= 0) return "";
} qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp;
static QString addrTypeStr(int t) { if (diff < 0) diff = 0;
switch (t) { case 0: return "NETIF"; case 1: return "DIRECT"; case 2: return "NAT_EIM"; case 3: return "NAT_STRICT"; default: return QString("?%1").arg(t); } if (diff < 60) return QString("%1s").arg(diff);
if (diff < 3600) return QString("%1m").arg(diff / 60);
if (diff < 86400) return QString("%1h").arg(diff / 3600);
return QString("%1d").arg(diff / 86400);
} }
AccountList::AccountList(DbManager* db, QWidget *parent) AccountList::AccountList(DbManager* db, QWidget *parent)
@ -63,11 +61,11 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
m_listView->setContextMenuPolicy(Qt::CustomContextMenu); m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
layout->addWidget(m_listView, 3); layout->addWidget(m_listView, 3);
m_detailText = new QPlainTextEdit(this); m_detailText = new QTextEdit(this);
m_detailText->setReadOnly(true); m_detailText->setReadOnly(true);
m_detailText->setFont(QFont("monospace", 9)); m_detailText->setFont(QFont("monospace", 10));
m_detailText->setStyleSheet( m_detailText->setStyleSheet(
"QPlainTextEdit { border-top: 1px solid palette(mid); border-bottom: none;" "QTextEdit { border-top: 1px solid palette(mid); border-bottom: none;"
" border-left: none; border-right: none; padding: 6px; background: palette(base); }"); " border-left: none; border-right: none; padding: 6px; background: palette(base); }");
m_detailText->setPlaceholderText("Select a member"); m_detailText->setPlaceholderText("Select a member");
layout->addWidget(m_detailText, 2); layout->addWidget(m_detailText, 2);
@ -81,11 +79,17 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
connect(m_listView, &QWidget::customContextMenuRequested, connect(m_listView, &QWidget::customContextMenuRequested,
this, &AccountList::onMemberContextMenu); this, &AccountList::onMemberContextMenu);
m_detailTimer = new QTimer(this);
m_detailTimer->setInterval(5000);
connect(m_detailTimer, &QTimer::timeout, this, &AccountList::onDetailTimerTick);
} }
void AccountList::setChannel(const QString& channelId) { void AccountList::setChannel(const QString& channelId) {
m_channelId = channelId; m_channelId = channelId;
m_model->setChannel(channelId); m_model->setChannel(channelId);
m_detailTimer->stop();
m_detailNodeId = 0;
m_detailText->clear(); m_detailText->clear();
} }
@ -99,71 +103,82 @@ void AccountList::onMemberClicked(const QModelIndex& index) {
void AccountList::showMemberDetail(quint64 nodeId) { void AccountList::showMemberDetail(quint64 nodeId) {
if (!m_db || !m_db->m_db) return; if (!m_db || !m_db->m_db) return;
sqlite3* d = m_db->m_db;
QString text; m_detailTimer->stop();
m_detailNodeId = nodeId;
m_detailName.clear();
m_detailOnline = false;
m_detailLastSeen = 0;
m_detailCreated = 0;
m_detailAddrs.clear();
sqlite3* d = m_db->m_db;
sqlite3_stmt* st = nullptr; sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(d, if (sqlite3_prepare_v2(d,
"SELECT node_id, name, online, x25519_pubkey, ed25519_pubkey," "SELECT name, online, last_seen_at, created_at FROM nodes WHERE node_id=?",
" last_seen_at, created_at FROM nodes WHERE node_id=?", -1, &st, nullptr) == SQLITE_OK) {
-1, &st, nullptr) != SQLITE_OK) { m_detailText->clear(); return; }
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
if (sqlite3_step(st) == SQLITE_ROW) { if (sqlite3_step(st) == SQLITE_ROW) {
QString name = DbManager::colText(st, 1); m_detailName = DbManager::colText(st, 0);
int online = sqlite3_column_int(st, 2); m_detailOnline = sqlite3_column_int(st, 1) != 0;
QByteArray x25 = DbManager::colBlob(st, 3); m_detailLastSeen = sqlite3_column_int64(st, 2);
QByteArray ed = DbManager::colBlob(st, 4); m_detailCreated = sqlite3_column_int64(st, 3);
qint64 last_seen = sqlite3_column_int64(st, 5);
qint64 created = sqlite3_column_int64(st, 6);
text += QString("Node: 0x%1").arg(nodeId, 16, 16, QChar('0'));
if (!name.isEmpty()) text += QString(" (%1)").arg(name);
text += QString("\n online: %1\n").arg(online ? "yes" : "no");
text += QString(" x25519: %1\n").arg(x25.isEmpty() ? "-" : shortenKey(x25));
text += QString(" ed25519: %1\n").arg(ed.isEmpty() ? "-" : shortenKey(ed));
if (last_seen > 0)
text += QString(" last_seen: %1\n").arg(QDateTime::fromSecsSinceEpoch(last_seen).toString("yyyy-MM-dd hh:mm:ss"));
if (created > 0)
text += QString(" created: %1\n").arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss"));
} else {
text = QString("Node 0x%1 not found in database.").arg(nodeId, 16, 16, QChar('0'));
} }
sqlite3_finalize(st); sqlite3_finalize(st);
}
if (m_detailName.isEmpty())
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper();
sqlite3_stmt* na = nullptr; sqlite3_stmt* na = nullptr;
sqlite3_prepare_v2(d, sqlite3_prepare_v2(d,
"SELECT family, protocol, address, port, rtt, addr_type" "SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port",
" FROM node_addresses WHERE node_id=? ORDER BY family, port",
-1, &na, nullptr); -1, &na, nullptr);
if (na) { if (na) {
sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId); sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId);
int ac = 0;
while (sqlite3_step(na) == SQLITE_ROW) { while (sqlite3_step(na) == SQLITE_ROW) {
if (ac == 0) text += " addresses:\n"; DetailAddress a;
int family = sqlite3_column_int(na, 0); a.family = sqlite3_column_int(na, 0);
int proto = sqlite3_column_int(na, 1); a.protocol = sqlite3_column_int(na, 1);
const void* addr = sqlite3_column_blob(na, 2); const void* adr = sqlite3_column_blob(na, 2);
int addrSz = sqlite3_column_bytes(na, 2); int sz = sqlite3_column_bytes(na, 2);
int port = sqlite3_column_int(na, 3); if (adr && sz > 0) a.addr = QByteArray((const char*)adr, sz);
int rtt = sqlite3_column_int(na, 4); a.port = sqlite3_column_int(na, 3);
int addrType = sqlite3_column_int(na, 5); if (!a.addr.isEmpty()) m_detailAddrs.append(a);
if (addr && addrSz > 0)
text += QString(" %1:%2 %3 %4 rtt=%5ms\n")
.arg(addrStr(addr, family))
.arg(port)
.arg(protoStr(proto))
.arg(addrTypeStr(addrType))
.arg(rtt > 0 ? QString::number(rtt / 10) : "-");
ac++;
} }
if (ac == 0) text += " addresses: none\n";
sqlite3_finalize(na); sqlite3_finalize(na);
} }
m_detailText->setPlainText(text); m_detailTimer->start();
updateDetailDisplay();
}
void AccountList::updateDetailDisplay() {
QStringList lines;
lines.append(QString("ID:0x%1").arg(m_detailNodeId, 16, 16, QChar('0')));
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
lines.append(QString("<span style='color:%1;'>●</span> %2")
.arg(dotColor, m_detailName.toHtmlEscaped()));
if (m_detailLastSeen > 0)
lines.append(QString("⌛ %1").arg(relativeTimeStr(m_detailLastSeen)));
if (m_detailCreated > 0)
lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated)));
for (const auto& a : m_detailAddrs) {
QString proto;
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; }
lines.append(QString("%1:%2:%3").arg(proto, addrStr(a.addr.constData(), a.family)).arg(a.port));
}
m_detailText->setHtml(lines.join("<br>"));
}
void AccountList::onDetailTimerTick() {
if (m_detailNodeId != 0) updateDetailDisplay();
} }
void AccountList::onMemberDoubleClicked(const QModelIndex& index) { void AccountList::onMemberDoubleClicked(const QModelIndex& index) {

24
tools/chatgui/src/accountlist.h

@ -2,13 +2,21 @@
#include <QWidget> #include <QWidget>
#include <QListView> #include <QListView>
#include <QPlainTextEdit> #include <QTextEdit>
#include <QTimer> #include <QTimer>
#include <QMenu> #include <QMenu>
#include <QVector>
class MemberListModel; class MemberListModel;
class DbManager; class DbManager;
struct DetailAddress {
int family;
int protocol;
QByteArray addr;
int port;
};
class AccountList : public QWidget { class AccountList : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
@ -21,14 +29,26 @@ private slots:
void onMemberClicked(const QModelIndex& index); void onMemberClicked(const QModelIndex& index);
void onMemberDoubleClicked(const QModelIndex& index); void onMemberDoubleClicked(const QModelIndex& index);
void onMemberContextMenu(const QPoint& pos); void onMemberContextMenu(const QPoint& pos);
void onDetailTimerTick();
private: private:
void showMemberDetail(quint64 nodeId); void showMemberDetail(quint64 nodeId);
void updateDetailDisplay();
static QString relativeTimeStr(qint64 timestamp);
static QString addrStr(const void* addr, int family);
QListView *m_listView; QListView *m_listView;
MemberListModel *m_model; MemberListModel *m_model;
DbManager *m_db; DbManager *m_db;
QPlainTextEdit *m_detailText; QTextEdit *m_detailText;
QTimer *m_refreshTimer; QTimer *m_refreshTimer;
QTimer *m_detailTimer;
QString m_channelId; QString m_channelId;
quint64 m_detailNodeId = 0;
QString m_detailName;
bool m_detailOnline = false;
qint64 m_detailLastSeen = 0;
qint64 m_detailCreated = 0;
QVector<DetailAddress> m_detailAddrs;
}; };

Loading…
Cancel
Save