From 4ce2e8c3d31e3dbd5241e029befa0c743e2c82ba Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:57:07 +0200 Subject: [PATCH] [UserList] Fix join/spectate from the profile hover card doing nothing (#7383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The games list in the profile hover card offered "Join game" and "Spectate", but UserInfoPopup::joinGameRequested was only forwarded to an identical UserListWidget::joinGameRequested signal that nothing connected to. Clicking either entry sent no command, opened no tab and showed no error. Route the request to TabSupervisor, which owns the join flow. The popup's games list spans every room the user is in, so the target room may not be open yet; IntentJoinServerGame already joins the room first and retries, which is why this goes through the intent rather than straight to GameSelector. Extract the intent construction that joinReportGame already had into a shared joinGameByIntent helper instead of duplicating it. Also stop disabling "Join game" for games that are full or already running. GameSelector::joinGame() offers to join as a spectator in that case, so a greyed-out entry read as a dead button; GameSelector's own context menu never disables it either. Co-authored-by: Lukas BrĂ¼bach --- .../widgets/server/user/user_info_popup.cpp | 5 +++-- .../widgets/server/user/user_list_widget.cpp | 14 ++++++++++--- .../widgets/server/user/user_list_widget.h | 1 - .../interface/widgets/tabs/tab_supervisor.cpp | 21 +++++++++++++++++-- .../interface/widgets/tabs/tab_supervisor.h | 4 ++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index 8be76eea0..967de2f2c 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -634,9 +634,10 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos) "QMenu::item:selected{background:%4;}") .arg(colorStr(theme.bg), colorStr(theme.text), colorStr(theme.border), colorStr(theme.buttonHover))); - const bool canJoin = !game.started() && game.player_count() < game.max_players(); + // Never grey the join action out: GameSelector::joinGame() asks whether to + // fall back to spectating when the game is full or already running, so a + // disabled entry would just read as a dead button. QAction *join = menu.addAction(tr("Join game")); - join->setEnabled(canJoin); QAction *spec = nullptr; if (game.spectators_allowed()) { diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index d9e1e1e7e..3b8fc08b7 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -430,9 +430,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, hidePopup(true); requestVisibleItemResources(); }); - - // Forward join requests from popup upward - connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); } else { // Dialog mode: keyboard selection drives the Invite button. connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) { @@ -692,6 +689,17 @@ void UserListWidget::connectPopupSignals() [this](const QString &n) { userContextMenu->execAdjustJudge(n, true); }); connect(userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this, [this](const QString &n) { userContextMenu->execAdjustJudge(n, false); }); + + // Joining is a tab-level action, not a per-user one: the popup's games list + // spans every room the user is in, so the target room may not be open yet. + // Hand it to the supervisor, which owns the intent that joins the room if + // needed. The popup closes here because that intent switches tabs, which + // would otherwise strand it on a list that is no longer on screen. + connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, + [this](const int gameId, const int roomId, const bool asSpectator) { + hidePopup(true); + tabSupervisor->joinGameFromUserCard(gameId, roomId, asSpectator); + }); } bool UserListWidget::eventFilter(QObject *obj, QEvent *event) diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 412271160..9802387b2 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -172,7 +172,6 @@ signals: void removeBuddy(const QString &userName); void addIgnore(const QString &userName); void removeIgnore(const QString &userName); - void joinGameRequested(int gameId, int roomId, bool asSpectator); void sectionExpanded(Section section, bool expanded); /** Dialog mode: the user activated (Enter/double-click) the given row. */ void userActivated(const QString &userName); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index c9bda7703..0dc246eaa 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -1100,10 +1100,27 @@ void TabSupervisor::replayLeft(TabGame *tab) } void TabSupervisor::joinReportGame(const int gameId, const int roomId) +{ + joinGameByIntent(gameId, roomId, true); +} + +void TabSupervisor::joinGameFromUserCard(const int gameId, const int roomId, const bool asSpectator) +{ + joinGameByIntent(gameId, roomId, asSpectator); +} + +/** + * Runs the join/spectate intent for @p gameId in @p roomId. + * + * The intent owns the whole flow: if the room tab is not open yet it joins the + * room first, then retries the game, so callers do not have to care whether the + * room happens to be on screen. + */ +void TabSupervisor::joinGameByIntent(const int gameId, const int roomId, const bool asSpectator) { auto *remoteClient = qobject_cast(client); if (!remoteClient) { - actShowPopup(tr("Report joins are only available on a remote server.")); + actShowPopup(tr("Game joins are only available on a remote server.")); return; } @@ -1112,7 +1129,7 @@ void TabSupervisor::joinReportGame(const int gameId, const int roomId) ctx->roomContext.serverContext.port = QString::number(remoteClient->peerPort()); ctx->roomContext.roomId = roomId; ctx->gameId = gameId; - ctx->asSpectator = true; + ctx->asSpectator = asSpectator; auto *joinGameIntent = new IntentJoinServerGame(this, remoteClient, std::move(ctx)); joinGameIntent->setParent(this); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 066c84a77..1a4f7f749 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -128,6 +128,8 @@ private: static QString sanitizeTabName(QString dirty); static QString sanitizeHtml(QString dirty); void resetTabsMenu(); + /** Shared join/spectate driver behind the report tab and the profile hover card. */ + void joinGameByIntent(int gameId, int roomId, bool asSpectator); public: explicit TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent = nullptr); @@ -198,6 +200,8 @@ public slots: TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); void openReplay(GameReplay *replay); void joinReportGame(int gameId, int roomId); + /** Joins or spectates a game listed on a user's profile hover card. */ + void joinGameFromUserCard(int gameId, int roomId, bool asSpectator); void openTabModeration(const QString &userName = {}); void switchToFirstAvailableNetworkTab(); void maximizeMainWindow();