-- -- ChatGameDrawer -- -- Contains ChatGameCard objects that represent pinned or in progress games. -- This lives at the top of a conversation window. -- local CoreGui = game:GetService("CoreGui") local GuiService = game:GetService("GuiService") local HttpService = game:GetService("HttpService") local PlayerService = game:GetService("Players") local Modules = CoreGui.RobloxGui.Modules local Common = Modules.Common local LuaApp = Modules.LuaApp local LuaChat = Modules.LuaChat local Analytics = require(Common.Analytics) local ChatGameCard = require(LuaChat.Components.ChatGameCard) local Constants = require(LuaApp.Constants) local GameParams = require(LuaChat.Models.GameParams) local FlagSettings = require(LuaChat.FlagSettings) local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions) local Roact = require(Common.Roact) local RoactRodux = require(Common.RoactRodux) local SortedActivelyPlayedGames = require(LuaChat.SortedActivelyPlayedGames) local User = require(LuaApp.Models.User) local ChatGameDrawer = Roact.PureComponent:extend("ChatGameDrawer") local urlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI") local luaChatPlayTogetherJoinGameInstance = FlagSettings.LuaChatPlayTogetherJoinGameInstance() -- Drawer properties: local DRAWER_BACKGROUND_COLOR = Constants.Color.WHITE local BORDER_SIZE = 12 local DRAWER_SHADOW_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/gr-overlay-shadow.png" local DRAWER_SHADOW_HEIGHT = 5 -- Pointer up to the image: local ICON_POINTER_HEIGHT = 6 local ICON_POINTER_WIDTH = 12 local ICON_POINTER_UP = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-up.png" local ICON_POINTER_FROMEDGE = 20 -- "Pinned Game" text properties: local PINNED_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png" local PINNED_ICON_SIZE = 12 local PINNED_SPACER = 6 local PINNED_TEXT_COLOR = Constants.Color.GRAY2 local PINNED_TEXT_FONT = Enum.Font.SourceSans local PINNED_TEXT_SIZE = 15 local PINNED_DIVIDER_COLOR = Constants.Color.GRAY4 local PINNED_BOTTOM_BORDER = Constants.Color.GRAY4 local PINNED_BOTTOM_BACKGROUND = Constants.Color.GRAY6 local SMALL_DIVIDER_OFFSET = 60 local CARD_HEIGHT_SMALL = 60 local CARD_HEIGHT_LARGE = 84 local MORE_TEXT_SIZE = 18 local MORE_TEXT_PADDING = 9 local MORE_TEXT_COLOR = Constants.Color.GRAY1 -- Set up some default state for this control: function ChatGameDrawer:init() self._analytics = Analytics.new() self.state = { isExpanded = false, pointerPosition = UDim2.new(1, -ICON_POINTER_FROMEDGE, 0, 0), pointerSet = false, renderWidth = 0, } self.isGameDrawerSized = false end function ChatGameDrawer:UnpinGame() local playTogetherUnpinGame = self.props.playTogetherUnpinGame playTogetherUnpinGame(self.props.conversationId) end -- Games are pinned by universeId (but they're accessed using placeId elsewhere): function ChatGameDrawer:PinGame(universeId) local playTogetherPinGame = self.props.playTogetherPinGame playTogetherPinGame(self.props.conversationId, universeId) end function ChatGameDrawer:ViewGameDetails(placeId) GuiService:BroadcastNotification(placeId, GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED) end function ChatGameDrawer:GameStart(game) local placeId = game.placeId if luaChatPlayTogetherJoinGameInstance then local friends = game.friends if #friends == 0 then -- No friends are here, join naively: self:GamePlayPlace(placeId) else -- We have friends! Go join them: local friend = friends[1] if friend.placeId == friend.rootPlaceId then -- If our friend is in the root instance, we can join the same instance: self:GameJoinInstance(friend.placeId, friend.rootPlaceId, friend.gameInstanceId) else -- Otherwise, we must join to their playerID: self:GameJoinUser(friend.uid, friend.placeId, friend.rootPlaceId) end end else self:GamePlayPlace(placeId) end end function ChatGameDrawer:GamePlayPlace(placeId) -- Report player start game via play together: self:ReportGamePlayIntent(placeId) self:ReportPlayerPlayGame(placeId) -- Start a game: local gameParams = GameParams.fromPlaceId(placeId) local payload = HttpService:JSONEncode(gameParams) GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME) end function ChatGameDrawer:GameJoinUser(userId, placeId, rootPlaceId) -- Report player join game via play together: self:ReportGamePlayIntent(rootPlaceId) self:ReportPlayerJoinGame(placeId, rootPlaceId, nil) -- Join a game: local gameParams = GameParams.fromUserId(userId) local payload = HttpService:JSONEncode(gameParams) GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME) end function ChatGameDrawer:GameJoinInstance(placeId, rootPlaceId, gameInstanceId) -- Report player join game via play together: self:ReportGamePlayIntent(rootPlaceId) self:ReportPlayerJoinGame(placeId, rootPlaceId, gameInstanceId) -- Join a game: local gameParams = GameParams.fromPlaceInstance(placeId, gameInstanceId) local payload = HttpService:JSONEncode(gameParams) GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME) end function ChatGameDrawer:GetGamesFromConversation(conversationId) local conversations = self.props.conversations local mostRecentlyPlayedGames = self.props.mostRecentlyPlayedGames local users = self.props.users -- Don't know if this is necessary: if not urlSupportNewGamesAPI then error("Server doesn't support new games API.") return { countFriendsInGames = 0, games = {}, } end -- Early out if we have no conversation: if conversationId == nil or conversationId == "nil" then return { countFriendsInGames = 0, games = {}, } end -- Find the specific conversation we're interested in: if not conversations then return { countFriendsInGames = 0, games = {}, } end local conversation = conversations[conversationId] if not conversation then warn("ChatGameDrawer - Can't find conversation, id:" .. conversationId .. " t:" .. type(conversationId)) return { countFriendsInGames = 0, games = {}, } end local pinnedGameRootPlaceId = conversation.pinnedGame.rootPlaceId local inGameParticipants = {} local mostRecentPlayedPlayableGamePlaceId = mostRecentlyPlayedGames.playableGamePlaceId local localPlayerId = tostring(PlayerService.LocalPlayer.UserId) for _, userId in pairs(conversation.participants) do if userId ~= localPlayerId then local user = users[userId] if user ~= nil then if (user.presence == User.PresenceType.IN_GAME) and user.placeId then table.insert(inGameParticipants, user) end end end end local countFriendsInGames = #inGameParticipants if countFriendsInGames > 0 then return { countFriendsInGames = countFriendsInGames, games = SortedActivelyPlayedGames.getSortedGamesPlusEmptyPinned(pinnedGameRootPlaceId, inGameParticipants), } end if pinnedGameRootPlaceId then return { countFriendsInGames = 0, games = { { friends = {}, pinned = true, placeId = pinnedGameRootPlaceId, recommended = false, } } } end if mostRecentPlayedPlayableGamePlaceId then return { countFriendsInGames = 0, games = { { friends = {}, pinned = false, placeId = mostRecentPlayedPlayableGamePlaceId, recommended = true, } } } end return { countFriendsInGames = 0, games = {} } end function ChatGameDrawer:ReportGamePlayIntent(rootPlaceId) local conversationId = self.props.conversationId local eventContext = "PlayGameFromPlayTogether" local eventName = "gamePlayIntent" local player = PlayerService.LocalPlayer local userId = "UNKNOWN" if player then userId = tostring(player.UserId) end local additionalArgs = { uid = userId, rootPlaceId = rootPlaceId, } self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) end function ChatGameDrawer:ReportPlayerPlayGame(placeId) local conversationId = self.props.conversationId local eventContext = "touch" local eventName = "clickPlayButtonInPlayTogether" local player = PlayerService.LocalPlayer local userId = "UNKNOWN" if player then userId = tostring(player.UserId) end local additionalArgs = { uid = userId, cid = conversationId, placeId = placeId } self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) end function ChatGameDrawer:ReportPlayerJoinGame(placeId, rootPlaceId, gameInstanceId) local conversationId = self.props.conversationId local eventContext = "touch" local eventName = "clickJoinButtonInPlayTogether" local player = PlayerService.LocalPlayer local userId = "UNKNOWN" if player then userId = tostring(player.UserId) end local additionalArgs = { uid = userId, conversationId = conversationId, placeId = placeId, rootPlaceId = rootPlaceId, gameInstanceId = gameInstanceId, } self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs) end function ChatGameDrawer:render() local anchorPoint = self.props.AnchorPoint local conversationId = self.props.conversationId local isGameDrawerOpen = self.props.isGameDrawerOpen local localization = self.props.Localization local onSize = self.props.onSize local parentLayoutOrder = self.props.layoutOrder local position = self.props.Position local isExpanded = self.state.isExpanded local pointerPosition = self.state.pointerPosition or UDim2.new(1, -ICON_POINTER_FROMEDGE, 0, 0) local pointerSet = self.state.pointerSet local pointerTransparency = 1 if pointerSet then pointerTransparency = 0 end local gameInfo = self:GetGamesFromConversation(conversationId) local countGames = #gameInfo.games -- Early exit if we have nothing to display in the drawer: if countGames == 0 then spawn(function() onSize(0, false) end) return nil end local hasFriendsActive = gameInfo.countFriendsInGames > 0 -- If we have active friends and this is the first time rendering, expand: if hasFriendsActive and not self.isGameDrawerSized then self.isGameDrawerSized = true if not isExpanded then spawn(function() self:setState({ isExpanded = true }) end) return nil end end -- Build up our drop-down items here for display inside our main element: local gameItems = {} local drawerHeight = 0 gameItems["Layout"] = Roact.createElement("UIListLayout", { FillDirection = Enum.FillDirection.Vertical, HorizontalAlignment = Enum.HorizontalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Top, }) -- Display our pinned game (if we have one): local layoutOrder = 1 local hasPinnedGame = false for _, game in ipairs(gameInfo.games) do if game.pinned then hasPinnedGame = true local placeId = game.placeId gameItems["PinnedTitle"] = Roact.createElement("TextButton", { AutoButtonColor = false, BackgroundTransparency = 1, BorderSizePixel = 0, LayoutOrder = layoutOrder, Size = UDim2.new(1, 0, 0, PINNED_ICON_SIZE + (BORDER_SIZE * 2)), Text = "", }, { Icon = Roact.createElement("ImageLabel", { AnchorPoint = Vector2.new(0, 0.5), BackgroundTransparency = 1, BorderSizePixel = 0, Image = PINNED_ICON, Position = UDim2.new(0, BORDER_SIZE, 0.5, 0), Size = UDim2.new(0, PINNED_ICON_SIZE, 0, PINNED_ICON_SIZE), }), Text = Roact.createElement("TextLabel", { AnchorPoint = Vector2.new(0, 0.5), BackgroundTransparency = 1, BorderSizePixel = 0, Font = PINNED_TEXT_FONT, Position = UDim2.new(0, BORDER_SIZE + PINNED_ICON_SIZE + PINNED_SPACER, 0.5, 0), Size = UDim2.new(1, -(PINNED_ICON_SIZE + PINNED_SPACER + (BORDER_SIZE * 2)), 1, 0), Text = localization:Format("Feature.Chat.Drawer.PinnedGame"), TextColor3 = PINNED_TEXT_COLOR, TextSize = PINNED_TEXT_SIZE, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Center, }), }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + PINNED_ICON_SIZE + (BORDER_SIZE * 2) gameItems["Divider"] = Roact.createElement("Frame", { BackgroundColor3 = PINNED_DIVIDER_COLOR, BorderSizePixel = 0, LayoutOrder = layoutOrder, Size = UDim2.new(1, -(BORDER_SIZE * 2), 0, 1), }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + 1 -- Index by pinned status and placeId: gameItems["Pinned" .. placeId] = Roact.createElement(ChatGameCard, { game = game, conversationId = conversationId, isGameDrawerOpen = isGameDrawerOpen, isPinnedGame = true, LayoutOrder = layoutOrder, Localization = localization, renderWidth = self.state.renderWidth, onGameStart = function() self:GameStart(game) end, onGameUnpin = function() self:UnpinGame() end, onViewDetails = function() self:ViewGameDetails(placeId) end, }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + CARD_HEIGHT_LARGE -- If we have more games to display, add a spacer between the pinned and regular games: if (countGames > 1) then gameItems["Spacer"] = Roact.createElement("Frame", { BackgroundColor3 = PINNED_BOTTOM_BACKGROUND, BackgroundTransparency = 0, BorderColor3 = PINNED_BOTTOM_BORDER, BorderSizePixel = 1, LayoutOrder = layoutOrder, Size = UDim2.new(1, 0, 0, PINNED_SPACER), }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + PINNED_SPACER end -- Done, we found our pinned game in the list: break end end -- Display all the other games in progress - but only if we don't have a -- pinned game or we're expanded: if (not hasPinnedGame) or isExpanded then local hasRegularGame = false for _, game in ipairs(gameInfo.games) do if not game.pinned then -- If we've already added a regular game, insert a spacer before the next: if hasRegularGame then gameItems[layoutOrder] = Roact.createElement("Frame", { BackgroundTransparency = 1, BorderSizePixel = 0, LayoutOrder = layoutOrder, Size = UDim2.new(1, 0, 0, 1), },{ divider = Roact.createElement("Frame", { AnchorPoint = Vector2.new(1, 0), BorderSizePixel = 0, BackgroundColor3 = PINNED_DIVIDER_COLOR, Position = UDim2.new(1, 0, 0, 0), Size = UDim2.new(1, -SMALL_DIVIDER_OFFSET, 0, 1), }), }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + 1 end local placeId = game.placeId -- Index as an unpinned game and placeId: gameItems["Game" .. placeId] = Roact.createElement(ChatGameCard, { game = game, conversationId = conversationId, isGameDrawerOpen = isGameDrawerOpen, isPinnedGame = false, isRecommended = game.recommended, LayoutOrder = layoutOrder, Localization = localization, renderWidth = self.state.renderWidth, onGameStart = function() self:GameStart(game) end, onGamePin = function(universeId) self:PinGame(universeId) end, onViewDetails = function() self:ViewGameDetails(placeId) end, }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + CARD_HEIGHT_SMALL -- If we're not expanded, we've hit our limit: if not isExpanded then break end -- Next card will have a spacer before it. hasRegularGame = true end end end -- The final item in the list should be the text to either show more or hide: local endTextDivider = false local endTextShow = false local endLocalizeText = "" if countGames == 0 then endTextShow = true endLocalizeText = localization:Format("Feature.Chat.Drawer.NoGames") elseif countGames > 1 then if isExpanded then endLocalizeText = localization:Format("Feature.Chat.Drawer.ShowLess") else endLocalizeText = localization:Format("Feature.Chat.Drawer.ShowMore") .. " (+" .. (countGames - 1) .. ")" end endTextDivider = true endTextShow = true end if endTextShow then if endTextDivider then gameItems["DividerBottom"] = Roact.createElement("Frame", { BorderSizePixel = 0, BackgroundColor3 = PINNED_DIVIDER_COLOR, LayoutOrder = layoutOrder, Size = UDim2.new(1, 0, 0, 1), }) layoutOrder = layoutOrder + 1 drawerHeight = drawerHeight + PINNED_SPACER end gameItems["ShowButton"] = Roact.createElement("TextButton", { BackgroundTransparency = 1, BorderSizePixel = 0, Font = PINNED_TEXT_FONT, LayoutOrder = layoutOrder, Size = UDim2.new(1, 0, 0, MORE_TEXT_SIZE + (MORE_TEXT_PADDING * 2)), Text = endLocalizeText, TextColor3 = MORE_TEXT_COLOR, TextSize = MORE_TEXT_SIZE, [Roact.Event.Activated] = function() self:setState({ isExpanded = not self.state.isExpanded }) end }) drawerHeight = drawerHeight + MORE_TEXT_SIZE + (MORE_TEXT_PADDING * 2) end -- Define the shadow component to hang off the bottom of the list: -- Note: Do not count the height since this isn't inside the frame. local shadow = Roact.createElement("ImageLabel", { BackgroundTransparency = 1, Image = DRAWER_SHADOW_IMAGE, Size = UDim2.new(1, 0, 0, DRAWER_SHADOW_HEIGHT), Position = UDim2.new(0, 0, 1, 0), }) spawn(function() onSize(drawerHeight, hasFriendsActive) end) -- Create and return the main control itself: return Roact.createElement("Frame", { AnchorPoint = anchorPoint, BackgroundColor3 = DRAWER_BACKGROUND_COLOR, BackgroundTransparency = 0, BorderSizePixel = 0, ClipsDescendants = false, LayoutOrder = parentLayoutOrder, Position = position, Size = UDim2.new(1, 0, 1, 0), [Roact.Ref] = function(rbx) if not rbx then return end spawn(function() self:resolveMetrics(rbx) end) end, }, { Pointer = Roact.createElement("ImageLabel", { AnchorPoint = Vector2.new(0.5, 1), BackgroundTransparency = 1, BorderSizePixel = 0, Image = ICON_POINTER_UP, ImageTransparency = pointerTransparency, Position = pointerPosition, Size = UDim2.new(0, ICON_POINTER_WIDTH, 0, ICON_POINTER_HEIGHT), }), Shadow = shadow, Frame = Roact.createElement("Frame", { BackgroundTransparency = 1, BorderSizePixel = 0, ClipsDescendants = true, Size = UDim2.new(1, 0, 1, 0), }, gameItems ) }) end function ChatGameDrawer:resolveMetrics(rbx) -- This function finds the ActiveGameIcon and positions an arrow pointing -- at it from our open drawer. The position of the icon can change depending -- on a number of factors so it can't be hardcoded. -- -- Also retrieves the actual width of our drawer to pass to child components -- which need to be width-aware to render properly. -- -- Note 1: I didn't want to attach this on the icon because it needs to line -- up with the edge of the drawer.) -- -- Note 2: we can't examine the GameDrawer position here because on the -- initial pass through it is invisible with a position of 0,0 on screen. -- Find the conversation header: local header = rbx:FindFirstAncestor("HeaderFrame") if header == nil then warn("Couldn't find header.") return end -- Find the "Play Together" icon: local iconPlayTogether = header:FindFirstChild("TopGameIcon", true) if iconPlayTogether == nil then warn("Couldn't find Play Together icon.") return end -- Figure out where on the screen iconPlayTogether is, so -- we can position our pointer directly underneath it: local iconFromEdge = (header.AbsolutePosition.X + header.AbsoluteSize.X) - (iconPlayTogether.AbsolutePosition.X + (iconPlayTogether.AbsoluteSize.X * 0.5)) -- Track our drawer's width for content-aware scaling: local renderWidth = rbx.AbsoluteSize.X -- Prevent updating metrics if we already have the correct values: if (not self.state.pointerSet) or (self.state.renderWidth ~= renderWidth) or (self.state.pointerPosition.X.Offset ~= -iconFromEdge) then -- Update the pointer position state so it will render in the correct location: -- Yes, we're using another spawn call here - but we need the 1-frame delay. spawn(function() self:setState({ pointerPosition = UDim2.new(1, -iconFromEdge, 0, 0), pointerSet = true, renderWidth = renderWidth, }) end) end end ChatGameDrawer = RoactRodux.UNSTABLE_connect2( function(state, props) return { conversations = state.ChatAppReducer.Conversations, mostRecentlyPlayedGames = state.ChatAppReducer.MostRecentlyPlayedGames, users = state.Users, } end, function(dispatch) return { playTogetherUnpinGame = function(conversationId) dispatch(PlayTogetherActions.UnpinGame(conversationId)) end, playTogetherPinGame = function(conversationId, universeId) dispatch(PlayTogetherActions.PinGame(conversationId, universeId)) end, } end )(ChatGameDrawer) return ChatGameDrawer