From 85bdc00aaf33db8ec3518fbf64cc382016cd594e Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 26 Jul 2026 09:19:31 +0300 Subject: [PATCH] =?UTF-8?q?chatgui:=20local=20nicknames=20per=20channel=20?= =?UTF-8?q?member=20=E2=80=94=20edit=20via=20MemberPropsDialog,=20override?= =?UTF-8?q?=20display=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/chatgui/db/db_manager.cpp | 17 +++++++++- tools/chatgui/db/db_manager.h | 2 ++ tools/chatgui/src/accountlist.cpp | 15 +++++++++ tools/chatgui/src/memberlistmodel.cpp | 3 +- tools/chatgui/src/memberlistmodel.h | 1 + tools/chatgui/src/memberpropsdialog.cpp | 45 +++++++++++++++++++------ tools/chatgui/src/memberpropsdialog.h | 2 ++ tools/chatgui/src/messagelist.cpp | 16 +++++++-- tools/chatgui/src/messagelist.h | 2 ++ 9 files changed, 87 insertions(+), 16 deletions(-) diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 942e651b..2e2457e9 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -214,7 +214,7 @@ int DbManager::getChannelMemberCount(const QString& chId) const { QList DbManager::getChannelMembers(const QString& chId, int offset, int limit) const { QList list; QString sql = QStringLiteral( - "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,'')" + "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''), COALESCE(p.local_nick,'')" " FROM \"%1\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " ORDER BY n.online DESC, p.node_id ASC LIMIT ? OFFSET ?" ).arg(peersTableName(chId)); @@ -227,12 +227,27 @@ QList DbManager::getChannelMembers(const QString& chId, int offse m.nodeId = (quint64)sqlite3_column_int64(stmt, 0); m.online = sqlite3_column_int(stmt, 1) != 0; m.name = colText(stmt, 2); + m.localNick = colText(stmt, 3); list.append(m); } sqlite3_finalize(stmt); return list; } +QMap DbManager::getLocalNicknames(const QString& chId) const { + QMap m; + if (!m_db) return m; + QString sql = QStringLiteral( + "SELECT node_id, local_nick FROM \"%1\" WHERE local_nick IS NOT NULL AND local_nick != ''" + ).arg(peersTableName(chId)); + sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); + if (!stmt) return m; + while (sqlite3_step(stmt) == SQLITE_ROW) + m.insert((quint64)sqlite3_column_int64(stmt, 0), colText(stmt, 1)); + sqlite3_finalize(stmt); + return m; +} + void DbManager::cleanOrphanedPeers(const QString& chId) const { if (!m_db) return; QString sql = QStringLiteral( diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index a9c678b8..61410f55 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -49,6 +49,7 @@ struct ChannelMember { quint64 nodeId = 0; bool online = false; QString name; // from nodes.name, empty if not set + QString localNick; // from peers.local_nick override, empty if not set }; struct NodeAddr { @@ -96,6 +97,7 @@ public: QList getChannelMembers(const QString& chId, int offset, int limit) const; void cleanOrphanedPeers(const QString& chId) const; QString getDisplayName(quint64 nodeId) const; + QMap getLocalNicknames(const QString& chId) const; /* ── Узлы ── */ QByteArray loadNodeInfo(quint64 nodeId) const; diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 7d2942cc..8efeebee 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -130,6 +130,21 @@ void AccountList::showMemberDetail(quint64 nodeId) { if (m_detailName.isEmpty()) m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper(); + /* prefer local nickname */ + QString tbl = m_db->peersTableName(m_channelId); + sqlite3_stmt* nickSt = nullptr; + QByteArray nickSql = QString( + "SELECT local_nick FROM \"%1\" WHERE node_id=?" + ).arg(tbl).toUtf8(); + if (sqlite3_prepare_v2(d, nickSql.constData(), -1, &nickSt, nullptr) == SQLITE_OK) { + sqlite3_bind_int64(nickSt, 1, (sqlite3_int64)nodeId); + if (sqlite3_step(nickSt) == SQLITE_ROW) { + QString n = DbManager::colText(nickSt, 0); + if (!n.isEmpty()) m_detailName = n; + } + sqlite3_finalize(nickSt); + } + sqlite3_stmt* na = nullptr; sqlite3_prepare_v2(d, "SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port", diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index fd247638..ee5891a3 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/tools/chatgui/src/memberlistmodel.cpp @@ -18,7 +18,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { auto it = m_cache.find(index.row()); if (it != m_cache.end()) { - if (role == Qt::DisplayRole) return it->name; + if (role == Qt::DisplayRole) return it->localNick.isEmpty() ? it->name : it->localNick; if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar)); if (role == Qt::UserRole + 1) { QString sub; @@ -93,6 +93,7 @@ void MemberListModel::loadBatch(int from, int count) { item.online = members[i].online; QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper(); item.name = members[i].name.isEmpty() ? shortId : members[i].name; + item.localNick = members[i].localNick; QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E"); item.avatar = makeAvatar(c, item.name[0]); } diff --git a/tools/chatgui/src/memberlistmodel.h b/tools/chatgui/src/memberlistmodel.h index 7e9832e5..c247b167 100644 --- a/tools/chatgui/src/memberlistmodel.h +++ b/tools/chatgui/src/memberlistmodel.h @@ -15,6 +15,7 @@ struct MemberListItem { bool online = false; QPixmap avatar; QString name; + QString localNick; int rtt = -1; // ms (min из node_addresses, -1 если нет) int natType = -1; // addr_type: 0=NET, 1=DIR, 2=EIM, 3=STR }; diff --git a/tools/chatgui/src/memberpropsdialog.cpp b/tools/chatgui/src/memberpropsdialog.cpp index 5f788164..24ea6828 100644 --- a/tools/chatgui/src/memberpropsdialog.cpp +++ b/tools/chatgui/src/memberpropsdialog.cpp @@ -21,7 +21,6 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, setMinimumWidth(340); setModal(true); - // читаем ed25519_privkey канала auto chs = m_db->getChannels(); for (const auto& ch : chs) { if (ch.channelId == channelId) { @@ -30,13 +29,14 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, } } bool haveKey = (m_edPrivkey.size() == 32); + bool isOwn = (m_nodeId == m_db->myNodeId()); - // читаем текущий adm_tags bool isAdmin = false, isSuper = false; + QString curNick; if (m_db->m_db) { QString tbl = m_db->peersTableName(channelId); QByteArray sql = QString( - "SELECT adm_tags FROM \"%1\" WHERE node_id=?" + "SELECT adm_tags, local_nick FROM \"%1\" WHERE node_id=?" ).arg(tbl).toUtf8(); sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) { @@ -47,10 +47,10 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, if (strstr(tags, "admin=yes")) isAdmin = true; if (strstr(tags, "supernode=yes")) isSuper = true; } + curNick = DbManager::colText(st, 1); } sqlite3_finalize(st); } - // страховка: добавить колонки если нет QByteArray alt = QString( "ALTER TABLE \"%1\" ADD COLUMN adm_tags TEXT" ).arg(tbl).toUtf8(); @@ -59,6 +59,10 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, "ALTER TABLE \"%1\" ADD COLUMN adm_tags_sig BLOB" ).arg(tbl).toUtf8(); sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr); + alt = QString( + "ALTER TABLE \"%1\" ADD COLUMN local_nick TEXT" + ).arg(tbl).toUtf8(); + sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr); } auto* layout = new QVBoxLayout(this); @@ -72,6 +76,12 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); + m_nickEdit = new QLineEdit(this); + m_nickEdit->setPlaceholderText("Local nickname..."); + m_nickEdit->setText(curNick); + m_nickEdit->setEnabled(!isOwn); + layout->addWidget(m_nickEdit); + m_adminCb = new QCheckBox("Admin", this); m_adminCb->setChecked(isAdmin); m_adminCb->setEnabled(haveKey); @@ -83,7 +93,7 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, layout->addWidget(m_superCb); if (!haveKey) { - auto* hint = new QLabel("(requires channel private key)", this); + auto* hint = new QLabel("(Admin/Super require channel private key)", this); hint->setStyleSheet("color: gray; font-size: 11px;"); layout->addWidget(hint); } @@ -105,7 +115,6 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, auto* saveBtn = new QPushButton("Save", this); saveBtn->setFixedWidth(90); saveBtn->setDefault(true); - saveBtn->setEnabled(haveKey); saveBtn->setStyleSheet( "QPushButton { border: none; border-radius: 4px;" " padding: 6px 16px; font-size: 13px; font-weight: bold;" @@ -119,9 +128,26 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, } void MemberPropsDialog::onSave() { - if (m_edPrivkey.size() != 32 || !m_db->m_db) return; + if (!m_db->m_db) return; + QString tbl = m_db->peersTableName(m_channelId); + + /* local_nick — always save (unsigned, local only) */ + QString nick = m_nickEdit->text().trimmed(); + QByteArray nickSql = QString( + "UPDATE \"%1\" SET local_nick=? WHERE node_id=?" + ).arg(tbl).toUtf8(); + sqlite3_stmt* ns = nullptr; + if (sqlite3_prepare_v2(m_db->m_db, nickSql.constData(), -1, &ns, nullptr) == SQLITE_OK) { + sqlite3_bind_text(ns, 1, nick.isEmpty() ? nullptr : nick.toUtf8().constData(), + -1, nick.isEmpty() ? SQLITE_STATIC : SQLITE_TRANSIENT); + sqlite3_bind_int64(ns, 2, (sqlite3_int64)m_nodeId); + sqlite3_step(ns); + sqlite3_finalize(ns); + } + + /* signed fields — only if key available */ + if (m_edPrivkey.size() != 32) { accept(); return; } - // собираем adm_tags QString tags; if (m_adminCb->isChecked()) tags += "admin=yes"; if (m_superCb->isChecked()) { @@ -129,7 +155,6 @@ void MemberPropsDialog::onSave() { tags += "supernode=yes"; } - // подписываем: tags || node_id(8 LE) QByteArray msg = tags.toUtf8(); uint64_t le = htole64(m_nodeId); msg.append((const char*)&le, 8); @@ -148,8 +173,6 @@ void MemberPropsDialog::onSave() { EVP_PKEY_free(pkey); } - // пишем в БД - QString tbl = m_db->peersTableName(m_channelId); QByteArray sql = QString( "UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=? WHERE node_id=?" ).arg(tbl).toUtf8(); diff --git a/tools/chatgui/src/memberpropsdialog.h b/tools/chatgui/src/memberpropsdialog.h index 78535407..bd19518b 100644 --- a/tools/chatgui/src/memberpropsdialog.h +++ b/tools/chatgui/src/memberpropsdialog.h @@ -2,6 +2,7 @@ #include #include +#include #include class DbManager; @@ -22,6 +23,7 @@ private: quint64 m_nodeId; QByteArray m_edPrivkey; + QLineEdit* m_nickEdit; QCheckBox* m_adminCb; QCheckBox* m_superCb; }; diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index c9c8697b..30e4010d 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -207,11 +207,13 @@ void MessageList::loadChannel(const QString& channelId) { m_readDebounce->stop(); if (!m_db || !m_db->isOpen()) return; + m_localNicks = m_db->getLocalNicknames(channelId); + auto msgs = m_db->getMessages(channelId, 100); for (const auto& m : msgs) { AccountRow acc = m_db->getAccount(m.authorNodeId); - QString author = m_db->getDisplayName(m.authorNodeId); + QString author = authorName(m.authorNodeId); QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; QColor color(acc.avatarColor); @@ -271,12 +273,14 @@ void MessageList::refresh() { auto newMsgs = m_db->getMessagesSince(m_currentChannelId, maxId); if (newMsgs.isEmpty()) return; + m_localNicks = m_db->getLocalNicknames(m_currentChannelId); + QScrollBar* sb = m_view->verticalScrollBar(); bool wasAtBottom = sb && (sb->value() >= sb->maximum()); for (const auto& m : newMsgs) { AccountRow acc = m_db->getAccount(m.authorNodeId); - QString author = m_db->getDisplayName(m.authorNodeId); + QString author = authorName(m.authorNodeId); QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; QColor color(acc.avatarColor); if (!m_avatarCache.contains(m.authorNodeId)) @@ -302,7 +306,7 @@ void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, if (channelId != m_currentChannelId) return; AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow(); - QString author = m_db ? m_db->getDisplayName(authorNodeId) : QString("node_%1").arg(authorNodeId); + QString author = m_db ? authorName(authorNodeId) : QString("node_%1").arg(authorNodeId); QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; QColor color(acc.avatarColor); auto avatar = makeAvatar(color, letter); @@ -356,6 +360,12 @@ void MessageList::saveChannelState() { if (lastPosMsgId > 0) saveKey("_last_pos", lastPosMsgId); } +QString MessageList::authorName(quint64 nodeId) const { + auto it = m_localNicks.find(nodeId); + if (it != m_localNicks.end() && !it->isEmpty()) return *it; + return m_db->getDisplayName(nodeId); +} + void MessageList::restoreScrollPosition(const QString& channelId) { if (!m_db || m_model->rowCount() == 0) { return; diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index 2880b89b..711eec8c 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -39,7 +39,9 @@ private: QTimer* m_readDebounce; qint64 m_savedTopMsgId = 0; bool m_wasAtBottom = false; + QMap m_localNicks; + QString authorName(quint64 nodeId) const; void restoreScrollPosition(const QString& channelId); void updateReadPosition(); };