瀏覽代碼

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
Ivan Savenko 1 年之前
父節點
當前提交
69f7b3169e

+ 3 - 1
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.",

+ 16 - 5
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();
+
+		lobbyWindowPtr->onGameChatMessage("System", message, time, "player", accountID);
+		lobbyWindowPtr->onInviteReceived(gameRoomID, accountID);
+	}
 
-	assert(0); //TODO
+	CCS->soundh->playSound(AudioPath::builtin("CHAT"));
 }
 
 void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)

+ 1 - 1
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -57,7 +57,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	filledBackground->playerColored(PlayerColor(1));
 	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>

+ 6 - 0
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<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
 		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)
 {
+	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<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);
 }

+ 19 - 1
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<GlobalLobbyAccount> & accounts)
 {
 	if (accounts.size() == widget->getAccountList()->size())
@@ -159,6 +172,11 @@ void GlobalLobbyWindow::onMatchesHistory(const std::vector<GlobalLobbyRoom> & 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();

+ 3 - 0
client/globalLobby/GlobalLobbyWindow.h

@@ -22,6 +22,7 @@ class GlobalLobbyWindow : public CWindowObject
 	std::string currentChannelName;
 
 	std::shared_ptr<GlobalLobbyWidget> widget;
+	std::set<std::string> 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<GlobalLobbyAccount> & accounts);
 	void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
 	void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);
+	void onInviteReceived(const std::string & invitedRoomID, const std::string & invitedByAccountID);
 	void onJoinedRoom();
 	void onLeftRoom();
 };

+ 7 - 1
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());
-}
+}

+ 17 - 2
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)

+ 1 - 0
lobby/LobbyDatabase.h

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

+ 4 - 4
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;