1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-07-17 01:32:21 +02:00

UI improvements for lobby:

- Added notifications sounds for invites and chat messages
- Added notifications for unread chat messages in inactive channels
- Added click sound when switching between channels
- Added workaround to prevent clicks due to list recreation
- Partial support for receiving invites
This commit is contained in:
Ivan Savenko
2024-03-25 18:04:44 +02:00
parent 536156dd92
commit 69f7b3169e
10 changed files with 77 additions and 15 deletions

View File

@ -95,7 +95,8 @@
"vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.", "vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.",
"vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.", "vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.",
"vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.", "vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.",
"vcmi.lobby.room.invite" : "Invite Players", "vcmi.lobby.invite.header" : "Invite Players",
"vcmi.lobby.invite.notification" : "Player has invited you to their game room. You can now join their private room.",
"vcmi.lobby.room.new" : "New Game", "vcmi.lobby.room.new" : "New Game",
"vcmi.lobby.room.load" : "Load Game", "vcmi.lobby.room.load" : "Load Game",
"vcmi.lobby.room.type" : "Room Type", "vcmi.lobby.room.type" : "Room Type",
@ -103,6 +104,7 @@
"vcmi.lobby.room.state.public" : "Public", "vcmi.lobby.room.state.public" : "Public",
"vcmi.lobby.room.state.private" : "Private", "vcmi.lobby.room.state.private" : "Private",
"vcmi.lobby.room.state.busy" : "In Game", "vcmi.lobby.room.state.busy" : "In Game",
"vcmi.lobby.room.state.invited" : "Invited",
"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s", "vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.", "vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",

View File

@ -15,12 +15,13 @@
#include "GlobalLobbyLoginWindow.h" #include "GlobalLobbyLoginWindow.h"
#include "GlobalLobbyWindow.h" #include "GlobalLobbyWindow.h"
#include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CServerHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../windows/InfoWindows.h"
#include "../CServerHandler.h"
#include "../mainmenu/CMainMenu.h" #include "../mainmenu/CMainMenu.h"
#include "../CGameInfo.h" #include "../windows/InfoWindows.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/MetaString.h" #include "../../lib/MetaString.h"
@ -189,6 +190,8 @@ void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
auto lobbyWindowPtr = lobbyWindow.lock(); auto lobbyWindowPtr = lobbyWindow.lock();
if(lobbyWindowPtr) if(lobbyWindowPtr)
lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName); lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
CCS->soundh->playSound(AudioPath::builtin("CHAT"));
} }
void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json) void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json)
@ -280,10 +283,18 @@ void GlobalLobbyClient::receiveMatchesHistory(const JsonNode & json)
void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json) void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
{ {
auto lobbyWindowPtr = lobbyWindow.lock(); auto lobbyWindowPtr = lobbyWindow.lock();
std::string gameRoomID = json["gameRoomID"].String();
std::string accountID = json["accountID"].String();
if(lobbyWindowPtr) if(lobbyWindowPtr)
lobbyWindowPtr->onMatchesHistory(activeRooms); {
std::string message = MetaString::createFromTextID("vcmi.lobby.invite.notification").toString();
std::string time = getCurrentTimeFormatted();
assert(0); //TODO lobbyWindowPtr->onGameChatMessage("System", message, time, "player", accountID);
lobbyWindowPtr->onInviteReceived(gameRoomID, accountID);
}
CCS->soundh->playSound(AudioPath::builtin("CHAT"));
} }
void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json) void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)

View File

@ -57,7 +57,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
filledBackground->playerColored(PlayerColor(1)); filledBackground->playerColored(PlayerColor(1));
labelTitle = std::make_shared<CLabel>( labelTitle = std::make_shared<CLabel>(
pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.room.invite").toString() pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.invite.header").toString()
); );
const auto & createAccountCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject> const auto & createAccountCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>

View File

@ -14,6 +14,8 @@
#include "GlobalLobbyClient.h" #include "GlobalLobbyClient.h"
#include "GlobalLobbyWindow.h" #include "GlobalLobbyWindow.h"
#include "../CGameInfo.h"
#include "../CMusicHandler.h"
#include "../CServerHandler.h" #include "../CServerHandler.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
@ -186,18 +188,22 @@ GlobalLobbyChannelCardBase::GlobalLobbyChannelCardBase(GlobalLobbyWindow * windo
if (window->isChannelOpen(channelType, channelName)) if (window->isChannelOpen(channelType, channelName))
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::YELLOW, 2); backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::YELLOW, 2);
else if (window->isChannelUnread(channelType, channelName))
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1);
else else
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
} }
void GlobalLobbyChannelCardBase::clickPressed(const Point & cursorPosition) void GlobalLobbyChannelCardBase::clickPressed(const Point & cursorPosition)
{ {
CCS->soundh->playSound(soundBase::button);
window->doOpenChannel(channelType, channelName, channelDescription); window->doOpenChannel(channelType, channelName, channelDescription);
} }
GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription) GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription)
: GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName) : GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName)
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName); labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
} }

View File

@ -49,6 +49,7 @@ void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std
currentChannelType = channelType; currentChannelType = channelType;
currentChannelName = channelName; currentChannelName = channelName;
chatHistory.clear(); chatHistory.clear();
unreadChannels.erase(channelType + "_" + channelName);
widget->getGameChat()->setText(""); widget->getGameChat()->setText("");
auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName); auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName);
@ -110,7 +111,14 @@ void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName) void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName)
{ {
if (channelType != currentChannelType || channelName != currentChannelName) if (channelType != currentChannelType || channelName != currentChannelName)
return; // TODO: send ping to player that another channel got a new message {
// mark channel as unread
unreadChannels.insert(channelType + "_" + channelName);
widget->getAccountList()->reset();
widget->getChannelList()->reset();
widget->getMatchList()->reset();
return;
}
MetaString chatMessageFormatted; MetaString chatMessageFormatted;
chatMessageFormatted.appendRawString("[%s] {%s}: %s\n"); chatMessageFormatted.appendRawString("[%s] {%s}: %s\n");
@ -123,6 +131,11 @@ void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std:
widget->getGameChat()->setText(chatHistory); widget->getGameChat()->setText(chatHistory);
} }
bool GlobalLobbyWindow::isChannelUnread(const std::string & channelType, const std::string & channelName)
{
return unreadChannels.count(channelType + "_" + channelName) > 0;
}
void GlobalLobbyWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts) void GlobalLobbyWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts)
{ {
if (accounts.size() == widget->getAccountList()->size()) if (accounts.size() == widget->getAccountList()->size())
@ -159,6 +172,11 @@ void GlobalLobbyWindow::onMatchesHistory(const std::vector<GlobalLobbyRoom> & hi
widget->getMatchListHeader()->setText(text.toString()); widget->getMatchListHeader()->setText(text.toString());
} }
void GlobalLobbyWindow::onInviteReceived(const std::string & invitedRoomID, const std::string & invitedByAccountID)
{
widget->getRoomList()->reset();
}
void GlobalLobbyWindow::onJoinedRoom() void GlobalLobbyWindow::onJoinedRoom()
{ {
widget->getAccountList()->reset(); widget->getAccountList()->reset();

View File

@ -22,6 +22,7 @@ class GlobalLobbyWindow : public CWindowObject
std::string currentChannelName; std::string currentChannelName;
std::shared_ptr<GlobalLobbyWidget> widget; std::shared_ptr<GlobalLobbyWidget> widget;
std::set<std::string> unreadChannels;
public: public:
GlobalLobbyWindow(); GlobalLobbyWindow();
@ -36,6 +37,7 @@ public:
/// Returns true if provided chat channel is the one that is currently open in UI /// Returns true if provided chat channel is the one that is currently open in UI
bool isChannelOpen(const std::string & channelType, const std::string & channelName); bool isChannelOpen(const std::string & channelType, const std::string & channelName);
bool isChannelUnread(const std::string & channelType, const std::string & channelName);
// Callbacks for network packs // Callbacks for network packs
@ -43,6 +45,7 @@ public:
void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts); void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts);
void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms); void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history); void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);
void onInviteReceived(const std::string & invitedRoomID, const std::string & invitedByAccountID);
void onJoinedRoom(); void onJoinedRoom();
void onLeftRoom(); void onLeftRoom();
}; };

View File

@ -66,6 +66,7 @@ size_t CTabbedInt::getActive() const
void CTabbedInt::reset() void CTabbedInt::reset()
{ {
deleteItem(activeTab); deleteItem(activeTab);
activeTab = createItem(activeID); activeTab = createItem(activeID);
activeTab->moveTo(pos.topLeft()); activeTab->moveTo(pos.topLeft());
@ -127,6 +128,11 @@ void CListBox::updatePositions()
void CListBox::reset() void CListBox::reset()
{ {
// hack to ensure that all items will be recreated with new address
// save current item list so all shared_ptr's will be destroyed only on scope exit and not inside loop below
// see comment in EventDispatcher::handleLeftButtonClick for details on why this hack is needed
auto itemsCopy = items;
size_t current = first; size_t current = first;
for (auto & elem : items) for (auto & elem : items)
{ {
@ -279,4 +285,4 @@ void CListBoxWithCallback::moveToPrev()
CListBox::moveToPrev(); CListBox::moveToPrev();
if(movedPosCallback) if(movedPosCallback)
movedPosCallback(getPos()); movedPosCallback(getPos());
} }

View File

@ -198,6 +198,12 @@ void LobbyDatabase::prepareStatements()
WHERE roomID = ? WHERE roomID = ?
)"); )");
getAccountInviteStatusStatement = database->prepare(R"(
SELECT COUNT(accountID)
FROM gameRoomInvites
WHERE accountID = ? AND roomID = ?
)");
getAccountGameHistoryStatement = database->prepare(R"( getAccountGameHistoryStatement = database->prepare(R"(
SELECT gr.roomID, hostAccountID, displayName, description, status, playerLimit, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime) AS secondsElapsed SELECT gr.roomID, hostAccountID, displayName, description, status, playerLimit, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime) AS secondsElapsed
FROM gameRoomPlayers grp FROM gameRoomPlayers grp
@ -438,8 +444,17 @@ LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & acco
LobbyInviteStatus LobbyDatabase::getAccountInviteStatus(const std::string & accountID, const std::string & roomID) LobbyInviteStatus LobbyDatabase::getAccountInviteStatus(const std::string & accountID, const std::string & roomID)
{ {
assert(0); int result = 0;
return {};
getAccountInviteStatusStatement->setBinds(accountID, roomID);
if(getAccountInviteStatusStatement->execute())
getAccountInviteStatusStatement->getColumns(result);
getAccountInviteStatusStatement->reset();
if (result > 0)
return LobbyInviteStatus::INVITED;
else
return LobbyInviteStatus::NOT_INVITED;
} }
LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID) LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID)

View File

@ -44,6 +44,7 @@ class LobbyDatabase
SQLiteStatementPtr getAccountGameHistoryStatement; SQLiteStatementPtr getAccountGameHistoryStatement;
SQLiteStatementPtr getActiveGameRoomsStatement; SQLiteStatementPtr getActiveGameRoomsStatement;
SQLiteStatementPtr getActiveAccountsStatement; SQLiteStatementPtr getActiveAccountsStatement;
SQLiteStatementPtr getAccountInviteStatusStatement;
SQLiteStatementPtr getAccountGameRoomStatement; SQLiteStatementPtr getAccountGameRoomStatement;
SQLiteStatementPtr getAccountDisplayNameStatement; SQLiteStatementPtr getAccountDisplayNameStatement;
SQLiteStatementPtr countRoomUsedSlotsStatement; SQLiteStatementPtr countRoomUsedSlotsStatement;

View File

@ -761,10 +761,10 @@ void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, con
std::string accountID = json["accountID"].String(); std::string accountID = json["accountID"].String();
std::string gameRoomID = database->getAccountGameRoom(senderName); std::string gameRoomID = database->getAccountGameRoom(senderName);
auto targetAccount = findAccount(accountID); auto targetAccountConnection = findAccount(accountID);
if(!targetAccount) if(!targetAccountConnection)
return sendOperationFailed(connection, "Invalid account to invite!"); return sendOperationFailed(connection, "Player is offline or does not exists!");
if(!database->isPlayerInGameRoom(senderName)) if(!database->isPlayerInGameRoom(senderName))
return sendOperationFailed(connection, "You are not in the room!"); return sendOperationFailed(connection, "You are not in the room!");
@ -776,7 +776,7 @@ void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, con
return sendOperationFailed(connection, "This player is already invited!"); return sendOperationFailed(connection, "This player is already invited!");
database->insertGameRoomInvite(accountID, gameRoomID); database->insertGameRoomInvite(accountID, gameRoomID);
sendInviteReceived(targetAccount, senderName, gameRoomID); sendInviteReceived(targetAccountConnection, senderName, gameRoomID);
} }
LobbyServer::~LobbyServer() = default; LobbyServer::~LobbyServer() = default;