Group deleted members separately in the desktop member list

This commit is contained in:
evgeny committed 2026-11-03 14:01:51 +01:00
1 parent 7ca717ff1f
commit e836f07f4d
3 files changed
+28 -8

No files matched your search

+1 -1
View File
@@ -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);
+21 -4
View File
@@ -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<const char*>(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<const char*>(member->devgroup), 32);
}
@@ -339,6 +342,9 @@ void GroupedMemberModel::rebuild(bool forceReset) {
QVector<int> 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);
+6 -3
View File
@@ -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);