Document composer acknowledgments, attachment ownership and context snapshots

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent cdaad39cf2
commit 88760510da
1 file changed
+106 -74
+106 -74
View File
@@ -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 <QWidget>
@@ -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:<conv_id>, 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<quint64, PendingReply> 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<quint64, PendingReply> 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<QString, QSet<QByteArray>> m_preparations;
QSet<QString> m_preparationErrors;
QString m_mediaDirBase; // База планируемых путей вложений; файлы PM без transcode остаются по исходному пути.
QString m_channelIdForRecord; // Текущий target composer: канал либо dm:<id>.
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<QString, QSet<QByteArray>> m_preparations; // Target → UUID активных задач ядра.
QSet<QString> m_preparationErrors; // Target с последней ошибкой, снимается при новом start или forget.
QSet<QByteArray> 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<QObject*> m_dropTargets;
QPointer<QLabel> m_dropHint;
quint64 m_attachmentContext = 0;
void rejectAttachment(const QString& reason, const QString& source = QString()); // Лог с target/source и пользовательский сигнал.
QSet<QObject*> m_dropTargets; // Заимствованные виджеты с фильтром, включая self и viewport; destroyed удаляет внешние записи.
QPointer<QLabel> m_dropHint; // Собственная drag-подсказка, QObject-parent — текущий drop target; может исчезнуть вместе с ним.
quint64 m_attachmentContext = 0; // Ревизия контекста, проверяемая до/после модального выбора и в queued drop.
};