chatgui: add scroll-to-bottom floating button, incremental message refresh via getMessagesSince
This commit is contained in:
1 parent
b6d44bd947
commit
ce9e58a925
6 files changed
+114
-16
No files matched your search
@@ -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<MessageRow> 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":<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;
|
||||
{ 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<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
||||
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) ── */
|
||||
|
||||
int DbManager::getChannelMemberCount(const QString& chId) const {
|
||||
|
||||
@@ -89,6 +89,7 @@ public:
|
||||
|
||||
/* ── Сообщения (UI) ── */
|
||||
QList<MessageRow> getMessages(const QString& chId, int limit = 50) const;
|
||||
QList<MessageRow> getMessagesSince(const QString& chId, qint64 sinceId) const;
|
||||
|
||||
/* ── Участники канала (UI) ── */
|
||||
int getChannelMemberCount(const QString& chId) const;
|
||||
|
||||
@@ -6,11 +6,38 @@
|
||||
#include <QDebug>
|
||||
#include <QMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QScrollBar>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QListView>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QPoint>
|
||||
#include <QPushButton>
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in new issue
Block a user