Browse Source

chatgui: local nicknames per channel member — edit via MemberPropsDialog, override display everywhere

topo_upd
Evgeny 2 months ago
parent
commit
85bdc00aaf
  1. 17
      tools/chatgui/db/db_manager.cpp
  2. 2
      tools/chatgui/db/db_manager.h
  3. 15
      tools/chatgui/src/accountlist.cpp
  4. 3
      tools/chatgui/src/memberlistmodel.cpp
  5. 1
      tools/chatgui/src/memberlistmodel.h
  6. 45
      tools/chatgui/src/memberpropsdialog.cpp
  7. 2
      tools/chatgui/src/memberpropsdialog.h
  8. 16
      tools/chatgui/src/messagelist.cpp
  9. 2
      tools/chatgui/src/messagelist.h

17
tools/chatgui/db/db_manager.cpp

@ -214,7 +214,7 @@ int DbManager::getChannelMemberCount(const QString& chId) const {
QList<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offset, int limit) const { QList<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offset, int limit) const {
QList<ChannelMember> list; QList<ChannelMember> list;
QString sql = QStringLiteral( 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" " 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 ?" " ORDER BY n.online DESC, p.node_id ASC LIMIT ? OFFSET ?"
).arg(peersTableName(chId)); ).arg(peersTableName(chId));
@ -227,12 +227,27 @@ QList<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offse
m.nodeId = (quint64)sqlite3_column_int64(stmt, 0); m.nodeId = (quint64)sqlite3_column_int64(stmt, 0);
m.online = sqlite3_column_int(stmt, 1) != 0; m.online = sqlite3_column_int(stmt, 1) != 0;
m.name = colText(stmt, 2); m.name = colText(stmt, 2);
m.localNick = colText(stmt, 3);
list.append(m); list.append(m);
} }
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return list; return list;
} }
QMap<quint64, QString> DbManager::getLocalNicknames(const QString& chId) const {
QMap<quint64, QString> 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 { void DbManager::cleanOrphanedPeers(const QString& chId) const {
if (!m_db) return; if (!m_db) return;
QString sql = QStringLiteral( QString sql = QStringLiteral(

2
tools/chatgui/db/db_manager.h

@ -49,6 +49,7 @@ struct ChannelMember {
quint64 nodeId = 0; quint64 nodeId = 0;
bool online = false; bool online = false;
QString name; // from nodes.name, empty if not set QString name; // from nodes.name, empty if not set
QString localNick; // from peers.local_nick override, empty if not set
}; };
struct NodeAddr { struct NodeAddr {
@ -96,6 +97,7 @@ public:
QList<ChannelMember> getChannelMembers(const QString& chId, int offset, int limit) const; QList<ChannelMember> getChannelMembers(const QString& chId, int offset, int limit) const;
void cleanOrphanedPeers(const QString& chId) const; void cleanOrphanedPeers(const QString& chId) const;
QString getDisplayName(quint64 nodeId) const; QString getDisplayName(quint64 nodeId) const;
QMap<quint64, QString> getLocalNicknames(const QString& chId) const;
/* ── Узлы ── */ /* ── Узлы ── */
QByteArray loadNodeInfo(quint64 nodeId) const; QByteArray loadNodeInfo(quint64 nodeId) const;

15
tools/chatgui/src/accountlist.cpp

@ -130,6 +130,21 @@ void AccountList::showMemberDetail(quint64 nodeId) {
if (m_detailName.isEmpty()) if (m_detailName.isEmpty())
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper(); 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_stmt* na = nullptr;
sqlite3_prepare_v2(d, sqlite3_prepare_v2(d,
"SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port", "SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port",

3
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()); auto it = m_cache.find(index.row());
if (it != m_cache.end()) { 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::DecorationRole) return QVariant::fromValue(QIcon(it->avatar));
if (role == Qt::UserRole + 1) { if (role == Qt::UserRole + 1) {
QString sub; QString sub;
@ -93,6 +93,7 @@ void MemberListModel::loadBatch(int from, int count) {
item.online = members[i].online; item.online = members[i].online;
QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper(); 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.name = members[i].name.isEmpty() ? shortId : members[i].name;
item.localNick = members[i].localNick;
QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E"); QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E");
item.avatar = makeAvatar(c, item.name[0]); item.avatar = makeAvatar(c, item.name[0]);
} }

1
tools/chatgui/src/memberlistmodel.h

@ -15,6 +15,7 @@ struct MemberListItem {
bool online = false; bool online = false;
QPixmap avatar; QPixmap avatar;
QString name; QString name;
QString localNick;
int rtt = -1; // ms (min из node_addresses, -1 если нет) int rtt = -1; // ms (min из node_addresses, -1 если нет)
int natType = -1; // addr_type: 0=NET, 1=DIR, 2=EIM, 3=STR int natType = -1; // addr_type: 0=NET, 1=DIR, 2=EIM, 3=STR
}; };

45
tools/chatgui/src/memberpropsdialog.cpp

@ -21,7 +21,6 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
setMinimumWidth(340); setMinimumWidth(340);
setModal(true); setModal(true);
// читаем ed25519_privkey канала
auto chs = m_db->getChannels(); auto chs = m_db->getChannels();
for (const auto& ch : chs) { for (const auto& ch : chs) {
if (ch.channelId == channelId) { if (ch.channelId == channelId) {
@ -30,13 +29,14 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
} }
} }
bool haveKey = (m_edPrivkey.size() == 32); bool haveKey = (m_edPrivkey.size() == 32);
bool isOwn = (m_nodeId == m_db->myNodeId());
// читаем текущий adm_tags
bool isAdmin = false, isSuper = false; bool isAdmin = false, isSuper = false;
QString curNick;
if (m_db->m_db) { if (m_db->m_db) {
QString tbl = m_db->peersTableName(channelId); QString tbl = m_db->peersTableName(channelId);
QByteArray sql = QString( QByteArray sql = QString(
"SELECT adm_tags FROM \"%1\" WHERE node_id=?" "SELECT adm_tags, local_nick FROM \"%1\" WHERE node_id=?"
).arg(tbl).toUtf8(); ).arg(tbl).toUtf8();
sqlite3_stmt* st = nullptr; sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) { 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, "admin=yes")) isAdmin = true;
if (strstr(tags, "supernode=yes")) isSuper = true; if (strstr(tags, "supernode=yes")) isSuper = true;
} }
curNick = DbManager::colText(st, 1);
} }
sqlite3_finalize(st); sqlite3_finalize(st);
} }
// страховка: добавить колонки если нет
QByteArray alt = QString( QByteArray alt = QString(
"ALTER TABLE \"%1\" ADD COLUMN adm_tags TEXT" "ALTER TABLE \"%1\" ADD COLUMN adm_tags TEXT"
).arg(tbl).toUtf8(); ).arg(tbl).toUtf8();
@ -59,6 +59,10 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
"ALTER TABLE \"%1\" ADD COLUMN adm_tags_sig BLOB" "ALTER TABLE \"%1\" ADD COLUMN adm_tags_sig BLOB"
).arg(tbl).toUtf8(); ).arg(tbl).toUtf8();
sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr); 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); auto* layout = new QVBoxLayout(this);
@ -72,6 +76,12 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
title->setStyleSheet("font-weight: bold; font-size: 13px;"); title->setStyleSheet("font-weight: bold; font-size: 13px;");
layout->addWidget(title); 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 = new QCheckBox("Admin", this);
m_adminCb->setChecked(isAdmin); m_adminCb->setChecked(isAdmin);
m_adminCb->setEnabled(haveKey); m_adminCb->setEnabled(haveKey);
@ -83,7 +93,7 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
layout->addWidget(m_superCb); layout->addWidget(m_superCb);
if (!haveKey) { 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;"); hint->setStyleSheet("color: gray; font-size: 11px;");
layout->addWidget(hint); layout->addWidget(hint);
} }
@ -105,7 +115,6 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
auto* saveBtn = new QPushButton("Save", this); auto* saveBtn = new QPushButton("Save", this);
saveBtn->setFixedWidth(90); saveBtn->setFixedWidth(90);
saveBtn->setDefault(true); saveBtn->setDefault(true);
saveBtn->setEnabled(haveKey);
saveBtn->setStyleSheet( saveBtn->setStyleSheet(
"QPushButton { border: none; border-radius: 4px;" "QPushButton { border: none; border-radius: 4px;"
" padding: 6px 16px; font-size: 13px; font-weight: bold;" " padding: 6px 16px; font-size: 13px; font-weight: bold;"
@ -119,9 +128,26 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
} }
void MemberPropsDialog::onSave() { 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; QString tags;
if (m_adminCb->isChecked()) tags += "admin=yes"; if (m_adminCb->isChecked()) tags += "admin=yes";
if (m_superCb->isChecked()) { if (m_superCb->isChecked()) {
@ -129,7 +155,6 @@ void MemberPropsDialog::onSave() {
tags += "supernode=yes"; tags += "supernode=yes";
} }
// подписываем: tags || node_id(8 LE)
QByteArray msg = tags.toUtf8(); QByteArray msg = tags.toUtf8();
uint64_t le = htole64(m_nodeId); uint64_t le = htole64(m_nodeId);
msg.append((const char*)&le, 8); msg.append((const char*)&le, 8);
@ -148,8 +173,6 @@ void MemberPropsDialog::onSave() {
EVP_PKEY_free(pkey); EVP_PKEY_free(pkey);
} }
// пишем в БД
QString tbl = m_db->peersTableName(m_channelId);
QByteArray sql = QString( QByteArray sql = QString(
"UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=? WHERE node_id=?" "UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=? WHERE node_id=?"
).arg(tbl).toUtf8(); ).arg(tbl).toUtf8();

2
tools/chatgui/src/memberpropsdialog.h

@ -2,6 +2,7 @@
#include <QDialog> #include <QDialog>
#include <QCheckBox> #include <QCheckBox>
#include <QLineEdit>
#include <QString> #include <QString>
class DbManager; class DbManager;
@ -22,6 +23,7 @@ private:
quint64 m_nodeId; quint64 m_nodeId;
QByteArray m_edPrivkey; QByteArray m_edPrivkey;
QLineEdit* m_nickEdit;
QCheckBox* m_adminCb; QCheckBox* m_adminCb;
QCheckBox* m_superCb; QCheckBox* m_superCb;
}; };

16
tools/chatgui/src/messagelist.cpp

@ -207,11 +207,13 @@ void MessageList::loadChannel(const QString& channelId) {
m_readDebounce->stop(); m_readDebounce->stop();
if (!m_db || !m_db->isOpen()) return; if (!m_db || !m_db->isOpen()) return;
m_localNicks = m_db->getLocalNicknames(channelId);
auto msgs = m_db->getMessages(channelId, 100); auto msgs = m_db->getMessages(channelId, 100);
for (const auto& m : msgs) { for (const auto& m : msgs) {
AccountRow acc = m_db->getAccount(m.authorNodeId); 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; QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
QColor color(acc.avatarColor); QColor color(acc.avatarColor);
@ -271,12 +273,14 @@ void MessageList::refresh() {
auto newMsgs = m_db->getMessagesSince(m_currentChannelId, maxId); auto newMsgs = m_db->getMessagesSince(m_currentChannelId, maxId);
if (newMsgs.isEmpty()) return; if (newMsgs.isEmpty()) return;
m_localNicks = m_db->getLocalNicknames(m_currentChannelId);
QScrollBar* sb = m_view->verticalScrollBar(); QScrollBar* sb = m_view->verticalScrollBar();
bool wasAtBottom = sb && (sb->value() >= sb->maximum()); bool wasAtBottom = sb && (sb->value() >= sb->maximum());
for (const auto& m : newMsgs) { for (const auto& m : newMsgs) {
AccountRow acc = m_db->getAccount(m.authorNodeId); 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; QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
QColor color(acc.avatarColor); QColor color(acc.avatarColor);
if (!m_avatarCache.contains(m.authorNodeId)) if (!m_avatarCache.contains(m.authorNodeId))
@ -302,7 +306,7 @@ void MessageList::addMessage(const QString& channelId, quint64 authorNodeId,
if (channelId != m_currentChannelId) return; if (channelId != m_currentChannelId) return;
AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow(); 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; QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
QColor color(acc.avatarColor); QColor color(acc.avatarColor);
auto avatar = makeAvatar(color, letter); auto avatar = makeAvatar(color, letter);
@ -356,6 +360,12 @@ void MessageList::saveChannelState() {
if (lastPosMsgId > 0) saveKey("_last_pos", lastPosMsgId); 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) { void MessageList::restoreScrollPosition(const QString& channelId) {
if (!m_db || m_model->rowCount() == 0) { if (!m_db || m_model->rowCount() == 0) {
return; return;

2
tools/chatgui/src/messagelist.h

@ -39,7 +39,9 @@ private:
QTimer* m_readDebounce; QTimer* m_readDebounce;
qint64 m_savedTopMsgId = 0; qint64 m_savedTopMsgId = 0;
bool m_wasAtBottom = false; bool m_wasAtBottom = false;
QMap<quint64, QString> m_localNicks;
QString authorName(quint64 nodeId) const;
void restoreScrollPosition(const QString& channelId); void restoreScrollPosition(const QString& channelId);
void updateReadPosition(); void updateReadPosition();
}; };

Loading…
Cancel
Save