This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,136 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Constants = require(Modules.LuaChat.Constants)
local formatInteger = require(Modules.LuaChat.Utils.formatInteger)
local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel)
local TEXT_FONT = Enum.Font.SourceSans
local GAME_NAME_LABEL_HEIGHT = 20
local GAME_NAME_LABEL_TEXT_SIZE = 23
local GAME_NAME_LABEL_TEXT_COLOR = Constants.Color.GRAY1
local CREATOR_NAME_LABEL_HEIGHT = 14
local CREATOR_NAME_LABEL_TEXT_SIZE = 15
local CREATOR_NAME_LABEL_TOP_PADDING = 6
local CREATOR_NAME_LABEL_COLOR = Constants.Color.GRAY2
local SUBTITLE_LABEL_HEIGHT = 14
local SUBTITLE_LABEL_TEXT_SIZE = 14
local SUBTITLE_LABEL_TOP_PADDING = 3
local SUBTITLE_FRAME_HEIGHT = SUBTITLE_LABEL_HEIGHT + SUBTITLE_LABEL_TOP_PADDING
local ROBUX_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-robux.png"
local ROBUX_ICON_SIZE = 12
local ROBUX_TO_PRICE_PADDING = 3
local PRICE_COLOR = Constants.Color.GREEN_PRIMARY
local FFlagLuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
local GameInformation = Roact.PureComponent:extend("GameInformation")
function GameInformation:render()
local gameModel = self.props.gameModel
if not gameModel then
return nil
end
local gameTitle = gameModel.name
local creatorName = gameModel.creatorName
-- TODO wait lua app team store the playability of the game, default value is true
-- Ticket: MOBLUAPP-683, Review http://swarm.roblox.local/reviews/226723/
-- TODO: SOC-3779 This ticket has been committed since this comment.
-- * New ticket: https://jira.roblox.com/browse/SOC-3779
local playable = FFlagLuaChatShareGameToChatFromChatV2 or self.props.gameModel.isPlayable
local gamePrice = gameModel.price
local showPrice = gamePrice and playable
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
Name = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Font = TEXT_FONT,
LayoutOrder = 1,
Size = UDim2.new(1, 0, 0, GAME_NAME_LABEL_HEIGHT),
Text = gameTitle,
TextColor3 = GAME_NAME_LABEL_TEXT_COLOR,
TextSize = GAME_NAME_LABEL_TEXT_SIZE,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
}),
Creator = Roact.createElement(LocalizedTextLabel, {
BackgroundTransparency = 1,
Font = TEXT_FONT,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, CREATOR_NAME_LABEL_HEIGHT + CREATOR_NAME_LABEL_TOP_PADDING),
Text = {"Feature.Chat.ShareGameToChat.By", creatorName = creatorName},
TextColor3 = CREATOR_NAME_LABEL_COLOR,
TextSize = CREATOR_NAME_LABEL_TEXT_SIZE,
TextTruncate = Enum.TextTruncate.AtEnd,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
}),
NotAvailableTip = not playable and Roact.createElement(LocalizedTextLabel, {
BackgroundTransparency = 1,
Font = TEXT_FONT,
LayoutOrder = 3,
Size = UDim2.new(1, 0, 0, SUBTITLE_FRAME_HEIGHT),
Text = "Feature.Chat.ShareGameToChat.GameNotAvailable",
TextColor3 = CREATOR_NAME_LABEL_COLOR,
TextSize = SUBTITLE_LABEL_TEXT_SIZE,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
}),
GamePrice = showPrice and Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 3,
Size = UDim2.new(1, 0, 0, SUBTITLE_FRAME_HEIGHT),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, ROBUX_TO_PRICE_PADDING),
}),
RobuxIcon = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = ROBUX_ICON,
LayoutOrder = 1,
ScaleType = Enum.ScaleType.Fit,
Size = UDim2.new(0, ROBUX_ICON_SIZE, 0, ROBUX_ICON_SIZE),
}),
Price = Roact.createElement("TextLabel",{
BackgroundTransparency = 1,
Size = UDim2.new(1, -(ROBUX_ICON_SIZE + ROBUX_TO_PRICE_PADDING), 1, 0),
Font = TEXT_FONT,
LayoutOrder = 2,
Text = formatInteger(gamePrice),
TextColor3 = PRICE_COLOR,
TextSize = SUBTITLE_LABEL_TEXT_SIZE,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
}),
}),
})
end
return GameInformation
@@ -0,0 +1,165 @@
return function()
local GameInformation = require(script.Parent.GameInformation)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
local FFlagLuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
-- TODO: SOC-3805 `gameModel` should be using an actual model.
local function createValidGameModel(isPlayable, price)
return {
imageToken = "mock-token",
name = "mock-name",
universeId = "mock-id",
isPlayable = isPlayable,
price = price or nil,
creatorName = "mock-creator",
}
end
describe("SHOULD create and destroy without errors", function()
it("WHEN passed no props", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("WHEN passed a valid gameModel", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
if not FFlagLuaChatShareGameToChatFromChatV2 then
describe("SHOULD determine if the game is available", function()
describe("SHOULD display playability status visible", function()
it("WHEN the game is not playable and free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(false, nil),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local notAvailableTip = folder:FindFirstChild("NotAvailableTip", true)
expect(notAvailableTip).to.be.ok()
Roact.unmount(instance)
end)
it("WHEN the game is not playable and not free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(false, 1000),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local notAvailableTip = folder:FindFirstChild("NotAvailableTip", true)
expect(notAvailableTip).to.be.ok()
Roact.unmount(instance)
end)
end)
describe("SHOULD display playability status as not visible", function()
it("WHEN the game is playable and free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(true, nil),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local notAvailableTip = folder:FindFirstChild("NotAvailableTip", true)
expect(notAvailableTip).to.never.be.ok()
Roact.unmount(instance)
end)
end)
end)
describe("SHOULD determine to show price", function()
describe("SHOULD display price as visible", function()
it("WHEN the game is playable and not free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(true, 1000),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local gamePrice = folder:FindFirstChild("GamePrice", true)
expect(gamePrice).to.be.ok()
Roact.unmount(instance)
end)
end)
describe("SHOULD display price as not visible", function()
it("WHEN the game is playable and free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(true, nil),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local gamePrice = folder:FindFirstChild("GamePrice", true)
expect(gamePrice).to.never.be.ok()
Roact.unmount(instance)
end)
it("WHEN the game is not playable and free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(false, nil),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local gamePrice = folder:FindFirstChild("GamePrice", true)
expect(gamePrice).to.never.be.ok()
Roact.unmount(instance)
end)
it("WHEN the game is not playable and not free", function()
local element = mockServices({
GameInformation = Roact.createElement(GameInformation, {
gameModel = createValidGameModel(false, 1000),
})
})
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local gamePrice = folder:FindFirstChild("GamePrice", true)
expect(gamePrice).to.never.be.ok()
Roact.unmount(instance)
end)
end)
end)
end
end
@@ -0,0 +1,29 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GAME_BORDER_ICON = "rbxasset://textures/ui/LuaChat/graphic/gr-game-border-60x60.png"
local RoundedIcon = Roact.PureComponent:extend("RoundedIcon")
function RoundedIcon:render()
local image = self.props.Image
local size = self.props.Size
local layoutOrder = self.props.LayoutOrder
return Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = image,
Size = size,
LayoutOrder = layoutOrder,
}, {
RoundCornerOverlay = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = GAME_BORDER_ICON,
Size = UDim2.new(1, 0, 1, 0),
}),
})
end
return RoundedIcon
@@ -0,0 +1,32 @@
return function()
local RoundedIcon = require(script.Parent.RoundedIcon)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
describe("SHOULD create and destroy without errors", function()
it("WHEN passed no props", function()
local element = Roact.createElement(RoundedIcon)
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("WHEN passed an Image", function()
local element = Roact.createElement(RoundedIcon, {
Image = "mock-id",
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("WHEN passed a valid Size", function()
local element = Roact.createElement(RoundedIcon, {
Size = UDim2.new(1, 0, 1, 0),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,74 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local isInputTypeTouchOrMouseDown = require(Modules.LuaChat.Utils.isInputTypeTouchOrMouseDown)
local SEND_BUTTON_ICON = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-24x24.png"
local SEND_BUTTON_ICON_PRESSED = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png"
local SEND_BUTTON_ICON_SIZE = 24
local SEND_BUTTON_LEFT_PADDING = 12
local SEND_BUTTON_RIGHT_PADDING = 25
local SEND_BUTTON_FULL_WIDTH = SEND_BUTTON_ICON_SIZE + SEND_BUTTON_LEFT_PADDING + SEND_BUTTON_RIGHT_PADDING
local SendToChatButton = Roact.PureComponent:extend("SendToChatButton")
function SendToChatButton.getWidth()
return SEND_BUTTON_FULL_WIDTH
end
function SendToChatButton:init()
self.state = {
isButtonPressed = false,
}
self.onSendButtonInputBegan = function(_, inputObject)
if isInputTypeTouchOrMouseDown(inputObject) then
self:setState({
isButtonPressed = true,
})
end
end
self.onSendButtonInputEnded = function(_, inputObject)
if isInputTypeTouchOrMouseDown(inputObject) then
self:setState({
isButtonPressed = false,
})
end
end
end
function SendToChatButton:render()
local onActivated = self.props.onActivated
local layoutOrder = self.props.LayoutOrder
local isButtonPressed = self.state.isButtonPressed
local sendButtonImage = SEND_BUTTON_ICON
if isButtonPressed then
sendButtonImage = SEND_BUTTON_ICON_PRESSED
end
return Roact.createElement("ImageButton", {
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
Size = UDim2.new(0, SEND_BUTTON_FULL_WIDTH, 1, 0),
[Roact.Event.InputBegan] = self.onSendButtonInputBegan,
[Roact.Event.InputEnded] = self.onSendButtonInputEnded,
[Roact.Event.Activated] = onActivated,
},{
SendButton = Roact.createElement("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
BackgroundTransparency = 1,
Image = sendButtonImage,
Position = UDim2.new(0, SEND_BUTTON_LEFT_PADDING, 0.5, 0),
Size = UDim2.new(0, SEND_BUTTON_ICON_SIZE, 0, SEND_BUTTON_ICON_SIZE),
}),
})
end
return SendToChatButton
@@ -0,0 +1,21 @@
return function()
local SendToChatButton = require(script.Parent.SendToChatButton)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
describe("SHOULD create and destroy without errors", function()
it("WHEN passed no props", function()
local element = Roact.createElement(SendToChatButton)
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end)
it("SHOULD return a number from getWidth", function()
local width = SendToChatButton.getWidth()
expect(type(width)).to.equal("number")
end)
end
@@ -0,0 +1,233 @@
local CoreGui = game:GetService("CoreGui")
local GuiService = game:GetService("GuiService")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local RoactAnalyticsSharedGameItem = require(Modules.LuaChat.Services.RoactAnalyticsSharedGameItem)
local RoactRodux = require(Modules.Common.RoactRodux)
local RoactServices = require(Modules.LuaApp.RoactServices)
local GameInformation = require(Modules.LuaChat.Components.ShareGameToChatFromChat.GameInformation)
local RoundedIcon = require(Modules.LuaChat.Components.ShareGameToChatFromChat.RoundedIcon)
local SendToChatButton = require(Modules.LuaChat.Components.ShareGameToChatFromChat.SendToChatButton)
local ConversationActions = require(Modules.LuaChat.Actions.ConversationActions)
local Constants = require(Modules.LuaChat.Constants)
local isInputTypeTouchOrMouseDown = require(Modules.LuaChat.Utils.isInputTypeTouchOrMouseDown)
local UNPRESSED_BACKGROUND_COLOR = Constants.Color.WHITE
local PRESSED_BACKGROUND_COLOR = Constants.Color.GRAY5
local DEFAULT_SEPARATOR_LINE_COLOR = Constants.Color.GRAY4
local TOTAL_HEIGHT = 84
local GAME_ICON_SIZE = Constants.SharedGamesConfig.Thumbnail.SHOWN_SIZE
local GAME_ICON_LEFT_PADDING = 15
local GAME_ICON_RIGHT_PADDING = 12
local GAME_ICON_TOP_PADDING = 12
local GAME_ICON_FULL_WIDTH = GAME_ICON_SIZE + GAME_ICON_LEFT_PADDING + GAME_ICON_RIGHT_PADDING
local GAME_LOADING_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png"
local SEND_BUTTON_WIDTH = SendToChatButton.getWidth()
local function verticalSpacer(props)
local height = props.height
local layoutOrder = props.LayoutOrder
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, height),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
end
local function horizontalSpacer(props)
local width = props.width
local layoutOrder = props.LayoutOrder
return Roact.createElement("Frame", {
Size = UDim2.new(0, width, 1, 0),
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
})
end
local SharedGameItem = Roact.PureComponent:extend("SharedGameItem")
function SharedGameItem:init()
self.state = {
isGameInputDown = false,
}
self.onGameButtonActivated = function()
-- TODO: SOC-3805 Can remove string coercion when using proper model
local universeId = self.props.game and tostring(self.props.game.universeId)
if universeId then
local notificationType = GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED
GuiService:BroadcastNotification(string.format("%s", universeId), notificationType)
end
end
self.onGameItemInputBegan = function(_, inputObject)
if isInputTypeTouchOrMouseDown(inputObject) then
self:setState({
isGameInputDown = true,
})
end
end
self.onGameItemInputEnded = function(_, inputObject)
if isInputTypeTouchOrMouseDown(inputObject) then
self:setState({
isGameInputDown = false,
})
end
end
self.onSendButtonActivated = function()
local gameModel = self.props.game
local activeConversationId = self.props.activeConversationId
local analytics = self.props.analytics
local gameUrl = self.props.gameUrl
local isSharing = self.props.isSharing
local universeId = gameModel and tostring(gameModel.universeId)
if universeId then
if not isSharing then
self.props.shareGameToChat(activeConversationId, analytics, universeId, gameUrl)
end
end
end
end
function SharedGameItem:render()
local gameModel = self.props.game
local layoutOrder = self.props.layoutOrder
local gameThumbnails = self.props.gameThumbnails
-- TODO: SOC-3805 `gameModel` should be using an actual model.
-- Currently this is the raw data derived from the WebApi endpoint
-- (called from ShareGameToChatFromChatThunks:FetchGames)
-- This means we have to coerce all ids into strings manually here
local gameId = gameModel and tostring(gameModel.universeId)
local gameIcon = gameId and gameThumbnails[gameId] or GAME_LOADING_ICON
local isGameInputDown = self.state.isGameInputDown
local backgroundColor = UNPRESSED_BACKGROUND_COLOR
if isGameInputDown then
backgroundColor = PRESSED_BACKGROUND_COLOR
end
return Roact.createElement("Frame", {
BackgroundColor3 = backgroundColor,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
Size = UDim2.new(1, 0, 0, TOTAL_HEIGHT),
},{
Content = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
}, {
Layout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
GameButtonContainer = Roact.createElement("ImageButton", {
BackgroundTransparency = 1,
LayoutOrder = 1,
Size = UDim2.new(1, -SEND_BUTTON_WIDTH, 1, 0),
[Roact.Event.Activated] = self.onGameButtonActivated,
[Roact.Event.InputBegan] = self.onGameItemInputBegan,
[Roact.Event.InputEnded] = self.onGameItemInputEnded,
},{
LayoutHorizontal = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
LeftMargin = Roact.createElement(horizontalSpacer, {
width = GAME_ICON_LEFT_PADDING,
LayoutOrder = 1,
}),
GameIconSection = Roact.createElement("Frame", {
BackgroundTransparency = 1,
LayoutOrder = 2,
Size = UDim2.new(0, GAME_ICON_SIZE, 1, 0),
}, {
LayoutVertical = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
TopMargin = Roact.createElement(verticalSpacer, {
height = GAME_ICON_TOP_PADDING,
LayoutOrder = 1,
}),
GameIcon = Roact.createElement(RoundedIcon, {
Image = gameIcon,
Size = UDim2.new(0, GAME_ICON_SIZE, 0, GAME_ICON_SIZE),
LayoutOrder = 2,
}),
}),
IconToInfoPadding = Roact.createElement(horizontalSpacer, {
width = GAME_ICON_RIGHT_PADDING,
LayoutOrder = 3,
}),
GameInfoAntiPadding = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -GAME_ICON_FULL_WIDTH, 1, 0),
LayoutOrder = 3,
}, {
GameInfo = gameModel and Roact.createElement(GameInformation, {
gameModel = gameModel,
}),
})
}),
SendButtonContainer = Roact.createElement(SendToChatButton, {
onActivated = self.onSendButtonActivated,
LayoutOrder = 2,
}),
}),
Separator = Roact.createElement("Frame", {
AnchorPoint = Vector2.new(0, 1),
BackgroundColor3 = DEFAULT_SEPARATOR_LINE_COLOR,
BorderSizePixel = 0,
Position = UDim2.new(0, GAME_ICON_FULL_WIDTH, 1, 0),
Size = UDim2.new(1, -GAME_ICON_FULL_WIDTH, 0, 1),
}),
})
end
SharedGameItem = RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
-- TODO: SOC-3392 Kill activeConversationId and pass conversationId as routing prop
activeConversationId = state.ChatAppReducer.ActiveConversationId,
gameThumbnails = state.GameThumbnails,
-- TODO: SOC-3896 Allow this to come from parent component and not in store
isSharing = state.ChatAppReducer.ShareGameToChatAsync.sharingGame,
}
end,
function(dispatch)
return {
shareGameToChat = function(activeConversationId, analytics, universeId)
analytics.reportShareGameToChatFromChat(activeConversationId, tostring(universeId))
return dispatch(ConversationActions.ShareGame(activeConversationId, universeId))
end,
}
end
)(SharedGameItem)
SharedGameItem = RoactServices.connect({
analytics = RoactAnalyticsSharedGameItem,
})(SharedGameItem)
return SharedGameItem
@@ -0,0 +1,69 @@
return function()
local SharedGameItem = require(script.Parent.SharedGameItem)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local AppReducer = require(Modules.LuaApp.AppReducer)
local Roact = require(Modules.Common.Roact)
local Rodux = require(Modules.Common.Rodux)
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
local MOCK_UNIVERSE_ID = "MOCK_UNIVERSE_ID"
local MOCK_THUMBNAIL = "MOCK_THUMBNAIL"
-- TODO: SOC-3805 `gameModel` should be using an actual model.
local function createValidGameModel(universeId)
return {
imageToken = "mock-token",
name = "mock-name",
universeId = universeId,
isPlayable = true,
price = 1000,
creatorName = "mock-creator",
}
end
local storeWithGameThumbnails = Rodux.Store.new(AppReducer, {
GameThumbnails = {
[MOCK_UNIVERSE_ID] = MOCK_THUMBNAIL,
},
})
local function createSharedGameItemTest(universeId)
local element = mockServices({
SharedGameItem = Roact.createElement(SharedGameItem, {
game = createValidGameModel(universeId),
}),
}, {
includeStoreProvider = true,
store = storeWithGameThumbnails,
})
return element
end
describe("SHOULD create and destroy without errors", function()
it("WHEN passed no props", function()
local element = mockServices({
SharedGameItem = Roact.createElement(SharedGameItem),
}, {
includeStoreProvider = true,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("WHEN passed a valid gameModel prop", function()
local element = createSharedGameItemTest(MOCK_UNIVERSE_ID)
local folder = Instance.new("Folder")
local instance = Roact.mount(element, folder)
local gameInfoInstance = folder:FindFirstChild("GameInfo", true)
expect(gameInfoInstance).to.be.ok()
Roact.unmount(instance)
end)
end)
end
@@ -0,0 +1,190 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local LuaApp = Modules.LuaApp
local LuaChat = Modules.LuaChat
local Immutable = require(Modules.Common.Immutable)
local memoize = require(Modules.Common.memoize)
local Roact = require(Modules.Common.Roact)
local RoactRodux = require(Modules.Common.RoactRodux)
local RoactServices = require(Modules.LuaApp.RoactServices)
local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking)
local LuaAppConstants = require(LuaApp.Constants)
local LuaChatConstants = require(LuaChat.Constants)
local LocalizedTextLabel = require(LuaApp.Components.LocalizedTextLabel)
local LoadingBar = require(LuaApp.Components.LoadingBar)
local RefreshScrollingFrame = require(LuaApp.Components.RefreshScrollingFrame)
local SharedGameItem = require(LuaChat.Components.SharedGameItem)
local ApiFetchGamesData = require(LuaApp.Thunks.ApiFetchGamesData)
local ApiFetchGamesInSort = require(LuaApp.Thunks.ApiFetchGamesInSort)
local RetrievalStatus = require(LuaApp.Enum.RetrievalStatus)
local GAME_ITEM_HEIGHT = 84
local NO_GAMES_TIPS_TOP_PADDING = 30
local NO_GAMES_TIPS_LABEL_HEIGHT = 25
local SharedGamesList = Roact.PureComponent:extend("SharedGamesList")
SharedGamesList.defaultProps = {
visible = false
}
function SharedGamesList:init()
self.loadMoreGames = function(count)
self.isLoadingMore = true
local loadCount = count or LuaChatConstants.DEFAULT_GAME_FETCH_COUNT
local sorts = self.props.sorts
local selectedSortName = self.props.gameSort
local dispatchLoadMoreGames = self.props.dispatchLoadMoreGames
local networking = self.props.networking
local selectedSort = sorts[selectedSortName]
return dispatchLoadMoreGames(networking, selectedSort, selectedSort.rowsRequested, loadCount,
selectedSort.nextPageExclusiveStartId)
end
self.refresh = function()
return self.props.dispatchRefresh(self.props.networking, self.props.gameSort)
end
if self.props.visible then
self.refresh()
end
end
function SharedGamesList:willUpdate()
if not self.props.fetchingSortTokenStatus or
self.props.fetchingSortTokenStatus == RetrievalStatus.Fetching then
return
end
if not self.props.fetchingGamesStatus then
self.refresh()
end
end
function SharedGamesList:render()
if not self.props.visible then
return nil
end
if not self.props.fetchingSortTokenStatus or
self.props.fetchingSortTokenStatus == RetrievalStatus.Fetching then
return Roact.createElement(LoadingBar)
end
local games = self.props.games
local sortName = self.props.gameSort
local sorts = self.props.sorts
local selectedSort = sorts[sortName]
local gameCount = #selectedSort.entries
local hasMoreRows = selectedSort.hasMoreRows
local gamesItems = {}
gamesItems["Layout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
})
for index = 1, gameCount do
local uid = selectedSort.entries[index].universeId
gamesItems[index] = Roact.createElement(SharedGameItem, {
game = games[uid],
itemHeight = GAME_ITEM_HEIGHT,
layoutOrder = index,
})
end
if (not self.props.fetchingGamesStatus or self.props.fetchingGamesStatus == RetrievalStatus.Fetching)
and gameCount == 0 then
return Roact.createElement(LoadingBar)
end
return Roact.createElement(RefreshScrollingFrame, {
BackgroundColor3 = LuaChatConstants.Color.GRAY6,
CanvasSize = UDim2.new(1, 0, 0, 0),
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, 0, 1, 0),
onLoadMore = hasMoreRows and self.loadMoreGames,
refresh = self.refresh,
}, {
SharedGamesList = gameCount > 0 and Roact.createElement("Frame", {
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, gameCount * GAME_ITEM_HEIGHT),
LayoutOrder = 3,
}, gamesItems),
NoGamesTip = (gameCount == 0) and Roact.createElement(LocalizedTextLabel, {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Font = Enum.Font.SourceSans,
Position = UDim2.new(0, 0, 0, NO_GAMES_TIPS_TOP_PADDING),
Size = UDim2.new(1, 0, 0, NO_GAMES_TIPS_LABEL_HEIGHT),
Text = LuaChatConstants.SharedGamesConfig.SortsAttribute[sortName].ERROR_TIP_LOCALIZATION_KEY,
TextColor3 = LuaChatConstants.Color.GRAY2,
TextSize = 20,
}),
})
end
local function sortSorts(a, b)
return a.displayName:lower() < b.displayName:lower()
end
local selectSorts = memoize(function(gameSorts, gameSortsContents)
local sorts = {}
for _, sortInfo in pairs(gameSorts) do
local gameSortContents = gameSortsContents[sortInfo.name]
local sort = Immutable.JoinDictionaries(sortInfo, {
entries = gameSortContents.entries,
rowsRequested = gameSortContents.rowsRequested,
hasMoreRows = gameSortContents.hasMoreRows,
nextPageExclusiveStartId = gameSortContents.nextPageExclusiveStartId,
})
table.insert(sorts, sort)
sorts[sort.name] = sort
end
table.sort(sorts, sortSorts)
return sorts
end)
SharedGamesList = RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
fetchingSortTokenStatus = state.RequestsStatus.GameSortTokenFetchingStatus[LuaAppConstants.GameSortGroups.ChatGames],
fetchingGamesStatus = state.RequestsStatus.GameSortsStatus[props.gameSort],
games = state.Games,
sorts = selectSorts(state.GameSorts, state.GameSortsContents),
}
end,
function(dispatch)
return {
dispatchRefresh = function(networking, sortName)
return dispatch(ApiFetchGamesData(networking, nil, sortName))
end,
dispatchLoadMoreGames = function(networking, sort, startRows, maxRows, nextPageExclusiveStartId)
return dispatch(ApiFetchGamesInSort(networking, sort, true, {
startRows = startRows,
maxRows = maxRows,
exclusiveStartId = nextPageExclusiveStartId
}))
end
}
end
)(SharedGamesList)
return RoactServices.connect({
networking = RoactNetworking,
})(SharedGamesList)
@@ -0,0 +1,21 @@
return function()
local SharedGamesList = require(script.Parent.SharedGamesList)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
it("should create and destroy without errors", function()
local element = mockServices({
SharedGamesList = Roact.createElement(SharedGamesList, {
gameSort = "Popular",
}),
}, {
includeStoreProvider = true,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end