From 459c61933ba4af1657ed282d21deaab326fb9f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 13 Oct 2026 21:26:45 +0200 Subject: [PATCH] [Client] Moderation and alts dashboards Turn the two staff tables into dashboards without behavior changes: TabModeration gets an identity header (display-size name, status, registered/last-login subline), Reports/Warnings/Bans/Alts count cards with an Open-graph deep link, full-width admin notes, and Alts/Sessions/Staff tabbed sections with tab memory. TabAlts gets toggle chips with match counts, bold usernames, palette-token dimming of shared-IP rows, and a resizable side card for the selected-user timeline with selection restore. Row activation works via Enter as well as double-click; async responses are guarded against stale investigations and retranslation re-applies cached dashboard data. --- .../src/interface/widgets/tabs/tab_alts.cpp | 163 ++++++++++---- .../src/interface/widgets/tabs/tab_alts.h | 8 +- .../interface/widgets/tabs/tab_moderation.cpp | 211 ++++++++++++------ .../interface/widgets/tabs/tab_moderation.h | 30 ++- 4 files changed, 286 insertions(+), 126 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp index 06863c131..ca4a495b9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp @@ -5,17 +5,19 @@ #include "libcockatrice/protocol/pb/response_user_alts.pb.h" #include -#include #include #include #include #include #include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -46,6 +48,17 @@ constexpr int COL_ALTS_BANS = 9; constexpr int COL_ALTS_ACTIVE = 10; constexpr int COL_ALTS_COUNT = 11; +constexpr int COL_WARNS_TIME = 0; +constexpr int COL_WARNS_MODERATOR = 1; +constexpr int COL_WARNS_REASON = 2; +constexpr int COL_WARNS_COUNT = 3; + +constexpr int COL_BANS_TIME = 0; +constexpr int COL_BANS_MODERATOR = 1; +constexpr int COL_BANS_LENGTH = 2; +constexpr int COL_BANS_REASON = 3; +constexpr int COL_BANS_COUNT = 4; + constexpr int MAX_ALT_DEPTH = 3; // Starting default only (adjustable 1-100000): shared VPN exits routinely // cover dozens of users, so 10 hides the obvious VPN noise but keeps @@ -94,15 +107,16 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q topBar->addWidget(depthSpin); topBar->addWidget(investigateButton); - emailCheck = new QCheckBox; - emailCheck->setChecked(true); - connect(emailCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged); - clientIdCheck = new QCheckBox; - clientIdCheck->setChecked(true); - connect(clientIdCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged); - ipCheck = new QCheckBox; - ipCheck->setChecked(true); - connect(ipCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged); + auto makeChip = [this](const QString &text) { + auto *chip = new QPushButton(text); + chip->setCheckable(true); + chip->setChecked(true); + connect(chip, &QPushButton::toggled, this, &TabAlts::onFilterChanged); + return chip; + }; + emailChip = makeChip(QString()); + clientIdChip = makeChip(QString()); + ipChip = makeChip(QString()); vpnLabel = new QLabel; vpnSpin = new QSpinBox; vpnSpin->setMinimum(1); @@ -111,17 +125,20 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q connect(vpnSpin, &QSpinBox::valueChanged, this, &TabAlts::onFilterChanged); auto *filterBar = new QHBoxLayout; - filterBar->addWidget(emailCheck); - filterBar->addWidget(clientIdCheck); - filterBar->addWidget(ipCheck); + filterBar->addWidget(emailChip); + filterBar->addWidget(clientIdChip); + filterBar->addWidget(ipChip); filterBar->addWidget(vpnLabel); filterBar->addWidget(vpnSpin); filterBar->addStretch(); altsTable = new QTableWidget(0, COL_ALTS_COUNT); configureAltTable(altsTable); - connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { - auto *userItem = altsTable->item(row, COL_ALTS_USER); + connect(altsTable, &QTableWidget::activated, this, [this](const QModelIndex &index) { + if (!index.isValid()) { + return; + } + auto *userItem = altsTable->item(index.row(), COL_ALTS_USER); if (userItem != nullptr && !userItem->text().isEmpty() && userItem->text() != currentUser) { investigate(userItem->text()); } @@ -134,9 +151,9 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q detailNotesEdit->setReadOnly(true); // Compact cap so the warn/ban tables stay visible without scrolling the tab. detailNotesEdit->setMaximumHeight(60); - warnsTable = new QTableWidget(0, 3); + warnsTable = new QTableWidget(0, COL_WARNS_COUNT); configureAltTable(warnsTable); - bansTable = new QTableWidget(0, 4); + bansTable = new QTableWidget(0, COL_BANS_COUNT); configureAltTable(bansTable); auto *detailGrid = new QGridLayout; @@ -150,21 +167,24 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q detailGrid->addWidget(detailNotesLabel, 1, 0); detailGrid->addWidget(detailNotesEdit, 1, 1, 1, 3); - auto *detailTables = new QHBoxLayout; - detailTables->addWidget(warnsTable); - detailTables->addWidget(bansTable); - detailsGroup = new QGroupBox; auto *detailsLayout = new QVBoxLayout(detailsGroup); detailsLayout->addLayout(detailGrid); - detailsLayout->addLayout(detailTables); + detailsLayout->addWidget(warnsTable, 1); + detailsLayout->addWidget(bansTable, 1); + + auto *contentSplitter = new QSplitter(Qt::Horizontal); + contentSplitter->addWidget(altsTable); + contentSplitter->addWidget(detailsGroup); + contentSplitter->setStretchFactor(0, 3); + contentSplitter->setStretchFactor(1, 2); + detailsGroup->setMinimumWidth(260); auto *mainLayout = new QVBoxLayout(centralWidget); mainLayout->addLayout(topBar); mainLayout->addLayout(filterBar); mainLayout->addWidget(statusLabel); - mainLayout->addWidget(altsTable, 3); - mainLayout->addWidget(detailsGroup, 2); + mainLayout->addWidget(contentSplitter, 1); retranslateUi(); clearDetails(); @@ -183,15 +203,19 @@ void TabAlts::retranslateUi() statusLabel->setText(tr("Enter a user name to investigate.")); } - emailCheck->setText(tr("Email")); - clientIdCheck->setText(tr("Client ID")); - ipCheck->setText(tr("IP")); + emailChip->setText(tr("Email")); + emailChip->setToolTip(tr("Show or hide email matches")); + clientIdChip->setText(tr("Client ID")); + clientIdChip->setToolTip(tr("Show or hide client ID matches")); + ipChip->setText(tr("IP")); + ipChip->setToolTip(tr("Show or hide IP address matches")); vpnLabel->setText(tr("Hide IPs shared by more than")); vpnSpin->setSuffix(tr(" users")); altsTable->setHorizontalHeaderLabels({tr("User"), tr("Via"), tr("Depth"), tr("Shared"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"), tr("Active")}); + altsTable->setToolTip(tr("Double-click or press Enter on a row to investigate that account")); warnsTable->setHorizontalHeaderLabels({tr("Warning time"), tr("Moderator"), tr("Reason")}); bansTable->setHorizontalHeaderLabels({tr("Ban time"), tr("Moderator"), tr("Length"), tr("Reason")}); @@ -199,6 +223,10 @@ void TabAlts::retranslateUi() detailNameLabel->setText(tr("Name:")); detailCountsLabel->setText(tr("Counts:")); detailNotesLabel->setText(tr("Admin notes:")); + + if (!lastAlts.isEmpty()) { + renderTable(); + } } QString TabAlts::formatEpoch(quint64 ts) const @@ -259,7 +287,11 @@ void TabAlts::requestAlts(const QString &userName, int depth) cmd.set_user_name(userName.toStdString()); cmd.set_depth(static_cast(depth)); PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabAlts::altsResponse); + connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) { + if (userName == currentUser) { + altsResponse(response); + } + }); client->sendCommand(pend); } @@ -290,13 +322,13 @@ bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const } const QString reason = QString::fromStdString(alt.match_reason()); if (reason == QStringLiteral("email")) { - return emailCheck->isChecked(); + return emailChip->isChecked(); } if (reason.startsWith(QStringLiteral("clientid:"))) { - return clientIdCheck->isChecked(); + return clientIdChip->isChecked(); } if (reason.startsWith(QStringLiteral("ip:"))) { - return ipCheck->isChecked() && + return ipChip->isChecked() && (!alt.has_shared_count() || alt.shared_count() <= static_cast(vpnSpin->value())); } return true; @@ -304,7 +336,32 @@ bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const void TabAlts::renderTable() { + // Blocked so the programmatic refill neither wipes the details panel + // (via the selection-cleared path) nor refetches them on restore. + const QSignalBlocker blocker(altsTable); altsTable->setRowCount(0); + + int emailCount = 0; + int clientIdCount = 0; + int ipCount = 0; + for (const ServerInfo_UserAlt &alt : lastAlts) { + const QString reason = QString::fromStdString(alt.match_reason()); + if (reason == QStringLiteral("email")) { + ++emailCount; + } else if (reason.startsWith(QStringLiteral("clientid:"))) { + ++clientIdCount; + } else if (reason.startsWith(QStringLiteral("ip:"))) { + ++ipCount; + } + } + emailChip->setText(tr("Email (%1)").arg(emailCount)); + clientIdChip->setText(tr("Client ID (%1)").arg(clientIdCount)); + ipChip->setText(tr("IP (%1)").arg(ipCount)); + + QFont userFont = altsTable->font(); + userFont.setBold(true); + const QColor dimColor = palette().color(QPalette::Disabled, QPalette::Text); + int hidden = 0; int row = 0; for (const ServerInfo_UserAlt &alt : lastAlts) { @@ -313,7 +370,9 @@ void TabAlts::renderTable() continue; } altsTable->insertRow(row); - altsTable->setItem(row, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name()))); + auto *userItem = new QTableWidgetItem(QString::fromStdString(alt.user_name())); + userItem->setFont(userFont); + altsTable->setItem(row, COL_ALTS_USER, userItem); altsTable->setItem(row, COL_ALTS_VIA, new QTableWidgetItem(alt.has_match_reason() && !alt.match_reason().empty() ? QString::fromStdString(alt.match_reason()) @@ -330,6 +389,14 @@ void TabAlts::renderTable() altsTable->setItem(row, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count()))); altsTable->setItem(row, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count()))); altsTable->setItem(row, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no"))); + if (QString::fromStdString(alt.match_reason()).startsWith(QStringLiteral("ip:")) && alt.has_shared_count() && + alt.shared_count() > 1) { + for (int col = 0; col < COL_ALTS_COUNT; ++col) { + if (auto *item = altsTable->item(row, col)) { + item->setForeground(dimColor); + } + } + } ++row; } if (hidden == 0) { @@ -341,6 +408,22 @@ void TabAlts::renderTable() .arg(lastAlts.size()) .arg(currentUser)); } + + if (!selectedUser.isEmpty()) { + bool restored = false; + for (int restoreRow = 0; restoreRow < altsTable->rowCount(); ++restoreRow) { + auto *userItem = altsTable->item(restoreRow, COL_ALTS_USER); + if (userItem != nullptr && userItem->text() == selectedUser) { + altsTable->setCurrentItem(userItem, QItemSelectionModel::Select | QItemSelectionModel::Rows); + restored = true; + break; + } + } + if (!restored) { + selectedUser.clear(); + clearDetails(); + } + } } void TabAlts::onFilterChanged() @@ -409,7 +492,7 @@ void TabAlts::requestDetails(const QString &userName) void TabAlts::clearDetails() { detailNameValue->clear(); - detailCountsValue->clear(); + detailCountsValue->setText(tr("Select a row to inspect its history.")); detailNotesEdit->clear(); warnsTable->setRowCount(0); bansTable->setRowCount(0); @@ -446,9 +529,9 @@ void TabAlts::warnHistoryResponse(const Response &response) warnsTable->setRowCount(resp.warn_list_size()); for (int i = 0; i < resp.warn_list_size(); ++i) { const ServerInfo_Warning &warn = resp.warn_list(i); - warnsTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(warn.time_of()))); - warnsTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(warn.admin_name()))); - warnsTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(warn.reason()))); + warnsTable->setItem(i, COL_WARNS_TIME, new QTableWidgetItem(QString::fromStdString(warn.time_of()))); + warnsTable->setItem(i, COL_WARNS_MODERATOR, new QTableWidgetItem(QString::fromStdString(warn.admin_name()))); + warnsTable->setItem(i, COL_WARNS_REASON, new QTableWidgetItem(QString::fromStdString(warn.reason()))); } } @@ -463,9 +546,9 @@ void TabAlts::banHistoryResponse(const Response &response) bansTable->setRowCount(resp.ban_list_size()); for (int i = 0; i < resp.ban_list_size(); ++i) { const ServerInfo_Ban &ban = resp.ban_list(i); - bansTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(ban.ban_time()))); - bansTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(ban.admin_name()))); - bansTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(ban.ban_length()))); - bansTable->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(ban.ban_reason()))); + bansTable->setItem(i, COL_BANS_TIME, new QTableWidgetItem(QString::fromStdString(ban.ban_time()))); + bansTable->setItem(i, COL_BANS_MODERATOR, new QTableWidgetItem(QString::fromStdString(ban.admin_name()))); + bansTable->setItem(i, COL_BANS_LENGTH, new QTableWidgetItem(QString::fromStdString(ban.ban_length()))); + bansTable->setItem(i, COL_BANS_REASON, new QTableWidgetItem(QString::fromStdString(ban.ban_reason()))); } } diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.h b/cockatrice/src/interface/widgets/tabs/tab_alts.h index 65833f90d..8c0086f25 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.h +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.h @@ -10,7 +10,7 @@ #include class AbstractClient; -class QCheckBox; +class QGroupBox; class QLabel; class QLineEdit; class QPushButton; @@ -70,9 +70,9 @@ private: QLabel *statusLabel; QTableWidget *altsTable; - QCheckBox *emailCheck; - QCheckBox *clientIdCheck; - QCheckBox *ipCheck; + QPushButton *emailChip; + QPushButton *clientIdChip; + QPushButton *ipChip; QLabel *vpnLabel; QSpinBox *vpnSpin; diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp index c04de44ce..2aacfb1be 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -94,38 +95,67 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli topBar->addWidget(removeAvatarButton); userInfoGroup = new QGroupBox; - userInfoNameLabel = new QLabel; userInfoNameValue = new QLabel; - userInfoRegisteredLabel = new QLabel; - userInfoRegisteredValue = new QLabel; - userInfoLastLoginLabel = new QLabel; - userInfoLastLoginValue = new QLabel; - userInfoStatusLabel = new QLabel; + QFont displayFont = userInfoNameValue->font(); + displayFont.setPointSize(displayFont.pointSize() + 6); + displayFont.setBold(true); + userInfoNameValue->setFont(displayFont); userInfoStatusValue = new QLabel; - userInfoCountsLabel = new QLabel; - userInfoCountsValue = new QLabel; + QFont statusFont = userInfoStatusValue->font(); + statusFont.setBold(true); + userInfoStatusValue->setFont(statusFont); + sublineLabel = new QLabel; + + auto makeCountCard = [](const QString &title, QLabel *&value) { + auto *card = new QGroupBox(title); + value = new QLabel(QStringLiteral("–"), card); + QFont cardFont = value->font(); + cardFont.setPointSize(cardFont.pointSize() + 6); + cardFont.setBold(true); + value->setFont(cardFont); + value->setAlignment(Qt::AlignCenter); + auto *cardLayout = new QVBoxLayout(card); + cardLayout->addWidget(value); + return card; + }; + cardReports = makeCountCard(QString(), cardReportsValue); + cardWarns = makeCountCard(QString(), cardWarnsValue); + cardBans = makeCountCard(QString(), cardBansValue); + + altsCard = new QGroupBox; + altsCountValue = new QLabel(QStringLiteral("–"), altsCard); + QFont altsFont = altsCountValue->font(); + altsFont.setPointSize(altsFont.pointSize() + 6); + altsFont.setBold(true); + altsCountValue->setFont(altsFont); + altsCountValue->setAlignment(Qt::AlignCenter); + openAltsButton = new QPushButton; + connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph); + auto *altsCardLayout = new QVBoxLayout(altsCard); + altsCardLayout->addWidget(altsCountValue); + altsCardLayout->addWidget(openAltsButton); + + auto *cardsRow = new QHBoxLayout; + cardsRow->addWidget(cardReports); + cardsRow->addWidget(cardWarns); + cardsRow->addWidget(cardBans); + cardsRow->addWidget(altsCard); + userInfoNotesLabel = new QLabel; userInfoNotesEdit = new QTextEdit; userInfoNotesEdit->setReadOnly(true); - auto *infoGrid = new QGridLayout; - infoGrid->addWidget(userInfoNameLabel, 0, 0); - infoGrid->addWidget(userInfoNameValue, 0, 1); - infoGrid->addWidget(userInfoRegisteredLabel, 0, 2); - infoGrid->addWidget(userInfoRegisteredValue, 0, 3); - infoGrid->addWidget(userInfoLastLoginLabel, 1, 0); - infoGrid->addWidget(userInfoLastLoginValue, 1, 1); - infoGrid->addWidget(userInfoStatusLabel, 1, 2); - infoGrid->addWidget(userInfoStatusValue, 1, 3); - infoGrid->addWidget(userInfoCountsLabel, 2, 0); - infoGrid->addWidget(userInfoCountsValue, 2, 1, 1, 3); - infoGrid->addWidget(userInfoNotesLabel, 3, 0, Qt::AlignTop); - infoGrid->addWidget(userInfoNotesEdit, 3, 1, 1, 3); - infoGrid->setColumnStretch(1, 1); - infoGrid->setColumnStretch(3, 1); + auto *identityRow = new QHBoxLayout; + identityRow->addWidget(userInfoNameValue); + identityRow->addStretch(); + identityRow->addWidget(userInfoStatusValue); auto *infoLayout = new QVBoxLayout(userInfoGroup); - infoLayout->addLayout(infoGrid); + infoLayout->addLayout(identityRow); + infoLayout->addWidget(sublineLabel); + infoLayout->addLayout(cardsRow); + infoLayout->addWidget(userInfoNotesLabel); + infoLayout->addWidget(userInfoNotesEdit); auto configureTable = [](QTableWidget *table) { table->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -137,31 +167,27 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli table->horizontalHeader()->setStretchLastSection(true); }; - altsGroup = new QGroupBox; altsTable = new QTableWidget(0, COL_ALTS_COUNT); configureTable(altsTable); - connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { - auto *userItem = altsTable->item(row, COL_ALTS_USER); + connect(altsTable, &QTableWidget::activated, this, [this](const QModelIndex &index) { + if (!index.isValid()) { + return; + } + auto *userItem = altsTable->item(index.row(), COL_ALTS_USER); if (userItem != nullptr && !userItem->text().isEmpty()) { getTabSupervisor()->openTabAlts(userItem->text()); } }); - openAltsButton = new QPushButton; - connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph); - auto *altsHeader = new QHBoxLayout; - altsHeader->addStretch(); - altsHeader->addWidget(openAltsButton); - auto *altsLayout = new QVBoxLayout(altsGroup); - altsLayout->addLayout(altsHeader); + auto *altsPage = new QWidget; + auto *altsLayout = new QVBoxLayout(altsPage); altsLayout->addWidget(altsTable); - sessionsGroup = new QGroupBox; sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT); configureTable(sessionsTable); - auto *sessionsLayout = new QVBoxLayout(sessionsGroup); + auto *sessionsPage = new QWidget; + auto *sessionsLayout = new QVBoxLayout(sessionsPage); sessionsLayout->addWidget(sessionsTable); - staffGroup = new QGroupBox; staffTable = new QTableWidget(0, COL_STAFF_COUNT); configureTable(staffTable); refreshStaffButton = new QPushButton; @@ -169,19 +195,21 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli auto *staffHeader = new QHBoxLayout; staffHeader->addStretch(); staffHeader->addWidget(refreshStaffButton); - auto *staffLayout = new QVBoxLayout(staffGroup); + auto *staffPage = new QWidget; + auto *staffLayout = new QVBoxLayout(staffPage); staffLayout->addWidget(staffTable); staffLayout->addLayout(staffHeader); + sectionTabs = new QTabWidget; + sectionTabs->addTab(altsPage, QString()); + sectionTabs->addTab(sessionsPage, QString()); + sectionTabs->addTab(staffPage, QString()); + auto *splitter = new QSplitter(Qt::Vertical); splitter->addWidget(userInfoGroup); - splitter->addWidget(altsGroup); - splitter->addWidget(sessionsGroup); - splitter->addWidget(staffGroup); + splitter->addWidget(sectionTabs); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 2); - splitter->setStretchFactor(2, 2); - splitter->setStretchFactor(3, 1); auto *mainLayout = new QVBoxLayout(centralWidget); mainLayout->addLayout(topBar); @@ -202,21 +230,28 @@ void TabModeration::retranslateUi() refreshStaffButton->setText(tr("Refresh")); userInfoGroup->setTitle(tr("User Info")); - userInfoNameLabel->setText(tr("Name:")); - userInfoRegisteredLabel->setText(tr("Registered:")); - userInfoLastLoginLabel->setText(tr("Last login:")); - userInfoStatusLabel->setText(tr("Status:")); - userInfoCountsLabel->setText(tr("Counts:")); userInfoNotesLabel->setText(tr("Admin notes:")); - - altsGroup->setTitle(tr("Alts")); - openAltsButton->setText(tr("Open alt graph")); + cardReports->setTitle(tr("Reports")); + cardWarns->setTitle(tr("Warnings")); + cardBans->setTitle(tr("Bans")); + altsCard->setTitle(tr("Alts")); + openAltsButton->setText(tr("Open graph")); openAltsButton->setToolTip(tr("Open this user in the User Alts tab")); - sessionsGroup->setTitle(tr("Sessions")); - staffGroup->setTitle(tr("Staff Last Logins")); + + sectionTabs->setTabText(0, tr("Alts")); + sectionTabs->setTabText(1, tr("Sessions")); + sectionTabs->setTabText(2, tr("Staff Last Logins")); + + if (cachedUserInfoValid) { + applyUserInfo(); + } + if (cachedAltsCount >= 0) { + applyAltsCount(); + } altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"), tr("Active")}); + altsTable->setToolTip(tr("Double-click or press Enter on a row to open it in the User Alts tab")); sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")}); staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")}); } @@ -232,11 +267,15 @@ QString TabModeration::formatEpoch(quint64 ts) const void TabModeration::clearUserData() { currentUser.clear(); + cachedUserInfoValid = false; + cachedAltsCount = -1; userInfoNameValue->clear(); - userInfoRegisteredValue->clear(); - userInfoLastLoginValue->clear(); userInfoStatusValue->clear(); - userInfoCountsValue->clear(); + sublineLabel->clear(); + cardReportsValue->setText(QStringLiteral("–")); + cardWarnsValue->setText(QStringLiteral("–")); + cardBansValue->setText(QStringLiteral("–")); + altsCountValue->setText(QStringLiteral("–")); userInfoNotesEdit->clear(); altsTable->setRowCount(0); sessionsTable->setRowCount(0); @@ -261,9 +300,12 @@ void TabModeration::investigateUser() return; } currentUser = userName; + cachedUserInfoValid = false; + cachedAltsCount = -1; resetPasswordButton->setEnabled(true); removeAvatarButton->setEnabled(true); openAltsButton->setEnabled(true); + altsCountValue->setText(QStringLiteral("…")); altsTable->setRowCount(0); sessionsTable->setRowCount(0); requestUserInfo(userName); @@ -282,10 +324,11 @@ void TabModeration::openAltsGraph() void TabModeration::requestUserInfo(const QString &userName) { userInfoNameValue->setText(userName); - userInfoRegisteredValue->setText(tr("Loading...")); - userInfoLastLoginValue->setText(tr("Loading...")); + sublineLabel->setText(tr("Loading...")); userInfoStatusValue->clear(); - userInfoCountsValue->clear(); + cardReportsValue->setText(QStringLiteral("…")); + cardWarnsValue->setText(QStringLiteral("…")); + cardBansValue->setText(QStringLiteral("…")); userInfoNotesEdit->clear(); Command_ReportUserInfo cmd; @@ -298,9 +341,12 @@ void TabModeration::requestUserInfo(const QString &userName) void TabModeration::userInfoResponse(const Response &response) { if (response.response_code() != Response::RespOk) { - userInfoRegisteredValue->clear(); - userInfoLastLoginValue->clear(); + cachedUserInfoValid = false; + sublineLabel->clear(); userInfoStatusValue->setText(tr("Error loading user info.")); + cardReportsValue->setText(QStringLiteral("–")); + cardWarnsValue->setText(QStringLiteral("–")); + cardBansValue->setText(QStringLiteral("–")); return; } @@ -309,20 +355,28 @@ void TabModeration::userInfoResponse(const Response &response) return; } - userInfoRegisteredValue->setText(formatEpoch(resp.registration_time())); - userInfoLastLoginValue->setText(formatEpoch(resp.last_login())); + cachedUserInfo.CopyFrom(resp); + cachedUserInfoValid = true; + applyUserInfo(); +} + +void TabModeration::applyUserInfo() +{ + const Response_ReportUserInfo &resp = cachedUserInfo; + sublineLabel->setText(tr("Registered %1 · Last login %2") + .arg(formatEpoch(resp.registration_time())) + .arg(formatEpoch(resp.last_login()))); QStringList statusParts; statusParts << (resp.is_active() ? tr("active") : tr("inactive")); if (resp.has_is_admin() && resp.is_admin()) { statusParts << tr("admin"); } - userInfoStatusValue->setText(statusParts.join(", ")); + userInfoStatusValue->setText(statusParts.join(QStringLiteral(", "))); - userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3") - .arg(resp.total_reports()) - .arg(resp.total_bans()) - .arg(resp.total_warns())); + cardReportsValue->setText(QString::number(resp.total_reports())); + cardWarnsValue->setText(QString::number(resp.total_warns())); + cardBansValue->setText(QString::number(resp.total_bans())); if (resp.has_admin_notes() && !resp.admin_notes().empty()) { userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes())); @@ -331,12 +385,21 @@ void TabModeration::userInfoResponse(const Response &response) } } +void TabModeration::applyAltsCount() +{ + altsCountValue->setText(tr("%1 related").arg(cachedAltsCount)); +} + void TabModeration::requestSessions(const QString &userName) { Command_GetUserSessions cmd; cmd.set_user_name(userName.toStdString()); PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::sessionsResponse); + connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) { + if (userName == currentUser) { + sessionsResponse(response); + } + }); client->sendCommand(pend); } @@ -368,7 +431,11 @@ void TabModeration::requestAlts(const QString &userName) Command_GetUserAlts cmd; cmd.set_user_name(userName.toStdString()); PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::altsResponse); + connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) { + if (userName == currentUser) { + altsResponse(response); + } + }); client->sendCommand(pend); } @@ -376,10 +443,14 @@ void TabModeration::altsResponse(const Response &response) { altsTable->setRowCount(0); if (response.response_code() != Response::RespOk) { + cachedAltsCount = -1; + altsCountValue->setText(QStringLiteral("–")); return; } const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext); + cachedAltsCount = resp.alts_size(); + applyAltsCount(); altsTable->setRowCount(resp.alts_size()); for (int i = 0; i < resp.alts_size(); ++i) { const ServerInfo_UserAlt &alt = resp.alts(i); diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.h b/cockatrice/src/interface/widgets/tabs/tab_moderation.h index 47d191dc7..ab3750080 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.h +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.h @@ -5,6 +5,7 @@ #include #include +#include #include class AbstractClient; @@ -13,6 +14,7 @@ class QLabel; class QLineEdit; class QPushButton; class QTableWidget; +class QTabWidget; class QTextEdit; class Response; class TabSupervisor; @@ -52,10 +54,15 @@ private: void requestAlts(const QString &userName); void requestModeratorLogins(); void clearUserData(); + void applyUserInfo(); + void applyAltsCount(); [[nodiscard]] QString formatEpoch(quint64 ts) const; AbstractClient *client; QString currentUser; + Response_ReportUserInfo cachedUserInfo; + bool cachedUserInfoValid = false; + int cachedAltsCount = -1; QLineEdit *searchEdit; QPushButton *investigateButton; @@ -63,26 +70,25 @@ private: QPushButton *removeAvatarButton; QGroupBox *userInfoGroup; - QLabel *userInfoNameLabel; QLabel *userInfoNameValue; - QLabel *userInfoRegisteredLabel; - QLabel *userInfoRegisteredValue; - QLabel *userInfoLastLoginLabel; - QLabel *userInfoLastLoginValue; - QLabel *userInfoStatusLabel; QLabel *userInfoStatusValue; - QLabel *userInfoCountsLabel; - QLabel *userInfoCountsValue; + QLabel *sublineLabel; + QGroupBox *cardReports; + QGroupBox *cardWarns; + QGroupBox *cardBans; + QGroupBox *altsCard; + QLabel *cardReportsValue; + QLabel *cardWarnsValue; + QLabel *cardBansValue; + QLabel *altsCountValue; QLabel *userInfoNotesLabel; QTextEdit *userInfoNotesEdit; - QGroupBox *altsGroup; + QTabWidget *sectionTabs; QTableWidget *altsTable; - QPushButton *openAltsButton; - QGroupBox *sessionsGroup; QTableWidget *sessionsTable; - QGroupBox *staffGroup; QTableWidget *staffTable; + QPushButton *openAltsButton; QPushButton *refreshStaffButton; };