Browse Source

Connect reply sending, quote resolution and history navigation

master
evgeny 4 days ago
parent
commit
38257a7f54
  1. 120
      tools/chatgui/src/messagelist.cpp
  2. 5
      tools/chatgui/src/messagelist.h

120
tools/chatgui/src/messagelist.cpp

@ -32,6 +32,7 @@
#include <QImageReader>
#include <QMenu>
#include <QFileDialog>
#include <QToolTip>
#include <vector>
#include <memory>
#include <thread>
@ -48,6 +49,7 @@ extern "C" {
#include "radio/radio.h"
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/audio_compressor.h"
#include "../../lib/debug_config.h"
void chat_core_save_ui_state_trampoline(void* arg);
}
@ -453,6 +455,12 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
});
connect(msgDelegate, &MessageDelegate::reactionRequested, this, &MessageList::requestReaction);
connect(msgDelegate, &MessageDelegate::pollRequested, this, &MessageList::showPoll);
connect(msgDelegate, &MessageDelegate::replyRequested, this, &MessageList::jumpToMessage);
connect(m_view, &ChatView::latestRequested, this, [this]() {
if (m_historyAnchor.isEmpty()) return;
m_historyAnchor.clear(); loadChannel(m_currentChannelId);
QTimer::singleShot(0, m_view, &QListView::scrollToBottom);
});
msgDelegate->setDownloadCallback([](const QModelIndex& index) {
QString target = index.data(MsgChannelIdRole).toString();
qint64 id = index.data(MsgIdRole).toLongLong();
@ -501,9 +509,13 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
layout->addWidget(m_inputBar);
m_inputBar->installFileDropTarget(m_view->viewport());
connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) {
connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text, const QByteArray& reply, quint64 request) {
if (!gui_bridge_get_inst() || !gui_bridge_is_uasync_ready()) {
GUI_ERROR("send: core unavailable"); m_inputBar->submissionResult(request, false); return;
}
if (m_currentChannelId.isEmpty()) {
qWarning("MessageList: no channel selected, message not sent");
m_inputBar->submissionResult(request, false);
return;
}
@ -527,7 +539,10 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
if (!req) { GUI_ERROR("send: text request allocation failed"); m_inputBar->submissionResult(request, false); return; }
memset(req, 0, sizeof(*req));
if (reply.size() == 32) memcpy(req->reply_to, reply.constData(), 32);
req->request_id = request;
req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
@ -540,6 +555,11 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
});
connect(m_inputBar, &InputBar::submitAttachment, this, [this](const QString& target, struct attachment_send_req* req) {
if (!gui_bridge_get_inst() || !gui_bridge_is_uasync_ready()) {
GUI_ERROR("send: attachment core unavailable"); m_inputBar->submissionResult(req->request_id, false);
if (req->pcm_release) req->pcm_release(req->pcm_owner); else u_free((void*)req->pcm);
audio_compressor_destroy(req->compressor); u_free(req); return;
}
req->inst = gui_bridge_get_inst(); req->is_dm = isDmChannel(target);
QByteArray id = (req->is_dm ? dmConvId(target) : target).toUtf8();
snprintf(req->target, sizeof(req->target), "%s", id.constData());
@ -547,8 +567,10 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
});
connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& target, const QString& mediaSrc, const QString& mediaDest,
const QString& displayName) {
if (target.isEmpty()) return;
const QString& displayName, const QByteArray& reply, quint64 request) {
if (target.isEmpty() || !gui_bridge_get_inst() || !gui_bridge_is_uasync_ready()) {
GUI_ERROR("send: file target/core unavailable"); m_inputBar->submissionResult(request, false); return;
}
if (isDmChannel(target)) {
auto* req = (struct dm_file_req*)u_calloc(1, sizeof(struct dm_file_req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "PM file request allocation failed"); return; }
@ -570,7 +592,10 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
if (!req) { GUI_ERROR("send: file request allocation failed"); m_inputBar->submissionResult(request, false); return; }
memset(req, 0, sizeof(*req));
if (reply.size() == 32) memcpy(req->reply_to, reply.constData(), 32);
req->request_id = request;
req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
target.toUtf8().constData());
@ -586,8 +611,10 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
});
connect(m_inputBar, &InputBar::sendImageAttachment, this, [this](const QString& target, const QString& mediaSrc, const QString& mediaDest,
const QString& displayName) {
if (target.isEmpty()) return;
const QString& displayName, const QByteArray& reply, quint64 request) {
if (target.isEmpty() || !gui_bridge_get_inst() || !gui_bridge_is_uasync_ready()) {
GUI_ERROR("send: image target/core unavailable"); m_inputBar->submissionResult(request, false); return;
}
QByteArray textData = displayName.toUtf8();
auto ts = QDateTime::currentMSecsSinceEpoch();
@ -610,7 +637,10 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
if (!req) { GUI_ERROR("send: image request allocation failed"); m_inputBar->submissionResult(request, false); return; }
memset(req, 0, sizeof(*req));
if (reply.size() == 32) memcpy(req->reply_to, reply.constData(), 32);
req->request_id = request;
req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
target.toUtf8().constData());
@ -646,6 +676,8 @@ void MessageList::loadChannel(const QString& channelId) {
QWidget* prevFocus = QApplication::focusWidget();
bool sameChannel = (m_currentChannelId == channelId);
if (!sameChannel) m_historyAnchor.clear();
m_view->setHistoricalWindow(!m_historyAnchor.isEmpty());
/* Save state of current channel before switching */
if (!m_currentChannelId.isEmpty()) {
@ -718,7 +750,7 @@ void MessageList::loadChannel(const QString& channelId) {
m_localNicks = m_db->getLocalNicknames(channelId);
auto msgs = m_db->getMessages(channelId, 200);
auto msgs = m_db->getMessages(channelId, 200, m_historyAnchor);
for (const auto& m : msgs) {
AccountRow acc = m_db->getAccount(m.authorNodeId);
@ -748,6 +780,7 @@ void MessageList::loadChannel(const QString& channelId) {
m_db ? m_db->dbPath() + "/media" : QString(), channelId, m.localAttrs);
m_model->appendRow(item);
setupFeedback(item, m, channelId);
updateReply(item, m.replyTo);
}
if (sameChannel) {
@ -789,6 +822,8 @@ void MessageList::refresh() {
if (!m_db || !m_db->isOpen()) return;
refreshReplies();
if (!m_historyAnchor.isEmpty()) return;
QSet<QString> present;
for (int row = 0; row < m_model->rowCount(); row++) present.insert(m_model->item(row)->data(MsgMessageKeyRole).toString());
auto newMsgs = m_db->getMessages(m_currentChannelId, 200);
@ -823,6 +858,7 @@ void MessageList::refresh() {
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, m.localAttrs);
m_model->appendRow(item);
setupFeedback(item, m, m_currentChannelId);
updateReply(item, m.replyTo);
while (m_model->rowCount() > 200)
m_model->removeRow(0);
}
@ -1047,6 +1083,71 @@ void MessageList::prepareVideoThumbnail(const QString& path) {
});
}
/* Цитаты разрешаются независимо от окна истории; поздняя синхронизация обновляет уже видимые ответы. */
void MessageList::updateReply(QStandardItem* item, const QByteArray& original) {
item->setData(QString::fromLatin1(original.toHex()), MsgReplyToRole);
item->setData(!original.isEmpty(), MsgHasQuoteRole);
if (original.isEmpty() || !m_db) return;
const auto message = m_db->getMessageByKey(m_currentChannelId, original);
const QString author = message.messageId.isEmpty() ? QCoreApplication::translate("ChatGui", "Reply") : authorName(message.authorNodeId);
const QString preview = message.messageId.isEmpty()
? QCoreApplication::translate("ChatGui", "Original message is not available yet") : DbManager::messagePreview(message);
if (item->data(MsgQuoteAuthorRole).toString() == author && item->data(MsgQuoteTextRole).toString() == preview) return;
item->setData(author, MsgQuoteAuthorRole); item->setData(preview, MsgQuoteTextRole);
GUI_DEBUG("reply: preview ch=%s original=%s resolved=%d", qPrintable(m_currentChannelId), original.toHex().constData(),
!message.messageId.isEmpty());
}
void MessageList::refreshReplies() {
for (int row = 0; row < m_model->rowCount(); ++row) {
auto* item = m_model->item(row);
const QByteArray key = QByteArray::fromHex(item->data(MsgReplyToRole).toString().toLatin1());
if (!key.isEmpty()) updateReply(item, key);
}
}
/* Перезагружаем только ограниченное окно вокруг цели; обычный refresh сохраняет открытый участок истории. */
void MessageList::jumpToMessage(const QString& messageId) {
if (!m_db || isDmChannel(m_currentChannelId)) return;
const QByteArray key = QByteArray::fromHex(messageId.toLatin1());
if (key.size() != 32) { GUI_ERROR("reply: invalid navigation key=%s", qPrintable(messageId)); return; }
bool present = false;
for (int row = 0; row < m_model->rowCount(); ++row)
if (m_model->item(row)->data(MsgMessageKeyRole).toString() == messageId) { present = true; break; }
if (!present) {
if (m_db->getMessageByKey(m_currentChannelId, key).messageId.isEmpty()) {
GUI_WARN("reply: navigation original unavailable ch=%s id=%s", qPrintable(m_currentChannelId), qPrintable(messageId));
QToolTip::showText(QCursor::pos(), QCoreApplication::translate("ChatGui", "Original message is not available yet"), m_view);
return;
}
m_historyAnchor = key;
loadChannel(m_currentChannelId);
}
GUI_DEBUG("reply: navigate ch=%s id=%s loaded_window=%d", qPrintable(m_currentChannelId), qPrintable(messageId), !present);
const QString channel = m_currentChannelId;
QTimer::singleShot(0, this, [this, channel, messageId]() {
if (channel != m_currentChannelId) return;
for (int row = 0; row < m_model->rowCount(); ++row) {
const QModelIndex index = m_model->index(row, 0);
if (index.data(MsgMessageKeyRole).toString() != messageId) continue;
m_view->scrollTo(index, QAbstractItemView::PositionAtCenter);
m_model->setData(index, true, MsgHighlightRole);
QTimer::singleShot(1500, this, [this, index = QPersistentModelIndex(index)]() {
if (index.isValid()) m_model->setData(index, false, MsgHighlightRole);
});
return;
}
GUI_WARN("reply: navigation target disappeared ch=%s id=%s", qPrintable(channel), qPrintable(messageId));
});
}
void MessageList::onMessageSubmitted(const uint8_t* data, int len) {
if (!data || len != 9 || data[8] > 1) { GUI_ERROR("reply: invalid submission result bytes=%d", len); return; }
quint64 request = 0;
for (unsigned i = 0; i < 8; ++i) request |= quint64(data[i]) << (8 * i);
m_inputBar->submissionResult(request, data[8] != 0);
}
void MessageList::onMessageContextMenu(const QPoint& pos) {
QModelIndex idx = m_view->indexAt(pos);
if (!idx.isValid()) return;
@ -1064,6 +1165,13 @@ void MessageList::onMessageContextMenu(const QPoint& pos) {
QMenu menu(m_view);
QString messageId = idx.data(MsgMessageKeyRole).toString();
if (!isDm && messageId.size() == 64) {
const QString channel = idx.data(MsgChannelIdRole).toString();
connect(menu.addAction(QCoreApplication::translate("ChatGui", "Reply")), &QAction::triggered, this, [this, channel, messageId]() {
if (channel != m_currentChannelId || !m_db) return;
const auto original = m_db->getMessageByKey(channel, QByteArray::fromHex(messageId.toLatin1()));
if (original.messageId.isEmpty()) { GUI_WARN("reply: selected original disappeared ch=%s", qPrintable(channel)); return; }
m_inputBar->setReply(original.messageId, authorName(original.authorNodeId), DbManager::messagePreview(original));
});
if (ct == "poll") connect(menu.addAction(QCoreApplication::translate("ChatGui", "Vote / results")), &QAction::triggered,
this, [this, messageId]() { showPoll(messageId); });
auto* reactions = menu.addMenu(QCoreApplication::translate("ChatGui", "React"));

5
tools/chatgui/src/messagelist.h

@ -44,6 +44,7 @@ public:
void onAttachmentPreparation(const uint8_t* data, int len);
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 onRadioTalk(quint64 groupId, quint64 srcNodeId, bool on); /* индикация говорящего */
signals:
@ -85,6 +86,10 @@ private:
QPointer<QDialog> m_pollDialog;
QString m_pollDialogMessage, m_pendingPoll;
quint64 m_feedbackSequence = 0;
QByteArray m_historyAnchor;
void updateReply(QStandardItem* item, const QByteArray& original);
void refreshReplies();
void jumpToMessage(const QString& messageId);
void sendFeedback(QJsonObject command, bool userAction = true);
void setupFeedback(QStandardItem* item, const MessageRow& message, const QString& channel);

Loading…
Cancel
Save