From 2b7def369d0f45b037011a6da79d60d7fa4121c8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 18 Oct 2026 02:16:29 +0200 Subject: [PATCH] Render locked group attachments and local decrypted media --- tools/chatgui/src/messagedelegate.h | 1 + tools/chatgui/src/messagelist.cpp | 74 +++++++++++++++++++++++++++-- tools/chatgui/src/messagelist.h | 7 +++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index daf8827d..18ed0f10 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -50,6 +50,7 @@ enum MessageDataRole { MsgRawTimestampRole = Qt::UserRole + 39, MsgReplyToRole = Qt::UserRole + 40, MsgHighlightRole = Qt::UserRole + 41, + MsgEncryptedRole = Qt::UserRole + 42, }; class MessageDelegate : public QStyledItemDelegate { diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index c38beea0..8266b421 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -34,6 +34,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -44,6 +49,7 @@ static std::vector> s_icons; extern "C" { #include "../transport/gui_bridge.h" #include "chat/chat_core.h" +#include "chat/chat_attachment.h" #include "chat/chat_poll.h" #include "dm/dm_core.h" #include "dm/dm_media.h" @@ -136,7 +142,8 @@ static QString contentTypeFromJson(const QByteArray& data) { static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data, const QString& contentType, const QString& mediaDirBase, - const QString& channelId, const QByteArray& localAttrs) { + const QString& channelId, const QByteArray& localAttrs, + DbManager* db, const QByteArray& messageId) { QByteArray body = data; if (body.startsWith('*')) { int pipePos = body.indexOf('|'); @@ -153,6 +160,56 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data, item->setData(ct, MsgContentTypeRole); item->setData(channelId, MsgChannelIdRole); + if (ct == QLatin1String(ENCRYPTED_ATTACHMENT_TYPE)) { + item->setData(true, MsgEncryptedRole); + item->setData(QStringLiteral("application/octet-stream"), MsgContentTypeRole); + item->setData(QCoreApplication::translate("ChatGui", "Encrypted attachment"), MsgFileDisplayNameRole); + item->setData(QString(), MsgVoiceFileRole); + item->setData(0, MsgFileDownloadStateRole); + const auto parts = QString::fromUtf8(body).split('|'); + if (parts.size() > 3) { item->setData(parts[1].toLongLong(), MsgFileSizeRole); item->setData(parts[3].toInt(), MsgMediaNumBlocksRole); } + sqlite3_stmt* st = db ? db->prepareOrNull("SELECT a.ready,a.downloaded,a.decrypting,a.error,a.plain_path,a.info,a.content_type," + "COALESCE(r.state,-1),length(a.key)=32 FROM attachment_keys a LEFT JOIN attachment_key_requests r" + " ON r.channel=a.channel AND r.message_id=a.message_id AND r.incoming=0 WHERE a.channel=? AND a.message_id=?") : nullptr; + if (!st) return; + const QByteArray channel = channelId.toUtf8(); + sqlite3_bind_text(st, 1, channel.constData(), -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, messageId.constData(), messageId.size(), SQLITE_STATIC); + if (sqlite3_step(st) == SQLITE_ROW) { + const bool ready = sqlite3_column_int(st, 0), downloaded = sqlite3_column_int(st, 1), decrypting = sqlite3_column_int(st, 2); + const bool error = sqlite3_column_int(st, 3), key = sqlite3_column_int(st, 8); + const int state = sqlite3_column_int(st, 7); + const QString path = DbManager::colText(st, 4); + struct attachment_info info = {}; + const bool available = ready && QFile::exists(path) && + !attachment_decode((const uint8_t*)sqlite3_column_blob(st, 5), sqlite3_column_bytes(st, 5), &info); + if (available) { + item->setData(DbManager::colText(st, 6), MsgContentTypeRole); + item->setData(QString::fromUtf8(info.name), MsgFileDisplayNameRole); + item->setData(QFileInfo(path).size(), MsgFileSizeRole); item->setData(QFileInfo(path).size(), MsgVoiceFileSizeRole); + item->setData(path, MsgVoiceFileRole); item->setData(2, MsgFileDownloadStateRole); + item->setData(QCoreApplication::translate("ChatGui", "Encrypted"), MsgDeliveryStateRole); + item->setData(QString::number(info.duration_ms / 1000.0, 'f', 1) + "s", MsgVoiceDurationRole); + item->setData(QString::fromLatin1(info.waveform), MsgVoiceWaveformRole); + item->setData(info.duration_ms, MsgVideoDurationMsRole); + item->setData(info.width, MsgVideoWidthRole); item->setData(info.height, MsgVideoHeightRole); + item->setData(info.width, MsgImageWidthRole); item->setData(info.height, MsgImageHeightRole); + } else { + QString status; + if (error) status = QCoreApplication::translate("ChatGui", "Decryption failed; click to retry"); + else if (state == 3) status = QCoreApplication::translate("ChatGui", "Access denied by author"); + else if (decrypting || (downloaded && key)) status = QCoreApplication::translate("ChatGui", "Decrypting…"); + else if (state == 1) status = QCoreApplication::translate("ChatGui", "Awaiting author's approval"); + else if (state == 0) status = QCoreApplication::translate("ChatGui", "Sending key request…"); + else if (downloaded) status = QCoreApplication::translate("ChatGui", "Requesting decryption…"); + else status = QCoreApplication::translate("ChatGui", "Download encrypted attachment"); + item->setData(status, MsgDeliveryStateRole); + item->setData(downloaded && !error ? 1 : 0, MsgFileDownloadStateRole); + } + } + sqlite3_finalize(st); + return; + } + if (ct == "audio/opus") { QString raw = QString::fromUtf8(body); if (raw.startsWith('{')) return; @@ -379,6 +436,11 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent m_channelTitle->setStyleSheet("font-weight: bold; font-size: 14px;"); m_channelTitle->setText(QCoreApplication::translate("ChatGui", "Channel")); hl->addWidget(m_channelTitle, 1); + m_keyRequestsButton = new QPushButton(header); + m_keyRequestsButton->setObjectName("attachmentKeyRequestsButton"); + m_keyRequestsButton->hide(); + connect(m_keyRequestsButton, &QPushButton::clicked, this, &MessageList::showKeyRequests); + hl->addWidget(m_keyRequestsButton); m_talkingLabel = new QLabel(header); m_talkingLabel->setStyleSheet("color: #27ae60; font-size: 12px;"); m_talkingLabel->hide(); @@ -780,6 +842,8 @@ void MessageList::loadChannel(const QString& channelId) { m_avatarCache.clear(); if (m_currentChannelId != channelId) { m_feedback.clear(); m_pendingPoll.clear(); } m_currentChannelId = channelId; + if (!sameChannel && m_keyRequestsDialog) m_keyRequestsDialog->close(); + refreshKeyRequests(); clearStreams(); watchStreams(); m_groupMenuButton->setVisible(channelId.toULongLong() != 0); clearDmCallRoute(); @@ -855,7 +919,7 @@ void MessageList::loadChannel(const QString& channelId) { item->setData(QVariant::fromValue(m.authorNodeId), MsgAuthorNodeIdRole); item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole); setVoiceMessageRoles(item, m.data, m.contentType, - m_db ? m_db->dbPath() + "/media" : QString(), channelId, m.localAttrs); + m_db ? m_db->dbPath() + "/media" : QString(), channelId, m.localAttrs, m_db, m.messageId); m_model->appendRow(item); setupFeedback(item, m, channelId); updateReply(item, m.replyTo); @@ -933,7 +997,7 @@ void MessageList::refresh() { item->setData(QVariant::fromValue(m.authorNodeId), MsgAuthorNodeIdRole); item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole); setVoiceMessageRoles(item, m.data, m.contentType, - m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, m.localAttrs); + m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, m.localAttrs, m_db, m.messageId); m_model->appendRow(item); setupFeedback(item, m, m_currentChannelId); updateReply(item, m.replyTo); @@ -1127,7 +1191,7 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) { QStandardItem* item = m_model->item(row); setVoiceMessageRoles(item, m.data, m.contentType, - m_db->dbPath() + "/media", chId, m.localAttrs); + m_db->dbPath() + "/media", chId, m.localAttrs, m_db, m.messageId); item->setData(0, MsgMediaBlocksDoneRole); item->setData(false, MsgVoiceDownloadPendingRole); @@ -1239,7 +1303,7 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { QString ct = idx.data(MsgContentTypeRole).toString(); bool isVideo = (ct == "video/mp4"); bool isImage = ct.startsWith("image/"); - bool isDmFile = isDmChannel(idx.data(MsgChannelIdRole).toString()) && ct == "application/octet-stream"; + bool isDmFile = (isDmChannel(idx.data(MsgChannelIdRole).toString()) || idx.data(MsgEncryptedRole).toBool()) && ct == "application/octet-stream"; bool isDm = isDmChannel(idx.data(MsgChannelIdRole).toString()); QString filePath = idx.data(MsgVoiceFileRole).toString(); diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index dbb1163e..b1dc1c2b 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -22,6 +22,7 @@ class QPushButton; class RadioPanel; class QDialog; class QMenu; +class QTreeWidget; struct MessageRow; class MessageList : public QWidget { @@ -45,6 +46,7 @@ public: void onFeedbackChanged(const uint8_t* data, int len); void onFeedbackResult(const uint8_t* data, int len); void onMessageSubmitted(const uint8_t* data, int len); + void onAttachmentKeysChanged(const uint8_t* data, int len); void onRadioTalk(quint64 groupId, quint64 srcNodeId, bool on); /* индикация говорящего */ void onStreamSources(const uint8_t* data, int len); void clearStreams(); @@ -85,6 +87,11 @@ private: QPushButton* m_callBtn = nullptr; QPushButton* m_inviteBtn = nullptr; QPushButton* m_groupMenuButton = nullptr; + QPushButton* m_keyRequestsButton = nullptr; + QPointer m_keyRequestsDialog; + QTreeWidget* m_keyRequestsList = nullptr; + void refreshKeyRequests(); + void showKeyRequests(); quint64 m_callPeer = 0; quint64 m_callGroup = 0; QPushButton* m_radioBtn = nullptr; /* шапка: переключатель рации */