chatgui: distinguish manual PM downloads and preserve exact attachment actions
This commit is contained in:
1 parent
d0f7b2a364
commit
4c308be47f
3 files changed
+60
-13
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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/обновления.
|
||||
|
||||
Reference in new issue
Block a user