[Report] Add context menu to report queue rows

- Open the standard UserContextMenu on right-click/Menu key for the
  reported user, selecting the row first so the details panel matches
  the menu target
- Carry the reported user's level in ServerInfo_Report so the menu
  shows the same actions as the user list, with an IsUser fallback for
  deleted or unregistered users
- Wire the Private chat action through TabReport to TabSupervisor and
  retranslate the menu on live language switches
This commit is contained in:
Lukas Brübach 2026-10-07 23:39:04 +02:00
parent a804c1a6fe
commit a8fe6419a1
5 changed files with 79 additions and 4 deletions

View file

@ -1,5 +1,6 @@
#include "tab_report.h" #include "tab_report.h"
#include "../server/user/user_context_menu.h"
#include "../utility/report_utils.h" #include "../utility/report_utils.h"
#include "abstract_client.h" #include "abstract_client.h"
#include "tab_supervisor.h" #include "tab_supervisor.h"
@ -25,6 +26,7 @@
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h> #include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h> #include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
#include <libcockatrice/protocol/pb/command_report_assign.pb.h> #include <libcockatrice/protocol/pb/command_report_assign.pb.h>
@ -111,6 +113,10 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents); table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged); connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(table, &QTableWidget::customContextMenuRequested, this, &TabReport::showTableContextMenu);
userContextMenu = new UserContextMenu(tabSupervisor, this);
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &TabReport::openMessageDialog);
descGroup = new QGroupBox; descGroup = new QGroupBox;
descriptionEdit = new QTextEdit; descriptionEdit = new QTextEdit;
@ -266,6 +272,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
void TabReport::retranslateUi() void TabReport::retranslateUi()
{ {
userContextMenu->retranslateUi();
searchEdit->setPlaceholderText(tr("Search by username, category...")); searchEdit->setPlaceholderText(tr("Search by username, category..."));
statusFilter->clear(); statusFilter->clear();
statusFilter->addItem(tr("All Statuses"), ""); statusFilter->addItem(tr("All Statuses"), "");
@ -496,6 +503,36 @@ void TabReport::onSelectionChanged()
updateActionStates(); updateActionStates();
} }
void TabReport::showTableContextMenu(const QPoint &pos)
{
QTableWidgetItem *clickedItem = table->itemAt(pos);
if (!clickedItem || clickedItem->row() < 0) {
return;
}
const int row = clickedItem->row();
const QTableWidgetItem *reportedItem = table->item(row, COL_REPORTED);
const QTableWidgetItem *idItem = table->item(row, COL_ID);
if (!reportedItem || !idItem || reportedItem->text().isEmpty()) {
return;
}
int userLevel = ServerInfo_User::IsUser;
const int reportId = idItem->data(Qt::UserRole).toInt();
for (const ServerInfo_Report &r : filteredReports) {
if (r.report_id() == reportId) {
if (r.reported_user_level() != 0) {
userLevel = r.reported_user_level();
}
break;
}
}
table->selectRow(row);
userContextMenu->showContextMenu(table->viewport()->mapToGlobal(pos), reportedItem->text(),
UserLevelFlags(userLevel));
}
void TabReport::loadReportDetails(int reportId) void TabReport::loadReportDetails(int reportId)
{ {
detailsRequestedReportId = reportId; detailsRequestedReportId = reportId;

View file

@ -4,6 +4,7 @@
#include "tab.h" #include "tab.h"
#include <QList> #include <QList>
#include <QPoint>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h> #include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
@ -19,6 +20,7 @@ class QTableWidget;
class QTextEdit; class QTextEdit;
class QTimer; class QTimer;
class GameReplay; class GameReplay;
class UserContextMenu;
class TabReport : public Tab class TabReport : public Tab
{ {
@ -34,6 +36,7 @@ public:
signals: signals:
void openReplay(GameReplay *replay); void openReplay(GameReplay *replay);
void requestJoinGame(int gameId, int roomId); void requestJoinGame(int gameId, int roomId);
void openMessageDialog(const QString &userName, bool focus);
private slots: private slots:
void refreshList(); void refreshList();
@ -43,6 +46,7 @@ private slots:
void resolveReport(bool dismissed, bool promptNote); void resolveReport(bool dismissed, bool promptNote);
void resolveResponse(const Response &response); void resolveResponse(const Response &response);
void onSelectionChanged(); void onSelectionChanged();
void showTableContextMenu(const QPoint &pos);
void viewReplay(); void viewReplay();
void viewReplayResponse(const Response &response); void viewReplayResponse(const Response &response);
void joinGame(); void joinGame();
@ -91,6 +95,7 @@ private:
QPushButton *joinGameButton; QPushButton *joinGameButton;
QLabel *statusLabel; QLabel *statusLabel;
QTimer *refreshTimer; QTimer *refreshTimer;
UserContextMenu *userContextMenu;
QGroupBox *userContextGroup; QGroupBox *userContextGroup;
QLabel *userContextName; QLabel *userContextName;

View file

@ -884,6 +884,7 @@ void TabSupervisor::openTabReport()
myAddTab(tabReport, aTabReport); myAddTab(tabReport, aTabReport);
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay); connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame); connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
connect(tabReport, &TabReport::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tabReport, &QObject::destroyed, this, [this] { connect(tabReport, &QObject::destroyed, this, [this] {
tabReport = nullptr; tabReport = nullptr;
aTabReport->setChecked(false); aTabReport->setChecked(false);

View file

@ -34,4 +34,6 @@ message ServerInfo_Report {
optional string resolution_note = 15; optional string resolution_note = 15;
optional string resolved_by_name = 16; optional string resolved_by_name = 16;
optional int32 reported_user_level = 17;
} }

View file

@ -1943,6 +1943,29 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com
return Response::RespOk; return Response::RespOk;
} }
namespace
{
int reportUserLevel(bool userExists, int adminFlags)
{
int userLevel = ServerInfo_User::IsUser;
if (userExists) {
userLevel |= ServerInfo_User::IsRegistered;
if (adminFlags & 1) {
userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator;
} else if (adminFlags & 2) {
userLevel |= ServerInfo_User::IsModerator;
}
if (adminFlags & 4) {
userLevel |= ServerInfo_User::IsJudge;
}
if (adminFlags & 8) {
userLevel |= ServerInfo_User::IsDeveloper;
}
}
return userLevel;
}
} // namespace
Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd, Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd,
ResponseContainer &rc) ResponseContainer &rc)
{ {
@ -1957,7 +1980,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
// Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id, // Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id,
// 4=category, 5=description, 6=created_at, 7=status, // 4=category, 5=description, 6=created_at, 7=status,
// 8=resolution_note, 9=assigned_mod_name, // 8=resolution_note, 9=assigned_mod_name,
// 10=room_id, 11=replay_id // 10=room_id, 11=replay_id,
// 12=reported_user_id, 13=reported_user_admin
QString whereClause; QString whereClause;
if (unresolvedOnly) { if (unresolvedOnly) {
whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' "; whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' ";
@ -1974,10 +1998,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
"r.category, r.description, r.created_at, r.status, " "r.category, r.description, r.created_at, r.status, "
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, " "r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, "
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
"AS replay_id " "AS replay_id, u2.id AS reported_user_id, u2.admin AS reported_user_admin "
"FROM {prefix}_reports r " "FROM {prefix}_reports r "
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id " "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "; "LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
"LEFT JOIN {prefix}_users u2 ON u2.name = r.reported_user_name ";
if (unresolvedOnly) { if (unresolvedOnly) {
queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' "; queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' ";
@ -2029,6 +2054,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
if (!query->value(12).isNull()) { if (!query->value(12).isNull()) {
info->set_replay_id(query->value(12).toInt()); info->set_replay_id(query->value(12).toInt());
} }
info->set_reported_user_level(reportUserLevel(!query->value(12).isNull(), query->value(13).toInt()));
} }
rc.setResponseExtension(re); rc.setResponseExtension(re);
@ -2703,10 +2730,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm
"r.category, r.description, r.created_at, r.status, " "r.category, r.description, r.created_at, r.status, "
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, " "r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, "
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
"AS replay_id " "AS replay_id, u2.id AS reported_user_id, u2.admin AS reported_user_admin "
"FROM {prefix}_reports r " "FROM {prefix}_reports r "
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id " "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id " "LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
"LEFT JOIN {prefix}_users u2 ON u2.name = r.reported_user_name "
"WHERE r.reporter_id = :reporter_id " "WHERE r.reporter_id = :reporter_id "
"ORDER BY r.created_at DESC LIMIT 200"; "ORDER BY r.created_at DESC LIMIT 200";
@ -2753,6 +2781,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm
if (!query->value(12).isNull()) { if (!query->value(12).isNull()) {
info->set_replay_id(query->value(12).toInt()); info->set_replay_id(query->value(12).toInt());
} }
info->set_reported_user_level(reportUserLevel(!query->value(12).isNull(), query->value(13).toInt()));
} }
rc.setResponseExtension(re); rc.setResponseExtension(re);