|
|
|
|
@ -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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── рация ── */ |
|
|
|
|
|