[Client] Alt investigation entry points and in-tab navigation

TabModeration gains an Open alt graph button plus alts-table double-click, both routing to the User Alts tab. UserContextMenu gains an Investigate alts action. TabAlts gains row double-click navigation with a Back history stack so moderators can walk the alt graph without retyping names.
This commit is contained in:
Lukas Brübach 2026-10-13 20:23:33 +02:00
parent ff6c0402a8
commit 66ae189a57
6 changed files with 78 additions and 1 deletions

View file

@ -83,6 +83,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDemoteFromDeveloper = new QAction(QString(), this); aDemoteFromDeveloper = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this); aInvestigateUser = new QAction(QString(), this);
aInvestigateAlts = new QAction(QString(), this);
retranslateUi(); retranslateUi();
} }
@ -111,6 +112,7 @@ void UserContextMenu::retranslateUi()
aDemoteFromDeveloper->setText(tr("Demote user from de&veloper")); aDemoteFromDeveloper->setText(tr("Demote user from de&veloper"));
aGetAdminNotes->setText(tr("View admin notes")); aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user")); aInvestigateUser->setText(tr("Investigate user"));
aInvestigateAlts->setText(tr("Investigate &alts"));
} }
void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer)
@ -454,6 +456,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
menu->addSeparator(); menu->addSeparator();
menu->addAction(aGetAdminNotes); menu->addAction(aGetAdminNotes);
menu->addAction(aInvestigateUser); menu->addAction(aInvestigateUser);
menu->addAction(aInvestigateAlts);
menu->addSeparator(); menu->addSeparator();
if (userLevel.testFlag(ServerInfo_User::IsModerator) && if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
@ -499,6 +502,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aBanHistory->setEnabled(anotherUser); aBanHistory->setEnabled(anotherUser);
aGetAdminNotes->setEnabled(anotherUser); aGetAdminNotes->setEnabled(anotherUser);
aInvestigateUser->setEnabled(anotherUser); aInvestigateUser->setEnabled(anotherUser);
aInvestigateAlts->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser);
aPromoteToJudge->setEnabled(anotherUser); aPromoteToJudge->setEnabled(anotherUser);
@ -553,6 +557,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execAdminNotes(userName); execAdminNotes(userName);
} else if (actionClicked == aInvestigateUser) { } else if (actionClicked == aInvestigateUser) {
execInvestigateUser(userName); execInvestigateUser(userName);
} else if (actionClicked == aInvestigateAlts) {
execInvestigateAlts(userName);
} else if (actionClicked == aCopyToClipBoard) { } else if (actionClicked == aCopyToClipBoard) {
QClipboard *clipboard = QGuiApplication::clipboard(); QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setText(deckHash); clipboard->setText(deckHash);
@ -745,6 +751,11 @@ void UserContextMenu::execInvestigateUser(const QString &userName)
tabSupervisor->openTabModeration(userName); tabSupervisor->openTabModeration(userName);
} }
void UserContextMenu::execInvestigateAlts(const QString &userName)
{
tabSupervisor->openTabAlts(userName);
}
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod) void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod)
{ {
Command_AdjustMod cmd; Command_AdjustMod cmd;

View file

@ -54,6 +54,7 @@ private:
QAction *aGetAdminNotes; QAction *aGetAdminNotes;
std::function<QList<GameInviteOption>()> gameInviteLinkProvider; std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
QAction *aInvestigateUser; QAction *aInvestigateUser;
QAction *aInvestigateAlts;
signals: signals:
void openMessageDialog(const QString &userName, bool focus); void openMessageDialog(const QString &userName, bool focus);
private slots: private slots:
@ -127,6 +128,7 @@ public:
void execWarnHistory(const QString &userName); void execWarnHistory(const QString &userName);
void execAdminNotes(const QString &userName); void execAdminNotes(const QString &userName);
void execInvestigateUser(const QString &userName); void execInvestigateUser(const QString &userName);
void execInvestigateAlts(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustMod(const QString &userName, bool shouldBeMod);
void execAdjustJudge(const QString &userName, bool shouldBeJudge); void execAdjustJudge(const QString &userName, bool shouldBeJudge);
void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper); void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);

View file

@ -58,9 +58,14 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
investigateButton = new QPushButton; investigateButton = new QPushButton;
connect(investigateButton, &QPushButton::clicked, this, &TabAlts::investigateUser); connect(investigateButton, &QPushButton::clicked, this, &TabAlts::investigateUser);
backButton = new QPushButton;
backButton->setEnabled(false);
connect(backButton, &QPushButton::clicked, this, &TabAlts::goBack);
statusLabel = new QLabel; statusLabel = new QLabel;
auto *topBar = new QHBoxLayout; auto *topBar = new QHBoxLayout;
topBar->addWidget(backButton);
topBar->addWidget(searchEdit); topBar->addWidget(searchEdit);
topBar->addWidget(depthSpin); topBar->addWidget(depthSpin);
topBar->addWidget(investigateButton); topBar->addWidget(investigateButton);
@ -73,6 +78,12 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
altsTable->setAlternatingRowColors(true); altsTable->setAlternatingRowColors(true);
altsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); altsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
altsTable->horizontalHeader()->setStretchLastSection(true); altsTable->horizontalHeader()->setStretchLastSection(true);
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
auto *userItem = altsTable->item(row, COL_ALTS_USER);
if (userItem != nullptr && !userItem->text().isEmpty() && userItem->text() != currentUser) {
investigate(userItem->text());
}
});
auto *mainLayout = new QVBoxLayout(centralWidget); auto *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->addLayout(topBar); mainLayout->addLayout(topBar);
@ -89,6 +100,8 @@ void TabAlts::retranslateUi()
depthSpin->setPrefix(tr("Depth: ")); depthSpin->setPrefix(tr("Depth: "));
depthSpin->setToolTip(tr("Search depth: 1 finds direct matches, higher values follow alts of alts.")); depthSpin->setToolTip(tr("Search depth: 1 finds direct matches, higher values follow alts of alts."));
investigateButton->setText(tr("Investigate")); investigateButton->setText(tr("Investigate"));
backButton->setText(tr("Back"));
backButton->setToolTip(tr("Back to the previously investigated user"));
if (currentUser.isEmpty()) { if (currentUser.isEmpty()) {
statusLabel->setText(tr("Enter a user name to investigate.")); statusLabel->setText(tr("Enter a user name to investigate."));
} }
@ -105,9 +118,29 @@ QString TabAlts::formatEpoch(quint64 ts) const
void TabAlts::investigate(const QString &userName) void TabAlts::investigate(const QString &userName)
{ {
if (userName.isEmpty()) { if (userName.isEmpty() || userName == currentUser) {
return; return;
} }
if (!currentUser.isEmpty()) {
backStack << currentUser;
backButton->setEnabled(true);
}
loadUser(userName);
}
void TabAlts::goBack()
{
if (backStack.isEmpty()) {
return;
}
const QString previousUser = backStack.takeLast();
backButton->setEnabled(!backStack.isEmpty());
loadUser(previousUser);
}
void TabAlts::loadUser(const QString &userName)
{
currentUser = userName;
searchEdit->setText(userName); searchEdit->setText(userName);
investigateUser(); investigateUser();
} }

View file

@ -35,18 +35,22 @@ public:
private slots: private slots:
void investigateUser(); void investigateUser();
void goBack();
void altsResponse(const Response &response); void altsResponse(const Response &response);
private: private:
void loadUser(const QString &userName);
void requestAlts(const QString &userName, int depth); void requestAlts(const QString &userName, int depth);
[[nodiscard]] QString formatEpoch(quint64 ts) const; [[nodiscard]] QString formatEpoch(quint64 ts) const;
AbstractClient *client; AbstractClient *client;
QString currentUser; QString currentUser;
QList<QString> backStack;
QLineEdit *searchEdit; QLineEdit *searchEdit;
QSpinBox *depthSpin; QSpinBox *depthSpin;
QPushButton *investigateButton; QPushButton *investigateButton;
QPushButton *backButton;
QLabel *statusLabel; QLabel *statusLabel;
QTableWidget *altsTable; QTableWidget *altsTable;
}; };

View file

@ -7,6 +7,7 @@
#include "libcockatrice/protocol/pb/serverinfo_moderator_login.pb.h" #include "libcockatrice/protocol/pb/serverinfo_moderator_login.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_user_alt.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user_alt.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_user_session.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user_session.pb.h"
#include "tab_supervisor.h"
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QDateTime> #include <QDateTime>
@ -146,7 +147,19 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
altsGroup = new QGroupBox; altsGroup = new QGroupBox;
altsTable = new QTableWidget(0, COL_ALTS_COUNT); altsTable = new QTableWidget(0, COL_ALTS_COUNT);
configureTable(altsTable); configureTable(altsTable);
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
auto *userItem = altsTable->item(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); auto *altsLayout = new QVBoxLayout(altsGroup);
altsLayout->addLayout(altsHeader);
altsLayout->addWidget(altsTable); altsLayout->addWidget(altsTable);
sessionsGroup = new QGroupBox; sessionsGroup = new QGroupBox;
@ -205,6 +218,8 @@ void TabModeration::retranslateUi()
userInfoNotesLabel->setText(tr("Admin notes:")); userInfoNotesLabel->setText(tr("Admin notes:"));
altsGroup->setTitle(tr("Alts")); altsGroup->setTitle(tr("Alts"));
openAltsButton->setText(tr("Open alt graph"));
openAltsButton->setToolTip(tr("Open this user in the User Alts tab"));
sessionsGroup->setTitle(tr("Sessions")); sessionsGroup->setTitle(tr("Sessions"));
staffGroup->setTitle(tr("Staff Last Logins")); staffGroup->setTitle(tr("Staff Last Logins"));
@ -233,6 +248,7 @@ void TabModeration::clearUserData()
resetPasswordButton->setEnabled(false); resetPasswordButton->setEnabled(false);
removeAvatarButton->setEnabled(false); removeAvatarButton->setEnabled(false);
removeBannerButton->setEnabled(false); removeBannerButton->setEnabled(false);
openAltsButton->setEnabled(false);
} }
void TabModeration::investigate(const QString &userName) void TabModeration::investigate(const QString &userName)
@ -254,6 +270,7 @@ void TabModeration::investigateUser()
resetPasswordButton->setEnabled(true); resetPasswordButton->setEnabled(true);
removeAvatarButton->setEnabled(true); removeAvatarButton->setEnabled(true);
removeBannerButton->setEnabled(true); removeBannerButton->setEnabled(true);
openAltsButton->setEnabled(true);
altsTable->setRowCount(0); altsTable->setRowCount(0);
sessionsTable->setRowCount(0); sessionsTable->setRowCount(0);
requestUserInfo(userName); requestUserInfo(userName);
@ -261,6 +278,14 @@ void TabModeration::investigateUser()
requestAlts(userName); requestAlts(userName);
} }
void TabModeration::openAltsGraph()
{
if (currentUser.isEmpty()) {
return;
}
getTabSupervisor()->openTabAlts(currentUser);
}
void TabModeration::requestUserInfo(const QString &userName) void TabModeration::requestUserInfo(const QString &userName)
{ {
userInfoNameValue->setText(userName); userInfoNameValue->setText(userName);

View file

@ -36,6 +36,7 @@ public:
private slots: private slots:
void investigateUser(); void investigateUser();
void openAltsGraph();
void userInfoResponse(const Response &response); void userInfoResponse(const Response &response);
void sessionsResponse(const Response &response); void sessionsResponse(const Response &response);
void altsResponse(const Response &response); void altsResponse(const Response &response);
@ -80,6 +81,7 @@ private:
QGroupBox *altsGroup; QGroupBox *altsGroup;
QTableWidget *altsTable; QTableWidget *altsTable;
QPushButton *openAltsButton;
QGroupBox *sessionsGroup; QGroupBox *sessionsGroup;
QTableWidget *sessionsTable; QTableWidget *sessionsTable;
QGroupBox *staffGroup; QGroupBox *staffGroup;