From 7fee3ec551cf8c0ea95563b3460a1df81fe8a096 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 16:24:09 +0300 Subject: [PATCH] Render asynchronous PM history and submit desktop file sends --- tools/chatgui/src/messagelist.cpp | 96 +++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index cc835b4c..cc7a9900 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ extern "C" { #include "../transport/gui_bridge.h" #include "chat/chat_core.h" #include "dm/dm_core.h" +#include "dm/dm_media.h" #include "video/video.h" #include "radio/radio.h" #include "../../lib/u_async.h" @@ -501,6 +503,20 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& mediaSrc, const QString& mediaDest, const QString& displayName) { if (m_currentChannelId.isEmpty()) return; + if (isDmChannel(m_currentChannelId)) { + auto* req = (struct dm_file_req*)u_calloc(1, sizeof(struct dm_file_req)); + if (!req) { GUI_ERROR("PM file request allocation failed"); return; } + req->inst = gui_bridge_get_inst(); + QByteArray conv = dmConvId(m_currentChannelId).toUtf8(), path = mediaSrc.toUtf8(), name = displayName.toUtf8(); + if (path.size() >= (int)sizeof(req->path) || name.size() >= (int)sizeof(req->name)) { + GUI_ERROR("PM attachment path or name is too long"); u_free(req); return; + } + snprintf(req->conv, sizeof(req->conv), "%s", conv.constData()); + snprintf(req->path, sizeof(req->path), "%s", path.constData()); + snprintf(req->name, sizeof(req->name), "%s", name.constData()); + gui_bridge_post_uasync_fn(dm_send_file_trampoline, req); + return; + } QByteArray textData = displayName.toUtf8(); auto ts = QDateTime::currentMSecsSinceEpoch(); @@ -663,10 +679,10 @@ void MessageList::loadChannel(const QString& channelId) { } if (m_channelTitle) m_channelTitle->setText(title); - /* DM: только текст; сообщения расшифровываются в dm_core (uasync-поток) */ + m_inputBar->setDmMode(isDmChannel(channelId)); + /* История и состояния PM поступают из ядра в одном снимке. */ if (isDmChannel(channelId)) { m_radioBtn->setEnabled(false); - m_inputBar->setMediaEnabled(false); requestDmMessages(channelId); if (prevFocus) prevFocus->setFocus(); @@ -1005,7 +1021,8 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { QString ct = idx.data(MsgContentTypeRole).toString(); bool isVideo = (ct == "video/mp4"); bool isImage = ct.startsWith("image/"); - if (!isVideo && !isImage) return; + bool isDmFile = isDmChannel(idx.data(MsgChannelIdRole).toString()) && ct == "application/octet-stream"; + if (!isVideo && !isImage && !isDmFile) return; QString filePath = idx.data(MsgVoiceFileRole).toString(); QString displayName = idx.data(MsgFileDisplayNameRole).toString(); @@ -1015,7 +1032,7 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { QAction* saveAct = menu.addAction(QString::fromUtf8("Сохранить в файл")); saveAct->setEnabled(downloaded); QAction* dlAct = nullptr; - if (!downloaded) dlAct = menu.addAction(QString::fromUtf8("Скачать")); + if (!downloaded && !isDmFile) dlAct = menu.addAction(QString::fromUtf8("Скачать")); QAction* chosen = menu.exec(m_view->viewport()->mapToGlobal(pos)); @@ -1024,17 +1041,15 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { if (base.isEmpty()) base = QFileInfo(filePath).fileName(); QString dest = QFileDialog::getSaveFileName(this, QString::fromUtf8("Сохранить файл"), base); if (dest.isEmpty()) return; - if (!QFile::copy(filePath, dest)) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "MessageList: save failed src=%s dst=%s", - qPrintable(filePath), qPrintable(dest)); - } else { - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MessageList: saved %s -> %s", - qPrintable(filePath), qPrintable(dest)); - } + std::thread([filePath, dest]() { + if (!QFile::copy(filePath, dest)) + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "MessageList: save failed src=%s dst=%s", qPrintable(filePath), qPrintable(dest)); + else DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MessageList: saved %s -> %s", qPrintable(filePath), qPrintable(dest)); + }).detach(); return; } - if (chosen == dlAct) { + if (dlAct && chosen == dlAct) { qint64 msgId = idx.data(MsgIdRole).toLongLong(); QString chId = idx.data(MsgChannelIdRole).toString(); if (chId.isEmpty() || msgId <= 0) return; @@ -1076,27 +1091,34 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { QString convId = QString::fromUtf8((const char*)data + 1, cl); if (m_currentChannelId != QStringLiteral("dm:") + convId) return; /* stale response */ - uint16_t count; memcpy(&count, data + 1 + cl, 2); - const uint8_t* p = data + 1 + cl + 2; - const uint8_t* end = data + len; - + QJsonParseError error; + auto document = QJsonDocument::fromJson(QByteArray((const char*)data + 1 + cl, len - 1 - cl), &error); + if (error.error != QJsonParseError::NoError || !document.isArray()) { + GUI_ERROR("Invalid PM history JSON: %s", qPrintable(error.errorString())); return; + } + auto* scrollbar = m_view->verticalScrollBar(); + bool atBottom = m_model->rowCount() == 0 || scrollbar->value() >= scrollbar->maximum(); + int previousScroll = scrollbar->value(); m_model->clear(); m_avatarCache.clear(); - for (int i = 0; i < count; i++) { - if (p + 1 + 8 + 8 + 8 + 1 > end) break; - int dir = p[0]; p += 1; - uint64_t seq; memcpy(&seq, p, 8); p += 8; - uint64_t ts; memcpy(&ts, p, 8); p += 8; - quint64 author; memcpy(&author, p, 8); p += 8; - uint8_t ctl = *p; p += 1; - if (p + ctl > end) break; - p += ctl; /* ct игнорируем: DM v1 — текст */ - if (p + 2 > end) break; - uint16_t dlen; memcpy(&dlen, p, 2); p += 2; - if (p + dlen > end) break; - QString text = QString::fromUtf8((const char*)p, dlen); - p += dlen; + for (const auto& entry : document.array()) { + auto message = entry.toObject(); + int dir = message.value("dir").toInt(); + qint64 seq = (qint64)message.value("seq").toDouble(); + qint64 ts = (qint64)message.value("ts").toDouble(); + quint64 author = message.value("author").toString().toULongLong(); + QString text = message.value("data").toString(); + bool file = message.value("ct").toString() == "file"; + QString state = message.value("media_status").toString(); + QString status; + if (file) { + if (state == "delivered") status = QString::fromUtf8("Доставлено"); + else if (state == "expired") status = QString::fromUtf8("Срок хранения истёк"); + else if (dir == 1) status = QString::fromUtf8("Ожидает доставки"); + else if (state == "ready") status = QString::fromUtf8("Получено"); + else status = QString::fromUtf8("Загрузка…"); + } else if (dir == 1) status = message.value("pending").toBool() ? QString::fromUtf8("Ожидает доставки") : QString::fromUtf8("Доставлено"); AccountRow acc = m_db ? m_db->getAccount(author) : AccountRow(); QString name = authorName(author); @@ -1106,6 +1128,7 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { m_avatarCache[author] = color; auto avatar = makeAvatar(color, letter); auto time = QDateTime::fromMSecsSinceEpoch((qint64)ts).toString("HH:mm"); + if (!file && !status.isEmpty()) time += QStringLiteral(" · ") + status; auto *item = new QStandardItem(); setMsg(item, name, text, time, avatar, false, @@ -1113,10 +1136,21 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { item->setData((qint64)seq, MsgIdRole); item->setData(QVariant::fromValue(author), MsgAuthorNodeIdRole); item->setData(dir == 1 ? 1 : 0, MsgIsOutgoingRole); + item->setData(m_currentChannelId, MsgChannelIdRole); + item->setData(file ? QStringLiteral("application/octet-stream") : QStringLiteral("text/plain"), MsgContentTypeRole); + if (file) { + QString path = message.value("media_path").toString(); + item->setData(path, MsgVoiceFileRole); + item->setData(text, MsgFileDisplayNameRole); + item->setData((qint64)message.value("media_size").toDouble(), MsgFileSizeRole); + item->setData(path.isEmpty() ? (state == "expired" ? 0 : 1) : 2, MsgFileDownloadStateRole); + item->setData(status, MsgDeliveryStateRole); + } m_model->appendRow(item); } - m_view->scrollToBottom(); + if (atBottom) m_view->scrollToBottom(); + else scrollbar->setValue(previousScroll); } /* ── рация ── */