Browse Source

Add reply composer with cancellation and correlated send results

master
evgeny 4 days ago
parent
commit
562901c84f
  1. 96
      tools/chatgui/src/inputbar.cpp
  2. 23
      tools/chatgui/src/inputbar.h

96
tools/chatgui/src/inputbar.cpp

@ -91,6 +91,17 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(8, 4, 8, 0);
mainLayout->setSpacing(2);
m_replyPreview = new QWidget(this);
auto* replyLayout = new QHBoxLayout(m_replyPreview); replyLayout->setContentsMargins(6, 4, 6, 4);
m_replyLabel = new QLabel(m_replyPreview); m_replyLabel->setTextFormat(Qt::PlainText);
m_replyLabel->setMinimumWidth(0); m_replyLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
m_replyLabel->setStyleSheet("border-left: 2px solid palette(highlight); padding-left: 6px;");
replyLayout->addWidget(m_replyLabel, 1);
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);
mainLayout->addWidget(m_replyPreview); m_replyPreview->hide();
/* Attachment preview bar (hidden until file selected) */
m_attachPreview = new QWidget(this);
@ -225,6 +236,55 @@ InputBar::~InputBar() {
delete m_dropHint.data();
}
/* Смена цели создаёт новую ревизию: поздний результат не очищает следующий ответ. */
void InputBar::setReply(const QByteArray& messageId, const QString& author, const QString& preview) {
if (m_dmMode || messageId.size() != 32 || m_channelIdForRecord.isEmpty()) {
GUI_ERROR("reply: invalid composer target/key bytes=%d", int(messageId.size())); return;
}
++m_replyGeneration; m_replyTo = messageId; m_replyFailed = false;
m_replyLabel->setText(author.simplified() + "\n" + preview.simplified());
m_replyPreview->show(); m_input->setFocus();
GUI_DEBUG("reply: selected ch=%s id=%s generation=%llu", qPrintable(m_channelIdForRecord),
messageId.toHex().constData(), (unsigned long long)m_replyGeneration);
}
void InputBar::clearReply() {
if (!m_replyTo.isEmpty()) GUI_DEBUG("reply: cleared ch=%s generation=%llu", qPrintable(m_channelIdForRecord),
(unsigned long long)m_replyGeneration);
++m_replyGeneration; m_replyTo.clear(); m_replyFailed = false; m_replyPreview->hide();
}
bool InputBar::replySending() const {
for (const auto& pending : m_pendingReplies) if (pending.generation == m_replyGeneration) return true;
return false;
}
/* Каждая часть отправки (текст/вложение) имеет собственное подтверждение и снимок черновика. */
quint64 InputBar::beginReplySend(const QByteArray& reply, quint64 generation, const QString& target,
const QString& text, const QString& source) {
if (reply.isEmpty()) return 0;
const quint64 request = ++m_submissionSequence;
m_pendingReplies.insert(request, {generation, target, text, source, m_input->document()->revision()});
GUI_DEBUG("reply: submit ch=%s id=%s request=%llu", qPrintable(target), reply.toHex().constData(), (unsigned long long)request);
return request;
}
void InputBar::submissionResult(quint64 request, bool success) {
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);
if (pending.generation != m_replyGeneration || pending.target != m_channelIdForRecord) return;
if (!success) {
m_replyFailed = true;
GUI_ERROR("reply: submission failed ch=%s request=%llu", qPrintable(pending.target), (unsigned long long)request);
emit attachmentRejected(QCoreApplication::translate("ChatGui", "Could not send reply. Please try again."));
return;
}
if (!pending.text.isEmpty() && pending.textRevision == m_input->document()->revision()) m_input->clear();
if (!pending.source.isEmpty() && pending.source == m_pendingAttachSrc) clearPendingAttachment();
if (!replySending() && !m_replyFailed) clearReply();
}
/* Qt доставляет drop в viewport, а не в QTextEdit/QListView. Родитель composer принимает drop на кнопках. */
void InputBar::installFileDropTarget(QWidget* target) {
if (!target || m_dropTargets.contains(target)) return;
@ -295,6 +355,7 @@ bool InputBar::eventFilter(QObject *obj, QEvent *event) {
}
if (obj == m_input && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
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) {
m_input->insertPlainText("\n");
@ -330,6 +391,11 @@ void InputBar::adjustInputHeight() {
}
void InputBar::onSendClicked() {
if (replySending()) {
GUI_WARN("reply: submission still pending ch=%s", qPrintable(m_channelIdForRecord)); return;
}
m_replyFailed = false;
const QByteArray reply = m_replyTo;
QString text = m_input->toPlainText();
while (!text.isEmpty() && (text.back() == ' ' || text.back() == '\t'))
text.chop(1);
@ -338,37 +404,34 @@ void InputBar::onSendClicked() {
if (!hasAttach && text.isEmpty()) return;
if (hasAttach) {
const quint64 requestId = beginReplySend(reply, m_replyGeneration, m_channelIdForRecord, {}, m_pendingAttachSrc);
if (m_pendingAttachAsImage) {
GUI_INFO("send: image=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
emit sendImageAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName);
m_attachPreview->hide();
emit sendImageAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName, reply, requestId);
} else if (m_pendingAttachAsVideo) {
GUI_INFO("send: video=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
QByteArray source = m_pendingAttachSrc.toUtf8(), name = (QFileInfo(m_pendingAttachName).completeBaseName() + ".mp4").toUtf8();
if (!request || source.size() >= (int)sizeof(request->source) || name.size() >= (int)sizeof(request->info.name)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: video request allocation/path failed"); u_free(request); return;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: video request allocation/path failed");
u_free(request); submissionResult(requestId, false); return;
}
request->info.kind = ATTACHMENT_VIDEO; request->transcode = 1;
if (reply.size() == 32) memcpy(request->reply_to, reply.constData(), 32);
request->request_id = requestId;
snprintf(request->source, sizeof(request->source), "%s", source.constData());
snprintf(request->info.name, sizeof(request->info.name), "%s", name.constData());
emit submitAttachment(m_channelIdForRecord, request);
m_attachPreview->hide();
} else {
GUI_INFO("send: file=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
emit sendFileAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName);
m_attachPreview->hide();
emit sendFileAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName, reply, requestId);
}
m_pendingAttachSrc.clear();
m_pendingAttachDest.clear();
m_pendingAttachName.clear();
m_pendingAttachSize = 0;
m_pendingAttachAsVideo = false;
m_pendingAttachAsImage = false;
if (reply.isEmpty()) clearPendingAttachment();
}
if (!text.isEmpty()) {
emit sendMessage(text);
m_input->clear();
const quint64 request = beginReplySend(reply, m_replyGeneration, m_channelIdForRecord, text);
emit sendMessage(text, reply, request);
if (reply.isEmpty()) m_input->clear();
if (m_emojiPanel) m_emojiPanel->hide();
m_input->setFocus();
}
@ -376,6 +439,7 @@ void InputBar::onSendClicked() {
void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& channelId) {
if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase) {
clearReply();
++m_attachmentContext;
clearPendingAttachment();
if (m_dropHint) m_dropHint->hide();
@ -398,10 +462,12 @@ void InputBar::setMediaEnabled(bool enabled) {
}
void InputBar::onPttPressed() {
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();
m_recordTarget = m_channelIdForRecord;
m_recordReply = m_replyTo; m_recordReplyGeneration = m_replyGeneration; m_replyFailed = false;
m_pttBtn->setStyleSheet(m_pttBtn->styleSheet() +
"QPushButton { background: #e74c3c; border-radius: 4px; }");
}
@ -424,6 +490,8 @@ void InputBar::onPttReleased() {
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: voice request allocation failed"); return; }
request->info.kind = ATTACHMENT_VOICE; request->preset = VoiceEncoder::preset();
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);
emit submitAttachment(m_recordTarget, request);
}

23
tools/chatgui/src/inputbar.h

@ -32,14 +32,17 @@ public:
void setDmMode(bool enabled);
void setMediaEnabled(bool enabled);
void updatePreparation(const QString& target, const QByteArray& job, int state);
void setReply(const QByteArray& messageId, const QString& author, const QString& preview);
void clearReply();
void submissionResult(quint64 request, bool success);
signals:
void sendMessage(const QString &text);
void sendMessage(const QString &text, const QByteArray& replyTo, 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 QString& displayName, const QByteArray& replyTo, quint64 request);
void sendImageAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest,
const QString& displayName);
const QString& displayName, const QByteArray& replyTo, quint64 request);
void attachmentRejected(const QString& reason);
protected:
@ -59,6 +62,20 @@ private slots:
private:
static QString formatFileSize(qint64 bytes);
struct PendingReply {
quint64 generation;
QString target, text, source;
int textRevision;
};
quint64 beginReplySend(const QByteArray& reply, quint64 generation, const QString& target,
const QString& text = {}, const QString& source = {});
bool replySending() const;
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;
QHash<quint64, PendingReply> m_pendingReplies;
QTextEdit *m_input;
QPushButton *m_emojiBtn;

Loading…
Cancel
Save