You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
7.1 KiB
203 lines
7.1 KiB
#include "memberlistmodel.h" |
|
#include "../db/db_manager.h" |
|
#include <QPainter> |
|
#include <QFont> |
|
#include <QTimer> |
|
|
|
MemberListModel::MemberListModel(DbManager* db, QObject* parent) |
|
: QAbstractListModel(parent), m_db(db) {} |
|
|
|
int MemberListModel::rowCount(const QModelIndex& parent) const { |
|
(void)parent; |
|
return m_totalCount; |
|
} |
|
|
|
QVariant MemberListModel::data(const QModelIndex& index, int role) const { |
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_totalCount) |
|
return {}; |
|
|
|
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::DecorationRole) return QVariant::fromValue(QIcon(it->avatar)); |
|
if (role == Qt::UserRole + 1) { |
|
QString sub; |
|
if (it->rtt >= 0) sub += QString("rtt=%1ms").arg(it->rtt / 10); |
|
if (it->natType >= 0) { |
|
static const char* nat[] = {"NET","DIR","EIM","STR"}; |
|
const char* nt = (it->natType <= 3) ? nat[it->natType] : "?"; |
|
if (!sub.isEmpty()) sub += " "; |
|
sub += nt; |
|
} |
|
return sub; |
|
} |
|
if (role == Qt::ToolTipRole) |
|
return QString(it->online ? "online" : "offline") + " | 0x" + |
|
QString("%1").arg(it->nodeId, 16, 16, QChar('0')); |
|
if (role == Qt::UserRole + 2) return it->isSelf; |
|
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_cache.clear(); |
|
m_pendingFrom = -1; |
|
endResetModel(); |
|
if (m_totalCount > 0) scheduleBatch(0); |
|
} |
|
|
|
void MemberListModel::refresh() { if (!m_channelId.isEmpty()) setChannel(m_channelId); } |
|
|
|
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); |
|
} |
|
|
|
void MemberListModel::loadPendingBatch() { |
|
if (m_pendingFrom < 0) return; |
|
int from = m_pendingFrom, count = m_pendingCount; |
|
m_pendingFrom = -1; |
|
loadBatch(from, count); |
|
} |
|
|
|
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; |
|
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); |
|
QPainter p(&pm); |
|
p.setRenderHint(QPainter::Antialiasing); |
|
p.setBrush(color); |
|
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.end(); |
|
return pm; |
|
}
|
|
|