Files
utun2/tools/chatgui/src/messagelist.h
T

179 lines
13 KiB
C++

/* История и composer выбранного канала/PM, read/scroll, загрузки, feedback, рация и каталог streams.
* Все методы/callbacks — GUI. Сетевые операции передаются мосту с владением запросом и привязкой instance.
* Модель держит самостоятельные Qt-значения; DbManager/AudioRecorder заимствованы.
* PM снимки проверяются по conv/generation, канальные строки — по message_id/version.
* Полная история ограничена 200 строками; окно старой цитаты защищено от вытеснения свежими сообщениями.
* messagefeedback.cpp реализует опросы/реакции этого же класса. */
#pragma once
#include <QWidget>
#include <QStandardItemModel>
#include <QHash>
#include <QTimer>
#include <QSet>
#include <QThreadPool>
#include <QJsonObject>
#include <QPointer>
#include "stream_sources.h"
class ChatView;
class InputBar;
class EmojiPanel;
class DbManager;
class ChannelList;
class AudioRecorder;
class QLabel;
class QPushButton;
class RadioPanel;
class QDialog;
class QMenu;
class QTreeWidget;
struct MessageRow;
class MessageList : public QWidget {
Q_OBJECT
public:
// db/recorder заимствованы; Qt parent владеет виджетом. nullptr db допустим для пустого UI.
explicit MessageList(DbManager* db = nullptr, AudioRecorder* recorder = nullptr,
QWidget *parent = nullptr);
// Сохранить канал, сменить target; для dm:<id> зафиксировать generation и запросить снимок.
void loadChannel(const QString& channelId);
// Добавить отсутствующие сообщения (до 200); не вытеснять историческое окно цитаты.
void refresh();
// Перечитать имена, обновить авторов и превью цитат.
void reloadLocalNicks();
// Для CHAT поставить read/верхнюю позицию в ui_state; окно старой цитаты сохраняет свежий watermark. PM пропускается.
void saveChannelState();
// После остановки AnimTimer удалить общие LottieIcon; не вызывать при их отрисовке.
static void cleanupAnimations();
// Обновить только (chId,msgId) текущей модели без сетевого запроса.
void updateMessageProgress(const QString& chId, int64_t msgId, int blocksDone, int numBlocks);
// Перечитать путь/роли; отложенный пользовательский play требует прежний target.
void onAttachmentDownloaded(const QString& chId, int64_t msgId);
// [conv_len,conv,generation,JSON]: чужое/старое поколение не применяется.
void onDmMessages(const uint8_t* data, int len);
/* Блокировать отправки ожидающего удаления; forget очищает UI только для удалённого поколения и возвращает true. */
void setDmDeleting(const QString& convId, quint64 generation, bool deleting);
bool forgetDmHistory(const QString& convId, quint64 generation);
// Запросить адресата/общую группу; отказ очищает доступность кнопки.
void refreshDmCallRoute();
// [conv_len,conv,peer,group] только для выбранного PM; group=0 отключает звонок.
void onDmCallRoute(const uint8_t* data, int len);
// Сбросить peer/group и показать offline.
void clearDmCallRoute();
// Проверить codec и передать target/job/state composer.
void onAttachmentPreparation(const uint8_t* data, int len);
// Запросить свежие реакции/опрос для изменённого сообщения текущего канала.
void onFeedbackChanged(const uint8_t* data, int len);
// Проверить результат/JSON-снимок; чужой канал не меняет модель.
void onFeedbackResult(const uint8_t* data, int len);
// 9 байт (request_id LE,success) подтверждают/отклоняют зафиксированный черновик.
void onMessageSubmitted(const uint8_t* data, int len);
// Применить новую версию подписанного item с сохранением scroll и превью ответов.
void onMessageUpdated(const uint8_t* data, int len);
// Обновить encrypted-вложение и диалог выдачи ключей текущего канала.
void onAttachmentKeysChanged(const uint8_t* data, int len);
void onRadioTalk(quint64 groupId, quint64 srcNodeId, bool on); /* индикация говорящего */
bool enableRadio(); /* идемпотентное включение рации выбранного канала, false при ошибке аудио */
// Применить проверенный полный каталог выбранной CHAT-группы.
void onStreamSources(const uint8_t* data, int len);
// Очистить каталог/кнопку remote streams.
void clearStreams();
// Наблюдать CHAT-группу; пустой target/PM передаёт 0 и снимает наблюдение.
void watchStreams();
/* Показывает кнопку своей публикации во всех каналах; group=0 скрывает после завершения. GUI-поток. */
void setStreamPublisherGroup(quint64 group);
signals:
void readPositionChanged(const QString& channelId, qint64 lastReadMsgId);
void callRequested(const QString& target, quint64 peer, const QString& name);
void openVideoRequested(const QString& filePath);
void streamRequested(quint64 group, quint64 source, quint64 epoch, const QString& name);
void allStreamsRequested(quint64 group);
void streamPublishRequested(quint64 group);
void openImageRequested(const QString& filePath);
void inviteRequested(const QString& sourceChannel, quint64 peer, const QString& name);
void ownFilesRequested(const QString& channelId);
void filesRequested(const QString& channelId);
void shareInviteRequested(const QString& channelId);
void inviteByLinkRequested(const QString& channelId);
void channelSettingsRequested(const QString& channelId);
void linkActivated(const QString& link);
private:
// Qt children: view/model/composer/panel. Внешний db не уничтожается этим виджетом.
ChatView *m_view;
QStandardItemModel *m_model;
InputBar *m_inputBar;
EmojiPanel *m_emojiPanel;
DbManager *m_db;
QString m_currentChannelId; // Decimal CHAT id, dm:<conv_id> или пусто.
quint64 m_currentDmGeneration = 0; /* Поколение выбранного PM; 0 для канала или отсутствующей беседы. */
QHash<QString, quint64> m_dmDeleting; // Ожидающие удаления conv → generation.
QHash<quint64, QColor> m_avatarCache; // Цвета авторов текущей модели.
qint64 m_lastReadMsgId = 0; // Принятый мостом локальный watermark выбранного target.
QTimer* m_readDebounce; // Single-shot 300ms после scroll/обновления.
qint64 m_savedTopMsgId = 0; // Якорь при перезагрузке того же канала.
bool m_wasAtBottom = false; // Восстановить bottom после reload, если пользователь уже был в конце.
QMap<quint64, QString> m_localNicks; // Локальные имена участников выбранного канала.
AudioRecorder* m_recorder = nullptr;
QLabel* m_peerStatus = nullptr;
QLabel* m_channelTitle = nullptr; /* шапка: название канала */
QLabel* m_talkingLabel = nullptr; /* шапка: кто говорит в рации */
QPushButton* m_callBtn = nullptr;
QPushButton* m_inviteBtn = nullptr;
QPushButton* m_groupMenuButton = nullptr;
QPushButton* m_keyRequestsButton = nullptr;
QPointer<QDialog> m_keyRequestsDialog; // Немодальный список запросов ключей текущего канала.
QTreeWidget* m_keyRequestsList = nullptr; // Child диалога; используется только при живом m_keyRequestsDialog.
void refreshKeyRequests();
void showKeyRequests();
quint64 m_callPeer = 0; // Адресат кнопки PM call; 0 при неизвестном маршруте.
quint64 m_callGroup = 0; // Общая READY-группа для вызова; 0 отключает кнопку.
QPushButton* m_radioBtn = nullptr; /* шапка: переключатель рации */
QPushButton* m_streamBtn = nullptr;
QPushButton* m_streamPublishBtn = nullptr; // Прямой переход к действующей собственной публикации.
quint64 m_streamPublisherGroup = 0; // Канал локального publisher, независимо от снимков каталога.
QList<LiveStreamSource> m_streamSources; // Проверенный полный снимок выбранной группы.
RadioPanel* m_radioPanel = nullptr; /* верхняя панель устройств, VAD и PTT */
bool m_radioOn = false; // Локальный audio engine успешно включён для текущей группы.
QThreadPool m_thumbnailPool; // До двух FFmpeg workers; деструктор pool дожидается jobs.
QSet<QString> m_thumbnailJobs; // Пути незавершённых jobs, без дублей.
QHash<QString, QJsonObject> m_feedback; // message_id hex → последний полный снимок реакций/опроса.
QPointer<QDialog> m_pollDialog; // Один диалог голосования; уничтожение owner безопасно для exec.
QString m_pollDialogMessage, m_pendingPoll; // Открытый message_id и ожидаемый запрос состояния.
quint64 m_feedbackSequence = 0; // Корреляция userAction, 0 у фоновых запросов.
QByteArray m_historyAnchor; // 32-байтный оригинал в историческом окне; пусто — свежая история.
void updateReply(QStandardItem* item, const QByteArray& original);
void refreshReplies();
// Разрешить подписанный ID в выбранном канале, загрузить окно и подсветить оригинал.
void jumpToMessage(const QString& messageId);
bool sendFeedback(QJsonObject command, bool userAction = true); // true — запрос принят; ch фиксируется до модального UI.
void setupFeedback(QStandardItem* item, const MessageRow& message, const QString& channel);
// Проверить UTF-8 лимиты и передать опрос в исходный канал диалога.
void createPoll();
// Показать один диалог опроса или запросить недостающий снимок.
void showPoll(const QString& messageId);
void applyFeedback(const QJsonObject& state);
void requestReaction(const QModelIndex& index, int emoji);
void addReactionActions(QMenu* menu, const QString& channel, const QString& messageId);
QString authorName(quint64 nodeId) const;
void restoreScrollPosition(const QString& channelId);
// Максимальный rowid полностью видимых строк; watermark меняется после принятия записи.
void updateReadPosition();
// Persistent index/QPointer защищают menu от замены модели/уничтожения UI.
void onMessageContextMenu(const QPoint& pos);
// Запросить выбранное поколение PM; ожидающее удаление блокирует запрос.
void requestDmMessages(const QString& channelId);
// Не более двух FFmpeg jobs; GUI callback защищён QPointer.
void prepareVideoThumbnail(const QString& path);
// Owned-подписка в uasync и local audio; ошибка восстанавливает кнопку.
void onRadioToggled(bool on);
void updateRadioUi();
static bool isDmChannel(const QString& channelId);
static QString dmConvId(const QString& channelId);
};