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,5 @@
{
"lint": {
"LocalShadow": "fatal"
}
}
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(user)
return {
user = user
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(users)
return {
users = users
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, response)
return {
userId = userId,
response = response
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(placesInfos)
return {
placesInfos = placesInfos,
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId,
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(deviceOrientation)
return {
deviceOrientation = deviceOrientation,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(count)
return {
count = count,
}
end)
@@ -0,0 +1,14 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
local ArgCheck = require(CorePackages.ArgCheck)
--[[
Each entry in the table is a type of GameIcon with the universe id as key
]]
return Action(script.Name, function(iconsTable)
ArgCheck.isType(iconsTable, "table", "iconsTable")
return {
gameIcons = iconsTable
}
end)
@@ -0,0 +1,27 @@
return function()
local SetGameIcons = require(script.Parent.SetGameIcons)
it("should assert if given a non-table for thumbnailsTable", function()
SetGameIcons({})
expect(function()
SetGameIcons("string")
end).to.throw()
expect(function()
SetGameIcons(0)
end).to.throw()
expect(function()
SetGameIcons(nil)
end).to.throw()
expect(function()
SetGameIcons(false)
end).to.throw()
expect(function()
SetGameIcons(function() end)
end).to.throw()
end)
end
@@ -0,0 +1,26 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
--[[
Passes a table that looks like this : { "universeId" : {json}, ... }
{
"26034470" : {
universeId : "26034470",
placeId : "70542190",
url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814,
final : true
}, {...}, ...
}
]]
--TODO MOBLUAPP-778 Refactor improper Setter Actions.
return Action(script.Name, function(thumbnailsTable)
assert(type(thumbnailsTable) == "table",
string.format("SetGameThumbnails action expects thumbnailsTable to be a table, was %s", type(thumbnailsTable)))
return {
thumbnails = thumbnailsTable
}
end)
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, universeId)
return {
userId = userId,
universeId = universeId,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, isFriend)
return {
userId = userId,
isFriend = isFriend,
}
end)
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, membershipType)
return {
userId = userId,
membershipType = membershipType,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, presence, lastLocation)
return {
userId = tostring(userId),
presence = presence,
lastLocation = lastLocation,
}
end)
@@ -0,0 +1,13 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize)
return {
userId = userId,
image = image,
thumbnailType = thumbnailType,
thumbnailSize = thumbnailSize,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(key, status)
return {
key = key,
status = status
}
end)
@@ -0,0 +1,21 @@
return function()
local CorePackages = game:GetService("CorePackages")
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
describe("Action UpdateFetchingStatus", function()
it("should return correct action name", function()
expect(UpdateFetchingStatus.name).to.equal("UpdateFetchingStatus")
end)
it("should return correct action type name", function()
local action = UpdateFetchingStatus()
expect(action.type).to.equal(UpdateFetchingStatus.name)
end)
it("should return a table with the correct key and status", function()
local action = UpdateFetchingStatus("key", "status")
expect(action.key).to.equal("key")
expect(action.status).to.equal("status")
end)
end)
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,51 @@
local Workspace = game:GetService("Workspace")
local RunService = game:GetService('RunService')
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3)
local BAR_MAX_SIZE = 15
local BAR_MAX_AMPLITUDE = 40
local BAR_DIAMETER = 4
local BAR_PERIOD = 1.25
local LoadingBar = Roact.Component:extend("LoadingBar")
function LoadingBar:init()
self.barRef = Roact.createRef()
end
function LoadingBar:render()
local zIndex = self.props.ZIndex
return Roact.createElement("ImageLabel", {
Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png",
ScaleType = "Slice",
SliceCenter = BAR_SLICE_CENTER,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = zIndex,
[Roact.Ref] = self.barRef,
})
end
function LoadingBar:didMount()
self.connection = RunService.RenderStepped:Connect(function()
local t = Workspace.DistributedGameTime
local instance = self.barRef.current
local period = 2.0 * math.pi / BAR_PERIOD
local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period))
instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER)
local x = BAR_MAX_AMPLITUDE * math.cos(t*period)
instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0)
end)
end
function LoadingBar:willUnmount()
self.connection:Disconnect()
end
return LoadingBar
@@ -0,0 +1,17 @@
return function()
local LoadingBar = require(script.Parent.LoadingBar)
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
it("should create and destroy without errors", function()
local element = Roact.createElement(LoadingBar, {
Position = UDim2.new(0.5, 0, 0.5, 5),
AnchorPoint = Vector2.new(0.5, 0.5),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,4 @@
return {
AvatarThumbnail = "AvatarThumbnail",
HeadShot = "HeadShot",
}
@@ -0,0 +1,21 @@
local RetrievalStatus = {}
local EnumValues =
{
NotStarted = "NotStarted",
Fetching = "Fetching",
Done = "Done",
Failed = "Failed",
}
setmetatable(RetrievalStatus,
{
__newindex = function(t, key, index)
end,
__index = function(t, index)
assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index)))
return EnumValues[index]
end
})
return RetrievalStatus
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
return {
[0] = User.PresenceType.OFFLINE,
[1] = User.PresenceType.ONLINE,
[2] = User.PresenceType.IN_GAME,
[3] = User.PresenceType.IN_STUDIO,
}
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,15 @@
local CorePackages = game:GetService("CorePackages")
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
local FIntAvatarEditorNewCatalogButton = settings():GetFVariable("AvatarEditorNewCatalogButton2")
return function(userId)
if tonumber(userId) then
local throttleNumber = tonumber(FIntAvatarEditorNewCatalogButton)
local id = tonumber(userId)
return ThrottleUserId(throttleNumber, id)
else
return false
end
end
@@ -0,0 +1,11 @@
-- TODO: Delete this file when deleting the flag: LuaAppConvertUniverseIdToStringV364
local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364")
return function(universeId)
-- When the flag is on, we've converted the universe id to string at the place we received it
if FFlagLuaAppConvertUniverseIdToString then
return universeId
else
return tostring(universeId)
end
end
@@ -0,0 +1,15 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
local FIntEnableFriendFooterOnHomePage = settings():GetFVariable("EnableFriendFooterOnHomePageV369")
-- Don't call this function globally because we cannot get the userId
-- Reason: The LocalPlayer wouldn't be ready if we called it globally.
return function()
local throttleNumber = tonumber(FIntEnableFriendFooterOnHomePage)
local userId = Players.LocalPlayer.UserId
return ThrottleUserId(throttleNumber, userId)
end
@@ -0,0 +1,5 @@
game:DefineFastFlag("LuaAppFixLightTheme", false)
return function()
return game:GetFastFlag("LuaAppFixLightTheme")
end
@@ -0,0 +1,3 @@
return function()
return settings():GetFFlag("UseDateTimeType3")
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, conversationId, universeId, decorators)
assert(requestImpl, "requestImpl is required")
assert(conversationId, "conversationId is required")
assert(universeId, "universeId is required")
local payload = HttpService:JSONEncode({
conversationId = conversationId,
universeId = universeId,
decorators = decorators
})
local url = string.format("%s/send-game-link-message", Url.CHAT_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,16 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, conversationId, messageText, decorators)
local payload = HttpService:JSONEncode({
conversationId = conversationId,
message = messageText,
decorators = decorators
})
local url = string.format("%s/send-message", Url.CHAT_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,14 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, userId, clientId)
local payload = HttpService:JSONEncode({
participantuserId = userId
})
local url = string.format("%s/start-one-to-one-conversation", Url.CHAT_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,25 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
Docs: https://thumbnails.roblox.com/docs#!/Games/get_v1_games_icons
This resolves to
{
"data": [
{
"targetId": 0,
"state": "Error",
"imageUrl": "string"
}
]
}
]]
return function (requestImpl, universeIds, size)
local qs = Url:makeQueryString({
universeIds = table.concat(universeIds, ","),
format = "png",
size = size,
})
local url = string.format("%sv1/games/icons?%s", Url.THUMBNAILS_URL, qs)
return requestImpl(url, "GET")
end
@@ -0,0 +1,54 @@
--[[
*** DEPRECATED ***
TODO: removed this file after new thumbnail API is being in use without any flags
RELATED: GAMEDISC-27 GAMEDISC-126 FIntLuaAppPercentRollOutNewThumbnailsApiV3
]]
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
This endpoint returns a promise that resolves to:
[
{
"final": true,
"url": "string",
"retryToken": "string",
"universeId": 0,
"placeId": 0
}, {...}, ...
]
]]
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
-- imageTokens - (array<long>) the placeIds of the places you want to get thumbnails for
-- height - (int) the height of the asset to render
-- width - (int) the width of the asset to render
return function(requestImpl, imageTokens, height, width)
local args = {}
if height then
table.insert(args, string.format("height=%d", height))
end
if width then
table.insert(args, string.format("width=%d", width))
end
-- append all of the thumbnail tokens
local totalTokens = 0
for _, value in pairs(imageTokens) do
totalTokens = totalTokens + 1
table.insert(args, string.format("imageTokens=%s", value))
end
if totalTokens == 0 then
error("cannot fetch thumbnails without tokens")
end
-- construct the url
local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&"))
-- return a promise of the result listed above
return requestImpl(url, "GET")
end
@@ -0,0 +1,33 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
This endpoint returns games' information with a batches of place ids
Doc: https://games.roblox.com/docs#!/Games/get_v1_games_multiget_place_details
{
"placeId": 0,
"name": "string",
"description": "string",
"url": "string",
"builder": "string",
"builderId": 0,
"isPlayable": true,
"reasonProhibited": "string",
"universeId": 0,
"universeRootPlaceId": 0,
"price": 0,
"imageToken": "string"
}
]]--
return function(requestImpl, placeIds)
local argTable = {
placeIds = placeIds,
}
local args = Url:makeQueryString(argTable)
local url = string.format("%s/v1/games/multiget-place-details?%s", Url.GAME_URL, args)
return requestImpl(url, "GET")
end
@@ -0,0 +1,17 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, placeIds)
local argTable = {
placeIds = placeIds,
}
-- construct the url
local args = Url:makeQueryString(argTable)
local url = string.format("%s/v1/games/multiget-place-details?%s",
Url.GAME_URL, args
)
return requestImpl(url, "GET")
end
@@ -0,0 +1,39 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
Documentation of endpoint:
https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar
input:
userIds
thumbnailSize
output:
[
{
"targetId": number,
"state": string,
"imageUrl": string,
},
]
]]
local MAX_USER_IDS = 100
return function (networkImpl, userIds, thumbnailSize)
assert(type(userIds) == "table", "ThumbnailsGetAvatar expects userIds to be a table")
if #userIds == 0 or #userIds > MAX_USER_IDS then
error(string.format("ThumbnailsGetAvatar request expects userIds count between 1-%d", MAX_USER_IDS))
end
local queryString = Url:makeQueryString({
userIds = table.concat(userIds, ","),
size = thumbnailSize,
format = "png",
})
local url = string.format("%sv1/users/avatar?%s", Url.THUMBNAILS_URL, queryString)
return networkImpl(url, "GET")
end
@@ -0,0 +1,39 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
Documentation of endpoint:
https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar_headshot
input:
userIds
thumbnailSize
output:
[
{
"targetId": number,
"state": string,
"imageUrl": string,
},
]
]]
local MAX_USER_IDS = 100
return function (networkImpl, userIds, thumbnailSize)
assert(type(userIds) == "table", "ThumbnailsGetAvatarHeadshot expects userIds to be a table")
if #userIds == 0 or #userIds > MAX_USER_IDS then
error(string.format("ThumbnailsGetAvatarHeadshot request expects userIds count between 1-%d", MAX_USER_IDS))
end
local queryString = Url:makeQueryString({
userIds = table.concat(userIds, ","),
size = thumbnailSize,
format = "png",
})
local url = string.format("%sv1/users/avatar-headshot?%s", Url.THUMBNAILS_URL, queryString)
return networkImpl(url, "GET")
end
@@ -0,0 +1,31 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled)
--[[
This endpoint returns a promise that resolves to:
[
{
"success:" true,
"count": "0"
},
]
]]--
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
return function(requestImpl)
local url = string.format("%s/user/get-friendship-count?%s",
Url.API_URL, tostring(Players.LocalPlayer.UserId)
)
if isNewFriendsEndpointsEnabled() then
url = string.format("%s/my/friends/count", Url.FRIEND_URL)
end
return requestImpl(url, "GET")
end
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, userId)
local url = string.format("%s/users/%s/friends",
Url.FRIEND_URL, userId
)
return requestImpl(url, "GET")
end
@@ -0,0 +1,25 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
-- Endpoint documented here:
-- https://presence.roblox.com/docs
return function(requestImpl, userIds)
local userIdsToNumber = {}
for _, id in pairs(userIds) do
local idToNumber = tonumber(id)
if idToNumber then
table.insert(userIdsToNumber, idToNumber)
end
end
local payload = HttpService:JSONEncode({
userIds = userIdsToNumber,
})
local url = string.format("%s/presence/users", Url.PRESENCE_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,48 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local THUMBNAIL_TYPE_BY_NAME = {
AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail,
HeadShot = Enum.ThumbnailType.HeadShot,
}
local THUMBNAIL_SIZE_BY_NAME = {
Size48x48 = Enum.ThumbnailSize.Size48x48,
Size60x60 = Enum.ThumbnailSize.Size60x60,
Size100x100 = Enum.ThumbnailSize.Size100x100,
Size150x150 = Enum.ThumbnailSize.Size150x150,
Size352x352 = Enum.ThumbnailSize.Size352x352
}
return function(userId, thumbnailType, thumbnailSize)
return Promise.new(function(resolve, reject)
--Async methods will yield the thread
spawn(function()
local result = {success = false}
local success, message = pcall(function()
local image, isFinal = Players:GetUserThumbnailAsync(
tonumber(userId), THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize]
)
result = {
success = true,
id = userId,
thumbnailType = thumbnailType,
thumbnailSize = thumbnailSize,
image = isFinal and image or nil,
isFinal = isFinal,
}
end)
if success then
resolve(result)
else
result.message = message
reject(result)
end
end)
end)
end
@@ -0,0 +1,181 @@
--[[
Url Constructor
Provides a single location for base urls.
]]--
local ContentProvider = game:GetService("ContentProvider")
local FFlagLuaFixEconomyCreatorStatsUrl = game:DefineFastFlag("LuaFixEconomyCreatorStatsUrl", false)
-- helper functions
local function parseBaseUrlInformation()
-- get the current base url from the current configuration
local baseUrl = ContentProvider.BaseUrl
-- keep a copy of the base url (https://www.roblox.com/)
-- append a trailing slash if there isn't one
if baseUrl:sub(#baseUrl) ~= "/" then
baseUrl = baseUrl .. "/"
end
-- parse out scheme (http, https)
local _, schemeEnd = baseUrl:find("://")
-- parse out the prefix (www, kyle, ying, etc.)
local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1)
local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1)
-- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.)
local baseDomain = baseUrl:sub(prefixEnd + 1)
return baseUrl, basePrefix, baseDomain
end
local function preventTableModification(aTable, key, value)
error("Attempt to modify read-only table")
end
local function createReadOnlyTable(aTable)
return setmetatable({}, {
__index = aTable,
__newindex = preventTableModification,
__metatable = false
});
end
-- url construction building blocks
local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation()
-- construct urls once
local _baseApiUrl = string.format("https://api.%s", _baseDomain)
local _baseApisUrl = string.format("https://apis.%s", _baseDomain)
local _baseAuthUrl = string.format("https://auth.%s", _baseDomain)
local _baseAccountSettingsUrl = string.format("https://accountsettings.%s", _baseDomain)
local _baseAvatarUrl = string.format("https://avatar.%s", _baseDomain)
local _baseCatalogUrl = string.format("https://catalog.%s", _baseDomain)
local _baseInventoryUrl = string.format("https://inventory.%s", _baseDomain)
local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain)
local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain)
local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain)
local _baseGamesUrl = string.format("https://games.%s", _baseDomain)
local _baseGroupsUrl = string.format("https://groups.%s", _baseDomain)
local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain)
local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain)
local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain)
local _baseWebUrl = string.format("https://web.%s", _baseDomain)
local _baseWwwUrl = string.format("https://www.%s", _baseDomain)
local _baseAdsUrl = string.format("https://ads.%s", _baseDomain)
local _baseFollowingsUrl = string.format("https://followings.%s", _baseDomain)
local _baseEconomyUrl = string.format("https://economy.%s", _baseDomain)
local _baseThumbnailsUrl = string.format("https://thumbnails.%s", _baseDomain)
local _baseAccountSettings = string.format("https://accountsettings.%s", _baseDomain)
local _basePremiumFeatures = string.format("https://premiumfeatures.%s", _baseDomain)
local _baseLocale = string.format("https://locale.%s", _baseDomain)
local _baseBadgesUrl = string.format("https://badges.%s", _baseDomain)
local _baseMetricsUrl = string.format("https://metrics.%sv1", _baseDomain)
local _baseApisRcsUrl = string.format("https://apis.rcs.%s", _baseDomain)
local _baseDiscussionsUrl = string.format("https://discussions.%s", _baseDomain)
local _baseContactsUrl = string.format("https://contacts.%s", _baseDomain)
local _baseSearchUrl = string.format("https://search.%s", _baseDomain)
local _baseStaticUrl = string.format("https://static.%s", _baseDomain)
local _baseGameSearchUITreatments = string.format("https://gamesearchuitreatments.api.%s", _baseDomain)
local _baseEconomyCreatorStats = FFlagLuaFixEconomyCreatorStatsUrl
and string.format("https://economycreatorstats.%s", _baseDomain)
or string.format("https://economycreatorstats.api.%s", _baseDomain)
local _baseUrlSecure = string.gsub(_baseUrl, "http://", "https://")
-- public api
local Url = {
DOMAIN = _baseDomain,
PREFIX = _basePrefix,
BASE_URL = _baseUrl,
BASE_URL_SECURE = _baseUrlSecure,
API_URL = _baseApiUrl,
APIS_URL = _baseApisUrl,
AUTH_URL = _baseAuthUrl,
ACCOUNT_SETTINGS_URL = _baseAccountSettingsUrl,
AVATAR_URL = _baseAvatarUrl,
CATALOG_URL = _baseCatalogUrl,
INVENTORY_URL = _baseInventoryUrl,
GAME_URL = _baseGamesUrl,
GAME_ASSET_URL = _baseGameAssetUrl,
GROUPS_URL = _baseGroupsUrl,
CHAT_URL = _baseChatUrl,
FRIEND_URL = _baseFriendUrl,
PRESENCE_URL = _basePresenceUrl,
NOTIFICATION_URL = _baseNotificationUrl,
REALTIME_URL = _baseRealtimeUrl,
WEB_URL = _baseWebUrl,
WWW_URL = _baseWwwUrl,
ADS_URL = _baseAdsUrl,
SEARCH_URL = _baseSearchUrl,
GAME_SEARCH_UI_TREATMENTS = _baseGameSearchUITreatments,
FOLLOWINGS_URL = _baseFollowingsUrl,
ECONOMY_URL = _baseEconomyUrl,
THUMBNAILS_URL = _baseThumbnailsUrl,
BADGES_URL = _baseBadgesUrl,
ACCOUNT_SETTINGS = _baseAccountSettings,
PREMIUM_FEATURES = _basePremiumFeatures,
LOCALE = _baseLocale,
METRICS_URL = _baseMetricsUrl,
APIS_RCS_URL = _baseApisRcsUrl,
DISCUSSIONS_URL = _baseDiscussionsUrl,
CONTACTS_URL = _baseContactsUrl,
STATIC_URL = _baseStaticUrl,
BLOG_URL = "https://blog.roblox.com/",
CORP_URL = "https://corp.roblox.com/",
ECNOMY_CREATOR_STATS = _baseEconomyCreatorStats,
}
function Url:getUserProfileUrl(userId)
return string.format("%susers/%s/profile", self.BASE_URL, userId)
end
function Url:getUserFriendsUrl(userId)
return string.format("%susers/%s/friends", self.BASE_URL, userId)
end
function Url:getUserInventoryUrl(userId)
return string.format("%susers/%s/inventory", self.BASE_URL, userId)
end
function Url:getPlaceDefaultThumbnailUrl(placeId, width, height)
return string.format(
"%sThumbs/Asset.ashx?width=%d&height=%d&assetId=%s&ignorePlaceMediaItems=true",
self.BASE_URL,
width,
height,
tostring(placeId))
end
function Url:isVanitySite()
return self.PREFIX ~= "www"
end
-- data - (table<string, string>) a table of key/value pairs to format
function Url:makeQueryString(data)
--NOTE - This function can be used to create a query string of parameters
-- at the end of url query, or create a application/form-url-encoded post body string
local params = {}
-- NOTE - Arrays are handled, but generally data is expected to be flat.
for key, value in pairs(data) do
if value ~= nil then --for optional params
if type(value) == "table" then
for i = 1, #value do
table.insert(params, key .. "=" .. value[i])
end
else
table.insert(params, key .. "=" .. tostring(value))
end
end
end
return table.concat(params, "&")
end
-- prevent anyone from modifying this table:
Url = createReadOnlyTable(Url)
return Url
@@ -0,0 +1,12 @@
return function()
local ContentProvider = game:GetService("ContentProvider")
local Url = require(script.Parent.Url)
it("The base url has not been changed for debugging", function()
local baseUrl = ContentProvider.BaseUrl
expect(baseUrl).to.equal(Url.BASE_URL)
end)
end
@@ -0,0 +1,17 @@
--[[
A function to return a fake ID, used for testing.
We turn all IDs into strings as we typically use them as keys in the state.
It's better to use a string than a number, because a number would indicate
an array index.
Roblox APIs expect to be given integers for IDs however, so just tonumber()
the ID in this case.
]]
local lastId = 0
return function()
lastId = lastId + 1
return tostring(lastId)
end
@@ -0,0 +1,5 @@
{
"language": {
"mode": "nonstrict"
}
}
@@ -0,0 +1,39 @@
--[[
{
universeId : string,
state : string,
url : string,
}
]]
local Thumbnail = {}
function Thumbnail.new()
local self = {}
return self
end
function Thumbnail.fromThumbnailData(thumbnailData, size)
local self = Thumbnail.new()
self.universeId = tostring(thumbnailData.targetId)
self.state = thumbnailData.state
self.url = thumbnailData.imageUrl
self.size = size
return self
end
function Thumbnail.isCompleteThumbnailData(thumbnailData)
return type(thumbnailData) == "table"
and type(thumbnailData.targetId) == "number"
and type(thumbnailData.state) == "string"
and (type(thumbnailData.imageUrl) == "string" or thumbnailData.imageUrl == nil)
end
function Thumbnail.checkStateIsFinal(thumbnailState)
return thumbnailState ~= "Pending"
end
return Thumbnail
@@ -0,0 +1,20 @@
return function()
local Thumbnail = require(script.Parent.Thumbnail)
it("should set fields without errors", function()
local testData =
{
targetId = 123456,
state = "Completed",
imageUrl = "a url",
}
local thumbnail = Thumbnail.fromThumbnailData(testData)
expect(thumbnail).to.be.a("table")
expect(thumbnail.universeId).to.equal("123456")
expect(thumbnail.state).to.equal("Completed")
expect(thumbnail.url).to.equal("a url")
end)
end
@@ -0,0 +1,18 @@
local ThumbnailRequest = {}
function ThumbnailRequest.new()
local self = {}
return self
end
function ThumbnailRequest.fromData(thumbnailType, thumbnailSize)
local self = ThumbnailRequest.new()
self.thumbnailType = thumbnailType
self.thumbnailSize = thumbnailSize
return self
end
return ThumbnailRequest
@@ -0,0 +1,134 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
local User = {}
User.PresenceType = {
OFFLINE = "OFFLINE",
ONLINE = "ONLINE",
IN_GAME = "IN_GAME",
IN_STUDIO = "IN_STUDIO",
}
function User.new()
local self = {}
return self
end
function User.mock()
local self = User.new()
self.id = MockId()
self.isFetching = false
self.isFriend = false
self.lastLocation = nil
self.name = "USER NAME"
self.universeId = nil
self.placeId = nil
self.rootPlaceId = nil
self.gameInstanceId = nil
self.presence = User.PresenceType.OFFLINE
self.membership = nil
self.thumbnails = nil
self.lastOnline = nil
self.displayName = "DN+" .. self.name
return self
end
-- Note: Going forward, leverage User.fromDataTable() instead.
-- It accepts a more flexible parameter than User.fromData() and constructs the same User model
function User.fromData(id, name, isFriend)
local self = User.new()
self.id = tostring(id)
self.isFetching = false
self.isFriend = isFriend
self.lastLocation = nil
self.name = name
self.universeId = nil
self.placeId = nil
self.rootPlaceId = nil
self.gameInstanceId = nil
self.presence = (Players.LocalPlayer and self.id == tostring(Players.LocalPlayer.UserId))
and User.PresenceType.ONLINE or nil
self.thumbnails = nil
self.lastOnline = nil
return self
end
function User.fromDataTable(data)
local self = User.new()
self.id = tostring(data.id)
self.isFriend = data.isFriend
self.presence = (Players.LocalPlayer
and self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil
self.isFetching = false
self.lastLocation = nil
self.name = data.name
self.displayName = data.displayName or data.name
self.universeId = nil
self.placeId = nil
self.rootPlaceId = nil
self.gameInstanceId = nil
self.thumbnails = nil
self.lastOnline = nil
return self
end
function User.compare(user1, user2)
assert(not(user1 == nil and user2 == nil))
assert(user1 == nil or typeof(user1) == "table")
assert(user2 == nil or typeof(user2) == "table")
-- Return false if any of the provided input is nil(empty).
if not user1 or not user2 then
return false
end
for field, valueInUser2 in pairs(user2) do
if user1[field] ~= valueInUser2 then
return false
end
end
for field, valueInUser1 in pairs(user1) do
if user2[field] ~= valueInUser1 then
return false
end
end
return true
end
function User.userPresenceToText(localization, user)
local presence = user.presence
local lastLocation = user.lastLocation
if not presence then
return ''
end
if presence == User.PresenceType.OFFLINE then
return localization:Format("Common.Presence.Label.Offline")
elseif presence == User.PresenceType.ONLINE then
return localization:Format("Common.Presence.Label.Online")
elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then
if lastLocation ~= nil then
return lastLocation
else
return localization:Format("Common.Presence.Label.Online")
end
end
end
return User
@@ -0,0 +1,98 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
it("should detect if provided users are identical", function()
local clone1 = User.fromData(1, "Andy", true)
local clone2 = Immutable.Set(clone1, "isFriend", true)
local result = User.compare(clone1, clone2)
expect(result).to.equal(true)
result = User.compare(clone2, clone1)
expect(result).to.equal(true)
end)
it("should detect when there is one or more fields with different values", function()
local andy = User.fromData(1, "Andy", true)
local ollie = Immutable.Set(andy, "name", "Ollie")
local result = User.compare(andy, ollie)
expect(result).to.equal(false)
result = User.compare(ollie, andy)
expect(result).to.equal(false)
end)
it("should detect descrepancy when one user model contains more fields than the other", function()
local andy = User.fromData(1, "Andy", true)
local secretlyNotAndy = Immutable.Set(andy, "someDifferentField", "I'm Ollie!")
local result = User.compare(andy, secretlyNotAndy)
expect(result).to.equal(false)
result = User.compare(secretlyNotAndy, andy)
expect(result).to.equal(false)
end)
it("should throw if invalid input is provided", function()
local aString = "I'm not a table."
local teddy = User.fromData(1, "Teddy", true)
expect(function() User.compare(nil, nil) end).to.throw()
expect(function() User.compare(aString, nil) end).to.throw()
expect(function() User.compare(nil, aString) end).to.throw()
expect(function() User.compare(aString, aString) end).to.throw()
expect(function() User.compare(teddy, aString) end).to.throw()
expect(function() User.compare(aString, teddy) end).to.throw()
end)
it("should return false if any one of the input is empty or nil)", function()
local emptyTable = {}
local teddy = User.fromData(1, "Teddy", true)
local result = User.compare(teddy, nil)
expect(result).to.equal(false)
result = User.compare(nil, teddy)
expect(result).to.equal(false)
result = User.compare(teddy, emptyTable)
expect(result).to.equal(false)
result = User.compare(emptyTable, teddy)
expect(result).to.equal(false)
end)
describe("fromDataTable", function()
it("should properly set user data", function()
local data = {
id = 1,
name = "FooBar",
displayName = "FooBar+DN",
isFriend = false,
}
local user = User.fromDataTable(data)
expect(user.id).to.equal("1")
expect(user.name).to.equal("FooBar")
expect(user.displayName).to.equal("FooBar+DN")
expect(user.isFriend).to.equal(false)
end)
it("should still set user data without a displayName property", function()
local data = {
id = 1,
name = "FooBar",
isFriend = false,
}
local user = User.fromDataTable(data)
expect(user.id).to.equal("1")
expect(user.name).to.equal("FooBar")
expect(user.displayName).to.equal("FooBar")
expect(user.isFriend).to.equal(false)
end)
end)
end
@@ -0,0 +1,51 @@
local percentReporting = tonumber(settings():GetFVariable("PercentReportingNetworkProfileAfterStartup"))
local FEATURE_NAME = "NetworkProfileDuringStartup"
local QUEUED_MEASURE_NAME = "Queued"
local NAME_LOOKUP_MEASURE_NAME = "NameLookup"
local CONNECT_MEASURE_NAME = "Connect"
local SSL_HANDSHAKE_MEASURE_NAME = "SSLHandshake"
local MAKE_REQUEST_MEASURE_NAME = "MakeRequest"
local RECEIVE_RESPONSE_MEASURE_NAME = "ReceiveResponse"
local NetworkProfiler = {}
NetworkProfiler.__index = NetworkProfiler
NetworkProfiler.aggregate = {
queued = 0.0,
nameLookup = 0.0,
connect = 0.0,
sslHandshake = 0.0,
makeRequest = 0.0,
receiveResponse = 0.0,
}
function NetworkProfiler:track(timeProfile)
self.aggregate.queued = self.aggregate.queued + timeProfile.queued
if timeProfile.nameLookup >= 0 then
self.aggregate.nameLookup = self.aggregate.nameLookup + timeProfile.nameLookup
end
if timeProfile.connect >= 0 then
self.aggregate.connect = self.aggregate.connect + timeProfile.connect
end
if timeProfile.sslHandshake >= 0 then
self.aggregate.sslHandshake = self.aggregate.sslHandshake + timeProfile.sslHandshake
end
if timeProfile.makeRequest >= 0 then
self.aggregate.makeRequest = self.aggregate.makeRequest + timeProfile.makeRequest
end
if timeProfile.receiveResponse >= 0 then
self.aggregate.receiveResponse = self.aggregate.receiveResponse + timeProfile.receiveResponse
end
end
function NetworkProfiler:report(reportToDiag)
reportToDiag(FEATURE_NAME, QUEUED_MEASURE_NAME, self.aggregate.queued, percentReporting)
reportToDiag(FEATURE_NAME, NAME_LOOKUP_MEASURE_NAME, self.aggregate.nameLookup, percentReporting)
reportToDiag(FEATURE_NAME, CONNECT_MEASURE_NAME, self.aggregate.connect, percentReporting)
reportToDiag(FEATURE_NAME, SSL_HANDSHAKE_MEASURE_NAME, self.aggregate.sslHandshake, percentReporting)
reportToDiag(FEATURE_NAME, MAKE_REQUEST_MEASURE_NAME, self.aggregate.makeRequest, percentReporting)
reportToDiag(FEATURE_NAME, RECEIVE_RESPONSE_MEASURE_NAME, self.aggregate.receiveResponse, percentReporting)
end
return NetworkProfiler
@@ -0,0 +1,8 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.Promise)
@@ -0,0 +1,91 @@
--[[
Provides utility functions for Promises
]]
local CorePackages = game:GetService("CorePackages")
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local PromiseUtilities = {}
--[[
Accept a table of promises;
promises = {
[1] = Promise.resolve(),
["Home"] = Promise.reject(),
...
}
Returns a new promise that:
* is resolved when all input promises are finished.
returns the results of each individual promises in a list of Results
results = {
[1] = Result1,
["Home"] = Result2,
...
}
* is never rejected.
]]
function PromiseUtilities.Batch(promises)
assert(type(promises) == "table", "PromiseUtilities expects a list of Promises!")
local numberOfPromises = 0
for _, promise in pairs(promises) do
assert(Promise.is(promise), "PromiseUtilities expects a list of Promises!")
numberOfPromises = numberOfPromises + 1
end
return Promise.new(function(resolve, reject)
local totalCompleted = 0
local results = {}
local function promiseCompleted(key, success, value)
results[key] = Result.new(success, value)
totalCompleted = totalCompleted + 1
if totalCompleted == numberOfPromises then
resolve(results)
end
end
if next(promises) == nil then
resolve(results)
end
for key, promise in pairs(promises) do
promise:andThen(
function(result, ...)
if select("#", ...) > 0 then
warn("Promises in PromiseUtilities.Batch should not return tuple")
end
promiseCompleted(key, true, result)
end,
function(reason)
promiseCompleted(key, false, reason)
end
)
end
end)
end
function PromiseUtilities.CountResults(batchPromiseResults)
local totalCount = 0
local failureCount = 0
for _, result in pairs(batchPromiseResults) do
local success, _ = result:unwrap()
if not success then
failureCount = failureCount + 1
end
totalCount = totalCount + 1
end
return {
successCount = totalCount - failureCount,
failureCount = failureCount,
totalCount = totalCount,
}
end
return PromiseUtilities
@@ -0,0 +1,188 @@
return function()
local CorePackages = game:GetService("CorePackages")
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
describe("PromiseUtilities.Batch", function()
it("should assert if input is not a list of Promises", function()
expect(function()
PromiseUtilities.Batch()
end).to.throw()
expect(function()
PromiseUtilities.Batch(Promise.resolve(), Promise.resolve())
end).to.throw()
expect(function()
PromiseUtilities.Batch({
Promise.resolve(),
"something else"
})
end).to.throw()
end)
it("should invoke the given resolve callback when all promises are finished", function()
local promises = {
[1] = Promise.resolve(),
["Home"] = Promise.resolve()
}
local callCount = 0
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function()
callCount = callCount + 1
end
)
expect(batchedPromise).to.be.ok()
expect(callCount).to.equal(1)
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
end)
it("should not invoke any callbacks when one of the promises are not finished", function()
local promises = {
[1] = Promise.resolve(),
["Home"] = Promise.new(function() end)
}
local callCount = 0
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function()
callCount = callCount + 1
end,
function()
callCount = callCount + 1
end
)
expect(batchedPromise).to.be.ok()
expect(callCount).to.equal(0)
expect(batchedPromise._status).to.equal(Promise.Status.Started)
end)
it("should return the correct results of each individual promise", function()
local promises = {
[1] = Promise.resolve(5),
["Home"] = Promise.reject("failed")
}
local promiseResults = nil
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function(results)
promiseResults = results
end
)
expect(batchedPromise).to.be.ok()
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
expect(TableUtilities.FieldCount(promiseResults)).to.equal(2)
expect(Result.is(promiseResults[1])).to.equal(true)
local success1, value1 = promiseResults[1]:unwrap()
expect(success1).to.equal(true)
expect(value1).to.equal(5)
local isMatchCalled1 = false
promiseResults[1]:match(function(result)
expect(result).to.equal(5)
isMatchCalled1 = true
end,
function()
error("should not be called")
end)
expect(isMatchCalled1).to.equal(true)
expect(Result.is(promiseResults["Home"])).to.equal(true)
local success2, value2 = promiseResults["Home"]:unwrap()
expect(success2).to.equal(false)
expect(value2).to.equal("failed")
local isMatchCalled2 = false
promiseResults["Home"]:match(function()
error("should not be called")
end,
function(err)
expect(err).to.equal("failed")
isMatchCalled2 = true
end)
expect(isMatchCalled2).to.equal(true)
end)
it("should return the correct results of each individual promise that resolved later", function()
local resolveLater
local rejectLater
local promises = {
[1] = Promise.new(function(resolve)
resolveLater = resolve
end),
["Home"] = Promise.new(function(_, reject)
rejectLater = reject
end)
}
local promiseResults = nil
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function(results)
promiseResults = results
end
)
resolveLater(5)
rejectLater("failed")
expect(batchedPromise).to.be.ok()
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
expect(TableUtilities.FieldCount(promiseResults)).to.equal(2)
expect(Result.is(promiseResults[1])).to.equal(true)
local success1, value1 = promiseResults[1]:unwrap()
expect(success1).to.equal(true)
expect(value1).to.equal(5)
expect(Result.is(promiseResults["Home"])).to.equal(true)
local success2, value2 = promiseResults["Home"]:unwrap()
expect(success2).to.equal(false)
expect(value2).to.equal("failed")
end)
it("should resolve if given an empty list of promises", function()
local emptyPromises = {}
local callCount = 0
local batchedPromise = PromiseUtilities.Batch(emptyPromises):andThen(
function(results)
callCount = callCount + 1
end
)
expect(batchedPromise).to.be.ok()
expect(callCount).to.equal(1)
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
end)
end)
describe("PromiseUtilities.CountResults", function()
it("should count the results correctly", function()
local emptyResults = {}
local countResult = PromiseUtilities.CountResults(emptyResults)
expect(countResult).to.be.ok()
expect(countResult.successCount).to.equal(0)
expect(countResult.failureCount).to.equal(0)
expect(countResult.totalCount).to.equal(0)
local promiseResults = { Result.success(0), Result.success(0), Result.error(1) }
countResult = PromiseUtilities.CountResults(promiseResults)
expect(countResult).to.be.ok()
expect(countResult.successCount).to.equal(2)
expect(countResult.failureCount).to.equal(1)
expect(countResult.totalCount).to.equal(3)
end)
end)
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,29 @@
local CorePackages = game:GetService("CorePackages")
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
local Cryo = require(CorePackages.Cryo)
return function(state, action)
state = state or {}
if action.type == UpdateFetchingStatus.name then
local key = action.key
local status = action.status
local value
if status ~= nil then
value = status
else
value = Cryo.None
end
state = Cryo.Dictionary.join(
state,
{
[key] = value,
}
)
end
return state
end
@@ -0,0 +1,52 @@
return function()
local CorePackages = game:GetService("CorePackages")
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
local FetchingStatusReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus)
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
local KEY_1 = "key_1"
local KEY_2 = "key_2"
describe("FetchingStatus", function()
it("should be empty by default", function()
local state = FetchingStatusReducer(nil, {})
expect(TableUtilities.FieldCount(state)).to.equal(0)
end)
it("should not be modified by other actions", function()
local oldState = FetchingStatusReducer(nil, {})
local newState = FetchingStatusReducer(oldState, { type = "not a real action" })
expect(newState).to.equal(oldState)
end)
it("should be changed using UpdateFetchingStatus", function()
local state = FetchingStatusReducer(nil, {})
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Fetching))
expect(state[KEY_1]).to.equal(RetrievalStatus.Fetching)
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed))
expect(state[KEY_1]).to.equal(RetrievalStatus.Failed)
end)
it("should store different values for different keys", function()
local state = FetchingStatusReducer(nil, {})
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed))
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_2, RetrievalStatus.Done))
expect(state[KEY_1]).to.equal(RetrievalStatus.Failed)
expect(state[KEY_2]).to.equal(RetrievalStatus.Done)
end)
it("should clear values for nil keys", function()
local state = { [KEY_1] = RetrievalStatus.Fetching }
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, nil))
expect(state[KEY_1]).to.equal(nil)
end)
end)
end
@@ -0,0 +1,43 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
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 function setFieldPerUser(state, fieldName, userId, value)
local field = state[fieldName] or {}
return Immutable.JoinDictionaries(state, {
[fieldName] = Immutable.JoinDictionaries(field, {
[userId] = value
})
})
end
local function setRetrievalStatus(state, userId, status)
return setFieldPerUser(state, "retrievalStatus", userId, status)
end
local function setRetrievalFailureResponse(state, userId, response)
return setFieldPerUser(state, "retrievalFailureResponse", userId, response)
end
return function(state, action)
state = state or {
retrievalStatus = {},
retrievalFailureResponse = {},
}
if action.type == FetchUserFriendsStarted.name then
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Fetching)
elseif action.type == FetchUserFriendsFailed.name then
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Failed)
state = setRetrievalFailureResponse(state, action.userId, action.response)
elseif action.type == FetchUserFriendsCompleted.name then
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Done)
end
return state
end
@@ -0,0 +1,21 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags
local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString)
return function(state, action)
state = state or {}
if action.type == ReceivedPlacesInfos.name then
for _, placeInfo in pairs(action.placesInfos) do
local universeId = convertUniverseIdToString(placeInfo.universeId)
state = Immutable.Set(state, universeId, placeInfo)
end
end
return state
end
@@ -0,0 +1,98 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser)
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
local RemoveUser = require(CorePackages.AppTempCommon.LuaApp.Actions.RemoveUser)
local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend)
local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType)
local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence)
local SetUserThumbnail = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserThumbnail)
return function(state, action)
state = state or {}
if action.type == AddUser.name then
local user = action.user
state = Immutable.Set(state, user.id, user)
elseif action.type == AddUsers.name then
local addedUsers = action.users
local usersUpdate = {}
for userId, addedUser in pairs(addedUsers) do
local existingUser = state[userId]
if existingUser then
usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser)
else
usersUpdate[userId] = addedUser
end
end
state = Immutable.JoinDictionaries(state, usersUpdate)
elseif action.type == SetUserIsFriend.name then
local user = state[action.userId]
if user then
local newUser = Immutable.Set(user, "isFriend", action.isFriend)
state = Immutable.Set(state, user.id, newUser)
else
warn("Setting isFriend on user", action.userId, "who doesn't exist yet")
end
elseif action.type == SetUserPresence.name then
local user = state[action.userId]
if user then
local newUser = Immutable.JoinDictionaries(user, {
presence = action.presence,
lastLocation = action.lastLocation,
})
state = Immutable.Set(state, user.id, newUser)
else
warn("Setting presence on user", action.userId, "who doesn't exist yet")
end
elseif action.type == ReceivedUserPresence.name then
local user = state[action.userId]
if user then
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
presence = action.presence,
lastLocation = action.lastLocation,
placeId = action.placeId,
rootPlaceId = action.rootPlaceId,
gameInstanceId = action.gameInstanceId,
lastOnline = action.lastOnline,
universeId = action.universeId,
}),
})
end
elseif action.type == SetUserThumbnail.name then
local user = state[action.userId]
if user then
local thumbnails = user.thumbnails or {}
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
thumbnails = Immutable.JoinDictionaries(thumbnails, {
[action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, {
[action.thumbnailSize] = action.image,
}),
}),
}),
})
end
elseif action.type == SetUserMembershipType.name then
local user = state[action.userId]
if user then
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
membership = action.membershipType,
}),
})
end
elseif action.type == RemoveUser.name then
if state[action.userId] then
state = Immutable.RemoveFromDictionary(state, action.userId)
end
end
return state
end
@@ -0,0 +1,106 @@
return function()
local CorePackages = game:GetService("CorePackages")
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users)
local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser)
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend)
local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType)
local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence)
describe("initial state", function()
it("should return an initial table when passed nil", function()
local state = Users(nil, {})
expect(state).to.be.a("table")
end)
end)
describe("AddUser", function()
it("should add a user to the store", function()
local user = User.mock()
local state = {}
state = Users(state, AddUser(user))
expect(state[user.id]).to.equal(user)
end)
end)
describe("SetUserIsFriend", function()
it("should set isFriend on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
expect(state[user.id].isFriend).to.equal(false)
state = Users(state, SetUserIsFriend(user.id, true))
expect(state[user.id].isFriend).to.equal(true)
state = Users(state, SetUserIsFriend(user.id, false))
expect(state[user.id].isFriend).to.equal(false)
end)
end)
describe("SetUserPresence", function()
it("should set presence on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE)
state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE))
expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE)
state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME))
expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME)
state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO))
expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO)
end)
end)
describe("ReceivedUserPresence", function()
it("should set presence on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
local existingPresence = user.presence
local newPresence = 'ONLINE'
local lastLocation = MockId()
local newPlaceId = MockId()
state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId))
expect(user.presence).to.equal(existingPresence)
expect(state[user.id].presence).to.equal(newPresence)
expect(state[user.id].lastLocation).to.equal(lastLocation)
expect(state[user.id].placeId).to.equal(newPlaceId)
end)
end)
describe("SetUserMembershipType", function()
it("should set membership on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
local existingMembership = user.membership
local newMembership = Enum.MembershipType.BuildersClub
state = Users(state, SetUserMembershipType(user.id, newMembership))
expect(user.membership).to.equal(existingMembership)
expect(state[user.id].membership).to.equal(newMembership)
end)
end)
end
@@ -0,0 +1,8 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.Result)
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,34 @@
--[[
The is a wrapper for the style provider for apps.
props:
style : table - Includes the name of the theme and font being used.
{
themeName : string - The name of the theme being used.
fontName : string - The name of the font being used.
}
]]
local CorePackages = game:GetService("CorePackages")
local ArgCheck = require(CorePackages.ArgCheck)
local Roact = require(CorePackages.Roact)
local UIBlox = require(CorePackages.UIBlox)
local StyleProvider = UIBlox.Style.Provider
local StylePalette = require(script.Parent.StylePalette)
local AppStyleProvider = Roact.Component:extend("AppStyleProvider")
function AppStyleProvider:render()
local style = self.props.style
ArgCheck.isNotNil(style, "style prop for AppStyleProvider")
local themeName = style.themeName
local fontName = style.fontName
local stylePalette = StylePalette.new()
stylePalette:updateTheme(themeName)
stylePalette:updateFont(fontName)
local appStyle = stylePalette:currentStyle()
return Roact.createElement(StyleProvider,{
style = appStyle,
}, self.props[Roact.Children])
end
return AppStyleProvider
@@ -0,0 +1,32 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local AppStyleProvider = require(script.Parent.AppStyleProvider)
local Constants = require(script.Parent.Constants)
local appStyle = {
themeName = Constants.ThemeName.Dark,
fontName = Constants.FontName.Gotham,
}
it("should create and destroy without errors", function()
local element = Roact.createElement("Frame")
local appStyleProvider = Roact.createElement(AppStyleProvider, {
style = appStyle,
},{
Element = element,
})
local instance = Roact.mount(appStyleProvider)
Roact.unmount(instance)
end)
it("should throw when style prop is nil", function()
local element = Roact.createElement("Frame")
local appStyleProvider = Roact.createElement(AppStyleProvider, {},{
Element = element,
})
expect(function()
local instance = Roact.mount(appStyleProvider)
Roact.unmount(instance)
end).to.throw()
end)
end
@@ -0,0 +1,23 @@
local Colors = {
--Common colors
Black = Color3.fromRGB(0, 0, 0),
White = Color3.fromRGB(255, 255, 255),
Green = Color3.fromRGB(0, 176, 111),
Red = Color3.fromRGB(247, 75, 82),
--Dark theme colors
Carbon = Color3.fromRGB(31, 33, 35),
Flint = Color3.fromRGB(57, 59, 61),
Graphite = Color3.fromRGB(101, 102, 104),
Obsidian = Color3.fromRGB(24, 25, 27),
Pumice = Color3.fromRGB(189, 190, 190),
Slate = Color3.fromRGB(35, 37, 39),
--Light theme colors
Alabaster = Color3.fromRGB(242, 244, 245),
Ash = Color3.fromRGB(234, 237, 239),
Chalk = Color3.fromRGB(216, 219, 222),
Smoke = Color3.fromRGB(96, 97, 98),
}
return Colors
@@ -0,0 +1,12 @@
local Constants = {}
Constants.ThemeName = {
Dark = "dark",
Light = "light",
}
Constants.FontName = {
Gotham = "gotham",
}
return Constants
@@ -0,0 +1,54 @@
local baseSize = 16
-- Nominal size conversion
-- https://confluence.rbx.com/display/PX/Font+Metrics
local nominalSizeFactor = 1.2
local font = {
BaseSize = baseSize * nominalSizeFactor,
Title = {
Font = Enum.Font.GothamBlack,
RelativeSize = 32 / baseSize,
RelativeMinSize = 24 / baseSize,
},
Header1 = {
Font = Enum.Font.GothamSemibold,
RelativeSize = 20 / baseSize,
RelativeMinSize = 16 / baseSize,
},
Header2 = {
Font = Enum.Font.GothamSemibold,
RelativeSize = 16 / baseSize,
RelativeMinSize = 12 / baseSize,
},
SubHeader1 = {
Font = Enum.Font.GothamSemibold,
RelativeSize = 16 / baseSize,
RelativeMinSize = 12 / baseSize,
},
Body = {
Font = Enum.Font.Gotham,
RelativeSize = 16 / baseSize,
RelativeMinSize = 12 / baseSize,
},
CaptionHeader = {
Font = Enum.Font.GothamSemibold,
RelativeSize = 12 / baseSize,
RelativeMinSize = 9 / baseSize,
},
CaptionSubHeader = {
Font = Enum.Font.GothamSemibold,
RelativeSize = 12 / baseSize,
RelativeMinSize = 9 / baseSize,
},
CaptionBody = {
Font = Enum.Font.Gotham,
RelativeSize = 12 / baseSize,
RelativeMinSize = 9 / baseSize,
},
Footer = {
Font = Enum.Font.GothamSemibold,
RelativeSize = 10 / baseSize,
RelativeMinSize = 8 / baseSize,
},
}
return font
@@ -0,0 +1,9 @@
return function()
it("should be valid font palette without errors", function()
local CorePackages = game:GetService("CorePackages")
local UIBlox = require(CorePackages.UIBlox)
local validateFont = UIBlox.Style.Validator.validateFont
local Gotham = require(script.Parent.Gotham)
assert(validateFont(Gotham))
end)
end
@@ -0,0 +1,20 @@
local CorePackages = game:GetService("CorePackages")
local ArgCheck = require(CorePackages.ArgCheck)
local Logging = require(CorePackages.Logging)
local UIBlox = require(CorePackages.UIBlox)
local validateFont = UIBlox.Style.Validator.validateFont
return function (fontName, defaultFont, fontMap)
local mappedFont
if fontName ~= nil and #fontName > 0 then
mappedFont = fontMap[string.lower(fontName)]
end
if mappedFont == nil then
mappedFont = fontMap[defaultFont]
Logging.warn(string.format("Unrecognized font name: `%s`", tostring(fontName)))
end
ArgCheck.assert(validateFont(mappedFont))
return mappedFont
end
@@ -0,0 +1,33 @@
return function()
local getFontFromName = require(script.Parent.getFontFromName)
local Constants = require(script.Parent.Parent.Constants)
it("should be able to get a font palette without errors", function()
local fontMap = {
[Constants.FontName.Gotham] = require(script.Parent.Gotham),
}
local fontTable = getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap)
expect(fontTable).to.be.a("table")
end)
it("should be able to get a font palette using default without errors", function()
local fontMap = {
[Constants.FontName.Gotham] = require(script.Parent.Gotham),
}
local fontTable = getFontFromName("sourceSans", Constants.FontName.Gotham, fontMap)
expect(fontTable).to.be.a("table")
end)
it("should throw the font palette is invalid", function()
expect(function()
local fontMap = {
[Constants.FontName.Gotham] = {
Font = {
Font = Enum.Font.Gotham,
RelativeSize = 1,
},
},
}
getFontFromName(Constants.FontName.Gotham, Constants.FontName.Gotham, fontMap)
end).to.throw()
end)
end
@@ -0,0 +1,49 @@
local getThemeFromName = require(script.Parent.Themes.getThemeFromName)
local getFontFromName = require(script.Parent.Fonts.getFontFromName)
local Constants = require(script.Parent.Constants)
local StylePalette = {}
StylePalette.__index = StylePalette
local DEFAULT_FONT = Constants.FontName.Gotham
local FONT_MAP = {
[Constants.FontName.Gotham] = require(script.Parent.Fonts.Gotham),
}
local DEFAULT_THEME = Constants.ThemeName.Light
local THEME_MAP = {
[Constants.ThemeName.Dark] = require(script.Parent.Themes.DarkTheme),
[Constants.ThemeName.Light] = require(script.Parent.Themes.LightTheme),
}
function StylePalette.new(style)
--By default a new style will be empty.
-- This will allow the font and theme to be merged independently even when one is empty.
local self = {}
if style ~= nil then
self.Font = style.Font
self.Theme = style.Theme
end
setmetatable(self, StylePalette)
return self
end
function StylePalette:updateFont(fontName)
self.Font = getFontFromName(fontName, DEFAULT_FONT, FONT_MAP)
end
function StylePalette:updateTheme(themeName)
self.Theme = getThemeFromName(themeName, DEFAULT_THEME, THEME_MAP)
end
function StylePalette:currentStyle()
local style = {
Font = self.Font,
Theme = self.Theme,
}
return style
end
return StylePalette
@@ -0,0 +1,58 @@
return function()
local CorePackages = game:GetService("CorePackages")
local UIBlox = require(CorePackages.UIBlox)
local StylePalette = require(script.Parent.StylePalette)
local validateStye = UIBlox.Style.Validator.validateStyle
it("should be able to create a style palette", function()
local stylePalette = StylePalette.new()
stylePalette:updateTheme("dark")
stylePalette:updateFont("gotham")
local appStyle = stylePalette:currentStyle()
expect(validateStye(appStyle)).equal(true)
end)
it("should be able to create a style palette and be able to update theme 1", function()
local stylePalette = StylePalette.new()
stylePalette:updateTheme("dark")
stylePalette:updateFont("gotham")
local appStyle = stylePalette:currentStyle()
expect(validateStye(appStyle)).equal(true)
stylePalette:updateTheme("light")
local newAppStyle = stylePalette:currentStyle()
expect(validateStye(newAppStyle)).equal(true)
end)
it("should be able to create a style palette and be able to update theme 2", function()
local stylePalette = StylePalette.new()
stylePalette:updateTheme("dark")
stylePalette:updateFont("gotham")
local appStyle = stylePalette:currentStyle()
expect(validateStye(appStyle)).equal(true)
stylePalette:updateFont("gotham")
local newAppStyle = stylePalette:currentStyle()
expect(validateStye(newAppStyle)).equal(true)
end)
it("should be able to create a style palette and be able to merge an old one in", function()
local stylePalette = StylePalette.new()
stylePalette:updateTheme("dark")
stylePalette:updateFont("gotham")
local appStyle = stylePalette:currentStyle()
expect(validateStye(appStyle)).equal(true)
local newstylePalette = StylePalette.new(stylePalette)
local newAppStyle = newstylePalette:currentStyle()
expect(validateStye(newAppStyle)).equal(true)
end)
it("should be able to create a empty style palette", function()
local stylePalette = StylePalette.new()
local appStyle = stylePalette:currentStyle()
expect(appStyle.Font).equal(nil)
expect(appStyle.Theme).equal(nil)
expect(validateStye(appStyle)).equal(false)
end)
end
@@ -0,0 +1,162 @@
local ThemesRoot = script.Parent
local StylesRoot = ThemesRoot.Parent
local Colors = require(StylesRoot.Colors)
local theme = {
BackgroundDefault = {
Color = Colors.Slate,
Transparency = 0,
},
BackgroundContrast = {
Color = Colors.Carbon,
Transparency = 0,
},
BackgroundMuted = {
Color = Colors.Obsidian,
Transparency = 0,
},
BackgroundUIDefault = {
Color = Colors.Flint,
Transparency = 0,
},
BackgroundUIContrast = {
Color = Colors.Black,
Transparency = 0.3, -- Alpha 0.7
},
BackgroundOnHover = {
Color = Colors.White,
Transparency = 0.9, -- Alpha 0.1
},
BackgroundOnPress = {
Color = Colors.Black,
Transparency = 0.7, -- Alpha 0.3
},
UIDefault = {
Color = Colors.Graphite,
Transparency = 0,
},
UIMuted = {
Color = Colors.Obsidian,
Transparency = 0.2, -- Alpha 0.8
},
UIEmphasis = {
Color = Colors.White,
Transparency = 0.7, -- Alpha 0.3
},
ContextualPrimaryDefault = {
Color = Colors.Green,
Transparency = 0,
},
ContextualPrimaryOnHover = {
Color = Colors.Green,
Transparency = 0,
},
ContextualPrimaryContent = {
Color = Colors.White,
Transparency = 0,
},
SystemPrimaryDefault = {
Color = Colors.White,
Transparency = 0,
},
SystemPrimaryOnHover = {
Color = Colors.White,
Transparency = 0,
},
SystemPrimaryContent = {
Color = Colors.Flint,
Transparency = 0,
},
SecondaryDefault = {
Color = Colors.White,
Transparency = 0.3, -- 0.7 Alpha
},
SecondaryOnHover = {
Color = Colors.White,
Transparency = 0,
},
SecondaryContent = {
Color = Colors.White,
Transparency = 0.3, -- 0.7 Alpha
},
IconDefault = {
Color = Colors.White,
Transparency = 0.3, -- 0.7 alpha
},
IconEmphasis = {
Color = Colors.White,
Transparency = 0,
},
IconOnHover = {
Color = Colors.White,
Transparency = 0,
},
TextEmphasis = {
Color = Colors.White,
Transparency = 0,
},
TextDefault = {
Color = Colors.Pumice,
Transparency = 0,
},
TextMuted = {
Color = Colors.White,
Transparency = 0.3, -- 0.7 Alpha
},
Divider = {
Color = Colors.White,
Transparency = 0.8, -- 0.2 Alpha
},
Overlay = {
Color = Colors.Black,
Transparency = 0.5, -- 0.5 Alpha
},
DropShadow = {
Color = Colors.Black,
Transparency = 0,
},
NavigationBar = {
Color = Colors.Carbon,
Transparency = 0,
},
PlaceHolder = {
Color = Colors.Flint,
Transparency = 0.5, -- 0.5 Alpha
},
OnlineStatus = {
Color = Colors.Green,
Transparency = 0,
},
OfflineStatus = {
Color = Colors.White,
Transparency = 0.3, -- 0.7 Alpha
},
Success = {
Color = Colors.Green,
Transparency = 0,
},
Alert = {
Color = Colors.Red,
Transparency = 0,
},
Badge = {
Color = Colors.White,
Transparency = 0,
},
BadgeContent = {
Color = Colors.Flint,
Transparency = 0,
},
}
return theme
@@ -0,0 +1,9 @@
return function()
it("should be a valid theme palette.", function()
local CorePackages = game:GetService("CorePackages")
local UIBlox = require(CorePackages.UIBlox)
local validateTheme = UIBlox.Style.Validator.validateTheme
local DarkTheme = require(script.Parent.DarkTheme)
assert(validateTheme(DarkTheme))
end)
end
@@ -0,0 +1,168 @@
local ThemesRoot = script.Parent
local StylesRoot = ThemesRoot.Parent
local LuaAppRoot = StylesRoot.Parent
local Colors = require(StylesRoot.Colors)
local GetFFlagLuaAppFixLightTheme = require(LuaAppRoot.Flags.GetFFlagLuaAppFixLightTheme)
local theme = {
BackgroundDefault = {
Color = Colors.Alabaster,
Transparency = 0,
},
BackgroundContrast = {
Color = Colors.Ash,
Transparency = 0,
},
BackgroundMuted = {
Color = Colors.Chalk,
Transparency = 0,
},
BackgroundUIDefault = {
Color = Colors.White,
Transparency = 0,
},
BackgroundUIContrast = {
Color = Colors.White,
Transparency = 0.1, -- Alpha 0.9
},
BackgroundOnHover = GetFFlagLuaAppFixLightTheme() and {
Color = Colors.Black,
Transparency = 0.9, -- Alpha 0.1
} or {
Color = Colors.White,
Transparency = 0.7, -- Alpha 0.3
},
BackgroundOnPress = {
Color = Colors.Black,
Transparency = 0.9, -- Alpha 0.1
},
UIDefault = {
Color = Colors.Pumice,
Transparency = 0,
},
UIMuted = {
Color = Colors.Black,
Transparency = 0.9, -- Alpha 0.1
},
UIEmphasis = {
Color = Colors.Black,
Transparency = 0.7, -- Alpha 0.3
},
ContextualPrimaryDefault = {
Color = Colors.Green,
Transparency = 0,
},
ContextualPrimaryOnHover = {
Color = Colors.Green,
Transparency = 0,
},
ContextualPrimaryContent = {
Color = Colors.White,
Transparency = 0,
},
SystemPrimaryDefault = {
Color = Colors.Flint,
Transparency = 0,
},
SystemPrimaryOnHover = {
Color = Colors.Flint,
Transparency = 0,
},
SystemPrimaryContent = {
Color = Colors.White,
Transparency = 0,
},
SecondaryDefault = {
Color = Colors.Black,
Transparency = 0.5, -- 0.5 Alpha
},
SecondaryOnHover = {
Color = Colors.Flint,
Transparency = 0,
},
SecondaryContent = {
Color = Colors.Black,
Transparency = 0.5, -- 0.5 Alpha
},
IconDefault = {
Color = Colors.Black,
Transparency = 0.4, -- 0.6 alpha
},
IconEmphasis = {
Color = Colors.Flint,
Transparency = 0,
},
IconOnHover = {
Color = Colors.Flint,
Transparency = 0,
},
TextEmphasis = {
Color = Colors.Flint,
Transparency = 0,
},
TextDefault = {
Color = Colors.Smoke,
Transparency = 0,
},
TextMuted = {
Color = Colors.Black,
Transparency = 0.4, -- 0.6 Alpha
},
Divider = {
Color = Colors.Pumice,
Transparency = 0,
},
Overlay = {
Color = Colors.Black,
Transparency = 0.7, -- 0.3 Alpha
},
DropShadow = {
Color = Colors.Black,
Transparency = 0,
},
NavigationBar = {
Color = Colors.White,
Transparency = 0,
},
PlaceHolder = {
Color = Colors.Chalk,
Transparency = 0.3, -- 0.7 Alpha
},
OnlineStatus = {
Color = Colors.Green,
Transparency = 0,
},
OfflineStatus = {
Color = Colors.Black,
Transparency = 0.5, -- 0.5 Alpha
},
Success = {
Color = Colors.Green,
Transparency = 0,
},
Alert = {
Color = Colors.Red,
Transparency = 0,
},
Badge = {
Color = Colors.Flint,
Transparency = 0,
},
BadgeContent = {
Color = Colors.White,
Transparency = 0,
},
}
return theme
@@ -0,0 +1,9 @@
return function()
it("should be a valid theme palette.", function()
local CorePackages = game:GetService("CorePackages")
local UIBlox = require(CorePackages.UIBlox)
local validateTheme = UIBlox.Style.Validator.validateTheme
local LightTheme = require(script.Parent.DarkTheme)
assert(validateTheme(LightTheme))
end)
end
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local ArgCheck = require(CorePackages.ArgCheck)
local Logging = require(CorePackages.Logging)
local UIBlox = require(CorePackages.UIBlox)
local validateTheme = UIBlox.Style.Validator.validateTheme
return function (themeName, defaultTheme, themeMap)
local mappedTheme
if themeName ~= nil and #themeName > 0 then
mappedTheme = themeMap[string.lower(themeName)]
end
if mappedTheme == nil then
mappedTheme = themeMap[defaultTheme]
Logging.warn(string.format("Unrecognized theme name: `%s`", tostring(themeName)))
end
ArgCheck.assert(validateTheme(mappedTheme))
return mappedTheme
end
@@ -0,0 +1,33 @@
return function()
local getThemeFromName = require(script.Parent.getThemeFromName)
local Constants = require(script.Parent.Parent.Constants)
it("should be able to get a theme palette without errors", function()
local themeMap = {
[Constants.ThemeName.Dark] = require(script.Parent.DarkTheme),
}
local themeTable = getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark,themeMap)
expect(themeTable).to.be.a("table")
end)
it("should be able to get a theme palette using default without errors", function()
local themeMap = {
[Constants.ThemeName.Dark] = require(script.Parent.DarkTheme),
}
local themeTable = getThemeFromName("classic", Constants.ThemeName.Dark, themeMap)
expect(themeTable).to.be.a("table")
end)
it("should throw with invalid theme palette", function()
expect(function()
local themeMap = {
[Constants.ThemeName.Dark] = {
Background = {
Color = Color3.fromRGB(0, 0, 0),
Transparency = 0,
},
}
}
getThemeFromName(Constants.ThemeName.Dark, Constants.ThemeName.Dark, themeMap)
end).to.throw()
end)
end
@@ -0,0 +1,21 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
--- This file is a compatibility bridge between old and new api ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
local tutils = require(CorePackages.tutils)
return {
CheckListConsistency = tutils.checkListConsistency,
DeepEqual = tutils.deepEqual,
EqualKey = tutils.equalKey,
FieldCount = tutils.fieldCount,
ListDifference = tutils.listDifferences,
Print = tutils.print,
RecursiveToString = tutils.toString,
ShallowEqual = tutils.shallowEqual,
TableDifference = tutils.tableDifference,
}
@@ -0,0 +1,228 @@
return function()
local TableUtilities = require(script.Parent.TableUtilities)
local NotForProductionUse = game:GetService("CoreGui").RobloxGui.Modules.NotForProductionUse
local expectedFields = require(NotForProductionUse.UnitTestHelpers.expectedFields)
describe("alias wrapper", function()
it("SHOULD have all required fields", function()
expectedFields(TableUtilities, {
"CheckListConsistency",
"DeepEqual",
"EqualKey",
"FieldCount",
"ListDifference",
"Print",
"RecursiveToString",
"ShallowEqual",
"TableDifference",
})
end)
end)
describe("legacy tests", function()
it("should return whether tables are equal to each other", function()
local tableA = nil
local tableB = nil
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
tableA = nil
tableB = {}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
tableA = {}
tableB = nil
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
tableA = {}
tableB = {}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true)
tableA = {
key1 = "value1",
}
tableB = {
key1 = "value1",
}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true)
tableA = {
key1 = "value1",
}
tableB = {
key1 = "value2",
}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key2 = "value1",
}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key2 = "value2",
}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key1 = "value1",
key2 = "value2",
}
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
end)
it("should return whether tables are equal to each other at key", function()
local tableA = nil
local tableB = nil
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = nil
tableB = {}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = {}
tableB = nil
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = {}
tableB = {}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key1 = "value1",
}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true)
tableA = {
key1 = "value1",
}
tableB = {
key1 = "value2",
}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key2 = "value1",
}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key2 = "value2",
}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
tableA = {
key1 = "value1",
}
tableB = {
key1 = "value1",
key2 = "value2",
}
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true)
expect(TableUtilities.EqualKey(tableA, tableB, "key2")).to.equal(false)
end)
it("should return table's field count", function()
local t = {}
expect(TableUtilities.FieldCount(t)).to.equal(0)
t = {
key1 = "value1",
}
expect(TableUtilities.FieldCount(t)).to.equal(1)
t = {
key1 = "value1",
key2 = "value2",
}
expect(TableUtilities.FieldCount(t)).to.equal(2)
end)
describe("TableUtilities.DeepEqual", function()
it("works for primitve data types", function()
expect(TableUtilities.DeepEqual(1, 1)).to.equal(true)
expect(TableUtilities.DeepEqual("str1", "str1")).to.equal(true)
expect(TableUtilities.DeepEqual(1, 2)).to.equal(false)
expect(TableUtilities.DeepEqual("str1", "str2")).to.equal(false)
end)
it("correctly identifies deeply-equal tables", function()
local table1 = {
num = 1,
innerTable = {
innerString = "str"
}
}
local table2 = {
num = 1,
innerTable = {
innerString = "str"
}
}
expect(TableUtilities.DeepEqual(table1, table2)).to.equal(true)
end)
it("correctly rejects non-deeply-equal tables", function()
local table1 = {
num = 1,
innerTable = {
innerString = "str"
}
}
local table2 = {
num = 1,
innerTable = {
innerString = "differentStr"
}
}
expect(TableUtilities.DeepEqual(table1, table2)).to.equal(false)
local table3 = {
num = 1,
innerTable = {
innerString = "str"
}
}
local table4 = {
num = 1,
innerTableWithDifferentKey = {
innerString = "str"
}
}
expect(TableUtilities.DeepEqual(table3, table4)).to.equal(false)
end)
end)
end)
end
@@ -0,0 +1,5 @@
{
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,30 @@
local CorePackages = game:GetService("CorePackages")
local AppTempCommon = CorePackages.AppTempCommon
local Promise = require(AppTempCommon.LuaApp.Promise)
local ApiFetchThumbnails = require(AppTempCommon.LuaApp.Utils.ApiFetchThumbnails)
local GamesGetIcons = require(AppTempCommon.LuaApp.Http.Requests.GamesGetIcons)
local SetGameIcons = require(AppTempCommon.LuaApp.Actions.SetGameIcons)
local DEFAULT_ICON_SIZE = "150x150"
return function (networkImpl, universeIds, imageSize)
return function(store)
local state = store:getState()
local stateToCheckForDuplicates = state.GameIcons
-- Filter out the icons that are already in the store.
local idsToGet = {}
for _, targetId in pairs(universeIds) do
if stateToCheckForDuplicates[targetId] == nil then
table.insert(idsToGet, targetId)
end
end
if #idsToGet == 0 then
return Promise.resolve()
else
return ApiFetchThumbnails.Fetch(networkImpl,
idsToGet, imageSize or DEFAULT_ICON_SIZE, "Game", GamesGetIcons, SetGameIcons, store)
end
end
end
@@ -0,0 +1,91 @@
local CorePackages = game:GetService("CorePackages")
local LuaApp = CorePackages.AppTempCommon.LuaApp
local GamesGetThumbnails = require(LuaApp.Http.Requests.GamesGetThumbnails)
local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails)
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
local Promise = require(LuaApp.Promise)
local Result = require(LuaApp.Result)
local TableUtilities = require(LuaApp.TableUtilities)
local THUMBNAIL_PAGE_COUNT = 20
local THUMBNAIL_SIZE = 150
local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries"))
local RETRY_TIME_MULTIPLIER = 2 -- seconds
local function convertToId(value)
if type(value) ~= "number" and type(value) ~= "string" then
return Result.error("convertToId expects value passed in to be a number or a string")
end
return Result.success(tostring(value))
end
local function subdivideThumbnailTokenArray(thumbnailTokens, tokenLimit)
local someTokens = {}
for i = 1, #thumbnailTokens, tokenLimit do
local subArray = Functional.Take(thumbnailTokens, tokenLimit, i)
table.insert(someTokens, subArray)
end
return someTokens
end
local function fetchThumbnailBatch(networkImpl, store, thumbnailTokens)
return GamesGetThumbnails(networkImpl, thumbnailTokens, THUMBNAIL_SIZE, THUMBNAIL_SIZE):andThen(function(result)
local thumbnails = {}
local unfinalizedThumbnails = {}
for _,image in pairs(result.responseBody) do
local convertToIdResult = convertToId(image.universeId)
convertToIdResult:match(function(universeId)
if image.final == false then
unfinalizedThumbnails[universeId] = image.retryToken
else
-- index all of the thumbnails by universeId
thumbnails[universeId] = image
end
end, function(convertToIdError)
warn(convertToIdError)
end)
end
store:dispatch(SetGameThumbnails(thumbnails))
return Promise.resolve(unfinalizedThumbnails)
end)
end
local function fetchSubdividedThumbnailsArray(networkImpl, store, thumbnailTokens)
return fetchThumbnailBatch(networkImpl, store, thumbnailTokens):andThen(function(unfinalizedThumbnails)
local remainingUnfinalizedThumbnails = unfinalizedThumbnails
for retryCount = 1, RETRY_MAX_COUNT do
if TableUtilities.FieldCount(remainingUnfinalizedThumbnails) == 0 then
return -- Bail out, we're done!
end
wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1))
remainingUnfinalizedThumbnails = fetchThumbnailBatch(networkImpl, store, remainingUnfinalizedThumbnails):await()
end
end)
end
local function fetchThumbnails(networkImpl, thumbnailTokens)
return function(store)
-- NOTE : because the size of each thumbnail token, me must limit the number we can fetch at a time.
-- So break apart the array of tokens we get into smaller, more manageable pieces.
local fetchPromises = {}
local someTokens = subdivideThumbnailTokenArray(thumbnailTokens, THUMBNAIL_PAGE_COUNT)
for _, thumbsArr in ipairs(someTokens) do
local promise = fetchSubdividedThumbnailsArray(networkImpl, store, thumbsArr)
table.insert(fetchPromises, promise)
end
return Promise.all(fetchPromises)
end
end
return fetchThumbnails
@@ -0,0 +1,37 @@
local CorePackages = game:GetService("CorePackages")
local ApiFetchGameIcons = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGameIcons)
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
local GamesMultigetPlaceDetails = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GamesMultigetPlaceDetails)
local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel)
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags
local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString)
return function(networkImpl, placeIds)
return function(store)
if not placeIds or #placeIds == 0 then
return
end
return GamesMultigetPlaceDetails(networkImpl, placeIds):andThen(function(result)
local data = result.responseBody
local thumbnailUniverseIds = {}
local placeInfos = Functional.Map(data, function(placeInfoData)
local placeInfo = PlaceInfoModel.fromWeb(placeInfoData)
local universeId = convertUniverseIdToString(placeInfo.universeId)
table.insert(thumbnailUniverseIds, universeId)
return placeInfo
end)
store:dispatch(ReceivedPlacesInfos(placeInfos))
if #thumbnailUniverseIds > 0 then
store:dispatch(ApiFetchGameIcons(networkImpl, thumbnailUniverseIds))
end
return placeInfos
end)
end
end
@@ -0,0 +1,24 @@
local CorePackages = game:GetService("CorePackages")
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
local GetPlaceInfos = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GetPlaceInfos)
-- LuaChat
local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel)
local ReceivedMultiplePlaceInfos = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos)
return function(networkImpl, placeIds)
return function(store)
return GetPlaceInfos(networkImpl, placeIds):andThen(function(result)
local data = result.responseBody
local placeInfos = Functional.Map(data, function(placeInfoData)
return PlaceInfoModel.fromWeb(placeInfoData)
end)
store:dispatch(ReceivedMultiplePlaceInfos(placeInfos))
return placeInfos
end)
end
end
@@ -0,0 +1,29 @@
local CorePackages = game:GetService("CorePackages")
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
local UsersGetFriendCount = require(Requests.UsersGetFriendCount)
local SetFriendCount = require(Actions.SetFriendCount)
local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled)
return function(networkImpl)
return function(store)
return UsersGetFriendCount(networkImpl):andThen(function(result)
local data = result.responseBody
if isNewFriendsEndpointsEnabled() then
if data.count then
store:dispatch(SetFriendCount(data.count))
end
else
if data.success and data.count then
store:dispatch(SetFriendCount(data.count))
end
end
return data.count
end)
end
end
@@ -0,0 +1,68 @@
local CorePackages = game:GetService("CorePackages")
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(CorePackages.AppTempCommon.LuaApp.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)
return function(requestImpl, userId, thumbnailRequest, checkPoints)
return function(store)
store:dispatch(FetchUserFriendsStarted(userId))
if checkPoints ~= nil and checkPoints.startFetchUserFriends ~= nil then
checkPoints:startFetchUserFriends()
end
local fetchedUserIds = {}
return UsersGetFriends(requestImpl, userId):andThen(function(response)
local responseBody = response.responseBody
local newUsers = {}
for _, userData in pairs(responseBody.data) do
local id = tostring(userData.id)
userData.isFriend = true
local newUser = UserModel.fromDataTable(userData)
table.insert(fetchedUserIds, id)
newUsers[newUser.id] = newUser
end
store:dispatch(UpdateUsers(newUsers))
if checkPoints ~= nil and checkPoints.finishFetchUserFriends ~= nil then
checkPoints:finishFetchUserFriends()
end
return fetchedUserIds
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.Fetch(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(fetchedUserIds)
end,
function(response)
store:dispatch(FetchUserFriendsFailed(userId, response))
return Promise.reject(response)
end
)
end
end
@@ -0,0 +1,22 @@
local CorePackages = game:GetService("CorePackages")
local LuaApp = CorePackages.AppTempCommon.LuaApp
local ChatUtils = CorePackages.AppTempCommon.LuaChat.Utils
local getPlaceIds = require(ChatUtils.getFriendsActiveGamesPlaceIdsFromUsersPresence)
local receiveUsersPresence = require(ChatUtils.receiveUsersPresence)
local ApiFetchGamesDataByPlaceIds = require(LuaApp.Thunks.ApiFetchGamesDataByPlaceIds)
local UsersGetPresence = require(LuaApp.Http.Requests.UsersGetPresence)
return function(networkImpl, userIds)
return function(store)
return UsersGetPresence(networkImpl, userIds):andThen(function(result)
local userPresences = result.responseBody.userPresences
receiveUsersPresence(userPresences, store)
local placeIds = getPlaceIds(userPresences, store)
store:dispatch(ApiFetchGamesDataByPlaceIds(networkImpl, placeIds))
end)
end
end
@@ -0,0 +1,192 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
local ThumbnailsGetAvatar = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatar)
local ThumbnailsGetAvatarHeadshot = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.ThumbnailsGetAvatarHeadshot)
local AvatarThumbnailTypes = require(CorePackages.AppTempCommon.LuaApp.Enum.AvatarThumbnailTypes)
local SetUserThumbnail = require(Actions.SetUserThumbnail)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local PerformFetch = require(CorePackages.AppTempCommon.LuaApp.Thunks.Networking.Util.PerformFetch)
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries"))
local RETRY_TIME_MULTIPLIER = math.max(0, settings():GetFVariable("LuaAppThumbnailsApiRetryTimeMultiplier"))
local MAX_REQUEST_COUNT = 100
local ThumbnailsTypeToApiMap = {
[AvatarThumbnailTypes.AvatarThumbnail] = ThumbnailsGetAvatar,
[AvatarThumbnailTypes.HeadShot] = ThumbnailsGetAvatarHeadshot,
}
local function subdivideEntries(entries, limit)
local subArrays = {}
for i = 1, #entries, limit do
local subArray = Cryo.List.getRange(entries, i, i + limit - 1)
table.insert(subArrays, subArray)
end
return subArrays
end
local function keyMapper(userId, thumbnailType, thumbnailSize)
return "luaapp.usersthumbnailsapi." .. userId .. "." .. thumbnailType .. "." .. thumbnailSize
end
local function isCompleteThumbnailData(entry)
return type(entry) == "table"
and type(entry.targetId) == "number"
and type(entry.state) == "string"
and type(entry.imageUrl) == "string"
end
local ApiFetchUsersThumbnail = {}
function ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize)
assert(typeof(thumbnailSize) == "string",
string.format("ApiFetchUsersThumbnail expects a string for thumbnailSize. Type: %s", typeof(thumbnailSize))
)
assert(string.match(thumbnailSize, 'Size.+x'),
string.format(
"ApiFetchUsersThumbnail expects thumbnailSize to follow format \"Size..x..\" Current thumbnailSize: ",
thumbnailSize
)
)
return string.gsub(thumbnailSize, "Size", "")
end
function ApiFetchUsersThumbnail._fetch(networkImpl, listOfUserIds, thumbnailRequest)
local thumbnailSize = thumbnailRequest.thumbnailSize
local thumbnailType = thumbnailRequest.thumbnailType
local thumbnailSizeRequestArg = ApiFetchUsersThumbnail.getThumbnailsSizeArgForSize(thumbnailSize)
local thumbnailsApiForThumbnailType = ThumbnailsTypeToApiMap[thumbnailType]
assert(typeof(thumbnailType) == "string",
"ApiFetchUsersThumbnail expects thumbnailType to be a string")
assert(typeof(thumbnailsApiForThumbnailType) == "function",
"ApiFetchUsersThumbnail failed to find api for given type: ", thumbnailType)
local function keyMapperForCurrentTypeAndSize(userId)
return keyMapper(userId, thumbnailType, thumbnailSize)
end
local function getTableOfFailedResults(userIds)
local results = {}
for _, userId in pairs(userIds) do
local key = keyMapperForCurrentTypeAndSize(userId)
results[key] = Result.new(false, {
targetId = userId,
})
end
return results
end
return PerformFetch.Batch(listOfUserIds, keyMapperForCurrentTypeAndSize, function(store, userIdsToFetch)
local function fetchThumbnails(userIdsProvided)
return thumbnailsApiForThumbnailType(networkImpl, userIdsProvided, thumbnailSizeRequestArg):andThen(
function(result)
local results = getTableOfFailedResults(userIdsProvided)
local data = result and result.responseBody and result.responseBody.data
if typeof(data) == "table" then
for _, entry in pairs(data) do
if isCompleteThumbnailData(entry) then
local userId = tostring(entry.targetId)
local key = keyMapperForCurrentTypeAndSize(userId)
local success = false
if entry.state == "Completed" then
store:dispatch(SetUserThumbnail(tostring(entry.targetId), entry.imageUrl, thumbnailType, thumbnailSize))
success = true
end
results[key] = Result.new(success, entry)
end
end
end
return Promise.resolve(results)
end,
function(err)
local results = getTableOfFailedResults(userIdsProvided)
return Promise.resolve(results)
end
)
end
return fetchThumbnails(userIdsToFetch):andThen(function(results)
local completedThumbnails = {}
local thumbnailResults = results
if _G.__TESTEZ_RUNNING_TEST__ then
RETRY_MAX_COUNT = 1
RETRY_TIME_MULTIPLIER = 0.001
end
local function retry(retryCount)
local remainingUserIdsToFetch = {}
for key, result in pairs(thumbnailResults) do
local isSuccessful, thumbnailInfo = result:unwrap()
if isSuccessful and thumbnailInfo.state == "Completed" then
completedThumbnails[key] = result
elseif isSuccessful and thumbnailInfo.state == "Pending" then
table.insert(remainingUserIdsToFetch, thumbnailInfo.targetId)
end
end
if TableUtilities.FieldCount(remainingUserIdsToFetch) == 0 then
return Promise.resolve(completedThumbnails)
end
local delayPromise = Promise.new(function(resolve, reject)
coroutine.wrap(function()
wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1))
resolve()
end)()
end)
return delayPromise:andThen(function()
return fetchThumbnails(remainingUserIdsToFetch)
end):andThen(function(newResults)
thumbnailResults = newResults
if retryCount > 1 then
return retry(retryCount - 1)
else
return Promise.resolve(completedThumbnails)
end
end)
end
return retry(RETRY_MAX_COUNT)
end)
end)
end
function ApiFetchUsersThumbnail.Fetch(networkImpl, userIds, thumbnailRequests)
return function(store)
local allPromises = {}
local subArraysOfUserIds = subdivideEntries(userIds, MAX_REQUEST_COUNT)
for _, thumbnailRequest in pairs(thumbnailRequests) do
for _, limitedListOfUserIds in pairs(subArraysOfUserIds) do
local promise = store:dispatch(ApiFetchUsersThumbnail._fetch(networkImpl, limitedListOfUserIds, thumbnailRequest))
table.insert(allPromises, promise)
end
end
return PromiseUtilities.Batch(allPromises)
end
end
function ApiFetchUsersThumbnail.GetFetchingStatus(state, userId, thumbnailType, thumbnailSize)
return PerformFetch.GetStatus(state, keyMapper(userId, thumbnailType, thumbnailSize))
end
return ApiFetchUsersThumbnail
@@ -0,0 +1,44 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
local ChatSendMessage = require(Requests.ChatSendMessage)
local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation)
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local ChatSendGameLinkMessage = require(Requests.ChatSendGameLinkMessage)
return function(networkImpl, userId, placeInfo)
local clientId = Players.LocalPlayer.UserId
-- Construct the invite messages based on place info
local inviteTextMessage
inviteTextMessage = RobloxTranslator:FormatByKey(
"Feature.SettingsHub.Message.InviteToGameTitle", { PLACENAME = placeInfo.name }
)
return function(store)
return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(conversationResult)
local conversation = conversationResult.responseBody.conversation
return ChatSendMessage(networkImpl, conversation.id, inviteTextMessage):andThen(function()
local function handleResult(inviteResult)
local data = inviteResult.responseBody
return {
resultType = data.resultType,
conversationId = conversation.id,
placeId = placeInfo.universeRootPlaceId,
}
end
return ChatSendGameLinkMessage(networkImpl, conversation.id, placeInfo.universeId):andThen(handleResult)
end)
end)
end
end
@@ -0,0 +1,5 @@
{
"language": {
"mode": "nonstrict"
}
}
@@ -0,0 +1,245 @@
local CorePackages = game:GetService("CorePackages")
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
--[[
PerformFetch wraps the notion of a network request together with its fetching status
so that it is easier to de-duplicate concurrent requests for the same resource. The
fetching status for individual fetching operations are available in the store as:
storeState.FetchingStatus[key]
When you use one of the methods in this helper, you provide a key (or keymap), and
supply a functor that will only be called when a fetch actually needs to be performed.
Any follow-up andThen/catch clauses will be correctly daisy-chained onto the original
ongoing fetch request if one is already underway.
]]
local PerformFetch = {}
local batchPromises = {} -- fetch key = outstanding promise from PerformFetch.Batch
--[[
Helper function for unit tests to be able to clean up batchPromises created from
previous test case. This is because unit tests don't wait until the mock requests
are resolved and moves onto the next test. If tests happen to generate duplicate
fetchStatusKey, unresolved batchPromise will throw thinking that the promise does
not have the correct status.
]]
function PerformFetch.ClearOutstandingPromiseStatus()
batchPromises = {}
end
local function singleFetchKeymapper(item)
-- Single fetch keys are used directly
return item
end
--[[
Get the fetching status for a given status key. Defaults to
RetrievalStatus.NotStarted for missing keys.
]]
function PerformFetch.GetStatus(state, fetchStatusKey)
assert(typeof(state) == "table")
assert(typeof(fetchStatusKey) == "string")
assert(#fetchStatusKey > 0)
return state.FetchingStatus[fetchStatusKey] or RetrievalStatus.NotStarted
end
--[[
Perform a fetch operation for a single resource.
Args:
fetchStatusKey - String key for the fetching status to index the Rodux store.
fetchFunctor - Functor to call when a fetch needs to be performed for fetchStatusKey.
Returns:
A Promise that resolves or rejects in accordance with the result of fetchFunctor, or the
promise for the original fetch if one is already ongoing.
Usage:
In your main thunk, wrap your inner store function with this thunk, like this:
return function(arg1, arg2)
return PerformFetch.Single("mykey", function(store)
return doYourLogicHere() -- Must return a Promise!!!
end)
end
Please note that in order for single fetches to integrate well with batch fetches,
your promise must NEVER resolve or reject with multiple arguments! Wrap your results
in a table instead.
]]
function PerformFetch.Single(fetchStatusKey, fetchFunctor)
assert(typeof(fetchStatusKey) == "string")
assert(typeof(fetchFunctor) == "function")
assert(#fetchStatusKey > 0)
return function(store)
-- Call batch API to handle the individual fetch
return PerformFetch.Batch({ fetchStatusKey }, singleFetchKeymapper, function(batchStore, itemsToFetch)
assert(#itemsToFetch == 1)
local functorPromise = fetchFunctor(batchStore)
assert(Promise.is(functorPromise))
return functorPromise:andThen(function(...)
assert(#{...} <= 1)
return Promise.resolve({ [fetchStatusKey] = Result.new(true, (...)) })
end, function(...)
assert(#{...} <= 1)
return Promise.resolve({ [fetchStatusKey] = Result.new(false, (...)) })
end)
end)(store):andThen(function(batchResults)
local success, value = batchResults[fetchStatusKey]:unwrap()
if success then
return Promise.resolve(value)
else
return Promise.reject(value)
end
end)
end
end
--[[
Perform a fetch operation for multiple resources at once (batching).
Args:
items - The list of item ids that need to be fetched.
keyMapper - A function that maps items to string keys for the Rodux store.
fetchFunctor - A function that will be called when at least one item needs to be fetched.
Returns:
A Promise that always resolves. Result data is returned in a single table to the
andThen() clause, where item fetch keys are the keys and the results for each key
are encoded using a Result object.
Usage:
In your main thunk, wrap your inner store function with this thunk, like this:
local MAPPER = function(item)
return doSomething(item) -- Each key must be unique
end
return function(arg1, arg2)
local allItems = makeYourItemsList()
return PerformFetch.Batch(allItems, MAPPER, function(store, itemsToFetch)
return doYourLogicHere(itemsToFetch) -- Must return a Promise!!!
end)
end
Your implementation of fetchFunctor should return a promise that resolves
according to the structure of PromiseUtilities.Batch, ex:
return Promise.resolve({
itemFetchKey1 = Result.new(true, payload1),
itemFetchKey2 = Result.new(false, payload2), -- failed
})
Any other resolving arguments will be dropped for consistency and safety of the API.
Since this is a batching API, your implementation should NOT reject().
Please keep in mind that batching calls have to fit into an environment where they may
be daisy chained onto other batching calls, and those results have to be amalgamated
at the end of the chain into unique tables for each of the callers!
]]
function PerformFetch.Batch(items, keyMapper, fetchFunctor)
assert(typeof(items) == "table")
assert(typeof(keyMapper) == "function")
assert(typeof(fetchFunctor) == "function")
return function(store)
local itemsToFetch = {}
local itemsToFetchKeyMap = {}
local batchPromisesForItemsAlreadyBeingFetched = {}
-- Filter out items that do not need to be fetched
for _, item in ipairs(items) do
local fetchStatusKey = keyMapper(item)
local fetchingStatus = PerformFetch.GetStatus(store:getState(), fetchStatusKey)
local batchPromise = batchPromises[fetchStatusKey]
if batchPromise then
assert(fetchingStatus == RetrievalStatus.Fetching)
batchPromisesForItemsAlreadyBeingFetched[fetchStatusKey] = batchPromise
else
assert(fetchingStatus ~= RetrievalStatus.Fetching)
table.insert(itemsToFetch, item)
itemsToFetchKeyMap[item] = fetchStatusKey
end
end
local doResolve
local batchFetchingPromise = Promise.new(function(resolve)
doResolve = resolve
end)
-- Call functor if there are items to fetch, otherwise short-circuit it
-- We want to call it FIRST because we need to kick off async fetch before blocking
-- on other responses.
local functorPromise
if #itemsToFetch > 0 then
-- Place remaining items into fetching state and make entry in table before
-- we kick off functor just in case it returns already-completed promise
for _, fetchStatusKey in pairs(itemsToFetchKeyMap) do
store:dispatch(UpdateFetchingStatus(fetchStatusKey, RetrievalStatus.Fetching))
batchPromises[fetchStatusKey] = batchFetchingPromise
end
functorPromise = fetchFunctor(store, itemsToFetch)
assert(Promise.is(functorPromise))
else
functorPromise = Promise.resolve({})
end
functorPromise:andThen(function(myResults)
myResults = myResults or {} -- No resolve args = empty table for ease of use
return PromiseUtilities.Batch(batchPromisesForItemsAlreadyBeingFetched):andThen(function(batchResults)
local filteredResults = {}
for batchKey, batchResult in pairs(batchResults) do
-- Extract only the result for the key we care about from the batch results
local _, value = batchResult:unwrap()
filteredResults[batchKey] = value[batchKey]
end
return myResults, filteredResults
end)
end,
function()
assert(false, "PerformFetch fetchFunctor should never reject")
end):andThen(function(myResults, batchResults)
-- Iterate on requested items rather than on actual result set
-- so that we are sure to check all our keys and ignore extra ones
for _, fetchKey in pairs(itemsToFetchKeyMap) do
local resultObj = myResults[fetchKey]
if Result.is(resultObj) then
batchResults[fetchKey] = resultObj
else
batchResults[fetchKey] = Result.error()
end
-- Update fetching status in store from Result object status
-- (The extra parens unwrap a multi-return value!)
local itemStatus = (batchResults[fetchKey]:unwrap()) and RetrievalStatus.Done or RetrievalStatus.Failed
store:dispatch(UpdateFetchingStatus(fetchKey, itemStatus))
batchPromises[fetchKey] = nil
end
return batchResults
end):andThen(function(joinedResults)
doResolve(joinedResults)
end)
return batchFetchingPromise
end
end
return PerformFetch

Some files were not shown because too many files have changed in this diff Show More