Document composer acknowledgments, attachment ownership and context snapshots
This commit is contained in:
1 parent
cdaad39cf2
commit
88760510da
1 file changed
+106
-74
+106
-74
@@ -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.
|
||||
};
|
||||
Reference in new issue
Block a user