[UserList] Context menu invite (#7138)

* [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 50 seconds


Took 3 minutes

* [Client] Extract sendPrivateMessage() to fix invite message draft overwrite

sendInviteMessage() was calling sayEdit->setText(text) then sendMessage(),
which overwrites any text the user had typed.  Extract the command-building
and sending logic into a new sendPrivateMessage(const QString &text) method
that takes the text directly.  sendMessage() now calls it after its guards
and clears sayEdit; sendInviteMessage() calls it directly without touching
the input field at all.

Took 33 minutes

* Rename method, address comments.

Took 5 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-17 10:49:19 +02:00 committed by GitHub
parent 776f917ffc
commit 765ebf8fb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 179 additions and 14 deletions

View file

@ -96,6 +96,18 @@ void TabMessage::closeEvent(QCloseEvent *event)
event->accept();
}
void TabMessage::sendPrivateMessage(const QString &text)
{
Command_Message cmd;
cmd.set_user_name(otherUserInfo->name());
cmd.set_message(text.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(text);
connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent);
client->sendCommand(pend);
}
void TabMessage::sendMessage()
{
if (sayEdit->text().isEmpty()) {
@ -103,24 +115,19 @@ void TabMessage::sendMessage()
}
if (!userOnline) {
// Keep the draft: the user may be back momentarily, and the typed text
// should not be lost to a transient offline spell.
notifyUserOffline();
return;
}
Command_Message cmd;
cmd.set_user_name(otherUserInfo->name());
cmd.set_message(sayEdit->text().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(sayEdit->text());
connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent);
client->sendCommand(pend);
sendPrivateMessage(sayEdit->text());
sayEdit->clear();
}
bool TabMessage::isUserOnline() const
{
return userOnline;
}
void TabMessage::messageSent(const Response &response,
const CommandContainer & /*commandContainer*/,
const QVariant &extraData)

View file

@ -64,6 +64,9 @@ public:
void processUserLeft();
void processUserJoined(const ServerInfo_User &_userInfo);
[[nodiscard]] bool isUserOnline() const;
void sendPrivateMessage(const QString &text);
private:
bool shouldShowSystemPopup(const Event_UserMessage &event);
void showSystemPopup(const Event_UserMessage &event);

View file

@ -4,6 +4,7 @@
#include "../../../client/settings/shortcuts_settings.h"
#include "../interface/widgets/dialogs/dlg_settings.h"
#include "../interface/widgets/server/chat_view/chat_view.h"
#include "../interface/widgets/server/game_link.h"
#include "../interface/widgets/server/game_selector.h"
#include "../interface/widgets/server/user/user_list_manager.h"
#include "../interface/widgets/server/user/user_list_panel_widget.h"
@ -30,6 +31,7 @@
#include <QToolButton>
#include <QVBoxLayout>
#include <QtCore/qdatetime.h>
#include <functional>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/get_pb_extension.h>
@ -66,6 +68,9 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
userList = userListPanel->getUserList();
connect(userListPanel, &UserListPanelWidget::openMessageDialog, this, &TabRoom::openMessageDialog);
const auto gameInviteLinkProvider = [this]() { return tabSupervisor->getGameInviteLinksForRoom(roomId); };
userList->setGameInviteLinkProvider(gameInviteLinkProvider);
chatView = new ChatView(tabSupervisor, nullptr, true, this);
connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup);
connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab);

View file

@ -3,6 +3,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/server/game_link.h"
#include "../interface/widgets/server/user/user_list_manager.h"
#include "../interface/widgets/server/user/user_list_widget.h"
#include "../main.h"
@ -950,6 +951,53 @@ void TabSupervisor::talkLeft(TabMessage *tab)
removeTab(indexOf(tab));
}
QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) const
{
QList<GameInviteOption> options;
if (isLocalGame) {
return options;
}
// The inviter may be in several games of the same room (hosting one and
// spectating another, for example). Return every game so the caller can
// let the user choose which one to invite to.
for (TabGame *tab : gameTabs) {
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
if (metaInfo->proto().room_id() != roomId) {
continue;
}
// A closed game is a dead end — drop it. Started/full games stay
// listed: an invite to them is a legitimate "come spectate" offer.
if (metaInfo->proto().closed()) {
continue;
}
const int gameId = metaInfo->gameId();
const QString description = QString::fromStdString(metaInfo->proto().description());
GameInviteOption option{
.gameId = gameId,
.label =
description.isEmpty() ? tr("Game #%1").arg(gameId) : tr("Game #%1 — %2").arg(gameId).arg(description),
.url = makeGameJoinLink(client->serverName(), client->serverPort(), roomId, gameId, description),
.description = description,
.onlyBuddies = metaInfo->proto().only_buddies(),
.creatorName = QString::fromStdString(metaInfo->proto().creator_info().name()),
};
options.append(option);
}
return options;
}
void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText)
{
TabMessage *tab = addMessageTab(userName, true);
if (tab && tab->isUserOnline()) {
tab->sendPrivateMessage(inviteText);
}
}
/**
* Creates a new deck editor tab and loads the deck into it.
* Creates either a classic or visual deck editor tab depending on settings

View file

@ -9,6 +9,7 @@
#define TAB_SUPERVISOR_H
#include "../../deck_loader/deck_loader.h"
#include "../interface/widgets/server/game_link.h"
#include "../interface/widgets/server/user/user_list_proxy.h"
#include "abstract_tab_deck_editor.h"
#include "api/archidekt/tab_archidekt.h"
@ -160,6 +161,8 @@ public:
{
return deckEditorTabs;
}
[[nodiscard]] QList<GameInviteOption> getGameInviteLinksForRoom(int roomId) const;
void sendInviteToUser(const QString &userName, const QString &inviteText);
[[nodiscard]] bool getAdminLocked() const;
void closeEvent(QCloseEvent *event) override;
bool switchToGameTabIfAlreadyExists(const int gameId);