From e483027e3e07701df726c78a3fe1e85968bddd7d Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 22:38:06 +0100 Subject: [PATCH] Edit group text in desktop composer with save cancel and draft restoration --- tools/chatgui/src/inputbar.cpp | 50 ++++++++++++++++++++++++++++++++-- tools/chatgui/src/inputbar.h | 7 +++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index 03702057..f358d984 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -102,7 +102,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par auto* cancelReply = new QPushButton(QString::fromUtf8("✕"), m_replyPreview); cancelReply->setToolTip(QCoreApplication::translate("ChatGui", "Cancel reply")); cancelReply->setFixedSize(24, 24); replyLayout->addWidget(cancelReply); - connect(cancelReply, &QPushButton::clicked, this, &InputBar::clearReply); + connect(cancelReply, &QPushButton::clicked, this, [this] { if (m_editId.isEmpty()) clearReply(); else cancelEdit(); }); mainLayout->addWidget(m_replyPreview); m_replyPreview->hide(); /* Attachment preview bar (hidden until file selected) */ @@ -168,6 +168,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par auto *sendBtn = makeIconBtn(QIcon(":/icons/send.png"), QCoreApplication::translate("ChatGui", "Send"), this); + m_sendBtn = sendBtn; sendBtn->setStyleSheet(sendBtn->styleSheet() + "QPushButton { border: 1px solid palette(highlight); }"); layout->addWidget(sendBtn, 0, Qt::AlignBottom); @@ -250,6 +251,7 @@ InputBar::~InputBar() { /* Смена цели создаёт новую ревизию: поздний результат не очищает следующий ответ. */ void InputBar::setReply(const QByteArray& messageId, const QString& author, const QString& preview) { + if (!m_editId.isEmpty()) cancelEdit(); if (m_dmMode || messageId.size() != 32 || m_channelIdForRecord.isEmpty()) { GUI_ERROR("reply: invalid composer target/key bytes=%d", int(messageId.size())); return; } @@ -282,6 +284,15 @@ quint64 InputBar::beginReplySend(const QByteArray& reply, quint64 generation, co } void InputBar::submissionResult(quint64 request, bool success) { + if (request && request == m_editRequest) { + m_editRequest = 0; m_input->setReadOnly(false); + if (success) cancelEdit(); + else { + GUI_ERROR("edit: save failed target=%s", qPrintable(m_channelIdForRecord)); + emit attachmentRejected(QCoreApplication::translate("ChatGui", "Could not save edit. The message may have changed. Reopen it to retry.")); + } + return; + } if (!request || !m_pendingReplies.contains(request)) return; const auto pending = m_pendingReplies.take(request); GUI_DEBUG("reply: result ch=%s request=%llu success=%d", qPrintable(pending.target), (unsigned long long)request, success); @@ -297,6 +308,32 @@ void InputBar::submissionResult(quint64 request, bool success) { if (!replySending() && !m_replyFailed && m_input->toPlainText().isEmpty() && m_pendingAttachSrc.isEmpty()) clearReply(); } +/* Черновик и выбранный ответ восстанавливаются после сохранения/отмены. Вложения остаются в composer. */ +void InputBar::editMessage(const QByteArray& id, quint64 version, const QString& text) { + if (m_dmMode || id.size() != 32 || !version || m_editRequest || replySending() || m_pttActive) { + GUI_WARN("edit: composer busy/invalid target=%s", qPrintable(m_channelIdForRecord)); return; + } + if (!m_editId.isEmpty()) cancelEdit(); + m_savedText = m_input->toPlainText(); m_savedReply = m_replyTo; m_savedReplyLabel = m_replyLabel->text(); + clearReply(); m_editId = id; m_editVersion = version; + m_replyLabel->setText(QCoreApplication::translate("ChatGui", "Editing message")); m_replyPreview->show(); + m_attachPreview->hide(); m_attachBtn->setEnabled(false); m_pttBtn->setEnabled(false); + m_sendBtn->setToolTip(QCoreApplication::translate("ChatGui", "Save edit")); + m_input->setPlainText(text); m_input->setFocus(); + GUI_INFO("edit: opened target=%s id=%s ver=%llu", qPrintable(m_channelIdForRecord), id.toHex().constData(), + (unsigned long long)version); +} + +void InputBar::cancelEdit() { + if (m_editId.isEmpty()) return; + m_editId.clear(); m_editVersion = 0; m_editRequest = 0; m_input->setReadOnly(false); + m_input->setPlainText(m_savedText); m_savedText.clear(); + m_replyTo = m_savedReply; m_savedReply.clear(); m_replyLabel->setText(m_savedReplyLabel); m_savedReplyLabel.clear(); + m_replyPreview->setVisible(!m_replyTo.isEmpty()); m_attachPreview->setVisible(!m_pendingAttachSrc.isEmpty()); + m_attachBtn->setEnabled(true); m_pttBtn->setEnabled(true); + m_sendBtn->setToolTip(QCoreApplication::translate("ChatGui", "Send")); +} + /* Qt доставляет drop в viewport, а не в QTextEdit/QListView. Родитель composer принимает drop на кнопках. */ void InputBar::installFileDropTarget(QWidget* target) { if (!target || m_dropTargets.contains(target)) return; @@ -308,7 +345,7 @@ void InputBar::installFileDropTarget(QWidget* target) { } bool InputBar::canAttachFile() const { - return isEnabled() && !m_channelIdForRecord.isEmpty() && m_attachBtn->isEnabled() && + return isEnabled() && m_editId.isEmpty() && !m_channelIdForRecord.isEmpty() && m_attachBtn->isEnabled() && !m_pttActive && (!m_recorder || !m_recorder->isRecording()); } @@ -367,6 +404,7 @@ bool InputBar::eventFilter(QObject *obj, QEvent *event) { } if (obj == m_input && event->type() == QEvent::KeyPress) { auto *ke = static_cast(event); + if (ke->key() == Qt::Key_Escape && !m_editId.isEmpty()) { cancelEdit(); return true; } if (ke->key() == Qt::Key_Escape && !m_replyTo.isEmpty()) { clearReply(); return true; } if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { if (ke->modifiers() & Qt::ControlModifier) { @@ -404,6 +442,12 @@ void InputBar::adjustInputHeight() { void InputBar::onSendClicked() { if (!isEnabled()) { GUI_WARN("send: composer unavailable target=%s", qPrintable(m_channelIdForRecord)); return; } + if (!m_editId.isEmpty()) { + const QString text = m_input->toPlainText(); + if (m_editRequest || text.isEmpty()) return; + m_editRequest = ++m_submissionSequence; m_input->setReadOnly(true); + emit saveEdit(m_channelIdForRecord, m_editId, m_editVersion, text, m_editRequest); return; + } if (replySending()) { GUI_WARN("reply: submission still pending ch=%s", qPrintable(m_channelIdForRecord)); return; } @@ -476,6 +520,7 @@ void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& c if (m_channelIdForRecord == channelId && m_dmGeneration && m_dmGeneration != dmGeneration) forgetDmHistory(channelId, m_dmGeneration); if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase || m_dmGeneration != dmGeneration) { + cancelEdit(); clearReply(); ++m_attachmentContext; clearPendingAttachment(); @@ -522,6 +567,7 @@ void InputBar::setMediaEnabled(bool enabled) { } void InputBar::onPttPressed() { + if (!m_editId.isEmpty()) { GUI_WARN("voice: blocked while editing"); return; } 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; diff --git a/tools/chatgui/src/inputbar.h b/tools/chatgui/src/inputbar.h index 65eb7f80..4ef687e9 100644 --- a/tools/chatgui/src/inputbar.h +++ b/tools/chatgui/src/inputbar.h @@ -38,9 +38,12 @@ public: void setReply(const QByteArray& messageId, const QString& author, const QString& preview); void clearReply(); void submissionResult(quint64 request, bool success); + void editMessage(const QByteArray& id, quint64 version, const QString& text); + void cancelEdit(); signals: void sendMessage(const QString &text, const QByteArray& replyTo, quint64 request); + void saveEdit(const QString& channel, const QByteArray& id, quint64 version, const QString& text, quint64 request); void submitAttachment(const QString& target, struct attachment_send_req* request); void sendFileAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest, const QString& displayName, const QByteArray& replyTo, quint64 request); @@ -74,6 +77,10 @@ private: 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;