560 lines
22 KiB
C++
560 lines
22 KiB
C++
#include <QCoreApplication>
|
|
#include "accountlist.h"
|
|
#include "accountdelegate.h"
|
|
#include "memberlistmodel.h"
|
|
#include "avatars/avatargallery.h"
|
|
#include "memberpropsdialog.h"
|
|
#include "renamedialog.h"
|
|
#include "../db/db_manager.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include <QVBoxLayout>
|
|
#include <QSplitter>
|
|
#include <QLabel>
|
|
#include <QDateTime>
|
|
#include <QFont>
|
|
#include <QSet>
|
|
#include <QMessageBox>
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include "../../../lib/platform_compat.h"
|
|
|
|
extern "C" {
|
|
#include "../lib/socket_compat.h"
|
|
#include "../lib/debug_config.h"
|
|
#include "chat/chat_core.h"
|
|
#include "../../lib/mem.h"
|
|
}
|
|
|
|
static AccountList* g_detailTarget = nullptr;
|
|
|
|
static void memberDetailCb(const uint8_t* data, int len) {
|
|
if (!g_detailTarget) return;
|
|
if (len < 13) return;
|
|
uint64_t nodeId; memcpy(&nodeId, data, 8);
|
|
g_detailTarget->onMemberDetailData(nodeId, data, len);
|
|
}
|
|
|
|
static QString addrRoleStr(int t) {
|
|
switch (t) { case 0: return "local"; case 1: return "public"; case 2: return "nat_eim"; case 3: return "nat_dependent"; case 4: return "unknown"; case 5: return "contact_required"; default: return QString("?%1").arg(t); }
|
|
}
|
|
|
|
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]);
|
|
}
|
|
if (family == 6) {
|
|
const uint16_t* a = (const uint16_t*)addr;
|
|
int zero = 1; for (int i = 0; i < 8; i++) if (a[i]) { zero = 0; break; }
|
|
if (zero) return "[::]";
|
|
return QString("%1:%2:%3:%4:%5:%6:%7:%8")
|
|
.arg(ntohs(a[0]), 0, 16).arg(ntohs(a[1]), 0, 16)
|
|
.arg(ntohs(a[2]), 0, 16).arg(ntohs(a[3]), 0, 16)
|
|
.arg(ntohs(a[4]), 0, 16).arg(ntohs(a[5]), 0, 16)
|
|
.arg(ntohs(a[6]), 0, 16).arg(ntohs(a[7]), 0, 16);
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
QString AccountList::relativeTimeStr(qint64 timestamp) {
|
|
if (timestamp <= 0) return "";
|
|
qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp;
|
|
if (diff < 0) diff = 0;
|
|
if (diff < 60) return QCoreApplication::translate("ChatGui", "%1s").arg(diff);
|
|
if (diff < 3600) return QCoreApplication::translate("ChatGui", "%1m").arg(diff / 60);
|
|
if (diff < 86400) return QCoreApplication::translate("ChatGui", "%1h").arg(diff / 3600);
|
|
return QCoreApplication::translate("ChatGui", "%1d").arg(diff / 86400);
|
|
}
|
|
|
|
AccountList::AccountList(DbManager* db, QWidget *parent)
|
|
: QWidget(parent)
|
|
, m_listView(new QListView(this))
|
|
, m_model(new MemberListModel(this))
|
|
, m_db(db)
|
|
, m_detailPanel(nullptr)
|
|
, m_header(nullptr)
|
|
{
|
|
auto *layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
layout->setSpacing(0);
|
|
|
|
m_header = new QLabel(QCoreApplication::translate("ChatGui", "Members"), this);
|
|
m_header->setAlignment(Qt::AlignCenter);
|
|
m_header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
|
|
layout->addWidget(m_header);
|
|
|
|
m_listView->setModel(m_model);
|
|
auto* delegate = new AccountDelegate(m_listView);
|
|
m_listView->setItemDelegate(delegate);
|
|
connect(delegate, &AccountDelegate::radioMuteRequested, this, [this](const QModelIndex& index) {
|
|
if (gui_bridge_toggle_member_radio_mute(m_channelId.toUtf8().constData(), m_model->nodeIdAt(index.row())) < 0)
|
|
QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Error"),
|
|
QCoreApplication::translate("ChatGui", "Cannot change radio listening setting"));
|
|
});
|
|
connect(delegate, &AccountDelegate::avatarRequested, this, [this](const QModelIndex& index) {
|
|
showAvatarGallery(m_channelId, m_model->nodeIdAt(index.row()));
|
|
});
|
|
connect(delegate, &AccountDelegate::inviteRequested, this, [this](const QModelIndex& index) {
|
|
const quint64 peer = m_model->nodeIdAt(index.row());
|
|
if (peer && m_db && peer != m_db->myNodeId())
|
|
emit inviteRequested(m_channelId, peer, index.data(Qt::DisplayRole).toString());
|
|
});
|
|
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
m_listView->setMouseTracking(true);
|
|
m_listView->setIconSize(QSize(MemberListModel::kAvatarSize, MemberListModel::kAvatarSize));
|
|
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
m_detailPanel = new QWidget(this);
|
|
auto* detailLayout = new QVBoxLayout(m_detailPanel);
|
|
detailLayout->setContentsMargins(0, 0, 0, 0);
|
|
detailLayout->setSpacing(0);
|
|
|
|
m_detailHeader = new QLabel(QCoreApplication::translate("ChatGui", "Select a member"), m_detailPanel);
|
|
m_detailHeader->setWordWrap(true);
|
|
m_detailHeader->setStyleSheet("font-weight: bold; padding: 4px 8px; background: palette(window);");
|
|
detailLayout->addWidget(m_detailHeader);
|
|
|
|
m_detailText = new QTextEdit(m_detailPanel);
|
|
m_detailText->setReadOnly(true);
|
|
m_detailText->setFont(QFont("monospace", 9));
|
|
m_detailText->setStyleSheet("QTextEdit { border: none; padding: 4px; background: palette(base); }");
|
|
detailLayout->addWidget(m_detailText, 1);
|
|
|
|
auto *memberSplitter = new QSplitter(Qt::Vertical, this);
|
|
memberSplitter->addWidget(m_listView);
|
|
memberSplitter->addWidget(m_detailPanel);
|
|
memberSplitter->setStretchFactor(0, 3);
|
|
memberSplitter->setStretchFactor(1, 2);
|
|
memberSplitter->setSizes({360, 240});
|
|
layout->addWidget(memberSplitter, 1);
|
|
|
|
connect(m_listView->selectionModel(), &QItemSelectionModel::selectionChanged,
|
|
m_listView->viewport(), qOverload<>(&QWidget::update));
|
|
connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked);
|
|
connect(m_listView, &QListView::doubleClicked, this, &AccountList::onMemberDoubleClicked);
|
|
|
|
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);
|
|
|
|
m_linkTimer = new QTimer(this);
|
|
m_linkTimer->setInterval(1000);
|
|
connect(m_linkTimer, &QTimer::timeout, this, [this]{
|
|
if (m_detailNodeId != 0) requestMemberDetail();
|
|
});
|
|
|
|
g_detailTarget = this;
|
|
gui_bridge_set_radio_mute_result_cb([](const uint8_t* data, int len) {
|
|
if (len != 17) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: invalid GUI mute result bytes=%d", len); return; }
|
|
if (g_detailTarget && !data[16]) QMessageBox::warning(g_detailTarget, QCoreApplication::translate("ChatGui", "Error"),
|
|
QCoreApplication::translate("ChatGui", "Cannot change radio listening setting"));
|
|
});
|
|
gui_bridge_set_member_detail_cb(memberDetailCb);
|
|
|
|
static bool nodeinfoRegistered = false;
|
|
if (!nodeinfoRegistered) {
|
|
gui_bridge_set_nodeinfo_update_cb([](const uint8_t* data, int len) {
|
|
if (g_detailTarget) g_detailTarget->onNodeinfoUpdate(data, len);
|
|
});
|
|
gui_bridge_set_bgp_info_cb([](const uint8_t* data, int len) {
|
|
if (!g_detailTarget) return;
|
|
g_detailTarget->m_bgpText = QString::fromUtf8((const char*)data, len);
|
|
g_detailTarget->updateDetailDisplay();
|
|
});
|
|
nodeinfoRegistered = true;
|
|
}
|
|
}
|
|
|
|
AccountList::~AccountList() {
|
|
if (g_detailTarget == this) { g_detailTarget = nullptr; gui_bridge_set_radio_mute_result_cb(nullptr); }
|
|
}
|
|
|
|
void AccountList::setChannel(const QString& channelId) {
|
|
m_channelId = channelId;
|
|
|
|
bool isOwner = false;
|
|
if (m_db && m_db->isOpen()) {
|
|
const auto chs = m_db->getChannels();
|
|
for (const auto& ch : chs) {
|
|
if (ch.channelId == channelId) { isOwner = (ch.ed25519_privkey.size() == 32); break; }
|
|
}
|
|
}
|
|
|
|
m_model->setChannel(channelId, isOwner);
|
|
m_detailTimer->stop();
|
|
m_linkTimer->stop();
|
|
m_detailNodeId = 0;
|
|
m_detailHeader->setText(QCoreApplication::translate("ChatGui", "Select a member"));
|
|
m_detailText->clear();
|
|
|
|
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::onMemberClicked(const QModelIndex& index) {
|
|
if (!index.isValid()) return;
|
|
quint64 nodeId = m_model->nodeIdAt(index.row());
|
|
if (nodeId) showMemberDetail(nodeId);
|
|
}
|
|
|
|
void AccountList::showMemberDetail(quint64 nodeId) {
|
|
if (!m_db || !m_db->m_db) return;
|
|
|
|
m_detailTimer->stop();
|
|
m_linkTimer->stop();
|
|
m_detailNodeId = nodeId;
|
|
m_detailName.clear();
|
|
m_detailOnline = false;
|
|
m_detailLastSeen = 0;
|
|
m_detailCreated = 0;
|
|
m_detailAddrs.clear();
|
|
m_snapConnPresent = false;
|
|
m_snapConnUp = false;
|
|
m_snapConnInit = false;
|
|
m_snapOwnTcpActive = 0;
|
|
m_snapChainRtt = 0;
|
|
m_snapHopCount = 0;
|
|
m_snapBgpNodes = 0;
|
|
m_bgpText.clear();
|
|
m_snapLinks.clear();
|
|
m_snapSocks.clear();
|
|
|
|
sqlite3* d = m_db->m_db;
|
|
sqlite3_stmt* st = nullptr;
|
|
if (sqlite3_prepare_v2(d,
|
|
"SELECT name, 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_detailLastSeen = sqlite3_column_int64(st, 1);
|
|
m_detailCreated = sqlite3_column_int64(st, 2);
|
|
}
|
|
sqlite3_finalize(st);
|
|
}
|
|
|
|
if (m_detailName.isEmpty())
|
|
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper();
|
|
|
|
/* prefer local nickname */
|
|
QString tbl = m_db->peersTableName(m_channelId);
|
|
sqlite3_stmt* nickSt = nullptr;
|
|
QByteArray nickSql = QString(
|
|
"SELECT local_nick FROM \"%1\" WHERE node_id=?"
|
|
).arg(tbl).toUtf8();
|
|
if (sqlite3_prepare_v2(d, nickSql.constData(), -1, &nickSt, nullptr) == SQLITE_OK) {
|
|
sqlite3_bind_int64(nickSt, 1, (sqlite3_int64)nodeId);
|
|
if (sqlite3_step(nickSt) == SQLITE_ROW) {
|
|
QString n = DbManager::colText(nickSt, 0);
|
|
if (!n.isEmpty()) m_detailName = n;
|
|
}
|
|
sqlite3_finalize(nickSt);
|
|
}
|
|
|
|
sqlite3_stmt* na = nullptr;
|
|
sqlite3_prepare_v2(d,
|
|
"SELECT family, protocol, address, port, socket_id, addr_type FROM node_addresses WHERE node_id=?",
|
|
-1, &na, nullptr);
|
|
if (na) {
|
|
sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId);
|
|
while (sqlite3_step(na) == SQLITE_ROW) {
|
|
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);
|
|
a.socket_id = sqlite3_column_int(na, 4);
|
|
a.addr_type = sqlite3_column_int(na, 5);
|
|
if (!a.addr.isEmpty()) m_detailAddrs.append(a);
|
|
}
|
|
sqlite3_finalize(na);
|
|
}
|
|
|
|
std::sort(m_detailAddrs.begin(), m_detailAddrs.end(),
|
|
[](const DetailAddress& a, const DetailAddress& b) {
|
|
if (a.socket_id != b.socket_id) return a.socket_id < b.socket_id;
|
|
if (a.addr_type != b.addr_type) return a.addr_type < b.addr_type;
|
|
if (a.addr != b.addr) return a.addr < b.addr;
|
|
if (a.protocol != b.protocol) return a.protocol < b.protocol;
|
|
return a.port < b.port;
|
|
});
|
|
|
|
m_detailTimer->start();
|
|
m_linkTimer->start();
|
|
updateDetailDisplay();
|
|
requestMemberDetail();
|
|
}
|
|
|
|
void AccountList::requestMemberDetail() {
|
|
if (m_detailNodeId == 0) return;
|
|
struct chat_node_arg* arg = (struct chat_node_arg*)u_malloc(sizeof(struct chat_node_arg));
|
|
if (!arg) { DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "AccountList: req alloc failed"); return; }
|
|
arg->inst = gui_bridge_get_inst();
|
|
arg->node_id = m_detailNodeId;
|
|
gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, arg);
|
|
}
|
|
|
|
void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len) {
|
|
if (nodeId != m_detailNodeId) return; /* stale response from earlier selection */
|
|
if (len < 13) return;
|
|
const uint8_t* p = data;
|
|
/* skip node_id(8) */
|
|
p += 8;
|
|
uint8_t flags = *p++;
|
|
m_snapConnPresent = (flags & 1) != 0;
|
|
m_snapConnUp = (flags & 2) != 0;
|
|
m_snapConnInit = (flags & 4) != 0;
|
|
m_detailOnline = (flags & 8) != 0;
|
|
uint16_t ownTcpActive;
|
|
memcpy(&ownTcpActive, p, 2); p += 2;
|
|
m_snapOwnTcpActive = ownTcpActive;
|
|
|
|
uint8_t linkCount = *p++;
|
|
m_snapLinks.clear();
|
|
for (int i = 0; i < linkCount; i++) {
|
|
if (p + MEMBER_DETAIL_LINK_SIZE > data + len) break;
|
|
MemberLinkSnap lk;
|
|
lk.isTcp = p[0] != 0;
|
|
lk.linkState = p[1];
|
|
lk.linkStatus = p[2];
|
|
lk.tcpReady = p[3] != 0;
|
|
lk.isServer = p[4] != 0;
|
|
lk.localFamily = p[5];
|
|
lk.localAddr = QByteArray((const char*)p + 6, 16);
|
|
memcpy(&lk.localPort, p + 22, 2);
|
|
lk.family = p[24];
|
|
lk.addr = QByteArray((const char*)p + 25, 16);
|
|
memcpy(&lk.port, p + 41, 2);
|
|
memcpy(lk.sockName, p + 43, 20); lk.sockName[19] = '\0';
|
|
lk.isReality = p[63] != 0;
|
|
p += MEMBER_DETAIL_LINK_SIZE;
|
|
m_snapLinks.append(lk);
|
|
}
|
|
|
|
if (p >= data + len) { updateDetailDisplay(); return; }
|
|
uint8_t sockCount = *p++;
|
|
m_snapSocks.clear();
|
|
for (int i = 0; i < sockCount; i++) {
|
|
if (p + MEMBER_DETAIL_SOCK_SIZE > data + len) break;
|
|
MemberSockSnap s;
|
|
memcpy(&s.sockId, p, 4);
|
|
memcpy(&s.port, p + 4, 2);
|
|
uint16_t lu = 0, ld = 0; memcpy(&lu, p + 6, 2); memcpy(&ld, p + 8, 2);
|
|
s.linksUp = lu; s.linksDown = ld;
|
|
s.ifname = QString::fromUtf8((const char*)p + 10).trimmed();
|
|
p += MEMBER_DETAIL_SOCK_SIZE;
|
|
m_snapSocks.append(s);
|
|
}
|
|
|
|
/* resolve interface names from snapshot sockets (by socket_id) */
|
|
for (auto& a : m_detailAddrs) {
|
|
for (const auto& s : m_snapSocks) {
|
|
if (s.sockId == a.socket_id) { a.ifname = s.ifname; break; }
|
|
}
|
|
}
|
|
std::sort(m_detailAddrs.begin(), m_detailAddrs.end(),
|
|
[](const DetailAddress& a, const DetailAddress& b) {
|
|
if (a.socket_id != b.socket_id) return a.socket_id < b.socket_id;
|
|
if (a.addr_type != b.addr_type) return a.addr_type < b.addr_type;
|
|
if (a.addr != b.addr) return a.addr < b.addr;
|
|
if (a.protocol != b.protocol) return a.protocol < b.protocol;
|
|
return a.port < b.port;
|
|
});
|
|
|
|
updateDetailDisplay();
|
|
}
|
|
|
|
void AccountList::updateDetailDisplay() {
|
|
bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false;
|
|
QString nodeHex = QString("0x%1").arg(m_detailNodeId, 16, 16, QChar('0'));
|
|
|
|
/* ── Header ── */
|
|
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
|
|
QStringList hdr;
|
|
hdr.append(QString("<span style='color:%1;'>●</span> %2 %3")
|
|
.arg(dotColor, m_detailName.toHtmlEscaped(), nodeHex));
|
|
if (m_detailLastSeen > 0)
|
|
hdr.append(QCoreApplication::translate("ChatGui", "seen %1").arg(relativeTimeStr(m_detailLastSeen)));
|
|
if (m_detailCreated > 0)
|
|
hdr.append(QCoreApplication::translate("ChatGui", "created %1").arg(relativeTimeStr(m_detailCreated)));
|
|
m_detailHeader->setText(hdr.join(" "));
|
|
|
|
/* ── Body ── */
|
|
QStringList lines;
|
|
|
|
auto protoName = [](int protocol) -> QString {
|
|
if (protocol & 0x04) return (protocol & 0x02) ? "TCP+REALITY" : "REALITY";
|
|
switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; default: return "?"; }
|
|
};
|
|
|
|
if (!m_detailAddrs.isEmpty()) {
|
|
lines.append(QCoreApplication::translate("ChatGui", "── Addresses ──"));
|
|
for (const auto& a : m_detailAddrs) {
|
|
lines.append(QString(" [sock%1] %2 %3 %4:%5 %6")
|
|
.arg(a.socket_id)
|
|
.arg(protoName(a.protocol))
|
|
.arg(a.ifname)
|
|
.arg(addrStr(a.addr.constData(), a.family))
|
|
.arg(a.port)
|
|
.arg(addrRoleStr(a.addr_type)));
|
|
}
|
|
}
|
|
|
|
if (isSelf) {
|
|
lines.append(QCoreApplication::translate("ChatGui", "── Listen ──"));
|
|
for (const auto& s : m_snapSocks) {
|
|
const DetailAddress* a = nullptr;
|
|
for (const auto& da : m_detailAddrs) {
|
|
if (da.socket_id == s.sockId) { a = &da; break; }
|
|
}
|
|
QString proto = a ? protoName(a->protocol) : "?";
|
|
QString ip = a ? addrStr(a->addr.constData(), a->family) : "?";
|
|
lines.append(QString(" [%1/%2] %3:%4 %5")
|
|
.arg(s.linksUp).arg(s.linksDown)
|
|
.arg(ip).arg(s.port)
|
|
.arg(proto));
|
|
}
|
|
} else {
|
|
if (!m_snapLinks.isEmpty()) {
|
|
lines.append(QCoreApplication::translate("ChatGui", "── Connections ──"));
|
|
for (const auto& lk : m_snapLinks) {
|
|
bool up = (lk.linkState == 3 && lk.linkStatus == 1);
|
|
QString c = up ? "#4CAF50" : "#E53935";
|
|
lines.append(QString("<span style='color:%1;'>●</span> %2 %3:%4 %5 %6:%7")
|
|
.arg(c)
|
|
.arg(lk.isReality ? "TCP+REALITY" : (lk.isTcp ? "TCP" : "UDP"))
|
|
.arg(addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort)
|
|
.arg(lk.isServer ? "\u25c0" : "\u25b6")
|
|
.arg(addrStr(lk.addr.constData(), lk.family)).arg(lk.port));
|
|
if (lk.sockName[0])
|
|
lines.append(QString(" %1").arg(lk.sockName));
|
|
}
|
|
}
|
|
/* summary */
|
|
if (!m_bgpText.isEmpty()) {
|
|
lines.append(m_bgpText);
|
|
} else if (m_snapConnUp) {
|
|
lines.append("UP");
|
|
}
|
|
}
|
|
|
|
if (lines.isEmpty())
|
|
m_detailText->clear();
|
|
else
|
|
m_detailText->setHtml(QString("<pre style='margin:0;'>") + lines.join("\n") + "</pre>");
|
|
}
|
|
|
|
void AccountList::onNodeinfoUpdate(const uint8_t* data, int len) {
|
|
if (len < 16 || m_detailNodeId == 0) return;
|
|
uint64_t nodeId; memcpy(&nodeId, data, 8);
|
|
if (nodeId != m_detailNodeId) return;
|
|
m_snapConnPresent = (data[8] & 1) != 0;
|
|
m_snapConnUp = (data[9] & 1) != 0;
|
|
memcpy(&m_snapChainRtt, data + 10, 2);
|
|
m_snapHopCount = data[12];
|
|
memcpy(&m_snapBgpNodes, data + 14, 2);
|
|
/* data[13] is reserved/unused */
|
|
if (m_snapConnPresent)
|
|
updateDetailDisplay();
|
|
}
|
|
|
|
void AccountList::onDetailTimerTick() {
|
|
if (m_detailNodeId != 0) requestMemberDetail();
|
|
}
|
|
|
|
void AccountList::onMemberDoubleClicked(const QModelIndex& index) {
|
|
if (!index.isValid()) return;
|
|
quint64 nodeId = m_model->nodeIdAt(index.row());
|
|
if (!nodeId) return;
|
|
QString name = m_model->data(index, Qt::DisplayRole).toString();
|
|
uint8_t flags = (uint8_t)(m_model->data(index, Qt::UserRole + 3).toInt() & 0xFF);
|
|
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, flags, this);
|
|
if (dlg.exec() == QDialog::Accepted) {
|
|
m_model->refresh();
|
|
emit memberRenamed();
|
|
}
|
|
}
|
|
|
|
void AccountList::onMemberContextMenu(const QPoint& pos) {
|
|
QModelIndex index = m_listView->indexAt(pos);
|
|
if (!index.isValid()) return;
|
|
|
|
quint64 nodeId = m_model->nodeIdAt(index.row());
|
|
if (!nodeId) return;
|
|
|
|
QString name = m_model->data(index, Qt::DisplayRole).toString();
|
|
|
|
QMenu menu(m_listView);
|
|
QAction* pmAction = menu.addAction(QCoreApplication::translate("ChatGui", "Send message"));
|
|
if (nodeId == m_db->myNodeId()) pmAction->setEnabled(false);
|
|
QAction* callAction = menu.addAction(QCoreApplication::translate("ChatGui", "Call peer"));
|
|
if (nodeId == m_db->myNodeId() || !m_model->isOnlineAt(index.row())) callAction->setEnabled(false);
|
|
QAction* connectAction = menu.addAction(QCoreApplication::translate("ChatGui", "Connect"));
|
|
if (nodeId == m_db->myNodeId()) connectAction->setEnabled(false);
|
|
QAction* renameAction = menu.addAction(QCoreApplication::translate("ChatGui", "Rename"));
|
|
if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false);
|
|
QAction* editAction = menu.addAction(QCoreApplication::translate("ChatGui", "Edit"));
|
|
QAction* filesAction = menu.addAction(QCoreApplication::translate("ChatGui", "Member files"));
|
|
QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos));
|
|
if (chosen == filesAction) { emit filesRequested(m_channelId, nodeId); return; }
|
|
|
|
if (chosen == pmAction) {
|
|
emit pmRequested(m_channelId, nodeId, name);
|
|
return;
|
|
}
|
|
|
|
if (chosen == callAction) {
|
|
emit callRequested(m_channelId, nodeId, name);
|
|
return;
|
|
}
|
|
|
|
if (chosen == connectAction) {
|
|
struct chat_connect_node_req* r = (struct chat_connect_node_req*)u_malloc(sizeof(*r));
|
|
if (!r) return;
|
|
memset(r, 0, sizeof(*r));
|
|
r->inst = gui_bridge_get_inst();
|
|
strncpy(r->ch_id, m_channelId.toUtf8().constData(), sizeof(r->ch_id) - 1);
|
|
r->node_id = nodeId;
|
|
gui_bridge_post_uasync_fn(chat_core_connect_node_trampoline, r);
|
|
return;
|
|
}
|
|
|
|
if (chosen == renameAction) {
|
|
RenameDialog dlg(m_db, m_channelId, nodeId, name, this);
|
|
if (dlg.exec() == QDialog::Accepted) {
|
|
m_model->refresh();
|
|
emit memberRenamed();
|
|
}
|
|
return;
|
|
}
|
|
if (chosen != editAction) return;
|
|
|
|
uint8_t flags = (uint8_t)(m_model->data(index, Qt::UserRole + 3).toInt() & 0xFF);
|
|
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, flags, this);
|
|
if (dlg.exec() == QDialog::Accepted) {
|
|
m_model->refresh();
|
|
emit memberRenamed();
|
|
}
|
|
}
|
|
|
|
void AccountList::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
|
|
m_model->onMemberList(chId, count, data, dataLen);
|
|
}
|
|
|
|
void AccountList::onMemberUpdated(const QString& chId, const uint8_t* data) {
|
|
m_model->onMemberUpdated(chId, data);
|
|
}
|
|
|
|
void AccountList::onMemberRemoved(const QString& chId, uint64_t nodeId) {
|
|
m_model->onMemberRemoved(chId, nodeId);
|
|
}
|