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).
This commit is contained in:
1 parent
de14f82048
commit
86d227bffe
10 files changed
+158
-12
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -28,4 +28,7 @@ private:
|
||||
QString m_currentChannelId;
|
||||
bool m_animHoverActive = false;
|
||||
QHash<quint64, QColor> m_avatarCache;
|
||||
|
||||
void saveChannelState();
|
||||
void restoreScrollPosition(const QString& channelId);
|
||||
};
|
||||
Reference in new issue
Block a user