From e836f07f4da4938b032fb4b8de2db335912d94c9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 14:01:51 +0100 Subject: [PATCH] Group deleted members separately in the desktop member list --- tools/chatgui/src/accountlist.cpp | 2 +- tools/chatgui/src/memberlistmodel.cpp | 25 +++++++++++++++++++++---- tools/chatgui/src/memberlistmodel.h | 9 ++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 76a78f1bd..ea56f88ac 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -166,7 +166,7 @@ AccountList::AccountList(DbManager* db, QWidget *parent) layout->addWidget(memberSplitter, 1); m_groupDevicesModel = new QSortFilterProxyModel(this); m_groupDevicesModel->setSourceModel(m_model); - m_groupDevicesModel->setFilterRole(MemberListModel::DevgroupRole); m_groupDevicesModel->setSortRole(Qt::UserRole + 2); + m_groupDevicesModel->setFilterRole(MemberListModel::ActiveDevgroupRole); m_groupDevicesModel->setSortRole(Qt::UserRole + 2); m_groupDevicesModel->sort(0, Qt::DescendingOrder); m_groupDevices = new QListView(m_detailPanel); m_groupDevices->setObjectName("devgroupDevices"); m_groupDevices->setModel(m_groupDevicesModel); m_groupDevices->setItemDelegate(devicesDelegate); diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index 32474dce3..6fe683836 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/tools/chatgui/src/memberlistmodel.cpp @@ -73,7 +73,8 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { if (role == RadioSpeakingRole) return item.snapshot.is_self ? m_selfTransmitting : m_radioTalkers.contains(item.snapshot.node_id); if (role == OnlineRole) return item.snapshot.online; if (role == NodeIdRole || role == AvatarNodeIdRole) return QVariant::fromValue(quint64(item.snapshot.node_id)); - if (role == DevgroupRole) return item.snapshot.devgroup_name[0] ? + if (role == ActiveDevgroupRole && (item.snapshot.flags & CHAT_MEMBER_FLAG_DELETED)) return QString(); + if (role == DevgroupRole || role == ActiveDevgroupRole) return item.snapshot.devgroup_name[0] ? QString::fromLatin1(QByteArray(reinterpret_cast(item.snapshot.devgroup), 32).toHex()) : QString(); return {}; } @@ -324,6 +325,8 @@ void MemberListModel::onRadioTalk(quint64 groupId, quint64 nodeId, bool on) { static QByteArray memberGroup(const MemberListModel* source, int row) { const auto member = source->member(source->nodeIdAt(row)); + // Служебный ключ отличается по длине от любого 32-байтового devgroup. + if (member && (member->flags & CHAT_MEMBER_FLAG_DELETED)) return QByteArrayLiteral("deleted"); if (!member || !member->devgroup_name[0]) return {}; return QByteArray(reinterpret_cast(member->devgroup), 32); } @@ -339,6 +342,9 @@ void GroupedMemberModel::rebuild(bool forceReset) { QVector order; order.reserve(m_source->rowCount()); for (int i = 0; i < m_source->rowCount(); ++i) order.append(i); std::stable_partition(order.begin(), order.end(), [this](int row) { return m_source->index(row).data(Qt::UserRole + 2).toBool(); }); + const auto deleted = std::stable_partition(order.begin(), order.end(), [this](int row) { + return !(m_source->index(row).data(Qt::UserRole + 3).toInt() & CHAT_MEMBER_FLAG_DELETED); + }); for (int i : order) { const auto group = memberGroup(m_source, i); if (group.isEmpty()) { rows.append(i); continue; } @@ -347,9 +353,10 @@ void GroupedMemberModel::rebuild(bool forceReset) { if (m_expanded.contains(group)) for (int j : order) if (memberGroup(m_source, j) == group) rows.append(j); } - if (rows != m_rows) GUI_DEBUG("Member order: self_first=%d rows=%d first_node=%016llx", + if (rows != m_rows) GUI_DEBUG("Member order: self_first=%d rows=%d first_node=%016llx deleted=%d expanded=%d", !order.isEmpty() && m_source->index(order[0]).data(Qt::UserRole + 2).toBool(), int(rows.size()), - (unsigned long long)m_source->nodeIdAt(order.value(0, -1))); + (unsigned long long)m_source->nodeIdAt(order.value(0, -1)), int(order.end() - deleted), + m_expanded.contains(QByteArrayLiteral("deleted"))); if (forceReset || rows != m_rows) { beginResetModel(); m_rows = rows; endResetModel(); } else if (!rows.isEmpty()) emit dataChanged(index(0), index(rows.size()-1)); } @@ -359,11 +366,12 @@ bool GroupedMemberModel::toggle(int row) { if (row < 0 || row >= m_rows.size() || m_rows[row] >= 0) return false; const auto group = memberGroup(m_source, -m_rows[row]-1); if (!m_expanded.remove(group)) m_expanded.insert(group); - GUI_DEBUG("Device group: toggled group=%s expanded=%d", group.toHex().constData(), m_expanded.contains(group)); + GUI_DEBUG("Member group: toggled group=%s expanded=%d", group.toHex().constData(), m_expanded.contains(group)); rebuild(); return true; } int GroupedMemberModel::avatarSourceRow(int row) const { if (row < 0 || row >= m_rows.size()) return -1; + if (m_rows[row] < 0 && memberGroup(m_source, -m_rows[row]-1) == QByteArrayLiteral("deleted")) return -1; return m_rows[row] >= 0 ? m_rows[row] : -m_rows[row]-1; } int GroupedMemberModel::actionSourceRow(int row) const { @@ -371,6 +379,7 @@ int GroupedMemberModel::actionSourceRow(int row) const { if (m_rows[row] >= 0) return m_rows[row]; const int first = -m_rows[row]-1; const auto group = memberGroup(m_source, first); + if (group == QByteArrayLiteral("deleted")) return -1; if (m_source->isOnlineAt(first) && !(m_source->index(first).data(Qt::UserRole + 3).toInt() & MEMBER_FLAG_DELETED)) return first; for (int i = 0; i < m_source->rowCount(); ++i) if (memberGroup(m_source, i) == group && m_source->isOnlineAt(i) && @@ -380,6 +389,14 @@ int GroupedMemberModel::actionSourceRow(int row) const { QVariant GroupedMemberModel::data(const QModelIndex& idx, int role) const { if (!idx.isValid() || idx.model() != this || idx.row() < 0 || idx.row() >= m_rows.size()) return {}; const int row = m_rows[idx.row()], target = actionSourceRow(idx.row()); + const bool deletedHeader = row < 0 && memberGroup(m_source, -row-1) == QByteArrayLiteral("deleted"); + if (role == DeletedGroupRole) return deletedHeader; + if (deletedHeader) { + if (role == GroupHeaderRole) return true; + if (role == GroupExpandedRole) return m_expanded.contains(QByteArrayLiteral("deleted")); + if (role == Qt::DisplayRole) return QCoreApplication::translate("ChatGui", "Deleted accounts"); + return {}; + } if (role == CallAvailableRole) { if (row >= 0) return m_source->isOnlineAt(row) && !m_source->index(row).data(Qt::UserRole + 2).toBool(); const auto group = memberGroup(m_source, -row-1); diff --git a/tools/chatgui/src/memberlistmodel.h b/tools/chatgui/src/memberlistmodel.h index 465312301..08ecc8a78 100644 --- a/tools/chatgui/src/memberlistmodel.h +++ b/tools/chatgui/src/memberlistmodel.h @@ -34,6 +34,7 @@ public: static constexpr int NodeIdRole = Qt::UserRole + 9; // цель действий для обычной строки и её proxy static constexpr int DevgroupRole = Qt::UserRole + 12; // ключ devgroup в hex, пусто без группы static constexpr int AvatarNodeIdRole = Qt::UserRole + 13; // владелец показываемого аватара + static constexpr int ActiveDevgroupRole = Qt::UserRole + 14; // ключ devgroup только неудалённых устройств для фильтра деталей explicit MemberListModel(QObject* parent = nullptr); // parent владеет; собственный timer опрашивает RTT каждые 2 с @@ -84,7 +85,8 @@ private: /* Представление поверх снимков: заголовки не являются устройствами; аватар берётся у первого * участника группы, кнопки действуют на первое online-устройство. RADIO/PHONE-флаги заголовка * объединяют состояния всех неудалённых участников; mute/self относятся к цели кнопок. - * Self/его группа всегда первые, в раскрытой группе self тоже первый; порядок остальных сохраняется. Источник заимствован. */ + * Self/его группа всегда первые, в раскрытой группе self тоже первый; порядок остальных сохраняется. + * Удалённые вынесены в последнюю свёрнутую группу без аватара и действий; их devgroup не учитывается. Источник заимствован. */ class GroupedMemberModel : public QAbstractListModel { Q_OBJECT public: @@ -93,13 +95,14 @@ public: static constexpr int ActionNodeIdRole = Qt::UserRole + 9; // конкретное устройство для кнопок; 0 если в группе нет online static constexpr int GroupBranchRole = Qt::UserRole + 10; // 0 — без ветки; 1 — продолжение; 2 — последнее устройство static constexpr int CallAvailableRole = Qt::UserRole + 11; // Есть доступный адресат кроме локального узла. + static constexpr int DeletedGroupRole = Qt::UserRole + 15; // служебный заголовок «Удалённые», без цели действий static constexpr int DisclosureWidth = 24; // одинаковая левая колонка во всех строках explicit GroupedMemberModel(MemberListModel* source, QObject* parent = nullptr); int rowCount(const QModelIndex& parent = {}) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int sourceRow(int row) const; // -1 для заголовка/неверной строки - int actionSourceRow(int row) const; // заголовок: первое online-устройство, -1 если их нет - int avatarSourceRow(int row) const; // заголовок: первое устройство независимо от online + int actionSourceRow(int row) const; // заголовок devgroup: первое online-устройство; «Удалённые»/нет цели — -1 + int avatarSourceRow(int row) const; // devgroup: первое устройство независимо от online; «Удалённые»/неверная строка — -1 bool toggle(int row); // true — обработан заголовок private: void rebuild(bool forceReset = false);