From 66ae189a57c27f04be2db29b989f47a81e6c6dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 13 Oct 2026 20:23:33 +0200 Subject: [PATCH] [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. --- .../widgets/server/user/user_context_menu.cpp | 11 ++++++ .../widgets/server/user/user_context_menu.h | 2 ++ .../src/interface/widgets/tabs/tab_alts.cpp | 35 ++++++++++++++++++- .../src/interface/widgets/tabs/tab_alts.h | 4 +++ .../interface/widgets/tabs/tab_moderation.cpp | 25 +++++++++++++ .../interface/widgets/tabs/tab_moderation.h | 2 ++ 6 files changed, 78 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index 447e63673..f61293013 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -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; diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h index 2536819a4..86be5eff2 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -54,6 +54,7 @@ private: QAction *aGetAdminNotes; std::function()> 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); diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp index b3b31b8da..c8fa57343 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp @@ -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(); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.h b/cockatrice/src/interface/widgets/tabs/tab_alts.h index 3b83cc4a9..de5a11ab8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.h +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.h @@ -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 backStack; QLineEdit *searchEdit; QSpinBox *depthSpin; QPushButton *investigateButton; + QPushButton *backButton; QLabel *statusLabel; QTableWidget *altsTable; }; diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp index c28217f25..73b87acaf 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp @@ -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 #include @@ -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); diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.h b/cockatrice/src/interface/widgets/tabs/tab_moderation.h index ce23b5a7a..3bd306d18 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.h +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.h @@ -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;