From e10354a0acb55de32e4eae0f41cc7a305b2c7fb0 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 21:25:34 +0100 Subject: [PATCH] Keep PM composer work bound to its history generation --- tools/chatgui/src/inputbar.cpp | 40 +++++++++++++++++++++++++++++++--- tools/chatgui/src/inputbar.h | 6 ++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index 59098b9f..b4e82103 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -308,7 +308,7 @@ void InputBar::installFileDropTarget(QWidget* target) { } bool InputBar::canAttachFile() const { - return !m_channelIdForRecord.isEmpty() && m_attachBtn->isEnabled() && + return isEnabled() && !m_channelIdForRecord.isEmpty() && m_attachBtn->isEnabled() && !m_pttActive && (!m_recorder || !m_recorder->isRecording()); } @@ -403,6 +403,7 @@ void InputBar::adjustInputHeight() { } void InputBar::onSendClicked() { + if (!isEnabled()) { GUI_WARN("send: composer unavailable target=%s", qPrintable(m_channelIdForRecord)); return; } if (replySending()) { GUI_WARN("reply: submission still pending ch=%s", qPrintable(m_channelIdForRecord)); return; } @@ -450,6 +451,7 @@ void InputBar::onSendClicked() { u_free(request); submissionResult(requestId, false); return; } request->info.kind = ATTACHMENT_VIDEO; request->transcode = 1; + request->dm_generation = m_dmGeneration; if (reply.size() == 32) memcpy(request->reply_to, reply.constData(), 32); request->request_id = requestId; snprintf(request->source, sizeof(request->source), "%s", source.constData()); @@ -470,8 +472,10 @@ void InputBar::onSendClicked() { } } -void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& channelId) { - if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase) { +void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& channelId, quint64 dmGeneration) { + if (m_channelIdForRecord == channelId && m_dmGeneration && m_dmGeneration != dmGeneration) + forgetDmHistory(channelId, m_dmGeneration); + if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase || m_dmGeneration != dmGeneration) { clearReply(); ++m_attachmentContext; clearPendingAttachment(); @@ -479,6 +483,24 @@ void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& c } m_mediaDirBase = mediaDirBase; m_channelIdForRecord = channelId; + m_dmGeneration = dmGeneration; + showPreparation(); +} + +/* Старое завершение worker не должно вернуть ошибку/индикатор в повторно открытый PM. */ +void InputBar::forgetDmHistory(const QString& target, quint64 generation) { + m_discardedPreparations.unite(m_preparations.take(target)); + m_preparationErrors.remove(target); + if (m_recordTarget == target && m_recordDmGeneration == generation) { + m_pttActive = false; + if (m_recorder && m_recorder->isRecording()) m_recorder->stopRecording(); + m_recordTarget.clear(); m_recordReply.clear(); m_recordDmGeneration = 0; + } + if (m_channelIdForRecord == target && m_dmGeneration == generation) { + ++m_attachmentContext; clearPendingAttachment(); clearReply(); m_input->clear(); + if (m_dropHint) m_dropHint->hide(); + } + GUI_INFO("PM composer cleared: target=%s generation=%llu", qPrintable(target), (unsigned long long)generation); showPreparation(); } @@ -498,12 +520,14 @@ void InputBar::setMediaEnabled(bool enabled) { } void InputBar::onPttPressed() { + if (!isEnabled() || m_channelIdForRecord.isEmpty()) { GUI_WARN("voice: composer unavailable"); return; } if (replySending()) { GUI_WARN("reply: recording blocked while submission pending"); return; } if (!m_recorder || !m_recorder->isInitialized()) return; m_recorder->startRecording(); m_pttActive = m_recorder->isRecording(); if (!m_pttActive) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice recording did not start; composer remains idle"); return; } m_recordTarget = m_channelIdForRecord; + m_recordDmGeneration = m_dmGeneration; m_recordReply = m_replyTo; m_recordReplyGeneration = m_replyGeneration; m_replyFailed = false; m_pttBtn->setStyleSheet(m_pttBtn->styleSheet() + "QPushButton { background: #e74c3c; border-radius: 4px; }"); @@ -517,6 +541,10 @@ void InputBar::onPttReleased() { "QPushButton { border: none; border-radius: 4px; background: transparent; }" "QPushButton:hover { background: palette(light); }"); + if (!isEnabled()) { + GUI_INFO("voice: discarded recording while composer unavailable target=%s", qPrintable(m_recordTarget)); return; + } + int durationMs = m_recorder->durationMs(); if (durationMs < 500) { GUI_INFO("InputBar: voice message too short (%dms), discarded", durationMs); @@ -526,6 +554,7 @@ void InputBar::onPttReleased() { auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req)); if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: voice request allocation failed"); return; } request->info.kind = ATTACHMENT_VOICE; request->preset = VoiceEncoder::preset(); + request->dm_generation = m_recordDmGeneration; if (!m_recorder->takeRecording(request)) { u_free(request); return; } if (m_recordReply.size() == 32) memcpy(request->reply_to, m_recordReply.constData(), 32); request->request_id = beginReplySend(m_recordReply, m_recordReplyGeneration, m_recordTarget); @@ -627,6 +656,11 @@ void InputBar::clearPendingAttachment() { } void InputBar::updatePreparation(const QString& target, const QByteArray& job, int state) { + if (m_discardedPreparations.contains(job)) { + if (state) m_discardedPreparations.remove(job); + GUI_DEBUG("PM preparation discarded after deletion: target=%s job=%s state=%d", qPrintable(target), job.toHex().constData(), state); + return; + } if (state == 0) { m_preparations[target].insert(job); m_preparationErrors.remove(target); } else { m_preparations[target].remove(job); diff --git a/tools/chatgui/src/inputbar.h b/tools/chatgui/src/inputbar.h index 29bec7ee..65eb7f80 100644 --- a/tools/chatgui/src/inputbar.h +++ b/tools/chatgui/src/inputbar.h @@ -25,7 +25,9 @@ public: QWidget *parent = nullptr); ~InputBar() override; - void setRecordingContext(const QString& mediaDirBase, const QString& channelId); + void setRecordingContext(const QString& mediaDirBase, const QString& channelId, quint64 dmGeneration = 0); + /* Очистить черновик/запись и задачи удалённого поколения PM; новый выбранный адресат не затрагивается. */ + void forgetDmHistory(const QString& target, quint64 generation); /* Один обработчик URL-drop для composer и viewport истории; отправка остаётся по кнопке/Enter. */ void installFileDropTarget(QWidget* target); @@ -96,6 +98,7 @@ private: QPushButton *m_attachClearBtn = nullptr; bool m_pttActive = false; bool m_dmMode = false; + quint64 m_dmGeneration = 0, m_recordDmGeneration = 0; /* Поколения текущего composer и начатой записи PM. */ QString m_mediaDirBase; QString m_channelIdForRecord; QString m_recordTarget; @@ -108,6 +111,7 @@ private: QLabel* m_preparationLabel = nullptr; QHash> m_preparations; QSet m_preparationErrors; + QSet m_discardedPreparations; /* Завершения задач удалённой истории, ещё ожидаемые из ядра. */ void showPreparation(); bool canAttachFile() const; bool prepareAttachment(const QString& source, quint64 context);