diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 69ec5dc5..0c355327 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -8,6 +8,17 @@ /* ── helpers ── */ +static QByteArray parseJsonField(const QByteArray& json, const char* key) { + QByteArray pat = "\"" + QByteArray(key) + "\":\""; + int idx = json.indexOf(pat); + if (idx < 0) return {}; + idx += pat.size(); + int end = idx; + while (end < json.size() && !(json[end] == '"' && (end == idx || json[end - 1] != '\\'))) end++; + if (end > idx) return QByteArray(json.constData() + idx, end - idx); + return {}; +} + QString DbManager::sanitizeTableName(const QString& chId) { QString s; s.reserve(chId.size()); @@ -136,17 +147,6 @@ QList DbManager::getMessages(const QString& chId, int limit) const { sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; sqlite3_bind_int(stmt, 1, limit); - /* helper: parse JSON field from data column: {"n":,"ch":"","ct":"<>","d":"<>"} */ - auto parseJsonField = [](const QByteArray& json, const char* key) -> QByteArray { - QByteArray pat = "\"" + QByteArray(key) + "\":\""; - int idx = json.indexOf(pat); - if (idx < 0) return {}; - idx += pat.size(); - int end = idx; - while (end < json.size() && !(json[end] == '"' && (end == idx || json[end - 1] != '\\'))) end++; - if (end > idx) return QByteArray(json.constData() + idx, end - idx); - return {}; - }; uint64_t myId = 0; { sqlite3_stmt* ms = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1"); if (ms) { if (sqlite3_step(ms) == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(ms, 0); sqlite3_finalize(ms); } } @@ -170,6 +170,33 @@ QList DbManager::getMessages(const QString& chId, int limit) const { return list; } +QList DbManager::getMessagesSince(const QString& chId, qint64 sinceId) const { + QList list; + QString sql = QStringLiteral( + "SELECT id, node_id, data, timestamp, chain_hash, author_signature" + " FROM \"%1\" WHERE id > ? ORDER BY id ASC").arg(msgTableName(chId)); + sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); + if (!stmt) return list; + sqlite3_bind_int64(stmt, 1, sinceId); + uint64_t myId = m_myNodeId; + while (sqlite3_step(stmt) == SQLITE_ROW) { + MessageRow m; + m.id = sqlite3_column_int64(stmt, 0); + m.authorNodeId = (quint64)sqlite3_column_int64(stmt, 1); + QByteArray jdata = colBlob(stmt, 2); + m.data = parseJsonField(jdata, "d"); + m.timestamp = sqlite3_column_int64(stmt, 3); + m.datahash = 0; + m.chainHash = colBlob(stmt, 4); + m.signature = colBlob(stmt, 5); + m.isOutgoing = (m.authorNodeId == myId); + m.isRead = true; + list.append(m); + } + sqlite3_finalize(stmt); + return list; +} + /* ── peers (UI) ── */ int DbManager::getChannelMemberCount(const QString& chId) const { diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index db31df20..a9c678b8 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -89,6 +89,7 @@ public: /* ── Сообщения (UI) ── */ QList getMessages(const QString& chId, int limit = 50) const; + QList getMessagesSince(const QString& chId, qint64 sinceId) const; /* ── Участники канала (UI) ── */ int getChannelMemberCount(const QString& chId) const; diff --git a/tools/chatgui/src/chatview.cpp b/tools/chatgui/src/chatview.cpp index daab56fb..b1bff242 100644 --- a/tools/chatgui/src/chatview.cpp +++ b/tools/chatgui/src/chatview.cpp @@ -6,11 +6,38 @@ #include #include #include +#include ChatView::ChatView(QWidget *parent) : QListView(parent) { setMouseTracking(true); + + m_scrollDownBtn = new QPushButton(QStringLiteral("\u25BC"), this); + m_scrollDownBtn->setFixedSize(36, 36); + m_scrollDownBtn->setCursor(Qt::PointingHandCursor); + m_scrollDownBtn->setStyleSheet( + "QPushButton {" + " background: rgba(60,60,60,180);" + " color: #ffffff;" + " border: none;" + " border-radius: 18px;" + " font-size: 16px;" + "}" + "QPushButton:hover {" + " background: rgba(90,90,90,220);" + "}" + ); + m_scrollDownBtn->setToolTip(tr("Scroll to bottom")); + m_scrollDownBtn->setVisible(false); + + connect(m_scrollDownBtn, &QPushButton::clicked, this, [this]() { + scrollToBottom(); + }); + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &ChatView::updateScrollDownBtn); + connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &ChatView::updateScrollDownBtn); + + m_scrollDownBtn->raise(); } void ChatView::setBackgroundImage(const QString &path) { @@ -24,6 +51,7 @@ void ChatView::setBackgroundImage(const QString &path) { void ChatView::resizeEvent(QResizeEvent *event) { QListView::resizeEvent(event); scheduleDelayedItemsLayout(); + updateScrollDownBtn(); } void ChatView::scrollContentsBy(int dx, int dy) { @@ -150,3 +178,14 @@ void ChatView::copySelection() { QApplication::clipboard()->setText(r); } } + +void ChatView::updateScrollDownBtn() { + QScrollBar* sb = verticalScrollBar(); + bool atBottom = sb && (sb->value() >= sb->maximum()); + m_scrollDownBtn->setVisible(!atBottom); + if (!atBottom) { + QRect vr = viewport()->geometry(); + m_scrollDownBtn->move(vr.right() - 44, vr.bottom() - 44); + m_scrollDownBtn->raise(); + } +} diff --git a/tools/chatgui/src/chatview.h b/tools/chatgui/src/chatview.h index c6250236..694c3e74 100644 --- a/tools/chatgui/src/chatview.h +++ b/tools/chatgui/src/chatview.h @@ -3,6 +3,7 @@ #include #include #include +#include struct ChatTextSelection { QPersistentModelIndex index; @@ -43,6 +44,7 @@ protected: private: void copySelection(); + void updateScrollDownBtn(); QPixmap m_background; QPersistentModelIndex m_selStart; @@ -52,4 +54,5 @@ private: QPersistentModelIndex m_pressIndex; QPoint m_pressPos; bool m_selecting = false; + QPushButton *m_scrollDownBtn = nullptr; }; diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index d6f5de2f..90f7b407 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -250,8 +250,39 @@ void MessageList::loadChannel(const QString& channelId) { } void MessageList::refresh() { - if (!m_currentChannelId.isEmpty()) - loadChannel(m_currentChannelId); + if (m_currentChannelId.isEmpty() || !m_db || !m_db->isOpen()) return; + + qint64 maxId = 0; + for (int r = 0; r < m_model->rowCount(); r++) + if (m_model->item(r)->data(MsgIdRole).toLongLong() > maxId) + maxId = m_model->item(r)->data(MsgIdRole).toLongLong(); + + auto newMsgs = m_db->getMessagesSince(m_currentChannelId, maxId); + if (newMsgs.isEmpty()) return; + + QScrollBar* sb = m_view->verticalScrollBar(); + bool wasAtBottom = sb && (sb->value() >= sb->maximum()); + + for (const auto& m : newMsgs) { + AccountRow acc = m_db->getAccount(m.authorNodeId); + QString author = m_db->getDisplayName(m.authorNodeId); + QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; + QColor color(acc.avatarColor); + if (!m_avatarCache.contains(m.authorNodeId)) + m_avatarCache[m.authorNodeId] = color; + auto avatar = makeAvatar(color, letter); + auto time = QDateTime::fromMSecsSinceEpoch(m.timestamp).toString("HH:mm"); + auto *item = new QStandardItem(); + setMsg(item, author, QString::fromUtf8(m.data), time, avatar, false, + false, QString(), QString(), QVariantList{}); + item->setData(m.id, MsgIdRole); + m_model->appendRow(item); + while (m_model->rowCount() > 100) + m_model->removeRow(0); + } + + if (wasAtBottom) m_view->scrollToBottom(); + m_readDebounce->start(); } void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, diff --git a/tools/chatgui/transport/chat_msg.c b/tools/chatgui/transport/chat_msg.c index b7f0d489..bcb554b6 100644 --- a/tools/chatgui/transport/chat_msg.c +++ b/tools/chatgui/transport/chat_msg.c @@ -59,9 +59,6 @@ void chat_core_submit_message(struct chat_msg_submit* req) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret); return; } - { uint8_t evt[73]; uint8_t cl=(uint8_t)strlen(req->channel_id); evt[0]=cl; memcpy(evt+1,req->channel_id,cl); - memcpy(evt+1+cl, &g_cc.my_node_id, 8); - gui_bridge_post(GUI_EVT_MSG_RECEIVED, evt, 1+cl+8); } } void chat_core_submit_trampoline(void* arg) { chat_core_submit_message((struct chat_msg_submit*)arg); u_free(arg); }