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