etcp: переупорядочить close + защита state==2 во всех API-точках; topo: правильная очистка group nodes и registry; chat/member: новый модуль member_sync; chatgui: обновления транспорта и UI
This commit is contained in:
1 parent
b2e4aadbf0
commit
7f901c01e1
38 files changed
+1275
-540
No files matched your search
@@ -1,11 +1,26 @@
|
||||
#include "memberlistmodel.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include <QPainter>
|
||||
#include <QFont>
|
||||
#include <QTimer>
|
||||
#include <QColor>
|
||||
#include <cstring>
|
||||
|
||||
MemberListModel::MemberListModel(DbManager* db, QObject* parent)
|
||||
: QAbstractListModel(parent), m_db(db) {}
|
||||
extern "C" {
|
||||
#include "../../../src/chat/chat_core.h"
|
||||
#include "../../../src/chat/chat_member.h"
|
||||
#include "../../../lib/mem.h"
|
||||
}
|
||||
|
||||
#define CHAT_MEMBER_DISPLAY_SIZE 79
|
||||
|
||||
MemberListModel::MemberListModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_rttTimer(new QTimer(this))
|
||||
{
|
||||
m_rttTimer->setInterval(2000);
|
||||
connect(m_rttTimer, &QTimer::timeout, this, &MemberListModel::refreshRtt);
|
||||
m_rttTimer->start();
|
||||
}
|
||||
|
||||
int MemberListModel::rowCount(const QModelIndex& parent) const {
|
||||
(void)parent;
|
||||
@@ -18,7 +33,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
|
||||
|
||||
auto it = m_cache.find(index.row());
|
||||
if (it != m_cache.end()) {
|
||||
if (role == Qt::DisplayRole) return it->localNick.isEmpty() ? it->name : it->localNick;
|
||||
if (role == Qt::DisplayRole) return it->name;
|
||||
if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar));
|
||||
if (role == Qt::UserRole + 1) {
|
||||
QString sub;
|
||||
@@ -38,168 +53,118 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
|
||||
if (role == Qt::UserRole + 3) return it->flags;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == Qt::DisplayRole) {
|
||||
scheduleBatch(index.row());
|
||||
return QStringLiteral("\u2026");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void MemberListModel::setChannel(const QString& channelId) {
|
||||
beginResetModel();
|
||||
m_channelId = channelId;
|
||||
m_db->cleanOrphanedPeers(channelId);
|
||||
m_totalCount = m_db ? m_db->getChannelMemberCount(channelId) : 0;
|
||||
m_totalCount = 0;
|
||||
m_cache.clear();
|
||||
m_pendingFrom = -1;
|
||||
endResetModel();
|
||||
if (m_totalCount > 0) scheduleBatch(0);
|
||||
|
||||
if (!channelId.isEmpty()) {
|
||||
char* ch = u_strdup(channelId.toUtf8().constData());
|
||||
gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, ch);
|
||||
}
|
||||
}
|
||||
|
||||
void MemberListModel::refresh() { if (!m_channelId.isEmpty()) setChannel(m_channelId); }
|
||||
void MemberListModel::refresh() {
|
||||
if (!m_channelId.isEmpty()) setChannel(m_channelId);
|
||||
}
|
||||
|
||||
void MemberListModel::refreshRtt() {
|
||||
if (m_channelId.isEmpty()) return;
|
||||
char* ch = u_strdup(m_channelId.toUtf8().constData());
|
||||
gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, ch);
|
||||
}
|
||||
|
||||
quint64 MemberListModel::nodeIdAt(int row) const {
|
||||
auto it = m_cache.find(row);
|
||||
return it != m_cache.end() ? it->nodeId : 0;
|
||||
}
|
||||
|
||||
void MemberListModel::scheduleBatch(int centerRow) const {
|
||||
if (m_pendingFrom >= 0) return;
|
||||
int from = centerRow - BATCH / 2;
|
||||
if (from < 0) from = 0;
|
||||
int maxFrom = m_totalCount - BATCH;
|
||||
if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0;
|
||||
const_cast<MemberListModel*>(this)->m_pendingFrom = from;
|
||||
const_cast<MemberListModel*>(this)->m_pendingCount = BATCH;
|
||||
QTimer::singleShot(0, const_cast<MemberListModel*>(this), &MemberListModel::loadPendingBatch);
|
||||
}
|
||||
static void deserialize_member(const uint8_t* src, MemberListItem& item) {
|
||||
memcpy(&item.nodeId, src, 8);
|
||||
uint8_t online = src[8];
|
||||
item.isSelf = src[9] != 0;
|
||||
item.flags = src[10];
|
||||
uint16_t rtt; memcpy(&rtt, src + 11, 2);
|
||||
item.rtt = (rtt == 0xFFFF) ? -1 : (int)rtt;
|
||||
int nat = (int)src[13];
|
||||
item.natType = (nat == 0xFF) ? -1 : nat;
|
||||
item.online = (online != 0);
|
||||
|
||||
void MemberListModel::loadPendingBatch() {
|
||||
if (m_pendingFrom < 0) return;
|
||||
int from = m_pendingFrom, count = m_pendingCount;
|
||||
m_pendingFrom = -1;
|
||||
loadBatch(from, count);
|
||||
}
|
||||
char name[64];
|
||||
memcpy(name, src + 15, 64); name[63] = '\0';
|
||||
item.name = QString::fromUtf8(name);
|
||||
|
||||
void MemberListModel::loadBatch(int from, int count) {
|
||||
if (!m_db || m_channelId.isEmpty()) return;
|
||||
int maxFrom = m_totalCount - count;
|
||||
if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0;
|
||||
|
||||
auto members = m_db->getChannelMembers(m_channelId, from, count);
|
||||
for (int i = 0; i < members.size(); i++) {
|
||||
int row = from + i;
|
||||
MemberListItem& item = m_cache[row];
|
||||
item.nodeId = members[i].nodeId;
|
||||
item.isSelf = (item.nodeId == m_db->myNodeId());
|
||||
item.online = members[i].online;
|
||||
item.flags = members[i].flags;
|
||||
QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper();
|
||||
item.name = members[i].name.isEmpty() ? shortId : members[i].name;
|
||||
item.localNick = members[i].localNick;
|
||||
QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
item.avatar = makeAvatar(c, item.name[0]);
|
||||
}
|
||||
|
||||
// batch-загрузка RTT и NAT типа из node_addresses
|
||||
QStringList idList;
|
||||
for (int i = 0; i < members.size(); i++)
|
||||
idList.append(QString::number(members[i].nodeId));
|
||||
QString ids = idList.isEmpty() ? QString("0") : idList.join(",");
|
||||
|
||||
QByteArray sql = QString(
|
||||
"SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses"
|
||||
" WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id"
|
||||
).arg(ids).toUtf8();
|
||||
|
||||
sqlite3_stmt* na = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) == SQLITE_OK) {
|
||||
while (sqlite3_step(na) == SQLITE_ROW) {
|
||||
quint64 nid = (quint64)sqlite3_column_int64(na, 0);
|
||||
int rttVal = sqlite3_column_int(na, 1);
|
||||
int at = sqlite3_column_int(na, 2);
|
||||
for (int i = 0; i < members.size(); i++) {
|
||||
if (members[i].nodeId == nid) {
|
||||
int row = from + i;
|
||||
auto it = m_cache.find(row);
|
||||
if (it != m_cache.end()) {
|
||||
it->rtt = rttVal;
|
||||
it->natType = at;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(na);
|
||||
}
|
||||
|
||||
evictDistant(from + BATCH / 2);
|
||||
|
||||
int last = from + (int)members.size() - 1;
|
||||
if (last >= from) emit dataChanged(index(from), index(last));
|
||||
}
|
||||
|
||||
void MemberListModel::refreshRtt() {
|
||||
if (m_cache.isEmpty() || !m_db || !m_db->m_db) return;
|
||||
|
||||
QStringList idList;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it)
|
||||
idList.append(QString::number(it->nodeId));
|
||||
if (idList.isEmpty()) return;
|
||||
QString ids = idList.join(",");
|
||||
|
||||
QByteArray sql = QString(
|
||||
"SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses"
|
||||
" WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id"
|
||||
).arg(ids).toUtf8();
|
||||
|
||||
sqlite3_stmt* na = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) != SQLITE_OK) return;
|
||||
|
||||
QList<int> changed;
|
||||
while (sqlite3_step(na) == SQLITE_ROW) {
|
||||
quint64 nid = (quint64)sqlite3_column_int64(na, 0);
|
||||
int rttVal = sqlite3_column_int(na, 1);
|
||||
int at = sqlite3_column_int(na, 2);
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
|
||||
if (it->nodeId == nid) {
|
||||
if (it->rtt != rttVal || it->natType != at) {
|
||||
it->rtt = rttVal;
|
||||
it->natType = at;
|
||||
changed.append(it.key());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(na);
|
||||
|
||||
for (int row : changed) emit dataChanged(index(row), index(row), {Qt::UserRole + 1});
|
||||
}
|
||||
|
||||
void MemberListModel::evictDistant(int centerRow) {
|
||||
if (m_cache.size() <= MAX_CACHE) return;
|
||||
int half = MAX_CACHE / 2;
|
||||
QList<int> toRemove;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it)
|
||||
if (qAbs(it.key() - centerRow) > half)
|
||||
toRemove.append(it.key());
|
||||
for (int k : toRemove) m_cache.remove(k);
|
||||
}
|
||||
|
||||
QPixmap MemberListModel::makeAvatar(QColor color, QChar letter) const {
|
||||
QPixmap pm(24, 24);
|
||||
pm.fill(Qt::transparent);
|
||||
QColor c = item.online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
QPixmap pm(24, 24); pm.fill(Qt::transparent);
|
||||
QPainter p(&pm);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setBrush(color);
|
||||
p.setBrush(c);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(2, 2, 20, 20);
|
||||
p.setPen(Qt::white);
|
||||
QFont f; f.setBold(true); f.setPixelSize(12);
|
||||
p.setFont(f);
|
||||
p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, QString(letter));
|
||||
p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, item.name.left(1));
|
||||
p.end();
|
||||
return pm;
|
||||
item.avatar = pm;
|
||||
}
|
||||
|
||||
void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
|
||||
(void)dataLen;
|
||||
if (chId != m_channelId) return;
|
||||
|
||||
beginResetModel();
|
||||
m_totalCount = count;
|
||||
m_cache.clear();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
MemberListItem item;
|
||||
deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item);
|
||||
m_cache[i] = item;
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data) {
|
||||
if (chId != m_channelId) return;
|
||||
|
||||
MemberListItem item;
|
||||
deserialize_member(data, item);
|
||||
|
||||
/* find existing row by nodeId */
|
||||
int row = -1;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
|
||||
if (it->nodeId == item.nodeId) { row = it.key(); break; }
|
||||
}
|
||||
|
||||
if (row >= 0) {
|
||||
m_cache[row] = item;
|
||||
emit dataChanged(index(row), index(row));
|
||||
} else {
|
||||
/* new member — add at end */
|
||||
row = m_totalCount++;
|
||||
m_cache[row] = item;
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) {
|
||||
if (chId != m_channelId) return;
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
|
||||
if (it->nodeId == nodeId) {
|
||||
int row = it.key();
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_cache.remove(row);
|
||||
m_totalCount--;
|
||||
endRemoveRows();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user