diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 4fccfb3d..7d2942cc 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -9,12 +9,7 @@ #include #include "../../../lib/platform_compat.h" -static QString shortenKey(const QByteArray& key) { - 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) { +QString AccountList::addrStr(const void* addr, int family) { if (family == 4) { 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]); @@ -30,11 +25,14 @@ static QString addrStr(const void* addr, int family) { return "?"; } -static QString protoStr(int protocol) { - switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; case 3: return "UDP+TCP"; default: return QString::number(protocol); } -} -static QString addrTypeStr(int t) { - 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); } +QString AccountList::relativeTimeStr(qint64 timestamp) { + if (timestamp <= 0) return ""; + qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp; + if (diff < 0) diff = 0; + 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) @@ -63,11 +61,11 @@ AccountList::AccountList(DbManager* db, QWidget *parent) m_listView->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_listView, 3); - m_detailText = new QPlainTextEdit(this); + m_detailText = new QTextEdit(this); m_detailText->setReadOnly(true); - m_detailText->setFont(QFont("monospace", 9)); + m_detailText->setFont(QFont("monospace", 10)); 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); }"); m_detailText->setPlaceholderText("Select a member"); layout->addWidget(m_detailText, 2); @@ -81,11 +79,17 @@ AccountList::AccountList(DbManager* db, QWidget *parent) connect(m_listView, &QWidget::customContextMenuRequested, 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) { m_channelId = channelId; m_model->setChannel(channelId); + m_detailTimer->stop(); + m_detailNodeId = 0; m_detailText->clear(); } @@ -99,71 +103,82 @@ void AccountList::onMemberClicked(const QModelIndex& index) { void AccountList::showMemberDetail(quint64 nodeId) { 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; if (sqlite3_prepare_v2(d, - "SELECT node_id, name, online, x25519_pubkey, ed25519_pubkey," - " last_seen_at, created_at FROM nodes WHERE node_id=?", - -1, &st, nullptr) != SQLITE_OK) { m_detailText->clear(); return; } - sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); - - if (sqlite3_step(st) == SQLITE_ROW) { - QString name = DbManager::colText(st, 1); - int online = sqlite3_column_int(st, 2); - QByteArray x25 = DbManager::colBlob(st, 3); - QByteArray ed = DbManager::colBlob(st, 4); - 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')); + "SELECT name, online, last_seen_at, created_at FROM nodes WHERE node_id=?", + -1, &st, nullptr) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); + if (sqlite3_step(st) == SQLITE_ROW) { + m_detailName = DbManager::colText(st, 0); + m_detailOnline = sqlite3_column_int(st, 1) != 0; + m_detailLastSeen = sqlite3_column_int64(st, 2); + m_detailCreated = sqlite3_column_int64(st, 3); + } + 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_prepare_v2(d, - "SELECT family, protocol, address, port, rtt, addr_type" - " FROM node_addresses WHERE node_id=? ORDER BY family, port", + "SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port", -1, &na, nullptr); if (na) { sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId); - int ac = 0; while (sqlite3_step(na) == SQLITE_ROW) { - if (ac == 0) text += " addresses:\n"; - int family = sqlite3_column_int(na, 0); - int proto = sqlite3_column_int(na, 1); - const void* addr = sqlite3_column_blob(na, 2); - int addrSz = sqlite3_column_bytes(na, 2); - int port = sqlite3_column_int(na, 3); - int rtt = sqlite3_column_int(na, 4); - int addrType = sqlite3_column_int(na, 5); - - 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++; + DetailAddress a; + a.family = sqlite3_column_int(na, 0); + a.protocol = sqlite3_column_int(na, 1); + const void* adr = sqlite3_column_blob(na, 2); + int sz = sqlite3_column_bytes(na, 2); + if (adr && sz > 0) a.addr = QByteArray((const char*)adr, sz); + a.port = sqlite3_column_int(na, 3); + if (!a.addr.isEmpty()) m_detailAddrs.append(a); } - if (ac == 0) text += " addresses: none\n"; 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("● %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("
")); +} + +void AccountList::onDetailTimerTick() { + if (m_detailNodeId != 0) updateDetailDisplay(); } void AccountList::onMemberDoubleClicked(const QModelIndex& index) { diff --git a/tools/chatgui/src/accountlist.h b/tools/chatgui/src/accountlist.h index aa1bbec2..26eb56ca 100644 --- a/tools/chatgui/src/accountlist.h +++ b/tools/chatgui/src/accountlist.h @@ -2,13 +2,21 @@ #include #include -#include +#include #include #include +#include class MemberListModel; class DbManager; +struct DetailAddress { + int family; + int protocol; + QByteArray addr; + int port; +}; + class AccountList : public QWidget { Q_OBJECT public: @@ -21,14 +29,26 @@ private slots: void onMemberClicked(const QModelIndex& index); void onMemberDoubleClicked(const QModelIndex& index); void onMemberContextMenu(const QPoint& pos); + void onDetailTimerTick(); private: void showMemberDetail(quint64 nodeId); + void updateDetailDisplay(); + static QString relativeTimeStr(qint64 timestamp); + static QString addrStr(const void* addr, int family); + + QListView *m_listView; + MemberListModel *m_model; + DbManager *m_db; + QTextEdit *m_detailText; + QTimer *m_refreshTimer; + QTimer *m_detailTimer; + QString m_channelId; - QListView *m_listView; - MemberListModel *m_model; - DbManager *m_db; - QPlainTextEdit *m_detailText; - QTimer *m_refreshTimer; - QString m_channelId; + quint64 m_detailNodeId = 0; + QString m_detailName; + bool m_detailOnline = false; + qint64 m_detailLastSeen = 0; + qint64 m_detailCreated = 0; + QVector m_detailAddrs; };