[Game] Add an invite button to non-started and not full games (#7143)

* [Client] Send game invites from the user context menu via a private message

The user context menu gains an "Invite to Game" submenu listing the
inviteable games in the room (the inviter's own games, honoring the
buddy-only setting). Picking one opens a private message to the target
user with a cockatrice://joingame link naming the game, so the target
gets a clickable invite instead of a raw URL. Multi-game rooms offer a
picker; a single inviteable game sends directly. Sending a message to
an offline user no longer swallows the draft — it reports that the
user is offline and keeps the typed text.

Took 1 minute

* [Client] Add invite-to-game dialog to the game window

Took 15 seconds

* [Client] Open the invite dialog taller by default without enforcing a minimum size

* Move button to bottom

Took 3 minutes

* Address comments.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-17 21:37:15 +02:00 committed by GitHub
parent 08d6b51db9
commit 7c550ee505
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 449 additions and 76 deletions

View file

@ -20,6 +20,7 @@
#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../interface/widgets/cards/card_info_frame_widget.h"
#include "../interface/widgets/dialogs/dlg_create_game.h"
#include "../interface/widgets/dialogs/dlg_invite_to_game.h"
#include "../interface/widgets/server/game_link.h"
#include "../interface/widgets/server/user/user_list_manager.h"
#include "../interface/widgets/utility/completer_utils.h"
@ -43,10 +44,12 @@
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QRegularExpression>
#include <QStackedWidget>
#include <QStringListModel>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
@ -296,6 +299,9 @@ void TabGame::retranslateUi()
QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId());
updatePlayerListDockTitle();
if (inviteButton) {
inviteButton->setText(tr("Invite"));
}
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
if (replayDock) {
@ -337,6 +343,9 @@ void TabGame::retranslateUi()
if (aCopyGameLink) {
aCopyGameLink->setText(tr("Cop&y game link"));
}
if (aInviteToGame) {
aInviteToGame->setText(tr("Invite to Game..."));
}
if (aConcede) {
if (game->getPlayerManager()->isMainPlayerConceded()) {
aConcede->setText(tr("Un&concede"));
@ -513,6 +522,47 @@ void TabGame::actCopyGameLink()
QApplication::clipboard()->setText(link);
}
void TabGame::updateInviteButtonState()
{
// The dock button stays conservative (pre-start, not full); the menu action
// additionally covers started/full games, which are legitimate spectate
// invites, so it only needs the server-linked + not-closed conditions.
const bool canInvite = !tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed() &&
!game->getGameMetaInfo()->started() &&
game->getPlayerManager()->getPlayerCount() < game->getGameMetaInfo()->maxPlayers();
if (inviteButton) {
inviteButton->setVisible(canInvite);
}
if (aInviteToGame) {
aInviteToGame->setEnabled(!tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed());
}
}
void TabGame::actInviteToGame()
{
if (!tabSupervisor || tabSupervisor->getIsLocalGame()) {
return;
}
GameMetaInfo *metaInfo = game->getGameMetaInfo();
const QString inviteUrl = makeGameJoinLink(
tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), metaInfo->proto().room_id(),
metaInfo->gameId(), QString::fromStdString(metaInfo->proto().description()));
QStringList excludeUserNames;
excludeUserNames << tabSupervisor->getUserListManager()->getOwnUsername();
for (auto player : game->getPlayerManager()->getPlayers()) {
excludeUserNames << player->getPlayerInfo()->getName();
}
for (auto it = game->getPlayerManager()->getSpectators().cbegin();
it != game->getPlayerManager()->getSpectators().cend(); ++it) {
excludeUserNames << QString::fromStdString(it.value().name());
}
DlgInviteToGame dlg(tabSupervisor, inviteUrl, metaInfo->proto().only_buddies(), excludeUserNames, this);
dlg.exec();
}
void TabGame::actConcede()
{
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
@ -1004,6 +1054,8 @@ void TabGame::createMenuItems()
aCopyGameLink = new QAction(this);
aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty());
connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink);
aInviteToGame = new QAction(this);
connect(aInviteToGame, &QAction::triggered, this, &TabGame::actInviteToGame);
aConcede = new QAction(this);
connect(aConcede, &QAction::triggered, this, &TabGame::actConcede);
if (!game->getGameMetaInfo()->started()) {
@ -1043,6 +1095,7 @@ void TabGame::createMenuItems()
gameMenu->addSeparator();
gameMenu->addAction(aGameInfo);
gameMenu->addAction(aCopyGameLink);
gameMenu->addAction(aInviteToGame);
gameMenu->addAction(aConcede);
gameMenu->addAction(aFocusChat);
gameMenu->addAction(aLeaveGame);
@ -1051,6 +1104,9 @@ void TabGame::createMenuItems()
aCardMenu = gameMenu->addMenu(new QMenu(this));
// Sync the new action with the same state the dock button already shows.
updateInviteButtonState();
addTabMenu(gameMenu);
}
@ -1066,6 +1122,7 @@ void TabGame::createReplayMenuItems()
aResetLayout = nullptr;
aGameInfo = nullptr;
aCopyGameLink = nullptr;
aInviteToGame = nullptr;
aConcede = nullptr;
aFocusChat = nullptr;
aLeaveGame = new QAction(this);
@ -1264,11 +1321,29 @@ void TabGame::createPlayerListDock(bool bReplay)
}
playerListWidget->setFocusPolicy(Qt::NoFocus);
auto *playerListBox = new QWidget(this);
auto *vbox = new QVBoxLayout(playerListBox);
vbox->setContentsMargins(0, 0, 0, 0);
vbox->setSpacing(0);
vbox->addWidget(playerListWidget);
if (!bReplay) {
inviteButton = new QPushButton(tr("Invite"), playerListBox);
inviteButton->setVisible(false);
connect(inviteButton, &QPushButton::clicked, this, &TabGame::actInviteToGame);
vbox->addWidget(inviteButton);
connect(game->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, &TabGame::updateInviteButtonState);
connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, this, &TabGame::updateInviteButtonState);
updateInviteButtonState();
}
playerListDock = new QDockWidget(this);
playerListDock->setObjectName("playerListDock");
playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
QDockWidget::DockWidgetMovable);
playerListDock->setWidget(playerListWidget);
playerListDock->setWidget(playerListBox);
playerListDock->setFloating(false);
}