Browse Source

chatgui: add scroll-to-bottom floating button, incremental message refresh via getMessagesSince

topo_upd
Evgeny 2 months ago
parent
commit
ce9e58a925
  1. 49
      tools/chatgui/db/db_manager.cpp
  2. 1
      tools/chatgui/db/db_manager.h
  3. 39
      tools/chatgui/src/chatview.cpp
  4. 3
      tools/chatgui/src/chatview.h
  5. 35
      tools/chatgui/src/messagelist.cpp
  6. 3
      tools/chatgui/transport/chat_msg.c

49
tools/chatgui/db/db_manager.cpp

@ -8,6 +8,17 @@
/* ── helpers ── */ /* ── 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 DbManager::sanitizeTableName(const QString& chId) {
QString s; QString s;
s.reserve(chId.size()); s.reserve(chId.size());
@ -136,17 +147,6 @@ QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return list; if (!stmt) return list;
sqlite3_bind_int(stmt, 1, limit); sqlite3_bind_int(stmt, 1, limit);
/* helper: parse JSON field from data column: {"n":<id>,"ch":"<id>","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; uint64_t myId = 0;
{ sqlite3_stmt* ms = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1"); { 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); } } if (ms) { if (sqlite3_step(ms) == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(ms, 0); sqlite3_finalize(ms); } }
@ -170,6 +170,33 @@ QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
return list; return list;
} }
QList<MessageRow> DbManager::getMessagesSince(const QString& chId, qint64 sinceId) const {
QList<MessageRow> 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) ── */ /* ── peers (UI) ── */
int DbManager::getChannelMemberCount(const QString& chId) const { int DbManager::getChannelMemberCount(const QString& chId) const {

1
tools/chatgui/db/db_manager.h

@ -89,6 +89,7 @@ public:
/* ── Сообщения (UI) ── */ /* ── Сообщения (UI) ── */
QList<MessageRow> getMessages(const QString& chId, int limit = 50) const; QList<MessageRow> getMessages(const QString& chId, int limit = 50) const;
QList<MessageRow> getMessagesSince(const QString& chId, qint64 sinceId) const;
/* ── Участники канала (UI) ── */ /* ── Участники канала (UI) ── */
int getChannelMemberCount(const QString& chId) const; int getChannelMemberCount(const QString& chId) const;

39
tools/chatgui/src/chatview.cpp

@ -6,11 +6,38 @@
#include <QDebug> #include <QDebug>
#include <QMouseEvent> #include <QMouseEvent>
#include <QKeyEvent> #include <QKeyEvent>
#include <QScrollBar>
ChatView::ChatView(QWidget *parent) ChatView::ChatView(QWidget *parent)
: QListView(parent) : QListView(parent)
{ {
setMouseTracking(true); 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) { void ChatView::setBackgroundImage(const QString &path) {
@ -24,6 +51,7 @@ void ChatView::setBackgroundImage(const QString &path) {
void ChatView::resizeEvent(QResizeEvent *event) { void ChatView::resizeEvent(QResizeEvent *event) {
QListView::resizeEvent(event); QListView::resizeEvent(event);
scheduleDelayedItemsLayout(); scheduleDelayedItemsLayout();
updateScrollDownBtn();
} }
void ChatView::scrollContentsBy(int dx, int dy) { void ChatView::scrollContentsBy(int dx, int dy) {
@ -150,3 +178,14 @@ void ChatView::copySelection() {
QApplication::clipboard()->setText(r); 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();
}
}

3
tools/chatgui/src/chatview.h

@ -3,6 +3,7 @@
#include <QListView> #include <QListView>
#include <QPersistentModelIndex> #include <QPersistentModelIndex>
#include <QPoint> #include <QPoint>
#include <QPushButton>
struct ChatTextSelection { struct ChatTextSelection {
QPersistentModelIndex index; QPersistentModelIndex index;
@ -43,6 +44,7 @@ protected:
private: private:
void copySelection(); void copySelection();
void updateScrollDownBtn();
QPixmap m_background; QPixmap m_background;
QPersistentModelIndex m_selStart; QPersistentModelIndex m_selStart;
@ -52,4 +54,5 @@ private:
QPersistentModelIndex m_pressIndex; QPersistentModelIndex m_pressIndex;
QPoint m_pressPos; QPoint m_pressPos;
bool m_selecting = false; bool m_selecting = false;
QPushButton *m_scrollDownBtn = nullptr;
}; };

35
tools/chatgui/src/messagelist.cpp

@ -250,8 +250,39 @@ void MessageList::loadChannel(const QString& channelId) {
} }
void MessageList::refresh() { void MessageList::refresh() {
if (!m_currentChannelId.isEmpty()) if (m_currentChannelId.isEmpty() || !m_db || !m_db->isOpen()) return;
loadChannel(m_currentChannelId);
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, void MessageList::addMessage(const QString& channelId, quint64 authorNodeId,

3
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); DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret);
return; 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); } void chat_core_submit_trampoline(void* arg) { chat_core_submit_message((struct chat_msg_submit*)arg); u_free(arg); }

Loading…
Cancel
Save