SyntaxGameServer/RCCService2018/content/internal/Chat/Modules/LuaChat/Components/ChatGameDrawer.lua

673 lines
20 KiB
Lua

--
-- 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