auto-connect + GUI per-channel indicators

- chat_core_connect_auto: direct ETCP connect from SQLite (no BGP)
- auto_connect: parallel connect to peers with 3s timeout, retry 10s
- RTT saved to node_addresses on disconnect
- vertical status bar + circle badge with online count per channel
- ChannelListView prevents deselection
This commit is contained in:
Evgeny committed 2026-07-15 06:00:43 +03:00
1 parent 1a0229fd4f
commit dccf54b30b
22 files changed
+909 -10

No files matched your search

+36 -3
View File
@@ -9,15 +9,30 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
const QModelIndex &index) const {
painter->save();
const int barWidth = 4;
const int margin = 8;
const int iconSize = 32;
int peersOnline = index.data(ChannelPeersOnlineRole).toInt();
int autoActive = index.data(ChannelAutoConnectActiveRole).toInt();
/* ── background ── */
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
} else if (option.state & QStyle::State_MouseOver) {
painter->fillRect(option.rect, option.palette.light());
}
const int margin = 8;
const int iconSize = 32;
QRect iconRect(option.rect.left() + margin,
/* ── vertical status bar (left edge, over background) ── */
QColor barColor = Qt::transparent;
if (peersOnline > 0) barColor = QColor("#4CAF50");
else if (autoActive && peersOnline == 0) barColor = QColor("#FFC107");
if (barColor.alpha() > 0) {
QRect barRect(option.rect.left(), option.rect.top(), barWidth, option.rect.height());
painter->fillRect(barRect, barColor);
}
QRect iconRect(option.rect.left() + margin + barWidth,
option.rect.top() + (option.rect.height() - iconSize) / 2,
iconSize, iconSize);
@@ -26,6 +41,24 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
icon.paint(painter, iconRect);
}
/* ── online count badge (top-left of avatar, above it) ── */
if (peersOnline > 0) {
int badgeSize = 16;
QRect badgeRect(iconRect.left() - 4, iconRect.top() - 6, badgeSize, badgeSize);
painter->setRenderHint(QPainter::Antialiasing);
painter->setBrush(QColor("#4CAF50"));
painter->setPen(QPen(Qt::white, 1.5));
painter->drawEllipse(badgeRect);
QFont badgeFont = option.font;
badgeFont.setBold(true);
badgeFont.setPixelSize(10);
painter->setFont(badgeFont);
painter->setPen(Qt::white);
QString num = QString::number(peersOnline);
if (peersOnline > 9) num = "9+";
painter->drawText(badgeRect, Qt::AlignCenter, num);
}
QRect textRect(iconRect.right() + margin, option.rect.top() + margin,
option.rect.right() - iconRect.right() - 2 * margin,
option.rect.height() - 2 * margin);
+6 -4
View File
@@ -3,10 +3,12 @@
#include <QStyledItemDelegate>
enum ChannelDataRole {
ChannelNameRole = Qt::DisplayRole,
ChannelIconRole = Qt::DecorationRole,
ChannelLastMessageRole = Qt::UserRole + 1,
ChannelChannelIdRole = Qt::UserRole + 2
ChannelNameRole = Qt::DisplayRole,
ChannelIconRole = Qt::DecorationRole,
ChannelLastMessageRole = Qt::UserRole + 1,
ChannelChannelIdRole = Qt::UserRole + 2,
ChannelPeersOnlineRole = Qt::UserRole + 3, /* int: count of online peers */
ChannelAutoConnectActiveRole = Qt::UserRole + 4 /* bool: 1 if auto-connect in progress */
};
class ChannelDelegate : public QStyledItemDelegate {
+42 -1
View File
@@ -8,6 +8,28 @@
#include <QPainter>
#include <QPixmap>
#include <QMenu>
#include <QMouseEvent>
/* QListView that prevents deselection */
class ChannelListView : public QListView {
public:
using QListView::QListView;
protected:
void mousePressEvent(QMouseEvent* e) override {
if (indexAt(e->pos()).isValid())
QListView::mousePressEvent(e);
}
QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex& idx, const QEvent* e) const override {
if (idx.isValid())
return QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current;
return QItemSelectionModel::NoUpdate;
}
void selectionChanged(const QItemSelection& sel, const QItemSelection& desel) override {
QListView::selectionChanged(sel, desel);
if (selectionModel() && !selectionModel()->hasSelection() && currentIndex().isValid())
selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect);
}
};
static QIcon makeChannelIcon(const QColor &color, const QString &letter) {
QPixmap pix(32, 32);
@@ -29,7 +51,7 @@ static QIcon makeChannelIcon(const QColor &color, const QString &letter) {
ChannelList::ChannelList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_listView(new QListView(this))
, m_listView(new ChannelListView(this))
, m_model(new QStandardItemModel(this))
, m_db(db)
{
@@ -161,3 +183,22 @@ void ChannelList::onContextMenu(const QPoint& pos) {
if (menu.exec(m_listView->viewport()->mapToGlobal(pos)) == inviteAction)
emit inviteRequested(cid);
}
void ChannelList::setChannelPeersOnline(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, ChannelPeersOnlineRole);
item->setData(m_autoConnectActive ? 1 : 0, ChannelAutoConnectActiveRole);
break;
}
}
}
void ChannelList::setAutoConnectActive(bool active) {
m_autoConnectActive = active;
for (int row = 0; row < m_model->rowCount(); row++) {
QStandardItem* item = m_model->item(row);
if (item) item->setData(active ? 1 : 0, ChannelAutoConnectActiveRole);
}
}
+3
View File
@@ -14,6 +14,8 @@ public:
void loadChannels();
void selectChannel(const QString& channelId);
void setChannelPeersOnline(const QString& channelId, int count);
void setAutoConnectActive(bool active);
signals:
void channelSelected(const QString& channelId);
@@ -29,4 +31,5 @@ private:
QListView *m_listView;
QStandardItemModel *m_model;
DbManager *m_db;
bool m_autoConnectActive = false;
};
+27
View File
@@ -46,6 +46,17 @@ static void onMembersChangedCallback(const char* ch_id, int ch_id_len) {
s_mainWindow->onMembersChanged(ch_id);
}
static void onAutoConnectStatusCallback(uint8_t status, uint16_t total_tried,
uint16_t node_count, uint16_t connected) {
if (s_mainWindow)
s_mainWindow->onAutoConnectStatus(status, total_tried, node_count, connected);
}
static void onChannelPeersOnlineCallback(const char* ch_id, int ch_id_len, uint16_t online) {
if (s_mainWindow)
s_mainWindow->onChannelPeersOnline(ch_id, ch_id_len, online);
}
MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
const QString& dbPath, const QString& debugFile,
const QString& debugLevel, const QString& debugCategories)
@@ -76,6 +87,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
qDebug("MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid);
if (s_mainWindow && s_mainWindow->m_db) s_mainWindow->m_db->setMyNodeId((quint64)nid);
});
gui_bridge_set_auto_connect_status_cb(onAutoConnectStatusCallback);
gui_bridge_set_channel_peers_online_cb(onChannelPeersOnlineCallback);
setupNode();
setupMessaging();
@@ -258,6 +271,20 @@ void MainWindow::onMembersChanged(const char* ch_id) {
m_accountList->refresh();
}
void MainWindow::onAutoConnectStatus(uint8_t status, uint16_t /*total_tried*/,
uint16_t /*node_count*/, uint16_t /*connected*/) {
if (m_channelList) {
m_channelList->setAutoConnectActive(status == 0);
}
}
void MainWindow::onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t online) {
if (m_channelList) {
QString cid = QString::fromUtf8(ch_id, ch_id_len);
m_channelList->setChannelPeersOnline(cid, (int)online);
}
}
void MainWindow::reloadChannels() {
if (m_channelList) {
m_channelList->loadChannels();
+2
View File
@@ -33,6 +33,8 @@ private slots:
public:
void onMessageReceived(const char* ch_id);
void onMembersChanged(const char* ch_id);
void onAutoConnectStatus(uint8_t status, uint16_t total, uint16_t totalNodes, uint16_t connected);
void onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t online);
void reloadChannels();
private: