Add reply composer with cancellation and correlated send results
This commit is contained in:
1 parent
eebef4d14f
commit
562901c84f
2 files changed
+102
-17
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user