Browse Source

Render asynchronous PM history and submit desktop file sends

master
evgeny 1 day ago
parent
commit
7fee3ec551
  1. 96
      tools/chatgui/src/messagelist.cpp

96
tools/chatgui/src/messagelist.cpp

@ -17,6 +17,7 @@
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <QScrollBar>
#include <QApplication>
@ -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);
}
/* ── рация ── */

Loading…
Cancel
Save