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,10 @@
{
"lint": {
"SameLineStatement": "fatal",
//"LocalShadow": "fatal",
"LocalUnused": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,7 @@
local CorePackages = game:GetService("CorePackages")
--TODO: Currently Under Migration to CorePackages
local Action = require(CorePackages.AppTempCommon.Common.Action)
--
return Action("GetRecentlyPlayedGames", function()
return {}
end)
@@ -0,0 +1,36 @@
return function()
describe("require", function()
it("should create without errors", function()
require(script.Parent.GetRecentlyPlayedGames)
end)
it("should set the name", function()
local action = require(script.Parent.GetRecentlyPlayedGames)
expect(action.name).to.equal("GetRecentlyPlayedGames")
end)
end)
describe("call", function()
it("should return a table when called as a function", function()
local action = require(script.Parent.GetRecentlyPlayedGames)
action = action({})
expect(action).to.be.a("table")
end)
it("should set the type", function()
local action = require(script.Parent.GetRecentlyPlayedGames)
action = action({})
expect(action.type).to.equal("GetRecentlyPlayedGames")
end)
it("should set the type and name to be equal", function()
local action = require(script.Parent.GetRecentlyPlayedGames)
local actionItem = action({})
expect(actionItem.type).to.equal(action.name)
end)
end)
end
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
--TODO: Currently Under Migration to CorePackages
local Action = require(CorePackages.AppTempCommon.Common.Action)
--
return Action("SetRecentlyPlayedGames", function(gameSortData)
return {
gameSortData = gameSortData,
}
end)
@@ -0,0 +1,44 @@
return function()
describe("require", function()
it("should create without errors", function()
require(script.Parent.SetRecentlyPlayedGames)
end)
it("should set the name", function()
local action = require(script.Parent.SetRecentlyPlayedGames)
expect(action.name).to.equal("SetRecentlyPlayedGames")
end)
end)
describe("call", function()
it("should return a table when called as a function", function()
local action = require(script.Parent.SetRecentlyPlayedGames)
action = action({})
expect(action).to.be.a("table")
end)
it("should set the type", function()
local action = require(script.Parent.SetRecentlyPlayedGames)
action = action({})
expect(action.type).to.equal("SetRecentlyPlayedGames")
end)
it("should set the games", function()
local action = require(script.Parent.SetRecentlyPlayedGames)
local gameSortData = {}
action = action(gameSortData)
expect(action.gameSortData).to.be.a("table")
end)
it("should set the type and name to be equal", function()
local action = require(script.Parent.SetRecentlyPlayedGames)
local actionItem = action({})
expect(actionItem.type).to.equal(action.name)
end)
end)
end
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
--TODO: Currently Under Migration to CorePackages
local Action = require(CorePackages.AppTempCommon.Common.Action)
--
return Action("SetRecentlyPlayedGamesFetching", function(fetching)
return {
fetching = fetching,
}
end)
@@ -0,0 +1,43 @@
return function()
describe("require", function()
it("should create without errors", function()
require(script.Parent.SetRecentlyPlayedGamesFetching)
end)
it("should set the name", function()
local action = require(script.Parent.SetRecentlyPlayedGamesFetching)
expect(action.name).to.equal("SetRecentlyPlayedGamesFetching")
end)
end)
describe("call", function()
it("should return a table when called as a function", function()
local action = require(script.Parent.SetRecentlyPlayedGamesFetching)
action = action({})
expect(action).to.be.a("table")
end)
it("should set the type", function()
local action = require(script.Parent.SetRecentlyPlayedGamesFetching)
action = action({})
expect(action.type).to.equal("SetRecentlyPlayedGamesFetching")
end)
it("should set fetching status.", function()
local action = require(script.Parent.SetRecentlyPlayedGamesFetching)
action = action(true)
expect(action.fetching).to.equal(true)
end)
it("should set the type and name to be equal", function()
local action = require(script.Parent.SetRecentlyPlayedGamesFetching)
local actionItem = action({})
expect(actionItem.type).to.equal(action.name)
end)
end)
end
@@ -0,0 +1,11 @@
--[[
Returns a blank selector image.
]]
return function()
local BlankSelectionImage = Instance.new("ImageLabel")
BlankSelectionImage.Name = "BlankSelectorImage"
BlankSelectionImage.Position = UDim2.new(0, 0, 0, 0)
BlankSelectionImage.Size = UDim2.new(1, 0, 1, 0)
BlankSelectionImage.BackgroundTransparency = 1
return BlankSelectionImage
end
@@ -0,0 +1,17 @@
--[[
Returns a selection image with rounded corners.
]]
local Settings = script.Parent.Parent
local Constants = require(Settings.Pages.LeaveGameScreen.Constants)
return function()
local RoundedSelectionImage = Instance.new("ImageLabel")
RoundedSelectionImage.Name = "SelectorImage"
RoundedSelectionImage.Image = Constants.Image.BUTTON_SELECTOR
RoundedSelectionImage.Position = UDim2.new(0, -7, 0, -7)
RoundedSelectionImage.Size = UDim2.new(1, 14, 1, 14)
RoundedSelectionImage.BackgroundTransparency = 1
RoundedSelectionImage.ScaleType = Enum.ScaleType.Slice
RoundedSelectionImage.SliceCenter = Rect.new(31, 31, 63, 63)
return RoundedSelectionImage
end
@@ -0,0 +1,97 @@
--[[
Creates a Roact component that is a blue rounded button
Props:
position : UDim2 - Position of the button.
zIndex : number - Determines the order of UI element rendering.
anchorPoint : UDim2 - The anchor point of the button.
objRef : Ref - The ref of this button.
]]
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Settings = script.Parent.Parent
local Modules = Settings.Parent
local Common = Modules.Common
local Analytics = require(Common.Analytics)
local Constants = require(Settings.Pages.LeaveGameScreen.Constants)
local RoundedSelectionImage = require(Settings.Common.RoundedSelectionImage)
local RobloxTranslator = require(Modules.RobloxTranslator)
local LeaveButton = Roact.PureComponent:extend("LeaveButton")
local LEAVE_GAME_FRAME_WAITS = 2
local LEAVE_GAME_BUTTON_LABEL = "Feature.SettingsHub.Label.LeaveButton"
-- NOTE: event context might need to be changed for other platforms.
local EVENT_CONTEXT = "XboxOne"
function LeaveButton:init()
self.analytics = Analytics.new()
self.onSelectionGained = function()
self:setState({
focused = true,
})
end
self.onSelectionLost = function()
self:setState({
focused = false,
})
end
self.leaveFunc = function()
GuiService.SelectedCoreObject = nil -- deselects the button and prevents spamming the popup to save in studio when using gamepad
-- need to wait for render frames so on slower devices the leave button highlight will update
-- otherwise, since on slow devices it takes so long to leave you are left wondering if you pressed the button
for i = 1, LEAVE_GAME_FRAME_WAITS do
RunService.RenderStepped:wait()
end
local eventName = "LeaveGame"
self.analytics.EventStream:setRBXEventStream(EVENT_CONTEXT, eventName)
game:Shutdown()
end
end
function LeaveButton:render()
local size = UDim2.new(0, 320, 0, 80)
local position = self.props.position or UDim2.new(0.5, 0, 0.5, 0)
local anchorPoint = self.props.anchorPoint or Vector2.new(0.5, 0.5)
local zIndex = self.props.zIndex
local text = RobloxTranslator:FormatByKey(LEAVE_GAME_BUTTON_LABEL)
local textColor = Constants.Color.WHITE
local selector = RoundedSelectionImage()
if self.state.focused then
textColor = Constants.Color.DARK
end
local label = Roact.createElement("TextLabel", {
Text = text,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Font = Enum.Font.SourceSans,
TextSize = Constants.TextSize.BUTTON,
TextXAlignment = Enum.TextXAlignment.Center,
TextColor3 = textColor,
BackgroundTransparency = 1,
ZIndex = zIndex + 1,
})
return Roact.createElement("ImageButton", {
Image = Constants.Image.BUTTON,
Size = size,
Position = position,
AnchorPoint = anchorPoint,
[Roact.Ref] = self.props.objRef,
[Roact.Event.SelectionGained] = self.onSelectionGained,
[Roact.Event.SelectionLost] = self.onSelectionLost,
[Roact.Event.Activated] = self.leaveFunc,
ImageColor3 = Constants.Color.BLUE,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = selector,
BackgroundTransparency = 1,
ZIndex = zIndex,
},{
Label = label,
})
end
return LeaveButton
@@ -0,0 +1,105 @@
--[[
A Roact play next game icon. When activated, the player will join that game.
Props:
universeId : number - The universe id of the game.
placeId: number - The place id of the game.
imageToken: string - The image token of the game thumbnail.
size : UDim2 - The size of the game icon.
position : position - The position of the game icon.
layoutOrder : number - The sort order of the game icon.
zIndex : number - Determines the order of UI element rendering.
focused : bool - Determines if the game icon focused.
onSelectionGained : function() - The callback function for selection gained.
objRef : Ref - The ref of the game icon.
]]
local CorePackages = game:GetService("CorePackages")
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local Roact = require(CorePackages.Roact)
local Otter = require(CorePackages.Otter)
local Settings = script.Parent.Parent.Parent
local Modules = Settings.Parent
local Common = Modules.Common
local Analytics = require(Common.Analytics)
local BlankSelectionImage = require(Settings.Common.BlankSelectionImage)
local Utility = require(Settings.Utility)
local GameIcon = Roact.PureComponent:extend("GameIcon")
local NORMAL_SIZE = 200
local FOCUSED_SIZE = 232
-- NOTE: event context might need to be changed for other platforms.
local EVENT_CONTEXT = "XboxOne"
function GameIcon:init()
self.ref = Roact.createRef()
self.analytics = Analytics.new()
self.motorOptions = {
dampingRatio = 1,
frequency = 5,
}
self.PlayNextGame = function()
if PlatformService then
local eventName = "PlayNextGame"
local args = {
UniverseID = self.props.universeId,
PlaceId = self.props.placeId
}
self.analytics.EventStream:setRBXEventStream(EVENT_CONTEXT, eventName, args)
PlatformService:PlayNextGame(self.props.placeId)
end
end
end
function GameIcon:render()
local layoutOrder = self.props.layoutOrder
local size = UDim2.new(0, NORMAL_SIZE, 0, NORMAL_SIZE)
local position = self.props.position
local image = string.format("https://games.roblox.com/v1/games/game-thumbnail?imageToken=%s&height=250&width=250", self.props.imageToken)
local zIndex = self.props.zIndex
local selector = BlankSelectionImage()
return Roact.createElement("ImageButton",
{
Size = size,
Position = position,
AnchorPoint = Vector2.new(0.5, 0.5),
Image = image,
BackgroundColor3 = Color3.fromRGB(0,0,0),
BackgroundTransparency = 0.8,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
SelectionImageObject = selector,
[Roact.Event.SelectionGained] = self.props.onSelectionGained,
[Roact.Event.Activated] = function()
self.PlayNextGame(self.props.placeId)
end,
[Roact.Ref] = self.ref,
ZIndex = zIndex,
})
end
function GameIcon:didMount()
self.motor = Otter.createSingleMotor(NORMAL_SIZE)
self.motor:onStep(function(value)
local t = value/100
if self.ref.current ~= nil then
local size = Utility:Lerp(t, FOCUSED_SIZE, NORMAL_SIZE)
size = Utility:Round(size)
self.ref.current.Size = UDim2.new(0, size, 0, size)
end
end)
self.motor:start()
self.motor:setGoal(Otter.instant(100))
end
function GameIcon:didUpdate(previousProps, previousState)
if self.props.focused == previousProps.focused then
return
end
local targetPercentage = self.props.focused and 0 or 100
self.motor:setGoal(Otter.spring(targetPercentage, self.motorOptions))
end
return GameIcon
@@ -0,0 +1,28 @@
--[[
A Roact play next game selector.
Props:
position : UDim2 - Position of the button.
anchorPoint : UDim2 - The anchor point of the button.
imageTransparency : number - The transparency of the image.
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Settings = script.Parent.Parent.Parent
local Constants = require(Settings.Pages.LeaveGameScreen.Constants)
local GameSelector = Roact.PureComponent:extend("GameSelector")
function GameSelector:render()
return Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 248, 0, 269),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
Image = Constants.Image.PLAY_NEXT_SELECTOR,
ImageTransparency = self.props.imageTransparency,
BackgroundTransparency = 1,
ZIndex = self.props.ZIndex,
})
end
return GameSelector
@@ -0,0 +1,123 @@
--[[
A scrolling games list
Props:
focused : bool - Determines if the game list focused.
zIndex : number - Determines the order of UI element rendering.
onMoveSelection : function - The callback function for on the game selection changed.
gamesList : array - An array of games. each element in the array is a map that must contain:
{
universeId : number,
placeId : number,
imageToken : string,
}
]]
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Settings = script.Parent.Parent.Parent
local Modules = Settings.Parent
local Common = Modules.Common
local Analytics = require(Common.Analytics)
local GameIcon = require(Settings.Components.PlayNextGame.GameIcon)
local GamesScrollList = Roact.PureComponent:extend("GamesScrollList")
-- NOTE: event context might need to be changed for other platforms.
local EVENT_CONTEXT = "XboxOne"
function GamesScrollList:init()
self.pageLayout = Roact.createRef()
self.analytics = Analytics.new()
self.setSelection = function(index)
if self.state.selectionIndex ~= index then
local eventName = "MoveNextGameSelection"
self.analytics.EventStream:setRBXEventStream(EVENT_CONTEXT, eventName)
self:setState({
selectionIndex = index
})
end
self.props.onMoveSelection(index)
end
end
function GamesScrollList:render()
local focused = self.props.focused
local gamesList = self.props.gamesList
local zIndex = self.props.zIndex
local children = {}
children.layout = Roact.createElement("UIPageLayout", {
Animated = true,
Circular = true,
EasingDirection = Enum.EasingDirection.InOut,
EasingStyle = Enum.EasingStyle.Quad,
Padding = UDim.new(0, 20),
TweenTime = 0.125,
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
GamepadInputEnabled = true,
[Roact.Ref] = self.pageLayout,
[Roact.Event.PageEnter] = function(rbx)
self.pageLayout.current:ApplyLayout()
end
})
-- NOTE:
-- we want to display 5, but it looks janky when we wraps. So double the elements to
-- fake the look of it wrapping. We can discuss with product about this. Maybe we can have
-- 6 or 7 games instead or duplicating the games.
--
for i in ipairs(gamesList) do
children[tostring(i)] = Roact.createElement(GameIcon, {
universeId = gamesList[i].universeId,
placeId = gamesList[i].placeId,
imageToken = gamesList[i].imageToken,
layoutOrder = i - 1,
focused = focused and self.state.selectionIndex == i,
onSelectionGained = function()
self.setSelection(i)
end,
zIndex = zIndex,
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(0, 1100, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
ClipsDescendants = true,
}, {
FocusedItem = Roact.createElement("Frame", {
Size = UDim2.new(0, 200, 0, 200),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}, children)
})
end
function GamesScrollList:didMount()
self.pageLayout.current:JumpToIndex(0)
-- UIPageLayout relies on child add order. This force applies the layout
-- will not be needed with Quantum GUI
self.pageLayout.current:ApplyLayout()
if self.props.focused then
delay(0, function()
GuiService.SelectedCoreObject = self.pageLayout.current.CurrentPage
end)
end
end
function GamesScrollList:didUpdate(previousProps, previousState)
if self.props.focused == previousProps.focused then
return
end
if self.props.focused then
GuiService.SelectedCoreObject = self.pageLayout.current.CurrentPage
end
end
return GamesScrollList
@@ -0,0 +1,286 @@
--[[
A Roact play next game list section of the leave game screen.
Props:
screenId : Variant the screen id of this section.
zIndex : number - Determines the order of UI element rendering.
focused : bool - Determines if this section is focused.
onLeaveNextGameList : function() - The callback function for when the selection leaves this section
]]
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Otter = require(CorePackages.Otter)
local Settings = script.Parent.Parent.Parent
local Modules = Settings.Parent
local Constants = require(Settings.Pages.LeaveGameScreen.Constants)
local GamesScrollList = require(Settings.Components.PlayNextGame.GamesScrollList)
local GameSelector = require(Settings.Components.PlayNextGame.GameSelector)
local Utility = require(Settings.Utility)
local RobloxTranslator = require(Modules.RobloxTranslator)
local PlayNextGameXbox = Roact.PureComponent:extend("PlayNextGameXbox")
local FRAME_HIDE_HEIGHT = 375
local FRAME_SHOW_HEIGHT = 547
local BG_HIDE_TRANSPARENCY = 1
local BG_SHOW_TRANSPARENCY = 0.5
local SELECTOR_HIDE_TRANSPARENCY = 1
local SELECTOR_SHOW_TRANSPARENCY = 0
local RECENTLY_PLAYED_LABEL = "Feature.SettingsHub.Label.RecentPlayed"
function PlayNextGameXbox:init()
self.screenId = self.props.screenId
self.ref = Roact.createRef()
self.selectorRef = Roact.createRef()
-- Initialize parameters for the spring motion
self.motorOptions = {
dampingRatio = 1,
frequency = 5,
}
self.onMoveSelection = function(index)
if self.state.currentSelection ~= index then
self:setState({
currentSelection = index
})
end
end
self.state =
{
currentSelection = 1,
moveSelection = true,
}
end
function PlayNextGameXbox:render()
local props = self.props
local focused = props.focused
local backgroundTransparency = 1
local selectorTransparency = focused and 0 or 1
local gamesdata = self.props.gamesData
local size = UDim2.new(1, 0, 0, FRAME_HIDE_HEIGHT)
local zIndex = self.props.zIndex + 1
local indicatorArrow;
local gameTitle;
local gameStats;
local gameSelector;
local dividerSize = UDim2.new(0, 864, 0, 2)
if focused then
local upVotes = gamesdata[self.state.currentSelection].totalUpVotes
local downVotes = gamesdata[self.state.currentSelection].totalDownVotes
local gameRating = 0
if upVotes + downVotes ~= 0 then
gameRating = math.floor(((upVotes / (upVotes + downVotes)) * 100) + 0.5);
end
gameTitle = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, 60),
Position = UDim2.new(0.5, 0, 0, 355),
AnchorPoint = Vector2.new(0.5, 0),
Text = gamesdata[self.state.currentSelection].name,
Font = Enum.Font.SourceSans,
FontSize = Enum.FontSize.Size48,
TextColor3 = Constants.Color.WHITE,
BackgroundTransparency = 1,
TextWrapped = true,
ZIndex = zIndex,
})
gameStats = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 28),
Position = UDim2.new(0.5, 0, 0, 420),
AnchorPoint = Vector2.new(0.5, 0),
BackgroundTransparency = 1,
},{
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, 15),
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
GameRating = Roact.createElement("Frame", {
Size = UDim2.new(0, 90, 0, 30),
BackgroundTransparency = 1,
LayoutOrder = 1,
},{
Icon = Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 28, 0, 28),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Image = Constants.Image.THUMB_ICON,
BackgroundTransparency = 1,
ZIndex = zIndex,
}),
Text = Roact.createElement("TextLabel", {
Size = UDim2.new(0, 60, 0, 30),
Position = UDim2.new(0, 36, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Text = tostring(gameRating)..' %',
Font = Enum.Font.SourceSans,
FontSize = Enum.FontSize.Size24,
TextColor3 = Constants.Color.WHITE,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
ZIndex = zIndex,
}),
}),
NumOfPlayers = Roact.createElement("Frame", {
Size = UDim2.new(0, 100, 0, 30),
BackgroundTransparency = 1,
LayoutOrder = 2,
},{
Icon = Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 28, 0, 28),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Image = Constants.Image.PLAYER_NUMBER_ICON,
BackgroundTransparency = 1,
ZIndex = zIndex,
}),
Text = Roact.createElement("TextLabel", {
Size = UDim2.new(0, 60, 1, 0),
Position = UDim2.new(0, 36, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Text = tostring(gamesdata[self.state.currentSelection].playerCount),
Font = Enum.Font.SourceSans,
FontSize = Enum.FontSize.Size24,
TextColor3 = Constants.Color.WHITE,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
ZIndex = zIndex,
}),
}),
})
gameSelector = Roact.createElement(GameSelector, {
position = UDim2.new(0.5, 0, 0, 192),
anchorPoint = Vector2.new(0.5, 0.5),
imageTransparency = selectorTransparency,
objRef = self.selectorRef,
onActivated = self.onSelectGame,
ZIndex = zIndex + 1,
})
else
indicatorArrow = Roact.createElement("ImageLabel", {
Image = Constants.Image.DOWN_ARROW,
Size = UDim2.new(0, 24, 0, 12),
AnchorPoint = Vector2.new(0.5, 0),
Position = UDim2.new(0.5, 0, 0, 40),
BackgroundTransparency = 1,
ZIndex = zIndex,
})
end
return Roact.createElement("Frame", {
Size = size,
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, 0),
BackgroundTransparency = backgroundTransparency,
BackgroundColor3 = Color3.fromRGB(1, 1, 1),
BorderSizePixel = 0,
ZIndex = zIndex,
[Roact.Ref] = self.ref,
},{
LeftDivider = Roact.createElement("Frame", {
Size = dividerSize,
AnchorPoint = Vector2.new(0, 0),
Position = UDim2.new(0, 0, 0, 0),
BorderSizePixel = 0,
BackgroundColor3 = Constants.Color.WHITE,
ZIndex = zIndex,
}),
RightDivider = Roact.createElement("Frame", {
Size = dividerSize,
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 0),
BorderSizePixel = 0,
BackgroundColor3 = Constants.Color.WHITE,
ZIndex = zIndex,
}),
NextGameListLabel = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, 30),
Position = UDim2.new(0.5, 0, 0, -3),
AnchorPoint = Vector2.new(0.5, 0.5),
--TODO: Need to be localized.
Text = RobloxTranslator:FormatByKey(RECENTLY_PLAYED_LABEL),
Font = Enum.Font.SourceSans,
FontSize = Enum.FontSize.Size24,
TextColor3 = Constants.Color.WHITE,
BackgroundTransparency = 1,
TextWrapped = true,
TextXAlignment = "Center",
TextYAlignment = "Center",
ZIndex = zIndex + 1,
}),
IndicatorArrow = indicatorArrow,
GamesList = Roact.createElement("Frame",{
Position = UDim2.new(0.5, 0, 0, 66),
AnchorPoint = Vector2.new(0.5, 0),
Size = UDim2.new(1, 0, 0, 232),
BackgroundTransparency = 1,
},{
GamesScrollList = Roact.createElement(GamesScrollList,{
focused = focused,
onMoveSelection = self.onMoveSelection,
gamesList = gamesdata,
zIndex = zIndex,
}),
}),
GameTitle = gameTitle,
GameStats = gameStats,
GameSelector = gameSelector,
Redirect = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 10),
Position = UDim2.new(0.5, 0, 0, -100),
AnchorPoint = Vector2.new(0.5, 0),
Selectable = true,
BackgroundTransparency = 1,
[Roact.Event.SelectionGained] = function()
GuiService.SelectedCoreObject = nil
self.props.onLeaveNextGameList()
end
}),
})
end
function PlayNextGameXbox:didMount()
delay(0, function()
if self.props.focused and self.ref.current then
GuiService:AddSelectionParent(self.screenId, self.ref.current)
end
end)
self.motor = Otter.createSingleMotor(0)
self.motor:onStep(function(value)
local t = value/100
if self.ref.current ~= nil then
self.ref.current.Size = UDim2.new(1, 0, 0, Utility:Round(Utility:Lerp(t, FRAME_HIDE_HEIGHT, FRAME_SHOW_HEIGHT)))
self.ref.current.BackgroundTransparency = Utility:Lerp(t, BG_HIDE_TRANSPARENCY, BG_SHOW_TRANSPARENCY)
end
if self.selectorRef.current ~= nil then
self.selectorRef.current.ImageTransparency = Utility:Lerp(t, SELECTOR_HIDE_TRANSPARENCY, SELECTOR_SHOW_TRANSPARENCY)
end
end)
self.motor:start()
end
function PlayNextGameXbox:didUpdate(previousProps, previousState)
if self.props.focused == previousProps.focused then
return
end
GuiService:RemoveSelectionGroup(self.screenId)
if self.props.focused and self.ref.current then
GuiService:AddSelectionParent(self.screenId, self.ref.current)
end
local targetPercentage = self.props.focused and 100 or 0
self.motor:setGoal(Otter.spring(targetPercentage, self.motorOptions))
end
function PlayNextGameXbox:willUnmount()
GuiService:RemoveSelectionGroup(self.screenId)
self.motor:destroy()
end
return PlayNextGameXbox
@@ -0,0 +1,78 @@
--[[
Creates a Roact component that is a grey rounded button
Props:
position : UDim2 - Position of the button.
zIndex : number - Determines the order of UI element rendering.
anchorPoint : UDim2 - The anchor point of the button.
onResume : function() - Fires when the button is activated.
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Settings = script.Parent.Parent
local Modules = Settings.Parent
local Constants = require(Settings.Pages.LeaveGameScreen.Constants)
local RoundedSelectionImage = require(Settings.Common.RoundedSelectionImage)
local RobloxTranslator = require(Modules.RobloxTranslator)
local ResumeButton = Roact.PureComponent:extend("ResumeButton")
local RESUME_GAME_BUTTON_LABEL = "Feature.SettingsHub.Label.DontLeaveButton"
function ResumeButton:init()
self.onSelectionGained = function()
self:setState({
focused = true
})
end
self.onSelectionLost = function()
self:setState({
focused = false
})
end
end
function ResumeButton:render()
local size = UDim2.new(0, 320, 0, 80)
local position = self.props.position or UDim2.new(0.5, 0, 0.5, 0)
local anchorPoint = self.props.anchorPoint or Vector2.new(0.5, 0.5)
local zIndex = self.props.zIndex
local text = RobloxTranslator:FormatByKey(RESUME_GAME_BUTTON_LABEL)
local buttonColor = Constants.Color.GREY
local textColor = Constants.Color.WHITE
local selector = RoundedSelectionImage()
if self.state.focused then
buttonColor = Constants.Color.BLUE
textColor = Constants.Color.DARK
end
local label = Roact.createElement("TextLabel",{
Text = text,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Font = Enum.Font.SourceSans,
TextSize = Constants.TextSize.BUTTON,
TextXAlignment = Enum.TextXAlignment.Center,
TextColor3 = textColor,
BackgroundTransparency = 1,
ZIndex = zIndex + 1
})
return Roact.createElement("ImageButton",{
Image = Constants.Image.BUTTON,
Size = size,
Position = position,
AnchorPoint = anchorPoint,
[Roact.Event.SelectionGained] = self.onSelectionGained,
[Roact.Event.SelectionLost] = self.onSelectionLost,
[Roact.Event.Activated] = self.props.onResume,
ImageColor3 = buttonColor,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = selector,
BackgroundTransparency = 1,
ZIndex = zIndex
},{
Label = label,
})
end
return ResumeButton
@@ -0,0 +1,44 @@
--[[
{
universeId : number,
placeId : number,
name : string,
playerCount : number,
imageToken : string,
totalUpVotes : number,
totalDownVotes : number,
}
]]
local Game = {}
function Game.new()
local self = {}
return self
end
function Game.fromGameCache(gameCache)
local self = Game.new()
self.universeId = gameCache.UniverseId
self.placeId = gameCache.PlaceId
self.name = gameCache.Name
self.playerCount = gameCache.PlayerCount
self.imageToken = gameCache.ImageToken
self.totalUpVotes = gameCache.VoteData.UpVotes
self.totalDownVotes = gameCache.VoteData.DownVotes
return self
end
function Game.fromJsonData(gameJson)
local self = Game.new()
self.universeId = gameJson.universeId
self.placeId = gameJson.placeId
self.name = gameJson.name
self.playerCount = gameJson.playerCount
self.imageToken = gameJson.imageToken
self.totalUpVotes = gameJson.totalUpVotes
self.totalDownVotes = gameJson.totalDownVotes
return self
end
return Game
@@ -0,0 +1,692 @@
--!nocheck
--[[
Filename: Help.lua
Written by: jeditkacheff
Version 1.0
Description: Takes care of the help page in Settings Menu
--]]
------------ FFLAGS -------------------
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
-------------- CONSTANTS --------------
local KEYBOARD_MOUSE_TAG = "KeyboardMouse"
local TOUCH_TAG = "Touch"
local GAMEPAD_TAG = "Gamepad"
local PC_TABLE_SPACING = 4
local XBOX_CONTROLLER_IMAGE_OFFSET = 30
local TEXT_EDGE_DISTANCE = 20
-------------- SERVICES --------------
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local TextService = game:GetService("TextService")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
----------- UTILITIES --------------
local utility = require(RobloxGui.Modules.Settings.Utility)
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
------------ Variables -------------------
local PageInstance = nil
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
this.HelpPages = {}
this.HelpPageContents = {}
this.ActiveHelpScheme = nil
local lastInputType = nil
function this:GetCurrentInputType()
if lastInputType == nil then -- we don't know what controls the user has, just use reasonable defaults
local platform = UserInputService:GetPlatform()
if platform == Enum.Platform.XBoxOne or platform == Enum.Platform.WiiU then
return GAMEPAD_TAG
elseif platform == Enum.Platform.Windows or platform == Enum.Platform.OSX then
return KEYBOARD_MOUSE_TAG
else
return TOUCH_TAG
end
end
if lastInputType == Enum.UserInputType.Keyboard or lastInputType == Enum.UserInputType.MouseMovement or
lastInputType == Enum.UserInputType.MouseButton1 or lastInputType == Enum.UserInputType.MouseButton2 or
lastInputType == Enum.UserInputType.MouseButton3 or lastInputType == Enum.UserInputType.MouseWheel then
return KEYBOARD_MOUSE_TAG
elseif lastInputType == Enum.UserInputType.Touch then
return TOUCH_TAG
elseif lastInputType == Enum.UserInputType.Gamepad1 or lastInputType == Enum.UserInputType.Gamepad2 or
lastInputType == Enum.UserInputType.Gamepad3 or lastInputType == Enum.UserInputType.Gamepad4 then
return GAMEPAD_TAG
end
return KEYBOARD_MOUSE_TAG
end
local function createPCHelp(parentFrame)
local function createPCGroup(title, actionInputBindings)
local textIndent = 9
local pcGroupFrame = utility:Create'Frame'
{
Size = UDim2.new(1/3,-PC_TABLE_SPACING,1,0),
BackgroundTransparency = 1,
Name = "PCGroupFrame" .. tostring(title)
};
local pcGroupTitle = utility:Create'TextLabel'
{
Position = UDim2.new(0,textIndent,0,0),
Size = UDim2.new(1,-textIndent,0,30),
BackgroundTransparency = 1,
Text = title,
Font = Enum.Font.SourceSansBold,
FontSize = Enum.FontSize.Size18,
TextColor3 = Color3.new(1,1,1),
TextXAlignment = Enum.TextXAlignment.Left,
Name = "PCGroupTitle" .. tostring(title),
ZIndex = 2,
Parent = pcGroupFrame
};
local count = 0
local frameHeight = 42
local spacing = 2
local offset = pcGroupTitle.Size.Y.Offset
for i = 1, #actionInputBindings do
for actionName, inputName in pairs(actionInputBindings[i]) do
local actionInputFrame = utility:Create'Frame'
{
Size = UDim2.new(1,0,0,frameHeight),
Position = UDim2.new(0,0,0, offset + ((frameHeight + spacing) * count)),
BackgroundTransparency = 0.65,
BorderSizePixel = 0,
ZIndex = 2,
Name = "ActionInputBinding" .. tostring(actionName),
Parent = pcGroupFrame
};
local nameLabel = utility:Create'TextLabel'
{
Size = UDim2.new(0.4,-textIndent,0,frameHeight),
Position = UDim2.new(0,textIndent,0,0),
BackgroundTransparency = 1,
Text = actionName,
Font = Enum.Font.SourceSansBold,
FontSize = Enum.FontSize.Size18,
TextColor3 = Color3.new(1,1,1),
TextXAlignment = Enum.TextXAlignment.Left,
Name = actionName .. "Label",
ZIndex = 2,
Parent = actionInputFrame,
TextWrapped = true,
TextScaled = true
};
do
local textSizeConstraint = Instance.new("UITextSizeConstraint",nameLabel)
textSizeConstraint.MaxTextSize = 18
end
local inputLabel = utility:Create'TextLabel'
{
Size = UDim2.new(0.5,0,0,frameHeight),
Position = UDim2.new(0.5,-4,0,0),
BackgroundTransparency = 1,
Text = inputName,
Font = Enum.Font.SourceSans,
FontSize = Enum.FontSize.Size18,
TextColor3 = Color3.new(1,1,1),
TextXAlignment = Enum.TextXAlignment.Left,
Name = inputName .. "Label",
ZIndex = 2,
Parent = actionInputFrame,
TextWrapped = true,
TextScaled = true
};
do
local textSizeConstraint = Instance.new("UITextSizeConstraint",inputLabel)
textSizeConstraint.MaxTextSize = 18
end
count = count + 1
end
end
pcGroupFrame.Size = UDim2.new(pcGroupFrame.Size.X.Scale,pcGroupFrame.Size.X.Offset,
0, offset + ((frameHeight + spacing) * count))
return pcGroupFrame
end
local rowOffset = 50
local isOSX = UserInputService:GetPlatform() == Enum.Platform.OSX
local charMoveFrame = createPCGroup( "Character Movement", {
[1] = {["Move Forward"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.W) .. "/"
..RobloxTranslator:FormatByKey("InGame.HelpMenu.UpArrow")},
[2] = {["Move Backward"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.S) .. "/"
..RobloxTranslator:FormatByKey("InGame.HelpMenu.DownArrow")},
[3] = {["Move Left"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.A) .. "/"
..RobloxTranslator:FormatByKey("InGame.HelpMenu.LeftArrow")},
[4] = {["Move Right"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.D) .. "/"
..RobloxTranslator:FormatByKey("InGame.HelpMenu.RightArrow")},
[5] = {["Jump"] = "Space"}} )
charMoveFrame.Parent = parentFrame
local oneChar = UserInputService:GetStringForKeyCode(Enum.KeyCode.One)
local twoChar = UserInputService:GetStringForKeyCode(Enum.KeyCode.Two)
local threeChar = UserInputService:GetStringForKeyCode(Enum.KeyCode.Three)
local toolKeys = oneChar .. "," .. twoChar .. "," .. threeChar .. "..."
local accessoriesFrame = createPCGroup("Accessories", {
[1] = {["Equip Tools"] = toolKeys},
[2] = {["Unequip Tools"] = toolKeys},
[3] = {["Drop Tool"] = "Backspace"},
[4] = {["Use Tool"] = "Left Mouse Button"} })
accessoriesFrame.Position = UDim2.new(1/3,PC_TABLE_SPACING,0,0)
accessoriesFrame.Parent = parentFrame
local miscActions = {}
local canShowRecordAndStats = not PolicyService:IsSubjectToChinaPolicies()
if canShowRecordAndStats then
table.insert(miscActions, {["Screenshot"] = isOSX and "Cmd + Shift + 3" or "Print Screen"})
if not isOSX then
table.insert(miscActions, {["Record Video"] = "F12"})
end
end
if canShowRecordAndStats then
table.insert(miscActions, {["Dev Console"] = isOSX and "F9/fn + F9" or "F9"})
end
table.insert(miscActions, {["Mouselock"] = "Shift"})
if canShowRecordAndStats then
table.insert(miscActions, {["Graphics Level"] = isOSX and "F10/fn + F10" or "F10"})
table.insert(miscActions, {["Fullscreen"] = isOSX and "F11/fn + F11" or "F11"})
end
if canShowRecordAndStats then
table.insert(miscActions, {["Perf. Stats"] = isOSX and "Fn+Opt+Cmd+F7" or "Ctrl + Shift + F7"})
end
local miscFrame = createPCGroup("Misc", miscActions)
miscFrame.Position = UDim2.new(2/3,PC_TABLE_SPACING * 2,0,0)
miscFrame.Parent = parentFrame
local camFrame = createPCGroup("Camera Movement", {
[1] = {["Rotate"] = "Right Mouse Button"},
[2] = {["Zoom In/Out"] = "Mouse Wheel"},
[3] = {["Zoom In"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.I)},
[4] = {["Zoom Out"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.O)}
})
camFrame.Position = UDim2.new(0,0,charMoveFrame.Size.Y.Scale,charMoveFrame.Size.Y.Offset + rowOffset)
camFrame.Parent = parentFrame
local menuFrame = createPCGroup("Menu Items", {
[1] = {["Roblox Menu"] = "Esc"},
[2] = {["Backpack"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.Backquote)},
[3] = {["Playerlist"] = "Tab"},
[4] = {["Chat"] = UserInputService:GetStringForKeyCode(Enum.KeyCode.Slash)} })
menuFrame.Position = UDim2.new(1/3,PC_TABLE_SPACING,charMoveFrame.Size.Y.Scale,charMoveFrame.Size.Y.Offset + rowOffset)
menuFrame.Parent = parentFrame
parentFrame.Size = UDim2.new(parentFrame.Size.X.Scale, parentFrame.Size.X.Offset, 0,
menuFrame.Size.Y.Offset + menuFrame.Position.Y.Offset)
end
local function createGamepadHelp(parentFrame)
local gamepadImage = nil
local imageSize = nil
gamepadImage = "rbxasset://textures/ui/Settings/Help/GenericController.png"
imageSize = UDim2.new(0, 473, 0, 287)
local imagePosition = UDim2.new(0.5, -imageSize.X.Offset/2, 0.5, -imageSize.Y.Offset/2)
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne or UserInputService:GetPlatform() == Enum.Platform.XBox360 then
gamepadImage = "rbxasset://textures/ui/Settings/Help/XboxController.png"
imageSize = UDim2.new(0, 745, 0, 452)
imagePosition = UDim2.new(0.5, (-imageSize.X.Offset/2) + XBOX_CONTROLLER_IMAGE_OFFSET, 0.5, -imageSize.Y.Offset/2 + 7)
elseif UserInputService:GetPlatform() == Enum.Platform.PS4 or UserInputService:GetPlatform() == Enum.Platform.PS3 then
gamepadImage = "rbxasset://textures/ui/Settings/Help/PSController.png"
end
local gamepadImageLabel = utility:Create'ImageLabel'
{
Name = "GamepadImage",
Size = imageSize,
Position = imagePosition,
Image = gamepadImage,
BackgroundTransparency = 1,
ZIndex = 2,
Parent = parentFrame
};
parentFrame.Size = UDim2.new(parentFrame.Size.X.Scale, parentFrame.Size.X.Offset, 0, gamepadImageLabel.Size.Y.Offset + 100)
local gamepadFontSize = isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size24
local textVerticalSize = (gamepadFontSize == Enum.FontSize.Size36) and 36 or 24
local function createGamepadLabel(text, position, size, rightAligned)
local nameLabel = nil
if FFlagUseNotificationsLocalization == true then
nameLabel = utility:Create'TextLabel'{
Position = position,
Size = size,
BackgroundTransparency = 1,
Text = text,
TextXAlignment = rightAligned and Enum.TextXAlignment.Right or Enum.TextXAlignment.Left,
AnchorPoint = rightAligned and Vector2.new(1, 0.5) or Vector2.new(0, 0.5),
Font = Enum.Font.SourceSansBold,
FontSize = gamepadFontSize,
TextColor3 = Color3.new(1,1,1),
Name = text .. "Label",
ZIndex = 2,
Parent = gamepadImageLabel,
TextScaled = true,
TextWrapped = true
};
else
nameLabel = utility:Create'TextLabel'{
Position = position,
Size = size,
BackgroundTransparency = 1,
Text = text,
TextXAlignment = rightAligned and Enum.TextXAlignment.Right or Enum.TextXAlignment.Left,
AnchorPoint = rightAligned and Vector2.new(1, 0.5) or Vector2.new(0, 0.5),
Font = Enum.Font.SourceSansBold,
FontSize = gamepadFontSize,
TextColor3 = Color3.new(1,1,1),
Name = text .. "Label",
ZIndex = 2,
Parent = gamepadImageLabel
};
end
nameLabel.TextWrapped = true
local textSize = TextService:GetTextSize(text, textVerticalSize, Enum.Font.SourceSansBold, Vector2.new(0, 0))
local minSizeXOffset = textSize.X
local distanceToCenter = math.abs(position.X.Offset)
local parentGui = (gamepadImage == "rbxasset://textures/ui/Settings/Help/XboxController.png") and RobloxGui or parentFrame
local function updateNameLabelSize()
local nameLabelSizeXOffset = nameLabel.Size.X.Offset
if gamepadImage == "rbxasset://textures/ui/Settings/Help/XboxController.png" then
nameLabelSizeXOffset = rightAligned and
RobloxGui.AbsoluteSize.X/2 + XBOX_CONTROLLER_IMAGE_OFFSET - distanceToCenter - TEXT_EDGE_DISTANCE or
RobloxGui.AbsoluteSize.X/2 - XBOX_CONTROLLER_IMAGE_OFFSET - distanceToCenter - TEXT_EDGE_DISTANCE
else
nameLabelSizeXOffset = parentFrame.AbsoluteSize.X/2 - distanceToCenter
end
if nameLabelSizeXOffset < minSizeXOffset then
nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabelSizeXOffset, nameLabel.Size.Y.Scale, textVerticalSize * 2)
nameLabel.TextScaled = true
else
nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabelSizeXOffset, nameLabel.Size.Y.Scale, textVerticalSize)
nameLabel.FontSize = gamepadFontSize
nameLabel.TextScaled = false
end
end
local _nameLabelChangeCn = parentGui:GetPropertyChangedSignal('AbsoluteSize'):connect(function()
updateNameLabelSize()
end)
updateNameLabelSize()
end
if gamepadImage == "rbxasset://textures/ui/Settings/Help/XboxController.png" then
createGamepadLabel("Switch Tool", UDim2.new(0.5, -390, 0, 0), UDim2.new(0, 100, 0, textVerticalSize), true)
createGamepadLabel("Game Menu Toggle", UDim2.new(0.5, -390, 0.15, 0), UDim2.new(0, 164, 0, textVerticalSize), true)
createGamepadLabel("Move", UDim2.new(0.5, -390, 0.31, 0), UDim2.new(0, 46, 0, textVerticalSize), true)
createGamepadLabel("Menu Navigation", UDim2.new(0.5, -390, 0.46, 0), UDim2.new(0, 164, 0, textVerticalSize), true)
createGamepadLabel("Use Tool", UDim2.new(0.5, 330, 0, 0), UDim2.new(0, 73, 0, textVerticalSize))
createGamepadLabel("Roblox Menu", UDim2.new(0.5, 330, 0.15, 0), UDim2.new(0, 122, 0, textVerticalSize))
createGamepadLabel("Back", UDim2.new(0.5, 330, 0.31, 0), UDim2.new(0, 43, 0, textVerticalSize))
createGamepadLabel("Jump", UDim2.new(0.5, 330, 0.46, 0), UDim2.new(0, 49, 0, textVerticalSize))
createGamepadLabel("Rotate Camera", UDim2.new(0.5, 380, 0.62, 0), UDim2.new(0, 132, 0, textVerticalSize))
createGamepadLabel("Camera Zoom", UDim2.new(0.5, 380, 0.77, 0), UDim2.new(0, 122, 0, textVerticalSize))
else
createGamepadLabel("Switch Tool", UDim2.new(0.5, -250, 0, 0), UDim2.new(0, 100, 0, textVerticalSize), true)
createGamepadLabel("Game Menu Toggle", UDim2.new(0.5, -250, 0.15, 0), UDim2.new(0, 164, 0, textVerticalSize), true)
createGamepadLabel("Move", UDim2.new(0.5, -250, 0.31, 0), UDim2.new(0, 46, 0, textVerticalSize), true)
createGamepadLabel("Menu Navigation", UDim2.new(0.5, -250, 0.46, 0), UDim2.new(0, 143, 0, textVerticalSize), true)
createGamepadLabel("Use Tool", UDim2.new(0.5, 215, 0, 0), UDim2.new(0, 73, 0, textVerticalSize))
createGamepadLabel("Roblox Menu", UDim2.new(0.5, 215, 0.15, 0), UDim2.new(0, 122, 0, textVerticalSize))
createGamepadLabel("Back", UDim2.new(0.5, 215, 0.31, 0), UDim2.new(0, 43, 0, textVerticalSize))
createGamepadLabel("Jump", UDim2.new(0.5, 215, 0.46, 0), UDim2.new(0, 49, 0, textVerticalSize))
createGamepadLabel("Rotate Camera", UDim2.new(0.5, 255, 0.62, 0), UDim2.new(0, 132, 0, textVerticalSize))
createGamepadLabel("Camera Zoom", UDim2.new(0.5, 255, 0.77, 0), UDim2.new(0, 122, 0, textVerticalSize))
end
-- NOTE: On consoles we put the dev console in the settings menu. Only place
-- owners can see this for now.
end
local function updateTouchLayout(scheme) -- adjust layout to work well with the various touch control schemes
this.ActiveHelpScheme = scheme
local isPortrait = utility:IsPortrait()
local helpFrame = this.HelpPages[TOUCH_TAG]
if helpFrame then
local helpElements = this.HelpPageContents[TOUCH_TAG]
local function hideUneeded(list)
if list then
for name, item in pairs(helpElements) do
item.Visible = not list[name]
end
end
end
local hidden
if scheme == Enum.TouchMovementMode.DynamicThumbstick or scheme == Enum.TouchMovementMode.Default then
-- show that movement is done by dragging
-- show that tapping on bottom of the screen is to jump
-- show that tapping on the top of the screen is to use tools
-- show that dragging on the top of the screen is to pan camera
hidden = {MoveImageCTM = true}
helpElements["MoveLabel"].Position = isPortrait and UDim2.new(0.25,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.75,-50) or UDim2.new(0.15,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.85,-helpElements["MoveLabel"].AbsoluteSize.y)
helpElements["JumpLabel"].Position = isPortrait and UDim2.new(0.75,-helpElements["JumpLabel"].AbsoluteSize.x/2,0.75,-50) or UDim2.new(0.85,-60,0.85,-helpElements["JumpLabel"].AbsoluteSize.y)
helpElements["RotateLabel"].Position = isPortrait and UDim2.new(1,-helpElements["RotateLabel"].AbsoluteSize.x-20,0.02,0) or UDim2.new(0.85,-helpElements["RotateLabel"].AbsoluteSize.x/2,0.02,0)
helpElements["UseToolLabel"].Position = isPortrait and UDim2.new(0.5,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.5,-100) or UDim2.new(0.5,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.5,-60)
helpElements["EquipLabel"].Position = isPortrait and UDim2.new(0.5,-60,0.75,50) or UDim2.new(0.5,-60,0.64,0)
helpElements["ZoomLabel"].Position = isPortrait and UDim2.new(0,20,0.02,0) or UDim2.new(0.15,-60,0.02,0)
elseif scheme == Enum.TouchMovementMode.ClickToMove then
-- show that dragging on the screen is to pan the camera
-- show that tapping is to move
hidden = {BottomHalfDisplay = true, MoveImageDTS = true, JumpLabel = true} -- cant manually jump on ctm
helpElements["MoveLabel"].Position = isPortrait and UDim2.new(0.25,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.5,0) or UDim2.new(0.25,-helpElements["MoveLabel"].AbsoluteSize.x/2,0.5,40)
helpElements["RotateLabel"].Position = isPortrait and UDim2.new(1,-helpElements["RotateLabel"].AbsoluteSize.x-20,0.02,0) or UDim2.new(0.5,-60,0.02,0)
helpElements["UseToolLabel"].Position = isPortrait and UDim2.new(0.75,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.5,0) or UDim2.new(0.85,-60,0.02,0)
helpElements["EquipLabel"].Position = isPortrait and UDim2.new(0.5,-60,0.75,50) or UDim2.new(0.5,-60,0.64,0)
helpElements["ZoomLabel"].Position = isPortrait and UDim2.new(0,20,0.02,0) or UDim2.new(0.15,-60,0.02,0)
else
-- keep the default style, but take portrait mode into account
hidden = {BottomHalfDisplay = true, MoveImageDTS = true, MoveImageCTM = true, JumpImage = true} -- if theres a jump button we don't need to do touch gestures, same with thumbstick
helpElements["MoveLabel"].Position = isPortrait and UDim2.new(0.06,0,1,-120) or UDim2.new(0.06,0,0.58,0)
helpElements["JumpLabel"].Position = isPortrait and UDim2.new(0.94,-helpElements["JumpLabel"].AbsoluteSize.x,1,-120) or UDim2.new(0.8,0,0.58,0)
helpElements["RotateLabel"].Position = isPortrait and UDim2.new(1,-helpElements["RotateLabel"].AbsoluteSize.x-20,0.02,0) or UDim2.new(0.5,-60,0.02,0)
helpElements["UseToolLabel"].Position = isPortrait and UDim2.new(.5,-helpElements["UseToolLabel"].AbsoluteSize.x/2,0.25,50) or UDim2.new(0.85,-60,0.02,0)
helpElements["EquipLabel"].Position = isPortrait and UDim2.new(0.5,-60,0.75,50) or UDim2.new(0.5,-60,0.64,0)
helpElements["ZoomLabel"].Position = isPortrait and UDim2.new(0,20,0.02,0) or UDim2.new(0.15,-60,0.02,0)
end
hideUneeded(hidden)
end
end
local function createTouchHelp(parentFrame)
local createdElements = {} -- dictionary of buttons created
local smallScreen = utility:IsSmallTouchScreen()
local ySize = GuiService:GetScreenResolution().y - 350
if smallScreen then
ySize = GuiService:GetScreenResolution().y - 100
end
parentFrame.Size = UDim2.new(1,0,0,ySize)
local function createDisplayFrame(name, position, size, transparency, color, parent)
local frame = utility:Create'Frame'
{
BackgroundColor3 = color,
Position = position,
Size = size,
BackgroundTransparency = transparency,
Name = name,
ZIndex = 1,
Parent = parent
};
return frame
end
local function createTouchLabel(text, position, size, parent)
local nameFrame = utility:Create'TextLabel'
{
Position = position,
Size = size,
BackgroundTransparency = 1,
Name = text .. "Frame",
Parent = parent,
}
local nameLabel = utility:Create'TextLabel'
{
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Text = text,
Font = Enum.Font.SourceSansBold,
FontSize = Enum.FontSize.Size14,
TextColor3 = Color3.new(1,1,1),
Name = text .. "Label",
ZIndex = 3,
Parent = nameFrame,
TextScaled = true,
TextWrapped = true
};
if not smallScreen then
nameLabel.FontSize = Enum.FontSize.Size18
nameLabel.Size = UDim2.new(nameLabel.Size.X.Scale, nameLabel.Size.X.Offset, nameLabel.Size.Y.Scale, nameLabel.Size.Y.Offset + 4)
end
local _nameBackgroundImage = utility:Create'ImageLabel'
{
Name = text .. "BackgroundImage",
Size = UDim2.new(1.25,0,1.25,0),
Position = UDim2.new(-0.125,0,-0.065,0),
BackgroundTransparency = 1,
Image = "rbxasset://textures/ui/Settings/Radial/RadialLabel.png",
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(12,2,65,21),
ZIndex = 2,
Parent = nameFrame
};
local textSizeConstraint = Instance.new("UITextSizeConstraint",nameLabel)
textSizeConstraint.MaxTextSize = 18
return nameFrame
end
local function createTouchGestureImage(name, image, position, size, parent)
local gestureImage = utility:Create'ImageLabel'
{
Name = name,
Size = size,
Position = position,
BackgroundTransparency = 1,
Image = image,
ZIndex = 2,
Parent = parent
};
return gestureImage
end
local xSizeOffset = 30
local ySize = 25
if smallScreen then xSizeOffset = 0 end
createdElements["BottomHalfDisplay"] = createDisplayFrame("BottomHalfFrame", UDim2.new(0,0,0.5,-16), UDim2.new(1,0,1,0), 0.35, Color3.new(0,0,0), parentFrame)
-- movement stuff
createdElements["MoveLabel"] = createTouchLabel("Move", UDim2.new(0.06,0,0.58,0), UDim2.new(0,77 + xSizeOffset,0,ySize), parentFrame)
createdElements["MoveImageDTS"] = createTouchGestureImage("MoveImageDTS", "rbxasset://textures/ui/Settings/Help/RotateCameraGesture.png", UDim2.new(0.5,-32,1,3), UDim2.new(0,65,0,48), createdElements["MoveLabel"])
createdElements["MoveImageCTM"] = createTouchGestureImage("MoveImageCTM", "rbxasset://textures/ui/Settings/Help/UseToolGesture.png", UDim2.new(0.5,-19,1,3), UDim2.new(0,38,0,52), createdElements["MoveLabel"])
-- jumping stuff
createdElements["JumpLabel"] = createTouchLabel("Jump", UDim2.new(0.8,0,0.58,0), UDim2.new(0,77 + xSizeOffset,0,ySize), parentFrame)
createdElements["JumpImage"] = createTouchGestureImage("JumpImage", "rbxasset://textures/ui/Settings/Help/UseToolGesture.png", UDim2.new(0.5,-19,1,3), UDim2.new(0,38,0,52), createdElements["JumpLabel"])
createdElements["EquipLabel"] = createTouchLabel("Equip/Unequip Tools", UDim2.new(0.5,-60,0.64,0), UDim2.new(0,120 + xSizeOffset,0,ySize), parentFrame)
createdElements["ZoomLabel"] = createTouchLabel("Zoom In/Out", UDim2.new(0.15,-60,0.02,0), UDim2.new(0,120,0,ySize), parentFrame)
createdElements["ZoomImage"] = createTouchGestureImage("ZoomImage", "rbxasset://textures/ui/Settings/Help/ZoomGesture.png", UDim2.new(0.5,-26,1,3), UDim2.new(0,53,0,59), createdElements["ZoomLabel"])
createdElements["RotateLabel"] = createTouchLabel("Rotate Camera", UDim2.new(0.5,-60,0.02,0), UDim2.new(0,120,0,ySize), parentFrame)
createdElements["RotateImage"] = createTouchGestureImage("RotateImage", "rbxasset://textures/ui/Settings/Help/RotateCameraGesture.png", UDim2.new(0.5,-32,1,3), UDim2.new(0,65,0,48), createdElements["RotateLabel"])
createdElements["UseToolLabel"] = createTouchLabel("Use Tool", UDim2.new(0.85,-60,0.02,0), UDim2.new(0,120,0,ySize), parentFrame)
createdElements["ToolImage"] = createTouchGestureImage("ToolImage", "rbxasset://textures/ui/Settings/Help/UseToolGesture.png", UDim2.new(0.5,-19,1,3), UDim2.new(0,38,0,52), createdElements["UseToolLabel"])
return createdElements
end
local function createHelpDisplay(typeOfHelp)
local helpContents = nil
local helpFrame = utility:Create'Frame'
{
Size = UDim2.new(1,0,1,0),
BackgroundTransparency = 1,
Name = "HelpFrame" .. tostring(typeOfHelp)
};
if typeOfHelp == KEYBOARD_MOUSE_TAG then
createPCHelp(helpFrame)
elseif typeOfHelp == GAMEPAD_TAG then
createGamepadHelp(helpFrame)
elseif typeOfHelp == TOUCH_TAG then
helpContents = createTouchHelp(helpFrame)
end
return helpFrame, helpContents
end
local function displayHelp(currentPage)
for i, helpPage in pairs(this.HelpPages) do
if helpPage == currentPage then
helpPage.Parent = this.Page
this.Page.Size = helpPage.Size
if isTenFootInterface then
this.Page.Size = UDim2.new(helpPage.Size.X.Scale, helpPage.Size.X.Offset, helpPage.Size.Y.Scale, 0)
end
else
helpPage.Parent = nil
end
end
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
this.HubRef.PageViewClipper.ClipsDescendants = false
this.HubRef.PageView.ClipsDescendants = false
end
end
local function switchToHelp(typeOfHelp)
local helpPage = this.HelpPages[typeOfHelp]
if helpPage then
displayHelp(helpPage)
if typeOfHelp == TOUCH_TAG then
-- update for the control scheme
local scheme = GameSettings.TouchMovementMode
if this.ActiveHelpScheme ~= scheme then
updateTouchLayout(scheme)
end
end
else
this.HelpPages[typeOfHelp], this.HelpPageContents[typeOfHelp] = createHelpDisplay(typeOfHelp)
switchToHelp(typeOfHelp)
end
end
local function showTypeOfHelp()
switchToHelp(this:GetCurrentInputType())
end
local function adjustForScreenLayout(givenSize) -- portrait mode was causing the help frame to be either too tall or short when changed between landscape mode and portrait.
if this:GetCurrentInputType() == TOUCH_TAG then
local scheme = GameSettings.TouchMovementMode
local smallScreen = utility:IsSmallTouchScreen()
local size = givenSize or GuiService:GetScreenResolution()
local ySize = size.y - 350
if smallScreen then
ySize = size.y - 100
end
this.HelpPages[TOUCH_TAG].Size = UDim2.new(1,0,0,ySize)
updateTouchLayout(scheme)
end
end
------ TAB CUSTOMIZATION -------
this.TabHeader.Name = "HelpTab"
this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/HelpTab.png"
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Text = "Help"
else
this.TabHeader.Icon.Title.Text = "Help"
end
------ PAGE CUSTOMIZATION -------
this.Page.Name = "Help"
UserInputService.InputBegan:connect(function(inputObject)
local inputType = inputObject.UserInputType
if inputType ~= Enum.UserInputType.Focus and inputType ~= Enum.UserInputType.None then
lastInputType = inputObject.UserInputType
showTypeOfHelp()
end
end)
utility:OnResized(this, function(newSize, isPortrait)
if this.HelpPages[TOUCH_TAG] then
adjustForScreenLayout(newSize)
end
end)
return this
end
----------- Public Facing API Additions --------------
do
PageInstance = Initialize()
PageInstance.Displayed.Event:connect(function()
local isPortrait = utility:IsPortrait()
if PageInstance:GetCurrentInputType() == TOUCH_TAG then
if PageInstance.HubRef.BottomButtonFrame and not utility:IsSmallTouchScreen() and not isPortrait then
PageInstance.HubRef.BottomButtonFrame.Visible = false
end
end
if PageInstance.HubRef.VersionContainer then
PageInstance.HubRef.VersionContainer.Visible = true
end
end)
PageInstance.Hidden.Event:connect(function()
PageInstance.HubRef.PageViewClipper.ClipsDescendants = true
PageInstance.HubRef.PageView.ClipsDescendants = true
PageInstance.HubRef:ShowShield()
local isPortrait = utility:IsPortrait()
if PageInstance:GetCurrentInputType() == TOUCH_TAG then
if PageInstance.HubRef.BottomButtonFrame and not utility:IsSmallTouchScreen() and not isPortrait then
PageInstance.HubRef.BottomButtonFrame.Visible = true
end
end
if PageInstance.HubRef.VersionContainer then
PageInstance.HubRef.VersionContainer.Visible = false
end
end)
end
return PageInstance
@@ -0,0 +1,103 @@
--[[
Filename: Home.lua
Written by: jeditkacheff
Version 1.0
Description: Takes care of the home page in Settings Menu
--]]
local BUTTON_OFFSET = 20
local BUTTON_SPACING = 10
-------------- SERVICES --------------
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GuiService = game:GetService("GuiService")
----------- UTILITIES --------------
local utility = require(RobloxGui.Modules.Settings.Utility)
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
------------ Variables -------------------
local PageInstance = nil
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local FFlagUpdateSettingsHubGameText = require(RobloxGui.Modules.Flags.FFlagUpdateSettingsHubGameText)
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
------ TAB CUSTOMIZATION -------
this.TabHeader.Name = "HomeTab"
this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/HomeTab.png"
this.TabHeader.Icon.Size = UDim2.new(0,32,0,30)
this.TabHeader.Icon.Position = UDim2.new(0,5,0.5,-15)
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Text = "Home"
else
this.TabHeader.Icon.Title.Text = "Home"
end
this.TabHeader.Size = UDim2.new(0,100,1,0)
------ PAGE CUSTOMIZATION -------
this.Page.Name = "Home"
local resumeGameFunc = function()
this.HubRef:SetVisibility(false)
end
local resumeGameText = "Resume Game"
if FFlagUpdateSettingsHubGameText then
resumeGameText = RobloxTranslator:FormatByKey("InGame.HelpMenu.Resume")
end
this.ResumeButton = utility:MakeStyledButton("ResumeButton", resumeGameText, UDim2.new(0, 200, 0, 50), resumeGameFunc)
this.ResumeButton.Position = UDim2.new(0.5,-100,0,BUTTON_OFFSET)
this.ResumeButton.Parent = this.Page
local resetFunc = function()
this.HubRef:SwitchToPage(this.HubRef.ResetCharacterPage, false, 1)
end
local resetButton = utility:MakeStyledButton("ResetButton", "Reset Character", UDim2.new(0, 200, 0, 50), resetFunc)
resetButton.Position = UDim2.new(0.5,-100,0,this.ResumeButton.AbsolutePosition.Y + this.ResumeButton.AbsoluteSize.Y + BUTTON_SPACING)
resetButton.Parent = this.Page
local leaveGameFunc = function()
this.HubRef:SwitchToPage(this.HubRef.LeaveGamePage, false, 1)
end
local leaveGameText = "Leave Game"
if FFlagUpdateSettingsHubGameText then
leaveGameText = RobloxTranslator:FormatByKey("InGame.HelpMenu.Leave")
end
local leaveButton = utility:MakeStyledButton("LeaveButton", leaveGameText, UDim2.new(0, 200, 0, 50), leaveGameFunc)
leaveButton.Position = UDim2.new(0.5,-100,0,resetButton.AbsolutePosition.Y + resetButton.AbsoluteSize.Y + BUTTON_SPACING)
leaveButton.Parent = this.Page
this.Page.Size = UDim2.new(1,0,0,leaveButton.AbsolutePosition.Y + leaveButton.AbsoluteSize.Y)
return this
end
----------- Public Facing API Additions --------------
do
PageInstance = Initialize()
PageInstance.Displayed.Event:connect(function()
if not utility:UsesSelectedObject() then return end
GuiService.SelectedCoreObject = PageInstance.ResumeButton
end)
end
return PageInstance
@@ -0,0 +1,174 @@
--[[
Filename: LeaveGame.lua
Written by: jeditkacheff
Version 1.0
Description: Takes care of the leave game in Settings Menu
--]]
-------------- CONSTANTS -------------
local LEAVE_GAME_ACTION = "LeaveGameCancelAction"
local LEAVE_GAME_FRAME_WAITS = 2
-------------- SERVICES --------------
local CoreGui = game:GetService("CoreGui")
local ContextActionService = game:GetService("ContextActionService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
----------- UTILITIES --------------
local utility = require(RobloxGui.Modules.Settings.Utility)
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
------------ Variables -------------------
local PageInstance = nil
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
local FFlagUpdateSettingsHubGameText = require(RobloxGui.Modules.Flags.FFlagUpdateSettingsHubGameText)
local GetFFlagAppUsesAutomaticQualityLevel = require(RobloxGui.Modules.Flags.GetFFlagAppUsesAutomaticQualityLevel)
local FFlagCollectAnalyticsForSystemMenu = settings():GetFFlag("CollectAnalyticsForSystemMenu")
local GetDefaultQualityLevel = require(RobloxGui.Modules.Common.GetDefaultQualityLevel)
local Constants
if FFlagCollectAnalyticsForSystemMenu then
Constants = require(RobloxGui.Modules:WaitForChild("InGameMenu"):WaitForChild("Resources"):WaitForChild("Constants"))
end
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
this.LeaveFunc = function()
GuiService.SelectedCoreObject = nil -- deselects the button and prevents spamming the popup to save in studio when using gamepad
if FFlagCollectAnalyticsForSystemMenu then
AnalyticsService:SetRBXEventStream(Constants.AnalyticsTargetName, Constants.AnalyticsInGameMenuName,
Constants.AnalyticsLeaveGameName, {confirmed = Constants.AnalyticsConfirmedName, universeid = tostring(game.GameId)})
end
-- need to wait for render frames so on slower devices the leave button highlight will update
-- otherwise, since on slow devices it takes so long to leave you are left wondering if you pressed the button
for i = 1, LEAVE_GAME_FRAME_WAITS do
RunService.RenderStepped:wait()
end
game:Shutdown()
if GetFFlagAppUsesAutomaticQualityLevel() then
settings().Rendering.QualityLevel = GetDefaultQualityLevel()
end
end
this.DontLeaveFunc = function(isUsingGamepad)
if this.HubRef then
this.HubRef:PopMenu(isUsingGamepad, true)
end
if FFlagCollectAnalyticsForSystemMenu then
AnalyticsService:SetRBXEventStream(Constants.AnalyticsTargetName, Constants.AnalyticsInGameMenuName,
Constants.AnalyticsLeaveGameName, {confirmed = Constants.AnalyticsCancelledName, universeid = tostring(game.GameId)})
end
end
this.DontLeaveFromHotkey = function(name, state, input)
if state == Enum.UserInputState.Begin then
local isUsingGamepad = input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2
or input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4
this.DontLeaveFunc(isUsingGamepad)
end
end
this.DontLeaveFromButton = function(isUsingGamepad)
this.DontLeaveFunc(isUsingGamepad)
end
------ TAB CUSTOMIZATION -------
this.TabHeader = nil -- no tab for this page
------ PAGE CUSTOMIZATION -------
this.Page.Name = "LeaveGamePage"
this.ShouldShowBottomBar = false
this.ShouldShowHubBar = false
local leaveGameConfirmationText = "Are you sure you want to leave the game?"
if FFlagUpdateSettingsHubGameText then
leaveGameConfirmationText = RobloxTranslator:FormatByKey("InGame.HelpMenu.ConfirmLeaveGame")
end
local leaveGameText = utility:Create'TextLabel'
{
Name = "LeaveGameText",
Text = leaveGameConfirmationText,
Font = Enum.Font.SourceSansBold,
FontSize = Enum.FontSize.Size36,
TextColor3 = Color3.new(1,1,1),
BackgroundTransparency = 1,
Size = UDim2.new(1,0,0,200),
TextWrapped = true,
ZIndex = 2,
Parent = this.Page,
Position = isTenFootInterface and UDim2.new(0,0,0,100) or UDim2.new(0,0,0,0)
};
local leaveButtonContainer = utility:Create"Frame"
{
Name = "LeaveButtonContainer",
Parent = leaveGameText,
Size = UDim2.new(1,0,0,400),
BackgroundTransparency = 1,
Position = UDim2.new(0,0,1,0)
};
local _leaveButtonLayout = utility:Create'UIGridLayout'
{
Name = "LeavetButtonsLayout",
CellSize = isTenFootInterface and UDim2.new(0, 300, 0, 80) or UDim2.new(0, 200, 0, 50),
CellPadding = UDim2.new(0,20,0,20),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
Parent = leaveButtonContainer
};
if utility:IsSmallTouchScreen() then
leaveGameText.FontSize = Enum.FontSize.Size24
leaveGameText.Size = UDim2.new(1,0,0,100)
elseif isTenFootInterface then
leaveGameText.FontSize = Enum.FontSize.Size48
end
this.LeaveGameButton = utility:MakeStyledButton("LeaveGame", "Leave", nil, this.LeaveFunc)
this.LeaveGameButton.NextSelectionRight = nil
this.LeaveGameButton.Parent = leaveButtonContainer
------------- Init ----------------------------------
local dontleaveGameButton = utility:MakeStyledButton("DontLeaveGame", "Don't Leave", nil, this.DontLeaveFromButton)
dontleaveGameButton.NextSelectionLeft = nil
dontleaveGameButton.Parent = leaveButtonContainer
this.Page.Size = UDim2.new(1,0,0,dontleaveGameButton.AbsolutePosition.Y + dontleaveGameButton.AbsoluteSize.Y)
return this
end
----------- Public Facing API Additions --------------
PageInstance = Initialize()
PageInstance.Displayed.Event:connect(function()
GuiService.SelectedCoreObject = PageInstance.LeaveGameButton
ContextActionService:BindCoreAction(LEAVE_GAME_ACTION, PageInstance.DontLeaveFromHotkey, false, Enum.KeyCode.ButtonB)
end)
PageInstance.Hidden.Event:connect(function()
ContextActionService:UnbindCoreAction(LEAVE_GAME_ACTION)
end)
return PageInstance
@@ -0,0 +1,23 @@
local Constants = {
Color = {
BLUE = Color3.fromRGB(0, 162, 255),
GREY = Color3.fromRGB(155, 155, 155),
WHITE = Color3.fromRGB(255, 255, 255),
DARK = Color3.fromRGB(25, 25, 25),
},
TextSize = {
BUTTON = 36
},
Image = {
BUTTON = "rbxasset://textures/ui/Settings/LeaveGame/Button_1080.png",
BUTTON_SELECTOR = "rbxasset://textures/ui/Settings/LeaveGame/gr-item selector-8px corner.png",
PLAY_NEXT_SELECTOR = "rbxasset://textures/ui/Settings/LeaveGame/selectorWithIcon.png",
DOWN_ARROW = "rbxasset://textures/ui/Settings/LeaveGame/artAssets_DownArrow.png",
THUMB_ICON = "rbxasset://textures/ui/Settings/LeaveGame/thumb_strokeStyle.png",
PLAYER_NUMBER_ICON = "rbxasset://textures/ui/Settings/LeaveGame/playernumber_strokeStyle.png",
},
}
return Constants
@@ -0,0 +1,230 @@
--[[
A Roact play next game screen.
Props:
screenId : Variant
zIndex : number
ref : Roact.Ref
focused : bool
closeScreen : function()
numOfGames : number
]]
local GuiService = game:GetService("GuiService")
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Otter = require(CorePackages.Otter)
local Settings = script.Parent.Parent.Parent
local Modules = Settings.Parent
local Common = Modules.Common
local Analytics = require(Common.Analytics)
local Constants = require(Settings.Pages.LeaveGameScreen.Constants)
local ResumeButton = require(Settings.Components.ResumeButton)
local LeaveButton = require(Settings.Components.LeaveButton)
local Utility = require(Settings.Utility)
local RobloxTranslator = require(Modules.RobloxTranslator)
--NOTE: This play next game view can be swaped different version such as one for touch and PC
local PlayNextGameView = require(Settings.Components.PlayNextGame.PlayNextGameXbox)
local ApiFetchRecentlyPlayedGames = require(Settings.Thunks.ApiFetchRecentlyPlayedGames)
local LEAVE_BUTTON_KEY = "LeaveButton"
local DONT_LEAVE_BUTTON_KEY = "DontLeaveButton"
local NEXT_GAME_LIST = "NextGameList"
local LEAVE_GAME_LABEL_KEY = "Feature.SettingsHub.Label.LeaveGame"
local LeaveGame = Roact.PureComponent:extend("LeaveGame")
local LEAVE_GAME_ALERT_SHOW_Y = 267
local LEAVE_GAME_ALERT_HIDE_Y = 183
-- NOTE: event context might need to be changed for other platforms.
local EVENT_CONTEXT = "XboxOne"
function LeaveGame:init()
self.screenId = self.props.screenId or HttpService:GenerateGUID(false)
self.ref = Roact.createRef()
self.analytics = Analytics.new()
self.motorOptions = {
dampingRatio = 1,
frequency = 5,
}
self.leaveButtonRef = Roact.createRef()
self.resume = function()
self.props.closeScreen()
end
self.enterNextGameList = function()
if #self.props.gamesList == 0 then
self:_selectButton()
return
end
local eventName = "EnterNextGameList"
self.analytics.EventStream:setRBXEventStream(EVENT_CONTEXT, eventName)
self:setState({
enterGameList = true
})
end
self.leaveNextGameList = function()
self:setState({
enterGameList = false
})
end
self.state = {
enterGameList = false,
}
end
function LeaveGame:willUpdate(nextProps, nextState)
assert(self.props.screenId == nextProps.screenId)
end
function LeaveGame:render()
local zIndex = self.props.zIndex or 1
local leaveGameAlert = Roact.createElement("Frame", {
Size = UDim2.new(0, 672, 0, 167),
Position = UDim2.new(0.5, 0, 0, LEAVE_GAME_ALERT_SHOW_Y),
AnchorPoint = Vector2.new(0.5, 0),
BackgroundTransparency = 1,
[Roact.Ref] = self.ref,
},{
LeaveGameText = Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, 45),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
Text = RobloxTranslator:FormatByKey(LEAVE_GAME_LABEL_KEY),
Font = Enum.Font.SourceSans,
FontSize = Enum.FontSize.Size36,
TextColor3 = Constants.Color.WHITE,
BackgroundTransparency = 1,
TextWrapped = true,
ZIndex = zIndex + 1,
}),
[LEAVE_BUTTON_KEY] = Roact.createElement(LeaveButton, {
position = UDim2.new(0, 0, 1, 0),
anchorPoint = Vector2.new(0, 1),
objRef = self.leaveButtonRef,
zIndex = zIndex,
}),
[DONT_LEAVE_BUTTON_KEY] = Roact.createElement(ResumeButton, {
position = UDim2.new(1, 0, 1, 0),
anchorPoint = Vector2.new(1, 1),
onResume = self.resume,
zIndex = zIndex,
}),
Redirect = Roact.createElement("Frame", {
Size = UDim2.new(1.5, 0, 0, 1),
Position = UDim2.new(0.5, 0, 1, 100),
AnchorPoint = Vector2.new(0.5, 0),
Selectable = true,
BackgroundTransparency = 1,
[Roact.Event.SelectionGained] = function()
GuiService.SelectedCoreObject = nil
self.enterNextGameList()
end
}),
})
local playNextGameView
if #self.props.gamesList ~= 0 then
playNextGameView = Roact.createElement(PlayNextGameView, {
screenId = NEXT_GAME_LIST,
focused = self.state.enterGameList,
onLeaveNextGameList = self.leaveNextGameList,
gamesData = self.props.gamesList,
zIndex = zIndex,
})
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Color3.fromRGB(25,25,25),
BackgroundTransparency = 0.7,
BorderSizePixel = 0,
ZIndex = zIndex,
},{
LeaveGameAlert = leaveGameAlert,
PlayNextGameView = playNextGameView,
})
end
function LeaveGame:_selectButton()
if self.props.focused then
local buttonRef = self.leaveButtonRef
local buttonObj = buttonRef and buttonRef.current or nil
if buttonObj ~= nil then
GuiService.SelectedCoreObject = buttonObj
end
end
end
function LeaveGame:didMount()
if #self.props.gamesList ~= self.props.numOfGames then
self.props.GetRecentlyPlayedGames(self.props.numOfGames, true)
end
delay(0, function()
if self.props.focused and self.ref.current then
GuiService:AddSelectionParent(self.screenId, self.ref.current)
end
self:_selectButton()
end)
self.motor = Otter.createSingleMotor(0)
self.motor:onStep(function(value)
local t = value/100
if self.ref.current ~= nil then
self.ref.current.Position = UDim2.new(0.5, 0, 0, Utility:Round(Utility:Lerp(t, LEAVE_GAME_ALERT_SHOW_Y, LEAVE_GAME_ALERT_HIDE_Y)))
end
end)
self.motor:start()
local eventName = "OpenedLeaveGameScreen"
self.analytics.EventStream:setRBXEventStream(EVENT_CONTEXT, eventName)
end
function LeaveGame:didUpdate(previousProps, previousState)
local targetPercentage = self.state.enterGameList and 100 or 0
self.motor:setGoal(Otter.spring(targetPercentage, self.motorOptions))
if self.state.enterGameList == true and
self.props.focused == previousProps.focused then
return
end
GuiService:RemoveSelectionGroup(self.screenId)
if self.props.focused and self.ref.current then
GuiService:AddSelectionParent(self.screenId, self.ref.current)
end
self:_selectButton()
end
function LeaveGame:willUnmount()
GuiService:RemoveSelectionGroup(self.screenId)
self.motor:destroy()
end
local function mapStateToProps(state, props)
local currentUniverseId = game.GameId
if state.RecentlyPlayedGamesFetchingStatus.fetching ~= false then
return {
gamesList = {}
}
end
local recentlyPlayedGamesState = state.RecentlyPlayedGames
local gameSort = recentlyPlayedGamesState.gameSort or {}
--Fliter our the current game if it is in the sort
local filteredSort = {}
for i in ipairs(gameSort) do
if gameSort[i].universeId ~= currentUniverseId then
table.insert(filteredSort, gameSort[i])
end
end
return {
gamesList = filteredSort,
}
end
local function mapDispatchToProps(dispatch)
return
{
GetRecentlyPlayedGames = function(numOfGames, forceUpdate)
return dispatch(ApiFetchRecentlyPlayedGames(numOfGames, forceUpdate))
end,
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(LeaveGame)
@@ -0,0 +1,81 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local ContextActionService = game:GetService("ContextActionService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local Settings = script.Parent.Parent
local SettingsState = require(Settings.SettingsState)
local SettingsPageFactory = require(Settings.SettingsPageFactory)
local ApiFetchRecentlyPlayedGames = require(Settings.Thunks.ApiFetchRecentlyPlayedGames)
local LeaveGameScreen = require(Settings.Pages.LeaveGameScreen.LeaveGame)
-------------- CONSTANTS -------------
local LEAVE_GAME_ACTION = "LeaveGameCancelAction"
-- NOTE: The current game will be filtered out.
local NUM_OF_GAMES = 7
local PageInstance
local function Initialize()
local this = SettingsPageFactory:CreateNewPage()
------ TAB CUSTOMIZATION -------
this.TabHeader = nil -- no tab for this page
------ TAB CUSTOMIZATION -------
this.TabHeader = nil -- no tab for this page
------ PAGE CUSTOMIZATION -------
this.Page.Name = "LeaveGamePage"
this.ShouldShowBottomBar = false
this.ShouldShowHubBar = false
this.IsPageClipped = false
SettingsState.store:dispatch(ApiFetchRecentlyPlayedGames(NUM_OF_GAMES))
this.unMountFunction = function(isUsingGamepad)
if this.HubRef then
this.HubRef:PopMenu(isUsingGamepad, true)
end
end
this.unMountFromHotkey = function(name, state, input)
if state == Enum.UserInputState.Begin then
local isUsingGamepad = input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2
or input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4
this.unMountFunction(isUsingGamepad)
end
end
this.RoactScreen = Roact.createElement(RoactRodux.StoreProvider,
{
store = SettingsState.store,
},{
Screen = Roact.createElement("ScreenGui", {}, {
LeaveGameScreen = Roact.createElement(LeaveGameScreen, {
focused = true,
closeScreen = this.unMountFunction,
zIndex = 2,
numOfGames = NUM_OF_GAMES,
}),
})
})
return this
end
----------- Public Facing API Additions --------------
PageInstance = Initialize()
PageInstance.Displayed.Event:connect(function()
PageInstance.handle = Roact.mount(PageInstance.RoactScreen, CoreGui)
ContextActionService:BindCoreAction(LEAVE_GAME_ACTION, PageInstance.unMountFromHotkey, false, Enum.KeyCode.ButtonB)
end)
PageInstance.Hidden.Event:connect(function()
ContextActionService:UnbindCoreAction(LEAVE_GAME_ACTION)
if PageInstance.handle ~= nil then
Roact.unmount(PageInstance.handle)
PageInstance.handle = nil
end
end)
return PageInstance
@@ -0,0 +1,810 @@
--[[
Filename: Players.lua
Written by: Stickmasterluke
Version 1.0
Description: Player list inside escape menu, with friend adding functionality.
--]]
-------------- SERVICES --------------
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GuiService = game:GetService("GuiService")
local PlayersService = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local RunService = game:GetService("RunService")
----------- UTILITIES --------------
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local ShareGameDirectory = CoreGui.RobloxGui.Modules.Settings.Pages.ShareGame
local utility = require(RobloxGui.Modules.Settings.Utility)
local reportAbuseMenu = require(RobloxGui.Modules.Settings.Pages.ReportAbuseMenu)
local SocialUtil = require(RobloxGui.Modules:WaitForChild("SocialUtil"))
local Diag = require(CorePackages.AppTempCommon.AnalyticsReporters.Diag)
local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream)
local ShareGameIcons = require(ShareGameDirectory.Spritesheets.ShareGameIcons)
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local InviteToGameAnalytics = require(ShareGameDirectory.Analytics.InviteToGameAnalytics)
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
local UsePlayerDisplayName = require(RobloxGui.Modules.Settings.UsePlayerDisplayName)
------------ Constants -------------------
local FRAME_DEFAULT_TRANSPARENCY = .85
local FRAME_SELECTED_TRANSPARENCY = .65
local REPORT_PLAYER_IMAGE = isTenFootInterface and "rbxasset://textures/ui/Settings/Players/ReportFlagIcon@2x.png" or "rbxasset://textures/ui/Settings/Players/ReportFlagIcon.png"
local ADD_FRIEND_IMAGE = isTenFootInterface and "rbxasset://textures/ui/Settings/Players/AddFriendIcon@2x.png" or "rbxasset://textures/ui/Settings/Players/AddFriendIcon.png"
local FRIEND_IMAGE = isTenFootInterface and "rbxasset://textures/ui/Settings/Players/FriendIcon@2x.png" or "rbxasset://textures/ui/Settings/Players/FriendIcon.png"
local INSPECT_IMAGE = "rbxasset://textures/ui/InspectMenu/ico_inspect.png"
local INSPECT_KEY = "InGame.InspectMenu.Action.View"
local PLAYER_ROW_HEIGHT = 62
local PLAYER_ROW_SPACING = 80
------------ Variables -------------------
local platform = UserInputService:GetPlatform()
local PageInstance = nil
local localPlayer = PlayersService.LocalPlayer
while not localPlayer do
PlayersService.ChildAdded:wait()
localPlayer = PlayersService.LocalPlayer
end
------------ FAST FLAGS -------------------
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local FFlagUpdateSettingsHubGameText = require(RobloxGui.Modules.Flags.FFlagUpdateSettingsHubGameText)
local FFlagDisableAutoTranslateForKeyTranslatedContent = require(RobloxGui.Modules.Flags.FFlagDisableAutoTranslateForKeyTranslatedContent)
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
this.PageListLayout.Padding = UDim.new(0, PLAYER_ROW_SPACING - PLAYER_ROW_HEIGHT)
------ TAB CUSTOMIZATION -------
this.TabHeader.Name = "PlayersTab"
this.TabHeader.Icon.Image = isTenFootInterface and "rbxasset://textures/ui/Settings/MenuBarIcons/PlayersTabIcon@2x.png" or "rbxasset://textures/ui/Settings/MenuBarIcons/PlayersTabIcon.png"
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Text = "Players"
else
this.TabHeader.Icon.Title.Text = "Players"
end
----- FRIENDSHIP FUNCTIONS ------
local function getFriendStatus(selectedPlayer)
local success, result = pcall(function()
-- NOTE: Core script only
return localPlayer:GetFriendStatus(selectedPlayer)
end)
if success then
return result
else
return Enum.FriendStatus.NotFriend
end
end
------ PAGE CUSTOMIZATION -------
this.Page.Name = "Players"
local function showRightSideButtons(player)
return player and player ~= localPlayer and player.UserId > 0 and localPlayer.UserId > 0
end
local function createFriendStatusTextLabel(status, player)
if status == nil then
return nil
end
local fakeSelection = Instance.new("Frame")
fakeSelection.BackgroundTransparency = 1
local friendLabel = nil
local friendLabelText = nil
if status == Enum.FriendStatus.Friend or status == Enum.FriendStatus.FriendRequestSent then
friendLabel = Instance.new("TextButton")
friendLabel.BackgroundTransparency = 1
friendLabel.FontSize = Enum.FontSize.Size24
friendLabel.Font = Enum.Font.SourceSans
friendLabel.TextColor3 = Color3.new(1,1,1)
friendLabel.SelectionImageObject = fakeSelection
if status == Enum.FriendStatus.Friend then
friendLabel.Text = "Friend"
else
friendLabel.Text = "Request Sent"
end
elseif status == Enum.FriendStatus.Unknown or status == Enum.FriendStatus.NotFriend or status == Enum.FriendStatus.FriendRequestReceived then
local addFriendFunc = function()
if friendLabel and friendLabelText and friendLabelText.Text ~= "" then
friendLabel.ImageTransparency = 1
friendLabelText.Text = ""
if localPlayer and player then
AnalyticsService:ReportCounter("PlayersMenu-RequestFriendship")
AnalyticsService:TrackEvent("Game", "RequestFriendship", "PlayersMenu")
localPlayer:RequestFriendship(player)
end
end
end
if platform ~= Enum.Platform.XBoxOne then
friendLabel, friendLabelText = utility:MakeStyledButton("FriendStatus", "Add Friend", UDim2.new(0, 182, 0, 46), addFriendFunc)
friendLabelText.ZIndex = 3
friendLabelText.Position = friendLabelText.Position + UDim2.new(0,0,0,1)
end
end
if friendLabel then
friendLabel.Name = "FriendStatus"
friendLabel.Size = UDim2.new(0,182,0,46)
friendLabel.Position = UDim2.new(1,-198,0,7)
friendLabel.ZIndex = 3
end
return friendLabel
end
local function createFriendStatusImageLabel(status, player)
if status == Enum.FriendStatus.Friend or status == Enum.FriendStatus.FriendRequestSent then
local friendLabel = Instance.new("ImageButton")
friendLabel.Name = "FriendStatus"
friendLabel.Size = UDim2.new(0, 46, 0, 46)
friendLabel.Image = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png"
friendLabel.ScaleType = Enum.ScaleType.Slice
friendLabel.SliceCenter = Rect.new(8,6,46,44)
friendLabel.AutoButtonColor = false
friendLabel.BackgroundTransparency = 1
friendLabel.ZIndex = 2
local friendImage = Instance.new("ImageLabel")
friendImage.BackgroundTransparency = 1
friendImage.Position = UDim2.new(0.5, 0, 0.5, 0)
friendImage.Size = UDim2.new(0, 28, 0, 28)
friendImage.AnchorPoint = Vector2.new(0.5, 0.5)
friendImage.ZIndex = 3
friendImage.Image = FRIEND_IMAGE
if status == Enum.FriendStatus.Friend then
friendImage.ImageTransparency = 0
else
friendImage.Image = ADD_FRIEND_IMAGE
friendImage.ImageTransparency = 0.5
end
friendImage.Parent = friendLabel
return friendLabel
elseif status == Enum.FriendStatus.Unknown or status == Enum.FriendStatus.NotFriend or status == Enum.FriendStatus.FriendRequestReceived then
local addFriendButton, addFriendImage = nil
local addFriendFunc = function()
if addFriendButton and addFriendImage and addFriendButton.ImageTransparency ~= 1 then
addFriendButton.ImageTransparency = 1
addFriendImage.ImageTransparency = 1
if localPlayer and player then
AnalyticsService:ReportCounter("PlayersMenu-RequestFriendship")
AnalyticsService:TrackEvent("Game", "RequestFriendship", "PlayersMenu")
localPlayer:RequestFriendship(player)
end
end
end
addFriendButton, addFriendImage = utility:MakeStyledImageButton("FriendStatus", ADD_FRIEND_IMAGE,
UDim2.new(0, 46, 0, 46), UDim2.new(0, 28, 0, 28), addFriendFunc)
addFriendButton.Name = "FriendStatus"
addFriendButton.Selectable = true
return addFriendButton
end
return nil
end
local shareGameButton
local function friendStatusCreate(playerLabel, player)
local friendLabelParent = nil
if playerLabel then
friendLabelParent = playerLabel:FindFirstChild("RightSideButtons")
end
if friendLabelParent then
-- remove any previous friend status labels
for _, item in pairs(friendLabelParent:GetChildren()) do
if item and item.Name == "FriendStatus" then
if GuiService.SelectedCoreObject == item then
GuiService.SelectedCoreObject = shareGameButton
end
item:Destroy()
end
end
end
end
local function resizeFriendButton(parent, player, isPortrait, wasIsPortrait)
local friendLabel = parent:FindFirstChild("FriendStatus")
if friendLabel and isPortrait == wasIsPortrait then
return
end
if friendLabel then
friendLabel:Destroy()
friendLabel = nil
end
local status
if showRightSideButtons(player) then
status = getFriendStatus(player)
end
if isPortrait then
friendLabel = createFriendStatusImageLabel(status, player)
else
friendLabel = createFriendStatusTextLabel(status, player)
end
if friendLabel then
friendLabel.Name = "FriendStatus"
friendLabel.LayoutOrder = 3
friendLabel.Selectable = true
friendLabel.Parent = parent
end
end
localPlayer.FriendStatusChanged:connect(function(player, friendStatus)
if player then
local playerLabel = this.Page:FindFirstChild("PlayerLabel"..player.Name)
if playerLabel then
friendStatusCreate(playerLabel, player)
end
end
end)
local buttonsContainer = utility:Create("Frame") {
Name = "ButtonsContainer",
Size = UDim2.new(1, 0, 0, 62),
BackgroundTransparency = 1,
Parent = this.Page,
Visible = false
}
local leaveGameFunc = function()
this.HubRef:SwitchToPage(this.HubRef.LeaveGamePage, false, 1)
end
local leaveGameText = "Leave Game"
if FFlagUpdateSettingsHubGameText then
leaveGameText = RobloxTranslator:FormatByKey("InGame.HelpMenu.Leave")
end
local leaveButton, leaveLabel = utility:MakeStyledButton("LeaveButton", leaveGameText, UDim2.new(1 / 3, -5, 1, 0), leaveGameFunc)
leaveButton.AnchorPoint = Vector2.new(0, 0)
leaveButton.Position = UDim2.new(0, 0, 0, 0)
leaveLabel.Size = UDim2.new(1, 0, 1, -6)
leaveButton.Parent = buttonsContainer
local resetFunc = function()
this.HubRef:SwitchToPage(this.HubRef.ResetCharacterPage, false, 1)
end
local resetButton, resetLabel = utility:MakeStyledButton("ResetButton", "Reset Character", UDim2.new(1 / 3, -5, 1, 0), resetFunc)
resetButton.AnchorPoint = Vector2.new(0.5, 0)
resetButton.Position = UDim2.new(0.5, 0, 0, 0)
resetLabel.Size = UDim2.new(1, 0, 1, -6)
resetButton.Parent = buttonsContainer
local resumeGameFunc = function()
this.HubRef:SetVisibility(false)
end
local resumeGameText = "Resume Game"
if FFlagUpdateSettingsHubGameText then
resumeGameText = RobloxTranslator:FormatByKey("InGame.HelpMenu.Resume")
end
local resumeButton, resumeLabel = utility:MakeStyledButton("ResumeButton", resumeGameText, UDim2.new(1 / 3, -5, 1, 0), resumeGameFunc)
resumeButton.AnchorPoint = Vector2.new(1, 0)
resumeButton.Position = UDim2.new(1, 0, 0, 0)
resumeLabel.Size = UDim2.new(1, 0, 1, -6)
resumeButton.Parent = buttonsContainer
utility:OnResized(buttonsContainer, function(newSize, isPortrait)
if isPortrait or utility:IsSmallTouchScreen() then
local buttonsFontSize = isPortrait and 18 or 24
buttonsContainer.Visible = true
buttonsContainer.Size = UDim2.new(1, 0, 0, isPortrait and 50 or 62)
resetLabel.TextSize = buttonsFontSize
leaveLabel.TextSize = buttonsFontSize
resumeLabel.TextSize = buttonsFontSize
else
buttonsContainer.Visible = false
buttonsContainer.Size = UDim2.new(1, 0, 0, 0)
end
end)
if FFlagUseNotificationsLocalization then
local function ApplyLocalizeTextSettingsToLabel(label)
label.AnchorPoint = Vector2.new(0.5,0.5)
label.Position = UDim2.new(0.5, 0, 0.5, -3)
label.Size = UDim2.new(0.75, 0, 0.5, 0)
end
ApplyLocalizeTextSettingsToLabel(leaveLabel)
ApplyLocalizeTextSettingsToLabel(resetLabel)
ApplyLocalizeTextSettingsToLabel(resetLabel)
end
local function reportAbuseButtonCreate(playerLabel, player)
local rightSideButtons = playerLabel:FindFirstChild("RightSideButtons")
if rightSideButtons then
local oldReportButton = rightSideButtons:FindFirstChild("ReportPlayer")
if oldReportButton then
oldReportButton:Destroy()
end
if showRightSideButtons(player) then
local reportPlayerFunction = function()
reportAbuseMenu:ReportPlayer(player)
end
local reportButton = utility:MakeStyledImageButton("ReportPlayer", REPORT_PLAYER_IMAGE,
UDim2.new(0, 46, 0, 46), UDim2.new(0, 28, 0, 28), reportPlayerFunction)
reportButton.Name = "ReportPlayer"
reportButton.Position = UDim2.new(1, -260, 0, 7)
reportButton.LayoutOrder = 1
reportButton.Selectable = true
reportButton.Parent = rightSideButtons
end
end
end
local createShareGameButton = nil
local createPlayerRow = nil
local function createRow(frameClassName, hasSecondRow)
local frame = Instance.new(frameClassName)
frame.Image = "rbxasset://textures/ui/dialog_white.png"
frame.ScaleType = "Slice"
frame.SliceCenter = Rect.new(10, 10, 10, 10)
frame.Size = UDim2.new(1, 0, 0, PLAYER_ROW_HEIGHT)
frame.Position = UDim2.new(0, 0, 0, 0)
frame.BackgroundTransparency = 1
frame.ZIndex = 2
frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY
local icon = Instance.new("ImageLabel")
icon.Name = "Icon"
icon.BackgroundTransparency = 1
icon.Size = UDim2.new(0, 36, 0, 36)
icon.Position = UDim2.new(0, 12, 0, 12)
icon.ZIndex = 3
icon.Parent = frame
local textLabel = Instance.new("TextLabel")
textLabel.TextXAlignment = Enum.TextXAlignment.Left
textLabel.Font = Enum.Font.SourceSans
textLabel.FontSize = hasSecondRow and Enum.FontSize.Size36 or Enum.FontSize.Size24
textLabel.TextColor3 = Color3.new(1, 1, 1)
textLabel.BackgroundTransparency = 1
textLabel.Position = hasSecondRow and UDim2.new(0, 60, 0.5, -10) or UDim2.new(0, 60, .5, 0)
textLabel.Size = UDim2.new(0, 0, 0, 0)
textLabel.ZIndex = 3
textLabel.Parent = frame
if hasSecondRow then
local secondRow = Instance.new("TextLabel")
secondRow.Name = "SecondRow"
secondRow.TextXAlignment = Enum.TextXAlignment.Left
secondRow.Font = Enum.Font.SourceSans
secondRow.FontSize = Enum.FontSize.Size24
secondRow.TextColor3 = Color3.fromRGB(162, 162, 162)
secondRow.BackgroundTransparency = 1
secondRow.Position = UDim2.new(0, 60, .5, 12)
secondRow.Size = UDim2.new(0, 0, 0, 0)
secondRow.ZIndex = 3
secondRow.Parent = frame
end
return frame
end
createShareGameButton = function()
local frame = createRow("ImageButton")
local textLabel = frame.TextLabel
local icon = frame.Icon
textLabel.Font = Enum.Font.SourceSansSemibold
textLabel.AutoLocalize = not FFlagDisableAutoTranslateForKeyTranslatedContent
textLabel.Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Action.InviteFriendsToPlay")
icon.Size = UDim2.new(0, 24, 0, 24)
icon.Position = UDim2.new(0, 18, 0, 18)
ShareGameIcons:ApplyImage(icon, "invite")
local function setIsHighlighted(isHighlighted)
if isHighlighted then
frame.ImageTransparency = FRAME_SELECTED_TRANSPARENCY
else
frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY
end
end
frame.InputBegan:Connect(function() setIsHighlighted(true) end)
frame.InputEnded:Connect(function() setIsHighlighted(false) end)
frame.Activated:Connect(function() setIsHighlighted(false) end)
frame.TouchPan:Connect(function(_, totalTranslation)
local TAP_ACCURACY_THREASHOLD = 20
if math.abs(totalTranslation.Y) > TAP_ACCURACY_THREASHOLD then
setIsHighlighted(false)
end
end)
frame.SelectionGained:connect(function() setIsHighlighted(true) end)
frame.SelectionLost:connect(function() setIsHighlighted(false) end)
frame.SelectionImageObject = frame:Clone()
return frame
end
local function createInspectButtonImage(activateInspectAndBuyMenu)
local inspectButton = utility:MakeStyledImageButton("InspectButton", INSPECT_IMAGE,
UDim2.new(0, 46, 0, 46), UDim2.new(0, 28, 0, 28), activateInspectAndBuyMenu)
return inspectButton
end
local function createInspectButtonText(activateInspectAndBuyMenu)
local inspectButton = utility:MakeStyledButton(
"InspectButton", RobloxTranslator:FormatByKey(INSPECT_KEY), UDim2.new(0, 130, 0, 46), activateInspectAndBuyMenu)
inspectButton.AutoLocalize = not FFlagDisableAutoTranslateForKeyTranslatedContent
return inspectButton
end
local function resizeInspectButton(parent, player, isPortrait, wasPortrait)
local inspectButton = parent:FindFirstChild("Inspect")
if inspectButton and isPortrait == wasPortrait then
return
end
if inspectButton then
inspectButton:Destroy()
end
local activateInspectAndBuyMenu = function()
GuiService:InspectPlayerFromUserIdWithCtx(player.UserId, "escapeMenu")
this.HubRef:SetVisibility(false)
end
if isPortrait then
inspectButton = createInspectButtonImage(activateInspectAndBuyMenu)
else
inspectButton = createInspectButtonText(activateInspectAndBuyMenu)
end
inspectButton.Name = "Inspect"
inspectButton.LayoutOrder = 2
inspectButton.Selectable = true
inspectButton.Parent = parent
end
createPlayerRow = function()
local showDisplayName = UsePlayerDisplayName()
local frame = createRow("ImageLabel", showDisplayName)
if showDisplayName then
frame.TextLabel.Name = "DisplayNameLabel"
frame.SecondRow.Name = "NameLabel"
else
frame.TextLabel.Name = "NameLabel"
end
local rightSideButtons = Instance.new("Frame")
rightSideButtons.Name = "RightSideButtons"
rightSideButtons.BackgroundTransparency = 1
rightSideButtons.ZIndex = 2
rightSideButtons.Position = UDim2.new(0, 0, 0, 0)
rightSideButtons.Size = UDim2.new(1, -10, 1, 0)
rightSideButtons.Parent = frame
-- Selection Highlighting logic:
local updateHighlight = function(lostSelectionObject)
if frame then
if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject ~= lostSelectionObject and GuiService.SelectedCoreObject.Parent == rightSideButtons then
frame.ImageTransparency = FRAME_SELECTED_TRANSPARENCY
else
frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY
end
end
end
local fakeSelectionObject = nil
rightSideButtons.ChildAdded:connect(function(child)
if child:IsA("GuiObject") then
if fakeSelectionObject and child ~= fakeSelectionObject then
fakeSelectionObject:Destroy()
fakeSelectionObject = nil
end
child.SelectionGained:connect(function() updateHighlight(nil) end)
child.SelectionLost:connect(function() updateHighlight(child) end)
end
end)
fakeSelectionObject = Instance.new("Frame")
fakeSelectionObject.Selectable = true
fakeSelectionObject.Size = UDim2.new(1, 0, 1, 0)
fakeSelectionObject.BackgroundTransparency = 1
fakeSelectionObject.SelectionImageObject = fakeSelectionObject:Clone()
fakeSelectionObject.Parent = rightSideButtons
local rightSideListLayout = Instance.new("UIListLayout")
rightSideListLayout.Name = "RightSideListLayout"
rightSideListLayout.FillDirection = Enum.FillDirection.Horizontal
rightSideListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right
rightSideListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
rightSideListLayout.SortOrder = Enum.SortOrder.LayoutOrder
rightSideListLayout.Padding = UDim.new(0, 20)
rightSideListLayout.Parent = rightSideButtons
pcall(function()
frame.NameLabel.Localize = false
if showDisplayName then
frame.DisplayNameLabel.Localize = false
end
end)
return frame
end
-- Manage cutting off a players name if it is too long when switching into portrait mode.
local function managePlayerNameCutoff(frame, player)
local wasIsPortrait = nil
local reportFlagAddedConnection = nil
local function reportFlagChanged(reportFlag, prop)
if prop == "AbsolutePosition" and wasIsPortrait then
local maxPlayerNameSize = reportFlag.AbsolutePosition.X - 20 - frame.NameLabel.AbsolutePosition.X
if UsePlayerDisplayName() then
frame.NameLabel.Text = "@" .. player.Name
frame.DisplayNameLabel.Text = player.DisplayName
else
frame.NameLabel.Text = player.Name
end
if UsePlayerDisplayName() then
local newDisplayNameLength = utf8.len(player.DisplayName)
while frame.NameLabel.TextBounds.X > maxPlayerNameSize and newDisplayNameLength > 0 do
local offset = utf8.offset(player.DisplayName, newDisplayNameLength)
frame.NameLabel.Text = string.sub(player.DisplayName, 1, offset) .. "..."
newDisplayNameLength = newDisplayNameLength - 1
end
end
local playerNameText = UsePlayerDisplayName() and "@" .. player.Name or player.Name
local newNameLength = string.len(playerNameText)
while frame.NameLabel.TextBounds.X > maxPlayerNameSize and newNameLength > 0 do
frame.NameLabel.Text = string.sub(playerNameText, 1, newNameLength) .. "..."
newNameLength = newNameLength - 1
end
end
end
utility:OnResized(frame.NameLabel, function(newSize, isPortrait)
if wasIsPortrait ~= nil and wasIsPortrait == isPortrait then
return
end
wasIsPortrait = isPortrait
if isPortrait then
if reportFlagAddedConnection == nil then
reportFlagAddedConnection = frame.RightSideButtons.ChildAdded:connect(function(child)
if child.Name == "ReportPlayer" then
child.Changed:connect(function(prop) reportFlagChanged(child, prop) end)
reportFlagChanged(child, "AbsolutePosition")
end
end)
end
local reportFlag = frame.RightSideButtons:FindFirstChild("ReportPlayer")
if reportFlag then
reportFlag.Changed:connect(function(prop) reportFlagChanged(reportFlag, prop) end)
reportFlagChanged(reportFlag, "AbsolutePosition")
end
else
if UsePlayerDisplayName() then
frame.NameLabel.Text = "@" .. player.Name
frame.DisplayNameLabel.Text = player.DisplayName
else
frame.NameLabel.Text = player.Name
end
end
end)
end
local function canShareCurrentGame()
return localPlayer.UserId > 0
end
local sortedPlayers
local existingPlayerLabels = {}
local livePlayers = {}
this.Displayed.Event:connect(function(switchedFromGamepadInput)
sortedPlayers = PlayersService:GetPlayers()
table.sort(sortedPlayers, function(item1,item2)
return item1.Name:lower() < item2.Name:lower()
end)
local extraOffset = 20
if utility:IsSmallTouchScreen() or utility:IsPortrait() then
extraOffset = 85
end
-- Create "invite friends" button if it doesn't exist yet
-- We shouldn't create this button if we're not in a live game
local isStudio = (not RunService:IsStudio())
if canShareCurrentGame() and not shareGameButton and isStudio then
local inviteToGameAnalytics = InviteToGameAnalytics.new()
:withEventStream(EventStream.new())
:withDiag(Diag.new(AnalyticsService))
:withButtonName(InviteToGameAnalytics.ButtonName.SettingsHub)
shareGameButton = createShareGameButton()
shareGameButton.Activated:connect(function()
inviteToGameAnalytics:inputShareGameEntryPoint()
this.HubRef:InviteToGame()
end)
-- Ensure the button is always at the top of the list
shareGameButton.LayoutOrder = 1
shareGameButton.Parent = this.Page
end
local inspectMenuEnabled = GuiService:GetInspectMenuEnabled()
-- iterate through players to reuse or create labels for players
for index=1, #sortedPlayers do
local player = sortedPlayers[index]
local frame
frame = existingPlayerLabels[player.Name]
if player then
livePlayers[player.Name] = true
-- create label (frame) for this player index if one does not exist
if not frame or not frame.Parent then
frame = createPlayerRow((index - 1)*PLAYER_ROW_SPACING + extraOffset)
frame.Parent = this.Page
existingPlayerLabels[player.Name] = frame
end
frame.Name = "PlayerLabel" ..player.Name
-- Immediately assign the image to an image that isn't guaranteed to be generated
frame.Icon.Image = SocialUtil.GetFallbackPlayerImageUrl(math.max(1, player.UserId), Enum.ThumbnailSize.Size100x100, Enum.ThumbnailType.AvatarThumbnail)
-- Spawn a function to get the generated image
spawn(function()
local imageUrl = SocialUtil.GetPlayerImage(math.max(1, player.UserId), Enum.ThumbnailSize.Size100x100, Enum.ThumbnailType.AvatarThumbnail)
if frame and frame.Parent and frame.Parent == this.Page then
frame.Icon.Image = imageUrl
end
end)
if UsePlayerDisplayName() then
frame.DisplayNameLabel.Text = player.DisplayName
frame.NameLabel.Text = "@" .. player.Name
else
frame.NameLabel.Text = player.Name
end
frame.ImageTransparency = FRAME_DEFAULT_TRANSPARENCY
-- extra index room for shareGameButton
frame.LayoutOrder = index + 1
managePlayerNameCutoff(frame, player)
friendStatusCreate(frame, player)
local wasIsPortrait = nil
utility:OnResized(frame, function(newSize, isPortrait)
local parent = frame:FindFirstChild("RightSideButtons")
if parent then
resizeFriendButton(parent, player, isPortrait, wasIsPortrait)
if inspectMenuEnabled then
resizeInspectButton(parent, player, isPortrait, wasIsPortrait)
end
wasIsPortrait = isPortrait
end
end)
local showReportAbuse = not PolicyService:IsSubjectToChinaPolicies()
if showReportAbuse then
reportAbuseButtonCreate(frame, player)
end
end
end
for playerName, frame in pairs(existingPlayerLabels) do
if not livePlayers[playerName] then
frame:Destroy()
existingPlayerLabels[playerName] = nil
end
end
if UserInputService.GamepadEnabled then
GuiService.SelectedCoreObject = shareGameButton
end
utility:OnResized("MenuPlayerListExtraPageSize", function(newSize, isPortrait)
local extraOffset = 20
if utility:IsSmallTouchScreen() or utility:IsPortrait() then
extraOffset = 85
end
local inviteToGameRow = 1
local playerListRowsCount = #sortedPlayers + inviteToGameRow
this.Page.Size = UDim2.new(1,0,0, extraOffset + PLAYER_ROW_SPACING * playerListRowsCount - 5)
end)
end)
PlayersService.PlayerRemoving:Connect(function (player)
livePlayers[player.Name] = nil
local playerLabel = existingPlayerLabels[player.Name]
if not playerLabel then
return
end
local buttons = playerLabel:FindFirstChild("RightSideButtons")
if not buttons then
return
end
local friendStatus = buttons:FindFirstChild("FriendStatus")
if friendStatus then
if GuiService.SelectedCoreObject == friendStatus then
if UserInputService.GamepadEnabled then
GuiService.SelectedCoreObject = shareGameButton
else
GuiService.SelectedCoreObject = nil
end
end
friendStatus:Destroy()
end
local reportPlayer = buttons:FindFirstChild("ReportPlayer")
if reportPlayer then
if GuiService.SelectedCoreObject == reportPlayer then
if UserInputService.GamepadEnabled then
GuiService.SelectedCoreObject = shareGameButton
else
GuiService.SelectedCoreObject = nil
end
end
reportPlayer:Destroy()
end
local inspectButton = buttons:FindFirstChild("Inspect")
if inspectButton then
if GuiService.SelectedCoreObject == inspectButton then
GuiService.SelectedCoreObject = nil
end
inspectButton:Destroy()
end
end)
-- If the developer disables the Inspect Menu, remove the button from the escape menu.
GuiService.InspectMenuEnabledChangedSignal:Connect(function(enabled)
if not enabled then
for _, frame in pairs(existingPlayerLabels) do
local buttons = frame:FindFirstChild("RightSideButtons")
if buttons then
local inspectButton = buttons:FindFirstChild("Inspect")
if inspectButton then
if GuiService.SelectedCoreObject == inspectButton then
GuiService.SelectedCoreObject = nil
end
inspectButton:Destroy()
end
end
end
end
end)
return this
end
----------- Public Facing API Additions --------------
PageInstance = Initialize()
return PageInstance
@@ -0,0 +1,165 @@
--[[r
Filename: Record.lua
Written by: jeditkacheff
Version 1.0
Description: Takes care of the Record Tab in Settings Menu
--]]
-------------- SERVICES --------------
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GuiService = game:GetService("GuiService")
local TextService = game:GetService("TextService")
local VRService = game:GetService("VRService")
----------- UTILITIES --------------
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local utility = require(RobloxGui.Modules.Settings.Utility)
------------ Variables -------------------
local PageInstance = nil
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
local isRecordingVideo = false
local recordingEvent = Instance.new("BindableEvent")
recordingEvent.Name = "RecordingEvent"
this.RecordingChanged = recordingEvent.Event
function this:IsRecording()
return isRecordingVideo
end
------ TAB CUSTOMIZATION -------
this.TabHeader.Name = "RecordTab"
this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/RecordTab.png"
this.TabHeader.Icon.AspectRatioConstraint.AspectRatio = 41 / 40
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Text = "Record"
else
this.TabHeader.Icon.Title.Text = "Record"
end
local function onVREnabled()
this.TabHeader.Visible = not VRService.VREnabled
end
onVREnabled()
VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled)
------ PAGE CUSTOMIZATION -------
this.Page.Name = "Record"
local function makeTextLabel(name, text, isTitle, parent, layoutOrder)
local leftPadding, rightPadding, bottomPadding, textSize, font = 10, 0, 10, 24, Enum.Font.SourceSans
if isTitle then
leftPadding, rightPadding, bottomPadding, textSize, font = 10, 0, 0, 36, Enum.Font.SourceSansBold
end
local container = utility:Create'Frame'
{
Name = name .. "Container",
BackgroundTransparency = 1,
ZIndex = 2,
LayoutOrder = layoutOrder,
Parent = parent
};
local textLabel = utility:Create'TextLabel'
{
Name = name,
BackgroundTransparency = 1,
Text = text,
TextWrapped = true,
Font = font,
TextSize = textSize,
TextColor3 = Color3.new(1,1,1),
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
Position = UDim2.new(0, leftPadding, 0, 0),
Size = UDim2.new(1, -(leftPadding + rightPadding), 1, 0),
ZIndex = 2,
Parent = container
};
local function onResized(prop)
if prop == "AbsoluteSize" then
local textSize = TextService:GetTextSize(text, textLabel.TextSize, textLabel.Font, Vector2.new(parent.AbsoluteSize.X - leftPadding - rightPadding, 1e4))
container.Size = UDim2.new(1, 0, 0, textSize.Y + bottomPadding)
end
end
onResized("AbsoluteSize")
parent.Changed:connect(onResized)
return textLabel, container
end
-- need to override this function from SettingsPageFactory
-- DropDown menus require hub to to be set when they are initialized
function this:SetHub(newHubRef)
this.HubRef = newHubRef
---------------------------------- SCREENSHOT -------------------------------------
local closeSettingsFunc = function()
this.HubRef:SetVisibility(false, true)
end
local _screenshotTitle = makeTextLabel("ScreenshotTitle", "Screenshot", true, this.Page, 1)
local _screenshotBody = makeTextLabel("ScreenshotBody", "By clicking the 'Take Screenshot' button, the menu will close and take a screenshot and save it to your computer.", false, this.Page, 2)
this.ScreenshotButtonRow, this.ScreenshotButton = utility:AddButtonRow(this, "ScreenshotButton", "Take Screenshot", UDim2.new(0, 300, 0, 44), closeSettingsFunc)
this.ScreenshotButtonRow.LayoutOrder = 3
---------------------------------- VIDEO -------------------------------------
local _videoTitle = makeTextLabel("VideoTitle", "Video", true, this.Page, 4)
local _videoBody = makeTextLabel("VideoBody", "By clicking the 'Record Video' button, the menu will close and start recording your screen.", false, this.Page, 5)
local recordButtonRow, recordButton = utility:AddButtonRow(this, "RecordButton", "Record Video", UDim2.new(0, 300, 0, 44), closeSettingsFunc)
recordButtonRow.LayoutOrder = 6
recordButton.MouseButton1Click:connect(function()
recordingEvent:Fire(not isRecordingVideo)
end)
local gameOptions = settings():FindFirstChild("Game Options")
if gameOptions then
gameOptions.VideoRecordingChangeRequest:connect(function(recording)
isRecordingVideo = recording
if recording then
recordButton.RecordButtonTextLabel.Text = "Stop Recording"
else
recordButton.RecordButtonTextLabel.Text = "Record Video"
end
end)
end
recordButton.Activated:Connect(function()
CoreGui:ToggleRecording()
end)
this.ScreenshotButton.Activated:Connect(function()
CoreGui:TakeScreenshot()
end)
this.Page.Size = UDim2.new(1,0,0,400)
end
return this
end
----------- Public Facing API Additions --------------
PageInstance = Initialize()
PageInstance.Displayed.Event:connect(function(switchedFromGamepadInput)
if switchedFromGamepadInput then
GuiService.SelectedCoreObject = PageInstance.ScreenshotButton
end
end)
return PageInstance
@@ -0,0 +1,418 @@
--[[
Filename: ReportAbuseMenu.lua
Written by: jeditkacheff
Version 1.0
Description: Takes care of the report abuse page in Settings Menu
--]]
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GuiService = game:GetService("GuiService")
local PlayersService = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local utility = require(RobloxGui.Modules.Settings.Utility)
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local ABUSE_TYPES_PLAYER = {
"Swearing",
"Inappropriate Username",
"Bullying",
"Scamming",
"Dating",
"Cheating/Exploiting",
"Personal Question",
"Offsite Links",
}
local ABUSE_TYPES_GAME = {
"Inappropriate Content"
}
local DEFAULT_ABUSE_DESC_TEXT = " Short Description (Optional)"
if utility:IsSmallTouchScreen() then
DEFAULT_ABUSE_DESC_TEXT = " (Optional)"
end
pcall(function()
if utility:IsSmallTouchScreen() then
DEFAULT_ABUSE_DESC_TEXT = RobloxTranslator:FormatByKey("KEY_DESCRIPTION_OPTIONAL")
else
DEFAULT_ABUSE_DESC_TEXT = RobloxTranslator:FormatByKey("KEY_DESCRIPTION_SHORT_DECRIPTION_OPTIONAL")
end
end)
local PageInstance = nil
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local FFlagCollectAnalyticsForSystemMenu = settings():GetFFlag("CollectAnalyticsForSystemMenu")
local UsePlayerDisplayName = require(RobloxGui.Modules.Settings.UsePlayerDisplayName)
local Constants
if FFlagCollectAnalyticsForSystemMenu then
Constants = require(RobloxGui.Modules:WaitForChild("InGameMenu"):WaitForChild("Resources"):WaitForChild("Constants"))
end
local MIN_GAME_REPORT_TEXT_LENGTH = 5
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
local playerNames = {}
local nameToRbxPlayer = {}
local nextPlayerToReport = nil
function this:GetPlayerNameText(player)
if UsePlayerDisplayName() then
return player.DisplayName .. " [@" .. player.Name .. "]"
else
return player.Name
end
end
function this:GetPlayerFromIndex(index)
local playerName = playerNames[index]
if playerName then
return nameToRbxPlayer[playerName]
end
return nil
end
function this:SetNextPlayerToReport(player)
nextPlayerToReport = player
end
function this:UpdatePlayerDropDown()
playerNames = {}
nameToRbxPlayer = {}
local players = PlayersService:GetPlayers()
local index = 1
for i = 1, #players do
local player = players[i]
if player ~= PlayersService.LocalPlayer and player.UserId > 0 then
local nameText = this:GetPlayerNameText(player)
playerNames[index] = nameText
nameToRbxPlayer[nameText] = player
index = index + 1
end
end
table.sort(playerNames, function(a, b)
return a:lower() < b:lower()
end)
this.WhichPlayerMode:UpdateDropDownList(playerNames)
--Reset GameOrPlayerMode to Game if no other players
if index == 1 then
this.GameOrPlayerMode:SetSelectionIndex(1)
end
this.GameOrPlayerMode:SetInteractable(index > 1)
if this.GameOrPlayerMode.CurrentIndex == 1 then
this.WhichPlayerMode:SetInteractable(false)
this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_GAME)
this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_GAME > 1)
else
this.WhichPlayerLabel.ZIndex = 2
this.WhichPlayerMode:SetInteractable(index > 1)
this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_PLAYER)
this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_PLAYER > 1)
end
if nextPlayerToReport then
local playerNameText = this:GetPlayerNameText(nextPlayerToReport)
local playerSelected = this.WhichPlayerMode:SetSelectionByValue(playerNameText)
nextPlayerToReport = nil
if this.GameOrPlayerMode.CurrentIndex == 2 then
if playerSelected then --if the reported player is still in game
--Auto select type of abuse when report a player
GuiService.SelectedCoreObject = this.TypeOfAbuseMode.DropDownFrame
else
GuiService.SelectedCoreObject = this.WhichPlayerMode.DropDownFrame
end
end
end
end
------ TAB CUSTOMIZATION -------
this.TabHeader.Name = "ReportAbuseTab"
this.TabHeader.Icon.Image = "rbxasset://textures/ui/Settings/MenuBarIcons/ReportAbuseTab.png"
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Text = "Report"
else
this.TabHeader.Icon.Title.Text = "Report"
end
------ PAGE CUSTOMIZATION -------
this.Page.Name = "ReportAbusePage"
-- need to override this function from SettingsPageFactory
-- DropDown menus require hub to to be set when they are initialized
function this:SetHub(newHubRef)
this.HubRef = newHubRef
if utility:IsSmallTouchScreen() then
this.GameOrPlayerFrame,
this.GameOrPlayerLabel,
this.GameOrPlayerMode = utility:AddNewRow(this, "Game or Player?", "Selector", {"Game", "Player"}, 2)
else
this.GameOrPlayerFrame,
this.GameOrPlayerLabel,
this.GameOrPlayerMode = utility:AddNewRow(this, "Game or Player?", "Selector", {"Game", "Player"}, 2, 3)
end
this.GameOrPlayerMode.Selection.LayoutOrder = 1
this.WhichPlayerFrame,
this.WhichPlayerLabel,
this.WhichPlayerMode = utility:AddNewRow(this, "Which Player?", "DropDown", {"update me"})
this.WhichPlayerMode:SetInteractable(false)
this.WhichPlayerLabel.ZIndex = 1
this.WhichPlayerFrame.LayoutOrder = 2
this.TypeOfAbuseFrame,
this.TypeOfAbuseLabel,
this.TypeOfAbuseMode = utility:AddNewRow(this, "Type Of Abuse", "DropDown", ABUSE_TYPES_GAME, 1)
this.TypeOfAbuseFrame.LayoutOrder = 3
if utility:IsSmallTouchScreen() then
this.AbuseDescriptionFrame,
this.AbuseDescriptionLabel,
this.AbuseDescription = utility:AddNewRow(this, DEFAULT_ABUSE_DESC_TEXT, "TextBox", nil, nil, 5)
this.AbuseDescriptionLabel.Text = "Abuse Description"
else
this.AbuseDescriptionFrame,
this.AbuseDescriptionLabel,
this.AbuseDescription = utility:AddNewRow(this, DEFAULT_ABUSE_DESC_TEXT, "TextBox", nil, nil, 5)
this.AbuseDescriptionFrame.Size = UDim2.new(1, -10, 0, 100)
this.AbuseDescription.Selection.Size = UDim2.new(1, 0, 1, 0)
end
this.AbuseDescriptionFrame.LayoutOrder = 4
this.AbuseDescription.Selection.FocusLost:connect(function()
if this.AbuseDescription.Selection.Text == "" then
this.AbuseDescription.Selection.Text = DEFAULT_ABUSE_DESC_TEXT
end
end)
local _SelectionOverrideObject = utility:Create'ImageLabel'
{
Image = "",
BackgroundTransparency = 1
};
local submitButton, submitText = nil, nil
local function makeSubmitButtonActive()
submitButton.ZIndex = 2
submitButton.Selectable = true
submitText.ZIndex = 2
end
local function makeSubmitButtonInactive()
submitButton.ZIndex = 1
submitButton.Selectable = false
submitText.ZIndex = 1
end
local function updateSubmitButton()
if this.GameOrPlayerMode.CurrentIndex == 1 then -- 1 is Report Game
if this.AbuseDescription.Selection.Text ~= DEFAULT_ABUSE_DESC_TEXT then
if utf8.len(utf8.nfcnormalize(this.AbuseDescription.Selection.Text)) > MIN_GAME_REPORT_TEXT_LENGTH then
makeSubmitButtonActive()
return
end
end
else
if this.WhichPlayerMode:GetSelectedIndex() then
if this.TypeOfAbuseMode:GetSelectedIndex() then
makeSubmitButtonActive()
return
end
end
end
makeSubmitButtonInactive()
end
local function updateAbuseDropDown()
this.WhichPlayerMode:ResetSelectionIndex()
this.TypeOfAbuseMode:ResetSelectionIndex()
if this.GameOrPlayerMode.CurrentIndex == 1 then
this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_GAME)
this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_GAME > 1)
this.TypeOfAbuseLabel.ZIndex = (#ABUSE_TYPES_GAME > 1 and 2 or 1)
this.WhichPlayerMode:SetInteractable(false)
this.WhichPlayerLabel.ZIndex = 1
updateSubmitButton()
else
this.TypeOfAbuseMode:UpdateDropDownList(ABUSE_TYPES_PLAYER)
this.TypeOfAbuseMode:SetInteractable(#ABUSE_TYPES_PLAYER > 1)
this.TypeOfAbuseLabel.ZIndex = (#ABUSE_TYPES_PLAYER > 1 and 2 or 1)
if #playerNames > 0 then
this.WhichPlayerMode:SetInteractable(true)
this.WhichPlayerLabel.ZIndex = 2
else
this.WhichPlayerMode:SetInteractable(false)
this.WhichPlayerLabel.ZIndex = 1
end
updateSubmitButton()
end
end
local function cleanupReportAbuseMenu()
updateAbuseDropDown()
this.AbuseDescription.Selection.Text = DEFAULT_ABUSE_DESC_TEXT
this.HubRef:SetVisibility(false, true)
end
local function reportAnalytics(reportType, id)
if not FFlagCollectAnalyticsForSystemMenu then return end
local stringTable = {}
stringTable[#stringTable + 1] = "report_type=" .. tostring(reportType)
stringTable[#stringTable + 1] = "report_source=ingame"
stringTable[#stringTable + 1] = "reported_entity_id=" .. tostring(id)
local infoString = table.concat(stringTable,"&")
local eventTable = {}
eventTable["universeid"] = tostring(game.GameId)
AnalyticsService:SetRBXEventStream(Constants.AnalyticsTargetName, Constants.AnalyticsReportSubmittedName, infoString, eventTable)
end
local function onReportSubmitted()
local abuseReason = nil
local reportSucceeded = false
if this.GameOrPlayerMode.CurrentIndex == 2 then
abuseReason = ABUSE_TYPES_PLAYER[this.TypeOfAbuseMode.CurrentIndex]
local currentAbusingPlayer = this:GetPlayerFromIndex(this.WhichPlayerMode.CurrentIndex)
if currentAbusingPlayer and abuseReason then
reportSucceeded = true
spawn(function()
PlayersService:ReportAbuse(currentAbusingPlayer, abuseReason, this.AbuseDescription.Selection.Text)
reportAnalytics("user", currentAbusingPlayer.UserId)
end)
end
else
abuseReason = ABUSE_TYPES_GAME[this.TypeOfAbuseMode.CurrentIndex]
if abuseReason then
reportSucceeded = true
spawn(function()
local placeId,placeName,placeDescription = tostring(game.PlaceId), "N/A", "N/A"
local abuseDescription = this.AbuseDescription.Selection.Text
pcall(function()
local productInfo = MarketplaceService:GetProductInfo(game.PlaceId, Enum.InfoType.Asset)
placeName = productInfo.Name
placeDescription = productInfo.Description
end)
local formattedText = string.format("User Report: \n %s \n".."Place Title: \n %s \n".."PlaceId: \n %s \n".."Place Description: \n %s \n",abuseDescription, placeName, placeId, placeDescription)
PlayersService:ReportAbuse(nil, abuseReason, formattedText)
reportAnalytics("game", game.GameId)
end)
end
end
if reportSucceeded then
local alertText = "Thanks for your report! Our moderators will review the chat logs and evaluate what happened."
if abuseReason == 'Cheating/Exploiting' then
alertText = "Thanks for your report! We've recorded your report for evaluation."
elseif abuseReason == 'Inappropriate Username' then
alertText = "Thanks for your report! Our moderators will evaluate the username."
elseif abuseReason == "Bad Model or Script" or abuseReason == "Inappropriate Content" or abuseReason == "Offsite Link" or abuseReason == "Offsite Links" then
alertText = "Thanks for your report! Our moderators will review the place and make a determination."
end
utility:ShowAlert(alertText, "Ok", this.HubRef, cleanupReportAbuseMenu)
this.LastSelectedObject = nil
end
end
submitButton, submitText = utility:MakeStyledButton("SubmitButton", "Submit", UDim2.new(0,198,0,50), onReportSubmitted, this)
submitButton.AnchorPoint = Vector2.new(0.5,0)
submitButton.Position = UDim2.new(0.5,0,1,5)
updateSubmitButton()
submitButton.Parent = this.AbuseDescription.Selection
local function playerSelectionChanged(newIndex)
updateSubmitButton()
end
this.WhichPlayerMode.IndexChanged:connect(playerSelectionChanged)
local function typeOfAbuseChanged(newIndex)
updateSubmitButton()
end
this.TypeOfAbuseMode.IndexChanged:connect(typeOfAbuseChanged)
this.GameOrPlayerMode.IndexChanged:connect(updateAbuseDropDown)
local function abuseDescriptionChanged()
updateSubmitButton()
end
this.AbuseDescription.Selection:GetPropertyChangedSignal("Text"):connect(abuseDescriptionChanged)
this:AddRow(nil, nil, this.AbuseDescription)
this.Page.Size = UDim2.new(1,0,0,submitButton.AbsolutePosition.Y + submitButton.AbsoluteSize.Y)
end
return this
end
----------- Public Facing API Additions --------------
do
PageInstance = Initialize()
PageInstance.Displayed.Event:connect(function()
PageInstance:UpdatePlayerDropDown()
end)
function PageInstance:ReportPlayer(player)
if player then
local setReportPlayerConnection = nil
PageInstance:SetNextPlayerToReport(player)
setReportPlayerConnection = PageInstance.Displayed.Event:connect(function()
PageInstance.GameOrPlayerMode:SetSelectionIndex(2)
if setReportPlayerConnection then
setReportPlayerConnection:disconnect()
setReportPlayerConnection = nil
end
end)
if not PageInstance.HubRef:GetVisibility() then
PageInstance.HubRef:SetVisibility(true, false, PageInstance)
else
PageInstance.HubRef:SwitchToPage(PageInstance, false)
end
end
end
end
return PageInstance
@@ -0,0 +1,183 @@
--[[
Filename: ResetCharacter.lua
Written by: jeditkacheff
Version 1.0
Description: Takes care of the reseting the character in Settings Menu
--]]
-------------- CONSTANTS -------------
local RESET_CHARACTER_GAME_ACTION = "ResetCharacterAction"
-------------- SERVICES --------------
local CoreGui = game:GetService("CoreGui")
local ContextActionService = game:GetService("ContextActionService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local GuiService = game:GetService("GuiService")
local PlayersService = game:GetService("Players")
local AnalyticsService = game:GetService("RbxAnalyticsService")
----------- UTILITIES --------------
local utility = require(RobloxGui.Modules.Settings.Utility)
------------ Variables -------------------
local PageInstance = nil
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
local FFlagCollectAnalyticsForSystemMenu = settings():GetFFlag("CollectAnalyticsForSystemMenu")
local Constants
if FFlagCollectAnalyticsForSystemMenu then
Constants = require(RobloxGui.Modules:WaitForChild("InGameMenu"):WaitForChild("Resources"):WaitForChild("Constants"))
end
----------- CLASS DECLARATION --------------
local function Initialize()
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
this.DontResetCharFunc = function(isUsingGamepad)
if FFlagCollectAnalyticsForSystemMenu then
AnalyticsService:SetRBXEventStream(Constants.AnalyticsTargetName, Constants.AnalyticsInGameMenuName,
Constants.AnalyticsRespawnCharacterName, {confirmed = Constants.AnalyticsCancelledName, universeid = tostring(game.GameId)})
end
if this.HubRef then
this.HubRef:PopMenu(isUsingGamepad, true)
end
end
this.DontResetCharFromHotkey = function(name, state, input)
if state == Enum.UserInputState.Begin then
local isUsingGamepad = input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2
or input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4
this.DontResetCharFunc(isUsingGamepad)
end
end
this.DontResetCharFromButton = function(isUsingGamepad)
this.DontResetCharFunc(isUsingGamepad)
end
------ TAB CUSTOMIZATION -------
this.TabHeader = nil -- no tab for this page
------ PAGE CUSTOMIZATION -------
this.Page.Name = "ResetCharacter"
this.ShouldShowBottomBar = false
this.ShouldShowHubBar = false
local resetCharacterText = utility:Create'TextLabel'
{
Name = "ResetCharacterText",
Text = "Are you sure you want to reset your character?",
Font = Enum.Font.SourceSansBold,
FontSize = Enum.FontSize.Size36,
TextColor3 = Color3.new(1,1,1),
BackgroundTransparency = 1,
Size = UDim2.new(1,0,0,200),
TextWrapped = true,
ZIndex = 2,
Parent = this.Page,
Position = isTenFootInterface and UDim2.new(0,0,0,100) or UDim2.new(0,0,0,0)
};
local resetButtonContainer = utility:Create"Frame"
{
Name = "ResetButtonContainer",
Parent = resetCharacterText,
Size = UDim2.new(1,0,0,400),
BackgroundTransparency = 1,
Position = UDim2.new(0,0,1,0)
};
local _resetButtonLayout = utility:Create'UIGridLayout'
{
Name = "ResetButtonsLayout",
CellSize = isTenFootInterface and UDim2.new(0, 300, 0, 80) or UDim2.new(0, 200, 0, 50),
CellPadding = UDim2.new(0,20,0,20),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
Parent = resetButtonContainer
};
if utility:IsSmallTouchScreen() then
resetCharacterText.FontSize = Enum.FontSize.Size24
resetCharacterText.Size = UDim2.new(1,0,0,100)
elseif isTenFootInterface then
resetCharacterText.FontSize = Enum.FontSize.Size48
end
------ Init -------
local resetCharFunc = function()
local player = PlayersService.LocalPlayer
if player then
local character = player.Character
if character then
local humanoid = character:FindFirstChild('Humanoid')
if humanoid then
humanoid.Health = 0
end
end
end
if FFlagCollectAnalyticsForSystemMenu then
AnalyticsService:SetRBXEventStream(Constants.AnalyticsTargetName, Constants.AnalyticsInGameMenuName,
Constants.AnalyticsRespawnCharacterName, {confirmed = Constants.AnalyticsConfirmedName, universeid = tostring(game.GameId)})
end
AnalyticsService:ReportCounter("InGameMenu-ResetCharacter")
end
this.ResetBindable = true
local onResetFunction = function()
if this.ResetBindable == true then
resetCharFunc()
elseif this.ResetBindable then
this.ResetBindable:Fire()
end
if this.HubRef then
this.HubRef:SetVisibility(false, true)
end
end
this.ResetCharacterButton = utility:MakeStyledButton("ResetCharacter", "Reset", nil, onResetFunction)
this.ResetCharacterButton.NextSelectionRight = nil
this.ResetCharacterButton.Parent = resetButtonContainer
local dontResetCharacterButton = utility:MakeStyledButton("DontResetCharacter", "Don't Reset", nil, this.DontResetCharFromButton)
dontResetCharacterButton.NextSelectionLeft = nil
dontResetCharacterButton.Parent = resetButtonContainer
this.Page.Size = UDim2.new(1,0,0,dontResetCharacterButton.AbsolutePosition.Y + dontResetCharacterButton.AbsoluteSize.Y)
return this
end
----------- Public Facing API Additions --------------
PageInstance = Initialize()
local isOpen = false
PageInstance.Displayed.Event:connect(function()
isOpen = true
GuiService.SelectedCoreObject = PageInstance.ResetCharacterButton
ContextActionService:BindCoreAction(RESET_CHARACTER_GAME_ACTION, PageInstance.DontResetCharFromHotkey, false, Enum.KeyCode.ButtonB)
end)
PageInstance.Hidden.Event:connect(function()
isOpen = false
ContextActionService:UnbindCoreAction(RESET_CHARACTER_GAME_ACTION)
end)
function PageInstance:SetResetCallback(bindableEvent)
if bindableEvent == false and isOpen then
-- We need to close this page if reseting was just disabled and the page is already open
PageInstance.HubRef:PopMenu(nil, true)
end
PageInstance.ResetBindable = bindableEvent
end
return PageInstance
@@ -0,0 +1,8 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(route)
return {
route = route,
}
end)
@@ -0,0 +1,8 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(route)
return {
route = route,
}
end)
@@ -0,0 +1,9 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(userId, inviteStatus)
return {
userId = tostring(userId),
inviteStatus = inviteStatus,
}
end)
@@ -0,0 +1,8 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(deviceLayout)
return {
deviceLayout = deviceLayout,
}
end)
@@ -0,0 +1,8 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(isSmallTouchScreen)
return {
isSmallTouchScreen = isSmallTouchScreen,
}
end)
@@ -0,0 +1,8 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(isActive)
return {
isActive = isActive,
}
end)
@@ -0,0 +1,8 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function(searchText)
return {
searchText = searchText,
}
end)
@@ -0,0 +1,6 @@
local Modules = game:GetService("CorePackages").AppTempCommon
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,105 @@
local InviteToGameAnalytics = {}
InviteToGameAnalytics.__index = InviteToGameAnalytics
InviteToGameAnalytics.ButtonName = {
SettingsHub = "settingsHub",
ModalPrompt = "modalPrompt",
}
InviteToGameAnalytics.EventName = {
InviteSent = "inputShareGameInviteSent",
EntryPoint = "inputShareGameEntryPoint",
}
InviteToGameAnalytics.DiagCounters = {
EntryPoint = {
[InviteToGameAnalytics.ButtonName.SettingsHub] = settings():GetFVariable("LuaShareGameSettingsHubEntryCounter"),
[InviteToGameAnalytics.ButtonName.ModalPrompt] = settings():GetFVariable("LuaShareGameModalPromptEntryCounter"),
},
InviteSent = {
[InviteToGameAnalytics.ButtonName.SettingsHub] = settings():GetFVariable("LuaShareGameSettingsHubInviteCounter"),
[InviteToGameAnalytics.ButtonName.ModalPrompt] = settings():GetFVariable("LuaShareGameModalPromptInviteCounter"),
},
}
function InviteToGameAnalytics.new()
local self = {
_eventStreamImpl = nil,
_diagImpl = nil,
_buttonName = nil,
}
setmetatable(self, InviteToGameAnalytics)
return self
end
function InviteToGameAnalytics:withEventStream(eventStreamImpl)
self._eventStreamImpl = eventStreamImpl
return self
end
function InviteToGameAnalytics:withDiag(diagImpl)
self._diagImpl = diagImpl
return self
end
function InviteToGameAnalytics:withButtonName(buttonName)
self._buttonName = buttonName
return self
end
function InviteToGameAnalytics:onActivatedInviteSent(senderId, conversationId, participants)
local buttonName = self:_getButtonName()
local eventContext = "inGame"
local eventName = InviteToGameAnalytics.EventName.InviteSent
local participantsString = table.concat(participants, ",")
local additionalArgs = {
btn = buttonName,
placeId = tostring(game.PlaceId),
gameId = tostring(game.GameId),
senderId = senderId,
conversationId = tostring(conversationId),
participants = participantsString,
}
self:_getEventStream():setRBXEventStream(eventContext, eventName, additionalArgs)
local counterName = InviteToGameAnalytics.DiagCounters.InviteSent[self:_getButtonName()]
if counterName then
self:_getDiag():reportCounter(counterName, 1)
end
end
function InviteToGameAnalytics:inputShareGameEntryPoint()
local buttonName = self:_getButtonName()
local eventContext = "inGame"
local eventName = InviteToGameAnalytics.EventName.EntryPoint
local additionalArgs = {
btn = buttonName,
placeId = tostring(game.PlaceId),
gameId = tostring(game.GameId),
}
self:_getEventStream():setRBXEventStream(eventContext, eventName, additionalArgs)
local counterName = InviteToGameAnalytics.DiagCounters.EntryPoint[self:_getButtonName()]
if counterName then
self:_getDiag():reportCounter(counterName, 1)
end
end
function InviteToGameAnalytics:_getEventStream()
assert(self._eventStreamImpl, "EventStream implementation not found. Did you forget to construct withEventStream?")
return self._eventStreamImpl
end
function InviteToGameAnalytics:_getDiag()
assert(self._diagImpl, "Diag implementation not found. Did you forget to construct withDiag?")
return self._diagImpl
end
function InviteToGameAnalytics:_getButtonName()
assert(self._buttonName, "ButtonName not found. Did you forget to construct withButtonName?")
return self._buttonName
end
return InviteToGameAnalytics
@@ -0,0 +1,166 @@
return function()
local CorePackages = game:GetService("CorePackages")
local InviteToGameAnalytics = require(script.Parent.InviteToGameAnalytics)
local Signal = require(CorePackages.AppTempCommon.Common.Signal)
local function createMockEventStream()
local eventStream = {
onSetRBXEventStream = Signal.new(),
}
function eventStream:setRBXEventStream(...)
self.onSetRBXEventStream:fire(...)
end
return eventStream
end
local function createMockDiag()
local diag = {
onReportCounter = Signal.new(),
}
function diag:reportCounter(...)
self.onReportCounter:fire(...)
end
return diag
end
describe("new", function()
it("SHOULD return an object", function()
local analytics = InviteToGameAnalytics.new()
expect(analytics).to.be.ok()
end)
end)
describe("withEventStream", function()
it("SHOULD return the original object", function()
local analytics = InviteToGameAnalytics.new():withEventStream()
expect(analytics).to.be.ok()
end)
end)
describe("withDiag", function()
it("SHOULD return the original object", function()
local analytics = InviteToGameAnalytics.new():withDiag()
expect(analytics).to.be.ok()
end)
end)
describe("withButtonName", function()
it("SHOULD return the original object", function()
local analytics = InviteToGameAnalytics.new():withEventStream()
expect(analytics).to.be.ok()
end)
end)
describe("_getEventStream", function()
it("SHOULD return the event stream when injected", function()
local eventStreamMock = {}
local analytics = InviteToGameAnalytics.new():withEventStream(eventStreamMock)
expect(analytics).to.be.ok()
expect(analytics:_getEventStream()).to.equal(eventStreamMock)
end)
it("SHOULD throw if not provided an EventStream", function()
local analytics = InviteToGameAnalytics.new()
expect(analytics).to.be.ok()
expect(function()
analytics:_getEventStream()
end).to.throw()
end)
end)
describe("_getButtonName", function()
it("SHOULD return the context when injected", function()
local buttonMock = "testing"
local analytics = InviteToGameAnalytics.new():withButtonName(buttonMock)
expect(analytics).to.be.ok()
expect(analytics:_getButtonName()).to.equal(buttonMock)
end)
it("SHOULD throw if not provided a Context", function()
local analytics = InviteToGameAnalytics.new()
expect(analytics).to.be.ok()
expect(function()
analytics:_getButtonName()
end).to.throw()
end)
end)
describe("inputShareGameEntryPoint", function()
it("SHOULD fire `EventName.EntryPoint` event", function()
local mockButtonName = "testing123"
local mockEventStream = createMockEventStream()
local lastEventContext = nil
local lastEventName = nil
mockEventStream.onSetRBXEventStream:connect(function(_, eventName, additionalArgs)
lastEventContext = additionalArgs.btn
lastEventName = eventName
end)
local analytics = InviteToGameAnalytics.new()
:withButtonName(mockButtonName)
:withEventStream(mockEventStream)
:withDiag(createMockDiag())
analytics:inputShareGameEntryPoint()
expect(lastEventContext).to.equal(mockButtonName)
expect(lastEventName).to.equal(InviteToGameAnalytics.EventName.EntryPoint)
end)
end)
describe("onActivatedInviteSent", function()
it("SHOULD fire `EventName.InviteSent` event", function()
local mockButtonName = "testing123"
local mockEventStream = createMockEventStream()
local lastEventContext = nil
local lastEventName = nil
mockEventStream.onSetRBXEventStream:connect(function(_, eventName, additionalArgs)
lastEventContext = additionalArgs.btn
lastEventName = eventName
end)
local analytics = InviteToGameAnalytics.new()
:withButtonName(mockButtonName)
:withEventStream(mockEventStream)
:withDiag(createMockDiag())
analytics:onActivatedInviteSent("senderId", "conversationId", {})
expect(lastEventContext).to.equal(mockButtonName)
expect(lastEventName).to.equal(InviteToGameAnalytics.EventName.InviteSent)
end)
it("SHOULD fire a diag counter", function()
local mockDiag = createMockDiag()
local diagSum = 0
mockDiag.onReportCounter:connect(function()
diagSum = diagSum + 1
end)
local analytics = InviteToGameAnalytics.new()
:withButtonName(InviteToGameAnalytics.ButtonName.SettingsHub)
:withEventStream(createMockEventStream())
:withDiag(mockDiag)
analytics:onActivatedInviteSent("senderId", "conversationId", {"recipientId"})
expect(diagSum).to.equal(1)
end)
end)
end
@@ -0,0 +1,32 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local PlaceInfos = require(AppTempCommon.LuaChat.Reducers.PlaceInfos)
local Users = require(AppTempCommon.LuaApp.Reducers.Users)
local Friends = require(AppTempCommon.LuaApp.Reducers.Friends)
local FriendCount = require(AppTempCommon.LuaChat.Reducers.FriendCount)
local ConversationsSearch = require(ShareGame.Reducers.ConversationsSearch)
local DeviceInfo = require(ShareGame.Reducers.DeviceInfo)
local Invites = require(ShareGame.Reducers.Invites)
local Page = require(ShareGame.Reducers.Page)
local Toasts = require(ShareGame.Reducers.Toasts)
return function(state, action)
state = state or {}
return {
ConversationsSearch = ConversationsSearch(state.ConversationsSearch, action),
DeviceInfo = DeviceInfo(state.DeviceInfo, action),
Invites = Invites(state.Invites, action),
Page = Page(state.Page, action),
PlaceInfos = PlaceInfos(state.PlaceInfos, action),
Toasts = Toasts(state.Toasts, action),
Users = Users(state.Users, action),
Friends = Friends(state.Friends, action),
FriendCount = FriendCount(state.FriendCount, action),
}
end
@@ -0,0 +1,71 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local ShareGame = Modules.Settings.Pages.ShareGame
local Constants = require(ShareGame.Constants)
local EventStream = require(AppTempCommon.Temp.EventStream)
local LayoutProvider = require(ShareGame.Components.LayoutProvider)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local ShareGameContainer
local ShareGamePageFrame
if FFlagLuaInviteModalEnabled then
ShareGameContainer = require(ShareGame.Components.ShareGameContainer)
else
ShareGamePageFrame = require(ShareGame.Components.ShareGamePageFrame)
end
local ShareGameApp = Roact.PureComponent:extend("App")
function ShareGameApp:render()
local analytics = self.props.analytics
local pageTarget = self.props.pageTarget
local pageFrame = nil
if self.props.isPageOpen then
if FFlagLuaInviteModalEnabled then
pageFrame = Roact.createElement(ShareGameContainer, {
analytics = analytics,
zIndex = Constants.SHARE_GAME_Z_INDEX,
})
else
pageFrame = Roact.createElement(ShareGamePageFrame, {
analytics = analytics,
zIndex = Constants.SHARE_GAME_Z_INDEX,
})
end
end
return Roact.createElement(LayoutProvider, nil, {
Roact.createElement(Roact.Portal, {
target = pageTarget,
}, {
ShareGamePageFrame = pageFrame,
})
})
end
function ShareGameApp:didMount()
self.eventStream = EventStream.new()
end
function ShareGameApp:willUnmount()
self.eventStream:releaseRBXEventStream()
end
local connector = RoactRodux.UNSTABLE_connect2(function(state, props)
return {
isPageOpen = state.Page.IsOpen,
}
end)
return connector(ShareGameApp)
@@ -0,0 +1,138 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local RectangleButton = require(ShareGame.Components.RectangleButton)
local IconButton = require(ShareGame.Components.IconButton)
local Roact = require(CorePackages.Roact)
local Constants = require(ShareGame.Constants)
local ShareGameIcons = require(RobloxGui.Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local BACK_IMAGE_SPRITE_PATH
if not FFlagLuaInviteModalEnabled then
BACK_IMAGE_SPRITE_PATH = ShareGameIcons:GetImagePath()
end
local BACK_IMAGE_SPRITE_FRAME = ShareGameIcons:GetFrame("back")
local BACK_BUTTON_TEXT_SIZE = 24
local BackButton = Roact.PureComponent:extend("BackButton")
if FFlagLuaInviteModalEnabled then
BackButton.IconType = {
Arrow = "Arrow",
Cross = "Cross",
None = "None",
}
BackButton.defaultProps = {
iconType = BackButton.IconType.None,
}
end
local ICON_TYPE_TO_ICON_NAME
if FFlagLuaInviteModalEnabled then
ICON_TYPE_TO_ICON_NAME = {
[BackButton.IconType.Arrow] = "back",
[BackButton.IconType.Cross] = "cross",
}
end
local createIconButton
if FFlagLuaInviteModalEnabled then
createIconButton = function(props)
local iconType = props.iconType
local visible = props.visible
local anchorPoint = props.anchorPoint
local position = props.position
local size = props.size
local zIndex = props.zIndex
local onClick = props.onClick
local iconName = ICON_TYPE_TO_ICON_NAME[iconType]
if iconName then
local iconSpritePath = ShareGameIcons:GetImagePath()
local iconSpriteFrame = ShareGameIcons:GetFrame(iconName)
return Roact.createElement(IconButton, {
visible = visible,
anchorPoint = anchorPoint,
position = position,
size = size,
zIndex = zIndex,
onClick = onClick,
iconHorizontalAlignment = Enum.HorizontalAlignment.Left,
iconSpritePath = iconSpritePath,
iconSpriteFrame = iconSpriteFrame,
})
end
return
end
end
function BackButton:render()
local isArrow
local iconType
if FFlagLuaInviteModalEnabled then
iconType = self.props.iconType
else
isArrow = self.props.isArrow
end
local visible = self.props.visible
local anchorPoint = self.props.anchorPoint
local position = self.props.position
local size = self.props.size
local zIndex = self.props.zIndex
local onClick = self.props.onClick
if FFlagLuaInviteModalEnabled then
if iconType == BackButton.IconType.None then
return Roact.createElement(RectangleButton, self.props, {
TextLabel = Roact.createElement("TextLabel", {
Position = UDim2.new(0.5, 0, 0.5, 0),
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Action.InviteFriendsBack"),
TextSize = BACK_BUTTON_TEXT_SIZE,
TextColor3 = Constants.Color.WHITE,
Font = Enum.Font.SourceSansSemibold,
ZIndex = zIndex,
}),
})
else
return createIconButton(self.props)
end
else
if isArrow then
return Roact.createElement(IconButton, {
visible = visible,
anchorPoint = anchorPoint,
position = position,
size = size,
zIndex = zIndex,
onClick = onClick,
iconHorizontalAlignment = Enum.HorizontalAlignment.Left,
iconSpritePath = BACK_IMAGE_SPRITE_PATH,
iconSpriteFrame = BACK_IMAGE_SPRITE_FRAME,
})
else
return Roact.createElement(RectangleButton, self.props, {
TextLabel = Roact.createElement("TextLabel", {
Position = UDim2.new(0.5, 0, 0.5, 0),
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Action.InviteFriendsBack"),
TextSize = BACK_BUTTON_TEXT_SIZE,
TextColor3 = Constants.Color.WHITE,
Font = Enum.Font.SourceSansSemibold,
ZIndex = zIndex,
}),
})
end
end
end
return BackButton
@@ -0,0 +1,100 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Roact = require(CorePackages.Roact)
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Constants = require(ShareGame.Constants)
local LIST_PADDING = 2
local TITLE_FONT = Enum.Font.SourceSans
local TITLE_COLOR = Constants.Color.WHITE
local TITLE_TEXT_SIZE = 19
local SUB_TITLE_FONT = Enum.Font.SourceSans
local SUB_TITLE_COLOR = Constants.Color.GRAY3
local SUB_TITLE_TEXT_SIZE = 16
local PRESENCE_FONT = Enum.Font.SourceSans
local PRESENCE_TEXT_SIZE = 16
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local ConversationDetails = Roact.PureComponent:extend("ConversationDetails")
function ConversationDetails:render()
local title = self.props.title
local subtitle = self.props.subtitle
local presence = self.props.presence
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local zIndex = self.props.zIndex
-- Show subtitle if it exists
local subtitleTextComponent
if subtitle ~= nil then
subtitleTextComponent = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, SUB_TITLE_TEXT_SIZE),
Text = subtitle,
Font = SUB_TITLE_FONT,
TextColor3 = SUB_TITLE_COLOR,
TextSize = SUB_TITLE_TEXT_SIZE,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
LayoutOrder = 1,
ZIndex = zIndex,
})
end
-- Show user presence if it was passed in
local presenceTextComponent
if presence ~= nil then
presenceTextComponent = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, PRESENCE_TEXT_SIZE),
Text = RobloxTranslator:FormatByKey(Constants.PresenceTextKey[presence]),
Font = PRESENCE_FONT,
TextColor3 = Constants.PresenceColors[presence],
TextSize = PRESENCE_TEXT_SIZE,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = 2,
ZIndex = zIndex,
})
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = size,
LayoutOrder = layoutOrder,
ZIndex = zIndex,
}, {
UIListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, LIST_PADDING)
}),
Title = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, TITLE_TEXT_SIZE),
Text = title or "",
Font = TITLE_FONT,
TextColor3 = TITLE_COLOR,
TextSize = TITLE_TEXT_SIZE,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
LayoutOrder = 0,
ZIndex = zIndex,
}),
Subtitle = subtitleTextComponent,
Presence = presenceTextComponent,
})
end
return ConversationDetails
@@ -0,0 +1,146 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(CorePackages.Roact)
local ShareGame = Modules.Settings.Pages.ShareGame
local ConversationDetails = require(ShareGame.Components.ConversationDetails)
local ConversationThumbnail = require(ShareGame.Components.ConversationThumbnail)
local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream)
local InviteButton = require(ShareGame.Components.InviteButton)
local isSelectionGroupEnabled = require(ShareGame.isSelectionGroupEnabled)
local Constants = require(ShareGame.Constants)
local InviteStatus = Constants.InviteStatus
local FFlagLuaInviteFailOnZeroPlaceId = settings():GetFFlag("LuaInviteFailOnZeroPlaceIdV384")
local ENTRY_BG_IMAGE = "rbxasset://textures/ui/dialog_white.png"
local ENTRY_BG_SLICE = Rect.new(10, 10, 10, 10)
local ENTRY_BG_TRANSPARENCY = 0.85
local THUMBNAIL_SIZE = 32
local INVITE_BUTTON_WIDTH = 69
local CONTENTS_PADDING = 12
local ConversationEntry = Roact.PureComponent:extend("ConversationEntry")
function ConversationEntry:init()
self.eventStream = EventStream.new()
self.onInvite = function()
if isSelectionGroupEnabled() then
local inviteStatus = self.props.inviteStatus
if inviteStatus and inviteStatus ~= InviteStatus.Failed then
return
end
end
local analytics = self.props.analytics
local users = self.props.users
local inviteUser = self.props.inviteUser
-- Check if this is a one-on-one convo
if #users == 1 then
local onSuccess = function(results)
if not results then
return
end
-- Pluck the userIds out of the user list
local participants = {}
for _, user in pairs(users) do
table.insert(participants, user.id)
end
local localPlayer = Players.LocalPlayer
analytics:onActivatedInviteSent(localPlayer.UserId, results.conversationId, participants)
end
local onReject
if FFlagLuaInviteFailOnZeroPlaceId then
onReject = function() end
end
inviteUser(users[1].id):andThen(onSuccess, onReject)
end
end
end
function ConversationEntry:render()
local visible = self.props.visible
local layoutOrder = self.props.layoutOrder
local zIndex = self.props.zIndex
local size = self.props.size
local subtitle = self.props.subtitle
local title = self.props.title
local users = self.props.users
local inviteStatus = self.props.inviteStatus
-- Presence gets passed in if there's only one user
local presence = self.props.presence
local isSelectable = nil
if isSelectionGroupEnabled() then
isSelectable = true
end
return Roact.createElement(isSelectionGroupEnabled() and "ImageButton" or "ImageLabel", {
Visible = visible,
Selectable = isSelectable,
BackgroundTransparency = 1,
Image = ENTRY_BG_IMAGE,
ImageTransparency = ENTRY_BG_TRANSPARENCY,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = ENTRY_BG_SLICE,
Size = size,
LayoutOrder = layoutOrder,
ZIndex = zIndex,
[Roact.Event.Activated] = isSelectionGroupEnabled() and self.onInvite or nil,
}, {
UIPadding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, CONTENTS_PADDING),
PaddingRight = UDim.new(0, CONTENTS_PADDING),
PaddingTop = UDim.new(0, CONTENTS_PADDING),
PaddingBottom = UDim.new(0, CONTENTS_PADDING),
}),
UIListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 12),
}),
Thumbnail = Roact.createElement(ConversationThumbnail, {
users = users,
size = UDim2.new(0, THUMBNAIL_SIZE, 0, THUMBNAIL_SIZE),
layoutOrder = 0,
zIndex = zIndex,
}),
Details = Roact.createElement(ConversationDetails, {
title = title,
subtitle = subtitle,
presence = presence,
size = UDim2.new(
-- Make details fullwidth and subtract the width of its siblings
1, -(THUMBNAIL_SIZE + INVITE_BUTTON_WIDTH + CONTENTS_PADDING * 2),
1, 0
),
layoutOrder = 1,
zIndex = zIndex,
}),
InviteButton = Roact.createElement(InviteButton, {
size = UDim2.new(0, INVITE_BUTTON_WIDTH, 1, 0),
layoutOrder = 2,
zIndex = zIndex,
onInvite = self.onInvite,
inviteStatus = inviteStatus,
}),
})
end
return ConversationEntry
@@ -0,0 +1,247 @@
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local AppTempCommon = CorePackages.AppTempCommon
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Players = game:GetService("Players")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local isSelectionGroupEnabled = require(ShareGame.isSelectionGroupEnabled)
local Constants = require(ShareGame.Constants)
local ConversationEntry = require(ShareGame.Components.ConversationEntry)
local FriendsErrorPage = require(ShareGame.Components.FriendsErrorPage)
local InviteUserIdToPlaceId = require(ShareGame.Thunks.InviteUserIdToPlaceId)
local LoadingFriendsPage = require(ShareGame.Components.LoadingFriendsPage)
local NoFriendsPage = require(ShareGame.Components.NoFriendsPage)
local PlayerSearchPredicate = require(CoreGui.RobloxGui.Modules.InGameMenu.Utility.PlayerSearchPredicate)
local User = require(AppTempCommon.LuaApp.Models.User)
local httpRequest = require(AppTempCommon.Temp.httpRequest)
local memoize = require(AppTempCommon.Common.memoize)
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local UsePlayerDisplayName = require(RobloxGui.Modules.Settings.UsePlayerDisplayName)
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local ENTRY_HEIGHT = 62
local ENTRY_PADDING = 18
local NO_RESULTS_FONT = Enum.Font.SourceSans
local NO_RESULTS_TEXTCOLOR = Constants.Color.GRAY3
local NO_RESULTS_TEXTSIZE = 19
local NO_RESULTS_TRANSPRENCY = 0.22
local PRESENCE_WEIGHTS = {
[User.PresenceType.ONLINE] = 3,
[User.PresenceType.IN_GAME] = 2,
[User.PresenceType.IN_STUDIO] = 1,
[User.PresenceType.OFFLINE] = 0,
}
local ConversationList = Roact.PureComponent:extend("ConversationList")
if FFlagLuaInviteModalEnabled then
ConversationList.defaultProps = {
entryHeight = ENTRY_HEIGHT,
entryPadding = ENTRY_PADDING
}
end
local function searchFilterPredicate(query, other)
if query == "" then
return true
end
return string.find(string.lower(other), query:lower(), 1, true) ~= nil
end
function ConversationList:init()
if isSelectionGroupEnabled() then
self.scrollingRef = Roact.createRef()
end
end
function ConversationList:render()
local analytics = self.props.analytics
local children = self.props[Roact.Children] or {}
local entryHeight
local entryPadding
if FFlagLuaInviteModalEnabled then
entryHeight = self.props.entryHeight
entryPadding = self.props.entryPadding
else
entryHeight = ENTRY_HEIGHT
entryPadding = ENTRY_PADDING
end
local friends = self.props.friends
local friendsRetrievalStatus = self.props.friendsRetrievalStatus
local layoutOrder = self.props.layoutOrder
local size = self.props.size
local zIndex = self.props.zIndex
local topPadding = self.props.topPadding
local invites = self.props.invites
local inviteUser = self.props.inviteUser
local searchText = self.props.searchText
children["RowListLayout"] = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Top,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, entryPadding),
})
local numEntries = 0
-- Populate list of conversations with friends
for i, user in ipairs(friends) do
local isEntryShown
if UsePlayerDisplayName() then
isEntryShown = PlayerSearchPredicate(searchText, user.name, user.displayName)
else
isEntryShown = searchFilterPredicate(searchText, user.name)
end
children["User-" .. tostring(i)] = Roact.createElement(ConversationEntry, {
analytics = analytics,
visible = isEntryShown,
size = UDim2.new(1, 0, 0, entryHeight),
layoutOrder = i,
zIndex = zIndex,
title = UsePlayerDisplayName() and user.displayName or user.name,
subtitle = UsePlayerDisplayName() and "@" .. user.name or nil,
presence = user.presence,
users = {user},
inviteUser = inviteUser,
inviteStatus = invites[user.id],
})
if isEntryShown then
numEntries = numEntries + 1
end
end
if #friends == 0 or friendsRetrievalStatus == RetrievalStatus.Fetching then
local zeroFriendsComponent = LoadingFriendsPage
if friendsRetrievalStatus == RetrievalStatus.Done then
zeroFriendsComponent = NoFriendsPage
elseif friendsRetrievalStatus == RetrievalStatus.Failed then
zeroFriendsComponent = FriendsErrorPage
end
return Roact.createElement(zeroFriendsComponent, {
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
Position = UDim2.new(0, 0, 0, topPadding),
ZIndex = zIndex,
})
else
if numEntries == 0 then
return Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
Font = NO_RESULTS_FONT,
Size = UDim2.new(1, 0, 0, entryHeight),
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Label.InviteSearchNoResults"),
TextColor3 = NO_RESULTS_TEXTCOLOR,
TextSize = NO_RESULTS_TEXTSIZE,
TextTransparency = NO_RESULTS_TRANSPRENCY,
ZIndex = zIndex,
})
end
end
local isSelectable = nil
if isSelectionGroupEnabled() then
isSelectable = false
end
return Roact.createElement("ScrollingFrame", {
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
Size = size,
CanvasSize = UDim2.new(0, 0, 0, numEntries * (entryHeight + entryPadding)),
ScrollBarThickness = 0,
ZIndex = zIndex,
Selectable = isSelectable,
[Roact.Ref] = isSelectionGroupEnabled() and self.scrollingRef or nil,
}, children)
end
local function handleBinding(self)
if isSelectionGroupEnabled() then
local conversationList = self.scrollingRef:getValue()
if conversationList then
if GuiService.SelectedCoreObject == nil then
GuiService:AddSelectionParent("invitePrompt", conversationList)
for _, object in ipairs(conversationList:GetChildren()) do
if object:IsA("GuiObject") and object.LayoutOrder == 1 then
GuiService.SelectedCoreObject = object
break
end
end
end
end
end
end
ConversationList.didUpdate = handleBinding
ConversationList.didMount = handleBinding
local selectFriends = memoize(function(users)
local friends = {}
local function friendPreference(friend1, friend2)
local friend1Weight = PRESENCE_WEIGHTS[friend1.presence]
local friend2Weight = PRESENCE_WEIGHTS[friend2.presence]
if friend1Weight == friend2Weight then
return friend1.name:lower() < friend2.name:lower()
else
return friend1Weight > friend2Weight
end
end
for _, user in pairs(users) do
if user.isFriend then
friends[#friends + 1] = user
end
end
table.sort(friends, friendPreference)
return friends
end)
local connector = RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
friends = selectFriends(state.Users),
friendsRetrievalStatus = state.Friends.retrievalStatus[tostring(Players.LocalPlayer.UserId)],
invites = state.Invites,
}
end,
function(dispatch)
return {
inviteUser = function(userId)
local requestImpl = httpRequest(HttpRbxApiService)
local placeId = tostring(game.PlaceId)
return dispatch(InviteUserIdToPlaceId(requestImpl, userId, placeId))
end,
}
end
)
return connector(ConversationList)
@@ -0,0 +1,174 @@
local CorePackages = game:GetService("CorePackages")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(CorePackages.Roact)
local Constants = require(Modules.Settings.Pages.ShareGame.Constants)
local BORDER_SIZE = 1
local BORDER_COLOR = Constants.Color.GRAY3
local THUMBNAIL_IMAGE_SIZE = Constants.InviteAvatarThumbnailSize
local THUMBNAIL_IMAGE_TYPE = Constants.InviteAvatarThumbnailType
local DEFAULT_THUMBNAIL_ICON = "rbxasset://textures/ui/LuaApp/graphic/ph-avatar-portrait.png"
-- (Borrowed values from LuaChat)
-- This lets us determine how to build the group thumbnail.
-- Index represents how many people are in the thumbnail!
local IMAGE_LAYOUT = {
[1] = {
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(1, 0, 1, 0),
FramePosition = UDim2.new(0, 0, 0, 0),
},
},
[2] = {
{
Size = UDim2.new(2, 0, 1, 0),
Position = UDim2.new(-0.5, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 1, 0),
FramePosition = UDim2.new(0, 0, 0, 0),
Border = {
BorderPosition = UDim2.new(0.5, -1, 0, 0),
BorderSize = UDim2.new(0, 1, 1, 0),
},
},
{
Size = UDim2.new(2, 0, 1, 0),
Position = UDim2.new(-0.5, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 1, 0),
FramePosition = UDim2.new(0.5, 1, 0, 0),
},
},
[3] = {
{
Size = UDim2.new(2, 0, 1, 0),
Position = UDim2.new(-0.5, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 1, 0),
FramePosition = UDim2.new(0, 0, 0, 0),
Border = {
BorderPosition = UDim2.new(0.5, -1, 0, 0),
BorderSize = UDim2.new(0, 1, 1, 0),
},
},
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
FramePosition = UDim2.new(0.5, 1, 0, 0),
Border = {
BorderPosition = UDim2.new(0.5, 0, 0.5, -1),
BorderSize = UDim2.new(0.5, 0, 0, 1),
},
},
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
FramePosition = UDim2.new(0.5, 1, 0.5, 1),
},
},
[4] = {
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
FramePosition = UDim2.new(0, 0, 0, 0),
Border = {
BorderPosition = UDim2.new(0, 0, 0.5, -1),
BorderSize = UDim2.new(1, 0, 0, 1),
},
},
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
FramePosition = UDim2.new(0.5, 1, 0, 0),
},
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
FramePosition = UDim2.new(0, 0, 0.5, 1),
Border = {
BorderPosition = UDim2.new(0.5, -1, 0, 0),
BorderSize = UDim2.new(0, 1, 1, 0),
},
},
{
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
FramePosition = UDim2.new(0.5, 1, 0.5, 1),
},
},
}
local ConversationThumbnail = Roact.PureComponent:extend("ConversationThumbnail")
function ConversationThumbnail:render()
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local zIndex = self.props.zIndex
local users = self.props.users
local numUsers = #users
-- Render the thumbnail contents
local thumbnailContents = {}
for i, user in ipairs(users) do
local layoutData = IMAGE_LAYOUT[numUsers][i]
-- Render the avatar inside a clipping container
thumbnailContents["AvatarHolder-"..i] = Roact.createElement("Frame", {
BackgroundTransparency = 1,
ClipsDescendants = true,
Size = layoutData.FrameSize,
Position = layoutData.FramePosition,
ZIndex = zIndex,
}, {
Avatar = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Size = layoutData.Size,
Position = layoutData.Position,
Image = user and user.thumbnails and user.thumbnails[THUMBNAIL_IMAGE_TYPE]
and user.thumbnails[THUMBNAIL_IMAGE_TYPE][THUMBNAIL_IMAGE_SIZE] or DEFAULT_THUMBNAIL_ICON,
ZIndex = zIndex,
})
})
-- Render any borders between avatars
if layoutData.Border then
thumbnailContents["Border-"..i] = Roact.createElement("Frame", {
Size = layoutData.Border.BorderSize,
Position = layoutData.Border.BorderPosition,
BorderSizePixel = 0,
BackgroundColor3 = BORDER_COLOR,
ZIndex = zIndex,
})
end
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = size,
LayoutOrder = layoutOrder,
ZIndex = zIndex,
}, {
ContentsContainer = Roact.createElement("Frame", {
-- Render the border inwards instead of outwards
BackgroundTransparency = 0,
Size = UDim2.new(1, -BORDER_SIZE, 1, -BORDER_SIZE),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundColor3 = Constants.Color.WHITE,
BorderColor3 = BORDER_COLOR,
BorderSizePixel = BORDER_SIZE,
ZIndex = zIndex,
}, thumbnailContents),
})
end
return ConversationThumbnail
@@ -0,0 +1,144 @@
local CorePackages = game:GetService("CorePackages")
local RunService = game:GetService("RunService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Constants = require(ShareGame.Constants)
local StoppedToastTimer = require(ShareGame.Actions.StoppedToastTimer)
local InviteStatus = Constants.InviteStatus
local INNER_TEXT_PADDING = 6
local TEXT_SIZE = 16
local TIMER_LENGTH = 4
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local ErrorToaster = Roact.PureComponent:extend("ErrorToaster")
function ErrorToaster:restartTimer()
if not self.timerConnection then
self.timerConnection =
RunService.RenderStepped:Connect(
function()
local failedInvites = self.props.failedInvites
if #failedInvites == 0 then
self:stopTimer()
return
end
local lastModerated = failedInvites[#failedInvites]
local goalTick = lastModerated.timeStamp + TIMER_LENGTH
local now = tick()
if now > goalTick then
self:stopTimer()
end
end
)
end
end
function ErrorToaster:stopTimer()
if self.timerConnection then
self.timerConnection:Disconnect()
self.timerConnection = nil
end
self.props.stoppedTimerDispatch()
end
function ErrorToaster:didUpdate()
local failedInvites = self.props.failedInvites
if #failedInvites ~= 0 then
self:restartTimer()
end
end
function ErrorToaster:render()
local deviceLayout = self.props.deviceLayout
local failedInvites = self.props.failedInvites
local layoutSpecific = Constants.LayoutSpecific[deviceLayout]
local toastHeight = layoutSpecific.TOAST_HEIGHT
if #failedInvites == 0 then
return
end
local errorMessageKey
local inviteStatus = failedInvites[#failedInvites].status
if inviteStatus == InviteStatus.Moderated then
errorMessageKey = "Feature.SettingsHub.Label.ModeratedInviteError"
elseif inviteStatus == InviteStatus.Failed then
errorMessageKey = "Feature.SettingsHub.Label.GameInviteError"
else
return
end
return Roact.createElement(
"ScreenGui",
{
DisplayOrder = 2,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
},
{
ToastFrame = Roact.createElement(
"Frame",
{
AnchorPoint = Vector2.new(0.5, 0),
BackgroundColor3 = Constants.Color.RED,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 0, 0, 0),
Size = UDim2.new(0.5, 0, 0, toastHeight)
},
{
InnerTextPadding = Roact.createElement(
"UIPadding",
{
PaddingLeft = UDim.new(0, INNER_TEXT_PADDING),
PaddingRight = UDim.new(0, INNER_TEXT_PADDING)
}
),
ToastText = Roact.createElement(
"TextLabel",
{
BackgroundTransparency = 1,
Font = Enum.Font.SourceSansSemibold,
Size = UDim2.new(1, 0, 1, 0),
Text = RobloxTranslator:FormatByKey(errorMessageKey),
TextColor3 = Constants.Color.WHITE,
TextSize = TEXT_SIZE,
TextWrapped = true
}
)
}
)
}
)
end
local function mapStateToProps(state)
return {
deviceLayout = state.DeviceInfo.DeviceLayout,
failedInvites = state.Toasts.failedInvites
}
end
local function mapDispatchToProps(dispatch)
return {
stoppedTimerDispatch = function()
dispatch(StoppedToastTimer())
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(ErrorToaster)
@@ -0,0 +1,54 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Roact = require(CorePackages.Roact)
local Constants = require(ShareGame.Constants)
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local FriendsErrorPage = Roact.PureComponent:extend("FriendsErrorPage")
function FriendsErrorPage:render()
local layoutOrder = self.props.LayoutOrder
local zIndex = self.props.ZIndex
local incrementingLayoutOrder = 0
local function incrementLayoutOrder()
incrementingLayoutOrder = incrementingLayoutOrder + 1
return incrementingLayoutOrder
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
LayoutOrder = layoutOrder,
ZIndex = zIndex,
}, {
listLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
subtitle = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Label.LoadingFriendsListFailed"),
TextColor3 = Constants.Color.GRAY5,
TextTransparency = 0.22,
TextSize = 21,
TextWrapped = true,
Font = Enum.Font.SourceSans,
LayoutOrder = incrementLayoutOrder(),
Size = UDim2.new(0, 280, 0, 42),
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = zIndex,
}),
})
end
return FriendsErrorPage
@@ -0,0 +1,46 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local ShareGameComponents = script.Parent
local ShareGameContainer = require(ShareGameComponents.ShareGameContainer)
local ModalShareGamePageFrame = require(ShareGameComponents.ModalShareGamePageFrame)
local LayoutProvider = require(ShareGameComponents.LayoutProvider)
local FullModalShareGameComponent = Roact.PureComponent:extend("FullModalShareGameComponent")
function FullModalShareGameComponent:render()
local isVisible = self.props.isVisible
local analytics = self.props.analytics
local onAfterClosePage = self.props.onAfterClosePage
local store = self.props.store
return Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
screenGui = Roact.createElement("ScreenGui", {
Enabled = isVisible,
DisplayOrder = -1,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, {
layoutProvider = Roact.createElement(LayoutProvider, nil, {
ShareGameContainer = Roact.createElement(ShareGameContainer, {
analytics = analytics,
isVisible = isVisible,
skeletonComponent = ModalShareGamePageFrame,
onAfterClosePage = function()
local sentToUserIds = {}
for userId, _ in pairs(store:getState().Invites) do
table.insert(sentToUserIds, userId)
end
onAfterClosePage(sentToUserIds)
end,
})
})
}),
})
end
return FullModalShareGameComponent
@@ -0,0 +1,52 @@
return function()
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
if not FFlagLuaInviteModalEnabled then
return
end
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local ShareGameAppReducer = require(script.Parent.Parent.AppReducer)
local FullModalShareGameComponent = require(script.Parent.FullModalShareGameComponent)
local function createStore()
return Rodux.Store.new(ShareGameAppReducer, nil, { Rodux.thunkMiddleware })
end
describe("createElement", function()
it("should mount and unmount without issue", function()
local fullModalElement = Roact.createElement(FullModalShareGameComponent, {
store = createStore(),
})
local fullModalInstance = Roact.mount(fullModalElement)
Roact.unmount(fullModalInstance)
end)
end)
describe("mount and reconcile w/ isVisible", function()
it("should mount and reconcile without issue", function()
local function createModal(isVisible)
return Roact.createElement(FullModalShareGameComponent, {
store = createStore(),
isVisible = isVisible,
})
end
local folder = Instance.new("Folder")
local instance = Roact.mount(createModal(false), folder)
expect(instance).to.be.ok()
expect(folder:FindFirstChildOfClass("ScreenGui", true).Enabled).to.equal(false)
local newInstance = Roact.update(instance, createModal(true))
expect(newInstance).to.be.ok()
expect(folder:FindFirstChildOfClass("ScreenGui", true).Enabled).to.equal(true)
end)
end)
end
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Roact = require(CorePackages.Roact)
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Constants = require(ShareGame.Constants)
local BackButton = require(ShareGame.Components.BackButton)
local SearchArea = require(ShareGame.Components.SearchArea)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local Header = Roact.PureComponent:extend("Header")
function Header:render()
local deviceLayout = self.props.deviceLayout
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local zIndex = self.props.zIndex
local closePage = self.props.closePage
local searchAreaActive = self.props.searchAreaActive
local toggleSearchIcon
local iconType
if FFlagLuaInviteModalEnabled then
toggleSearchIcon = self.props.toggleSearchIcon
iconType = self.props.iconType
end
local layoutSpecific = Constants.LayoutSpecific[deviceLayout]
local isDesktop = deviceLayout == Constants.DeviceLayout.DESKTOP
local isSearchingOnMobile
if not FFlagLuaInviteModalEnabled then
isSearchingOnMobile = (not isDesktop) and searchAreaActive
end
local isSearchingWithIcon
local backButtonWidth
if FFlagLuaInviteModalEnabled then
isSearchingWithIcon = toggleSearchIcon and searchAreaActive
if iconType == BackButton.IconType.None then
backButtonWidth = layoutSpecific.BACK_BUTTON_WIDTH
else
backButtonWidth = layoutSpecific.BACK_BUTTON_MODAL_WIDTH
end
end
local isArrow
local isTitleVisible
local visible
if FFlagLuaInviteModalEnabled then
isTitleVisible = not isSearchingWithIcon
visible = true
else
isTitleVisible = not isSearchingOnMobile
isArrow = not isDesktop
visible = not isSearchingOnMobile
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = size,
AnchorPoint = Vector2.new(0, 1),
LayoutOrder = layoutOrder,
ZIndex = zIndex,
}, {
Title = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Visible = isTitleVisible,
Position = UDim2.new(0.5, 0, 0.5, 0),
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Heading.InviteFriends"),
TextSize = layoutSpecific.PAGE_TITLE_TEXT_SIZE,
TextColor3 = Constants.Color.WHITE,
Font = Enum.Font.SourceSansSemibold,
ZIndex = zIndex,
}),
BackButton = Roact.createElement(BackButton, {
isArrow = isArrow,
visible = visible,
iconType = iconType,
position = UDim2.new(0, 0, 0.5, 0),
size = UDim2.new(
0, FFlagLuaInviteModalEnabled and backButtonWidth or layoutSpecific.BACK_BUTTON_WIDTH,
0, layoutSpecific.BACK_BUTTON_HEIGHT
),
anchorPoint = Vector2.new(0, 0.5),
zIndex = zIndex,
onClick = closePage,
}),
SearchArea = Roact.createElement(SearchArea, {
fullWidthSearchBar = FFlagLuaInviteModalEnabled and toggleSearchIcon or not isDesktop,
searchBoxMargin = layoutSpecific.SEARCH_BOX_MARGIN,
anchorPoint = Vector2.new(1, 0.5),
position = UDim2.new(1, 0, 0.5, 0),
size = FFlagLuaInviteModalEnabled and UDim2.new(1, -backButtonWidth, 1, 0),
zIndex = zIndex,
})
})
end
return Header
@@ -0,0 +1,74 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local IconButton = Roact.PureComponent:extend("IconButton")
function IconButton:render()
local visible = self.props.visible
local anchorPoint = self.props.anchorPoint
local position = self.props.position
local size = self.props.size
local zIndex = self.props.zIndex
local onClick = self.props.onClick
local iconHorizontalAlignment = self.props.iconHorizontalAlignment or Enum.HorizontalAlignment.Center
local iconVerticalAlignment = self.props.iconVerticalAlignment or Enum.VerticalAlignment.Center
local iconSpritePath = self.props.iconSpritePath
local iconSpriteFrame = self.props.iconSpriteFrame
local horizontalInset = self.props.horizontalInset or 12
local offsetLayoutOrder = 0
local iconLayoutOrder = 1
if iconHorizontalAlignment == Enum.HorizontalAlignment.Center then
horizontalInset = 0
elseif iconHorizontalAlignment == Enum.HorizontalAlignment.Left then
position = UDim2.new(position.X.Scale, position.X.Offset - horizontalInset, position.Y.Scale, position.Y.Offset)
elseif iconHorizontalAlignment == Enum.HorizontalAlignment.Right then
position = UDim2.new(position.X.Scale, position.X.Offset + horizontalInset, position.Y.Scale, position.Y.Offset)
offsetLayoutOrder = 1
iconLayoutOrder = 0
end
size = UDim2.new(size.X.Scale, size.X.Offset + horizontalInset, size.Y.Scale, size.Y.Offset)
return Roact.createElement("ImageButton", {
Visible = visible,
BackgroundTransparency = 1,
BorderSizePixel = 0,
AnchorPoint = anchorPoint,
Position = position,
Size = size,
ZIndex = zIndex,
[Roact.Event.Activated] = onClick,
}, {
IconLayout = Roact.createElement("UIListLayout", {
HorizontalAlignment = iconHorizontalAlignment,
VerticalAlignment = iconVerticalAlignment,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Offset = Roact.createElement("Frame", {
BorderSizePixel = 0,
LayoutOrder = offsetLayoutOrder,
Size = UDim2.new(0, horizontalInset, 0, 0),
}),
BackIcon = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(0, iconSpriteFrame.size.X, 0, iconSpriteFrame.size.Y),
Image = iconSpritePath,
ImageRectOffset = iconSpriteFrame.offset,
ImageRectSize = iconSpriteFrame.size,
ZIndex = zIndex,
LayoutOrder = iconLayoutOrder,
}),
})
end
return IconButton
@@ -0,0 +1,81 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Roact = require(CorePackages.Roact)
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Immutable = require(AppTempCommon.Common.Immutable)
local Constants = require(ShareGame.Constants)
local RectangleButton = require(ShareGame.Components.RectangleButton)
local INVITE_TEXT_FONT = Enum.Font.SourceSansSemibold
local INVITE_TEXT_SIZE = 19
local InviteStatus = Constants.InviteStatus
local MODERATED_TEXT = "Feature.SettingsHub.Label.Moderated"
local INVITE_STATUS_TEXT = {
[InviteStatus.Success] = "Feature.SettingsHub.Label.Invited",
[InviteStatus.Moderated] = MODERATED_TEXT,
[InviteStatus.Pending] = "Feature.SettingsHub.Label.Sending",
}
local FFlagLuaInviteGameHandleUnknownResponse = settings():GetFFlag("LuaInviteGameHandleUnknownResponse")
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local InviteButton = Roact.PureComponent:extend("InviteButton")
function InviteButton:render()
local anchorPoint = self.props.anchorPoint
local position = self.props.position
local size = self.props.size
local layoutOrder = self.props.layoutOrder
local zIndex = self.props.zIndex
local onInvite = self.props.onInvite
local inviteStatus = self.props.inviteStatus
if not inviteStatus or inviteStatus == InviteStatus.Failed then
local buttonProps = Immutable.Set(self.props, "onClick", function()
onInvite()
end)
return Roact.createElement(RectangleButton, buttonProps, {
InviteText = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Position = UDim2.new(0.5, 0, 0.5, 0),
Font = INVITE_TEXT_FONT,
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Action.InviteFriend"),
TextSize = INVITE_TEXT_SIZE,
TextColor3 = Constants.Color.WHITE,
ZIndex = zIndex,
})
})
else
local inviteText = INVITE_STATUS_TEXT[inviteStatus]
if FFlagLuaInviteGameHandleUnknownResponse then
if not inviteText then
inviteText = MODERATED_TEXT
end
end
return Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
AnchorPoint = anchorPoint,
Position = position,
Size = size,
Font = INVITE_TEXT_FONT,
Text = RobloxTranslator:FormatByKey(inviteText),
TextSize = INVITE_TEXT_SIZE,
TextColor3 = Constants.Color.WHITE,
TextXAlignment = Enum.TextXAlignment.Right,
LayoutOrder = layoutOrder,
ZIndex = zIndex,
})
end
end
return InviteButton
@@ -0,0 +1,133 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local ShareGame = Modules.Settings.Pages.ShareGame
local Constants = require(ShareGame.Constants)
local SetDeviceOrientation = require(AppTempCommon.LuaApp.Actions.SetDeviceOrientation)
local SetIsSmallTouchScreen = require(ShareGame.Actions.SetIsSmallTouchScreen)
local SetDeviceLayout = require(ShareGame.Actions.SetDeviceLayout)
-- Magic values derived from CoreScripts/Modules/Settings/Utility.lua
local SMALL_DEVICE_HEIGHT = 500
local SMALL_DEVICE_WIDTH = 700
local LayoutProvider = Roact.Component:extend("LayoutProvider")
function LayoutProvider:didMount()
if workspace.CurrentCamera then
self:setObservedCamera(workspace.CurrentCamera)
end
self.cameraChangedListener = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
if workspace.CurrentCamera then
self:setObservedCamera(workspace.CurrentCamera)
end
end)
end
function LayoutProvider:willUnmount()
if self.cameraChangedListener then
self.cameraChangedListener:Disconnect()
end
if self.viewportSizeListener then
self.viewportSizeListener:Disconnect()
end
end
function LayoutProvider:setObservedCamera(camera)
if self.viewportSizeListener then
self.viewportSizeListener:Disconnect()
end
-- Listen for changes to ViewportSize and update DeviceInfo accordingly
self:checkAllDeviceInfo(camera.ViewportSize)
self.viewportSizeListener = camera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
-- Hacky code awaits underlying mechanism fix.
-- Viewport will get a 0,0,1,1 rect before it is properly set.
local viewportSize = camera.ViewportSize
if viewportSize.X <= 1 or viewportSize.Y <= 1 then
return
end
self:checkAllDeviceInfo(viewportSize)
end)
end
function LayoutProvider:checkDeviceOrientation(viewportSize)
local deviceOrientation = viewportSize.X > viewportSize.Y and
Constants.DeviceOrientation.LANDSCAPE or Constants.DeviceOrientation.PORTRAIT
if self._deviceOrientation ~= deviceOrientation then
self._deviceOrientation = deviceOrientation
self.props.setDeviceOrientation(deviceOrientation)
end
end
function LayoutProvider:checkDeviceIsSmallTouchScreen(viewportSize)
local isSmallTouchScreen = UserInputService.TouchEnabled and
(viewportSize.X < SMALL_DEVICE_WIDTH or viewportSize.Y < SMALL_DEVICE_HEIGHT)
if self._isSmallTouchScreen ~= isSmallTouchScreen then
self._isSmallTouchScreen = isSmallTouchScreen
self.props.setIsSmallTouchScreen(self._isSmallTouchScreen)
end
end
function LayoutProvider:checkDeviceLayout()
local deviceLayout
if self._isSmallTouchScreen then
if self._deviceOrientation == Constants.DeviceOrientation.LANDSCAPE then
deviceLayout = Constants.DeviceLayout.PHONE_LANDSCAPE
else
deviceLayout = Constants.DeviceLayout.PHONE_PORTRAIT
end
elseif UserInputService.TouchEnabled then
if self._deviceOrientation == Constants.DeviceOrientation.LANDSCAPE then
deviceLayout = Constants.DeviceLayout.TABLET_LANDSCAPE
else
deviceLayout = Constants.DeviceLayout.TABLET_PORTRAIT
end
else
deviceLayout = Constants.DeviceLayout.DESKTOP
end
if self._deviceLayout ~= deviceLayout then
self._deviceLayout = deviceLayout
self.props.setDeviceLayout(self._deviceLayout)
end
end
function LayoutProvider:checkAllDeviceInfo(viewportSize)
self:checkDeviceOrientation(viewportSize)
self:checkDeviceIsSmallTouchScreen(viewportSize)
self:checkDeviceLayout()
end
function LayoutProvider:render()
return Roact.oneChild(self.props[Roact.Children])
end
local connector = RoactRodux.UNSTABLE_connect2(nil, function(dispatch)
return {
setDeviceOrientation = function(orientation)
return dispatch(SetDeviceOrientation(orientation))
end,
setIsSmallTouchScreen = function(isSmall)
return dispatch(SetIsSmallTouchScreen(isSmall))
end,
setDeviceLayout = function(deviceLayout)
return dispatch(SetDeviceLayout(deviceLayout))
end,
}
end)
return connector(LayoutProvider)
@@ -0,0 +1,50 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local _RobloxGui = CoreGui:WaitForChild("RobloxGui")
local LoadingBar = require(CorePackages.AppTempCommon.LuaApp.Components.LoadingBar)
local Roact = require(CorePackages.Roact)
local LoadingFriendsPage = Roact.PureComponent:extend("LoadingFriendsPage")
local LOADING_INDICATOR_WIDTH = 70
local LOADING_INDICATOR_HEIGHT = 16
function LoadingFriendsPage:render()
local layoutOrder = self.props.LayoutOrder
local zIndex = self.props.ZIndex
local scale = 1
local incrementingLayoutOrder = 0
local function incrementLayoutOrder()
incrementingLayoutOrder = incrementingLayoutOrder + 1
return incrementingLayoutOrder
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
LayoutOrder = layoutOrder,
ZIndex = zIndex,
}, {
listLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
loadingIndicator = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, scale * LOADING_INDICATOR_WIDTH, 0, scale * LOADING_INDICATOR_HEIGHT),
LayoutOrder = incrementLayoutOrder(),
ZIndex = zIndex,
}, {
loadingBar = Roact.createElement(LoadingBar, {
ZIndex = zIndex,
}),
})
})
end
return LoadingFriendsPage
@@ -0,0 +1,128 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(CorePackages.Roact)
local ShareGame = Modules.Settings.Pages.ShareGame
local Header = require(ShareGame.Components.Header)
local ConversationList = require(ShareGame.Components.ConversationList)
local ToasterComponent = require(ShareGame.Components.ErrorToaster)
local BackButton = require(ShareGame.Components.BackButton)
local isSelectionGroupEnabled = require(ShareGame.isSelectionGroupEnabled)
local HEADER_HEIGHT = 60
local USER_LIST_PADDING = 10
local CONTENT_PADDING = 15
local CONVERSATION_ENTRY_HEIGHT = 62
local CONVERSATION_ENTRY_PADDING = 18
local CLAMP_TO_FIVE_HALF_ENTRIES = (HEADER_HEIGHT)
+ ((CONVERSATION_ENTRY_HEIGHT + CONVERSATION_ENTRY_PADDING) * 5.5)
local MAX_MODAL_WIDTH = UDim.new(0.8, 320)
local MAX_MODAL_HEIGHT = UDim.new(0.7, CLAMP_TO_FIVE_HALF_ENTRIES)
local POSITION_HEIGHT_OFFSET = 0.075
local IMAGE_ROUNDED_BACKGROUND = "rbxasset://textures/ui/LuaChat/9-slice/btn-control-sm.png"
-- Color 41/41/41 comes from the SettingsShield background color
local SETTINGS_SHIELD_BACKGROUND_COLOR = Color3.fromRGB(41, 41, 41)
local BACKGROUND_BORDER_RADIUS = 3
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local ModalShareGamePageFrame = Roact.PureComponent:extend("ModalShareGamePageFrame")
ModalShareGamePageFrame.defaultProps = {
isVisible = true,
}
function ModalShareGamePageFrame:init()
self.onClosePage = function()
self.props.closePage()
if self.props.onAfterClosePage then
self.props.onAfterClosePage()
end
end
end
function ModalShareGamePageFrame:render()
local analytics = self.props.analytics
local deviceLayout = self.props.deviceLayout
local zIndex = self.props.zIndex
local searchAreaActive = self.props.searchAreaActive
local searchText = self.props.searchText
local isVisible = nil
if isSelectionGroupEnabled() then
isVisible = self.props.isVisible
end
return Roact.createElement("ImageButton", {
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = IMAGE_ROUNDED_BACKGROUND,
ImageColor3 = SETTINGS_SHIELD_BACKGROUND_COLOR,
ImageTransparency = 0.1,
Modal = isVisible,
Position = UDim2.new(0.5, 0, 0.5 - POSITION_HEIGHT_OFFSET, 0),
Size = UDim2.new(MAX_MODAL_WIDTH.Scale, 0, MAX_MODAL_HEIGHT.Scale, 0),
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(
Vector2.new(BACKGROUND_BORDER_RADIUS, BACKGROUND_BORDER_RADIUS),
Vector2.new(BACKGROUND_BORDER_RADIUS, BACKGROUND_BORDER_RADIUS)
),
}, {
sizeConstraint = Roact.createElement("UISizeConstraint", {
MaxSize = Vector2.new(MAX_MODAL_WIDTH.Offset, MAX_MODAL_HEIGHT.Offset),
}),
uiPadding2 = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, CONTENT_PADDING),
PaddingRight = UDim.new(0, CONTENT_PADDING),
PaddingBottom = UDim.new(0, CONTENT_PADDING),
}),
content = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
ZIndex = zIndex,
}, {
toasterPortal = Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
Toaster = Roact.createElement(ToasterComponent),
}),
Layout = Roact.createElement("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Header = Roact.createElement(Header, {
deviceLayout = deviceLayout,
size = UDim2.new(1, 0, 0, HEADER_HEIGHT),
layoutOrder = 0,
zIndex = zIndex,
closePage = self.onClosePage,
searchAreaActive = searchAreaActive,
toggleSearchIcon = true,
iconType = FFlagLuaInviteModalEnabled and BackButton.IconType.Cross or nil,
}),
ConversationList = Roact.createElement(ConversationList, {
analytics = analytics,
size = UDim2.new(1, 0, 1, -HEADER_HEIGHT),
topPadding = USER_LIST_PADDING,
layoutOrder = 1,
zIndex = zIndex,
searchText = searchText,
entryHeight = CONVERSATION_ENTRY_HEIGHT,
entryPadding = CONVERSATION_ENTRY_PADDING,
isVisible = isVisible,
})
})
})
end
return ModalShareGamePageFrame
@@ -0,0 +1,81 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Roact = require(CorePackages.Roact)
local Constants = require(ShareGame.Constants)
local ShareGameIcons = require(ShareGame.Spritesheets.ShareGameIcons)
local FRIENDS_ICON_FRAME = ShareGameIcons:GetFrame("friends")
local SHARE_GAME_ICONS_IMAGE = ShareGameIcons:GetImagePath()
local ICON_TO_SUBTITLE_PADDING = 34
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local NoFriendsPage = Roact.PureComponent:extend("NoFriendsPage")
function NoFriendsPage:render()
local layoutOrder = self.props.LayoutOrder
local zIndex = self.props.ZIndex
local incrementingLayoutOrder = 0
local function incrementLayoutOrder()
incrementingLayoutOrder = incrementingLayoutOrder + 1
return incrementingLayoutOrder
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
LayoutOrder = layoutOrder,
ZIndex = zIndex,
}, {
listLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
friendsIcon = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = SHARE_GAME_ICONS_IMAGE,
ImageRectOffset = FRIENDS_ICON_FRAME.offset,
ImageRectSize = FRIENDS_ICON_FRAME.size,
Size = UDim2.new(0, 72, 0, 72),
LayoutOrder = incrementLayoutOrder(),
ZIndex = zIndex,
}),
iconToSubtitleSpacer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 0, ICON_TO_SUBTITLE_PADDING),
LayoutOrder = incrementLayoutOrder(),
ZIndex = zIndex,
}),
subtitle = Roact.createElement("TextLabel", {
BackgroundTransparency = 1,
Text = RobloxTranslator:FormatByKey("Feature.SettingsHub.Label.NoFriendsScreen"),
TextColor3 = Constants.Color.GRAY5,
TextTransparency = 0.22,
TextSize = 21,
TextWrapped = true,
Font = Enum.Font.SourceSans,
LayoutOrder = incrementLayoutOrder(),
Size = UDim2.new(0, 280, 0, 42),
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = zIndex,
}),
bottomSpacer = Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 0, 120),
LayoutOrder = incrementLayoutOrder(),
ZIndex = zIndex,
}),
})
end
return NoFriendsPage
@@ -0,0 +1,102 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local isSelectionGroupEnabled = require(script:FindFirstAncestor("ShareGame").isSelectionGroupEnabled)
local BUTTON_IMAGE = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButton.png"
local BUTTON_IMAGE_ACTIVE = "rbxasset://textures/ui/Settings/MenuBarAssets/MenuButtonSelected.png"
local BUTTON_SLICE = Rect.new(8, 6, 46, 44)
local DROPSHADOW_SIZE = {
Left = 4, Right = 4,
Top = 2, Bottom = 6,
}
local RectangleButton = Roact.PureComponent:extend("RectangleButton")
RectangleButton.defaultProps = {
visible = true,
}
function RectangleButton:init()
self.state = {
isHovering = false,
}
end
function RectangleButton:render()
local size = self.props.size
local position = self.props.position
local anchorPoint = self.props.anchorPoint
local layoutOrder = self.props.layoutOrder
local zIndex = self.props.zIndex
local onClick = self.props.onClick
local visible
if FFlagLuaInviteModalEnabled then
visible = self.props.visible
end
local children = self.props[Roact.Children] or {}
local buttonImage = self.state.isHovering and BUTTON_IMAGE_ACTIVE or BUTTON_IMAGE
-- Insert padding so that child elements of this component are positioned
-- inside the button as expected. This is to offset the dropshadow
-- extending outside the button bounds.
children["UIPadding"] = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, DROPSHADOW_SIZE.Left),
PaddingRight = UDim.new(0, DROPSHADOW_SIZE.Right),
PaddingTop = UDim.new(0, DROPSHADOW_SIZE.Top),
PaddingBottom = UDim.new(0, DROPSHADOW_SIZE.Bottom),
})
local isSelectable = nil
if isSelectionGroupEnabled() then
isSelectable = false
end
return Roact.createElement("ImageButton", {
BackgroundTransparency = 1,
Image = "",
Selectable = isSelectable,
Size = size,
Position = position,
AnchorPoint = anchorPoint,
LayoutOrder = layoutOrder,
ZIndex = zIndex,
Visible = visible,
[Roact.Event.InputBegan] = function()
self:setState({isHovering = true})
end,
[Roact.Event.InputEnded] = function()
self:setState({isHovering = false})
end,
[Roact.Event.Activated] = function()
if onClick then
self:setState({isHovering = false})
onClick()
end
end,
}, {
ButtonBackground = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Position = UDim2.new(
0, -DROPSHADOW_SIZE.Left,
0, -DROPSHADOW_SIZE.Top
),
Size = UDim2.new(
1, DROPSHADOW_SIZE.Left + DROPSHADOW_SIZE.Right,
1, DROPSHADOW_SIZE.Top + DROPSHADOW_SIZE.Bottom
),
Image = buttonImage,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = BUTTON_SLICE,
ZIndex = zIndex,
}, children),
})
end
return RectangleButton
@@ -0,0 +1,210 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local SearchBox = require(ShareGame.Components.SearchBox)
local IconButton = require(ShareGame.Components.IconButton)
local Constants = require(ShareGame.Constants)
local Text = require(AppTempCommon.Common.Text)
local SetSearchAreaActive = require(ShareGame.Actions.SetSearchAreaActive)
local SetSearchText = require(ShareGame.Actions.SetSearchText)
local ShareGameIcons = require(RobloxGui.Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
local SEARCH_ICON_SPRITE_PATH = ShareGameIcons:GetImagePath()
local SEARCH_ICON_SPRITE_FRAME = ShareGameIcons:GetFrame("search_large")
local SEARCH_ICON_SIZE = 44
local SEARCH_BOX_WIDTH = 177
local SEARCH_BOX_HEIGHT = 28
local CANCEL_TEXT_FONT = Enum.Font.SourceSans
local CANCEL_TEXT_COLOR = Constants.Color.GRAY3
local CANCEL_TEXT_SIZE = 20
local SearchArea = Roact.PureComponent:extend("SearchArea")
function SearchArea:init()
self.state = {
cancelText = RobloxTranslator:FormatByKey("Feature.SettingsHub.Action.CancelSearch"),
}
self.searchField = nil
end
function SearchArea:render()
local fullWidthSearchBar = self.props.fullWidthSearchBar
local searchBoxMargin = self.props.searchBoxMargin
local anchorPoint = self.props.anchorPoint
local position = self.props.position
local zIndex = self.props.zIndex
local searchAreaActive = self.props.searchAreaActive
local size
if FFlagLuaInviteModalEnabled then
size = self.props.size
end
local cancelText = self.state.cancelText
local cancelTextWidth = Text.GetTextWidth(cancelText, CANCEL_TEXT_FONT, CANCEL_TEXT_SIZE)
local searchBoxSizeOffset = searchBoxMargin + cancelTextWidth
local setSearchText = self.props.setSearchText
if fullWidthSearchBar then
-- When full-width is true, the search box spans the entire width of the
-- parent frame, and it becomes hidden behind a search button.
return Roact.createElement("Frame", {
AnchorPoint = anchorPoint,
Position = position,
Size = size,
BackgroundTransparency = 1,
ZIndex = zIndex,
}, {
-- Render the search button if the search area hasn't been activated
-- yet.
SearchButton = Roact.createElement(IconButton, {
visible = not searchAreaActive,
anchorPoint = anchorPoint,
position = position,
size = UDim2.new(0, SEARCH_ICON_SIZE, 0, SEARCH_ICON_SIZE),
zIndex = zIndex,
iconHorizontalAlignment = Enum.HorizontalAlignment.Right,
iconSpritePath = SEARCH_ICON_SPRITE_PATH,
iconSpriteFrame = SEARCH_ICON_SPRITE_FRAME,
onClick = function(rbx)
self.props.setSearchAreaActive(true)
end,
}),
-- Show search box with cancel button if the search area is active
SearchBox = Roact.createElement(SearchBox, {
anchorPoint = Vector2.new(0, 0.5),
position = UDim2.new(0, 0, 0.5, 0),
size = UDim2.new(1, -searchBoxSizeOffset, 0, SEARCH_BOX_HEIGHT),
zIndex = zIndex,
visible = searchAreaActive,
modalFocused = searchAreaActive,
onTextChanged = setSearchText,
onTextBoxFocusLost = function(text)
if text == "" then
self.props.setSearchAreaActive(false)
end
end,
searchFieldRef = function(rbx)
self.searchField = rbx
end,
}),
Cancel = Roact.createElement("TextButton", {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.new(0, cancelTextWidth, 1, 0),
TextSize = CANCEL_TEXT_SIZE,
TextColor3 = CANCEL_TEXT_COLOR,
Font = CANCEL_TEXT_FONT,
ZIndex = zIndex,
Visible = searchAreaActive,
[Roact.Ref] = function(rbx)
if rbx then
-- Set initial text on this TextLabel
rbx.Text = cancelText
-- Listen to Text changed events in case of localization
-- (this is so we can trigger a resize)
if not self.textConnection then
local textChangedSignal = rbx:GetPropertyChangedSignal("Text")
self.textConnection = textChangedSignal:connect(function()
self:setState({
cancelText = rbx.Text
})
end)
end
end
end,
[Roact.Event.Activated] = function(rbx)
self.props.setSearchAreaActive(false)
end,
}),
})
else
-- Show a search box of a fixed size without any added behavior.
-- We don't bother setting SearchAreaActive here since we're not working
-- with a modal-style search box (i.e. nothing has to be shown or
-- hidden).
return Roact.createElement(SearchBox, {
anchorPoint = Vector2.new(1, 0.5),
position = UDim2.new(1, 0, 0.5, 0),
size = UDim2.new(0, SEARCH_BOX_WIDTH, 0, SEARCH_BOX_HEIGHT),
zIndex = zIndex,
onTextChanged = setSearchText,
searchFieldRef = function(rbx)
self.searchField = rbx
end,
})
end
end
function SearchArea:didUpdate(prevProps)
local fullWidthSearchBar = self.props.fullWidthSearchBar
if self.searchField then
-- Check if the page was closed so we can reset the search query
if not self.props.isPageOpen and prevProps.isPageOpen then
-- Spawn new thread so we don't trigger state updates from this
-- function
spawn(function()
-- Reset the entire search area
if fullWidthSearchBar then
self.props.setSearchAreaActive(false)
else
self.searchField.Text = ""
end
end)
end
-- Check if the search area has become active/inactive
if fullWidthSearchBar then
local searchAreaActive = self.props.searchAreaActive
local wasActive = prevProps.searchAreaActive
if searchAreaActive and not wasActive then
self.searchField:CaptureFocus()
elseif not searchAreaActive and wasActive then
self.searchField.Text = ""
end
end
end
end
SearchArea = RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
isPageOpen = state.Page.IsOpen,
searchAreaActive = state.ConversationsSearch.SearchAreaActive,
}
end,
function(dispatch)
return {
setSearchAreaActive = function(isActive)
dispatch(SetSearchAreaActive(isActive))
end,
setSearchText = function(text)
dispatch(SetSearchText(text))
end,
}
end
)(SearchArea)
return SearchArea
@@ -0,0 +1,158 @@
local CorePackages = game:GetService("CorePackages")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Roact = require(CorePackages.Roact)
local Constants = require(RobloxGui.Modules.Settings.Pages.ShareGame.Constants)
local SearchBox = Roact.Component:extend("SearchBox")
local ShareGameIcons = require(RobloxGui.Modules.Settings.Pages.ShareGame.Spritesheets.ShareGameIcons)
local SHARE_GAME_SPRITE_PATH = ShareGameIcons:GetImagePath()
local SEARCH_BORDER_SPRITE_IMAGE = SHARE_GAME_SPRITE_PATH
local SEARCH_BORDER_SPRITE_FRAME = ShareGameIcons:GetFrame("search_border")
local SEARCH_BORDER_SLICE = Rect.new(3, 3, 4, 4)
local SEARCH_ICON_SPRITE_IMAGE = SHARE_GAME_SPRITE_PATH
local SEARCH_ICON_SPRITE_FRAME = ShareGameIcons:GetFrame("search_small")
local SEARCH_ICON_SIZE = 16
local CLEAR_ICON_SPRITE_IMAGE = SHARE_GAME_SPRITE_PATH
local CLEAR_ICON_SPRITE_FRAME = ShareGameIcons:GetFrame("clear")
local CLEAR_ICON_SIZE = 16
local SEARCH_MARGINS_HORIZONTAL = 8
local SEARCH_MARGINS_VERTICAL = 6
local SEARCH_FIELD_MARGINS = 12
local SEARCH_BOX_TEXT_SIZE = 16
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local getTranslator = require(ShareGame.getTranslator)
local RobloxTranslator = getTranslator()
function SearchBox:init()
self.state = {
isTextWritten = false,
}
self.searchField = nil
end
function SearchBox:render()
local anchorPoint = self.props.anchorPoint
local onTextBoxFocusLost = self.props.onTextBoxFocusLost
local position = self.props.position
local searchFieldRef = self.props.searchFieldRef
local size = self.props.size
local visible = self.props.visible
local zIndex = self.props.zIndex
local isTextWritten = self.state.isTextWritten
return Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
AnchorPoint = anchorPoint,
Position = position,
Size = size,
Visible = visible,
Image = SEARCH_BORDER_SPRITE_IMAGE,
ImageRectOffset = SEARCH_BORDER_SPRITE_FRAME.offset,
ImageRectSize = SEARCH_BORDER_SPRITE_FRAME.size,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = SEARCH_BORDER_SLICE,
ZIndex = zIndex,
}, {
UIPadding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, SEARCH_MARGINS_HORIZONTAL),
PaddingRight = UDim.new(0, SEARCH_MARGINS_HORIZONTAL),
PaddingTop = UDim.new(0, SEARCH_MARGINS_VERTICAL),
PaddingBottom = UDim.new(0, SEARCH_MARGINS_VERTICAL),
}),
SearchIcon = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = SEARCH_ICON_SPRITE_IMAGE,
ImageRectOffset = SEARCH_ICON_SPRITE_FRAME.offset,
ImageRectSize = SEARCH_ICON_SPRITE_FRAME.size,
Size = UDim2.new(0, SEARCH_ICON_SIZE, 0, SEARCH_ICON_SIZE),
ZIndex = zIndex
}),
SearchField = Roact.createElement("TextBox", {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, SEARCH_ICON_SIZE + SEARCH_FIELD_MARGINS, 0.5, 0),
Size = UDim2.new(
1, -(SEARCH_ICON_SIZE + CLEAR_ICON_SIZE + SEARCH_FIELD_MARGINS * 2),
1, 0
),
ClearTextOnFocus = false,
PlaceholderColor3 = Constants.Color.GREY3,
PlaceholderText = RobloxTranslator:FormatByKey("Feature.SettingsHub.Label.SearchForFriendsPlaceholder"),
Text = "",
TextColor3 = Constants.Color.WHITE,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = SEARCH_BOX_TEXT_SIZE,
Font = Enum.Font.SourceSans,
ZIndex = zIndex,
[Roact.Ref] = function(rbx)
self.searchField = rbx
-- Check if a higher reference function has been passed in and
-- then call it.
if searchFieldRef then
searchFieldRef(rbx)
end
end,
[Roact.Event.FocusLost] = function(rbx)
if onTextBoxFocusLost then
onTextBoxFocusLost(rbx.Text)
end
end
}),
ClearButton = Roact.createElement("ImageButton", {
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0, CLEAR_ICON_SIZE, 0, SEARCH_ICON_SIZE),
Image = CLEAR_ICON_SPRITE_IMAGE,
ImageRectOffset = CLEAR_ICON_SPRITE_FRAME.offset,
ImageRectSize = CLEAR_ICON_SPRITE_FRAME.size,
ZIndex = zIndex,
Visible = isTextWritten,
[Roact.Event.Activated] = function()
if self.searchField then
self.searchField.Text = ""
-- We lost focus when clicking this button so we have to
-- recapture it.
self.searchField:CaptureFocus()
end
end
})
})
end
function SearchBox:didMount()
local searchField = self.searchField
local onTextChanged = self.props.onTextChanged
if searchField then
self.textBoxChangedConnection = searchField:GetPropertyChangedSignal("Text"):Connect(function()
local text = searchField.Text
self:setState({
isTextWritten = text:len() > 0
})
onTextChanged(text)
end)
end
end
function SearchBox:willUnmount()
if self.textBoxChangedConnection then
self.textBoxChangedConnection:Disconnect()
end
end
return SearchBox
@@ -0,0 +1,61 @@
--[[
Container for both the Share Game Page contents and header
]]
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local Players = game:GetService("Players")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = CoreGui.RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local httpRequest = require(AppTempCommon.Temp.httpRequest)
local ShareGamePageFrame = require(ShareGame.Components.ShareGamePageFrame)
local Constants = require(ShareGame.Constants)
local FetchUserFriends = require(ShareGame.Thunks.FetchUserFriends)
local ClosePage = require(ShareGame.Actions.ClosePage)
local ShareGameContainer = Roact.PureComponent:extend("ShareGameContainer")
ShareGameContainer.defaultProps = {
skeletonComponent = ShareGamePageFrame,
}
function ShareGameContainer:init()
self.props.reFetch()
end
function ShareGameContainer:render()
return Roact.createElement(self.props.skeletonComponent, self.props)
end
ShareGameContainer = RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
deviceLayout = state.DeviceInfo.DeviceLayout,
searchAreaActive = state.ConversationsSearch.SearchAreaActive,
searchText = state.ConversationsSearch.SearchText,
}
end,
function(dispatch)
return {
closePage = function()
dispatch(ClosePage(Constants.PageRoute.SHARE_GAME))
end,
reFetch = function()
local userId = tostring(Players.LocalPlayer.UserId)
local requestImpl = httpRequest(HttpRbxApiService)
dispatch(FetchUserFriends(requestImpl, userId))
end
}
end
)(ShareGameContainer)
return ShareGameContainer
@@ -0,0 +1,138 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local Players = game:GetService("Players")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = CoreGui.RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local isSelectionGroupEnabled = require(ShareGame.isSelectionGroupEnabled)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local FFlagDisableAutoTranslateForKeyTranslatedContent = require(RobloxGui.Modules.Flags.FFlagDisableAutoTranslateForKeyTranslatedContent)
local Roact = require(CorePackages.Roact)
local RoactRodux = require(CorePackages.RoactRodux)
local httpRequest
if not FFlagLuaInviteModalEnabled then
httpRequest = require(AppTempCommon.Temp.httpRequest)
end
local Header = require(ShareGame.Components.Header)
local ConversationList = require(ShareGame.Components.ConversationList)
local Constants = require(ShareGame.Constants)
local FetchUserFriends
local ClosePage
local BackButton
if FFlagLuaInviteModalEnabled then
BackButton = require(ShareGame.Components.BackButton)
else
FetchUserFriends = require(ShareGame.Thunks.FetchUserFriends)
ClosePage = require(ShareGame.Actions.ClosePage)
end
local USER_LIST_PADDING = 10
local ShareGamePageFrame = Roact.PureComponent:extend("ShareGamePageFrame")
local ToasterComponent = require(ShareGame.Components.ErrorToaster)
if not FFlagLuaInviteModalEnabled then
function ShareGamePageFrame:init()
self.props.reFetch()
end
end
function ShareGamePageFrame:render()
local analytics = self.props.analytics
local deviceLayout = self.props.deviceLayout
local zIndex = self.props.zIndex
local closePage = self.props.closePage
local searchAreaActive = self.props.searchAreaActive
local searchText = self.props.searchText
local layoutSpecific = Constants.LayoutSpecific[deviceLayout]
local headerHeight = layoutSpecific.HEADER_HEIGHT
local isDesktop
local iconType
local toggleSearchIcon
if FFlagLuaInviteModalEnabled then
isDesktop = deviceLayout == Constants.DeviceLayout.DESKTOP
iconType = not isDesktop and BackButton.IconType.Arrow or BackButton.IconType.None
toggleSearchIcon = not isDesktop
end
local isVisible = nil
if isSelectionGroupEnabled() then
isVisible = self.props.isVisible
end
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
ZIndex = zIndex,
AutoLocalize = not FFlagDisableAutoTranslateForKeyTranslatedContent,
}, {
toasterPortal = Roact.createElement(Roact.Portal, {
target = CoreGui,
}, {
Toaster = Roact.createElement(ToasterComponent),
}),
Header = Roact.createElement(Header, {
deviceLayout = deviceLayout,
size = UDim2.new(1, 0, 0, headerHeight),
position = UDim2.new(0, 0, 0, -headerHeight),
layoutOrder = 0,
zIndex = zIndex,
closePage = closePage,
searchAreaActive = searchAreaActive,
toggleSearchIcon = toggleSearchIcon,
iconType = iconType,
}),
ConversationList = Roact.createElement(ConversationList, {
analytics = analytics,
size = UDim2.new(1, 0, 1, layoutSpecific.EXTEND_BOTTOM_SIZE - USER_LIST_PADDING),
topPadding = USER_LIST_PADDING,
layoutOrder = 1,
zIndex = zIndex,
searchText = searchText,
isVisible = isVisible,
}),
})
end
if not FFlagLuaInviteModalEnabled then
-- This logic is being moved to ShareGameContainer.lua
-- so it can be shared with this file and ModalShareGamePageFrame.lua
ShareGamePageFrame = RoactRodux.UNSTABLE_connect2(
function(state, props)
return {
deviceLayout = state.DeviceInfo.DeviceLayout,
searchAreaActive = state.ConversationsSearch.SearchAreaActive,
searchText = state.ConversationsSearch.SearchText,
}
end,
function(dispatch)
return {
closePage = function()
dispatch(ClosePage(Constants.PageRoute.SHARE_GAME))
end,
reFetch = function()
local userId = tostring(Players.LocalPlayer.UserId)
local requestImpl = httpRequest(HttpRbxApiService)
dispatch(FetchUserFriends(requestImpl, userId))
end
}
end
)(ShareGamePageFrame)
end
return ShareGamePageFrame
@@ -0,0 +1,147 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local User = require(AppTempCommon.LuaApp.Models.User)
local ThumbnailRequest = require(AppTempCommon.LuaApp.Models.ThumbnailRequest)
local FFlagLuaInviteModalEnabled = settings():GetFFlag("LuaInviteModalEnabledV384")
local DeviceLayout = {
PHONE_PORTRAIT = "PHONE_PORTRAIT",
PHONE_LANDSCAPE = "PHONE_LANDSCAPE",
TABLET_PORTRAIT = "TABLET_PORTRAIT",
TABLET_LANDSCAPE = "TABLET_LANDSCAPE",
DESKTOP = "DESKTOP",
}
local Color = {
WHITE = Color3.fromRGB(255, 255, 255),
GRAY1 = Color3.fromRGB(25, 25, 25),
GRAY2 = Color3.fromRGB(117, 117, 117),
GRAY3 = Color3.fromRGB(184, 184, 184),
GRAY4 = Color3.fromRGB(227, 227, 227),
GRAY5 = Color3.fromRGB(242, 242, 242),
GRAY6 = Color3.fromRGB(245, 245, 245),
RED = Color3.fromRGB(254, 68, 72),
}
local Constants = {
Color = Color,
PresenceColors = {
[User.PresenceType.ONLINE] = Color3.fromRGB(0, 162, 255),
[User.PresenceType.IN_GAME] = Color3.fromRGB(2, 183, 87),
[User.PresenceType.IN_STUDIO] = Color3.fromRGB(246, 136, 2),
[User.PresenceType.OFFLINE] = Color.GRAY3,
},
PresenceTextKey = {
[User.PresenceType.ONLINE] = "InGame.Presence.Label.Online",
[User.PresenceType.IN_GAME] = "InGame.Presence.Label.InGame",
[User.PresenceType.IN_STUDIO] = "InGame.Presence.Label.InStudio",
[User.PresenceType.OFFLINE] = "InGame.Presence.Label.Offline",
},
InviteStatus = {
Success = "Success", -- Should match API resultType
Moderated = "Moderated", -- Should match API resultType
Failed = "Failed",
Pending = "Pending",
},
DeviceOrientation = {
PORTRAIT = "PORTRAIT",
LANDSCAPE = "LANDSCAPE",
INVALID = "INVALID",
},
PageRoute = {
NONE = "NONE",
SETTINGS_HUB = "SETTINGS_HUB",
SHARE_GAME = "SHARE_GAME",
},
AvatarThumbnailTypes = {
AvatarThumbnail = "AvatarThumbnail",
HeadShot = "HeadShot",
},
AvatarThumbnailSizes = {
Size48x48 = "Size48x48",
Size60x60 = "Size60x60",
Size100x100 = "Size100x100",
Size150x150 = "Size150x150",
Size352x352 = "Size352x352",
},
SHARE_GAME_Z_INDEX = 2,
--[[
Used for determining how the ShareGame page will be rendered across
devices.
]]
DeviceLayout = DeviceLayout,
LayoutSpecific = {
[DeviceLayout.PHONE_LANDSCAPE] = {
HEADER_HEIGHT = 40,
PAGE_TITLE_TEXT_SIZE = 23,
SEARCH_BOX_MARGIN = 12,
PAGE_SIDE_MARGINS = 7,
BACK_BUTTON_HEIGHT = 44,
BACK_BUTTON_WIDTH = 44,
BACK_BUTTON_MODAL_WIDTH = FFlagLuaInviteModalEnabled and 44,
EXTEND_BOTTOM_SIZE = 0,
TOAST_HEIGHT = 40,
},
[DeviceLayout.PHONE_PORTRAIT] = {
HEADER_HEIGHT = 40,
PAGE_TITLE_TEXT_SIZE = 23,
SEARCH_BOX_MARGIN = 15,
PAGE_SIDE_MARGINS = 5,
BACK_BUTTON_HEIGHT = 44,
BACK_BUTTON_WIDTH = 44,
BACK_BUTTON_MODAL_WIDTH = FFlagLuaInviteModalEnabled and 44,
EXTEND_BOTTOM_SIZE = 0,
TOAST_HEIGHT = 40,
},
[DeviceLayout.TABLET_PORTRAIT] = {
HEADER_HEIGHT = 40,
PAGE_TITLE_TEXT_SIZE = 23,
SEARCH_BOX_MARGIN = 15,
PAGE_SIDE_MARGINS = 15,
BACK_BUTTON_HEIGHT = 44,
BACK_BUTTON_WIDTH = 44,
BACK_BUTTON_MODAL_WIDTH = FFlagLuaInviteModalEnabled and 44,
EXTEND_BOTTOM_SIZE = 0,
TOAST_HEIGHT = 80,
},
[DeviceLayout.TABLET_LANDSCAPE] = {
HEADER_HEIGHT = 60,
PAGE_TITLE_TEXT_SIZE = 23,
SEARCH_BOX_MARGIN = 15,
PAGE_SIDE_MARGINS = 5,
BACK_BUTTON_HEIGHT = 44,
BACK_BUTTON_WIDTH = 44,
BACK_BUTTON_MODAL_WIDTH = FFlagLuaInviteModalEnabled and 44,
EXTEND_BOTTOM_SIZE = 68,
TOAST_HEIGHT = 80,
},
[DeviceLayout.DESKTOP] = {
HEADER_HEIGHT = 60,
PAGE_TITLE_TEXT_SIZE = 29,
SEARCH_BOX_MARGIN = 0,
PAGE_SIDE_MARGINS = 0,
BACK_BUTTON_HEIGHT = 44,
BACK_BUTTON_WIDTH = 154,
BACK_BUTTON_MODAL_WIDTH = FFlagLuaInviteModalEnabled and 44,
EXTEND_BOTTOM_SIZE = 68,
TOAST_HEIGHT = 80,
},
},
}
Constants.InviteAvatarThumbnailType = Constants.AvatarThumbnailTypes.HeadShot
Constants.InviteAvatarThumbnailSize = Constants.AvatarThumbnailSizes.Size150x150
Constants.ThumbnailRequest = {
InviteToGame = {ThumbnailRequest.fromData(
Constants.InviteAvatarThumbnailType,
Constants.InviteAvatarThumbnailSize
)},
}
return Constants
@@ -0,0 +1,113 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local GuiService = game:GetService("GuiService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local Modules = RobloxGui.Modules
local SettingsHubDirectory = Modules.Settings
local ShareGameDirectory = SettingsHubDirectory.Pages.ShareGame
local FullModalShareGameComponent = require(ShareGameDirectory.Components.FullModalShareGameComponent)
local AppReducer = require(ShareGameDirectory.AppReducer)
local isSelectionGroupEnabled = require(ShareGameDirectory.isSelectionGroupEnabled)
local HIDE_INVITE_CONTEXT_BIND = "hideInvitePrompt"
local InviteToGamePrompt = {}
InviteToGamePrompt.__index = InviteToGamePrompt
function InviteToGamePrompt.new(mountTarget)
local self = {
mountTarget = mountTarget,
isActive = false,
}
setmetatable(self, InviteToGamePrompt)
return self
end
function InviteToGamePrompt:withSocialServiceAndLocalPlayer(socialService, localPlayer)
self.socialService = socialService
self.localPlayer = localPlayer
return self
end
function InviteToGamePrompt:withAnalytics(analytics)
self.analytics = analytics
return self
end
function InviteToGamePrompt:_createTree(isVisible)
return Roact.createElement(FullModalShareGameComponent, {
store = Rodux.Store.new(AppReducer, nil, { Rodux.thunkMiddleware }),
isVisible = isVisible,
analytics = self.analytics,
onAfterClosePage = function(_)
-- * "Why are we no-opting sentToUserIds?"
-- Originally our specs required us to pass the userIds of
-- invited users to our creators through this event.
-- After reviewing and observing how this information could be misused,
-- we have determined we do not want to incentivize inviting friends
-- needlessly this way and have disabled this feature in the meantime.
local sentToUserIds = {}
self:hide(sentToUserIds)
end,
})
end
function InviteToGamePrompt:show()
if self.isActive then
return
end
self.isActive = true
if not self.instance then
self.instance = Roact.mount(self:_createTree(true), self.mountTarget, isSelectionGroupEnabled() and "invitePrompt" or nil)
else
self.instance = Roact.update(self.instance, self:_createTree(true))
end
if self.analytics then
self.analytics:inputShareGameEntryPoint()
end
if isSelectionGroupEnabled() then
ContextActionService:BindCoreAction(HIDE_INVITE_CONTEXT_BIND, function(_, userInputState, inputObject)
if userInputState == Enum.UserInputState.Begin then
self:hide()
end
end, false, Enum.KeyCode.ButtonB, Enum.KeyCode.Backspace)
end
end
function InviteToGamePrompt:hide(sentToUserIds)
if not self.isActive then
return
end
self.isActive = false
self.instance = Roact.update(self.instance, self:_createTree(false))
if self.socialService and self.localPlayer then
self.socialService:InvokeGameInvitePromptClosed(self.localPlayer, {})
end
if isSelectionGroupEnabled() then
ContextActionService:UnbindCoreAction(HIDE_INVITE_CONTEXT_BIND)
GuiService.SelectedCoreObject = nil
GuiService:RemoveSelectionGroup("invitePrompt")
end
end
function InviteToGamePrompt:destruct()
if self.instance then
Roact.unmount(self.instance)
end
end
return InviteToGamePrompt
@@ -0,0 +1,194 @@
return function()
local InviteToGamePrompt = require(script.Parent.InviteToGamePrompt)
describe("new", function()
it("should return a new prompt", function()
local prompt = InviteToGamePrompt.new()
expect(prompt).to.be.ok()
expect(prompt.show).to.be.ok()
expect(prompt.hide).to.be.ok()
expect(prompt.isActive).to.equal(false)
end)
it("should accept mountTarget as a parameter", function()
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
expect(prompt).to.be.ok()
expect(prompt.show).to.be.ok()
expect(prompt.hide).to.be.ok()
expect(prompt.isActive).to.equal(false)
expect(prompt.mountTarget).to.be.ok()
expect(prompt.mountTarget).to.equal(folder)
folder:Destroy()
end)
end)
describe("withSocialServiceAndLocalPlayer", function()
it("should accept passed socialService", function()
local mockSocialService = {}
local prompt = InviteToGamePrompt.new()
local promptWithSocial = prompt:withSocialServiceAndLocalPlayer(mockSocialService)
expect(promptWithSocial).to.be.ok()
expect(promptWithSocial.socialService).to.equal(mockSocialService)
expect(promptWithSocial).to.equal(prompt)
end)
it("should accept passed localPlayer", function()
local mockLocalPlayer = {}
local prompt = InviteToGamePrompt.new()
local promptWithPlayer = prompt:withSocialServiceAndLocalPlayer(nil, mockLocalPlayer)
expect(promptWithPlayer).to.be.ok()
expect(promptWithPlayer.localPlayer).to.equal(mockLocalPlayer)
expect(promptWithPlayer).to.equal(prompt)
end)
end)
describe("withAnalytics", function()
it("should accept passed analytics", function()
local mockAnalytics = {}
local prompt = InviteToGamePrompt.new()
local promptWithAnalytics = prompt:withAnalytics(mockAnalytics)
expect(promptWithAnalytics).to.be.ok()
expect(promptWithAnalytics.analytics).to.equal(mockAnalytics)
expect(promptWithAnalytics).to.equal(prompt)
end)
end)
describe("show", function()
it("should create an instance on the first show", function()
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
expect(prompt.instance).to.never.be.ok()
prompt:show()
expect(prompt.instance).to.be.ok()
InviteToGamePrompt:destruct()
folder:Destroy()
end)
it("should make the prompt visible", function()
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
prompt:show()
local screenGui = folder:FindFirstChildOfClass("ScreenGui", true)
expect(screenGui).to.be.ok()
expect(screenGui.Enabled).to.equal(true)
InviteToGamePrompt:destruct()
folder:Destroy()
end)
it("should do nothing if prompt is already visible", function()
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
prompt:show()
prompt:show()
local screenGui = folder:FindFirstChildOfClass("ScreenGui", true)
expect(screenGui).to.be.ok()
expect(screenGui.Enabled).to.equal(true)
InviteToGamePrompt:destruct()
folder:Destroy()
end)
end)
describe("hide", function()
it("should do nothing if prompt is already hidden", function()
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
prompt:hide()
prompt:hide()
expect(prompt.instance).to.never.be.ok()
InviteToGamePrompt:destruct()
folder:Destroy()
end)
end)
it("should hide the active prompt if it was shown", function()
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
prompt:show()
prompt:hide()
expect(prompt.instance).to.be.ok()
local screenGui = folder:FindFirstChildOfClass("ScreenGui", true)
expect(screenGui).to.be.ok()
expect(screenGui.Enabled).to.equal(false)
InviteToGamePrompt:destruct()
folder:Destroy()
end)
it("should invoke socialService's InvokeGameInvitePromptClosed after shown", function()
local lastSentLocalPlayer
local lastSentUserIds
local mockSocialService = {
InvokeGameInvitePromptClosed = function(self, localPlayer, sentUserIds)
lastSentLocalPlayer = localPlayer
lastSentUserIds = sentUserIds
end,
}
local mockLocalPlayer = {}
local mockSentUserIds = {}
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
:withSocialServiceAndLocalPlayer(mockSocialService, mockLocalPlayer)
prompt:show()
prompt:hide(mockSentUserIds)
expect(lastSentLocalPlayer).to.equal(mockLocalPlayer)
-- lastSentUserIds should always be an empty array
expect(#lastSentUserIds).to.equal(0)
InviteToGamePrompt:destruct()
folder:Destroy()
end)
it("should not invoke socialService's InvokeGameInvitePromptClosed if never shown", function()
local lastSentLocalPlayer
local lastSentUserIds
local mockSocialService = {
InvokeGameInvitePromptClosed = function(self, localPlayer, sentUserIds)
lastSentLocalPlayer = localPlayer
lastSentUserIds = sentUserIds
end,
}
local mockLocalPlayer = {}
local mockSentUserIds = {}
local folder = Instance.new("Folder")
local prompt = InviteToGamePrompt.new(folder)
:withSocialServiceAndLocalPlayer(mockSocialService, mockLocalPlayer)
-- intentionally do not show
prompt:hide(mockSentUserIds)
expect(lastSentLocalPlayer).to.equal(nil)
expect(lastSentUserIds).to.equal(nil)
InviteToGamePrompt:destruct()
folder:Destroy()
end)
end
@@ -0,0 +1,31 @@
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = CoreGui.RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local Immutable = require(AppTempCommon.Common.Immutable)
local ClosePage = require(ShareGame.Actions.ClosePage)
local SetSearchAreaActive = require(ShareGame.Actions.SetSearchAreaActive)
local SetSearchText = require(ShareGame.Actions.SetSearchText)
local DEFAULT_STATE = {
SearchAreaActive = false,
SearchText = "",
}
return function(state, action)
state = state or DEFAULT_STATE
if action.type == SetSearchAreaActive.name then
state = Immutable.Set(state, "SearchAreaActive", action.isActive)
elseif action.type == SetSearchText.name then
state = Immutable.Set(state, "SearchText", action.searchText)
elseif action.type == ClosePage.name then
state = DEFAULT_STATE
end
return state
end
@@ -0,0 +1,30 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local Immutable = require(AppTempCommon.Common.Immutable)
local Constants = require(ShareGame.Constants)
local SetDeviceLayout = require(ShareGame.Actions.SetDeviceLayout)
local SetDeviceOrientation = require(AppTempCommon.LuaApp.Actions.SetDeviceOrientation)
local SetIsSmallTouchScreen = require(ShareGame.Actions.SetIsSmallTouchScreen)
return function(state, action)
state = state or {
DeviceLayout = Constants.DeviceLayout.DESKTOP,
DeviceOrientation = Constants.DeviceOrientation.INVALID,
IsSmallTouchScreen = false,
}
if action.type == SetDeviceOrientation.name then
state = Immutable.Set(state, "DeviceOrientation", action.deviceOrientation)
elseif action.type == SetDeviceLayout.name then
state = Immutable.Set(state, "DeviceLayout", action.deviceLayout)
elseif action.type == SetIsSmallTouchScreen.name then
state = Immutable.Set(state, "IsSmallTouchScreen", action.isSmallTouchScreen)
end
return state
end
@@ -0,0 +1,18 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local Immutable = require(AppTempCommon.Common.Immutable)
local ReceivedUserInviteStatus = require(ShareGame.Actions.ReceivedUserInviteStatus)
return function(state, action)
state = state or {}
if action.type == ReceivedUserInviteStatus.name then
state = Immutable.Set(state, action.userId, action.inviteStatus)
end
return state
end
@@ -0,0 +1,31 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local Immutable = require(AppTempCommon.Common.Immutable)
local Constants = require(ShareGame.Constants)
local OpenPage = require(ShareGame.Actions.OpenPage)
local ClosePage = require(ShareGame.Actions.ClosePage)
return function(state, action)
state = state or {
IsOpen = false,
Route = Constants.PageRoute.NONE,
}
if action.type == OpenPage.name then
state = Immutable.JoinDictionaries(state, {
IsOpen = true,
Route = action.route or Constants.PageRoute.NONE,
})
elseif action.type == ClosePage.name then
state = Immutable.JoinDictionaries(state, {
IsOpen = false,
Route = action.route or Constants.PageRoute.NONE,
})
end
return state
end
@@ -0,0 +1,44 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShareGame = Modules.Settings.Pages.ShareGame
local Immutable = require(AppTempCommon.Common.Immutable)
local ReceivedUserInviteStatus = require(ShareGame.Actions.ReceivedUserInviteStatus)
local StoppedToastTimer = require(ShareGame.Actions.StoppedToastTimer)
local Constants = require(ShareGame.Constants)
local InviteStatus = Constants.InviteStatus
return function(state, action)
state = state or {
failedInvites = {},
}
if action.type == ReceivedUserInviteStatus.name then
local inviteStatus = action.inviteStatus
if inviteStatus == InviteStatus.Moderated
or inviteStatus == InviteStatus.Failed then
local inviteStatusModel = {
timeStamp = tick(),
userId = action.userId,
status = inviteStatus,
}
state = Immutable.JoinDictionaries(state, {
failedInvites = Immutable.JoinDictionaries(
state.failedInvites, Immutable.Append(state.failedInvites, inviteStatusModel)
),
})
end
elseif action.type == StoppedToastTimer.name then
state = Immutable.JoinDictionaries(state, {
failedInvites = {},
})
end
return state
end
@@ -0,0 +1,42 @@
local IMAGE_PATH = "rbxasset://textures/ui/Settings/ShareGame/icons.png"
local function createFrameModel(offset, size)
return {
offset = offset,
size = size,
}
end
local SHEET_MODEL = {
frames = {
back = createFrameModel(Vector2.new(2, 19), Vector2.new(24, 24)),
clear = createFrameModel(Vector2.new(6, 51), Vector2.new(16, 16)),
invite = createFrameModel(Vector2.new(2, 75), Vector2.new(24, 24)),
search_border = createFrameModel(Vector2.new(11, 1), Vector2.new(7, 7)),
search_large = createFrameModel(Vector2.new(3, 132), Vector2.new(22, 22)),
search_small = createFrameModel(Vector2.new(6, 106), Vector2.new(16, 16)),
friends = createFrameModel(Vector2.new(0, 159), Vector2.new(72, 72)),
cross = createFrameModel(Vector2.new(4, 231), Vector2.new(24, 24)),
modal_border = createFrameModel(Vector2.new(0, 255), Vector2.new(7, 7)),
},
}
local ShareGameIcons = {}
function ShareGameIcons:GetFrame(key)
return SHEET_MODEL.frames[key]
end
function ShareGameIcons:GetImagePath()
return IMAGE_PATH
end
function ShareGameIcons:ApplyImage(guiObject, key)
local frameModel = self:GetFrame(key)
guiObject.Image = IMAGE_PATH
guiObject.ImageRectOffset = frameModel.offset
guiObject.ImageRectSize = frameModel.size
end
return ShareGameIcons
@@ -0,0 +1,77 @@
local CorePackages = game:GetService("CorePackages")
local RobloxGui = game:GetService("CoreGui").RobloxGui
local ShareGame = game:GetService("CoreGui").RobloxGui.Modules.Settings.Pages.ShareGame
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local ApiFetchUsersPresences = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersPresences)
local ApiFetchUsersThumbnail = require(ShareGame.Thunks.ApiFetchUsersThumbnail)
local UsersGetFriends = require(Requests.UsersGetFriends)
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User)
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
local UsePlayerDisplayName = require(RobloxGui.Modules.Settings.UsePlayerDisplayName)
return function(requestImpl, userId, thumbnailRequest, checkPoints)
return function(store)
store:dispatch(FetchUserFriendsStarted(userId))
if checkPoints ~= nil and checkPoints.startFetchUserFriends ~= nil then
checkPoints:startFetchUserFriends()
end
return UsersGetFriends(requestImpl, userId):andThen(function(response)
local responseBody = response.responseBody
local userIds = {}
local newUsers = {}
for _, userData in pairs(responseBody.data) do
local id = tostring(userData.id)
local newUser
if UsePlayerDisplayName() then
userData.isFriend = true
newUser = UserModel.fromDataTable(userData)
else
newUser = UserModel.fromData(id, userData.name, true)
end
table.insert(userIds, id)
newUsers[newUser.id] = newUser
end
store:dispatch(UpdateUsers(newUsers))
if checkPoints ~= nil and checkPoints.finishFetchUserFriends ~= nil then
checkPoints:finishFetchUserFriends()
end
return userIds
end):andThen(function(userIds)
if checkPoints ~= nil and checkPoints.startFetchUsersPresences ~= nil then
checkPoints:startFetchUsersPresences()
end
-- Asynchronously fetch friend thumbnails so we don't block display of UI
store:dispatch(ApiFetchUsersThumbnail(requestImpl, userIds, thumbnailRequest))
return store:dispatch(ApiFetchUsersPresences(requestImpl, userIds))
end):andThen(
function(result)
store:dispatch(FetchUserFriendsCompleted(userId))
if checkPoints ~= nil and checkPoints.finishFetchUsersPresences ~= nil then
checkPoints:finishFetchUsersPresences()
end
return Promise.resolve(result)
end,
function(response)
store:dispatch(FetchUserFriendsFailed(userId, response))
return Promise.reject(response)
end
)
end
end
@@ -0,0 +1,41 @@
local CorePackages = game:GetService("CorePackages")
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
local UsersGetThumbnail = require(Requests.UsersGetThumbnail)
local SetUserThumbnail = require(Actions.SetUserThumbnail)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local function fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest)
local fetchedPromises = {}
for _, userId in pairs(userIds) do
table.insert(fetchedPromises,
UsersGetThumbnail(userId, thumbnailRequest.thumbnailType, thumbnailRequest.thumbnailSize)
)
end
return Promise.all(fetchedPromises)
end
return function(networkImpl, userIds, thumbnailRequests)
return function(store)
-- We currently cannot batch request user avatar thumbnails,
-- so each thumbnailRequest has to be processed individually.
local fetchedPromises = {}
for _, thumbnailRequest in pairs(thumbnailRequests) do
table.insert(fetchedPromises,
fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest):andThen(function(result)
for _, data in pairs(result) do
store:dispatch(SetUserThumbnail(data.id, data.image, data.thumbnailType, data.thumbnailSize))
end
end)
)
end
return Promise.all(fetchedPromises)
end
end
@@ -0,0 +1,22 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local RetrievalStatus = require(AppTempCommon.LuaApp.Enum.RetrievalStatus)
local ApiFetchUsersFriends = require(ShareGame.Thunks.ApiFetchUsersFriends)
local Constants = require(ShareGame.Constants)
return function(requestImpl, userId)
return function(store)
local state = store:getState()
local friendsRetrievalStatus = state.Friends.retrievalStatus[userId]
if friendsRetrievalStatus ~= RetrievalStatus.Fetching then
store:dispatch(ApiFetchUsersFriends(requestImpl, userId, Constants.ThumbnailRequest.InviteToGame))
end
end
end
@@ -0,0 +1,70 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local ApiSendGameInvite = require(AppTempCommon.LuaApp.Thunks.ApiSendGameInvite)
local ApiFetchPlaceInfos = require(AppTempCommon.LuaApp.Thunks.ApiFetchPlaceInfos)
local Promise = require(AppTempCommon.LuaApp.Promise)
local ReceivedUserInviteStatus = require(ShareGame.Actions.ReceivedUserInviteStatus)
local Constants = require(ShareGame.Constants)
local InviteStatus = Constants.InviteStatus
local FFlagLuaInviteFailOnZeroPlaceId = settings():GetFFlag("LuaInviteFailOnZeroPlaceIdV384")
local EMPTY_PLACE_ID = "0"
return function(requestImpl, userId, placeId)
return function(store)
if FFlagLuaInviteFailOnZeroPlaceId then
if placeId == EMPTY_PLACE_ID then
warn("Game Invite failed to send. Cannot send invite to unpublished Place.")
store:dispatch(ReceivedUserInviteStatus(userId, InviteStatus.Failed))
return Promise.reject()
end
end
local latestState = store:getState()
return Promise.new(function(resolve, reject)
-- Check that we haven't already invited this user
if latestState.Invites[tostring(userId)] == InviteStatus.Pending then
reject()
else
resolve()
end
end):andThen(function()
local maybePlaceInfo = latestState.PlaceInfos[placeId]
return Promise.new(function(resolve, reject)
-- Log that we've tried inviting this user
store:dispatch(ReceivedUserInviteStatus(userId, InviteStatus.Pending))
if maybePlaceInfo then
resolve(maybePlaceInfo)
else
store:dispatch(ApiFetchPlaceInfos(requestImpl, {placeId})):andThen(function(placeInfos)
if placeInfos[1] ~= nil then
resolve(placeInfos[1])
else
reject()
end
end, function()
reject()
end)
end
end):andThen(function(placeInfo)
return store:dispatch(ApiSendGameInvite(requestImpl, userId, placeInfo))
end):andThen(function(results)
store:dispatch(ReceivedUserInviteStatus(userId, results.resultType))
return results
end, function()
store:dispatch(ReceivedUserInviteStatus(userId, InviteStatus.Failed))
end)
end)
end
end
@@ -0,0 +1,15 @@
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local mockTranslator = require(script.Parent.mockTranslator)
return function()
local coreScriptLocalization = CoreGui:FindFirstChild("CoreScriptLocalization")
if coreScriptLocalization then
return RobloxTranslator
else
return mockTranslator
end
end
@@ -0,0 +1,7 @@
return function()
local getTranslator = require(script.Parent.getTranslator)
it("should return a valid Localization mock in unit tests", function()
expect(getTranslator()).to.be.ok()
expect(getTranslator().FormatByKey).to.be.ok()
end)
end
@@ -0,0 +1,5 @@
game:DefineFastFlag("LuaInviteSelectionGroupEnabled", false)
return function()
return game:GetFastFlag("LuaInviteSelectionGroupEnabled")
end
@@ -0,0 +1,7 @@
local mockTranslator = {}
function mockTranslator:FormatByKey(key, args)
return tostring(key)
end
return mockTranslator
@@ -0,0 +1,23 @@
return function()
local mockTranslator = require(script.Parent.mockTranslator)
it("should have FormatByKey function", function()
expect(mockTranslator.FormatByKey).to.be.ok()
expect(type(mockTranslator.FormatByKey)).to.equal("function")
end)
describe("FormatByKey", function()
it("should return a string value", function()
local string = mockTranslator:FormatByKey("testing")
expect(string).to.be.ok()
expect(type(string)).to.equal("string")
local string2 = mockTranslator:FormatByKey()
expect(string2).to.be.ok()
expect(type(string2)).to.equal("string")
end)
end)
end
@@ -0,0 +1,64 @@
--[[
Placeholder page for SettingsHub that manages top level Roact view
This page is a container and controller for the Roact app
--]]
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local ShareGame = RobloxGui.Modules.Settings.Pages.ShareGame
local Constants = require(ShareGame.Constants)
local OpenPage = require(ShareGame.Actions.OpenPage)
local ClosePage = require(ShareGame.Actions.ClosePage)
local settingsPageFactory = require(RobloxGui.Modules.Settings.SettingsPageFactory)
local this = settingsPageFactory:CreateNewPage()
this.TabHeader = nil -- no tab for this page
this.PageListLayout.Parent = nil -- no list layout for this page
this.ShouldShowBottomBar = false
this.ShouldShowHubBar = false
this.IsPageClipped = false
this.Page.Name = "ShareGameDummy"
this.Page.Size = UDim2.new(1, 0, 0, 0)
function this:ConnectHubToApp(settingsHub, shareGameApp)
this:SetHub(settingsHub)
shareGameApp.store.changed:connect(function(state, prevState)
local page = state.Page
local wasOpen = prevState.Page.IsOpen
-- Check if the user closed the page via the Roact app.
if page.Route == Constants.PageRoute.SHARE_GAME and (wasOpen and not page.IsOpen) then
-- Close the page to sync up Settings Hub with the state change
this.HubRef:PopMenu(nil, true)
end
end)
this.Displayed.Event:Connect(function()
local state = shareGameApp.store:getState()
if not state.Page.IsOpen then
-- Tell Roact app that the page was opened via Settings Hub
shareGameApp.store:dispatch(OpenPage(Constants.PageRoute.SETTINGS_HUB))
end
end)
this.Hidden.Event:Connect(function()
-- The user closed the page via the Settings Hub (instead of
-- pressing back on the page), so we have to sync the app state up
-- with the Settings Hub action.
local state = shareGameApp.store:getState()
if state.Page.IsOpen then
shareGameApp.store:dispatch(ClosePage(Constants.PageRoute.SETTINGS_HUB))
end
end)
shareGameApp.store:dispatch(ClosePage(Constants.PageRoute.SETTINGS_HUB))
end
return this
@@ -0,0 +1,28 @@
local CorePackages = game:GetService("CorePackages")
local Settings = script.Parent.Parent
local Actions = Settings.Actions
--TODO: Currently Under Migration to CorePackages
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
--
local SetRecentlyPlayedGames = require(Actions.SetRecentlyPlayedGames)
local Game = require(Settings.Models.Game)
return function(state, action)
state = state or {}
if action.type == SetRecentlyPlayedGames.name then
local gameSortState = state.gameSort or {}
local gameSortData = action.gameSortData
local gameSort = {}
if gameSortData then
for i in ipairs(gameSortData) do
local gameData = Game.fromGameCache(gameSortData[i])
table.insert(gameSort, gameData)
end
end
state = {
gameSort = Immutable.JoinDictionaries(gameSortState, gameSort),
}
end
return state
end
@@ -0,0 +1,13 @@
local Settings = script.Parent.Parent
local Actions = Settings.Actions
local SetRecentlyPlayedGamesFetching = require(Actions.SetRecentlyPlayedGamesFetching)
return function(state, action)
state = state or {}
if action.type == SetRecentlyPlayedGamesFetching.name then
state = {
fetching = action.fetching,
}
end
return state
end
@@ -0,0 +1,12 @@
local Reducers = script.Parent
local RecentlyPlayedGamesFetchingStatus = require(Reducers.RecentlyPlayedGamesFetchingStatus)
local RecentlyPlayedGames = require(Reducers.RecentlyPlayedGames)
return function(state, action)
state = state or {}
return {
RecentlyPlayedGamesFetchingStatus = RecentlyPlayedGamesFetchingStatus(state.RecentlyPlayedGamesFetchingStatus, action),
RecentlyPlayedGames = RecentlyPlayedGames(state.RecentlyPlayedGames, action),
}
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,365 @@
--!nocheck
--[[
Filename: SettingsPageFactory.lua
Written by: jeditkacheff
Version 1.0
Description: Base Page Functionality for all Settings Pages
--]]
----------------- SERVICES ------------------------------
local GuiService = game:GetService("GuiService")
local HttpService = game:GetService("HttpService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
----------- UTILITIES --------------
local utility = require(RobloxGui.Modules.Settings.Utility)
local StyleWidgets = require(RobloxGui.Modules.StyleWidgets)
----------- VARIABLES --------------
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
----------- CLASS DECLARATION --------------
local function Initialize()
local this = {}
this.HubRef = nil
this.LastSelectedObject = nil
this.TabPosition = 0
this.Active = false
this.OpenStateChangedCount = 0
this.ShouldShowBottomBar = true
this.ShouldShowHubBar = true
this.IsPageClipped = true
local rows = {}
local displayed = false
------ TAB CREATION -------
this.TabHeader = utility:Create'TextButton'
{
Name = "Header",
Text = "",
BackgroundTransparency = 1,
Size = UDim2.new(1/5, 0,1,0),
Position = UDim2.new(0,0,0,0)
};
if utility:IsSmallTouchScreen() then
this.TabHeader.Size = UDim2.new(0,84,1,0)
elseif isTenFootInterface then
this.TabHeader.Size = UDim2.new(0,220,1,0)
end
this.TabHeader.MouseButton1Click:connect(function()
if this.HubRef then
this.HubRef:SwitchToPage(this, true)
end
end)
local icon = utility:Create'ImageLabel'
{
Name = "Icon",
BackgroundTransparency = 1,
Size = UDim2.new(0.75, 0, 0.75, 0),
Position = UDim2.new(0,10,0.5,-18),
Image = "",
ImageTransparency = 0.5,
Parent = this.TabHeader
};
local _iconAspectRatio = utility:Create'UIAspectRatioConstraint'
{
Name = "AspectRatioConstraint",
AspectRatio = 1,
Parent = icon
};
local title = utility:Create'TextLabel'
{
Name = "Title",
Text = "",
Font = Enum.Font.SourceSansBold,
FontSize = Enum.FontSize.Size24,
TextColor3 = Color3.new(1,1,1),
BackgroundTransparency = 1,
Size = UDim2.new(1.05,0,1,0), --overwritten
Position = UDim2.new(1.2,0,0,0), --overwritten
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = 0.5
};
local titleTextSizeConstraint = Instance.new("UITextSizeConstraint")
titleTextSizeConstraint.MaxTextSize = 24
if FFlagUseNotificationsLocalization then
title.Parent = this.TabHeader
title.TextScaled = true
title.TextWrapped = true
titleTextSizeConstraint.Parent = title
else
title.Parent = icon
end
if utility:IsSmallTouchScreen() then
title.FontSize = Enum.FontSize.Size18
titleTextSizeConstraint.MaxTextSize = 18
elseif isTenFootInterface then
title.FontSize = Enum.FontSize.Size48
titleTextSizeConstraint.MaxTextSize = 48
end
local _tabSelection = StyleWidgets.MakeTabSelectionWidget(this.TabHeader)
local titleScaleInitial = Vector2.new(title.Size.X.Scale, title.Size.Y.Scale)
local function onResized()
if not this.TabHeader then
return
end
if utility:IsSmallTouchScreen() then
this.TabHeader.Icon.Size = UDim2.new(0,34,0,28)
this.TabHeader.Icon.Position = UDim2.new(this.TabHeader.Icon.Position.X.Scale,this.TabHeader.Icon.Position.X.Offset,0.5,-14)
this.TabHeader.Icon.AnchorPoint = Vector2.new(0, 0)
elseif isTenFootInterface then
this.TabHeader.Icon.Size = UDim2.new(0,88,0,74)
this.TabHeader.Icon.Position = UDim2.new(0,0,0.5,0)
this.TabHeader.Icon.AnchorPoint = Vector2.new(0, 0.5)
else
this.TabHeader.Icon.Size = UDim2.new(0,44,0,37)
this.TabHeader.Icon.Position = UDim2.new(0,15,0.5,-18)
this.TabHeader.Icon.AnchorPoint = Vector2.new(0, 0)
end
local isPortrait = utility:IsPortrait()
if isPortrait then
this.TabHeader.Icon.Position = UDim2.new(0.5, 0, 0.5, 0)
this.TabHeader.Icon.AnchorPoint = Vector2.new(0.5, 0.5)
this.TabHeader.Icon.Size = UDim2.new(0.5, 0, 0.5, 0)
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Visible = false
else
this.TabHeader.Icon.Title.Visible = false
end
else
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.Visible = true
else
this.TabHeader.Icon.Title.Visible = true
end
end
if FFlagUseNotificationsLocalization then
local iconSize = this.TabHeader.Icon.AbsoluteSize
local paddingLeft = 0.125
local paddingRight = 0.025
title.Position = UDim2.new(
paddingLeft,
iconSize.X,
0.225,
0
)
title.Size = UDim2.new(
titleScaleInitial.X - paddingLeft - paddingRight,
-iconSize.X,
0.5,
0
)
end
end --end local function onResized()
utility:OnResized(this.TabHeader, onResized)
------ PAGE CREATION -------
this.Page = utility:Create'Frame'
{
Name = "Page",
BackgroundTransparency = 1,
Size = UDim2.new(1,0,1,0)
};
this.PageListLayout = utility:Create'UIListLayout'
{
Name = "RowListLayout",
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Top,
Padding = UDim.new(0, 3),
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = this.Page
};
-- make sure each page has a unique selection group (for gamepad selection)
GuiService:AddSelectionParent(HttpService:GenerateGUID(false), this.Page)
----------------- Events ------------------------
this.Displayed = Instance.new("BindableEvent")
this.Displayed.Name = "Displayed"
this.Displayed.Event:connect(function()
if not this.HubRef.Shield.Visible then return end
this:SelectARow()
end)
this.Hidden = Instance.new("BindableEvent")
this.Hidden.Event:connect(function()
if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject:IsDescendantOf(this.Page) then
GuiService.SelectedCoreObject = nil
end
end)
this.Hidden.Name = "Hidden"
----------------- FUNCTIONS ------------------------
function this:SelectARow(forced) -- Selects the first row or the most recently selected row
if forced or not GuiService.SelectedCoreObject or not GuiService.SelectedCoreObject:IsDescendantOf(this.Page) then
if this.LastSelectedObject then
GuiService.SelectedCoreObject = this.LastSelectedObject
else
if rows and #rows > 0 then
local valueChangerFrame = nil
if type(rows[1].ValueChanger) ~= "table" then
valueChangerFrame = rows[1].ValueChanger
else
valueChangerFrame = rows[1].ValueChanger.SliderFrame and
rows[1].ValueChanger.SliderFrame or rows[1].ValueChanger.SelectorFrame
end
GuiService.SelectedCoreObject = valueChangerFrame
end
end
end
end
function this:Display(pageParent, skipAnimation)
this.OpenStateChangedCount = this.OpenStateChangedCount + 1
if this.TabHeader then
this.TabHeader.TabSelection.Visible = true
this.TabHeader.Icon.ImageTransparency = 0
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.TextTransparency = 0
else
this.TabHeader.Icon.Title.TextTransparency = 0
end
end
this.Page.Parent = pageParent
this.Page.Visible = true
local endPos = UDim2.new(0,0,0,0)
local animationComplete = function()
this.Page.Visible = true
displayed = true
this.Displayed:Fire()
end
if skipAnimation then
this.Page.Position = endPos
animationComplete()
else
this.Page:TweenPosition(endPos, Enum.EasingDirection.In, Enum.EasingStyle.Quad, 0.1, true, animationComplete)
end
end
function this:Hide(direction, newPagePos, skipAnimation, delayBeforeHiding)
this.OpenStateChangedCount = this.OpenStateChangedCount + 1
if this.TabHeader then
this.TabHeader.TabSelection.Visible = false
this.TabHeader.Icon.ImageTransparency = 0.5
if FFlagUseNotificationsLocalization then
this.TabHeader.Title.TextTransparency = 0.5
else
this.TabHeader.Icon.Title.TextTransparency = 0.5
end
end
if this.Page.Parent then
local endPos = UDim2.new(1 * direction,0,0,0)
local animationComplete = function()
this.Page.Visible = false
this.Page.Position = UDim2.new(this.TabPosition - newPagePos,0,0,0)
displayed = false
this.Hidden:Fire()
end
local remove = function()
if skipAnimation then
this.Page.Position = endPos
animationComplete()
else
this.Page:TweenPosition(endPos, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.1, true, animationComplete)
end
end
if delayBeforeHiding then
local myOpenStateChangedCount = this.OpenStateChangedCount
delay(delayBeforeHiding, function()
if myOpenStateChangedCount == this.OpenStateChangedCount then
remove()
end
end)
else
remove()
end
end
end
function this:GetDisplayed()
return displayed
end
function this:GetVisibility()
return this.Page.Parent
end
function this:GetTabHeader()
return this.TabHeader
end
function this:SetHub(hubRef)
this.HubRef = hubRef
for i, row in next, rows do
if type(row.ValueChanger) == 'table' then
row.ValueChanger.HubRef = this.HubRef
end
end
end
function this:GetSize()
return this.Page.AbsoluteSize
end
function this:AddRow(RowFrame, RowLabel, ValueChangerInstance, ExtraRowSpacing)
rows[#rows + 1] = {SelectionFrame = RowFrame, Label = RowLabel, ValueChanger = ValueChangerInstance}
local rowFrameYSize = 0
if RowFrame then
rowFrameYSize = RowFrame.Size.Y.Offset
end
if ExtraRowSpacing then
this.Page.Size = UDim2.new(1, 0, 0, this.Page.Size.Y.Offset + rowFrameYSize + ExtraRowSpacing)
else
this.Page.Size = UDim2.new(1, 0, 0, this.Page.Size.Y.Offset + rowFrameYSize)
end
if this.HubRef and type(ValueChangerInstance) == 'table' then
ValueChangerInstance.HubRef = this.HubRef
end
end
return this
end
-------- public facing API ----------------
local moduleApiTable = {}
function moduleApiTable:CreateNewPage()
return Initialize()
end
return moduleApiTable
@@ -0,0 +1,16 @@
local CorePackages = game:GetService("CorePackages")
local Settings = script.Parent
local SettingsReducer = require(Settings.Reducers.SettingsReducer)
local Rodux = require(CorePackages.Rodux)
local Store = Rodux.Store
local SettingsState = {}
function SettingsState:Destruct()
self.store:destruct()
end
SettingsState.store = Store.new(SettingsReducer, nil, {Rodux.thunkMiddleware})
return SettingsState
@@ -0,0 +1,31 @@
local CorePackages = game:GetService("CorePackages")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local ShareGame = Modules.Settings.Pages.ShareGame
local App = require(ShareGame.Components.App)
local AppReducer = require(ShareGame.AppReducer)
local ShareGameMaster = {}
function ShareGameMaster.createApp(parentGui, analytics)
local self = {}
self.store = Rodux.Store.new(AppReducer, nil, { Rodux.thunkMiddleware })
self._instanceHandle = Roact.mount(
Roact.createElement(RoactRodux.StoreProvider, {
store = self.store
}, {
Roact.createElement(App, {
analytics = analytics,
pageTarget = parentGui,
})
})
)
return self
end
return ShareGameMaster
@@ -0,0 +1,83 @@
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local Actions = script.Parent.Parent.Actions
local SetRecentlyPlayedGamesFetching = require(Actions.SetRecentlyPlayedGamesFetching)
local SetRecentlyPlayedGames = require(Actions.SetRecentlyPlayedGames)
local RETRIES = 6
local RECENT_SORT_KEY = "-2"
local FEATURED_SORT_KEY = "8"
local function fetchRecentlyPlayedGamesAsync(store, numOfGames)
local success = false
local tryCount = 1
-- This retry logic is here for those cases where the appshell data model failed to get the recent sort and is retrying.
while tryCount <= RETRIES do
local sharedData = PlatformService:ReadSharedData() or {}
local gameCache = sharedData.games
if sharedData.sorts ~= nil and gameCache ~= nil then
local gamesIncluded = {}
local count = 0
local recentSortData = {}
if sharedData.sorts[RECENT_SORT_KEY] ~= nil and sharedData.sorts[RECENT_SORT_KEY].orderedSort ~= nil then
local recentSort = sharedData.sorts[RECENT_SORT_KEY].orderedSort
for i in ipairs(recentSort) do
if recentSort[i].HasData == false or count == numOfGames then
break
end
--Check if there are cached game data available.
local gameData = sharedData.games[tostring(recentSort[i].PlaceId)]
if gameData then
gamesIncluded[recentSort[i].PlaceId] = true
table.insert(recentSortData, gameData)
count = count + 1
end
end
end
if count < numOfGames then
-- Add recommended games to fill up the remining games.
if sharedData.sorts[FEATURED_SORT_KEY] ~= nil and sharedData.sorts[FEATURED_SORT_KEY].orderedSort ~= nil then
local backupSort = sharedData.sorts[FEATURED_SORT_KEY].orderedSort
for i in ipairs(backupSort) do
if backupSort[i].HasData == false or count == numOfGames then
break
end
local gameData = sharedData.games[tostring(backupSort[i].PlaceId)]
if gameData and gamesIncluded[backupSort[i].PlaceId] ~= true then
gamesIncluded[backupSort[i].PlaceId] = true
table.insert(recentSortData, gameData)
count = count + 1
end
end
end
end
if count >= numOfGames then
store:dispatch(SetRecentlyPlayedGames(recentSortData))
store:dispatch(SetRecentlyPlayedGamesFetching(false))
success = true
break
end
end
tryCount = tryCount + 1
wait(tryCount ^ 2)
end
if success == false then
store:dispatch(SetRecentlyPlayedGames(nil))
end
end
return function(numOfGames, forceUpdate)
return function(store)
local state = store:getState()
local isFetching = state.RecentlyPlayedGamesFetchingStatus.fetching
if isFetching then
return
elseif isFetching ~= nil and not forceUpdate then
return
end
store:dispatch(SetRecentlyPlayedGamesFetching(true))
spawn(function()
fetchRecentlyPlayedGamesAsync(store, numOfGames)
end)
end
end
@@ -0,0 +1,21 @@
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
local FFlagOldInGameMenuDisplayNameForAll = game:DefineFastFlag("OldInGameMenuDisplayNameForAll3", false)
local FFlagOldInGameMenuDisplayNamePolicy = game:DefineFastFlag("OldInGameMenuDisplayNamePolicy3", false)
local function UsePlayerDisplayName()
if FFlagOldInGameMenuDisplayNameForAll then
return true
end
if FFlagOldInGameMenuDisplayNamePolicy then
return PolicyService:IsSubjectToChinaPolicies()
end
return false
end
return UsePlayerDisplayName
File diff suppressed because it is too large Load Diff