mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-23 01:56:08 -04: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 2017 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QColorDialog>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
@@ -12,9 +14,13 @@
|
|||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QMetaType>
|
#include <QMetaType>
|
||||||
|
#include <QPainter>
|
||||||
#include <QTime>
|
#include <QTime>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QtConcurrent/QtConcurrentRun>
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QWidgetAction>
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "network/announce_multiplayer_session.h"
|
#include "network/announce_multiplayer_session.h"
|
||||||
#include "ui_chat_room.h"
|
#include "ui_chat_room.h"
|
||||||
@@ -62,11 +68,17 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format the message using the players color
|
/// 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")) ||
|
const bool is_dark_theme = QIcon::themeName().contains(QStringLiteral("dark")) ||
|
||||||
QIcon::themeName().contains(QStringLiteral("midnight"));
|
QIcon::themeName().contains(QStringLiteral("midnight"));
|
||||||
auto color =
|
|
||||||
is_dark_theme ? player_color_dark[player % 16] : player_color_default[player % 16];
|
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;
|
QString name;
|
||||||
if (username.isEmpty() || username == nickname) {
|
if (username.isEmpty() || username == nickname) {
|
||||||
name = nickname;
|
name = nickname;
|
||||||
@@ -82,10 +94,11 @@ public:
|
|||||||
text_color = QStringLiteral("color='#000000'");
|
text_color = QStringLiteral("color='#000000'");
|
||||||
}
|
}
|
||||||
|
|
||||||
return QStringLiteral("[%1] <font color='%2'><%3></font> <font style='%4' "
|
QString time_str = show_timestamps ? QStringLiteral("[%1] ").arg(timestamp) : QStringLiteral("");
|
||||||
"%5>%6</font>")
|
return QStringLiteral("%1<font color='%2'><%3></font> <font style='%4' "
|
||||||
.arg(timestamp, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color,
|
"%5>%6</font>")
|
||||||
message.toHtmlEscaped());
|
.arg(time_str, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color,
|
||||||
|
message.toHtmlEscaped());
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -113,9 +126,10 @@ public:
|
|||||||
message = msg;
|
message = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString GetSystemChatMessage() const {
|
QString GetSystemChatMessage(bool show_timestamps) const {
|
||||||
return QStringLiteral("[%1] <font color='%2'>* %3</font>")
|
QString time_str = show_timestamps ? QStringLiteral("[%1] ").arg(timestamp) : QStringLiteral("");
|
||||||
.arg(timestamp, QString::fromStdString(system_color), message);
|
return QStringLiteral("%1<font color='%2'>* %3</font>")
|
||||||
|
.arg(time_str, QString::fromStdString(system_color), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -131,6 +145,7 @@ public:
|
|||||||
static const int AvatarUrlRole = Qt::UserRole + 3;
|
static const int AvatarUrlRole = Qt::UserRole + 3;
|
||||||
static const int GameNameRole = Qt::UserRole + 4;
|
static const int GameNameRole = Qt::UserRole + 4;
|
||||||
static const int GameVersionRole = Qt::UserRole + 5;
|
static const int GameVersionRole = Qt::UserRole + 5;
|
||||||
|
static const int StatusDotRole = Qt::UserRole + 6;
|
||||||
|
|
||||||
PlayerListItem() = default;
|
PlayerListItem() = default;
|
||||||
explicit PlayerListItem(const std::string& nickname, const std::string& username,
|
explicit PlayerListItem(const std::string& nickname, const std::string& username,
|
||||||
@@ -160,46 +175,99 @@ public:
|
|||||||
} else {
|
} else {
|
||||||
name = QStringLiteral("%1 (%2)").arg(nickname, username);
|
name = QStringLiteral("%1 (%2)").arg(nickname, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString version = data(GameVersionRole).toString();
|
const QString version = data(GameVersionRole).toString();
|
||||||
QString version_string;
|
QString version_string;
|
||||||
if (!version.isEmpty()) {
|
if (!version.isEmpty()) {
|
||||||
version_string = QStringLiteral("(%1)").arg(version);
|
version_string = QStringLiteral("(%1)").arg(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QStringLiteral("%1\n %2 %3")
|
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>()) {
|
ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) {
|
||||||
ui->setupUi(this);
|
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);
|
player_list = new QStandardItemModel(ui->player_view);
|
||||||
ui->player_view->setModel(player_list);
|
ui->player_view->setModel(player_list);
|
||||||
ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
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->insertColumns(0, 1);
|
||||||
player_list->setHeaderData(0, Qt::Horizontal, tr("Members"));
|
player_list->setHeaderData(0, Qt::Horizontal, tr("Members"));
|
||||||
|
|
||||||
ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
|
ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
|
||||||
|
ui->chat_history->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
|
||||||
auto font = ui->chat_history->font();
|
auto font = ui->chat_history->font();
|
||||||
font.setPointSizeF(10);
|
font.setPointSizeF(10);
|
||||||
ui->chat_history->setFont(font);
|
ui->chat_history->setFont(font);
|
||||||
|
|
||||||
// register the network structs to use in slots and signals
|
|
||||||
qRegisterMetaType<Network::ChatEntry>();
|
qRegisterMetaType<Network::ChatEntry>();
|
||||||
qRegisterMetaType<Network::StatusMessageEntry>();
|
qRegisterMetaType<Network::StatusMessageEntry>();
|
||||||
qRegisterMetaType<Network::RoomInformation>();
|
qRegisterMetaType<Network::RoomInformation>();
|
||||||
qRegisterMetaType<Network::RoomMember::State>();
|
qRegisterMetaType<Network::RoomMember::State>();
|
||||||
|
|
||||||
// Connect all the widgets to the appropriate events
|
|
||||||
connect(ui->player_view, &QTreeView::customContextMenuRequested, this,
|
connect(ui->player_view, &QTreeView::customContextMenuRequested, this,
|
||||||
&ChatRoom::PopupContextMenu);
|
&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::returnPressed, this, &ChatRoom::OnSendChat);
|
||||||
connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged);
|
connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged);
|
||||||
connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat);
|
connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat);
|
||||||
|
connect(ui->player_view, &QTreeView::doubleClicked, this, &ChatRoom::OnPlayerDoubleClicked);
|
||||||
|
|
||||||
UpdateTheme();
|
UpdateTheme();
|
||||||
}
|
}
|
||||||
@@ -208,7 +276,6 @@ ChatRoom::~ChatRoom() = default;
|
|||||||
|
|
||||||
void ChatRoom::Initialize(Network::RoomNetwork* room_network_) {
|
void ChatRoom::Initialize(Network::RoomNetwork* room_network_) {
|
||||||
room_network = room_network_;
|
room_network = room_network_;
|
||||||
// setup the callbacks for network updates
|
|
||||||
if (auto member = room_network->GetRoomMember().lock()) {
|
if (auto member = room_network->GetRoomMember().lock()) {
|
||||||
member->BindOnChatMessageReceived(
|
member->BindOnChatMessageReceived(
|
||||||
[this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
|
[this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
|
||||||
@@ -223,17 +290,9 @@ void ChatRoom::Initialize(Network::RoomNetwork* room_network_) {
|
|||||||
|
|
||||||
void ChatRoom::Shutdown() {
|
void ChatRoom::Shutdown() {
|
||||||
if (room_network) {
|
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::ChatReceived, this, &ChatRoom::OnChatReceive);
|
||||||
disconnect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive);
|
disconnect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive);
|
||||||
|
room_network = nullptr;
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,10 +310,12 @@ void ChatRoom::Clear() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ChatRoom::AppendStatusMessage(const QString& msg) {
|
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) {
|
void ChatRoom::AppendChatMessage(const QString& msg) {
|
||||||
|
if (chat_muted) return;
|
||||||
ui->chat_history->append(msg);
|
ui->chat_history->append(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +339,6 @@ bool ChatRoom::ValidateMessage(const std::string& msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
|
void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
|
||||||
// TODO(B3N30): change title
|
|
||||||
if (auto room_member = room_network->GetRoomMember().lock()) {
|
if (auto room_member = room_network->GetRoomMember().lock()) {
|
||||||
SetPlayerList(room_member->GetMemberInformation());
|
SetPlayerList(room_member->GetMemberInformation());
|
||||||
}
|
}
|
||||||
@@ -299,7 +359,6 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (auto room = room_network->GetRoomMember().lock()) {
|
if (auto room = room_network->GetRoomMember().lock()) {
|
||||||
// get the id of the player
|
|
||||||
auto members = room->GetMemberInformation();
|
auto members = room->GetMemberInformation();
|
||||||
auto it = std::find_if(members.begin(), members.end(),
|
auto it = std::find_if(members.begin(), members.end(),
|
||||||
[&chat](const Network::RoomMember::MemberInformation& member) {
|
[&chat](const Network::RoomMember::MemberInformation& member) {
|
||||||
@@ -320,7 +379,13 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
|
|||||||
if (m.ContainsPing()) {
|
if (m.ContainsPing()) {
|
||||||
emit UserPinged();
|
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()) {
|
if (!room_member->IsConnected()) {
|
||||||
return;
|
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();
|
auto message = ui->chat_message->text().toStdString();
|
||||||
if (!ValidateMessage(message)) {
|
if (!ValidateMessage(message)) {
|
||||||
return;
|
return;
|
||||||
@@ -378,8 +457,16 @@ void ChatRoom::OnSendChat() {
|
|||||||
}
|
}
|
||||||
auto player = std::distance(members.begin(), it);
|
auto player = std::distance(members.begin(), it);
|
||||||
ChatMessage m(chat, *room_network);
|
ChatMessage m(chat, *room_network);
|
||||||
|
|
||||||
room_member->SendChatMessage(message);
|
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();
|
ui->chat_message->clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,62 +474,105 @@ void ChatRoom::OnSendChat() {
|
|||||||
void ChatRoom::UpdateIconDisplay() {
|
void ChatRoom::UpdateIconDisplay() {
|
||||||
for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) {
|
for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) {
|
||||||
QStandardItem* item = player_list->invisibleRootItem()->child(row);
|
QStandardItem* item = player_list->invisibleRootItem()->child(row);
|
||||||
const std::string avatar_url =
|
const std::string avatar_url = item->data(PlayerListItem::AvatarUrlRole).toString().toStdString();
|
||||||
item->data(PlayerListItem::AvatarUrlRole).toString().toStdString();
|
|
||||||
|
QPixmap pixmap;
|
||||||
if (icon_cache.count(avatar_url)) {
|
if (icon_cache.count(avatar_url)) {
|
||||||
item->setData(icon_cache.at(avatar_url), Qt::DecorationRole);
|
pixmap = icon_cache.at(avatar_url);
|
||||||
} else {
|
} else {
|
||||||
item->setData(QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48),
|
pixmap = QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48);
|
||||||
Qt::DecorationRole);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) {
|
||||||
// TODO(B3N30): Remember which row is selected
|
|
||||||
player_list->removeRows(0, player_list->rowCount());
|
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) {
|
for (const auto& member : member_list) {
|
||||||
if (member.nickname.empty())
|
if (member.nickname.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
QStandardItem* name_item = new PlayerListItem(member.nickname, member.username,
|
QStandardItem* name_item = new PlayerListItem(member.nickname, member.username,
|
||||||
member.avatar_url, member.game_info);
|
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
|
#ifdef ENABLE_WEB_SERVICE
|
||||||
if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) {
|
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));
|
const QUrl url(QString::fromStdString(member.avatar_url));
|
||||||
QFuture<std::string> future = QtConcurrent::run([url] {
|
QFuture<std::string> future = QtConcurrent::run([url] {
|
||||||
WebService::Client client(
|
WebService::Client client(
|
||||||
QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", "");
|
QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", "");
|
||||||
auto result = client.GetImage(url.path().toStdString(), true);
|
auto result = client.GetImage(url.path().toStdString(), true);
|
||||||
if (result.returned_data.empty()) {
|
|
||||||
LOG_ERROR(WebService, "Failed to get avatar");
|
|
||||||
}
|
|
||||||
return result.returned_data;
|
return result.returned_data;
|
||||||
});
|
});
|
||||||
auto* future_watcher = new QFutureWatcher<std::string>(this);
|
auto* future_watcher = new QFutureWatcher<std::string>(this);
|
||||||
connect(future_watcher, &QFutureWatcher<std::string>::finished, this,
|
connect(future_watcher, &QFutureWatcher<std::string>::finished, this,
|
||||||
[this, future_watcher, avatar_url = member.avatar_url] {
|
[this, future_watcher, avatar_url = member.avatar_url] {
|
||||||
const std::string result = future_watcher->result();
|
const std::string result = future_watcher->result();
|
||||||
if (result.empty())
|
if (result.empty()) return;
|
||||||
return;
|
|
||||||
QPixmap pixmap;
|
QPixmap pixmap;
|
||||||
if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()),
|
if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()), static_cast<uint>(result.size()))) return;
|
||||||
static_cast<uint>(result.size())))
|
icon_cache[avatar_url] = pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
return;
|
|
||||||
icon_cache[avatar_url] =
|
|
||||||
pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
||||||
// Update all the displayed icons with the new icon_cache
|
|
||||||
UpdateIconDisplay();
|
UpdateIconDisplay();
|
||||||
|
future_watcher->deleteLater();
|
||||||
});
|
});
|
||||||
future_watcher->setFuture(future);
|
future_watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
player_list->invisibleRootItem()->appendRow(name_item);
|
player_list->invisibleRootItem()->appendRow(name_item);
|
||||||
}
|
}
|
||||||
UpdateIconDisplay();
|
UpdateIconDisplay();
|
||||||
// TODO(B3N30): Restore row selection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRoom::OnChatTextChanged() {
|
void ChatRoom::OnChatTextChanged() {
|
||||||
@@ -453,20 +583,24 @@ void ChatRoom::OnChatTextChanged() {
|
|||||||
|
|
||||||
void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
||||||
QModelIndex item = ui->player_view->indexAt(menu_location);
|
QModelIndex item = ui->player_view->indexAt(menu_location);
|
||||||
if (!item.isValid())
|
if (!item.isValid()) return;
|
||||||
return;
|
|
||||||
|
|
||||||
std::string nickname =
|
|
||||||
player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString();
|
|
||||||
|
|
||||||
|
std::string nickname = player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString();
|
||||||
QMenu context_menu;
|
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();
|
QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString();
|
||||||
if (!username.isEmpty()) {
|
if (!username.isEmpty()) {
|
||||||
QAction* view_profile_action = context_menu.addAction(tr("View Profile"));
|
QAction* view_profile_action = context_menu.addAction(tr("View Profile"));
|
||||||
connect(view_profile_action, &QAction::triggered, [username] {
|
connect(view_profile_action, &QAction::triggered, [username] {
|
||||||
QDesktopServices::openUrl(
|
QDesktopServices::openUrl(QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username)));
|
||||||
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();
|
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"));
|
QAction* block_action = context_menu.addAction(tr("Block Player"));
|
||||||
|
|
||||||
block_action->setCheckable(true);
|
block_action->setCheckable(true);
|
||||||
block_action->setChecked(block_list.count(nickname) > 0);
|
block_action->setChecked(block_list.count(nickname) > 0);
|
||||||
|
|
||||||
@@ -487,41 +620,19 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
|||||||
} else {
|
} else {
|
||||||
QMessageBox::StandardButton result = QMessageBox::question(
|
QMessageBox::StandardButton result = QMessageBox::question(
|
||||||
this, tr("Block Player"),
|
this, tr("Block Player"),
|
||||||
tr("When you block a player, you will no longer receive chat messages from "
|
tr("Are you sure you would like to block %1?").arg(QString::fromStdString(nickname)),
|
||||||
"them.<br><br>Are you sure you would like to block %1?")
|
|
||||||
.arg(QString::fromStdString(nickname)),
|
|
||||||
QMessageBox::Yes | QMessageBox::No);
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
if (result == QMessageBox::Yes)
|
if (result == QMessageBox::Yes) block_list.emplace(nickname);
|
||||||
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();
|
context_menu.addSeparator();
|
||||||
|
|
||||||
QAction* kick_action = context_menu.addAction(tr("Kick"));
|
QAction* kick_action = context_menu.addAction(tr("Kick"));
|
||||||
QAction* ban_action = context_menu.addAction(tr("Ban"));
|
QAction* ban_action = context_menu.addAction(tr("Ban"));
|
||||||
|
connect(kick_action, &QAction::triggered, [this, nickname] { SendModerationRequest(Network::IdModKick, nickname); });
|
||||||
connect(kick_action, &QAction::triggered, [this, nickname] {
|
connect(ban_action, &QAction::triggered, [this, nickname] { SendModerationRequest(Network::IdModBan, 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location));
|
context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location));
|
||||||
@@ -530,57 +641,54 @@ void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
|
|||||||
void ChatRoom::UpdateTheme() {
|
void ChatRoom::UpdateTheme() {
|
||||||
QString style_sheet;
|
QString style_sheet;
|
||||||
const QString accent_color = Theme::GetAccentColor();
|
const QString accent_color = Theme::GetAccentColor();
|
||||||
|
|
||||||
if (UISettings::IsDarkTheme()) {
|
if (UISettings::IsDarkTheme()) {
|
||||||
style_sheet = QStringLiteral(R"(
|
style_sheet = QStringLiteral(R"(
|
||||||
QListView, QTextEdit, QLineEdit {
|
QListView, QTextEdit, QLineEdit { background-color: #252525; color: #E0E0E0; border: 1px solid #4A4A4A; border-radius: 4px; }
|
||||||
background-color: #252525;
|
QListView::item:selected { background-color: %1; }
|
||||||
color: #E0E0E0;
|
QPushButton { background-color: #3E3E3E; color: #E0E0E0; border: 1px solid #5A5A5A; padding: 5px; border-radius: 4px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
)").arg(accent_color);
|
)").arg(accent_color);
|
||||||
} else {
|
} else {
|
||||||
style_sheet = QStringLiteral(R"(
|
style_sheet = QStringLiteral(R"(
|
||||||
QListView, QTextEdit, QLineEdit {
|
QListView, QTextEdit, QLineEdit { background-color: #FFFFFF; color: #000000; border: 1px solid #CFCFCF; border-radius: 4px; }
|
||||||
background-color: #FFFFFF;
|
QListView::item:selected { background-color: %1; }
|
||||||
color: #000000;
|
QPushButton { background-color: #F0F0F0; color: #000000; border: 1px solid #BDBDBD; padding: 5px; border-radius: 4px; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
)").arg(accent_color);
|
)").arg(accent_color);
|
||||||
}
|
}
|
||||||
this->setStyleSheet(style_sheet);
|
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
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono> // time tracking
|
||||||
|
#include <vector> // storing timestamps
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
@@ -49,6 +51,8 @@ public slots:
|
|||||||
void OnSendChat();
|
void OnSendChat();
|
||||||
void OnChatTextChanged();
|
void OnChatTextChanged();
|
||||||
void PopupContextMenu(const QPoint& menu_location);
|
void PopupContextMenu(const QPoint& menu_location);
|
||||||
|
void OnChatContextMenu(const QPoint& menu_location);
|
||||||
|
void OnPlayerDoubleClicked(const QModelIndex& index);
|
||||||
void Disable();
|
void Disable();
|
||||||
void Enable();
|
void Enable();
|
||||||
void UpdateTheme();
|
void UpdateTheme();
|
||||||
@@ -69,7 +73,13 @@ private:
|
|||||||
std::unique_ptr<Ui::ChatRoom> ui;
|
std::unique_ptr<Ui::ChatRoom> ui;
|
||||||
std::unordered_set<std::string> block_list;
|
std::unordered_set<std::string> block_list;
|
||||||
std::unordered_map<std::string, QPixmap> icon_cache;
|
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);
|
Q_DECLARE_METATYPE(Network::ChatEntry);
|
||||||
|
|||||||
Reference in New Issue
Block a user