[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

@ -0,0 +1,112 @@
#include "dlg_invite_to_game.h"
#include "../server/user/user_list_manager.h"
#include "../server/user/user_list_widget.h"
#include "../tabs/tab_supervisor.h"
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QScreen>
#include <QUrl>
#include <QUrlQuery>
#include <QVBoxLayout>
DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor,
const QString &_inviteUrl,
bool _onlyBuddies,
const QStringList &_excludeUserNames,
QWidget *parent)
: QDialog(parent), tabSupervisor(_tabSupervisor), inviteUrl(_inviteUrl), onlyBuddies(_onlyBuddies),
excludeUserNames(_excludeUserNames)
{
setModal(true);
searchEdit = new QLineEdit(this);
searchEdit->setClearButtonEnabled(true);
connect(searchEdit, &QLineEdit::textChanged, this, &DlgInviteToGame::searchTextChanged);
// The embedded list is the real room user list without the hover popup:
// same manager, same delegate/painter, same sections, live via manager
// signals while the modal loop runs.
UserListManager *manager = tabSupervisor->getUserListManager();
userList = new UserListWidget(tabSupervisor, tabSupervisor->getClient(), UserListWidget::RoomList, this,
/*hasUserInfoPopup=*/false);
userList->setUserFilter([this, manager](const QString &name, bool online) {
return !excludeUserNames.contains(name) && online && !manager->isUserIgnored(name);
});
if (onlyBuddies) {
userList->setSectioned({UserListWidget::Section::Buddy});
} else {
userList->setSectioned({UserListWidget::Section::Buddy, UserListWidget::Section::Online});
}
userList->bind(manager);
userList->rebuild();
connect(userList, &UserListWidget::userActivated, this, &DlgInviteToGame::inviteCurrentUser);
connect(userList, &UserListWidget::currentUserChanged, this, [this](const QString &userName) {
currentUserName = userName;
inviteButton->setEnabled(!userName.isEmpty());
});
inviteButton = new QPushButton(this);
inviteButton->setEnabled(false);
inviteButton->setDefault(true);
connect(inviteButton, &QPushButton::clicked, this, [this] { inviteCurrentUser(currentUserName); });
cancelButton = new QPushButton(this);
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
auto *buttonRow = new QHBoxLayout;
buttonRow->addStretch();
buttonRow->addWidget(inviteButton);
buttonRow->addWidget(cancelButton);
auto *layout = new QVBoxLayout(this);
layout->addWidget(searchEdit);
layout->addWidget(userList, 1);
layout->addLayout(buttonRow);
retranslateUi();
// Default to a comfortably tall dialog so the list has room to breathe,
// capped by the available screen. No minimum is enforced: small screens
// and manual resizing can go shorter than this.
const QRect availableScreen = QGuiApplication::primaryScreen()->availableGeometry();
resize(sizeHint().width(), qMin(sizeHint().height() * 3, availableScreen.height() * 4 / 5));
}
void DlgInviteToGame::searchTextChanged(const QString &text)
{
userList->setFilterText(text);
}
void DlgInviteToGame::inviteCurrentUser(const QString &userName)
{
if (userName.isEmpty()) {
return;
}
// The invite link carries the game's id and, when the game has one, its
// description (makeGameJoinLink embeds both). Read them back so the prefix
// names the game by description first, then its id — identical to the
// context-menu invite so recipients see one consistent message style.
const QUrl inviteUrlObj(inviteUrl);
const QUrlQuery inviteQuery(inviteUrlObj);
const int gameId = inviteQuery.queryItemValue("gameid").toInt();
const QString gameDescription = inviteQuery.queryItemValue("game");
const QString prefix = gameDescription.isEmpty()
? tr("Join my game (#%1):").arg(gameId)
: tr("Join my game \"%1\" (#%2):").arg(gameDescription).arg(gameId);
tabSupervisor->sendInviteToUser(userName, prefix + " " + inviteUrl);
accept();
}
void DlgInviteToGame::retranslateUi()
{
setWindowTitle(tr("Invite to Game"));
searchEdit->setPlaceholderText(tr("Search users..."));
inviteButton->setText(tr("Invite"));
cancelButton->setText(tr("Cancel"));
}