Browse Source

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
topo_upd
Evgeny 2 months ago
parent
commit
49da899964
  1. 16
      111.txt
  2. 1
      src/routing_layer/topo_node_sqlite.c
  3. 36
      tools/chatgui/db/db_manager.cpp
  4. 6
      tools/chatgui/db/db_manager.h
  5. 3
      tools/chatgui/src/channeldelegate.h
  6. 11
      tools/chatgui/src/channellist.cpp
  7. 3
      tools/chatgui/src/mainwindow.cpp
  8. 29
      tools/chatgui/src/settingsdialog.cpp
  9. 2
      tools/chatgui/src/settingsdialog.h
  10. 10
      tools/chatgui/transport/chat_msg.c

16
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

1
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())" // когда создан
");";

36
tools/chatgui/db/db_manager.cpp

@ -105,13 +105,17 @@ QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) {
/* ── channels ── */
QList<ChannelRow> DbManager::getChannels() const {
QList<ChannelRow> DbManager::getChannels(ChannelSortMode sort) const {
QList<ChannelRow> 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<ChannelRow> 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<ChannelRow> 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<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {

6
tools/chatgui/db/db_manager.h

@ -8,6 +8,8 @@
#include <QObject>
#include <cstdint>
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<ChannelRow> getChannels() const;
QList<ChannelRow> 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 ── */

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

11
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++;
}
}

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

29
tools/chatgui/src/settingsdialog.cpp

@ -18,6 +18,7 @@ extern "C" {
#include <QLabel>
#include <QFormLayout>
#include <QLineEdit>
#include <QComboBox>
#include <QFile>
#include <QTextStream>
@ -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();
}

2
tools/chatgui/src/settingsdialog.h

@ -6,6 +6,7 @@
#include <QStackedWidget>
#include <QPushButton>
#include <QLineEdit>
#include <QComboBox>
#include <QString>
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;

10
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);
}
}

Loading…
Cancel
Save