#include #include "accountlist.h" #include "addressprofiletext.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 #include #include #include #include #include #include #include #include #include #include #include "../../../lib/platform_compat.h" extern "C" { #include "../lib/socket_compat.h" #include "../lib/debug_config.h" #include "chat/chat_core.h" #include "routing_layer/topo_node.h" #include "member_files/protocol.h" #include "../../lib/mem.h" } static AccountList* g_detailTarget = nullptr; static quint64 g_nextDetailRequest = 0; // только GUI-поток, не сбрасывается при пересоздании панели static void memberDetailCb(const uint8_t* data, int len) { if (!g_detailTarget) return; g_detailTarget->onMemberDetailData(data, len); } 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::radioCallRequested, this, [this](const QModelIndex& index) { const quint64 peer = m_model->nodeIdAt(index.row()); if (!peer || index.data(Qt::UserRole + 2).toBool() || !m_model->isOnlineAt(index.row())) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "member radio call ignored: peer=%016llx online=%d", (unsigned long long)peer, m_model->isOnlineAt(index.row())); return; } DEBUG_INFO(DEBUG_CATEGORY_RADIO, "member radio call clicked ch=%s peer=%016llx", qPrintable(m_channelId), (unsigned long long)peer); emit radioCallRequested(m_channelId, peer); }); connect(delegate, &AccountDelegate::callRequested, this, [this](const QModelIndex& index) { const quint64 peer = m_model->nodeIdAt(index.row()); if (!peer || index.data(Qt::UserRole + 2).toBool() || !m_model->isOnlineAt(index.row())) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "member call ignored: peer=%016llx online=%d", (unsigned long long)peer, m_model->isOnlineAt(index.row())); return; } DEBUG_INFO(DEBUG_CATEGORY_CALL, "member call clicked ch=%s peer=%016llx", qPrintable(m_channelId), (unsigned long long)peer); emit callRequested(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->setObjectName("memberDetailHeader"); 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->setObjectName("memberDetailText"); 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_detailClock.start(); m_detailTimer = new QTimer(this); m_detailTimer->setInterval(1000); connect(m_detailTimer, &QTimer::timeout, this, &AccountList::onDetailTimerTick); connect(m_model, &MemberListModel::snapshotChanged, this, &AccountList::onSnapshotChanged); 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); } AccountList::~AccountList() { clearMemberDetail(); if (g_detailTarget == this) { g_detailTarget = nullptr; gui_bridge_set_radio_mute_result_cb(nullptr); gui_bridge_set_member_detail_cb(nullptr); } } void AccountList::setChannel(const QString& channelId) { if (m_channelId != channelId) clearMemberDetail(); 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_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::clearMemberDetail() { if (m_detailNodeId || m_detailRequest) DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "member detail cleared channel=%s node=%016llx request=%llu", qPrintable(m_channelId), (unsigned long long)m_detailNodeId, (unsigned long long)m_detailRequest); m_detailTimer->stop(); m_detailNodeId = 0; m_detailRequest = 0; m_detailDirty = false; m_detailHops.clear(); m_snapLinks.clear(); m_snapSocks.clear(); m_detailAddrs.clear(); m_detailLastSeen = 0; m_detailCreated = 0; m_detailHeader->setText(QCoreApplication::translate("ChatGui", "Select a member")); m_detailText->clear(); } void AccountList::showEvent(QShowEvent* event) { QWidget::showEvent(event); if (m_detailNodeId) { m_detailTimer->start(); requestMemberDetail(); } } void AccountList::hideEvent(QHideEvent* event) { m_detailTimer->stop(); m_detailRequest = 0; m_detailHops.clear(); QWidget::hideEvent(event); } void AccountList::onSnapshotChanged(quint64 nodeId) { if (!m_detailNodeId || (nodeId && nodeId != m_detailNodeId)) return; if (!m_model->member(m_detailNodeId)) { clearMemberDetail(); return; } m_detailDirty = true; m_detailHops.clear(); for (int row = 0; row < m_model->rowCount(); row++) if (m_model->nodeIdAt(row) == m_detailNodeId) { m_listView->setCurrentIndex(m_model->index(row)); break; } updateDetailDisplay(); requestMemberDetail(); } void AccountList::showMemberDetail(quint64 nodeId) { if (!m_model->member(nodeId)) return; clearMemberDetail(); m_detailNodeId = nodeId; DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "member detail selected channel=%s node=%016llx", qPrintable(m_channelId), (unsigned long long)nodeId); readDetailMetadata(); updateDetailDisplay(); if (isVisible()) { m_detailTimer->start(); requestMemberDetail(); } } /* Только статические сведения БД; имя и live-состояние принадлежат модели участников. */ void AccountList::readDetailMetadata() { m_detailAddrs.clear(); m_detailLastSeen = 0; m_detailCreated = 0; if (!m_db || !m_db->m_db || !m_detailNodeId) return; quint64 nodeId = m_detailNodeId; sqlite3* d = m_db->m_db; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(d, "SELECT last_seen_at,created_at FROM nodes WHERE node_id=?", -1, &st, nullptr) == SQLITE_OK) { sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId); int rc = sqlite3_step(st); if (rc == SQLITE_ROW) { m_detailLastSeen = sqlite3_column_int64(st, 0); m_detailCreated = sqlite3_column_int64(st, 1); } else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail dates read: %s", sqlite3_errmsg(d)); sqlite3_finalize(st); } else DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail dates prepare: %s", sqlite3_errmsg(d)); sqlite3_stmt* na = nullptr; if (sqlite3_prepare_v2(d, "SELECT family, protocol, address, port, socket_id, addr_type, nat_mapping, nat_inbound, nat_source" " FROM node_addresses WHERE node_id=?", -1, &na, nullptr) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail addresses prepare: %s", sqlite3_errmsg(d)); return; } if (na) { sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId); int rc; while ((rc = 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); a.nat_mapping = sqlite3_column_int(na, 6); a.nat_inbound = sqlite3_column_int(na, 7); a.nat_source = sqlite3_column_int(na, 8); if (!a.addr.isEmpty()) m_detailAddrs.append(a); } if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail addresses read: %s", sqlite3_errmsg(d)); 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; }); } void AccountList::requestMemberDetail() { if (!m_detailNodeId || !isVisible() || !gui_bridge_is_uasync_ready()) return; const qint64 now = m_detailClock.elapsed(); if (m_detailRequest && now - m_detailRequestedAt < 5000) return; if (m_detailRequest) DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "member detail timeout channel=%s node=%016llx request=%llu", qPrintable(m_channelId), (unsigned long long)m_detailNodeId, (unsigned long long)m_detailRequest); auto* req = (chat_member_detail_req*)u_calloc(1, sizeof(chat_member_detail_req)); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail request allocation failed"); m_detailRequest = 0; return; } req->inst = gui_bridge_get_inst(); req->group_id = m_channelId.toULongLong(); req->node_id = m_detailNodeId; if (++g_nextDetailRequest == 0) ++g_nextDetailRequest; req->request_id = g_nextDetailRequest; m_detailRequest = req->request_id; m_detailRequestedAt = now; m_detailDirty = false; DEBUG_TRACE(DEBUG_CATEGORY_CONNECTION, "member detail requested channel=%s node=%016llx request=%llu", qPrintable(m_channelId), (unsigned long long)m_detailNodeId, (unsigned long long)m_detailRequest); gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, req); } void AccountList::onMemberDetailData(const uint8_t* data, int len) { chat_member_detail_view view{}; if (len < 0 || chat_core_parse_member_detail(data, (size_t)len, &view) < 0) return; if (!m_detailRequest || view.request_id != m_detailRequest || view.group_id != m_channelId.toULongLong() || view.node_id != m_detailNodeId) { DEBUG_TRACE(DEBUG_CATEGORY_CONNECTION, "member detail ignored stale request=%llu group=%016llx node=%016llx", (unsigned long long)view.request_id, (unsigned long long)view.group_id, (unsigned long long)view.node_id); return; } m_detailRequest = 0; if (view.result != CHAT_MEMBER_DETAIL_OK) { DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "member detail failed channel=%s node=%016llx request=%llu result=%u", qPrintable(m_channelId), (unsigned long long)m_detailNodeId, (unsigned long long)view.request_id, view.result); m_detailHops.clear(); m_snapLinks.clear(); m_snapSocks.clear(); updateDetailDisplay(); return; } if (m_detailDirty) { requestMemberDetail(); return; } QVector links; for (int i = 0; i < view.link_count; i++) { const uint8_t* p = view.links + i * CHAT_MEMBER_DETAIL_LINK_SIZE; 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); lk.localPort = (uint16_t)p[22] | ((uint16_t)p[23] << 8); lk.family = p[24]; lk.addr = QByteArray((const char*)p + 25, 16); lk.port = (uint16_t)p[41] | ((uint16_t)p[42] << 8); memcpy(lk.sockName, p + 43, 20); lk.isReality = p[63] != 0; links.append(lk); } QVector socks; for (int i = 0; i < view.sock_count; i++) { const uint8_t* p = view.socks + i * CHAT_MEMBER_DETAIL_SOCK_SIZE; MemberSockSnap s{}; uint32_t id = 0; for (int j = 0; j < 4; j++) id |= (uint32_t)p[j] << (8 * j); s.sockId = id; s.port = (uint16_t)p[4] | ((uint16_t)p[5] << 8); s.linksUp = (uint16_t)p[6] | ((uint16_t)p[7] << 8); s.linksDown = (uint16_t)p[8] | ((uint16_t)p[9] << 8); s.ifname = QString::fromUtf8((const char*)p + 10); socks.append(s); } QVector hops; for (int i = 0; i < view.hop_count; i++) hops.append(mf_get64(view.hops + i * 8)); m_snapLinks = links; m_snapSocks = socks; m_detailHops = hops; readDetailMetadata(); /* 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; } } } updateDetailDisplay(); } void AccountList::updateDetailDisplay() { const auto member = m_model->member(m_detailNodeId); if (!member) return; const bool isSelf = member->is_self != 0; QString nodeHex = QString("0x%1").arg(m_detailNodeId, 16, 16, QChar('0')); /* ── Header ── */ QString dotColor = member->online ? "#4CAF50" : "#9E9E9E"; QStringList hdr; hdr.append(QString("● %2 %3") .arg(dotColor, QString::fromUtf8(member->name).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(addressProfileText(a.nat_mapping, a.nat_inbound, a.nat_source))); } } 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("● %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)); } } QString route = !member->online ? QCoreApplication::translate("ChatGui", "offline") : member->conn_up & NCONN_DIRECT ? "D" : member->conn_up & NCONN_INDIRECT ? "I" : "B"; if (member->online && member->rtt != 0xffff) route += QString(" · rtt=%1ms").arg(member->rtt / 10.0, 0, 'f', 1); if (member->online && member->intermediaries) route += QString(" · hop:%1").arg(member->intermediaries); lines.prepend(route); if (!m_detailHops.isEmpty() && member->online) { QStringList hops; for (auto it = m_detailHops.crbegin(); it != m_detailHops.crend(); ++it) { QString label = QString("%1").arg(*it, 16, 16, QChar('0')); const auto hop = m_model->member(*it); if (hop) label += ' ' + QString::fromUtf8(hop->name).toHtmlEscaped(); hops.append(label); } lines.append(QString("BGP: ") + hops.join(" → ")); } } if (lines.isEmpty()) m_detailText->clear(); else m_detailText->setHtml(QString("
") + lines.join("\n") + "
"); } 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* inviteAction = menu.addAction(QCoreApplication::translate("ChatGui", "Invite to group")); inviteAction->setEnabled(nodeId != m_db->myNodeId()); QAction* callAction = menu.addAction(QCoreApplication::translate("ChatGui", "Call peer")); if (nodeId == m_db->myNodeId() || !m_model->isOnlineAt(index.row())) callAction->setEnabled(false); QAction* radioCallAction = menu.addAction(QCoreApplication::translate("ChatGui", "Call by radio")); radioCallAction->setEnabled(nodeId != m_db->myNodeId() && m_model->isOnlineAt(index.row())); 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 == inviteAction) { emit inviteRequested(m_channelId, nodeId, name); return; } if (chosen == radioCallAction) { emit radioCallRequested(m_channelId, nodeId); return; } 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); }