mirror of
https://git.citron-emu.org/citron/emulator
synced 2026-01-26 12:43:29 +00:00
Merge pull request 'feat(multiplayer): Chatroom QoL Changes & Additions' (#92) from feat/chatroom-additions into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/92
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <future>
|
||||
#include <QColor>
|
||||
#include <QColorDialog>
|
||||
#include <QDesktopServices>
|
||||
#include <QFutureWatcher>
|
||||
#include <QImage>
|
||||
@@ -12,9 +14,13 @@
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMetaType>
|
||||
#include <QPainter>
|
||||
#include <QTime>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#include <QToolButton>
|
||||
#include <QGridLayout>
|
||||
#include <QWidgetAction>
|
||||
#include "common/logging/log.h"
|
||||
#include "network/announce_multiplayer_session.h"
|
||||
#include "ui_chat_room.h"
|
||||
@@ -62,11 +68,17 @@ public:
|
||||
}
|
||||
|
||||
/// Format the message using the players color
|
||||
QString GetPlayerChatMessage(u16 player) const {
|
||||
QString GetPlayerChatMessage(u16 player, bool show_timestamps, const std::string& override_color = "") const {
|
||||
const bool is_dark_theme = QIcon::themeName().contains(QStringLiteral("dark")) ||
|
||||
QIcon::themeName().contains(QStringLiteral("midnight"));
|
||||
auto color =
|
||||
is_dark_theme ? player_color_dark[player % 16] : player_color_default[player % 16];
|
||||
QIcon::themeName().contains(QStringLiteral("midnight"));
|
||||
|
||||
std::string color;
|
||||
if (!override_color.empty()) {
|
||||
color = override_color;
|
||||
} else {
|
||||
color = is_dark_theme ? player_color_dark[player % 16] : player_color_default[player % 16];
|
||||
}
|
||||
|
||||
QString name;
|
||||
if (username.isEmpty() || username == nickname) {
|
||||
name = nickname;
|
||||
@@ -82,10 +94,11 @@ public:
|
||||
text_color = QStringLiteral("color='#000000'");
|
||||
}
|
||||
|
||||
return QStringLiteral("[%1] <font color='%2'><%3></font> <font style='%4' "
|
||||
"%5>%6</font>")
|
||||
.arg(timestamp, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color,
|
||||
message.toHtmlEscaped());
|
||||
QString time_str = show_timestamps ? QStringLiteral("[%1] ").arg(timestamp) : QStringLiteral("");
|
||||
return QStringLiteral("%1<font color='%2'><%3></font> <font style='%4' "
|
||||
"%5>%6</font>")
|
||||
.arg(time_str, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color,
|
||||
message.toHtmlEscaped());
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -113,9 +126,10 @@ public:
|
||||
message = msg;
|
||||
}
|
||||
|
||||
QString GetSystemChatMessage() const {
|
||||
return QStringLiteral("[%1] <font color='%2'>* %3</font>")
|
||||
.arg(timestamp, QString::fromStdString(system_color), message);
|
||||
QString GetSystemChatMessage(bool show_timestamps) const {
|
||||
QString time_str = show_timestamps ? QStringLiteral("[%1] ").arg(timestamp) : QStringLiteral("");
|
||||
return QStringLiteral("%1<font color='%2'>* %3</font>")
|
||||
.arg(time_str, QString::fromStdString(system_color), message);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -131,6 +145,7 @@ public:
|
||||
static const int AvatarUrlRole = Qt::UserRole + 3;
|
||||
static const int GameNameRole = Qt::UserRole + 4;
|
||||
static const int GameVersionRole = Qt::UserRole + 5;
|
||||
static const int StatusDotRole = Qt::UserRole + 6;
|
||||
|
||||
PlayerListItem() = default;
|
||||
explicit PlayerListItem(const std::string& nickname, const std::string& username,
|
||||
@@ -160,46 +175,99 @@ public:
|
||||
} else {
|
||||
name = QStringLiteral("%1 (%2)").arg(nickname, username);
|
||||
}
|
||||
|
||||
const QString version = data(GameVersionRole).toString();
|
||||
QString version_string;
|
||||
if (!version.isEmpty()) {
|
||||
version_string = QStringLiteral("(%1)").arg(version);
|
||||
}
|
||||
|
||||
return QStringLiteral("%1\n %2 %3")
|
||||
.arg(name, data(GameNameRole).toString(), version_string);
|
||||
.arg(name, data(GameNameRole).toString(), version_string);
|
||||
}
|
||||
};
|
||||
|
||||
ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) {
|
||||
ui->setupUi(this);
|
||||
|
||||
// set the item_model for player_view
|
||||
QToolButton* emoji_button = new QToolButton(this);
|
||||
emoji_button->setText(QStringLiteral("😀"));
|
||||
emoji_button->setPopupMode(QToolButton::InstantPopup);
|
||||
emoji_button->setAutoRaise(true);
|
||||
emoji_button->setFixedSize(30, 30);
|
||||
// Hide the arrow indicator and remove padding to ensure the emoji is dead-center
|
||||
emoji_button->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; } QToolButton { padding: 0px; }"));
|
||||
|
||||
ui->horizontalLayout_3->insertWidget(1, emoji_button);
|
||||
|
||||
QMenu* emoji_menu = new QMenu(this);
|
||||
|
||||
QStringList emojis = {
|
||||
QStringLiteral("😀"), QStringLiteral("😂"), QStringLiteral("🤣"), QStringLiteral("😊"), QStringLiteral("😎"),
|
||||
QStringLiteral("🤔"), QStringLiteral("🤨"), QStringLiteral("😭"), QStringLiteral("😮"), QStringLiteral("💀"),
|
||||
QStringLiteral("👍"), QStringLiteral("👎"), QStringLiteral("🔥"), QStringLiteral("✨"), QStringLiteral("❤️"),
|
||||
QStringLiteral("🎉"), QStringLiteral("💯"), QStringLiteral("🚀"), QStringLiteral("🎮"), QStringLiteral("🕹️"),
|
||||
QStringLiteral("👾"), QStringLiteral("🍄"), QStringLiteral("⭐️"), QStringLiteral("⚔️"), QStringLiteral("🛡️")
|
||||
};
|
||||
|
||||
// Create a container widget for the grid
|
||||
QWidget* grid_container = new QWidget(emoji_menu);
|
||||
QGridLayout* grid_layout = new QGridLayout(grid_container);
|
||||
grid_layout->setSpacing(2);
|
||||
grid_layout->setContentsMargins(5, 5, 5, 5);
|
||||
|
||||
const int max_columns = 5;
|
||||
|
||||
for (int i = 0; i < emojis.size(); ++i) {
|
||||
const QString emoji = emojis[i];
|
||||
QToolButton* btn = new QToolButton(grid_container);
|
||||
btn->setText(emoji);
|
||||
btn->setFixedSize(34, 30);
|
||||
btn->setAutoRaise(true);
|
||||
|
||||
connect(btn, &QToolButton::clicked, [this, emoji, emoji_menu]() {
|
||||
ui->chat_message->insert(emoji);
|
||||
ui->chat_message->setFocus();
|
||||
emoji_menu->close(); // Close the menu after picking
|
||||
});
|
||||
|
||||
// Add to grid: row = i / columns, col = i % columns
|
||||
grid_layout->addWidget(btn, i / max_columns, i % max_columns);
|
||||
}
|
||||
|
||||
// Use QWidgetAction to "stuff" the grid into the QMenu
|
||||
QWidgetAction* action = new QWidgetAction(emoji_menu);
|
||||
action->setDefaultWidget(grid_container);
|
||||
emoji_menu->addAction(action);
|
||||
|
||||
emoji_button->setMenu(emoji_menu);
|
||||
|
||||
player_list = new QStandardItemModel(ui->player_view);
|
||||
ui->player_view->setModel(player_list);
|
||||
ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
// set a header to make it look better though there is only one column
|
||||
player_list->insertColumns(0, 1);
|
||||
player_list->setHeaderData(0, Qt::Horizontal, tr("Members"));
|
||||
|
||||
ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
|
||||
ui->chat_history->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
auto font = ui->chat_history->font();
|
||||
font.setPointSizeF(10);
|
||||
ui->chat_history->setFont(font);
|
||||
|
||||
// register the network structs to use in slots and signals
|
||||
qRegisterMetaType<Network::ChatEntry>();
|
||||
qRegisterMetaType<Network::StatusMessageEntry>();
|
||||
qRegisterMetaType<Network::RoomInformation>();
|
||||
qRegisterMetaType<Network::RoomMember::State>();
|
||||
|
||||
// Connect all the widgets to the appropriate events
|
||||
connect(ui->player_view, &QTreeView::customContextMenuRequested, this,
|
||||
&ChatRoom::PopupContextMenu);
|
||||
connect(ui->chat_history, &QTextEdit::customContextMenuRequested, this,
|
||||
&ChatRoom::OnChatContextMenu);
|
||||
connect(ui->chat_message, &QLineEdit::returnPressed, this, &ChatRoom::OnSendChat);
|
||||
connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged);
|
||||
connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat);
|
||||
connect(ui->player_view, &QTreeView::doubleClicked, this, &ChatRoom::OnPlayerDoubleClicked);
|
||||
|
||||
UpdateTheme();
|
||||
}
|
||||
@@ -208,7 +276,6 @@ ChatRoom::~ChatRoom() = default;
|
||||
|
||||
void ChatRoom::Initialize(Network::RoomNetwork* room_network_) {
|
||||
room_network = room_network_;
|
||||
// setup the callbacks for network updates
|
||||
if (auto member = room_network->GetRoomMember().lock()) {
|
||||
member->BindOnChatMessageReceived(
|
||||
[this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
|
||||
@@ -223,17 +290,9 @@ void ChatRoom::Initialize(Network::RoomNetwork* room_network_) {
|
||||
|
||||
void ChatRoom::Shutdown() {
|
||||
if (room_network) {
|
||||
// Disconnect the signals that were connected in Initialize.
|
||||
// It's safe to call disconnect even if the connection doesn't exist.
|
||||
disconnect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive);
|
||||
disconnect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive);
|
||||
|
||||
// NOTE: The Bind... functions do not have a direct unbind. The intended way
|
||||
// to stop them is to let the 'member' object be destroyed or to stop calling them
|
||||
// from the network backend. Since we are disconnecting from the room, this is safe.
|
||||
// The important part is disconnecting the Qt signals.
|
||||
|
||||
room_network = nullptr; // Clear the pointer to prevent further use.
|
||||
room_network = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,10 +310,12 @@ void ChatRoom::Clear() {
|
||||
}
|
||||
|
||||
void ChatRoom::AppendStatusMessage(const QString& msg) {
|
||||
ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage());
|
||||
if (chat_muted) return;
|
||||
ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage(show_timestamps));
|
||||
}
|
||||
|
||||
void ChatRoom::AppendChatMessage(const QString& msg) {
|
||||
if (chat_muted) return;
|
||||
ui->chat_history->append(msg);
|
||||
}
|
||||
|
||||
@@ -278,7 +339,6 @@ bool ChatRoom::ValidateMessage(const std::string& msg) {
|
||||
}
|
||||
|
||||
void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
|
||||
// TODO(B3N30): change title
|
||||
if (auto room_member = room_network->GetRoomMember().lock()) {
|
||||
SetPlayerList(room_member->GetMemberInformation());
|
||||
}
|
||||
@@ -299,7 +359,6 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
|
||||
return;
|
||||
}
|
||||
if (auto room = room_network->GetRoomMember().lock()) {
|
||||
// get the id of the player
|
||||
auto members = room->GetMemberInformation();
|
||||
auto it = std::find_if(members.begin(), members.end(),
|
||||
[&chat](const Network::RoomMember::MemberInformation& member) {
|
||||
@@ -320,7 +379,13 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
|
||||
if (m.ContainsPing()) {
|
||||
emit UserPinged();
|
||||
}
|
||||
AppendChatMessage(m.GetPlayerChatMessage(player));
|
||||
|
||||
std::string override_color = "";
|
||||
if (color_overrides.count(chat.nickname)) {
|
||||
override_color = color_overrides[chat.nickname];
|
||||
}
|
||||
|
||||
AppendChatMessage(m.GetPlayerChatMessage(static_cast<u16>(player), show_timestamps, override_color));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +424,20 @@ void ChatRoom::OnSendChat() {
|
||||
if (!room_member->IsConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
sent_message_timestamps.erase(
|
||||
std::remove_if(sent_message_timestamps.begin(), sent_message_timestamps.end(),
|
||||
[now](const auto& ts) {
|
||||
return (now - ts) > THROTTLE_INTERVAL;
|
||||
}),
|
||||
sent_message_timestamps.end());
|
||||
|
||||
if (sent_message_timestamps.size() >= MAX_MESSAGES_PER_INTERVAL) {
|
||||
AppendStatusMessage(tr("Spam detected. Please don't send more than 3 messages per every 5 seconds."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto message = ui->chat_message->text().toStdString();
|
||||
if (!ValidateMessage(message)) {
|
||||
return;
|
||||
@@ -378,8 +457,16 @@ void ChatRoom::OnSendChat() {
|
||||
}
|
||||
auto player = std::distance(members.begin(), it);
|
||||
ChatMessage m(chat, *room_network);
|
||||
|
||||
room_member->SendChatMessage(message);
|
||||
AppendChatMessage(m.GetPlayerChatMessage(player));
|
||||
sent_message_timestamps.push_back(now);
|
||||
|
||||
std::string override_color = "";
|
||||
if (color_overrides.count(nick)) {
|
||||
override_color = color_overrides[nick];
|
||||
}
|
||||
|
||||
AppendChatMessage(m.GetPlayerChatMessage(static_cast<u16>(player), show_timestamps, override_color));
|
||||
ui->chat_message->clear();
|
||||
}
|
||||
}
|
||||
@@ -387,62 +474,105 @@ void ChatRoom::OnSendChat() {
|
||||
void ChatRoom::UpdateIconDisplay() {
|
||||
for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) {
|
||||
QStandardItem* item = player_list->invisibleRootItem()->child(row);
|
||||
const std::string avatar_url =
|
||||
item->data(PlayerListItem::AvatarUrlRole).toString().toStdString();
|
||||
const std::string avatar_url = item->data(PlayerListItem::AvatarUrlRole).toString().toStdString();
|
||||
|
||||
QPixmap pixmap;
|
||||
if (icon_cache.count(avatar_url)) {
|
||||
item->setData(icon_cache.at(avatar_url), Qt::DecorationRole);
|
||||
pixmap = icon_cache.at(avatar_url);
|
||||
} else {
|
||||
item->setData(QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48),
|
||||
Qt::DecorationRole);
|
||||
pixmap = QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48);
|
||||
}
|
||||
|
||||
QPixmap canvas = pixmap.copy();
|
||||
QPainter painter(&canvas);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QString dot_type = item->data(PlayerListItem::StatusDotRole).toString();
|
||||
QColor dot_color;
|
||||
if (dot_type == QStringLiteral("🟢")) dot_color = Qt::green;
|
||||
else if (dot_type == QStringLiteral("🟡")) dot_color = Qt::yellow;
|
||||
else dot_color = Qt::gray;
|
||||
|
||||
// Draw a small "outline" circle
|
||||
painter.setBrush(QColor(30, 30, 30));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawEllipse(32, 32, 14, 14);
|
||||
|
||||
// Draw the actual status dot
|
||||
painter.setBrush(dot_color);
|
||||
painter.drawEllipse(34, 34, 10, 10);
|
||||
|
||||
painter.end();
|
||||
|
||||
// Set the final icon
|
||||
item->setData(canvas, Qt::DecorationRole);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) {
|
||||
// TODO(B3N30): Remember which row is selected
|
||||
player_list->removeRows(0, player_list->rowCount());
|
||||
|
||||
// 1. Find the local player's game info to use as a baseline
|
||||
AnnounceMultiplayerRoom::GameInfo local_game_info;
|
||||
if (room_network) {
|
||||
if (auto room_member = room_network->GetRoomMember().lock()) {
|
||||
std::string my_nick = room_member->GetNickname();
|
||||
for (const auto& m : member_list) {
|
||||
if (m.nickname == my_nick) {
|
||||
local_game_info = m.game_info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create the list items
|
||||
for (const auto& member : member_list) {
|
||||
if (member.nickname.empty())
|
||||
continue;
|
||||
|
||||
QStandardItem* name_item = new PlayerListItem(member.nickname, member.username,
|
||||
member.avatar_url, member.game_info);
|
||||
|
||||
// Determine the Status Dot logic
|
||||
QString status_dot = QStringLiteral("⚪");
|
||||
if (!member.game_info.name.empty() && !local_game_info.name.empty()) {
|
||||
if (member.game_info.name == local_game_info.name) {
|
||||
if (member.game_info.version == local_game_info.version) {
|
||||
status_dot = QStringLiteral("🟢");
|
||||
} else {
|
||||
status_dot = QStringLiteral("🟡");
|
||||
}
|
||||
}
|
||||
}
|
||||
name_item->setData(status_dot, PlayerListItem::StatusDotRole);
|
||||
|
||||
#ifdef ENABLE_WEB_SERVICE
|
||||
if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) {
|
||||
// Start a request to get the member's avatar
|
||||
const QUrl url(QString::fromStdString(member.avatar_url));
|
||||
QFuture<std::string> future = QtConcurrent::run([url] {
|
||||
WebService::Client client(
|
||||
QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", "");
|
||||
auto result = client.GetImage(url.path().toStdString(), true);
|
||||
if (result.returned_data.empty()) {
|
||||
LOG_ERROR(WebService, "Failed to get avatar");
|
||||
}
|
||||
return result.returned_data;
|
||||
});
|
||||
auto* future_watcher = new QFutureWatcher<std::string>(this);
|
||||
connect(future_watcher, &QFutureWatcher<std::string>::finished, this,
|
||||
[this, future_watcher, avatar_url = member.avatar_url] {
|
||||
const std::string result = future_watcher->result();
|
||||
if (result.empty())
|
||||
return;
|
||||
if (result.empty()) return;
|
||||
QPixmap pixmap;
|
||||
if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()),
|
||||
static_cast<uint>(result.size())))
|
||||
return;
|
||||
icon_cache[avatar_url] =
|
||||
pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
// Update all the displayed icons with the new icon_cache
|
||||
if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()), static_cast<uint>(result.size()))) return;
|
||||
icon_cache[avatar_url] = pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
UpdateIconDisplay();
|
||||
future_watcher->deleteLater();
|
||||
});
|
||||
future_watcher->setFuture(future);
|
||||
}
|
||||
#endif
|
||||
|
||||
player_list->invisibleRootItem()->appendRow(name_item);
|
||||
}
|
||||
UpdateIconDisplay();
|
||||
// TODO(B3N30): Restore row selection
|
||||
}
|
||||
|
||||
void ChatRoom::OnChatTextChanged() {
|
||||
@@ -453,20 +583,24 @@ void ChatRoom::OnChatTextChanged() {
|
||||
|
||||
void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
||||
QModelIndex item = ui->player_view->indexAt(menu_location);
|
||||
if (!item.isValid())
|
||||
return;
|
||||
|
||||
std::string nickname =
|
||||
player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString();
|
||||
if (!item.isValid()) return;
|
||||
|
||||
std::string nickname = player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString();
|
||||
QMenu context_menu;
|
||||
|
||||
QAction* color_action = context_menu.addAction(tr("Set Name Color"));
|
||||
connect(color_action, &QAction::triggered, [this, nickname] {
|
||||
QColor color = QColorDialog::getColor(Qt::white, this, tr("Select Color for %1").arg(QString::fromStdString(nickname)));
|
||||
if (color.isValid()) {
|
||||
color_overrides[nickname] = color.name().toStdString();
|
||||
}
|
||||
});
|
||||
|
||||
QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString();
|
||||
if (!username.isEmpty()) {
|
||||
QAction* view_profile_action = context_menu.addAction(tr("View Profile"));
|
||||
connect(view_profile_action, &QAction::triggered, [username] {
|
||||
QDesktopServices::openUrl(
|
||||
QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username)));
|
||||
QDesktopServices::openUrl(QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -475,9 +609,8 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
||||
cur_nickname = room->GetNickname();
|
||||
}
|
||||
|
||||
if (nickname != cur_nickname) { // You can't block yourself
|
||||
if (nickname != cur_nickname) {
|
||||
QAction* block_action = context_menu.addAction(tr("Block Player"));
|
||||
|
||||
block_action->setCheckable(true);
|
||||
block_action->setChecked(block_list.count(nickname) > 0);
|
||||
|
||||
@@ -487,41 +620,19 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
||||
} else {
|
||||
QMessageBox::StandardButton result = QMessageBox::question(
|
||||
this, tr("Block Player"),
|
||||
tr("When you block a player, you will no longer receive chat messages from "
|
||||
"them.<br><br>Are you sure you would like to block %1?")
|
||||
.arg(QString::fromStdString(nickname)),
|
||||
tr("Are you sure you would like to block %1?").arg(QString::fromStdString(nickname)),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (result == QMessageBox::Yes)
|
||||
block_list.emplace(nickname);
|
||||
if (result == QMessageBox::Yes) block_list.emplace(nickname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself
|
||||
if (has_mod_perms && nickname != cur_nickname) {
|
||||
context_menu.addSeparator();
|
||||
|
||||
QAction* kick_action = context_menu.addAction(tr("Kick"));
|
||||
QAction* ban_action = context_menu.addAction(tr("Ban"));
|
||||
|
||||
connect(kick_action, &QAction::triggered, [this, nickname] {
|
||||
QMessageBox::StandardButton result =
|
||||
QMessageBox::question(this, tr("Kick Player"),
|
||||
tr("Are you sure you would like to <b>kick</b> %1?")
|
||||
.arg(QString::fromStdString(nickname)),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (result == QMessageBox::Yes)
|
||||
SendModerationRequest(Network::IdModKick, nickname);
|
||||
});
|
||||
connect(ban_action, &QAction::triggered, [this, nickname] {
|
||||
QMessageBox::StandardButton result = QMessageBox::question(
|
||||
this, tr("Ban Player"),
|
||||
tr("Are you sure you would like to <b>kick and ban</b> %1?\n\nThis would "
|
||||
"ban both their forum username and their IP address.")
|
||||
.arg(QString::fromStdString(nickname)),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (result == QMessageBox::Yes)
|
||||
SendModerationRequest(Network::IdModBan, nickname);
|
||||
});
|
||||
connect(kick_action, &QAction::triggered, [this, nickname] { SendModerationRequest(Network::IdModKick, nickname); });
|
||||
connect(ban_action, &QAction::triggered, [this, nickname] { SendModerationRequest(Network::IdModBan, nickname); });
|
||||
}
|
||||
|
||||
context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location));
|
||||
@@ -530,57 +641,54 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
||||
void ChatRoom::UpdateTheme() {
|
||||
QString style_sheet;
|
||||
const QString accent_color = Theme::GetAccentColor();
|
||||
|
||||
if (UISettings::IsDarkTheme()) {
|
||||
style_sheet = QStringLiteral(R"(
|
||||
QListView, QTextEdit, QLineEdit {
|
||||
background-color: #252525;
|
||||
color: #E0E0E0;
|
||||
border: 1px solid #4A4A4A;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QListView::item:selected {
|
||||
background-color: %1;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #3E3E3E;
|
||||
color: #E0E0E0;
|
||||
border: 1px solid #5A5A5A;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #4A4A4A;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
}
|
||||
QListView, QTextEdit, QLineEdit { background-color: #252525; color: #E0E0E0; border: 1px solid #4A4A4A; border-radius: 4px; }
|
||||
QListView::item:selected { background-color: %1; }
|
||||
QPushButton { background-color: #3E3E3E; color: #E0E0E0; border: 1px solid #5A5A5A; padding: 5px; border-radius: 4px; }
|
||||
)").arg(accent_color);
|
||||
} else {
|
||||
style_sheet = QStringLiteral(R"(
|
||||
QListView, QTextEdit, QLineEdit {
|
||||
background-color: #FFFFFF;
|
||||
color: #000000;
|
||||
border: 1px solid #CFCFCF;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QListView::item:selected {
|
||||
background-color: %1;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #F0F0F0;
|
||||
color: #000000;
|
||||
border: 1px solid #BDBDBD;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #D0D0D0;
|
||||
}
|
||||
QListView, QTextEdit, QLineEdit { background-color: #FFFFFF; color: #000000; border: 1px solid #CFCFCF; border-radius: 4px; }
|
||||
QListView::item:selected { background-color: %1; }
|
||||
QPushButton { background-color: #F0F0F0; color: #000000; border: 1px solid #BDBDBD; padding: 5px; border-radius: 4px; }
|
||||
)").arg(accent_color);
|
||||
}
|
||||
this->setStyleSheet(style_sheet);
|
||||
}
|
||||
|
||||
void ChatRoom::OnChatContextMenu(const QPoint& menu_location) {
|
||||
QMenu* context_menu = ui->chat_history->createStandardContextMenu(menu_location);
|
||||
context_menu->addSeparator();
|
||||
QAction* clear_action = context_menu->addAction(tr("Clear Chat History"));
|
||||
connect(clear_action, &QAction::triggered, this, &ChatRoom::Clear);
|
||||
|
||||
QAction* mute_action = context_menu->addAction(tr("Hide Future Messages"));
|
||||
mute_action->setCheckable(true);
|
||||
mute_action->setChecked(chat_muted);
|
||||
connect(mute_action, &QAction::triggered, [this](bool checked) {
|
||||
this->chat_muted = checked;
|
||||
if (checked) {
|
||||
ui->chat_history->clear();
|
||||
ui->chat_history->append(tr("<font color='#FF8C00'>* Chat Paused. Right-click to resume.</font>"));
|
||||
}
|
||||
});
|
||||
|
||||
QAction* time_action = context_menu->addAction(tr("Show Timestamps"));
|
||||
time_action->setCheckable(true);
|
||||
time_action->setChecked(show_timestamps);
|
||||
connect(time_action, &QAction::triggered, [this](bool checked) { show_timestamps = checked; });
|
||||
|
||||
context_menu->exec(ui->chat_history->viewport()->mapToGlobal(menu_location));
|
||||
delete context_menu;
|
||||
}
|
||||
|
||||
void ChatRoom::OnPlayerDoubleClicked(const QModelIndex& index) {
|
||||
QString nickname = player_list->data(index, PlayerListItem::NicknameRole).toString();
|
||||
if (!nickname.isEmpty()) {
|
||||
QString currentText = ui->chat_message->text();
|
||||
if (!currentText.isEmpty() && !currentText.endsWith(QStringLiteral(" "))) currentText += QStringLiteral(" ");
|
||||
ui->chat_message->setText(currentText + QStringLiteral("@%1 ").arg(nickname));
|
||||
ui->chat_message->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono> // time tracking
|
||||
#include <vector> // storing timestamps
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -49,6 +51,8 @@ public slots:
|
||||
void OnSendChat();
|
||||
void OnChatTextChanged();
|
||||
void PopupContextMenu(const QPoint& menu_location);
|
||||
void OnChatContextMenu(const QPoint& menu_location);
|
||||
void OnPlayerDoubleClicked(const QModelIndex& index);
|
||||
void Disable();
|
||||
void Enable();
|
||||
void UpdateTheme();
|
||||
@@ -69,7 +73,13 @@ private:
|
||||
std::unique_ptr<Ui::ChatRoom> ui;
|
||||
std::unordered_set<std::string> block_list;
|
||||
std::unordered_map<std::string, QPixmap> icon_cache;
|
||||
Network::RoomNetwork* room_network = nullptr; // Initialize to nullptr
|
||||
std::unordered_map<std::string, std::string> color_overrides;
|
||||
bool chat_muted = false;
|
||||
bool show_timestamps = true;
|
||||
Network::RoomNetwork* room_network = nullptr;
|
||||
std::vector<std::chrono::steady_clock::time_point> sent_message_timestamps;
|
||||
static constexpr size_t MAX_MESSAGES_PER_INTERVAL = 3;
|
||||
static constexpr std::chrono::seconds THROTTLE_INTERVAL{5};
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Network::ChatEntry);
|
||||
|
||||
Reference in New Issue
Block a user