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

View file

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

View file

@ -58,9 +58,14 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
investigateButton = new QPushButton;
connect(investigateButton, &QPushButton::clicked, this, &TabAlts::investigateUser);
backButton = new QPushButton;
backButton->setEnabled(false);
connect(backButton, &QPushButton::clicked, this, &TabAlts::goBack);
statusLabel = new QLabel;
auto *topBar = new QHBoxLayout;
topBar->addWidget(backButton);
topBar->addWidget(searchEdit);
topBar->addWidget(depthSpin);
topBar->addWidget(investigateButton);
@ -73,6 +78,12 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
altsTable->setAlternatingRowColors(true);
altsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
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);
mainLayout->addLayout(topBar);
@ -89,6 +100,8 @@ void TabAlts::retranslateUi()
depthSpin->setPrefix(tr("Depth: "));
depthSpin->setToolTip(tr("Search depth: 1 finds direct matches, higher values follow alts of alts."));
investigateButton->setText(tr("Investigate"));
backButton->setText(tr("Back"));
backButton->setToolTip(tr("Back to the previously investigated user"));
if (currentUser.isEmpty()) {
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)
{
if (userName.isEmpty()) {
if (userName.isEmpty() || userName == currentUser) {
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);
investigateUser();
}

View file

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

View file

@ -7,6 +7,7 @@
#include "libcockatrice/protocol/pb/serverinfo_moderator_login.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_user_alt.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_user_session.pb.h"
#include "tab_supervisor.h"
#include <QAbstractItemView>
#include <QDateTime>
@ -146,7 +147,19 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
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);
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);
altsLayout->addWidget(altsTable);
sessionsGroup = new QGroupBox;
@ -205,6 +218,8 @@ void TabModeration::retranslateUi()
userInfoNotesLabel->setText(tr("Admin notes:"));
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"));
staffGroup->setTitle(tr("Staff Last Logins"));
@ -233,6 +248,7 @@ void TabModeration::clearUserData()
resetPasswordButton->setEnabled(false);
removeAvatarButton->setEnabled(false);
removeBannerButton->setEnabled(false);
openAltsButton->setEnabled(false);
}
void TabModeration::investigate(const QString &userName)
@ -254,6 +270,7 @@ void TabModeration::investigateUser()
resetPasswordButton->setEnabled(true);
removeAvatarButton->setEnabled(true);
removeBannerButton->setEnabled(true);
openAltsButton->setEnabled(true);
altsTable->setRowCount(0);
sessionsTable->setRowCount(0);
requestUserInfo(userName);
@ -261,6 +278,14 @@ void TabModeration::investigateUser()
requestAlts(userName);
}
void TabModeration::openAltsGraph()
{
if (currentUser.isEmpty()) {
return;
}
getTabSupervisor()->openTabAlts(currentUser);
}
void TabModeration::requestUserInfo(const QString &userName)
{
userInfoNameValue->setText(userName);

View file

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