1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-02-03 13:01:33 +02:00

Merge pull request #2748 from Nordsoft91/lobby-chat

Advanced chat in lobby
This commit is contained in:
Nordsoft91 2023-09-07 22:01:43 +04:00 committed by GitHub
commit 43ef20efc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 482 additions and 96 deletions

View File

@ -18,6 +18,7 @@ set(launcher_SRCS
lobby/lobby.cpp
lobby/lobby_moc.cpp
lobby/lobbyroomrequest_moc.cpp
lobby/chat_moc.cpp
)
set(launcher_HEADERS
@ -39,6 +40,7 @@ set(launcher_HEADERS
lobby/lobby.h
lobby/lobby_moc.h
lobby/lobbyroomrequest_moc.h
lobby/chat_moc.h
main.h
)
@ -52,6 +54,7 @@ set(launcher_FORMS
updatedialog_moc.ui
lobby/lobby_moc.ui
lobby/lobbyroomrequest_moc.ui
lobby/chat_moc.ui
)
set(launcher_TS

173
launcher/lobby/chat_moc.cpp Normal file
View File

@ -0,0 +1,173 @@
/*
* chat_moc.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "chat_moc.h"
#include "ui_chat_moc.h"
Chat::Chat(QWidget *parent) :
QWidget(parent),
ui(new Ui::Chat)
{
ui->setupUi(this);
namesCompleter.setModel(ui->listUsers->model());
namesCompleter.setCompletionMode(QCompleter::InlineCompletion);
ui->messageEdit->setCompleter(&namesCompleter);
for([[maybe_unused]] auto i : {GLOBAL, ROOM})
chatDocuments.push_back(new QTextDocument(this));
setChatId(GLOBAL);
}
Chat::~Chat()
{
delete ui;
}
void Chat::setUsername(const QString & user)
{
username = user;
}
void Chat::setSession(const QString & s)
{
session = s;
on_chatSwitch_clicked();
}
void Chat::setChannel(const QString & channel)
{
static const QMap<QString, ChatId> chatNames{{"global", GLOBAL}, {"room", ROOM}};
setChatId(chatNames.value(channel));
}
void Chat::addUser(const QString & user)
{
ui->listUsers->addItem(new QListWidgetItem("@" + user));
}
void Chat::clearUsers()
{
ui->listUsers->clear();
}
void Chat::chatMessage(const QString & title, const QString & channel, QString body, bool isSystem)
{
const QTextCharFormat regularFormat;
const QString boldHtml = "<b>%1</b>";
const QString colorHtml = "<font color=\"%1\">%2</font>";
bool meMentioned = false;
bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24);
static const QMap<QString, ChatId> chatNames{{"global", GLOBAL}, {"room", ROOM}};
QTextDocument * doc = ui->chat->document();
if(chatNames.contains(channel))
doc = chatDocuments[chatNames.value(channel)];
QTextCursor curs(doc);
curs.movePosition(QTextCursor::End);
QString titleColor = "Olive";
if(isSystem || title == "System")
titleColor = "ForestGreen";
if(title == username)
titleColor = "Gold";
curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": ")));
QRegularExpression mentionRe("@[\\w\\d]+");
auto subBody = body;
int mem = 0;
for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody))
{
body.insert(mem + match.capturedEnd(), QChar(-1));
body.insert(mem + match.capturedStart(), QChar(-1));
mem += match.capturedEnd() + 2;
subBody = body.right(body.size() - mem);
}
auto pieces = body.split(QChar(-1));
for(auto & block : pieces)
{
if(block.startsWith("@"))
{
if(block == "@" + username)
{
meMentioned = true;
curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block)));
}
else
curs.insertHtml(colorHtml.arg("DeepSkyBlue", block));
}
else
{
if(isSystem)
curs.insertHtml(colorHtml.arg("ForestGreen", block));
else
curs.insertText(block, regularFormat);
}
}
curs.insertText("\n", regularFormat);
if(doc == ui->chat->document() && (meMentioned || isScrollBarBottom))
{
ui->chat->ensureCursorVisible();
ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum());
}
}
void Chat::chatMessage(const QString & title, QString body, bool isSystem)
{
chatMessage(title, "", body, isSystem);
}
void Chat::sysMessage(QString body)
{
chatMessage("System", body, true);
}
void Chat::sendMessage()
{
QString msg(ui->messageEdit->text());
ui->messageEdit->clear();
emit messageSent(msg);
}
void Chat::on_messageEdit_returnPressed()
{
sendMessage();
}
void Chat::on_sendButton_clicked()
{
sendMessage();
}
void Chat::on_chatSwitch_clicked()
{
static const QMap<ChatId, QString> chatNames{{GLOBAL, "global"}, {ROOM, "room"}};
if(chatId == GLOBAL && !session.isEmpty())
emit channelSwitch(chatNames[ROOM]);
else
emit channelSwitch(chatNames[GLOBAL]);
}
void Chat::setChatId(ChatId _chatId)
{
static const QMap<ChatId, QString> chatNames{{GLOBAL, "Global"}, {ROOM, "Room"}};
chatId = _chatId;
ui->chatSwitch->setText(chatNames[chatId] + " chat");
ui->chat->setDocument(chatDocuments[chatId]);
}

69
launcher/lobby/chat_moc.h Normal file
View File

@ -0,0 +1,69 @@
/*
* chat_moc.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include <QWidget>
#include <QCompleter>
namespace Ui {
class Chat;
}
class Chat : public QWidget
{
Q_OBJECT
enum ChatId
{
GLOBAL = 0,
ROOM
};
QCompleter namesCompleter;
QString username, session;
ChatId chatId = GLOBAL;
QVector<QTextDocument*> chatDocuments;
private:
void setChatId(ChatId);
void sendMessage();
public:
explicit Chat(QWidget *parent = nullptr);
~Chat();
void setUsername(const QString &);
void setSession(const QString &);
void setChannel(const QString &);
void clearUsers();
void addUser(const QString & user);
void chatMessage(const QString & title, const QString & channel, QString body, bool isSystem = false);
void chatMessage(const QString & title, QString body, bool isSystem = false);
signals:
void messageSent(QString);
void channelSwitch(QString);
public slots:
void sysMessage(QString body);
private slots:
void on_messageEdit_returnPressed();
void on_sendButton_clicked();
void on_chatSwitch_clicked();
private:
Ui::Chat *ui;
};

121
launcher/lobby/chat_moc.ui Normal file
View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Chat</class>
<widget class="QWidget" name="Chat">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>465</width>
<height>413</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout2">
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Users in lobby</string>
</property>
<property name="indent">
<number>-1</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="chatSwitch">
<property name="text">
<string>Global chat</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="listUsers">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>96</height>
</size>
</property>
<property name="midLineWidth">
<number>0</number>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="layoutMode">
<enum>QListView::SinglePass</enum>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="chat"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>-1</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="messageEdit">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>type you message</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sendButton">
<property name="text">
<string>send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -12,7 +12,7 @@
#include <QTcpSocket>
#include <QAbstractSocket>
const unsigned int ProtocolVersion = 4;
const unsigned int ProtocolVersion = 5;
const std::string ProtocolEncoding = "utf8";
class ProtocolError: public std::runtime_error
@ -24,10 +24,10 @@ public:
enum ProtocolConsts
{
//client consts
GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE,
GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, SETCHANNEL,
//server consts
SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE
SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, CHATCHANNEL, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE, CHANNEL
};
const QMap<ProtocolConsts, QString> ProtocolStrings
@ -88,6 +88,10 @@ const QMap<ProtocolConsts, QString> ProtocolStrings
//host sets game mode (new game or load game)
//%1: game mode - 0 for new game, 1 for load game
{HOSTMODE, "<HOSTMODE>%1"},
//set new chat channel
//%1: channel name
{SETCHANNEL, "<CHANNEL>%1"},
//=== server commands ===
//server commands are started from :>>, arguments are enumerated by : symbol
@ -149,9 +153,16 @@ const QMap<ProtocolConsts, QString> ProtocolStrings
//received chat message
//arg[0]: sender username
//arg[1]: message text
//arg[1]: channel
//arg[2]: message text
{CHAT, "MSG"},
//received chat message to specific channel
//arg[0]: sender username
//arg[1]: channel
//arg[2]: message text
{CHATCHANNEL, "MSGCH"},
//list of users currently in lobby
//arg[0]: amount of players, following arguments depend on it
//arg[x]: username
@ -164,6 +175,10 @@ const QMap<ProtocolConsts, QString> ProtocolStrings
//game mode (new game or load game) set by host
//arg[0]: game mode
{GAMEMODE, "GAMEMODE"},
//chat channel changed
//arg[0]: channel name
{CHANNEL, "CHANNEL"},
};
class ServerCommand

View File

@ -34,9 +34,11 @@ Lobby::Lobby(QWidget *parent) :
{
ui->setupUi(this);
connect(&socketLobby, SIGNAL(text(QString)), this, SLOT(sysMessage(QString)));
connect(&socketLobby, SIGNAL(text(QString)), ui->chatWidget, SLOT(sysMessage(QString)));
connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString)));
connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected()));
connect(ui->chatWidget, SIGNAL(messageSent(QString)), this, SLOT(onMessageSent(QString)));
connect(ui->chatWidget, SIGNAL(channelSwitch(QString)), this, SLOT(onChannelSwitch(QString)));
QString hostString("%1:%2");
hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String()));
@ -110,7 +112,7 @@ void Lobby::serverCommand(const ServerCommand & command) try
{
case SRVERROR:
protocolAssert(args.size());
chatMessage("System error", args[0], true);
ui->chatWidget->chatMessage("System error", args[0], true);
if(authentificationStatus == AuthStatus::AUTH_NONE)
authentificationStatus = AuthStatus::AUTH_ERROR;
break;
@ -119,7 +121,7 @@ void Lobby::serverCommand(const ServerCommand & command) try
protocolAssert(args.size());
hostSession = args[0];
session = args[0];
sysMessage("new session started");
ui->chatWidget->setSession(session);
break;
case SESSIONS:
@ -151,15 +153,15 @@ void Lobby::serverCommand(const ServerCommand & command) try
case JOINED:
case KICKED:
protocolAssert(args.size() == 2);
joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2");
session = "";
ui->chatWidget->setSession(session);
if(args[1] == username)
{
hostModsMap.clear();
ui->buttonReady->setText("Ready");
ui->optNewGame->setChecked(true);
sysMessage(joinStr.arg("you", args[0]));
session = args[0];
ui->chatWidget->setSession(session);
bool isHost = command.command == JOINED && hostSession == session;
ui->optNewGame->setEnabled(isHost);
ui->optLoadGame->setEnabled(isHost);
@ -167,7 +169,8 @@ void Lobby::serverCommand(const ServerCommand & command) try
}
else
{
sysMessage(joinStr.arg(args[1], args[0]));
joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2");
ui->chatWidget->sysMessage(joinStr.arg(args[1], args[0]));
}
break;
@ -244,7 +247,22 @@ void Lobby::serverCommand(const ServerCommand & command) try
QString msg;
for(int i = 1; i < args.size(); ++i)
msg += args[i];
chatMessage(args[0], msg);
ui->chatWidget->chatMessage(args[0], msg);
break;
}
case CHATCHANNEL: {
protocolAssert(args.size() > 2);
QString msg;
for(int i = 2; i < args.size(); ++i)
msg += args[i];
ui->chatWidget->chatMessage(args[0], args[1], msg);
break;
}
case CHANNEL: {
protocolAssert(args.size() == 1);
ui->chatWidget->setChannel(args[0]);
break;
}
@ -258,10 +276,10 @@ void Lobby::serverCommand(const ServerCommand & command) try
amount = args[0].toInt();
protocolAssert(amount == (args.size() - 1));
ui->listUsers->clear();
ui->chatWidget->clearUsers();
for(int i = 0; i < amount; ++i)
{
ui->listUsers->addItem(new QListWidgetItem(args[i + 1]));
ui->chatWidget->addUser(args[i + 1]);
}
break;
}
@ -277,7 +295,7 @@ void Lobby::serverCommand(const ServerCommand & command) try
}
default:
sysMessage("Unknown server command");
ui->chatWidget->sysMessage("Unknown server command");
}
if(authentificationStatus == AuthStatus::AUTH_ERROR)
@ -292,7 +310,7 @@ void Lobby::serverCommand(const ServerCommand & command) try
}
catch(const ProtocolError & e)
{
chatMessage("System error", e.what(), true);
ui->chatWidget->chatMessage("System error", e.what(), true);
}
void Lobby::dispatchMessage(QString txt) try
@ -316,12 +334,14 @@ void Lobby::dispatchMessage(QString txt) try
}
catch(const ProtocolError & e)
{
chatMessage("System error", e.what(), true);
ui->chatWidget->chatMessage("System error", e.what(), true);
}
void Lobby::onDisconnected()
{
authentificationStatus = AuthStatus::AUTH_NONE;
session = "";
ui->chatWidget->setSession(session);
ui->stackedWidget->setCurrentWidget(ui->sessionsPage);
ui->connectButton->setChecked(false);
ui->serverEdit->setEnabled(true);
@ -331,37 +351,12 @@ void Lobby::onDisconnected()
ui->sessionsTable->setRowCount(0);
}
void Lobby::chatMessage(QString title, QString body, bool isSystem)
{
QTextCharFormat fmtBody, fmtTitle;
fmtTitle.setFontWeight(QFont::Bold);
if(isSystem)
fmtBody.setFontWeight(QFont::DemiBold);
QTextCursor curs(ui->chat->document());
curs.movePosition(QTextCursor::End);
curs.insertText(title + ": ", fmtTitle);
curs.insertText(body + "\n", fmtBody);
ui->chat->ensureCursorVisible();
}
void Lobby::sysMessage(QString body)
{
chatMessage("System", body, true);
}
void Lobby::protocolAssert(bool expr)
{
if(!expr)
throw ProtocolError("Protocol error");
}
void Lobby::on_messageEdit_returnPressed()
{
socketLobby.send(ProtocolStrings[MESSAGE].arg(ui->messageEdit->text()));
ui->messageEdit->clear();
}
void Lobby::on_connectButton_toggled(bool checked)
{
if(checked)
@ -369,6 +364,7 @@ void Lobby::on_connectButton_toggled(bool checked)
ui->connectButton->setText(tr("Disconnect"));
authentificationStatus = AuthStatus::AUTH_NONE;
username = ui->userEdit->text();
ui->chatWidget->setUsername(username);
const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer();
auto serverStrings = ui->serverEdit->text().split(":");
@ -389,9 +385,9 @@ void Lobby::on_connectButton_toggled(bool checked)
ui->serverEdit->setEnabled(false);
ui->userEdit->setEnabled(false);
sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort));
ui->chatWidget->sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort));
//show text immediately
ui->chat->repaint();
ui->chatWidget->repaint();
qApp->processEvents();
socketLobby.connectServer(serverUrl, serverPort, username, connectionTimeout);
@ -401,7 +397,7 @@ void Lobby::on_connectButton_toggled(bool checked)
ui->connectButton->setText(tr("Connect"));
ui->serverEdit->setEnabled(true);
ui->userEdit->setEnabled(true);
ui->listUsers->clear();
ui->chatWidget->clearUsers();
hostModsMap.clear();
updateMods();
socketLobby.disconnectServer();
@ -564,3 +560,12 @@ void Lobby::on_optLoadGame_toggled(bool checked)
}
}
void Lobby::onMessageSent(QString message)
{
socketLobby.send(ProtocolStrings[MESSAGE].arg(message));
}
void Lobby::onChannelSwitch(QString channel)
{
socketLobby.send(ProtocolStrings[SETCHANNEL].arg(channel));
}

View File

@ -33,12 +33,10 @@ public slots:
void updateMods();
private slots:
void on_messageEdit_returnPressed();
void chatMessage(QString title, QString body, bool isSystem = false);
void sysMessage(QString body);
void dispatchMessage(QString);
void serverCommand(const ServerCommand &);
void onMessageSent(QString message);
void onChannelSwitch(QString channel);
void on_connectButton_toggled(bool checked);

View File

@ -14,6 +14,9 @@
<string/>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="3">
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
@ -62,69 +65,33 @@
</item>
<item row="1" column="0" colspan="6">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>-1</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Players in lobby</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="listUsers">
<widget class="Chat" name="chatWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>96</height>
</size>
</property>
<property name="midLineWidth">
<number>0</number>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="layoutMode">
<enum>QListView::SinglePass</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Lobby chat</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="chat">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="messageEdit"/>
</item>
</layout>
</item>
<item>
@ -140,6 +107,18 @@
</property>
<widget class="QWidget" name="sessionsPage">
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QPushButton" name="newButton">
<property name="enabled">
@ -210,6 +189,21 @@
</widget>
<widget class="QWidget" name="roomPage">
<layout class="QGridLayout" name="gridLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>-1</number>
</property>
<item row="1" column="1">
<widget class="QPushButton" name="kickButton">
<property name="text">
@ -304,6 +298,14 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Chat</class>
<extends>QWidget</extends>
<header>lobby/chat_moc.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>