mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-11 18:46:08 -05:00
[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:
@@ -355,6 +355,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||
{
|
||||
QAction *aCopyToClipBoard = nullptr, *aRemoveMessages = nullptr;
|
||||
aUserName->setText(userName);
|
||||
const bool anotherUser = userName != userListProxy->getOwnUsername();
|
||||
|
||||
auto *menu = new QMenu(static_cast<QWidget *>(parent()));
|
||||
menu->addAction(aUserName);
|
||||
@@ -366,6 +367,17 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||
menu->addAction(aDetails);
|
||||
menu->addAction(aShowGames);
|
||||
menu->addAction(aChat);
|
||||
const QList<GameInviteOption> inviteOptions = inviteOptionsForUser(userName);
|
||||
if (!inviteOptions.isEmpty()) {
|
||||
auto *inviteMenu = new QMenu(tr("&Invite to Game"), menu);
|
||||
for (const GameInviteOption &option : inviteOptions) {
|
||||
QAction *inviteAction = inviteMenu->addAction(option.label);
|
||||
inviteAction->setEnabled(anotherUser && online);
|
||||
connect(inviteAction, &QAction::triggered, this,
|
||||
[this, userName, option] { execInvite(userName, option); });
|
||||
}
|
||||
menu->addMenu(inviteMenu);
|
||||
}
|
||||
if (userLevel.testFlag(ServerInfo_User::IsRegistered) && userListProxy->isOwnUserRegistered()) {
|
||||
menu->addSeparator();
|
||||
if (userListProxy->isUserBuddy(userName)) {
|
||||
@@ -416,7 +428,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||
menu->addAction(aPromoteToJudge);
|
||||
}
|
||||
}
|
||||
bool anotherUser = userName != userListProxy->getOwnUsername();
|
||||
aDetails->setEnabled(true);
|
||||
aChat->setEnabled(anotherUser && online);
|
||||
aShowGames->setEnabled(online);
|
||||
@@ -480,6 +491,60 @@ void UserContextMenu::execChat(const QString &userName)
|
||||
emit openMessageDialog(userName, true);
|
||||
}
|
||||
|
||||
QList<GameInviteOption> UserContextMenu::inviteOptionsForUser(const QString &userName) const
|
||||
{
|
||||
if (!gameInviteLinkProvider) {
|
||||
return {};
|
||||
}
|
||||
const QList<GameInviteOption> options = gameInviteLinkProvider();
|
||||
QList<GameInviteOption> result;
|
||||
for (const GameInviteOption &option : options) {
|
||||
// Buddy-only games accept invites only from their creator, and only to
|
||||
// users on the creator's buddy list.
|
||||
if (option.onlyBuddies &&
|
||||
(option.creatorName != userListProxy->getOwnUsername() || !userListProxy->isUserBuddy(userName))) {
|
||||
continue;
|
||||
}
|
||||
result.append(option);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvite(const QString &userName)
|
||||
{
|
||||
const QList<GameInviteOption> options = inviteOptionsForUser(userName);
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.size() == 1) {
|
||||
execInvite(userName, options.first());
|
||||
return;
|
||||
}
|
||||
|
||||
// More than one game in the room — let the user pick which one to invite to.
|
||||
auto *menu = new QMenu(static_cast<QWidget *>(parent()));
|
||||
for (const GameInviteOption &option : options) {
|
||||
QAction *action = menu->addAction(option.label);
|
||||
connect(action, &QAction::triggered, this, [this, userName, option] { execInvite(userName, option); });
|
||||
}
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvite(const QString &userName, const GameInviteOption &option)
|
||||
{
|
||||
// Name the game by description first, then its id — "Join my game 'Magic'
|
||||
// (#123)" — so a description-less fallback still identifies the game.
|
||||
// The multi-arg .arg() overloads replace in a single pass, so a description
|
||||
// containing "%…" cannot corrupt later placeholders.
|
||||
const QString prefix =
|
||||
option.description.isEmpty()
|
||||
? tr("Join my game (#%1):").arg(option.gameId)
|
||||
: tr("Join my game \"%1\" (#%2):").arg(option.description, QString::number(option.gameId));
|
||||
tabSupervisor->sendInviteToUser(userName, prefix + " " + option.url);
|
||||
}
|
||||
|
||||
void UserContextMenu::execDetails(const QString &userName)
|
||||
{
|
||||
auto *w = new UserInfoBox(client, false, static_cast<QWidget *>(parent()),
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
#ifndef USER_CONTEXT_MENU_H
|
||||
#define USER_CONTEXT_MENU_H
|
||||
|
||||
#include <QObject>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include "../../interface/widgets/server/game_link.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
class AbstractGame;
|
||||
class UserListProxy;
|
||||
class AbstractClient;
|
||||
@@ -43,6 +46,7 @@ private:
|
||||
QAction *aPromoteToJudge, *aDemoteFromJudge;
|
||||
QAction *aWarnUser, *aWarnHistory;
|
||||
QAction *aGetAdminNotes;
|
||||
std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
private slots:
|
||||
@@ -80,9 +84,28 @@ public:
|
||||
return userListProxy;
|
||||
}
|
||||
|
||||
void setGameInviteLinkProvider(std::function<QList<GameInviteOption>()> provider)
|
||||
{
|
||||
gameInviteLinkProvider = std::move(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* The games currently inviteable for @p userName, honoring the room's
|
||||
* buddy-only setting (the inviter must be the game's creator and the
|
||||
* target a buddy of theirs). Empty when there is no live provider.
|
||||
*/
|
||||
QList<GameInviteOption> inviteOptionsForUser(const QString &userName) const;
|
||||
|
||||
/** Whether at least one invite link is currently available for @p userName. */
|
||||
bool hasGameInviteLink(const QString &userName) const
|
||||
{
|
||||
return !inviteOptionsForUser(userName).isEmpty();
|
||||
}
|
||||
|
||||
// Individual action entry points — used by UserInfoPopup to trigger
|
||||
// actions without re-running the full context menu flow.
|
||||
void execChat(const QString &userName);
|
||||
void execInvite(const QString &userName);
|
||||
void execDetails(const QString &userName);
|
||||
void execShowGames(const QString &userName);
|
||||
void execAddToBuddy(const QString &userName);
|
||||
@@ -97,6 +120,9 @@ public:
|
||||
void execAdminNotes(const QString &userName);
|
||||
void execAdjustMod(const QString &userName, bool shouldBeMod);
|
||||
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
|
||||
|
||||
private:
|
||||
void execInvite(const QString &userName, const GameInviteOption &option);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1838,3 +1838,8 @@ void UserListWidget::finishSectionedMutation()
|
||||
applyFilter();
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
|
||||
void UserListWidget::setGameInviteLinkProvider(std::function<QList<GameInviteOption>()> provider)
|
||||
{
|
||||
userContextMenu->setGameInviteLinkProvider(std::move(provider));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define USERLIST_H
|
||||
|
||||
#include "../../cards/card_info_picture_art_crop_widget.h"
|
||||
#include "../../interface/widgets/server/game_link.h"
|
||||
#include "user_avatar_provider.h"
|
||||
#include "user_card_art_provider.h"
|
||||
#include "user_info_popup.h"
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
|
||||
@@ -271,6 +273,7 @@ public:
|
||||
}
|
||||
void showContextMenu(const QPoint &pos, const QModelIndex &index);
|
||||
void sortItems();
|
||||
void setGameInviteLinkProvider(std::function<QList<GameInviteOption>()> provider);
|
||||
|
||||
protected:
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user