475 lines
15 KiB
Lua
475 lines
15 KiB
Lua
--
|
|
-- ChatGameCard
|
|
--
|
|
-- This is a game that is shown (and possibly pinned) at the top of a chat
|
|
-- conversation.
|
|
--
|
|
|
|
local CoreGui = game:GetService("CoreGui")
|
|
|
|
local Modules = CoreGui.RobloxGui.Modules
|
|
local LuaApp = Modules.LuaApp
|
|
local LuaChat = Modules.LuaChat
|
|
|
|
local Constants = require(LuaApp.Constants)
|
|
local ContextualMenu = require(LuaApp.Components.ContextualMenu)
|
|
local FriendCarousel = require(LuaChat.Components.FriendCarousel)
|
|
local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos)
|
|
local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail)
|
|
local Roact = require(Modules.Common.Roact)
|
|
local RoactRodux = require(Modules.Common.RoactRodux)
|
|
local RoactAnalyticsGameCardLoaded = require(LuaChat.Services.RoactAnalyticsGameCardLoaded)
|
|
local RoactServices = require(LuaApp.RoactServices)
|
|
|
|
local ChatGameCard = Roact.PureComponent:extend("ChatGameCard")
|
|
|
|
local FFlagLuaChatLoadGameLinkCardInChatAnalytics = settings():GetFFlag("LuaChatLoadGameLinkCardInChatAnalytics")
|
|
|
|
local ICON_SIZE_SMALL = 36
|
|
local ICON_SIZE_LARGE = 60
|
|
local ICON_SIZE_SMALL_AMENDED = 48
|
|
|
|
local CARD_MARGINS = 12
|
|
local CARD_HEIGHT_SMALL = ICON_SIZE_SMALL + (CARD_MARGINS * 2)
|
|
local CARD_HEIGHT_LARGE = ICON_SIZE_LARGE + (CARD_MARGINS * 2)
|
|
|
|
local GAME_TEXT_COLOR = Constants.Color.GRAY1
|
|
local GAME_TEXT_FONT = Enum.Font.SourceSans
|
|
local GAME_TEXT_HEIGHT = 25
|
|
local GAME_TEXT_HEIGHT_WITH_SUBTITLE = 20
|
|
local GAME_TEXT_SIZE = 23
|
|
local GAME_TEXT_SIZE_WITH_SUBTITLE = 20
|
|
|
|
local SUBTITLE_TEXT_SIZE = 18
|
|
local SUBTITLE_TEXT_COLOR = Constants.Color.GRAY2
|
|
|
|
local CAROUSEL_SMALL_ICON_SIZE = 24
|
|
local CAROUSEL_LARGE_ICON_SIZE = 32
|
|
local CAROUSEL_SMALL_GAP = 3
|
|
local CAROUSEL_LARGE_GAP = 9
|
|
local CAROUSEL_SMALL_DOT_SIZE = 8
|
|
local CAROUSEL_LARGE_DOT_SIZE = 10
|
|
|
|
local ACTION_BUTTON_WIDTH = 60
|
|
local ACTION_BUTTON_HEIGHT = 32
|
|
local ACTION_COLOR_PLAY = Constants.Color.GREEN_PRIMARY
|
|
local ACTION_COLOR_TEXT = Constants.Color.WHITE
|
|
|
|
local DEBUG_OUTLINE = 0
|
|
local DEBUG_TRANSPARENCY = 1
|
|
|
|
local DEFAULT_GAME_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png"
|
|
local FADEOUT_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/friendmask.png"
|
|
local FADEOUT_MASK_WIDTH = 10
|
|
local GAME_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/gr-mask-game-icon.png"
|
|
local ROUNDED_BUTTON = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png"
|
|
|
|
-- Set up some default state for this control:
|
|
function ChatGameCard:init()
|
|
self.state = {
|
|
isMenuOpen = false,
|
|
}
|
|
|
|
-- Localize strings. Needs to be done in context because of the way the localization object is being passed to us:
|
|
local localization = self.props.Localization
|
|
self.MenuInfoPlayGame = {
|
|
displayIcon = "rbxasset://textures/ui/LuaApp/icons/ic-games.png",
|
|
name = "PlayGameButton",
|
|
displayName = localization:Format("Feature.Chat.Drawer.PlayGame"),
|
|
}
|
|
self.MenuInfoPinGame = {
|
|
displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png",
|
|
name = "PinGameButton",
|
|
displayName = localization:Format("Feature.Chat.Drawer.PinGame")
|
|
}
|
|
self.MenuInfoUnpinGame = {
|
|
displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-unpin-20x20.png",
|
|
name = "UnpinGameButton",
|
|
displayName = localization:Format("Feature.Chat.Drawer.UnpinGame")
|
|
}
|
|
self.MenuItemInfoViewGameDetail = {
|
|
displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-viewdetails-20x20.png",
|
|
name = "ViewDetailsButton",
|
|
displayName = localization:Format("Feature.Chat.Drawer.ViewDetails"),
|
|
}
|
|
end
|
|
|
|
function ChatGameCard:render()
|
|
-- Information about the game is passed in as properties. Action to take
|
|
-- *on* the game should also be passed in, so we have containment and this
|
|
-- module only knows the miniumum necessary.
|
|
|
|
local parentLayoutOrder = self.props.LayoutOrder
|
|
|
|
-- Visual properties of this game card:
|
|
local isPinnedGame = self.props.isPinnedGame or false
|
|
local isRecommendedGame = self.props.isRecommended or false
|
|
|
|
local game = self.props.game
|
|
local getPlaceInfo = self.props.getPlaceInfo
|
|
local getPlaceThumbnail = self.props.getPlaceThumbnail
|
|
local localization = self.props.Localization
|
|
local onGamePin = self.props.onGamePin
|
|
local onGameStart = self.props.onGameStart
|
|
local onGameUnpin = self.props.onGameUnpin
|
|
local onViewDetails = self.props.onViewDetails
|
|
local placeInfos = self.props.placeInfos
|
|
local placeThumbnails = self.props.placeThumbnails or {}
|
|
local renderWidth = self.props.renderWidth or 0
|
|
|
|
-- Unpack from our properties:
|
|
local gameFriends = game.friends or {}
|
|
local placeId = game.placeId
|
|
|
|
-- Read or retrieve information about our place:
|
|
local placeInfo = placeInfos[placeId]
|
|
local gameName
|
|
local imageToken = nil
|
|
local universeId = nil
|
|
local isPlayable = false
|
|
if (placeInfo == nil) then
|
|
getPlaceInfo(placeId)
|
|
gameName = "(" .. localization:Format("Feature.Chat.Drawer.Loading") .. ")"
|
|
else
|
|
gameName = placeInfo.name
|
|
imageToken = placeInfo.imageToken
|
|
universeId = placeInfo.universeId
|
|
isPlayable = placeInfo.isPlayable
|
|
end
|
|
|
|
-- Configure some dimensions based on properties, the GameInfo section in
|
|
-- particular changes for a large (pinned) vs regular size card:
|
|
local cardHeight = CARD_HEIGHT_SMALL
|
|
local carouselItemDotSize = CAROUSEL_SMALL_DOT_SIZE
|
|
local carouselItemGap = CAROUSEL_SMALL_GAP
|
|
local carouselItemHeight = CAROUSEL_SMALL_ICON_SIZE
|
|
local friendAlignment = Enum.HorizontalAlignment.Right
|
|
local gameIconHeight = ICON_SIZE_SMALL
|
|
local gameIconWidth = gameIconHeight
|
|
local gameTextHeight = GAME_TEXT_HEIGHT
|
|
local gameTextSize = GAME_TEXT_SIZE
|
|
local infoFillDirection = Enum.FillDirection.Horizontal
|
|
local subtitle = ""
|
|
local subtitleVisibility = false
|
|
|
|
-- If we don't have an action button, zero the reserved width:
|
|
local buttonWidth = ACTION_BUTTON_WIDTH
|
|
if not isPlayable then
|
|
buttonWidth = 0
|
|
end
|
|
|
|
-- Scaling of elements:
|
|
local friendsWidthOffset = 0
|
|
local friendsWidthScale = 1
|
|
local textWidthOffset = 0
|
|
local textWidthScale = 1
|
|
|
|
if isPinnedGame then
|
|
cardHeight = CARD_HEIGHT_LARGE
|
|
carouselItemDotSize = CAROUSEL_LARGE_DOT_SIZE
|
|
carouselItemGap = CAROUSEL_LARGE_GAP
|
|
carouselItemHeight = CAROUSEL_LARGE_ICON_SIZE
|
|
friendAlignment = Enum.HorizontalAlignment.Left
|
|
gameIconHeight = ICON_SIZE_LARGE
|
|
gameIconWidth = gameIconHeight
|
|
infoFillDirection = Enum.FillDirection.Vertical
|
|
elseif isRecommendedGame then
|
|
carouselItemHeight = 0
|
|
gameTextHeight = GAME_TEXT_HEIGHT_WITH_SUBTITLE
|
|
gameTextSize = GAME_TEXT_SIZE_WITH_SUBTITLE
|
|
infoFillDirection = Enum.FillDirection.Vertical
|
|
subtitle = localization:Format("Feature.Chat.Drawer.Recommended")
|
|
subtitleVisibility = true
|
|
else
|
|
friendsWidthScale = 0.5
|
|
friendsWidthOffset = 0
|
|
textWidthScale = 0.5
|
|
textWidthOffset = 0
|
|
end
|
|
|
|
-- This is how much space we have in the center of the card:
|
|
local centerNegativeSpace = CARD_MARGINS + gameIconWidth + CARD_MARGINS + CARD_MARGINS
|
|
local countFriends = #gameFriends
|
|
|
|
-- Default is Play if nobody else is in the game, Join if we have friends:
|
|
local actionText
|
|
if isPlayable then
|
|
centerNegativeSpace = centerNegativeSpace + buttonWidth + CARD_MARGINS
|
|
local actionTextKey = "Feature.Chat.Drawer.Play"
|
|
if countFriends > 0 then
|
|
actionTextKey = "Feature.Chat.Drawer.Join"
|
|
end
|
|
actionText = localization:Format(actionTextKey)
|
|
end
|
|
local actionColor = ACTION_COLOR_PLAY
|
|
local actionTextColor = ACTION_COLOR_TEXT
|
|
|
|
-- ...and as a last metrics step, adjust the ratio of the game name and friend carousel:
|
|
if not (isPinnedGame or isRecommendedGame) then
|
|
local actualSpace = renderWidth - centerNegativeSpace
|
|
if actualSpace > 0 then
|
|
local friendSpace = carouselItemHeight * countFriends
|
|
local textAdjust = (actualSpace * 0.5) - (friendSpace + CARD_MARGINS)
|
|
if textAdjust > 0 then
|
|
textWidthOffset = textWidthOffset + textAdjust
|
|
friendsWidthOffset = friendsWidthOffset - textAdjust
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Obtain the thumbnail for this game:
|
|
local gameIcon = DEFAULT_GAME_ICON
|
|
if placeInfo then
|
|
local thumbnail = placeThumbnails[imageToken]
|
|
if thumbnail == nil then
|
|
if imageToken and imageToken ~= "" then
|
|
if gameIconWidth == ICON_SIZE_SMALL then
|
|
getPlaceThumbnail(imageToken, ICON_SIZE_SMALL_AMENDED, ICON_SIZE_SMALL_AMENDED)
|
|
else
|
|
getPlaceThumbnail(imageToken, gameIconWidth, gameIconHeight)
|
|
end
|
|
end
|
|
elseif thumbnail.image ~= "" then
|
|
gameIcon = thumbnail.image
|
|
end
|
|
end
|
|
|
|
-- Build up a horizontal list of items for our card:
|
|
local cardItems = {}
|
|
cardItems["Layout"] = Roact.createElement("UIListLayout", {
|
|
FillDirection = Enum.FillDirection.Horizontal,
|
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
Padding = UDim.new(0, CARD_MARGINS)
|
|
})
|
|
|
|
-- Game icon:
|
|
local layoutOrder = 1
|
|
cardItems["GameIcon"] = Roact.createElement("ImageLabel", {
|
|
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
|
BorderSizePixel = DEBUG_OUTLINE,
|
|
LayoutOrder = layoutOrder,
|
|
Size = UDim2.new(0, gameIconHeight, 0, gameIconHeight),
|
|
Image = gameIcon,
|
|
}, {
|
|
Mask = Roact.createElement("ImageLabel", {
|
|
BackgroundTransparency = 1,
|
|
BorderSizePixel = 0,
|
|
Image = GAME_MASK_IMAGE,
|
|
ImageColor3 = Constants.Color.WHITE,
|
|
ScaleType = Enum.ScaleType.Slice,
|
|
Size = UDim2.new(1, 0, 1, 0),
|
|
SliceCenter = Rect.new(3,3,4,4),
|
|
}),
|
|
})
|
|
layoutOrder = layoutOrder + 1
|
|
|
|
cardItems["GameInfo"] = Roact.createElement("Frame", {
|
|
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
|
BorderSizePixel = DEBUG_OUTLINE,
|
|
ClipsDescendants = true,
|
|
LayoutOrder = layoutOrder,
|
|
Size = UDim2.new(1, -centerNegativeSpace, 1, 0),
|
|
}, {
|
|
Layout = Roact.createElement("UIListLayout", {
|
|
FillDirection = infoFillDirection,
|
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
}),
|
|
|
|
GameName = Roact.createElement("TextLabel", {
|
|
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
|
BorderSizePixel = DEBUG_OUTLINE,
|
|
ClipsDescendants = true,
|
|
Font = GAME_TEXT_FONT,
|
|
LayoutOrder = 1,
|
|
Size = UDim2.new(textWidthScale, textWidthOffset, 0, gameTextHeight),
|
|
Text = gameName,
|
|
TextColor3 = GAME_TEXT_COLOR,
|
|
TextSize = gameTextSize,
|
|
TextXAlignment = Enum.TextXAlignment.Left,
|
|
TextYAlignment = Enum.TextYAlignment.Top
|
|
},{
|
|
MaskRight = Roact.createElement("ImageLabel", {
|
|
BackgroundTransparency = 1,
|
|
BorderSizePixel = 0,
|
|
Image = FADEOUT_MASK_IMAGE,
|
|
Position = UDim2.new(1, -FADEOUT_MASK_WIDTH, 0, 0),
|
|
Size = UDim2.new(0, FADEOUT_MASK_WIDTH, 1, 0),
|
|
ZIndex = 2,
|
|
}),
|
|
}),
|
|
|
|
Subtitle = Roact.createElement("TextLabel", {
|
|
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
|
BorderSizePixel = DEBUG_OUTLINE,
|
|
ClipsDescendants = true,
|
|
Font = GAME_TEXT_FONT,
|
|
LayoutOrder = 1,
|
|
Size = UDim2.new(textWidthScale, textWidthOffset, 0, ICON_SIZE_SMALL - gameTextHeight),
|
|
Text = subtitle,
|
|
TextColor3 = SUBTITLE_TEXT_COLOR,
|
|
TextSize = SUBTITLE_TEXT_SIZE,
|
|
TextXAlignment = Enum.TextXAlignment.Left,
|
|
Visible = subtitleVisibility,
|
|
}),
|
|
|
|
GameFriends = Roact.createElement(FriendCarousel, {
|
|
dotSize = carouselItemDotSize,
|
|
friends = gameFriends,
|
|
HorizontalAlignment = friendAlignment,
|
|
itemGap = carouselItemGap,
|
|
itemSize = carouselItemHeight,
|
|
LayoutOrder = 2,
|
|
Size = UDim2.new(friendsWidthScale, friendsWidthOffset, 0, carouselItemHeight),
|
|
}),
|
|
})
|
|
layoutOrder = layoutOrder + 1
|
|
|
|
if isPlayable then
|
|
cardItems["ActionButton"] = Roact.createElement("ImageButton", {
|
|
AutoButtonColor = false,
|
|
BackgroundTransparency = 1,
|
|
BorderSizePixel = DEBUG_OUTLINE,
|
|
Image = ROUNDED_BUTTON,
|
|
ImageColor3 = actionColor,
|
|
LayoutOrder = layoutOrder,
|
|
ScaleType = Enum.ScaleType.Slice,
|
|
Size = UDim2.new(0, ACTION_BUTTON_WIDTH, 0, ACTION_BUTTON_HEIGHT),
|
|
SliceCenter = Rect.new(3,3,4,4),
|
|
|
|
[Roact.Event.Activated] = function(rbx)
|
|
onGameStart()
|
|
end
|
|
},{
|
|
ActionLabel = Roact.createElement("TextLabel", {
|
|
BackgroundTransparency = 1,
|
|
BorderSizePixel = 0,
|
|
Font = GAME_TEXT_FONT,
|
|
LayoutOrder = layoutOrder,
|
|
Size = UDim2.new(1, 0, 1, 0),
|
|
Text = actionText,
|
|
TextColor3 = actionTextColor,
|
|
TextSize = GAME_TEXT_SIZE,
|
|
}),
|
|
})
|
|
end
|
|
|
|
if self.state.isMenuOpen then
|
|
local menuItems
|
|
if isPinnedGame then
|
|
menuItems = { self.MenuInfoUnpinGame }
|
|
else
|
|
menuItems = { self.MenuInfoPinGame }
|
|
end
|
|
|
|
if isPlayable then
|
|
table.insert(menuItems, 1, self.MenuInfoPlayGame)
|
|
end
|
|
table.insert(menuItems, self.MenuItemInfoViewGameDetail)
|
|
|
|
local callbackCancel = function()
|
|
self:setState({ isMenuOpen = false })
|
|
end
|
|
|
|
local callbackSelect = function(item)
|
|
if item.name == self.MenuInfoPlayGame.name then
|
|
onGameStart()
|
|
elseif item.name == self.MenuInfoPinGame.name then
|
|
onGamePin(universeId)
|
|
elseif item.name == self.MenuInfoUnpinGame.name then
|
|
onGameUnpin()
|
|
elseif item.name == self.MenuItemInfoViewGameDetail.name then
|
|
onViewDetails()
|
|
end
|
|
callbackCancel()
|
|
end
|
|
|
|
cardItems["ContextMenu"] = Roact.createElement(ContextualMenu, {
|
|
callbackCancel = callbackCancel,
|
|
callbackSelect = callbackSelect,
|
|
menuItems = menuItems,
|
|
screenShape = self.state.screenShape,
|
|
})
|
|
end
|
|
|
|
-- Put a clickable wrapper around the entire card:
|
|
return Roact.createElement("TextButton", {
|
|
AutoButtonColor = false,
|
|
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
|
BorderSizePixel = DEBUG_OUTLINE,
|
|
LayoutOrder = parentLayoutOrder,
|
|
Size = UDim2.new(1, 0, 0, cardHeight),
|
|
Text = "",
|
|
[Roact.Event.Activated] = function(rbx)
|
|
-- TODO: Move this screen size functionality into a helper component
|
|
-- so that it doesn't get repeated everywhere (see: MOBLUAPP-241).
|
|
|
|
-- We need to know the size of the screen, so we can position the
|
|
-- popout component appropriately. So we climb up the object
|
|
-- heirachy until we find the current ScreenGui:
|
|
local screenWidth = 0
|
|
local screenHeight = 0
|
|
local screenGui = rbx:FindFirstAncestorOfClass("ScreenGui")
|
|
if screenGui ~= nil then
|
|
screenWidth = screenGui.AbsoluteSize.X
|
|
screenHeight = screenGui.AbsoluteSize.Y
|
|
end
|
|
|
|
self:setState({
|
|
isMenuOpen = true,
|
|
screenShape = {
|
|
x = rbx.AbsolutePosition.X,
|
|
y = rbx.AbsolutePosition.Y,
|
|
width = rbx.AbsoluteSize.X,
|
|
height = rbx.AbsoluteSize.Y,
|
|
parentWidth = screenWidth,
|
|
parentHeight = screenHeight,
|
|
},
|
|
})
|
|
end,
|
|
|
|
}, cardItems)
|
|
end
|
|
|
|
function ChatGameCard:didUpdate(previousProps, previousState)
|
|
local conversationId = self.props.conversationId
|
|
local isGameDrawerOpen = self.props.isGameDrawerOpen
|
|
local placeId = self.props.game.placeId
|
|
if FFlagLuaChatLoadGameLinkCardInChatAnalytics then
|
|
local sendAnalytics = false
|
|
if isGameDrawerOpen ~= previousProps.isGameDrawerOpen and self.state == previousState then
|
|
sendAnalytics = true
|
|
end
|
|
if isGameDrawerOpen and sendAnalytics then
|
|
self.props.analytics.reportGameCardLoadedInLuaChat(tostring(conversationId), tostring(placeId))
|
|
end
|
|
end
|
|
end
|
|
|
|
ChatGameCard = RoactRodux.UNSTABLE_connect2(
|
|
function(state, props)
|
|
return {
|
|
placeInfos = state.ChatAppReducer.PlaceInfos,
|
|
placeThumbnails = state.ChatAppReducer.PlaceThumbnails,
|
|
}
|
|
end,
|
|
function(dispatch)
|
|
return {
|
|
getPlaceInfo = function(placeId)
|
|
dispatch(GetMultiplePlaceInfos({placeId}))
|
|
end,
|
|
getPlaceThumbnail = function(imageToken, iconWidth, iconHeight)
|
|
dispatch(GetPlaceThumbnail(imageToken, iconWidth, iconHeight))
|
|
end,
|
|
}
|
|
end
|
|
)(ChatGameCard)
|
|
|
|
ChatGameCard = RoactServices.connect({
|
|
analytics = RoactAnalyticsGameCardLoaded,
|
|
})(ChatGameCard)
|
|
|
|
return ChatGameCard |