From 4c308be47fa98336b89b3c99bf239edcf3e39aaa Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 13:22:53 +0100 Subject: [PATCH] chatgui: distinguish manual PM downloads and preserve exact attachment actions --- tools/chatgui/src/messagedelegate.h | 2 + tools/chatgui/src/messagelist.cpp | 66 +++++++++++++++++++++++------ tools/chatgui/src/messagelist.h | 5 +++ 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index 0f2076ba6..712b70b02 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -92,6 +92,8 @@ public: void setImageOpenCallback(std::function cb) { m_imageOpenCb = std::move(cb); } void setDownloadCallback(std::function cb) { m_downloadCb = std::move(cb); } signals: + void attachmentOpening(const QModelIndex& index); // Сохранить адрес сообщения до асинхронного открытия. + void attachmentOpened(const QString& path); // Успешное открытие/воспроизведение, не подготовка превью. void avatarRequested(const QString& channel, quint64 owner); void reactionRequested(const QModelIndex& index, int emoji); /* -1 открывает выбор реакции */ void pollRequested(const QString& messageId); diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 3b3bebe72..0f79275b6 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -618,10 +618,19 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent connect(msgDelegate, &MessageDelegate::pollRequested, this, &MessageList::showPoll); connect(msgDelegate, &MessageDelegate::replyRequested, this, &MessageList::jumpToMessage); connect(m_view, &ChatView::latestRequested, this, &MessageList::showLatestMessages); - msgDelegate->setDownloadCallback([](const QModelIndex& index) { + connect(msgDelegate, &MessageDelegate::attachmentOpening, this, [this](const QModelIndex& index) { + const QString channel = index.data(MsgChannelIdRole).toString(); + if (!isDmChannel(channel) || channel != m_currentChannelId) return; + const QString path = index.data(MsgVoiceFileRole).toString(); + m_openingDmFiles.insert(path, {dmConvId(channel), index.data(MsgAuthorNodeIdRole).toULongLong(), + index.data(MsgIdRole).toULongLong(), m_currentDmGeneration}); + }); + connect(msgDelegate, &MessageDelegate::attachmentOpened, this, &MessageList::onAttachmentOpened); + msgDelegate->setDownloadCallback([this](const QModelIndex& index) { QString target = index.data(MsgChannelIdRole).toString(); qint64 id = index.data(MsgIdRole).toLongLong(); - if (target.isEmpty() || target.startsWith("dm:") || id <= 0) return false; + if (target.startsWith("dm:")) return downloadDmFile(index); + if (target.isEmpty() || id <= 0) return false; return gui_bridge_download_attachment(target.toUtf8().constData(), id) == 0; }); m_view->setItemDelegate(msgDelegate); @@ -863,7 +872,10 @@ void MessageList::loadChannel(const QString& channelId) { const auto conversations = m_db && isDmChannel(channelId) ? m_db->getDmConversations() : QList(); quint64 generation = 0; - for (const auto& conv : conversations) if (conv.convId == dmConvId(channelId)) { generation = conv.generation; break; } + bool closed = false; + for (const auto& conv : conversations) if (conv.convId == dmConvId(channelId)) { + generation = conv.generation; closed = conv.closed; break; + } bool sameChannel = m_currentChannelId == channelId && m_currentDmGeneration == generation; /* Save state of current channel before switching */ @@ -904,7 +916,7 @@ void MessageList::loadChannel(const QString& channelId) { m_inviteBtn->setVisible(isDmChannel(channelId)); m_peerStatus->setVisible(isDmChannel(channelId)); m_radioBtn->setVisible(!channelId.isEmpty() && !isDmChannel(channelId)); - const bool dmAvailable = generation && m_dmDeleting.value(dmConvId(channelId)) != generation; + const bool dmAvailable = generation && !closed && m_dmDeleting.value(dmConvId(channelId)) != generation; m_inputBar->setEnabled(!channelId.isEmpty() && (!isDmChannel(channelId) || dmAvailable)); m_inviteBtn->setEnabled(dmAvailable); m_inputBar->setRecordingContext(m_db ? m_db->dbPath() + "/media" : QString(), channelId, generation); @@ -930,7 +942,7 @@ void MessageList::loadChannel(const QString& channelId) { if (m_db) { for (const auto& conv : conversations) { if (conv.convId != dmConvId(channelId)) continue; - QString name = m_db->getDisplayName(conv.peerNodeId); + QString name = conv.peerGroup ? conv.peerName : m_db->getDisplayName(conv.peerNodeId); if (name.isEmpty()) name = conv.peerName; if (name.isEmpty()) name = QStringLiteral("0x%1").arg(conv.peerNodeId, 16, 16, QChar('0')); m_channelTitle->setText(name); @@ -1477,7 +1489,8 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { saveAct = menu.addAction(QCoreApplication::translate("ChatGui", "Save to file")); saveAct->setEnabled(downloaded); } QAction* dlAct = nullptr; - if ((isVideo || isImage) && !downloaded && !isDm) dlAct = menu.addAction(QCoreApplication::translate("ChatGui", "Download")); + if ((isVideo || isImage || isDmFile || (isDm && ct == "audio/opus")) && !downloaded) + dlAct = menu.addAction(QCoreApplication::translate("ChatGui", "Download")); QAction* chosen = menu.exec(m_view->viewport()->mapToGlobal(pos)); if (!guard) return; @@ -1497,6 +1510,7 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { } if (dlAct && chosen == dlAct) { + if (isDm) { downloadDmFile(idx); return; } qint64 msgId = idx.data(MsgIdRole).toLongLong(); QString chId = idx.data(MsgChannelIdRole).toString(); if (chId.isEmpty() || msgId <= 0) return; @@ -1506,6 +1520,29 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { /* ── DM (p2p чат) ── */ +bool MessageList::downloadDmFile(const QModelIndex& index) { + if (index.data(MsgChannelIdRole).toString() != m_currentChannelId || !isDmChannel(m_currentChannelId) || + !m_currentDmGeneration) return false; + auto* req = (struct dm_media_action_req*)u_calloc(1, sizeof(struct dm_media_action_req)); + strncpy(req->conv, dmConvId(m_currentChannelId).toUtf8().constData(), sizeof(req->conv) - 1); + req->author = index.data(MsgAuthorNodeIdRole).toULongLong(); req->seq = index.data(MsgIdRole).toULongLong(); + req->generation = m_currentDmGeneration; + if (gui_bridge_post_bound_fn(dm_media_action_trampoline, req, &req->inst) == 0) return true; + u_free(req); GUI_WARN("PM download: core unavailable"); return false; +} + +void MessageList::onAttachmentOpened(const QString& path) { + auto found = m_openingDmFiles.find(path); + if (found == m_openingDmFiles.end()) return; + const auto action = found.value(); m_openingDmFiles.erase(found); + auto* req = (struct dm_media_action_req*)u_calloc(1, sizeof(struct dm_media_action_req)); + strncpy(req->conv, action.conv.toUtf8().constData(), sizeof(req->conv) - 1); + req->author = action.author; req->seq = action.seq; req->generation = action.generation; req->opened = 1; + if (gui_bridge_post_bound_fn(dm_media_action_trampoline, req, &req->inst) < 0) { + u_free(req); GUI_WARN("PM opened: core unavailable"); + } +} + void MessageList::clearDmCallRoute() { m_callPeer = 0; m_callGroup = 0; m_callBtn->setEnabled(false); @@ -1611,8 +1648,8 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { auto* scrollbar = m_view->verticalScrollBar(); bool atBottom = m_model->rowCount() == 0 || scrollbar->value() >= scrollbar->maximum(); const bool nearBottom = nearHistoryBottom(); - qint64 previousLatest = 0; - for (int row = 0; row < m_model->rowCount(); ++row) previousLatest = qMax(previousLatest, m_model->item(row)->data(MsgIdRole).toLongLong()); + QSet previousMessages; + for (int row = 0; row < m_model->rowCount(); ++row) previousMessages.insert(m_model->item(row)->data(MsgMessageKeyRole).toString()); bool newMessage = false, outgoingMessage = false; int previousScroll = scrollbar->value(); m_model->clear(); @@ -1622,9 +1659,10 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { auto message = entry.toObject(); int dir = message.value("dir").toInt(); qint64 seq = (qint64)message.value("seq").toDouble(); - if (seq > previousLatest) { newMessage = true; if (dir == 1) outgoingMessage = true; } qint64 ts = (qint64)message.value("ts").toDouble(); quint64 author = message.value("author").toString().toULongLong(); + const QString messageKey = QString::number(author) + ':' + QString::number(seq); + if (!previousMessages.contains(messageKey)) { newMessage = true; if (dir == 1) outgoingMessage = true; } QString text = message.value("data").toString(); const bool invite = message.value("ct").toString() == QStringLiteral("invite"); const QString inviteLink = invite ? text : QString(); @@ -1640,10 +1678,11 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { if (file) { if (state == "delivered") status = QCoreApplication::translate("ChatGui", "Delivered"); else if (state == "expired") status = QCoreApplication::translate("ChatGui", "Storage period expired"); - else if (dir == 1) status = QCoreApplication::translate("ChatGui", "Awaiting delivery"); else if (state == "ready") status = QCoreApplication::translate("ChatGui", "Received"); - else status = QCoreApplication::translate("ChatGui", "Downloading…"); - } else if (dir == 1) status = message.value("pending").toBool() ? QCoreApplication::translate("ChatGui", + else if (state == "downloading") status = QCoreApplication::translate("ChatGui", "Downloading…"); + else if (state == "unavailable") status = QCoreApplication::translate("ChatGui", "File unavailable"); + else status = QCoreApplication::translate("ChatGui", "Download"); + } else if (message.value("source").toBool()) status = message.value("pending").toBool() ? QCoreApplication::translate("ChatGui", "Awaiting delivery") : QCoreApplication::translate("ChatGui", "Delivered"); @@ -1665,6 +1704,7 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { setMsg(item, name, text, time, avatar, false, false, QString(), QString(), QVariantList{}); item->setData((qint64)seq, MsgIdRole); + item->setData(messageKey, MsgMessageKeyRole); item->setData(QVariant::fromValue(author), MsgAuthorNodeIdRole); item->setData(dir == 1 ? 1 : 0, MsgIsOutgoingRole); item->setData(m_currentChannelId, MsgChannelIdRole); @@ -1676,7 +1716,7 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { 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(path.isEmpty() ? (state == "downloading" ? 1 : 0) : 2, MsgFileDownloadStateRole); item->setData(status, MsgDeliveryStateRole); int duration = message.value("duration_ms").toInt(); if (type == "audio/opus") { diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index 8818b279d..907a65c9a 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -57,6 +57,8 @@ public: void onAttachmentDownloaded(const QString& chId, int64_t msgId); // [conv_len,conv,generation,JSON]: чужое/старое поколение не применяется. void onDmMessages(const uint8_t* data, int len); + // Только успешное пользовательское открытие; snapshot сохраняется до запуска просмотрщика. + void onAttachmentOpened(const QString& path); /* Блокировать отправки ожидающего удаления; forget очищает UI только для удалённого поколения и возвращает true. */ void setDmDeleting(const QString& convId, quint64 generation, bool deleting); bool forgetDmHistory(const QString& convId, quint64 generation); @@ -119,6 +121,9 @@ private: QString m_currentChannelId; // Decimal CHAT id, dm: или пусто. quint64 m_currentDmGeneration = 0; /* Поколение выбранного PM; 0 для канала или отсутствующей беседы. */ QHash m_dmDeleting; // Ожидающие удаления conv → generation. + struct DmFileAction { QString conv; quint64 author, seq, generation; }; + QHash m_openingDmFiles; // Независим от смены выбранной беседы во время открытия. + bool downloadDmFile(const QModelIndex& index); QHash m_avatarCache; // Цвета авторов текущей модели. qint64 m_lastReadMsgId = 0; // Принятый мостом локальный watermark выбранного target. QTimer* m_readDebounce; // Single-shot 300ms после scroll/обновления.