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:
Collecting
2026-01-11 20:22:52 +00:00
2 changed files with 251 additions and 133 deletions

View File

@@ -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'>&lt;%3&gt;</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'>&lt;%3&gt;</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();
}
}

View File

@@ -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);