From 88760510da8a9fd2886293d444de2c672c6e9909 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 18:29:55 +0100 Subject: [PATCH] Document composer acknowledgments, attachment ownership and context snapshots --- tools/chatgui/src/inputbar.h | 180 +++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 74 deletions(-) diff --git a/tools/chatgui/src/inputbar.h b/tools/chatgui/src/inputbar.h index d1997fc4..05ba157e 100644 --- a/tools/chatgui/src/inputbar.h +++ b/tools/chatgui/src/inputbar.h @@ -1,6 +1,8 @@ -/* Composer канала/PM: текст, emoji, запись голосового и выбор вложений. - * Контекст target фиксируется в запросе вложения; подготовку и отправку выполняет ядро. - * Переданный submitAttachment request должен принять и освободить обработчик сигнала. */ +/* GUI-composer канала/PM: текст, emoji, ответ/редактирование, голосовая запись и preview одного вложения. + * Сам не отправляет данные в сеть: владелец обрабатывает сигналы в GUI и передаёт работу ядру. + * Для submitAttachment нужен ровно один обработчик, принимающий request вместе с PCM/compressor даже при отказе. + * Ответы/правки ждут submissionResult; обычный текст и вложение без reply очищаются сразу после сигнала отправки. + * emojiPanel/recorder заимствованы, могут быть nullptr и должны жить во время использования; остальные виджеты — дочерние. */ #pragma once #include @@ -21,110 +23,140 @@ struct attachment_send_req; class InputBar : public QWidget { Q_OBJECT public: + /* Создать пустой composer; parent владеет виджетом. Перед отправкой задать цель через setRecordingContext. + * Для PTT recorder должен быть init; его жизненным циклом и остановкой при закрытии владеет вызывающий. */ explicit InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *parent = nullptr); - ~InputBar() override; + ~InputBar() override; // Снимает drop-фильтры/callbacks, удаляет drag-подсказку; не уничтожает заимствованные объекты. + /* mediaDirBase — каталог media; channelId — канал либо dm:, dmGeneration — поколение PM (для канала 0). + * Изменение контекста отменяет edit/reply/preview и отложенный drop; обычный текст сохраняется. + * Запись уже фиксирует прежний target/generation; удаление текущего поколения PM отдельно очищает его черновик. */ void setRecordingContext(const QString& mediaDirBase, const QString& channelId, quint64 dmGeneration = 0); - /* Очистить черновик/запись и задачи удалённого поколения PM; новый выбранный адресат не затрагивается. */ + /* Очистить текст/reply/preview и остановить запись только при совпадении target/generation. + * Известные задачи target исключаются из индикатора; их поздние завершения игнорируются. Другой выбранный адресат не меняется. */ void forgetDmHistory(const QString& target, quint64 generation); - /* Один обработчик URL-drop для composer и viewport истории; отправка остаётся по кнопке/Enter. */ + /* Заимствовать target, включить acceptDrops и установить общий фильтр URL-drop; повтор/null — no-op. + * Уничтожение target снимает регистрацию, destructor composer — фильтр. acceptDrops обратно не переключается. + * Drop только готовит preview; отправку инициируют кнопка/Enter. */ void installFileDropTarget(QWidget* target); - /* Общий composer для канала и PM. */ + /* Режим PM скрывает/снимает групповое шифрование вложения; метод также разрешает PTT/скрепку. + * Ограничение media/edit нужно применять после него через setMediaEnabled. */ void setDmMode(bool enabled); - void setMediaEnabled(bool enabled); + void setMediaEnabled(bool enabled); // Разрешить media-кнопки с учётом edit; false не останавливает уже идущую запись. + /* Состояние подготовки job в target: 0=start, 1=готово, 2=ошибка; job — непрозрачный UUID задачи. + * Кеширует и для невыбранных целей; индикатор показывает только текущую, активные задачи имеют приоритет над ошибкой. */ void updatePreparation(const QString& target, const QByteArray& job, int state); + /* Выбрать ответ в канале: messageId ровно 32 байта, author/preview — plain text, пробелы в подписи упрощаются. + * PM/пустая цель/неверный ключ отклоняются; edit сначала отменяется. Новый ответ имеет собственное поколение. */ void setReply(const QByteArray& messageId, const QString& author, const QString& preview); - void clearReply(); + void clearReply(); // Снять ответ и сменить поколение; текст/preview вложения не очищаются, старые результаты не применяются. + /* Подтвердить правку либо одну часть ответа по request; 0/неизвестный ID игнорируется. + * Ошибка сохраняет черновик и показывает отказ. Успех очищает лишь неизменённую ревизию текста/вложения; + * ответ снимается после успеха всех частей и очистки черновика. Старое поколение/другая цель не затрагиваются. */ void submissionResult(quint64 request, bool success); + /* Открыть edit канального сообщения: id=32 байта, version>0, text — исходный текст. + * Сохраняет обычный черновик/reply, скрывает вложение; PM, pending edit/reply или PTT дают отказ с логом. */ void editMessage(const QByteArray& id, quint64 version, const QString& text); - void cancelEdit(); + void cancelEdit(); // Восстановить обычный черновик/reply/preview; late result edit игнорируется, запрос ядра не отменяется. signals: + /* Текст текущего контекста; справа удалены только пробелы/табуляции. replyTo пуст либо ключ 32 байта. + * request ненулевой для ответа, 0 для обычной отправки; владелец должен обработать текущую цель синхронно. */ void sendMessage(const QString &text, const QByteArray& replyTo, quint64 request); + /* Снимок правки: channel/id/version/text и ненулевой request; ввод read-only до результата/отмены. */ void saveEdit(const QString& channel, const QByteArray& id, quint64 version, const QString& text, quint64 request); + /* Передать единственному GUI-обработчику собственный u_calloc-request голоса/видео/зашифрованного файла. + * target зафиксирован (для PTT при старте); обработчик заполняет inst/target/is_dm и передаёт владение ядру + * либо освобождает request/PCM/compressor. После сигнала InputBar не использует request. */ void submitAttachment(const QString& target, struct attachment_send_req* request); + /* Файл без подготовки видео: source не копируется здесь; dest — запланированный путь, в PM совпадает с source. + * target/name/reply/request относятся к выбранному preview. request=0 без ответа, иначе нужен submissionResult. */ void sendFileAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest, const QString& displayName, const QByteArray& replyTo, quint64 request); + /* Изображение канала; аргументы/подтверждение как у file. В PM картинки идут обычным file-сигналом. */ void sendImageAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest, const QString& displayName, const QByteArray& replyTo, quint64 request); - void attachmentRejected(const QString& reason); + void attachmentRejected(const QString& reason); // Причина отказа для пользователя; подключён внутренний показ QToolTip. protected: - bool eventFilter(QObject *obj, QEvent *event) override; - void resizeEvent(QResizeEvent* event) override; + bool eventFilter(QObject *obj, QEvent *event) override; // URL-drop; Enter отправляет, Ctrl+Enter вставляет LF, Escape снимает edit/reply. + void resizeEvent(QResizeEvent* event) override; // Выравнивает overlay записи по геометрии текстового поля. private slots: - void onSendClicked(); - void adjustInputHeight(); - void onPttPressed(); - void onPttReleased(); - void onAttachClicked(); - void clearPendingAttachment(); - void updateRecordingUI(int ms); - void showRecordOverlay(); - void hideRecordOverlay(); + void onSendClicked(); // Edit либо вложение и текст отдельными сигналами; повтор текущего pending reply блокируется. + void adjustInputHeight(); // От 1 до 12 строк, дальше включает вертикальную прокрутку с сохранением видимости курсора. + void onPttPressed(); // Запускает recorder и фиксирует target/generation/reply только при успешном старте. + void onPttReleased(); // Stop; запись от 500 мс передаётся через takeRecording/submitAttachment, меньшая отбрасывается. + void onAttachClicked(); // QFileDialog → та же подготовка preview, что у drop; контекст проверяется после модального выбора. + void clearPendingAttachment(); // Снять preview/encrypt, увеличить ревизию; исходный файл не удаляется. + void updateRecordingUI(int ms); // Длительность и RMS recorder (-70..0 dB → 0..100), чтение peakLevel потребляет метрику. + void showRecordOverlay(); // Показать overlay и начальную длительность/уровень по recordingStarted. + void hideRecordOverlay(); // Скрыть по recordingStopped; не выполняет отправку записи. private: - static QString formatFileSize(qint64 bytes); + static QString formatFileSize(qint64 bytes); // Человекочитаемые B/KB/MB/GB, шаг 1024; bytes ожидается неотрицательным. struct PendingReply { - quint64 generation; - QString target, text, source; - int textRevision; - quint64 attachmentRevision; + quint64 generation; // Поколение выбранного ответа на момент отправки этой части. + QString target, text, source; // Собственные снимки цели и отправленного текста/исходника; одна из частей может быть пустой. + int textRevision; // QTextDocument::revision для защиты нового/изменённого черновика от позднего success. + quint64 attachmentRevision; // Ревизия preview для такой же защиты выбранного файла. }; + /* Создать запись подтверждения части ответа, вернуть ненулевой ID; без reply вернуть 0 и ничего не запоминать. */ quint64 beginReplySend(const QByteArray& reply, quint64 generation, const QString& target, const QString& text = {}, const QString& source = {}); - bool replySending() const; - QByteArray m_editId, m_savedReply; - QString m_savedText, m_savedReplyLabel; - quint64 m_editVersion = 0, m_editRequest = 0; - QPushButton* m_sendBtn = nullptr; - QWidget* m_replyPreview = nullptr; - QLabel* m_replyLabel = nullptr; - QByteArray m_replyTo, m_recordReply; - quint64 m_replyGeneration = 0, m_recordReplyGeneration = 0, m_submissionSequence = 0; - bool m_replyFailed = false; - quint64 m_attachmentRevision = 0; - QHash m_pendingReplies; + bool replySending() const; // Есть хотя бы одна ожидающая часть именно текущего поколения ответа. + QByteArray m_editId, m_savedReply; // Ключ редактируемого сообщения и ответ обычного черновика, сохранённый на время edit. + QString m_savedText, m_savedReplyLabel; // Обычный текст/подпись ответа для cancelEdit или успешного сохранения. + quint64 m_editVersion = 0, m_editRequest = 0; // Ожидаемая версия и ненулевой ID pending save; 0 — нет запроса. + QPushButton* m_sendBtn = nullptr; // Send либо Save edit, действие определяется m_editId. + QWidget* m_replyPreview = nullptr; // Общий preview ответа или режима edit. + QLabel* m_replyLabel = nullptr; // Plain text автора/цитаты либо подписи edit. + QByteArray m_replyTo, m_recordReply; // Выбранный ответ и его снимок для начатой голосовой записи. + quint64 m_replyGeneration = 0, m_recordReplyGeneration = 0, m_submissionSequence = 0; // Поколения ответов и общий счётчик request ID. + bool m_replyFailed = false; // Хотя бы одна часть текущего ответа получила отказ; ответ не снимается при успехе другой части. + quint64 m_attachmentRevision = 0; // Меняется при замене/очистке preview, не зависит от совпадения пути файла. + QHash m_pendingReplies; // request ID → ожидающая часть; чужие поколения удаляются при получении результата. - QTextEdit *m_input; - QPushButton *m_emojiBtn; - QPushButton *m_pttBtn; - QPushButton *m_attachBtn = nullptr; - QCheckBox *m_encryptCheck = nullptr; - EmojiPanel *m_emojiPanel; - AudioRecorder *m_recorder; - QWidget *m_recordOverlay = nullptr; - QLabel *m_recordLabel = nullptr; - QPushButton *m_cancelRecBtn = nullptr; - QProgressBar *m_levelBar = nullptr; - QWidget *m_attachPreview = nullptr; - QLabel *m_attachPreviewLabel = nullptr; - QPushButton *m_attachClearBtn = nullptr; - bool m_pttActive = false; - bool m_dmMode = false; - bool m_mediaEnabled = true; + QTextEdit *m_input; // Plain-text поле и focusProxy composer. + QPushButton *m_emojiBtn; // Переключает видимость заимствованной панели, если она есть. + QPushButton *m_pttBtn; // Press/start, release/stop, красный фон при активной записи. + QPushButton *m_attachBtn = nullptr; // Открывает выбор одного локального файла. + QCheckBox *m_encryptCheck = nullptr; // Только канал; сбрасывается при смене/очистке preview и переходе в PM. + EmojiPanel *m_emojiPanel; // Заимствована; emojiSelected вставляется в текущую позицию курсора. + AudioRecorder *m_recorder; // Заимствован, nullptr отключает запуск PTT. + QWidget *m_recordOverlay = nullptr; // Дочерний overlay над m_input. + QLabel *m_recordLabel = nullptr; // Время записи M:SS. + QPushButton *m_cancelRecBtn = nullptr; // Stop без отправки: снимает m_pttActive до остановки recorder. + QProgressBar *m_levelBar = nullptr; // RMS последнего обработанного блока recorder, 0..100. + QWidget *m_attachPreview = nullptr; // Preview выбранного файла, скрыт до успешной подготовки выбора. + QLabel *m_attachPreviewLabel = nullptr; // Имя/размер файла; его содержимое здесь не читается. + QPushButton *m_attachClearBtn = nullptr; // Снять preview без удаления исходника. + bool m_pttActive = false; // Composer начал запись, которую release ещё может отправить. + bool m_dmMode = false; // PM: изображения идут файлами, групповое шифрование скрыто. + bool m_mediaEnabled = true; // Запрошенное разрешение media, используется при восстановлении кнопок после edit. quint64 m_dmGeneration = 0, m_recordDmGeneration = 0; /* Поколения текущего composer и начатой записи PM. */ - QString m_mediaDirBase; - QString m_channelIdForRecord; - QString m_recordTarget; - QString m_pendingAttachSrc; - QString m_pendingAttachDest; - QString m_pendingAttachName; - qint64 m_pendingAttachSize = 0; - bool m_pendingAttachAsVideo = false; - bool m_pendingAttachAsImage = false; - QLabel* m_preparationLabel = nullptr; - QHash> m_preparations; - QSet m_preparationErrors; + QString m_mediaDirBase; // База планируемых путей вложений; файлы PM без transcode остаются по исходному пути. + QString m_channelIdForRecord; // Текущий target composer: канал либо dm:. + QString m_recordTarget; // Target, зафиксированный при старте PTT, независимо от дальнейшего выбора канала. + QString m_pendingAttachSrc; // Локальный source preview, не копируется/удаляется composer. + QString m_pendingAttachDest; // Планируемый mediaDirBase/target/имя либо исходный путь PM. + QString m_pendingAttachName; // Исходное имя для отображения/метаданных; видео при отправке получает суффикс mp4. + qint64 m_pendingAttachSize = 0; // Размер на момент выбора, bytes. + bool m_pendingAttachAsVideo = false; // Пользователь выбрал подготовку/transcode видео. + bool m_pendingAttachAsImage = false; // Расширение картинки в канале; PM не использует этот режим. + QLabel* m_preparationLabel = nullptr; // Статус подготовки текущего target. + QHash> m_preparations; // Target → UUID активных задач ядра. + QSet m_preparationErrors; // Target с последней ошибкой, снимается при новом start или forget. QSet m_discardedPreparations; /* Завершения задач удалённой истории, ещё ожидаемые из ядра. */ - void showPreparation(); - bool canAttachFile() const; + void showPreparation(); // Обновить индикатор для текущей цели по кешу задач/ошибок. + bool canAttachFile() const; // Enabled, выбран target, скрепка разрешена, нет edit/PTT/записи recorder. + /* Проверить source/context, выбрать режим видео и подготовить preview; true — выбор принят, false — отмена/отказ. + * Старый preview заменяется лишь после успеха; source остаётся неизменным, для групп/видео создаётся каталог назначения. */ bool prepareAttachment(const QString& source, quint64 context); - void rejectAttachment(const QString& reason, const QString& source = QString()); - QSet m_dropTargets; - QPointer m_dropHint; - quint64 m_attachmentContext = 0; + void rejectAttachment(const QString& reason, const QString& source = QString()); // Лог с target/source и пользовательский сигнал. + QSet m_dropTargets; // Заимствованные виджеты с фильтром, включая self и viewport; destroyed удаляет внешние записи. + QPointer m_dropHint; // Собственная drag-подсказка, QObject-parent — текущий drop target; может исчезнуть вместе с ним. + quint64 m_attachmentContext = 0; // Ревизия контекста, проверяемая до/после модального выбора и в queued drop. };