Browse Source

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).
topo_upd
Evgeny 2 months ago
parent
commit
86d227bffe
  1. 27
      tools/chatgui/db/db_manager.cpp
  2. 5
      tools/chatgui/db/db_manager.h
  3. 22
      tools/chatgui/src/channeldelegate.cpp
  4. 3
      tools/chatgui/src/channeldelegate.h
  5. 25
      tools/chatgui/src/channellist.cpp
  6. 2
      tools/chatgui/src/channellist.h
  7. 12
      tools/chatgui/src/mainwindow.cpp
  8. 1
      tools/chatgui/src/messagedelegate.h
  9. 70
      tools/chatgui/src/messagelist.cpp
  10. 3
      tools/chatgui/src/messagelist.h

27
tools/chatgui/db/db_manager.cpp

@ -116,6 +116,9 @@ QList<ChannelRow> 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<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
QList<MessageRow> 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<MessageRow> 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 {

5
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<ChannelRow> 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<MessageRow> getMessages(const QString& chId, int limit = 50) const;

22
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;

3
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 {

25
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"

2
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);

12
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) {

1
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 {

70
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<qint64, QHash<QString, QList<QColor>>> 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();
}

3
tools/chatgui/src/messagelist.h

@ -28,4 +28,7 @@ private:
QString m_currentChannelId;
bool m_animHoverActive = false;
QHash<quint64, QColor> m_avatarCache;
void saveChannelState();
void restoreScrollPosition(const QString& channelId);
};

Loading…
Cancel
Save