chatgui: distinguish manual PM downloads and preserve exact attachment actions

This commit is contained in:
evgeny committed 2026-11-03 13:22:53 +01:00
1 parent d0f7b2a364
commit 4c308be47f
3 files changed
+60 -13

No files matched your search

+2
View File
@@ -92,6 +92,8 @@ public:
void setImageOpenCallback(std::function<void(const QString&)> cb) { m_imageOpenCb = std::move(cb); }
void setDownloadCallback(std::function<bool(const QModelIndex&)> 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);
+53 -13
View File
@@ -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<DmConversation>();
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<QString> 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") {
+5
View File
@@ -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:<conv_id> или пусто.
quint64 m_currentDmGeneration = 0; /* Поколение выбранного PM; 0 для канала или отсутствующей беседы. */
QHash<QString, quint64> m_dmDeleting; // Ожидающие удаления conv → generation.
struct DmFileAction { QString conv; quint64 author, seq, generation; };
QHash<QString, DmFileAction> m_openingDmFiles; // Независим от смены выбранной беседы во время открытия.
bool downloadDmFile(const QModelIndex& index);
QHash<quint64, QColor> m_avatarCache; // Цвета авторов текущей модели.
qint64 m_lastReadMsgId = 0; // Принятый мостом локальный watermark выбранного target.
QTimer* m_readDebounce; // Single-shot 300ms после scroll/обновления.