/* Qt-модель участников канала: ленивый кэш строк, обновления CHAT_EVT_MEMBER_* и RTT. * События bridge применяются в GUI-потоке; данные строки доступны через Qt roles. */ #ifndef MEMBERLISTMODEL_H #define MEMBERLISTMODEL_H #include #include #include #include #include #include #include #include #include struct MemberListItem { quint64 nodeId = 0; bool online = false; bool isSelf = false; QPixmap avatar; QString name; int rtt = -1; int natType = -1; uint8_t flags = 0; uint8_t storage = 0; }; class MemberListModel : public QAbstractListModel { Q_OBJECT public: /* Высота строки 36 px, между аватарками остаётся 2 px. */ static constexpr int kRowHeight = 36; static constexpr int kAvatarSize = kRowHeight - 2; static constexpr int RadioSpeakingRole = Qt::UserRole + 5; explicit MemberListModel(QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; void setChannel(const QString& channelId, bool isOwner); void refresh(); void refreshRtt(); quint64 nodeIdAt(int row) const; bool isOnlineAt(int row) const; /* Called from bridge callbacks (GUI thread) */ 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); /* Эфемерное состояние рации; вызывается в GUI-потоке, независимо от обновлений строк. */ void setRadioState(quint64 groupId, bool active, bool transmitting); void onRadioTalk(quint64 groupId, quint64 nodeId, bool on); private: void loadBatch(int from, int count); QPixmap makeAvatar(QColor color, QChar letter) const; QString m_channelId; bool m_isOwner = false; int m_totalCount = 0; mutable QMap m_cache; QSet m_radioTalkers; bool m_radioActive = false; bool m_selfTransmitting = false; QTimer* m_rttTimer; }; #endif