From 86d227bffee4efcf5c48d2b1990dc34570c356e1 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 23 Jul 2026 23:47:35 +0300 Subject: [PATCH] chatgui: remember last read message, restore scroll position, show unread count badge Store read/scroll positions per channel via ui_state table (key-value). Draw red +N badge on channel list items for unread messages. Fix getMessages() to fetch last N messages (was fetching first N). --- tools/chatgui/db/db_manager.cpp | 27 ++++++++++- tools/chatgui/db/db_manager.h | 5 ++ tools/chatgui/src/channeldelegate.cpp | 22 +++++++++ tools/chatgui/src/channeldelegate.h | 3 +- tools/chatgui/src/channellist.cpp | 25 +++++++++- tools/chatgui/src/channellist.h | 2 + tools/chatgui/src/mainwindow.cpp | 12 ++++- tools/chatgui/src/messagedelegate.h | 1 + tools/chatgui/src/messagelist.cpp | 70 +++++++++++++++++++++++++-- tools/chatgui/src/messagelist.h | 3 ++ 10 files changed, 158 insertions(+), 12 deletions(-) diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index ac8e8dc6..974b566f 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -116,6 +116,9 @@ QList DbManager::getChannels() const { list.append(r); } sqlite3_finalize(stmt); + for (auto& ch : list) { + ch.unreadCount = getUnreadCount(ch.channelId, ch.lastReadMsgId); + } return list; } @@ -125,7 +128,7 @@ QList DbManager::getMessages(const QString& chId, int limit) const { QList list; QString sql = QStringLiteral( "SELECT id, node_id, data, timestamp, chain_hash, author_signature" - " FROM \"%1\" ORDER BY timestamp, author_signature ASC LIMIT ?").arg(msgTableName(chId)); + " FROM \"%1\" ORDER BY timestamp DESC, author_signature DESC LIMIT ?").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; sqlite3_bind_int(stmt, 1, limit); @@ -157,7 +160,7 @@ QList DbManager::getMessages(const QString& chId, int limit) const { m.signature = colBlob(stmt, 5); m.isOutgoing = (m.authorNodeId == myId); m.isRead = true; - list.append(m); + list.prepend(m); } sqlite3_finalize(stmt); return list; @@ -383,6 +386,26 @@ int DbManager::getUiStateInt(const QString& key, int defaultVal) const { return ok ? v : defaultVal; } +qint64 DbManager::getUiStateInt64(const QString& key, qint64 defaultVal) const { + QString val = getUiState(key); + if (val.isEmpty()) return defaultVal; + bool ok = false; + qint64 v = val.toLongLong(&ok); + return ok ? v : defaultVal; +} + +int DbManager::getUnreadCount(const QString& channelId, qint64 lastReadMsgId) const { + QString sql = QStringLiteral( + "SELECT COUNT(*) FROM \"%1\" WHERE id > ?").arg(msgTableName(channelId)); + sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); + if (!stmt) return 0; + sqlite3_bind_int64(stmt, 1, lastReadMsgId); + int count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + return count; +} + /* ── sync operations (read-only, for gui_bridge) ── */ QByteArray DbManager::syncListChannels() const { diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index 3eeecc72..db31df20 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -21,6 +21,7 @@ struct ChannelRow { qint64 lastReadMsgId = 0; qint64 lastPosMsgId = 0; qint64 createdAt = 0; + int unreadCount = 0; }; struct MessageRow { @@ -81,6 +82,10 @@ public: /* ── Каналы ── */ QList getChannels() const; + int getUnreadCount(const QString& channelId, qint64 lastReadMsgId) const; + + /* ── UI state for channel read/scroll ── */ + qint64 getUiStateInt64(const QString& key, qint64 defaultVal = 0) const; /* ── Сообщения (UI) ── */ QList getMessages(const QString& chId, int limit = 50) const; diff --git a/tools/chatgui/src/channeldelegate.cpp b/tools/chatgui/src/channeldelegate.cpp index 9381aeec..a4c1a260 100644 --- a/tools/chatgui/src/channeldelegate.cpp +++ b/tools/chatgui/src/channeldelegate.cpp @@ -125,6 +125,28 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio } } + /* ── unread count badge (right of channel name) ── */ + int unread = index.data(ChannelUnreadCountRole).toInt(); + if (unread > 0) { + QString unreadText = unread > 99 ? QStringLiteral("99+") : QString::number(unread); + QFont badgeFont = option.font; + badgeFont.setBold(true); + badgeFont.setPixelSize(11); + QFontMetrics badgeFm(badgeFont); + int textW = badgeFm.horizontalAdvance(unreadText) + 8; + int badgeH = 18; + if (textW < badgeH) textW = badgeH; + QRect badgeRect(textRect.right() - textW, nameRect.top(), + textW, badgeH); + painter->setRenderHint(QPainter::Antialiasing); + painter->setBrush(QColor("#E53935")); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(badgeRect, badgeH / 2, badgeH / 2); + painter->setFont(badgeFont); + painter->setPen(Qt::white); + painter->drawText(badgeRect, Qt::AlignCenter, unreadText); + } + /* ── invite plus button (bottom-right) ── */ QRect plusRect = inviteButtonRect(option.rect); bool plusHovered = index.data(ChannelInviteBtnHoveredRole).toInt() != 0; diff --git a/tools/chatgui/src/channeldelegate.h b/tools/chatgui/src/channeldelegate.h index 0e1f4bf8..cd9dab1f 100644 --- a/tools/chatgui/src/channeldelegate.h +++ b/tools/chatgui/src/channeldelegate.h @@ -9,7 +9,8 @@ enum ChannelDataRole { ChannelChannelIdRole = Qt::UserRole + 2, ChannelPeersOnlineRole = Qt::UserRole + 3, /* int: count of online peers */ ChannelAutoConnectActiveRole = Qt::UserRole + 4, /* bool: 1 if auto-connect in progress */ - ChannelInviteBtnHoveredRole = Qt::UserRole + 5 /* bool: 1 when mouse hovers invite button */ + ChannelInviteBtnHoveredRole = Qt::UserRole + 5, /* bool: 1 when mouse hovers invite button */ + ChannelUnreadCountRole = Qt::UserRole + 6 /* int: unread message count, 0 if none */ }; class ChannelDelegate : public QStyledItemDelegate { diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 549b9e6d..31c167b5 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -180,10 +180,11 @@ void ChannelList::loadChannels() { item->setData(ch.name, ChannelNameRole); item->setData(ch.channelId, ChannelChannelIdRole); item->setData(QString(), ChannelLastMessageRole); + item->setData(ch.unreadCount, ChannelUnreadCountRole); item->setData(makeChannelIcon(colors[ci % 6], letter), ChannelIconRole); m_model->appendRow(item); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels row=%d cid='%s' name='%s'", - m_model->rowCount()-1, qPrintable(ch.channelId), qPrintable(ch.name)); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels row=%d cid='%s' name='%s' unread=%d", + m_model->rowCount()-1, qPrintable(ch.channelId), qPrintable(ch.name), ch.unreadCount); ci++; } } @@ -240,4 +241,24 @@ void ChannelList::setAutoConnectActive(bool active) { } } +void ChannelList::setChannelUnreadCount(const QString& channelId, int count) { + for (int row = 0; row < m_model->rowCount(); row++) { + QStandardItem* item = m_model->item(row); + if (item && item->data(ChannelChannelIdRole).toString() == channelId) { + item->setData(count, ChannelUnreadCountRole); + emit m_model->dataChanged(m_model->index(row, 0), m_model->index(row, 0)); + return; + } + } +} + +int ChannelList::getChannelUnreadCount(const QString& channelId) const { + for (int row = 0; row < m_model->rowCount(); row++) { + QStandardItem* item = m_model->item(row); + if (item && item->data(ChannelChannelIdRole).toString() == channelId) + return item->data(ChannelUnreadCountRole).toInt(); + } + return 0; +} + #include "channellist.moc" diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index 4faaa80f..b779d312 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -16,6 +16,8 @@ public: void selectChannel(const QString& channelId); void setChannelPeersOnline(const QString& channelId, int count); void setAutoConnectActive(bool active); + void setChannelUnreadCount(const QString& channelId, int count); + int getChannelUnreadCount(const QString& channelId) const; signals: void channelSelected(const QString& channelId); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index d6082030..83b36168 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -212,6 +212,7 @@ void MainWindow::setupConnects() { this, [this](const QString& channelId) { QSettings().setValue("last_channel_id", channelId); m_currentChannelId = channelId; + m_channelList->setChannelUnreadCount(channelId, 0); m_messageList->loadChannel(channelId); m_accountList->setChannel(channelId); QByteArray cidBA = channelId.toUtf8(); @@ -322,8 +323,15 @@ void MainWindow::showSettings() { void MainWindow::onMessageReceived(const char* ch_id) { DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: msg received ch=%s", ch_id); SoundManager::instance()->play("msg_incoming"); - if (m_messageList) - m_messageList->refresh(); + QString cid = QString::fromUtf8(ch_id); + if (m_messageList) { + if (cid == m_currentChannelId) { + m_messageList->refresh(); + } else if (m_channelList) { + int unread = m_channelList->getChannelUnreadCount(cid) + 1; + m_channelList->setChannelUnreadCount(cid, unread); + } + } } void MainWindow::onMembersChanged(const char* ch_id) { diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index e151712e..f10dcd5b 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -12,6 +12,7 @@ enum MessageDataRole { MsgQuoteAuthorRole = Qt::UserRole + 7, MsgQuoteTextRole = Qt::UserRole + 8, MsgReactionsRole = Qt::UserRole + 9, + MsgIdRole = Qt::UserRole + 10, }; class MessageDelegate : public QStyledItemDelegate { diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 4702fe7f..562daa7f 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -167,6 +167,11 @@ MessageList::MessageList(DbManager* db, QWidget *parent) void MessageList::loadChannel(const QString& channelId) { QWidget* prevFocus = QApplication::focusWidget(); + + /* Save state of current channel before switching */ + if (!m_currentChannelId.isEmpty() && m_currentChannelId != channelId) + saveChannelState(); + m_model->clear(); m_avatarCache.clear(); m_currentChannelId = channelId; @@ -174,9 +179,6 @@ void MessageList::loadChannel(const QString& channelId) { auto msgs = m_db->getMessages(channelId, 100); - struct ReactionKey { qint64 msgId; quint64 node; QString emoji; }; - QHash>> reactMap; - for (const auto& m : msgs) { AccountRow acc = m_db->getAccount(m.authorNodeId); QString author = m_db->getDisplayName(m.authorNodeId); @@ -194,11 +196,12 @@ void MessageList::loadChannel(const QString& channelId) { auto *item = new QStandardItem(); setMsg(item, author, QString::fromUtf8(m.data), time, avatar, false, false, QString(), QString(), reacts); + item->setData(m.id, MsgIdRole); m_model->appendRow(item); } - if (m_model->rowCount() > 0) - m_view->scrollToBottom(); + restoreScrollPosition(channelId); + if (prevFocus) prevFocus->setFocus(); else @@ -238,3 +241,60 @@ void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, m_view->scrollToBottom(); } + +void MessageList::saveChannelState() { + if (m_currentChannelId.isEmpty() || m_model->rowCount() == 0) return; + + int vpHeight = m_view->viewport()->height(); + QModelIndex topIdx = m_view->indexAt(QPoint(0, 0)); + QModelIndex bottomIdx = m_view->indexAt(QPoint(0, vpHeight - 1)); + + qint64 lastReadMsgId = 0; + qint64 lastPosMsgId = 0; + + if (topIdx.isValid()) + lastPosMsgId = topIdx.data(MsgIdRole).toLongLong(); + if (bottomIdx.isValid()) + lastReadMsgId = bottomIdx.data(MsgIdRole).toLongLong(); + else if (m_model->rowCount() > 0) + lastReadMsgId = m_model->item(m_model->rowCount() - 1)->data(MsgIdRole).toLongLong(); + + if (lastReadMsgId == 0 && lastPosMsgId == 0) return; + + QByteArray cid = m_currentChannelId.toUtf8(); + auto saveKey = [&cid](const char* suffix, qint64 val) { + QByteArray key = "ch_" + cid + suffix; + QByteArray valStr = QByteArray::number(val); + size_t klen = key.size(), vlen = valStr.size(); + size_t total = klen + 1 + vlen + 1; + if (total > 256) return; + void* arg = u_malloc(total); + memcpy(arg, key.constData(), klen + 1); + memcpy((char*)arg + klen + 1, valStr.constData(), vlen + 1); + gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); + }; + + if (lastReadMsgId > 0) saveKey("_last_read", lastReadMsgId); + if (lastPosMsgId > 0) saveKey("_last_pos", lastPosMsgId); +} + +void MessageList::restoreScrollPosition(const QString& channelId) { + if (!m_db || m_model->rowCount() == 0) { + return; + } + + qint64 lastPosMsgId = m_db->getUiStateInt64( + QStringLiteral("ch_%1_last_pos").arg(channelId), 0); + + if (lastPosMsgId > 0) { + for (int row = 0; row < m_model->rowCount(); row++) { + QStandardItem* item = m_model->item(row); + if (item && item->data(MsgIdRole).toLongLong() == lastPosMsgId) { + m_view->scrollTo(m_model->index(row, 0), QAbstractItemView::PositionAtTop); + return; + } + } + } + + m_view->scrollToBottom(); +} diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index 6c15250a..4af75c75 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -28,4 +28,7 @@ private: QString m_currentChannelId; bool m_animHoverActive = false; QHash m_avatarCache; + + void saveChannelState(); + void restoreScrollPosition(const QString& channelId); };