Keep member rows consistent and own queued refresh requests
This commit is contained in:
1 parent
6c0f9c9068
commit
a8958865bc
5 files changed
+224
-93
No files matched your search
@@ -7,6 +7,9 @@
|
||||
#include <QPainterPath>
|
||||
#include <QFont>
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
@@ -36,8 +39,8 @@ MemberListModel::MemberListModel(QObject* parent)
|
||||
if (auto* manager = AvatarManager::instance()) connect(manager, &AvatarManager::changed, this,
|
||||
[this](const QString& channel, quint64 owner) {
|
||||
if (channel != m_channelId) return;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) if (it->snapshot.node_id == owner) {
|
||||
applyPhoto(*it, channel); emit dataChanged(index(it.key()), index(it.key()), {Qt::DecorationRole});
|
||||
for (int row = 0; row < m_members.size(); ++row) if (m_members[row].snapshot.node_id == owner) {
|
||||
applyPhoto(m_members[row], channel); emit dataChanged(index(row), index(row), {Qt::DecorationRole});
|
||||
}
|
||||
});
|
||||
m_rttTimer->setInterval(2000);
|
||||
@@ -46,55 +49,63 @@ MemberListModel::MemberListModel(QObject* parent)
|
||||
}
|
||||
|
||||
int MemberListModel::rowCount(const QModelIndex& parent) const {
|
||||
(void)parent;
|
||||
return m_totalCount;
|
||||
return parent.isValid() ? 0 : int(m_members.size());
|
||||
}
|
||||
|
||||
QVariant MemberListModel::data(const QModelIndex& index, int role) const {
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_totalCount)
|
||||
if (!index.isValid() || index.model() != this || index.column() != 0 || index.row() < 0 || index.row() >= m_members.size())
|
||||
return {};
|
||||
|
||||
auto it = m_cache.find(index.row());
|
||||
if (it != m_cache.end()) {
|
||||
if (role == Qt::DisplayRole) return it->name;
|
||||
if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar));
|
||||
if (role == Qt::UserRole + 1) {
|
||||
QString sub;
|
||||
if (it->snapshot.rtt != 0xffff) sub += QString("rtt=%1ms").arg(it->snapshot.rtt / 10);
|
||||
if (it->snapshot.nat_type != 0xff) {
|
||||
static const char* nat[] = {"NET","DIR","EIM","DEP","NAT?","CONTACT","EIM?"};
|
||||
const char* nt = (it->snapshot.nat_type <= 6) ? nat[it->snapshot.nat_type] : "?";
|
||||
if (!sub.isEmpty()) sub += " ";
|
||||
sub += nt;
|
||||
}
|
||||
return sub;
|
||||
const auto& item = m_members.at(index.row());
|
||||
if (role == Qt::DisplayRole) return item.name;
|
||||
if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(item.avatar));
|
||||
if (role == Qt::UserRole + 1) {
|
||||
QString sub;
|
||||
if (item.snapshot.rtt != 0xffff) sub += QString("rtt=%1ms").arg(item.snapshot.rtt / 10);
|
||||
if (item.snapshot.nat_type != 0xff) {
|
||||
static const char* nat[] = {"NET","DIR","EIM","DEP","NAT?","CONTACT","EIM?"};
|
||||
const char* nt = (item.snapshot.nat_type <= 6) ? nat[item.snapshot.nat_type] : "?";
|
||||
if (!sub.isEmpty()) sub += " ";
|
||||
sub += nt;
|
||||
}
|
||||
if (role == Qt::ToolTipRole)
|
||||
return (it->snapshot.online ? QCoreApplication::translate("ChatGui",
|
||||
"online") : QCoreApplication::translate("ChatGui",
|
||||
"offline")) + " | 0x" +
|
||||
QString("%1").arg(it->snapshot.node_id, 16, 16, QChar('0')) +
|
||||
((it->snapshot.flags & CHAT_MEMBER_FLAG_RADIO) ? QCoreApplication::translate("ChatGui", " · Radio enabled") : QString());
|
||||
if (role == Qt::UserRole + 2) return it->snapshot.is_self;
|
||||
if (role == Qt::UserRole + 3) return it->snapshot.flags;
|
||||
if (role == Qt::UserRole + 4) return (bool)it->snapshot.storage;
|
||||
if (role == RadioSpeakingRole) return it->snapshot.is_self ? m_selfTransmitting : m_radioTalkers.contains(it->snapshot.node_id);
|
||||
if (role == OnlineRole) return it->snapshot.online;
|
||||
return {};
|
||||
return sub;
|
||||
}
|
||||
if (role == Qt::ToolTipRole)
|
||||
return QCoreApplication::translate("ChatGui", item.snapshot.online ? "online" : "offline") + " | 0x" +
|
||||
QString("%1").arg(item.snapshot.node_id, 16, 16, QChar('0')) +
|
||||
((item.snapshot.flags & CHAT_MEMBER_FLAG_RADIO) ? QCoreApplication::translate("ChatGui", " · Radio enabled") : QString());
|
||||
if (role == Qt::UserRole + 2) return item.snapshot.is_self;
|
||||
if (role == Qt::UserRole + 3) return item.snapshot.flags;
|
||||
if (role == Qt::UserRole + 4) return bool(item.snapshot.storage);
|
||||
if (role == RadioSpeakingRole) return item.snapshot.is_self ? m_selfTransmitting : m_radioTalkers.contains(item.snapshot.node_id);
|
||||
if (role == OnlineRole) return item.snapshot.online;
|
||||
return {};
|
||||
}
|
||||
|
||||
void MemberListModel::setChannel(const QString& channelId, bool isOwner) {
|
||||
if (m_channelId != channelId) {
|
||||
beginResetModel();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: channel %s -> %s cleared talkers=%d self_tx=%d",
|
||||
qPrintable(m_channelId), qPrintable(channelId), int(m_radioTalkers.size()), m_selfTransmitting);
|
||||
m_radioTalkers.clear(); m_radioActive = false; m_selfTransmitting = false;
|
||||
m_totalCount = 0; m_cache.clear(); endResetModel();
|
||||
emit snapshotChanged(0);
|
||||
const QByteArray channel = channelId.toUtf8();
|
||||
if (channel.size() >= int(sizeof(chat_member_list_req::ch_id)) || channel.contains('\0')) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member channel rejected: bytes=%lld embedded_nul=%d limit=63",
|
||||
(long long)channel.size(), channel.contains('\0')); return;
|
||||
}
|
||||
m_channelId = channelId; m_isOwner = isOwner;
|
||||
const bool changed = m_channelId != channelId;
|
||||
if (changed || (m_isOwner && !isOwner)) {
|
||||
beginResetModel();
|
||||
if (changed) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: channel %s -> %s cleared talkers=%d self_tx=%d",
|
||||
qPrintable(m_channelId), qPrintable(channelId), int(m_radioTalkers.size()), m_selfTransmitting);
|
||||
m_channelId = channelId;
|
||||
m_radioTalkers.clear(); m_radioActive = false; m_selfTransmitting = false;
|
||||
m_members.clear();
|
||||
} else {
|
||||
m_members.erase(std::remove_if(m_members.begin(), m_members.end(), [](const MemberListItem& item) {
|
||||
return item.snapshot.flags & CHAT_MEMBER_FLAG_DELETED;
|
||||
}), m_members.end());
|
||||
}
|
||||
m_isOwner = isOwner;
|
||||
endResetModel();
|
||||
emit snapshotChanged(0);
|
||||
} else m_isOwner = isOwner;
|
||||
refresh();
|
||||
}
|
||||
|
||||
@@ -104,7 +115,9 @@ void MemberListModel::refresh() {
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member list request allocation failed"); return; }
|
||||
req->inst = gui_bridge_get_inst();
|
||||
strncpy(req->ch_id, m_channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, req);
|
||||
if (gui_bridge_post_uasync_owned_fn(chat_core_request_member_list_trampoline, req) < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member list request rejected ch=%s", qPrintable(m_channelId)); u_free(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,27 +127,28 @@ void MemberListModel::refreshRtt() {
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member RTT request allocation failed"); return; }
|
||||
req->inst = gui_bridge_get_inst();
|
||||
strncpy(req->ch_id, m_channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, req);
|
||||
if (gui_bridge_post_uasync_owned_fn(chat_core_request_member_rtt_trampoline, req) < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member RTT request rejected ch=%s", qPrintable(m_channelId)); u_free(req);
|
||||
}
|
||||
}
|
||||
|
||||
quint64 MemberListModel::nodeIdAt(int row) const {
|
||||
auto it = m_cache.find(row);
|
||||
return it != m_cache.end() ? it->snapshot.node_id : 0;
|
||||
return row >= 0 && row < m_members.size() ? m_members.at(row).snapshot.node_id : 0;
|
||||
}
|
||||
|
||||
bool MemberListModel::isOnlineAt(int row) const {
|
||||
auto it = m_cache.find(row);
|
||||
return it != m_cache.end() ? it->snapshot.online : false;
|
||||
return row >= 0 && row < m_members.size() ? m_members.at(row).snapshot.online : false;
|
||||
}
|
||||
|
||||
std::optional<chat_member_display> MemberListModel::member(quint64 nodeId) const {
|
||||
for (auto it = m_cache.cbegin(); it != m_cache.cend(); ++it)
|
||||
if (it->snapshot.node_id == nodeId) return it->snapshot;
|
||||
for (const auto& item : m_members) if (item.snapshot.node_id == nodeId) return item.snapshot;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static void deserialize_member(const uint8_t* src, MemberListItem& item) {
|
||||
chat_core_deserialize_member(src, &item.snapshot);
|
||||
static bool deserialize_member(const uint8_t* src, MemberListItem& item) {
|
||||
if (chat_core_deserialize_member(src, &item.snapshot) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot decode failed: missing data"); return false;
|
||||
}
|
||||
item.name = QString::fromUtf8(item.snapshot.name);
|
||||
|
||||
QColor c = item.snapshot.online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
@@ -151,6 +165,7 @@ static void deserialize_member(const uint8_t* src, MemberListItem& item) {
|
||||
p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, item.name.left(1));
|
||||
p.end();
|
||||
item.avatar = pm;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
|
||||
@@ -160,20 +175,20 @@ void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member list: invalid count=%d bytes=%d", count, dataLen); return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_totalCount = 0;
|
||||
m_cache.clear();
|
||||
|
||||
int row = 0;
|
||||
QVector<MemberListItem> members;
|
||||
members.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
MemberListItem item;
|
||||
deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item);
|
||||
if (!deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item)) return;
|
||||
applyPhoto(item, chId);
|
||||
if ((item.snapshot.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) continue;
|
||||
m_cache[row++] = item;
|
||||
members.append(item);
|
||||
}
|
||||
m_totalCount = row;
|
||||
beginResetModel();
|
||||
m_members = std::move(members);
|
||||
endResetModel();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "member list applied ch=%s count=%d visible=%d owner=%d",
|
||||
qPrintable(chId), count, int(m_members.size()), m_isOwner);
|
||||
emit snapshotChanged(0);
|
||||
}
|
||||
|
||||
@@ -181,13 +196,13 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data)
|
||||
if (chId != m_channelId) return;
|
||||
|
||||
MemberListItem item;
|
||||
deserialize_member(data, item);
|
||||
if (!deserialize_member(data, item)) return;
|
||||
applyPhoto(item, chId);
|
||||
|
||||
/* find existing row by nodeId */
|
||||
int row = -1;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
|
||||
if (it->snapshot.node_id == item.snapshot.node_id) { row = it.key(); break; }
|
||||
for (int i = 0; i < m_members.size(); ++i) {
|
||||
if (m_members[i].snapshot.node_id == item.snapshot.node_id) { row = i; break; }
|
||||
}
|
||||
|
||||
if ((item.snapshot.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) {
|
||||
@@ -197,14 +212,13 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data)
|
||||
}
|
||||
|
||||
if (row >= 0) {
|
||||
m_cache[row] = item;
|
||||
m_members[row] = item;
|
||||
emit dataChanged(index(row), index(row));
|
||||
} else {
|
||||
/* new member — add at end */
|
||||
row = m_totalCount;
|
||||
row = int(m_members.size());
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
m_cache[row] = item;
|
||||
m_totalCount++;
|
||||
m_members.append(item);
|
||||
endInsertRows();
|
||||
}
|
||||
emit snapshotChanged(item.snapshot.node_id);
|
||||
@@ -216,13 +230,10 @@ void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: removed talker ch=%s node=%016llx",
|
||||
qPrintable(chId), (unsigned long long)nodeId);
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
|
||||
if (it->snapshot.node_id == nodeId) {
|
||||
int row = it.key();
|
||||
for (int row = 0; row < m_members.size(); ++row) {
|
||||
if (m_members[row].snapshot.node_id == nodeId) {
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_cache.remove(row);
|
||||
for (int next = row + 1; next < m_totalCount; ++next) m_cache[next - 1] = m_cache.take(next);
|
||||
m_totalCount--;
|
||||
m_members.removeAt(row);
|
||||
endRemoveRows();
|
||||
emit snapshotChanged(nodeId);
|
||||
return;
|
||||
@@ -239,7 +250,7 @@ void MemberListModel::setRadioState(quint64 groupId, bool active, bool transmitt
|
||||
qPrintable(m_channelId), (unsigned long long)groupId, active, transmitting, int(m_radioTalkers.size()));
|
||||
m_radioActive = active; m_selfTransmitting = transmitting;
|
||||
if (!active) m_radioTalkers.clear();
|
||||
if (m_totalCount) emit dataChanged(index(0), index(m_totalCount - 1), {RadioSpeakingRole});
|
||||
if (!m_members.isEmpty()) emit dataChanged(index(0), index(m_members.size() - 1), {RadioSpeakingRole});
|
||||
}
|
||||
|
||||
/* START может прийти до списка участников; STOP снимает только указанный источник. */
|
||||
@@ -259,8 +270,8 @@ void MemberListModel::onRadioTalk(quint64 groupId, quint64 nodeId, bool on) {
|
||||
else m_radioTalkers.remove(nodeId);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: talk ch=%s node=%016llx on=%d talkers=%d",
|
||||
qPrintable(m_channelId), (unsigned long long)nodeId, on, int(m_radioTalkers.size()));
|
||||
for (auto it = m_cache.cbegin(); it != m_cache.cend(); ++it) if (it->snapshot.node_id == nodeId) {
|
||||
emit dataChanged(index(it.key()), index(it.key()), {RadioSpeakingRole});
|
||||
for (int row = 0; row < m_members.size(); ++row) if (m_members[row].snapshot.node_id == nodeId) {
|
||||
emit dataChanged(index(row), index(row), {RadioSpeakingRole});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
/* Qt-модель участников выбранного канала: полные снимки CHAT_EVT_MEMBER_* в GUI-потоке.
|
||||
* Список читает Qt roles, карточка — копию той же записи через member(nodeId). */
|
||||
/* Плоская Qt-модель участников одного канала, только GUI-поток с QApplication.
|
||||
* Хранит полные снимки CHAT_EVT_MEMBER_*, без чтения живых структур ядра/БД.
|
||||
* Последовательный массив совпадает с порядком строк; member(nodeId) возвращает независимую копию.
|
||||
* Полный снимок проверяется/готовится до reset, обновление одной записи заменяет её целиком.
|
||||
* Без isOwner удалённые записи скрыты, смена канала очищает снимки и эфемерную речь.
|
||||
* Запросы идут через owned bridge API: остановка ядра отменяет их и освобождает аргументы. */
|
||||
#ifndef MEMBERLISTMODEL_H
|
||||
#define MEMBERLISTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QTimer>
|
||||
#include <stdint.h>
|
||||
@@ -30,20 +32,26 @@ public:
|
||||
static constexpr int RadioSpeakingRole = Qt::UserRole + 5;
|
||||
static constexpr int OnlineRole = Qt::UserRole + 6; /* живой статус BGP для кнопок вызова */
|
||||
|
||||
explicit MemberListModel(QObject* parent = nullptr);
|
||||
explicit MemberListModel(QObject* parent = nullptr); // parent владеет; собственный timer опрашивает RTT каждые 2 с
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/* channelId — строковый ID до 63 байт UTF-8, пустой очищает модель. Смена owner-политики
|
||||
* скрывает удалённых сразу; полный refresh асинхронный, при недоступном ядре сохраняет снимок. */
|
||||
void setChannel(const QString& channelId, bool isOwner);
|
||||
void refresh();
|
||||
void refreshRtt();
|
||||
quint64 nodeIdAt(int row) const;
|
||||
bool isOnlineAt(int row) const;
|
||||
void refresh(); // запрос полного списка, без очистки показанных строк
|
||||
void refreshRtt(); // запрос полных MEMBER_UPDATED, включая все live-поля, не только RTT
|
||||
quint64 nodeIdAt(int row) const; // 0 вне диапазона
|
||||
bool isOnlineAt(int row) const; // false вне диапазона; online из последнего снимка, не из БД
|
||||
/* Копия записи: указатели в row-cache нельзя сохранять через reset/обновления. */
|
||||
std::optional<chat_member_display> member(quint64 nodeId) const;
|
||||
|
||||
/* Called from bridge callbacks (GUI thread) */
|
||||
/* GUI callbacks, data заимствован на время вызова. Другой chId игнорируется.
|
||||
* List требует ровно count*CHAT_MEMBER_DISPLAY_SIZE байтов; (0,NULL,0) очищает список.
|
||||
* Updated требует доступный полный CHAT_MEMBER_DISPLAY_SIZE: длину проверяет caller/bridge codec.
|
||||
* NULL/ошибка списка сохраняют прежнюю модель; неизвестный nodeId добавляется в конец.
|
||||
* Removed сдвигает следующие строки с корректными Qt-уведомлениями; отсутствие допустимо. */
|
||||
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
|
||||
void onMemberUpdated(const QString& chId, const uint8_t* data);
|
||||
void onMemberRemoved(const QString& chId, uint64_t nodeId);
|
||||
@@ -58,14 +66,13 @@ signals:
|
||||
|
||||
private:
|
||||
|
||||
QString m_channelId;
|
||||
bool m_isOwner = false;
|
||||
int m_totalCount = 0;
|
||||
mutable QMap<int, MemberListItem> m_cache;
|
||||
QSet<quint64> m_radioTalkers;
|
||||
bool m_radioActive = false;
|
||||
bool m_selfTransmitting = false;
|
||||
QTimer* m_rttTimer;
|
||||
QString m_channelId; // выбранный канал, публикуется до modelReset/snapshotChanged
|
||||
bool m_isOwner = false; // разрешает показывать удалённых участников
|
||||
QVector<MemberListItem> m_members; // собственные снимки, индекс массива == номер строки
|
||||
QSet<quint64> m_radioTalkers; // START может прийти до загрузки строки; STOP/stop radio/смена канала очищают
|
||||
bool m_radioActive = false; // приём START только для активной рации выбранного канала
|
||||
bool m_selfTransmitting = false; // PTT/VAD из аудио-движка, не состояние удалённых источников
|
||||
QTimer* m_rttTimer; // QObject-child, останавливается при уничтожении модели
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include <QtTest>
|
||||
#include <QListView>
|
||||
#include <QAbstractItemModelTester>
|
||||
#include <vector>
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
@@ -17,6 +18,9 @@ static UTUN_INSTANCE instance{};
|
||||
static std::vector<chat_member_detail_req> requests;
|
||||
static gui_member_detail_fn detailCallback;
|
||||
static bool ready = true;
|
||||
static bool rejectOwned = false;
|
||||
static int ownedPosts = 0;
|
||||
static std::vector<chat_member_list_req> memberRequests;
|
||||
|
||||
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; }
|
||||
extern "C" int gui_bridge_is_uasync_ready() { return ready; }
|
||||
@@ -25,8 +29,16 @@ extern "C" void gui_bridge_set_radio_mute_result_cb(gui_dm_messages_fn) {}
|
||||
extern "C" int gui_bridge_toggle_member_radio_mute(const char*, uint64_t) { return 0; }
|
||||
extern "C" void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) {
|
||||
if (fn == chat_core_collect_member_detail_trampoline) requests.push_back(*static_cast<chat_member_detail_req*>(arg));
|
||||
if (fn == chat_core_request_member_list_trampoline || fn == chat_core_request_member_rtt_trampoline)
|
||||
memberRequests.push_back(*static_cast<chat_member_list_req*>(arg));
|
||||
u_free(arg);
|
||||
}
|
||||
extern "C" int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg) {
|
||||
++ownedPosts;
|
||||
if (rejectOwned) return -1;
|
||||
gui_bridge_post_uasync_fn(fn, arg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static QByteArray row(quint64 node, bool online = true, int up = 5, int rtt = 30) {
|
||||
chat_member_display member{};
|
||||
@@ -65,7 +77,81 @@ class MemberDetailTest : public QObject {
|
||||
return QMetaObject::invokeMethod(&p, "onMemberClicked", Qt::DirectConnection, Q_ARG(QModelIndex, p.model()->index(rowNumber)));
|
||||
}
|
||||
private slots:
|
||||
void init() { requests.clear(); ready = true; debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); }
|
||||
void init() {
|
||||
requests.clear(); memberRequests.clear(); ownedPosts = 0; rejectOwned = false; ready = true;
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
|
||||
}
|
||||
|
||||
void flatModelPreservesRowsSnapshotsAndPersistentIndexes() {
|
||||
MemberListModel model;
|
||||
QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest);
|
||||
model.setChannel("123", false);
|
||||
auto bytes = row(7) + row(8) + row(9);
|
||||
model.onMemberList("123", 3, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
QCOMPARE(model.rowCount(model.index(0)), 0);
|
||||
const QPersistentModelIndex last = model.index(2);
|
||||
const auto snapshot = model.member(9); QVERIFY(snapshot);
|
||||
model.onMemberRemoved("123", 7);
|
||||
QCOMPARE(last.row(), 1); QCOMPARE(model.nodeIdAt(last.row()), quint64(9));
|
||||
const auto changed = row(9, false, 0, 0xffff);
|
||||
model.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(changed.constData()));
|
||||
QVERIFY(snapshot->online); QVERIFY(!model.member(9)->online);
|
||||
const auto added = row(10);
|
||||
model.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(added.constData()));
|
||||
QCOMPARE(model.rowCount(), 3); QCOMPARE(model.nodeIdAt(2), quint64(10));
|
||||
QVERIFY(!model.member(7)); QCOMPARE(model.nodeIdAt(-1), quint64(0));
|
||||
}
|
||||
|
||||
void snapshotChangedSeesNewChannel() {
|
||||
MemberListModel model; model.setChannel("123", false);
|
||||
memberRequests.clear();
|
||||
connect(&model, &MemberListModel::snapshotChanged, &model, [&] { model.refresh(); });
|
||||
model.setChannel("456", false);
|
||||
QCOMPARE(memberRequests.size(), size_t(2));
|
||||
for (const auto& request : memberRequests) QCOMPARE(QString::fromUtf8(request.ch_id), QString("456"));
|
||||
}
|
||||
|
||||
void losingOwnerPolicyHidesDeletedRowsWithoutCore() {
|
||||
MemberListModel model; model.setChannel("123", true);
|
||||
auto bytes = row(7) + row(8);
|
||||
bytes[CHAT_MEMBER_DISPLAY_SIZE + 10] = CHAT_MEMBER_FLAG_DELETED;
|
||||
model.onMemberList("123", 2, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
ready = false; model.setChannel("123", false);
|
||||
QCOMPARE(model.rowCount(), 1); QVERIFY(!model.member(8));
|
||||
}
|
||||
|
||||
void malformedUpdatesKeepExistingSnapshot() {
|
||||
MemberListModel model; model.setChannel("123", false);
|
||||
const auto bytes = row(7);
|
||||
model.onMemberList("123", 1, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
model.onMemberList("123", 2, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
model.onMemberList("123", -1, nullptr, -1);
|
||||
model.onMemberUpdated("123", nullptr);
|
||||
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.nodeIdAt(0), quint64(7));
|
||||
}
|
||||
|
||||
void rejectedMemberRequestsReleaseArguments() {
|
||||
const auto before = u_get_allocated_count();
|
||||
rejectOwned = true;
|
||||
MemberListModel model; model.setChannel("123", false);
|
||||
model.refresh(); model.refreshRtt();
|
||||
QCOMPARE(ownedPosts, 3);
|
||||
QCOMPARE(u_get_allocated_count(), before);
|
||||
}
|
||||
|
||||
void invalidChannelDoesNotReplaceVisibleSnapshotOrSendRequests() {
|
||||
MemberListModel model; model.setChannel("123", false);
|
||||
const auto bytes = row(7);
|
||||
model.onMemberList("123", 1, reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
memberRequests.clear();
|
||||
model.setChannel(QString(64, '1'), false);
|
||||
model.setChannel(QString::fromUtf8("123\0suffix", 10), false);
|
||||
QCOMPARE(model.rowCount(), 1); QCOMPARE(model.nodeIdAt(0), quint64(7));
|
||||
QVERIFY(memberRequests.empty());
|
||||
model.refresh(); QCOMPARE(memberRequests.size(), size_t(1));
|
||||
QCOMPARE(QString::fromUtf8(memberRequests.back().ch_id), QString("123"));
|
||||
}
|
||||
|
||||
void modelOwnsPresenceAndRttAndRefreshPreservesSelection() {
|
||||
AccountList panel; panel.resize(600, 800); panel.show();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QListView>
|
||||
#include <QScopeGuard>
|
||||
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
@@ -16,6 +17,7 @@ extern "C" {
|
||||
#include "routing_layer/topo_node_sqlite.h"
|
||||
#include "u_async.h"
|
||||
#include "debug_config.h"
|
||||
#include "mem.h"
|
||||
}
|
||||
|
||||
struct Probe {
|
||||
@@ -81,6 +83,26 @@ static void mute_result(const uint8_t* data, int len) {
|
||||
class RadioMuteBridgeTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void pendingMemberRequestsAreFreedBeforeCoreDestroy() {
|
||||
gui_bridge_init();
|
||||
const auto before = u_get_allocated_count();
|
||||
{
|
||||
UTUN_INSTANCE instance{};
|
||||
instance.ua = uasync_create(); QVERIFY(instance.ua);
|
||||
const auto release = qScopeGuard([&] {
|
||||
gui_bridge_set_inst(nullptr); gui_bridge_set_uasync(nullptr); uasync_destroy(instance.ua, 1);
|
||||
});
|
||||
gui_bridge_set_uasync(instance.ua); gui_bridge_set_inst(&instance);
|
||||
const auto coreOnly = u_get_allocated_count();
|
||||
MemberListModel model; model.setChannel("123", false); model.refreshRtt();
|
||||
QVERIFY(u_get_allocated_count() > coreOnly);
|
||||
gui_bridge_set_inst(nullptr);
|
||||
QCOMPARE(u_get_allocated_count(), coreOnly);
|
||||
QVERIFY(!instance.ua->posted_tasks_head);
|
||||
}
|
||||
QCOMPARE(u_get_allocated_count(), before);
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
gui_bridge_set_member_updated_cb(nullptr); gui_bridge_set_radio_mute_result_cb(nullptr);
|
||||
gui_bridge_set_member_list_cb(nullptr); gui_bridge_set_radio_talk_cb(nullptr);
|
||||
|
||||
@@ -42,6 +42,11 @@ extern "C" void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) {
|
||||
if (fn == chat_core_request_member_list_trampoline || fn == chat_core_request_member_rtt_trampoline) { u_free(arg); return; }
|
||||
uasync_post(instance.ua, fn, arg);
|
||||
}
|
||||
extern "C" int gui_bridge_post_uasync_owned_fn(void (*fn)(void*), void* arg) {
|
||||
if (!instance.ua) return -1;
|
||||
gui_bridge_post_uasync_fn(fn, arg);
|
||||
return 0;
|
||||
}
|
||||
extern "C" int __wrap_radio_talk_begin(UTUN_INSTANCE*, uint64_t) { ++begins; return 0; }
|
||||
extern "C" int __wrap_radio_talk_end(UTUN_INSTANCE*, uint64_t) { ++ends; return 0; }
|
||||
extern "C" int __wrap_radio_talk_send(UTUN_INSTANCE*, uint64_t, const uint8_t*, int) { ++packets; return 0; }
|
||||
|
||||
Reference in new issue
Block a user