From 69f7b3169e3e69c7c3abd839a27ab3159eaebb76 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 25 Mar 2024 18:04:44 +0200 Subject: [PATCH] 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 --- Mods/vcmi/config/vcmi/english.json | 4 +++- client/globalLobby/GlobalLobbyClient.cpp | 21 ++++++++++++++----- .../globalLobby/GlobalLobbyInviteWindow.cpp | 2 +- client/globalLobby/GlobalLobbyWidget.cpp | 6 ++++++ client/globalLobby/GlobalLobbyWindow.cpp | 20 +++++++++++++++++- client/globalLobby/GlobalLobbyWindow.h | 3 +++ client/widgets/ObjectLists.cpp | 8 ++++++- lobby/LobbyDatabase.cpp | 19 +++++++++++++++-- lobby/LobbyDatabase.h | 1 + lobby/LobbyServer.cpp | 8 +++---- 10 files changed, 77 insertions(+), 15 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 3381a9bb3..1689c8786 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -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.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.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.load" : "Load Game", "vcmi.lobby.room.type" : "Room Type", @@ -103,6 +104,7 @@ "vcmi.lobby.room.state.public" : "Public", "vcmi.lobby.room.state.private" : "Private", "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.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.", diff --git a/client/globalLobby/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp index bee0156e3..ecdb3c5fc 100644 --- a/client/globalLobby/GlobalLobbyClient.cpp +++ b/client/globalLobby/GlobalLobbyClient.cpp @@ -15,12 +15,13 @@ #include "GlobalLobbyLoginWindow.h" #include "GlobalLobbyWindow.h" +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" -#include "../windows/InfoWindows.h" -#include "../CServerHandler.h" #include "../mainmenu/CMainMenu.h" -#include "../CGameInfo.h" +#include "../windows/InfoWindows.h" #include "../../lib/CConfigHandler.h" #include "../../lib/MetaString.h" @@ -189,6 +190,8 @@ void GlobalLobbyClient::receiveChatMessage(const JsonNode & json) auto lobbyWindowPtr = lobbyWindow.lock(); if(lobbyWindowPtr) lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName); + + CCS->soundh->playSound(AudioPath::builtin("CHAT")); } void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json) @@ -280,10 +283,18 @@ void GlobalLobbyClient::receiveMatchesHistory(const JsonNode & json) void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json) { auto lobbyWindowPtr = lobbyWindow.lock(); + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = json["accountID"].String(); 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) diff --git a/client/globalLobby/GlobalLobbyInviteWindow.cpp b/client/globalLobby/GlobalLobbyInviteWindow.cpp index 0799e940c..5802257cb 100644 --- a/client/globalLobby/GlobalLobbyInviteWindow.cpp +++ b/client/globalLobby/GlobalLobbyInviteWindow.cpp @@ -57,7 +57,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow() filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); filledBackground->playerColored(PlayerColor(1)); labelTitle = std::make_shared( - 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 diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index 34ddfba69..2c236f936 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -14,6 +14,8 @@ #include "GlobalLobbyClient.h" #include "GlobalLobbyWindow.h" +#include "../CGameInfo.h" +#include "../CMusicHandler.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" @@ -186,18 +188,22 @@ GlobalLobbyChannelCardBase::GlobalLobbyChannelCardBase(GlobalLobbyWindow * windo if (window->isChannelOpen(channelType, channelName)) backgroundOverlay = std::make_shared(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(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1); else backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); } void GlobalLobbyChannelCardBase::clickPressed(const Point & cursorPosition) { + CCS->soundh->playSound(soundBase::button); window->doOpenChannel(channelType, channelName, channelDescription); } GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription) : GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName) { + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); } diff --git a/client/globalLobby/GlobalLobbyWindow.cpp b/client/globalLobby/GlobalLobbyWindow.cpp index 5d189f3a1..65632dbfe 100644 --- a/client/globalLobby/GlobalLobbyWindow.cpp +++ b/client/globalLobby/GlobalLobbyWindow.cpp @@ -49,6 +49,7 @@ void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std currentChannelType = channelType; currentChannelName = channelName; chatHistory.clear(); + unreadChannels.erase(channelType + "_" + channelName); widget->getGameChat()->setText(""); 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) { 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; chatMessageFormatted.appendRawString("[%s] {%s}: %s\n"); @@ -123,6 +131,11 @@ void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std: 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 & accounts) { if (accounts.size() == widget->getAccountList()->size()) @@ -159,6 +172,11 @@ void GlobalLobbyWindow::onMatchesHistory(const std::vector & hi widget->getMatchListHeader()->setText(text.toString()); } +void GlobalLobbyWindow::onInviteReceived(const std::string & invitedRoomID, const std::string & invitedByAccountID) +{ + widget->getRoomList()->reset(); +} + void GlobalLobbyWindow::onJoinedRoom() { widget->getAccountList()->reset(); diff --git a/client/globalLobby/GlobalLobbyWindow.h b/client/globalLobby/GlobalLobbyWindow.h index 537288979..c013edb47 100644 --- a/client/globalLobby/GlobalLobbyWindow.h +++ b/client/globalLobby/GlobalLobbyWindow.h @@ -22,6 +22,7 @@ class GlobalLobbyWindow : public CWindowObject std::string currentChannelName; std::shared_ptr widget; + std::set unreadChannels; public: GlobalLobbyWindow(); @@ -36,6 +37,7 @@ public: /// 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 isChannelUnread(const std::string & channelType, const std::string & channelName); // Callbacks for network packs @@ -43,6 +45,7 @@ public: void onActiveAccounts(const std::vector & accounts); void onActiveRooms(const std::vector & rooms); void onMatchesHistory(const std::vector & history); + void onInviteReceived(const std::string & invitedRoomID, const std::string & invitedByAccountID); void onJoinedRoom(); void onLeftRoom(); }; diff --git a/client/widgets/ObjectLists.cpp b/client/widgets/ObjectLists.cpp index e86b473e5..ec3e1560e 100644 --- a/client/widgets/ObjectLists.cpp +++ b/client/widgets/ObjectLists.cpp @@ -66,6 +66,7 @@ size_t CTabbedInt::getActive() const void CTabbedInt::reset() { + deleteItem(activeTab); activeTab = createItem(activeID); activeTab->moveTo(pos.topLeft()); @@ -127,6 +128,11 @@ void CListBox::updatePositions() 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; for (auto & elem : items) { @@ -279,4 +285,4 @@ void CListBoxWithCallback::moveToPrev() CListBox::moveToPrev(); if(movedPosCallback) movedPosCallback(getPos()); -} \ No newline at end of file +} diff --git a/lobby/LobbyDatabase.cpp b/lobby/LobbyDatabase.cpp index 940ccbf3a..a9170e747 100644 --- a/lobby/LobbyDatabase.cpp +++ b/lobby/LobbyDatabase.cpp @@ -198,6 +198,12 @@ void LobbyDatabase::prepareStatements() WHERE roomID = ? )"); + getAccountInviteStatusStatement = database->prepare(R"( + SELECT COUNT(accountID) + FROM gameRoomInvites + WHERE accountID = ? AND roomID = ? + )"); + getAccountGameHistoryStatement = database->prepare(R"( SELECT gr.roomID, hostAccountID, displayName, description, status, playerLimit, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',gr.creationTime) AS secondsElapsed 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) { - assert(0); - return {}; + int result = 0; + + 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) diff --git a/lobby/LobbyDatabase.h b/lobby/LobbyDatabase.h index ba279bb3a..2fa0e41b6 100644 --- a/lobby/LobbyDatabase.h +++ b/lobby/LobbyDatabase.h @@ -44,6 +44,7 @@ class LobbyDatabase SQLiteStatementPtr getAccountGameHistoryStatement; SQLiteStatementPtr getActiveGameRoomsStatement; SQLiteStatementPtr getActiveAccountsStatement; + SQLiteStatementPtr getAccountInviteStatusStatement; SQLiteStatementPtr getAccountGameRoomStatement; SQLiteStatementPtr getAccountDisplayNameStatement; SQLiteStatementPtr countRoomUsedSlotsStatement; diff --git a/lobby/LobbyServer.cpp b/lobby/LobbyServer.cpp index 10692e9a5..12c4d41ec 100644 --- a/lobby/LobbyServer.cpp +++ b/lobby/LobbyServer.cpp @@ -761,10 +761,10 @@ void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, con std::string accountID = json["accountID"].String(); std::string gameRoomID = database->getAccountGameRoom(senderName); - auto targetAccount = findAccount(accountID); + auto targetAccountConnection = findAccount(accountID); - if(!targetAccount) - return sendOperationFailed(connection, "Invalid account to invite!"); + if(!targetAccountConnection) + return sendOperationFailed(connection, "Player is offline or does not exists!"); if(!database->isPlayerInGameRoom(senderName)) 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!"); database->insertGameRoomInvite(accountID, gameRoomID); - sendInviteReceived(targetAccount, senderName, gameRoomID); + sendInviteReceived(targetAccountConnection, senderName, gameRoomID); } LobbyServer::~LobbyServer() = default;