Browse Source

chat: deleted-аккаунты — колонка deleted, скрытие/зачёркивание, не подключаемся

v2
evgeny 1 month ago
parent
commit
a17a85a383
  1. 6
      src/chat/chat_member.c
  2. 1
      src/chat/chat_member.h
  3. 74
      src/routing_layer/topo_node_sqlite.c
  4. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  5. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  6. 11
      tools/chatgui/src/accountdelegate.cpp
  7. 14
      tools/chatgui/src/accountlist.cpp
  8. 1
      tools/chatgui/src/flagpainter.h
  9. 23
      tools/chatgui/src/memberlistmodel.cpp
  10. 3
      tools/chatgui/src/memberlistmodel.h

6
src/chat/chat_member.c

@ -108,7 +108,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
"SELECT p.node_id, COALESCE(n.name,'')," "SELECT p.node_id, COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')," " COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.storage,0)" " COALESCE(p.storage,0), COALESCE(p.deleted,0)"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY p.node_id ASC", peers_tbl); " ORDER BY p.node_id ASC", peers_tbl);
@ -137,6 +137,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
m->storage = (uint8_t)sqlite3_column_int(st, 4); m->storage = (uint8_t)sqlite3_column_int(st, 4);
m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0); m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0);
m->flags = parse_adm_tags_flags(adm_tags); m->flags = parse_adm_tags_flags(adm_tags);
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
m->rtt = 0xFFFF; m->rtt = 0xFFFF;
m->nat_type = 0xFF; m->nat_type = 0xFF;
@ -228,7 +229,7 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
"SELECT COALESCE(n.name,'')," "SELECT COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')," " COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.storage,0)" " COALESCE(p.storage,0), COALESCE(p.deleted,0)"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" WHERE p.node_id=?", peers_tbl); " WHERE p.node_id=?", peers_tbl);
@ -245,6 +246,7 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
m.storage = (uint8_t)sqlite3_column_int(st, 3); m.storage = (uint8_t)sqlite3_column_int(st, 3);
m.is_self = (uint8_t)(node_id == my_id ? 1 : 0); m.is_self = (uint8_t)(node_id == my_id ? 1 : 0);
m.flags = parse_adm_tags_flags(adm_tags); m.flags = parse_adm_tags_flags(adm_tags);
if (sqlite3_column_int(st, 4)) m.flags |= CHAT_MEMBER_FLAG_DELETED;
m.rtt = 0xFFFF; m.rtt = 0xFFFF;
m.nat_type = 0xFF; m.nat_type = 0xFF;

1
src/chat/chat_member.h

@ -29,6 +29,7 @@ extern "C" {
#define CHAT_MEMBER_FLAG_SUPERNODE 0x01 #define CHAT_MEMBER_FLAG_SUPERNODE 0x01
#define CHAT_MEMBER_FLAG_ADMIN 0x02 #define CHAT_MEMBER_FLAG_ADMIN 0x02
#define CHAT_MEMBER_FLAG_MODER 0x04 #define CHAT_MEMBER_FLAG_MODER 0x04
#define CHAT_MEMBER_FLAG_DELETED 0x08
#define CHAT_MEMBER_DISPLAY_SIZE 79 #define CHAT_MEMBER_DISPLAY_SIZE 79

74
src/routing_layer/topo_node_sqlite.c

@ -32,6 +32,12 @@ static void peers_table_name(const char* ch_id, char* buf, size_t sz) {
snprintf(buf, sz, "peers_%s", san); snprintf(buf, sz, "peers_%s", san);
} }
static int adm_tags_deleted(const char* tags) {
if (!tags || !tags[0]) return 0;
char buf[16];
return json_flat_get(tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0;
}
/* ── Классификация адресов (локально, по IP) ── */ /* ── Классификация адресов (локально, по IP) ── */
static int addr_type_from_ip(int family, const uint8_t* addr) { static int addr_type_from_ip(int family, const uint8_t* addr) {
@ -292,6 +298,9 @@ int topo_node_sqlite_init(sqlite3* db) {
snprintf(isql, sizeof(isql), snprintf(isql, sizeof(isql),
"ALTER TABLE \"%s\" ADD COLUMN source INTEGER NOT NULL DEFAULT 0", tbl); "ALTER TABLE \"%s\" ADD COLUMN source INTEGER NOT NULL DEFAULT 0", tbl);
sqlite3_exec(db, isql, NULL, NULL, NULL); sqlite3_exec(db, isql, NULL, NULL, NULL);
snprintf(isql, sizeof(isql),
"ALTER TABLE \"%s\" ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0", tbl);
sqlite3_exec(db, isql, NULL, NULL, NULL);
} }
sqlite3_finalize(s); sqlite3_finalize(s);
} }
@ -338,6 +347,7 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
" node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */ " node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */
" connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */ " connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */
" storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */ " storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */
" deleted INTEGER NOT NULL DEFAULT 0," /* 1 = аккаунт удалён владельцем (не отображаем, не подключаемся) */
" local_nick TEXT," /* локальный ник (GUI only, не синхронизируется) */ " local_nick TEXT," /* локальный ник (GUI only, не синхронизируется) */
" source INTEGER NOT NULL DEFAULT 0" /* 0=полная merkle-запись 1=упрощённый плейсхолдер topo_group " source INTEGER NOT NULL DEFAULT 0" /* 0=полная merkle-запись 1=упрощённый плейсхолдер topo_group
(не участвует в merkle-хеше, дополняется полной записью) */ (не участвует в merkle-хеше, дополняется полной записью) */
@ -424,7 +434,7 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no
if (adm_tags || adm_tags_sig || storage > 0) { if (adm_tags || adm_tags_sig || storage > 0) {
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=? WHERE node_id=?", peers_tbl); "UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=?, deleted=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* u = NULL; sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC); if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC);
@ -432,7 +442,8 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no
if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC); if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC);
else sqlite3_bind_null(u, 2); else sqlite3_bind_null(u, 2);
sqlite3_bind_int(u, 3, storage); sqlite3_bind_int(u, 3, storage);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id); sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags));
sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u); sqlite3_step(u); sqlite3_finalize(u);
} }
} }
@ -512,7 +523,7 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; char sql[256];
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=? WHERE node_id=?", peers_tbl); "UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=?, deleted=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* u = NULL; sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) != SQLITE_OK) return -1; if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) != SQLITE_OK) return -1;
if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC); if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC);
@ -520,7 +531,8 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6
if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC); if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC);
else sqlite3_bind_null(u, 2); else sqlite3_bind_null(u, 2);
sqlite3_bind_int(u, 3, storage); sqlite3_bind_int(u, 3, storage);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id); sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags));
sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u); sqlite3_step(u); sqlite3_finalize(u);
return 0; return 0;
} }
@ -785,32 +797,34 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) {
while (sqlite3_step(s) == SQLITE_ROW) { while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0); const char* tbl = (const char*)sqlite3_column_text(s, 0);
int node_type = base_type; int is_storage = 0; int node_type = base_type; int is_storage = 0; int is_deleted = 0;
if (best_at == ADDR_TYPE_DIRECT && base_type == 1) { snprintf(sql, sizeof(sql),
snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl);
"SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl); sqlite3_stmt* a = NULL;
sqlite3_stmt* a = NULL; if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) { sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id);
sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id); if (sqlite3_step(a) == SQLITE_ROW) {
if (sqlite3_step(a) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(a, 0);
const char* tags = (const char*)sqlite3_column_text(a, 0); if (tags) {
if (tags) { char buf[64];
char buf[64]; if (best_at == ADDR_TYPE_DIRECT && base_type == 1) {
if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4; if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4;
if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1; if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1;
} }
is_deleted = adm_tags_deleted(tags);
} }
sqlite3_finalize(a);
} }
sqlite3_finalize(a);
} }
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET node_type=?, storage=? WHERE node_id=?", tbl); "UPDATE \"%s\" SET node_type=?, storage=?, deleted=? WHERE node_id=?", tbl);
sqlite3_stmt* u = NULL; sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
sqlite3_bind_int(u, 1, node_type); sqlite3_bind_int(u, 1, node_type);
sqlite3_bind_int(u, 2, is_storage); sqlite3_bind_int(u, 2, is_storage);
sqlite3_bind_int64(u, 3, (sqlite3_int64)node_id); sqlite3_bind_int(u, 3, is_deleted);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u); sqlite3_step(u); sqlite3_finalize(u);
} }
} }
@ -1013,7 +1027,7 @@ int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id,
if (!db || !channel_id || !out_ids || !out_count) return -1; if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0; *out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[128]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1", peers_tbl); char sql[192]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", peers_tbl);
sqlite3_stmt* st = NULL; sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int cnt = 0; int cnt = 0;
@ -1030,13 +1044,15 @@ int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id,
int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id, int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) { uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !out_ids || !out_count) return -1; if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0; *out_ids = NULL; *out_count = 0;
(void)channel_id; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
const char* sql = char sql[384]; snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na" "SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)" " WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"; " AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL; sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id); sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);
@ -1058,7 +1074,7 @@ int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id,
*out_ids = NULL; *out_count = 0; *out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; snprintf(sql, sizeof(sql), char sql[256]; snprintf(sql, sizeof(sql),
"SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4" "SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4 AND p.deleted=0"
" AND EXISTS (SELECT 1 FROM node_addresses na WHERE na.node_id=p.node_id AND na.family IN (4,6))", " AND EXISTS (SELECT 1 FROM node_addresses na WHERE na.node_id=p.node_id AND na.family IN (4,6))",
peers_tbl); peers_tbl);
sqlite3_stmt* st = NULL; sqlite3_stmt* st = NULL;
@ -1077,13 +1093,15 @@ int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id,
int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id, int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) { uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !out_ids || !out_count) return -1; if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0; *out_ids = NULL; *out_count = 0;
(void)channel_id; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
const char* sql = char sql[384]; snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na" "SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)" " WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"; " AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL; sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id); sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);

2
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -24,6 +24,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -176,6 +177,7 @@ private fun MemberListItem(
fontSize = 15.sp, fontSize = 15.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textDecoration = if (member.isDeleted) TextDecoration.LineThrough else TextDecoration.None,
color = if (member.isSelf) Color(0xFF4CAF50) else Color.Unspecified color = if (member.isSelf) Color(0xFF4CAF50) else Color.Unspecified
) )
val parts = mutableListOf<String>() val parts = mutableListOf<String>()

2
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -382,7 +382,7 @@ class ChatViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val (raw, owner) = withContext(Dispatchers.IO) { r.getMembers(chId) to r.isChannelOwner(chId) } val (raw, owner) = withContext(Dispatchers.IO) { r.getMembers(chId) to r.isChannelOwner(chId) }
_channelOwner.value = owner _channelOwner.value = owner
_members.value = raw.map { m -> _members.value = raw.filter { owner || !it.isDeleted }.map { m ->
val ns = statuses[m.nodeId] val ns = statuses[m.nodeId]
val base = if (ns != null) m.copy( val base = if (ns != null) m.copy(
online = ns.online, rtt = ns.bestRtt, online = ns.online, rtt = ns.bestRtt,

11
tools/chatgui/src/accountdelegate.cpp

@ -29,9 +29,12 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
const int w = option.rect.right() - x - margin; const int w = option.rect.right() - x - margin;
const int yMid = option.rect.top() + option.rect.height() / 2; const int yMid = option.rect.top() + option.rect.height() / 2;
uint8_t flags = (uint8_t)(index.data(Qt::UserRole + 3).toInt() & 0xFF);
QFont nameFont = painter->font(); QFont nameFont = painter->font();
nameFont.setBold(true); nameFont.setBold(true);
nameFont.setPixelSize(13); nameFont.setPixelSize(13);
if (flags & MEMBER_FLAG_DELETED) nameFont.setStrikeOut(true);
painter->setFont(nameFont); painter->setFont(nameFont);
QColor textColor = option.palette.color(QPalette::Text); QColor textColor = option.palette.color(QPalette::Text);
@ -39,12 +42,12 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
if (index.data(Qt::UserRole + 2).toBool()) textColor = QColor("#4CAF50"); if (index.data(Qt::UserRole + 2).toBool()) textColor = QColor("#4CAF50");
painter->setPen(textColor); painter->setPen(textColor);
uint8_t flags = (uint8_t)(index.data(Qt::UserRole + 3).toInt() & 0xFF); const uint8_t visualFlags = flags & (MEMBER_FLAG_SUPERNODE | MEMBER_FLAG_ADMIN | MEMBER_FLAG_MODER);
int flagX = x; int flagX = x;
if (flags) { if (visualFlags) {
FlagPainter::draw(painter, flags, flagX, option.rect.top() + (option.rect.height() - nameFont.pixelSize()) / 2, nameFont.pixelSize()); FlagPainter::draw(painter, flags, flagX, option.rect.top() + (option.rect.height() - nameFont.pixelSize()) / 2, nameFont.pixelSize());
flagX += nameFont.pixelSize() * ((flags & 0x01 ? 1 : 0) + (flags & 0x02 ? 1 : 0) + (flags & 0x04 ? 1 : 0)) int n = __builtin_popcount(visualFlags);
+ ((__builtin_popcount(flags) - 1) * 2); flagX += nameFont.pixelSize() * n + (n - 1) * 2;
} }
QString name = index.data(Qt::DisplayRole).toString(); QString name = index.data(Qt::DisplayRole).toString();

14
tools/chatgui/src/accountlist.cpp

@ -149,12 +149,6 @@ AccountList::~AccountList() {
void AccountList::setChannel(const QString& channelId) { void AccountList::setChannel(const QString& channelId) {
m_channelId = channelId; m_channelId = channelId;
m_model->setChannel(channelId);
m_detailTimer->stop();
m_linkTimer->stop();
m_detailNodeId = 0;
m_detailHeader->setText("Select a member");
m_detailText->clear();
bool isOwner = false; bool isOwner = false;
if (m_db && m_db->isOpen()) { if (m_db && m_db->isOpen()) {
@ -163,6 +157,14 @@ void AccountList::setChannel(const QString& channelId) {
if (ch.channelId == channelId) { isOwner = (ch.ed25519_privkey.size() == 32); break; } if (ch.channelId == channelId) { isOwner = (ch.ed25519_privkey.size() == 32); break; }
} }
} }
m_model->setChannel(channelId, isOwner);
m_detailTimer->stop();
m_linkTimer->stop();
m_detailNodeId = 0;
m_detailHeader->setText("Select a member");
m_detailText->clear();
m_header->setText(isOwner ? QString::fromUtf8("Members \xF0\x9F\x94\x91") : QStringLiteral("Members")); m_header->setText(isOwner ? QString::fromUtf8("Members \xF0\x9F\x94\x91") : QStringLiteral("Members"));
m_header->setToolTip(isOwner ? QStringLiteral("Channel owner (has private key)") : QString()); m_header->setToolTip(isOwner ? QStringLiteral("Channel owner (has private key)") : QString());
} }

1
tools/chatgui/src/flagpainter.h

@ -8,6 +8,7 @@
#define MEMBER_FLAG_SUPERNODE 0x01 #define MEMBER_FLAG_SUPERNODE 0x01
#define MEMBER_FLAG_ADMIN 0x02 #define MEMBER_FLAG_ADMIN 0x02
#define MEMBER_FLAG_MODER 0x04 #define MEMBER_FLAG_MODER 0x04
#define MEMBER_FLAG_DELETED 0x08
class FlagPainter { class FlagPainter {
public: public:

23
tools/chatgui/src/memberlistmodel.cpp

@ -56,9 +56,10 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
return {}; return {};
} }
void MemberListModel::setChannel(const QString& channelId) { void MemberListModel::setChannel(const QString& channelId, bool isOwner) {
beginResetModel(); beginResetModel();
m_channelId = channelId; m_channelId = channelId;
m_isOwner = isOwner;
m_totalCount = 0; m_totalCount = 0;
m_cache.clear(); m_cache.clear();
endResetModel(); endResetModel();
@ -70,7 +71,7 @@ void MemberListModel::setChannel(const QString& channelId) {
} }
void MemberListModel::refresh() { void MemberListModel::refresh() {
if (!m_channelId.isEmpty()) setChannel(m_channelId); if (!m_channelId.isEmpty()) setChannel(m_channelId, m_isOwner);
} }
void MemberListModel::refreshRtt() { void MemberListModel::refreshRtt() {
@ -119,14 +120,17 @@ void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t
if (chId != m_channelId) return; if (chId != m_channelId) return;
beginResetModel(); beginResetModel();
m_totalCount = count; m_totalCount = 0;
m_cache.clear(); m_cache.clear();
int row = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
MemberListItem item; MemberListItem item;
deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item); deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item);
m_cache[i] = item; if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) continue;
m_cache[row++] = item;
} }
m_totalCount = row;
endResetModel(); endResetModel();
} }
@ -142,6 +146,17 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data)
if (it->nodeId == item.nodeId) { row = it.key(); break; } if (it->nodeId == item.nodeId) { row = it.key(); break; }
} }
if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) {
/* deleted member — remove from non-owner's list */
if (row >= 0) {
beginRemoveRows(QModelIndex(), row, row);
m_cache.remove(row);
m_totalCount--;
endRemoveRows();
}
return;
}
if (row >= 0) { if (row >= 0) {
m_cache[row] = item; m_cache[row] = item;
emit dataChanged(index(row), index(row)); emit dataChanged(index(row), index(row));

3
tools/chatgui/src/memberlistmodel.h

@ -29,7 +29,7 @@ public:
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
void setChannel(const QString& channelId); void setChannel(const QString& channelId, bool isOwner);
void refresh(); void refresh();
void refreshRtt(); void refreshRtt();
quint64 nodeIdAt(int row) const; quint64 nodeIdAt(int row) const;
@ -44,6 +44,7 @@ private:
QPixmap makeAvatar(QColor color, QChar letter) const; QPixmap makeAvatar(QColor color, QChar letter) const;
QString m_channelId; QString m_channelId;
bool m_isOwner = false;
int m_totalCount = 0; int m_totalCount = 0;
mutable QMap<int, MemberListItem> m_cache; mutable QMap<int, MemberListItem> m_cache;
QTimer* m_rttTimer; QTimer* m_rttTimer;

Loading…
Cancel
Save