From 49da89996443567f5f354637f0fbd10f5c23a466 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 26 Jul 2026 11:08:59 +0300 Subject: [PATCH] chatgui: channel sorting by name or last message time with preview - Add last_msg_at column to channels table - Update last_msg_at on every message insert (send/receive) - Parameterize getChannels() with ChannelSortMode enum (Alphabetical/Recent) - Add getChannelLastMessage() for preview text in channel list - Add sorting combo to Profile settings page (stored in ui_state) - Reload channels on message receive when sort is recent --- 111.txt | 16 +++++++++++++ src/routing_layer/topo_node_sqlite.c | 1 + tools/chatgui/db/db_manager.cpp | 36 ++++++++++++++++++++++++---- tools/chatgui/db/db_manager.h | 6 ++++- tools/chatgui/src/channeldelegate.h | 3 ++- tools/chatgui/src/channellist.cpp | 11 +++++---- tools/chatgui/src/mainwindow.cpp | 3 +++ tools/chatgui/src/settingsdialog.cpp | 29 ++++++++++++++++++++++ tools/chatgui/src/settingsdialog.h | 2 ++ tools/chatgui/transport/chat_msg.c | 10 +++++++- 10 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 111.txt diff --git a/111.txt b/111.txt new file mode 100644 index 00000000..fbe9b168 --- /dev/null +++ b/111.txt @@ -0,0 +1,16 @@ +было: +Node: 0x32a3a1ee5625ceb9 (chatgui) + online: no + x25519: 33 2A 8D A2..FC 87 00 3F + ed25519: B4 94 9F 4A..5D 9D A9 FB + last_seen: 2026-07-26 07:19:18 + created: 2026-07-23 13:14:13 + addresses: + 192.168.40.200:1333 UDP NETIF rtt=6ms + +стало: +ID:0x32a3a1ee5625ceb9 +[chatgui] * <- зеленая или красная точка если онлайн +[иконка last seen] 1h (5s с интервалом 5 сек, 1m 1h 1d) +[icon created] 20d (дней назад) +UDP:192.168.40.200:1333 diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 01427a98..4e4f7d31 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -70,6 +70,7 @@ int topo_node_sqlite_init(sqlite3* db) { " signature BLOB NOT NULL," // подпись при создании канал приватным ключом канала channel_id(NUL-terminated) || name(NUL-terminated) || owner_node_id(8 байт, LE uint64) || x25519_pubkey(32 байта) || ed25519_pubkey(32 байта) " last_read_msg_id INTEGER," // для скроллинга чата " last_pos_msg_id INTEGER," // для скроллинга чата + " last_msg_at INTEGER DEFAULT 0," // время последнего сообщения (unix timestamp) " created_at INTEGER DEFAULT (unixepoch())" // когда создан ");"; diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 2e2457e9..6e764378 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -105,13 +105,17 @@ QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) { /* ── channels ── */ -QList DbManager::getChannels() const { +QList DbManager::getChannels(ChannelSortMode sort) const { QList list; - sqlite3_stmt* stmt = prepareOrNull( + const char* orderBy = (sort == ChannelSortMode::Recent) + ? " ORDER BY last_msg_at DESC, name COLLATE NOCASE ASC" + : " ORDER BY name COLLATE NOCASE ASC"; + char sql[512]; snprintf(sql, sizeof(sql), "SELECT channel_id, name, owner_node_id," " x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature," - " last_read_msg_id, last_pos_msg_id, created_at" - " FROM channels ORDER BY created_at ASC"); + " last_read_msg_id, last_pos_msg_id, last_msg_at, created_at" + " FROM channels%s", orderBy); + sqlite3_stmt* stmt = prepareOrNull(sql); if (!stmt) return list; while (sqlite3_step(stmt) == SQLITE_ROW) { ChannelRow r; @@ -125,7 +129,8 @@ QList DbManager::getChannels() const { r.signature = colBlob(stmt, 7); r.lastReadMsgId = sqlite3_column_int64(stmt, 8); r.lastPosMsgId = sqlite3_column_int64(stmt, 9); - r.createdAt = sqlite3_column_int64(stmt, 10); + r.lastMsgAt = sqlite3_column_int64(stmt, 10); + r.createdAt = sqlite3_column_int64(stmt, 11); list.append(r); } sqlite3_finalize(stmt); @@ -139,6 +144,27 @@ QList DbManager::getChannels() const { return list; } +QString DbManager::getChannelLastMessage(const QString& channelId) const { + QString sql = QStringLiteral( + "SELECT node_id, data FROM \"%1\" ORDER BY id DESC LIMIT 1") + .arg(msgTableName(channelId)); + sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); + if (!stmt) return {}; + QString result; + if (sqlite3_step(stmt) == SQLITE_ROW) { + quint64 author = (quint64)sqlite3_column_int64(stmt, 0); + QByteArray jdata = colBlob(stmt, 1); + QString text = QString::fromUtf8(parseJsonField(jdata, "d")); + if (text.isEmpty()) text = QString::fromUtf8(parseJsonField(jdata, "ct")); + QString nick = getDisplayName(author); + QString shortText = text.left(60); + if (text.size() > 60) shortText += QStringLiteral("..."); + result = nick + QStringLiteral(": ") + shortText; + } + sqlite3_finalize(stmt); + return result; +} + /* ── messages (UI) ── */ QList DbManager::getMessages(const QString& chId, int limit) const { diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index 61410f55..7acf8e07 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -8,6 +8,8 @@ #include #include +enum class ChannelSortMode { Alphabetical, Recent }; + struct ChannelRow { QString channelId; QString name; @@ -20,6 +22,7 @@ struct ChannelRow { qint64 lastReadMsgId = 0; qint64 lastPosMsgId = 0; + qint64 lastMsgAt = 0; qint64 createdAt = 0; int unreadCount = 0; }; @@ -82,7 +85,8 @@ public: void setMyNodeId(quint64 nodeId) { m_myNodeId = nodeId; } /* ── Каналы ── */ - QList getChannels() const; + QList getChannels(ChannelSortMode sort = ChannelSortMode::Alphabetical) const; + QString getChannelLastMessage(const QString& channelId) const; int getUnreadCount(const QString& channelId, qint64 lastReadMsgId) const; /* ── UI state for channel read/scroll ── */ diff --git a/tools/chatgui/src/channeldelegate.h b/tools/chatgui/src/channeldelegate.h index cd9dab1f..2be95c15 100644 --- a/tools/chatgui/src/channeldelegate.h +++ b/tools/chatgui/src/channeldelegate.h @@ -10,7 +10,8 @@ enum ChannelDataRole { 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 */ - ChannelUnreadCountRole = Qt::UserRole + 6 /* int: unread message count, 0 if none */ + ChannelUnreadCountRole = Qt::UserRole + 6, /* int: unread message count, 0 if none */ + ChannelLastMsgAtRole = Qt::UserRole + 7 /* qint64: last message timestamp for sorting */ }; class ChannelDelegate : public QStyledItemDelegate { diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index a11b91fb..0cf579e2 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -166,7 +166,8 @@ void ChannelList::loadChannels() { sel->blockSignals(true); m_model->clear(); if (m_db && m_db->isOpen()) { - auto channels = m_db->getChannels(); + int sortMode = m_db->getUiStateInt("channel_sort", 0); + auto channels = m_db->getChannels(sortMode == 1 ? ChannelSortMode::Recent : ChannelSortMode::Alphabetical); static const QColor colors[] = { QColor(74,144,226), QColor(126,211,33), QColor(245,166,35), QColor(208,2,27), QColor(144,19,254), QColor(0,184,148) @@ -179,12 +180,14 @@ void ChannelList::loadChannels() { auto *item = new QStandardItem(); item->setData(ch.name, ChannelNameRole); item->setData(ch.channelId, ChannelChannelIdRole); - item->setData(QString(), ChannelLastMessageRole); + item->setData(m_db->getChannelLastMessage(ch.channelId), ChannelLastMessageRole); item->setData(ch.unreadCount, ChannelUnreadCountRole); + item->setData(ch.lastMsgAt, ChannelLastMsgAtRole); 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' unread=%d", - m_model->rowCount()-1, qPrintable(ch.channelId), qPrintable(ch.name), ch.unreadCount); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ChannelList::loadChannels row=%d cid='%s' name='%s' unread=%d lastMsgAt=%lld sort=%d", + m_model->rowCount()-1, qPrintable(ch.channelId), qPrintable(ch.name), ch.unreadCount, + (long long)ch.lastMsgAt, sortMode); ci++; } } diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index ebc3fdb1..1e770188 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -340,6 +340,7 @@ void MainWindow::showJoinDialog() { void MainWindow::showSettings() { SettingsDialog dlg(m_cfgPath, m_db, m_node, this); dlg.exec(); + reloadChannels(); } void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author) { @@ -358,6 +359,8 @@ void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t au m_channelList->setChannelUnreadCount(cid, unread); } } + if (m_db && m_db->getUiStateInt("channel_sort", 0) == 1) + reloadChannels(); } void MainWindow::onMembersChanged(const char* ch_id) { diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index 37f07da9..548d52cb 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -18,6 +18,7 @@ extern "C" { #include #include #include +#include #include #include @@ -69,6 +70,15 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod "QLineEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); profLayout->addRow(nameLabel, m_nicknameEdit); + + auto* sortLabel = new QLabel("Channel sorting:", profilePage); + m_sortCombo = new QComboBox(profilePage); + m_sortCombo->addItem("Alphabetical (A-Z)"); + m_sortCombo->addItem("Recent Activity"); + m_sortCombo->setStyleSheet( + "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" + " padding: 6px 10px; font-size: 13px; background: palette(base); }"); + profLayout->addRow(sortLabel, m_sortCombo); m_pages->addWidget(profilePage); // Network page @@ -124,6 +134,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod // Load current values loadProfileFromConfig(m_configPath); + if (db) { + int sortMode = db->getUiStateInt("channel_sort", 0); + m_sortCombo->setCurrentIndex(sortMode == 1 ? 1 : 0); + } m_networkPage->loadFromConfig(m_configPath); m_soundPage->loadFromDb(); m_categoryList->setCurrentRow(0); @@ -189,6 +203,21 @@ void SettingsDialog::onSave() { f.close(); m_networkPage->saveToConfig(m_configPath); + + /* save channel sort preference to ui_state */ + { + const char* key = "channel_sort"; + QByteArray val = QByteArray::number(m_sortCombo->currentIndex()); + size_t klen = strlen(key), vlen = (size_t)val.size(); + size_t total = klen + 1 + vlen + 1; + if (total <= 256) { + void* arg = u_malloc(total); + memcpy(arg, key, klen + 1); + memcpy((char*)arg + klen + 1, val.constData(), vlen + 1); + gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); + } + } + m_soundPage->applyAndSave(); accept(); } diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index ead5c963..2de4fb6c 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -6,6 +6,7 @@ #include #include #include +#include #include class NetworkSettingsPage; @@ -33,6 +34,7 @@ private: QStackedWidget* m_pages; QPushButton* m_saveBtn; QLineEdit* m_nicknameEdit; + QComboBox* m_sortCombo; QString m_oldNickname; NetworkSettingsPage* m_networkPage; DatabaseSettingsPage* m_databasePage; diff --git a/tools/chatgui/transport/chat_msg.c b/tools/chatgui/transport/chat_msg.c index bcb554b6..e6582f61 100644 --- a/tools/chatgui/transport/chat_msg.c +++ b/tools/chatgui/transport/chat_msg.c @@ -214,9 +214,17 @@ int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size, /* ─── db_sync callback ─── */ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) { - (void)si; (void)record_ts; (void)data; (void)len; (void)author; + (void)si; (void)data; (void)len; const char* ch_id = (const char*)arg; uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ch_id); evt[0] = cl; memcpy(evt + 1, ch_id, cl); memcpy(evt + 1 + cl, &author, 8); gui_bridge_post(GUI_EVT_MSG_RECEIVED, evt, 1 + cl + 8); + + sqlite3_stmt* st = NULL; + sqlite3_prepare_v2(g_cc.db, "UPDATE channels SET last_msg_at = ? WHERE channel_id = ?", -1, &st, NULL); + if (st) { + sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts); + sqlite3_bind_text(st, 2, ch_id, -1, SQLITE_STATIC); + sqlite3_step(st); sqlite3_finalize(st); + } }