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,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,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,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,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,15 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
local FIntLuaHomePageEnablePlacesListV1 = settings():GetFVariable("LuaHomePageEnablePlacesListV1V361")
-- 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(FIntLuaHomePageEnablePlacesListV1)
local userId = Players.LocalPlayer.UserId
return ThrottleUserId(throttleNumber, userId)
end
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local FFlagPlacesListV1 = require(CorePackages.AppTempCommon.LuaApp.Flags.IsPlacesListV1Enabled)
local FFlagLuaHomePeopleListV1V361 = settings():GetFFlag("LuaHomePeopleListV1V361")
return function()
-- When we should fetch games data could change in the future
-- Right now, we should fetch games data when people list on or place list on
return FFlagLuaHomePeopleListV1V361 or FFlagPlacesListV1()
end
@@ -0,0 +1,15 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, conversationId, messageText)
local payload = HttpService:JSONEncode({
conversationId = conversationId,
message = messageText,
})
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,48 @@
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,30 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
This endpoint returns a promise that resolves to:
[
{
"success:" true,
"count": "0"
},
]
]]--
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
return function(requestImpl)
local argTable = {
userId = Players.LocalPlayer.UserId,
}
local args = Url:makeQueryString(argTable)
local url = string.format("%s/user/get-friendship-count?%s",
Url.API_URL, tostring(Players.LocalPlayer.UserId), args
)
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(
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,114 @@
--[[
Url Constructor
Provides a single location for base urls.
]]--
local ContentProvider = game:GetService("ContentProvider")
-- 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 _baseAuthUrl = string.format("https://auth.%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 _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)
-- public api
local Url = {
DOMAIN = _baseDomain,
PREFIX = _basePrefix,
BASE_URL = _baseUrl,
API_URL = _baseApiUrl,
AUTH_URL = _baseAuthUrl,
GAME_URL = _baseGamesUrl,
GAME_ASSET_URL = _baseGameAssetUrl,
CHAT_URL = _baseChatUrl,
FRIEND_URL = _baseFriendUrl,
PRESENCE_URL = _basePresenceUrl,
NOTIFICATION_URL = _baseNotificationUrl,
REALTIME_URL = _baseRealtimeUrl,
WEB_URL = _baseWebUrl,
WWW_URL = _baseWwwUrl,
ADS_URL = _baseAdsUrl,
}
function Url:getUserProfileUrl(userId)
return string.format("%susers/%s/profile", self.BASE_URL, userId)
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,10 @@
--[[
A function to return a fake ID, used for testing
]]
local lastId = 0
return function()
lastId = lastId + 1
return ("MOCK-%d"):format(lastId)
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,117 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss")
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.lastOnline = 0
self.presence = User.PresenceType.OFFLINE
self.membership = nil
if not FFlagFixUsersReducerDataLoss then
self.thumbnails = {}
end
return self
end
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.lastOnline = 0
if FFlagFixUsersReducerDataLoss then
self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil
else
self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or
User.PresenceType.OFFLINE
self.thumbnails = {}
end
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,68 @@
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)
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,343 @@
--[[
An implementation of Promises similar to Promise/A+.
]]
local TU = require(script.Parent.TableUtilities)
local PROMISE_DEBUG = true
-- If promise debugging is on, use a version of pcall that warns on failure.
-- This is useful for finding errors that happen within Promise itself.
local wpcall
if PROMISE_DEBUG then
wpcall = function(f, ...)
local result = { pcall(f, ...) }
if not result[1] then
warn(result[2])
end
return unpack(result)
end
else
wpcall = pcall
end
--[[
Creates a function that invokes a callback with correct error handling and
resolution mechanisms.
]]
local function createAdvancer(callback, resolve, reject)
return function(...)
local result = { wpcall(callback, ...) }
local ok = table.remove(result, 1)
if ok then
resolve(unpack(result))
else
reject(unpack(result))
end
end
end
local function isEmpty(t)
return next(t) == nil
end
local Promise = {}
Promise.__index = Promise
Promise.Status = {
Started = "Started",
Resolved = "Resolved",
Rejected = "Rejected",
}
--[[
Constructs a new Promise with the given initializing callback.
This is generally only called when directly wrapping a non-promise API into
a promise-based version.
The callback will receive 'resolve' and 'reject' methods, used to start
invoking the promise chain.
For example:
local function get(url)
return Promise.new(function(resolve, reject)
spawn(function()
resolve(HttpService:GetAsync(url))
end)
end)
end
get("https://google.com")
:andThen(function(stuff)
print("Got some stuff!", stuff)
end)
]]
function Promise.new(callback)
local promise = {
-- Used to locate where a promise was created
_source = debug.traceback(),
-- A tag to identify us as a promise
_type = "Promise",
_status = Promise.Status.Started,
-- A table containing a list of all results, whether success or failure.
-- Only valid if _status is set to something besides Started
_value = nil,
-- Queues representing functions we should invoke when we update!
_queuedResolve = {},
_queuedReject = {},
}
setmetatable(promise, Promise)
local function resolve(...)
promise:_resolve(...)
end
local function reject(...)
promise:_reject(...)
end
local ok, err = wpcall(callback, resolve, reject)
if not ok and promise._status == Promise.Status.Started then
reject(err)
end
return promise
end
--[[
Create a promise that represents the immediately resolved value.
]]
function Promise.resolve(value)
return Promise.new(function(resolve)
resolve(value)
end)
end
--[[
Create a promise that represents the immediately rejected value.
]]
function Promise.reject(value)
return Promise.new(function(_, reject)
reject(value)
end)
end
--[[
Returns a new promise that:
* is resolved when all input promises resolve
* is rejected if ANY input promises reject
]]
function Promise.all(...)
local promises = {...}
-- check if we've been given a list of promises, not just a variable number of promises
if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then
-- we've been given a table of promises already
promises = promises[1]
end
return Promise.new(function(resolve, reject)
local isResolved = false
local results = {}
local totalCompleted = 0
local function promiseCompleted(index, result)
if isResolved then
return
end
results[index] = result
totalCompleted = totalCompleted + 1
if totalCompleted == #promises then
resolve(results)
isResolved = true
end
end
if #promises == 0 then
resolve(results)
isResolved = true
return
end
for index, promise in ipairs(promises) do
-- if a promise isn't resolved yet, add listeners for when it does
if promise._status == Promise.Status.Started then
promise:andThen(function(result)
promiseCompleted(index, result)
end):catch(function(reason)
isResolved = true
reject(reason)
end)
-- if a promise is already resolved, move on
elseif promise._status == Promise.Status.Resolved then
promiseCompleted(index, unpack(promise._value))
-- if a promise is rejected, reject the whole chain
else --if promise._status == Promise.Status.Rejected then
isResolved = true
reject(unpack(promise._value))
end
end
end)
end
--[[
Is the given object a Promise instance?
]]
function Promise.is(object)
if type(object) ~= "table" then
return false
end
return object._type == "Promise"
end
--[[
Creates a new promise that receives the result of this promise.
The given callbacks are invoked depending on that result.
]]
function Promise:andThen(successHandler, failureHandler)
-- Create a new promise to follow this part of the chain
return Promise.new(function(resolve, reject)
-- Our default callbacks just pass values onto the next promise.
-- This lets success and failure cascade correctly!
local successCallback = resolve
if successHandler then
successCallback = createAdvancer(successHandler, resolve, reject)
end
local failureCallback = reject
if failureHandler then
failureCallback = createAdvancer(failureHandler, resolve, reject)
end
if self._status == Promise.Status.Started then
-- If we haven't resolved yet, put ourselves into the queue
table.insert(self._queuedResolve, successCallback)
table.insert(self._queuedReject, failureCallback)
elseif self._status == Promise.Status.Resolved then
-- This promise has already resolved! Trigger success immediately.
successCallback(unpack(self._value))
elseif self._status == Promise.Status.Rejected then
-- This promise died a terrible death! Trigger failure immediately.
failureCallback(unpack(self._value))
end
end)
end
--[[
Used to catch any errors that may have occurred in the promise.
]]
function Promise:catch(failureCallback)
return self:andThen(nil, failureCallback)
end
--[[
Yield until the promise is completed.
This matches the execution model of normal Roblox functions.
]]
function Promise:await()
if self._status == Promise.Status.Started then
local result
local bindable = Instance.new("BindableEvent")
self:andThen(function(...)
result = {...}
bindable:Fire(true)
end, function(...)
result = {...}
bindable:Fire(false)
end)
local ok = bindable.Event:Wait()
bindable:Destroy()
if not ok then
error(tostring(result[1]), 2)
end
return unpack(result)
elseif self._status == Promise.Status.Resolved then
return unpack(self._value)
elseif self._status == Promise.Status.Rejected then
error(tostring(self._value[1]), 2)
end
end
function Promise:_resolve(...)
if self._status ~= Promise.Status.Started then
return
end
-- If the resolved value was a Promise, we chain onto it!
if Promise.is((...)) then
-- Without this warning, arguments sometimes mysteriously disappear
if select("#", ...) > 1 then
local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format(
self._source
)
warn(message)
end
(...):andThen(function(...)
self:_resolve(...)
end, function(...)
self:_reject(...)
end)
return
end
self._status = Promise.Status.Resolved
self._value = {...}
-- We assume that these callbacks will not throw errors.
for _, callback in ipairs(self._queuedResolve) do
callback(...)
end
end
function Promise:_reject(...)
if self._status ~= Promise.Status.Started then
return
end
self._status = Promise.Status.Rejected
self._value = {...}
-- If there are any rejection handlers, call those!
if not isEmpty(self._queuedReject) then
-- We assume that these callbacks will not throw errors.
for _, callback in ipairs(self._queuedReject) do
callback(...)
end
else
-- At this point, no one was able to observe the error.
-- An error handler might still be attached if the error occurred
-- synchronously. We'll wait one tick, and if there are still no
-- observers, then we should put a message in the console.
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
TU.RecursiveToString((...)),
self._source
)
warn(message)
end
end
return Promise
@@ -0,0 +1,331 @@
return function()
local Promise = require(script.Parent.Promise)
describe("Promise.new", function()
it("should instantiate with a callback", function()
local promise = Promise.new(function() end)
expect(promise).to.be.ok()
end)
it("should invoke the given callback with resolve and reject", function()
local callCount = 0
local resolveArg
local rejectArg
local promise = Promise.new(function(resolve, reject)
callCount = callCount + 1
resolveArg = resolve
rejectArg = reject
end)
expect(promise).to.be.ok()
expect(callCount).to.equal(1)
expect(resolveArg).to.be.a("function")
expect(rejectArg).to.be.a("function")
expect(promise._status).to.equal(Promise.Status.Started)
end)
it("should resolve promises on resolve()", function()
local callCount = 0
local promise = Promise.new(function(resolve)
callCount = callCount + 1
resolve()
end)
expect(promise).to.be.ok()
expect(callCount).to.equal(1)
expect(promise._status).to.equal(Promise.Status.Resolved)
end)
it("should reject promises on reject()", function()
local callCount = 0
local promise = Promise.new(function(resolve, reject)
callCount = callCount + 1
reject()
end)
expect(promise).to.be.ok()
expect(callCount).to.equal(1)
expect(promise._status).to.equal(Promise.Status.Rejected)
end)
it("should reject on error in callback", function()
local callCount = 0
local promise = Promise.new(function()
callCount = callCount + 1
error("hahah")
end)
expect(promise).to.be.ok()
expect(callCount).to.equal(1)
expect(promise._status).to.equal(Promise.Status.Rejected)
expect(promise._value[1]:find("hahah")).to.be.ok()
end)
end)
describe("Promise.resolve", function()
it("should immediately resolve with a value", function()
local promise = Promise.resolve(5)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
expect(promise._value[1]).to.equal(5)
end)
it("should chain onto passed promises", function()
local promise = Promise.resolve(Promise.new(function(_, reject)
reject(7)
end))
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
expect(promise._value[1]).to.equal(7)
end)
end)
describe("Promise.reject", function()
it("should immediately reject with a value", function()
local promise = Promise.reject(6)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
expect(promise._value[1]).to.equal(6)
end)
it("should pass a promise as-is as an error", function()
local innerPromise = Promise.new(function(resolve)
resolve(6)
end)
local promise = Promise.reject(innerPromise)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
expect(promise._value[1]).to.equal(innerPromise)
end)
end)
describe("Promise:andThen", function()
it("should chain onto resolved promises", function()
local args
local argsLength
local callCount = 0
local badCallCount = 0
local promise = Promise.resolve(5)
local chained = promise
:andThen(function(...)
args = {...}
argsLength = select("#", ...)
callCount = callCount + 1
end, function()
badCallCount = badCallCount + 1
end)
expect(badCallCount).to.equal(0)
expect(callCount).to.equal(1)
expect(argsLength).to.equal(1)
expect(args[1]).to.equal(5)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
expect(promise._value[1]).to.equal(5)
expect(chained).to.be.ok()
expect(chained).never.to.equal(promise)
expect(chained._status).to.equal(Promise.Status.Resolved)
expect(#chained._value).to.equal(0)
end)
it("should chain onto rejected promises", function()
local args
local argsLength
local callCount = 0
local badCallCount = 0
local promise = Promise.reject(5)
local chained = promise
:andThen(function(...)
badCallCount = badCallCount + 1
end, function(...)
args = {...}
argsLength = select("#", ...)
callCount = callCount + 1
end)
expect(badCallCount).to.equal(0)
expect(callCount).to.equal(1)
expect(argsLength).to.equal(1)
expect(args[1]).to.equal(5)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
expect(promise._value[1]).to.equal(5)
expect(chained).to.be.ok()
expect(chained).never.to.equal(promise)
expect(chained._status).to.equal(Promise.Status.Resolved)
expect(#chained._value).to.equal(0)
end)
it("should chain onto asynchronously resolved promises", function()
local args
local argsLength
local callCount = 0
local badCallCount = 0
local startResolution
local promise = Promise.new(function(resolve)
startResolution = resolve
end)
local chained = promise
:andThen(function(...)
args = {...}
argsLength = select("#", ...)
callCount = callCount + 1
end, function()
badCallCount = badCallCount + 1
end)
expect(callCount).to.equal(0)
expect(badCallCount).to.equal(0)
startResolution(6)
expect(badCallCount).to.equal(0)
expect(callCount).to.equal(1)
expect(argsLength).to.equal(1)
expect(args[1]).to.equal(6)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
expect(promise._value[1]).to.equal(6)
expect(chained).to.be.ok()
expect(chained).never.to.equal(promise)
expect(chained._status).to.equal(Promise.Status.Resolved)
expect(#chained._value).to.equal(0)
end)
it("should chain onto asynchronously rejected promises", function()
local args
local argsLength
local callCount = 0
local badCallCount = 0
local startResolution
local promise = Promise.new(function(_, reject)
startResolution = reject
end)
local chained = promise
:andThen(function()
badCallCount = badCallCount + 1
end, function(...)
args = {...}
argsLength = select("#", ...)
callCount = callCount + 1
end)
expect(callCount).to.equal(0)
expect(badCallCount).to.equal(0)
startResolution(6)
expect(badCallCount).to.equal(0)
expect(callCount).to.equal(1)
expect(argsLength).to.equal(1)
expect(args[1]).to.equal(6)
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
expect(promise._value[1]).to.equal(6)
expect(chained).to.be.ok()
expect(chained).never.to.equal(promise)
expect(chained._status).to.equal(Promise.Status.Resolved)
expect(#chained._value).to.equal(0)
end)
end)
describe("Promise.all", function()
it("should resolve immediately with an empty table argument", function()
local promise = Promise.all({})
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
local result = unpack(promise._value)
expect(type(result)).to.equal("table")
expect(#result).to.equal(0)
end)
it("should resolve immediately with no arguments", function()
local promise = Promise.all()
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
local result = unpack(promise._value)
expect(type(result)).to.equal("table")
expect(#result).to.equal(0)
end)
it("should resolve with one result with one resolved argument", function()
local promise = Promise.all(Promise.resolve(7))
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
local result = unpack(promise._value)
expect(type(result)).to.equal("table")
expect(#result).to.equal(1)
expect(result[1]).to.equal(7)
end)
it("should resolve with multiple resolved arguments", function()
local promise = Promise.all(Promise.resolve(7), Promise.resolve(4))
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Resolved)
local result = unpack(promise._value)
expect(type(result)).to.equal("table")
expect(#result).to.equal(2)
expect(result[1]).to.equal(7)
expect(result[2]).to.equal(4)
end)
it("should reject with one rejected argument", function()
local promise = Promise.all(Promise.reject(5))
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
local result = unpack(promise._value)
expect(result).to.equal(5)
end)
it("should reject with one success followed by one rejected argument", function()
local promise = Promise.all(Promise.resolve(7), Promise.reject(4))
expect(promise).to.be.ok()
expect(promise._status).to.equal(Promise.Status.Rejected)
local result = unpack(promise._value)
expect(result).to.equal(4)
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,16 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
return function(state, action)
state = state or {}
if action.type == ReceivedPlacesInfos.name then
for _, placeInfo in pairs(action.placesInfos) do
state = Immutable.Set(state, tostring(placeInfo.universeId), placeInfo)
end
end
return state
end
@@ -0,0 +1,117 @@
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)
local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss")
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
if FFlagFixUsersReducerDataLoss 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)
else
local users = action.users
state = Immutable.JoinDictionaries(state, users)
end
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
if FFlagFixUsersReducerDataLoss 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,
}),
}),
}),
})
else
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
thumbnails = Immutable.JoinDictionaries(user.thumbnails, {
[action.thumbnailType] = Immutable.JoinDictionaries(user.thumbnails[action.thumbnailType] or {}, {
[action.thumbnailSize] = action.image,
}),
}),
}),
})
end
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,100 @@
local Result = {}
Result.__index = Result
local ResultTypeSymbol = newproxy(true)
function Result.new(status, value)
assert(typeof(status) == "boolean")
local result = {
-- Used to locate where a result was created
_source = debug.traceback(),
-- A tag to identify us as a result
[ResultTypeSymbol] = true,
_status = status,
-- The value success value or error message.
_value = value,
}
setmetatable(result, Result)
return result
end
function Result.success(value)
return Result.new(true, value)
end
function Result.error(value)
return Result.new(false, value)
end
--[[
Is the given object a Result instance?
]]
function Result.is(object)
if typeof(object) ~= "table" then
return false
end
return object[ResultTypeSymbol]
end
--[[
The given callbacks are invoked depending on that result.
Creates a new result that receives the output of the callback.
]]
function Result:match(successHandler, errorHandler)
assert(typeof(successHandler) == "function" or typeof(successHandler) == "nil",
string.format("Result:match expects successHandler to be a function or nil, got %s", typeof(successHandler)))
assert(typeof(errorHandler) == "function" or typeof(errorHandler) == "nil",
string.format("Result:match expects errorHandler to be a function or nil, got %s", typeof(errorHandler)))
local newResult
if self._status then
if successHandler ~= nil then
newResult = successHandler(self._value)
else
return self
end
else
if errorHandler ~= nil then
newResult = errorHandler(self._value)
else
return self
end
end
if Result.is(newResult) then
return newResult
else
return Result.success(newResult)
end
end
--[[
The given callback is invoked if the result is success.
Creates a new result that receives the output of the callback.
]]
function Result:matchSuccess(successHandler)
return self:match(successHandler, nil)
end
--[[
The given callback is invoked if the result is error.
Creates a new result that receives the output of the callback.
]]
function Result:matchError(errorHandler)
return self:match(nil, errorHandler)
end
function Result:unwrap()
return self._status, self._value
end
return Result
@@ -0,0 +1,217 @@
return function()
local Result = require(script.Parent.Result)
describe("Constructors", function()
it("should return a success result from Result.new with a success value", function()
local success, value = Result.new(true, "foo"):unwrap()
expect(success).to.equal(true)
expect(value).to.equal("foo")
end)
it("should return an error result from Result.new with a failure value", function()
local success, value = Result.new(false, "foo"):unwrap()
expect(success).to.equal(false)
expect(value).to.equal("foo")
end)
it("should return a success result from Result.success", function()
local success, value = Result.success("foo"):unwrap()
expect(success).to.equal(true)
expect(value).to.equal("foo")
end)
it("should return an error result from Result.error", function()
local success, value = Result.error("foo"):unwrap()
expect(success).to.equal(false)
expect(value).to.equal("foo")
end)
end)
describe("Result:match", function()
it("should call the first callback with the value if it's a success result", function()
local called = false
Result.success("foo"):match(
function(value)
called = true
expect(value).to.equal("foo")
end,
function(value)
assert(false)
end
)
expect(called).to.equal(true)
end)
it("should call the second callback with the error if it's an error result", function()
local called = false
Result.error("foo"):match(
function(value)
assert(false)
end,
function(value)
expect(value).to.equal("foo")
called = true
end
)
expect(called).to.equal(true)
end)
it("should return the result of the first callback if it's a result", function()
Result.success("foo"):match(
function()
return Result.success("bar")
end,
nil
):match(
function(value)
expect(value).to.equal("bar")
end,
nil
)
end)
it("should return the result of the second callback if it's a result", function()
Result.error("foo"):match(
nil,
function()
return Result.success("bar")
end
):match(
function(value)
expect(value).to.equal("bar")
end,
nil
)
end)
it("should return self if it's success and the first callback isn't provided", function()
local result1 = Result.success("foo")
local result2 = result1:match(nil, nil)
expect(result1).to.equal(result2)
end)
it("should return self if it's error and the second callback isn't provided", function()
local result1 = Result.error("foo")
local result2 = result1:match(nil, nil)
expect(result1).to.equal(result2)
end)
it("should return success result wrapping value returned by first callback if not a result", function()
Result.success("foo"):match(
function()
return "bar"
end,
nil
):match(
function(value)
expect(value).to.equal("bar")
end,
nil
)
end)
it("should return success result wrapping value returned by second callback if not a result", function()
Result.error("foo"):match(
nil,
function()
return "bar"
end
):match(
function(value)
expect(value).to.equal("bar")
end,
nil
)
end)
end)
describe("Result:matchSuccess", function()
it("should call the callback with the value if it's a success result", function()
local called = false
Result.success("foo"):matchSuccess(
function(value)
expect(value).to.equal("foo")
called = true
end
)
expect(called).to.equal(true)
end)
it("should not call the callback if it's an error result", function()
Result.error("foo"):matchSuccess(
function(value)
assert(false)
end
)
end)
it("should return the result of the callback if it's a result", function()
Result.success("foo"):matchSuccess(
function()
return Result.success("bar")
end
):matchSuccess(
function(value)
expect(value).to.equal("bar")
end
)
end)
it("should return success result wrapping value returned by callback if not a result", function()
Result.success("foo"):matchSuccess(
function()
return "bar"
end
):matchSuccess(
function(value)
expect(value).to.equal("bar")
end
)
end)
end)
describe("Result:matchError", function()
it("should call the callback with the value if it's an error result", function()
local called = false
Result.error("foo"):matchError(
function(value)
expect(value).to.equal("foo")
called = true
end
)
expect(called).to.equal(true)
end)
it("should not call the callback if it's a success result", function()
Result.success("foo"):matchError(
function(value)
assert(false)
end
)
end)
it("should return the result of the callback if it's a result", function()
Result.error("foo"):matchError(
function()
return Result.success("bar")
end
):matchSuccess(
function(value)
expect(value).to.equal("bar")
end
)
end)
it("should return success result wrapping value returned by callback if not a result", function()
Result.error("foo"):matchError(
function()
return "bar"
end
):matchSuccess(
function(value)
expect(value).to.equal("bar")
end
)
end)
end)
end
@@ -0,0 +1,184 @@
--[[
Provides functions for comparing and printing lua tables.
]]
local TableUtilities = {}
local defaultIgnore = {}
--[[
Takes two tables A and B, returns if they have the same key-value pairs
Except ignored keys
]]
function TableUtilities.ShallowEqual(A, B, ignore)
if not A or not B then
return false
elseif A == B then
return true
end
if not ignore then
ignore = defaultIgnore
end
for key, value in pairs(A) do
if B[key] ~= value and not ignore[key] then
return false
end
end
for key, value in pairs(B) do
if A[key] ~= value and not ignore[key] then
return false
end
end
return true
end
--[[
Takes two tables A, B and a key, returns if two tables have the same value at key
]]
function TableUtilities.EqualKey(A, B, key)
if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then
return true
end
return false
end
--[[
Takes two tables A and B, returns a new table with elements of A
which are either not keys in B or have a different value in B
]]
function TableUtilities.TableDifference(A, B)
local new = {}
for key, value in pairs(A) do
if B[key] ~= A[key] then
new[key] = value
end
end
return new
end
--[[
Takes a list and returns a table whose
keys are elements of the list and whose
values are all true
]]
local function membershipTable(list)
local result = {}
for i = 1, #list do
result[list[i]] = true
end
return result
end
--[[
Takes a table and returns a list of keys in that table
]]
local function listOfKeys(t)
local result = {}
for key,_ in pairs(t) do
table.insert(result, key)
end
return result
end
--[[
Takes two lists A and B, returns a new list of elements of A
which are not in B
]]
function TableUtilities.ListDifference(A, B)
return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B)))
end
--[[
For debugging. Returns false if the given table has any of the following:
- a key that is neither a number or a string
- a mix of number and string keys
- number keys which are not exactly 1..#t
]]
function TableUtilities.CheckListConsistency(t)
local containsNumberKey = false
local containsStringKey = false
local numberConsistency = true
local index = 1
for x, _ in pairs(t) do
if type(x) == 'string' then
containsStringKey = true
elseif type(x) == 'number' then
if index ~= x then
numberConsistency = false
end
containsNumberKey = true
else
return false
end
if containsStringKey and containsNumberKey then
return false
end
index = index + 1
end
if containsNumberKey then
return numberConsistency
end
return true
end
--[[
For debugging, serializes the given table to a reasonable string that might even interpret as lua.
]]
function TableUtilities.RecursiveToString(t, indent)
indent = indent or ''
if type(t) == 'table' then
local result = ""
if not TableUtilities.CheckListConsistency(t) then
result = result .. "-- WARNING: this table fails the list consistency test\n"
end
result = result .. "{\n"
for k,v in pairs(t) do
if type(k) == 'string' then
result = result
.. " "
.. indent
.. tostring(k)
.. " = "
.. TableUtilities.RecursiveToString(v, " "..indent)
..";\n"
end
if type(k) == 'number' then
result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n"
end
end
result = result .. indent .. "}"
return result
else
return tostring(t)
end
end
--[[
Takes a table and returns the field count
]]
function TableUtilities.FieldCount(t)
local fieldCount = 0
for _ in pairs(t) do
fieldCount = fieldCount + 1
end
return fieldCount
end
return TableUtilities
@@ -0,0 +1,156 @@
return function()
local TableUtilities = require(script.Parent.TableUtilities)
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 table = {}
expect(TableUtilities.FieldCount(table)).to.equal(0)
table = {
key1 = "value1",
}
expect(TableUtilities.FieldCount(table)).to.equal(1)
table = {
key1 = "value1",
key2 = "value2",
}
expect(TableUtilities.FieldCount(table)).to.equal(2)
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,40 @@
local CorePackages = game:GetService("CorePackages")
local ApiFetchGameThumbnails = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGameThumbnails)
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)
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 imageTokens = {}
local placeInfos = Functional.Map(data, function(placeInfoData)
local placeInfo = PlaceInfoModel.fromWeb(placeInfoData)
local universePlaceInfos = store:getState().UniversePlaceInfos
local universeId = placeInfo.universeRootPlaceId
if not universePlaceInfos[universeId] or placeInfo.imageToken ~= universePlaceInfos[universeId].imageToken then
table.insert(imageTokens, placeInfo.imageToken)
end
return placeInfo
end)
store:dispatch(ReceivedPlacesInfos(placeInfos))
if #imageTokens > 0 then
store:dispatch(ApiFetchGameThumbnails(networkImpl, imageTokens))
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,21 @@
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)
return function(networkImpl)
return function(store)
return UsersGetFriendCount(networkImpl):andThen(function(result)
local data = result.responseBody
if data.success and data.count then
store:dispatch(SetFriendCount(data.count))
end
return data.count
end)
end
end
@@ -0,0 +1,63 @@
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 ApiFetchUsersFriendCount = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersFriendCount)
local UsersGetFriends = require(Requests.UsersGetFriends)
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User)
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
local LuaAppRemoveGetFriendshipCountApiCalls = settings():GetFFlag("LuaAppRemoveGetFriendshipCountApiCalls")
local homePageDataFetchRefactor = settings():GetFFlag('LuaHomePageDataFetchRefactor')
return function(requestImpl, userId, thumbnailRequest)
return function(store)
store:dispatch(FetchUserFriendsStarted(userId))
if not LuaAppRemoveGetFriendshipCountApiCalls then
store:dispatch(ApiFetchUsersFriendCount(requestImpl))
end
return UsersGetFriends(requestImpl, userId):andThen(function(response)
local responseBody = response.responseBody
local userIds = {}
local newUsers = {}
for _, userData in pairs(responseBody.data) do
local id = tostring(userData.id)
local newUser = UserModel.fromData(id, userData.name, true)
table.insert(userIds, id)
newUsers[newUser.id] = newUser
end
if LuaAppRemoveGetFriendshipCountApiCalls then
store:dispatch(UpdateUsers(newUsers))
else
store:dispatch(AddUsers(newUsers))
end
return userIds
end):andThen(function(userIds)
-- Asynchronously fetch friend thumbnails so we don't block display of UI
store:dispatch(ApiFetchUsersThumbnail(requestImpl, userIds, thumbnailRequest))
return store:dispatch(ApiFetchUsersPresences(requestImpl, userIds))
end):andThen(
function(result)
store:dispatch(FetchUserFriendsCompleted(userId))
if homePageDataFetchRefactor then
return Promise.resolve(result)
end
end,
function(response)
store:dispatch(FetchUserFriendsFailed(userId, response))
if homePageDataFetchRefactor then
return Promise.reject(response)
end
end
)
end
end
@@ -0,0 +1,26 @@
local CorePackages = game:GetService("CorePackages")
local Utils = CorePackages.AppTempCommon.LuaChat.Utils
local getPlaceIds = require(Utils.getFriendsActiveGamesPlaceIdsFromUsersPresence)
local receiveUsersPresence = require(Utils.receiveUsersPresence)
local ApiFetchGamesDataByPlaceIds = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGamesDataByPlaceIds)
local UsersGetPresence = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.UsersGetPresence)
local FFlagLuaHomeGetFriendsPlayingGamesInfo = settings():GetFFlag("LuaHomeGetFriendsPlayingGamesInfo")
return function(networkImpl, userIds)
return function(store)
return UsersGetPresence(networkImpl, userIds):andThen(function(result)
local userPresences = result.responseBody.userPresences
receiveUsersPresence(userPresences, store)
if FFlagLuaHomeGetFriendsPlayingGamesInfo then
local placeIds = getPlaceIds(userPresences, store)
store:dispatch(ApiFetchGamesDataByPlaceIds(networkImpl, placeIds))
end
end)
end
end
@@ -0,0 +1,40 @@
local CorePackages = game:GetService("CorePackages")
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
local UsersGetThumbnail = require(Requests.UsersGetThumbnail)
local SetUserThumbnail = require(Actions.SetUserThumbnail)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local function fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest)
local fetchedPromises = {}
for _, userId in pairs(userIds) do
table.insert(fetchedPromises,
UsersGetThumbnail(userId, thumbnailRequest.thumbnailType, thumbnailRequest.thumbnailSize)
)
end
return Promise.all(fetchedPromises)
end
return function(networkImpl, userIds, thumbnailRequests)
return function(store)
-- We currently cannot batch request user avatar thumbnails,
-- so each thumbnailRequest has to be processed individually.
local fetchedPromises = {}
for _, thumbnailRequest in pairs(thumbnailRequests) do
table.insert(fetchedPromises,
fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest):andThen(function(result)
for _, data in pairs(result) do
store:dispatch(SetUserThumbnail(data.id, data.image, data.thumbnailType, data.thumbnailSize))
end
end)
)
end
return Promise.all(fetchedPromises)
end
end
@@ -0,0 +1,42 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local AppTempCommon = CorePackages.AppTempCommon
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
local ChatSendMessage = require(Requests.ChatSendMessage)
local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation)
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local trimCharacterFromEndString = require(AppTempCommon.Temp.trimCharacterFromEndString)
local INVITE_TEXT_MESSAGE = "Come join me in %s"
local INVITE_LINK_MESSAGE = "%s/games/%s"
return function(networkImpl, userId, placeInfo)
local clientId = Players.LocalPlayer.UserId
-- Construct the invite messages based on place info
local inviteTextMessage = string.format(INVITE_TEXT_MESSAGE, placeInfo.name)
local trimmedUrl = trimCharacterFromEndString(Url.WWW_URL, "/")
local inviteLinkMessage = string.format(INVITE_LINK_MESSAGE, trimmedUrl, placeInfo.universeRootPlaceId)
return function(store)
return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(conversationResult)
local conversation = conversationResult.responseBody.conversation
return ChatSendMessage(networkImpl, conversation.id, inviteTextMessage):andThen(function()
return ChatSendMessage(networkImpl, conversation.id, inviteLinkMessage):andThen(function(inviteResult)
local data = inviteResult.responseBody
return {
resultType = data.resultType,
conversationId = conversation.id,
placeId = placeInfo.universeRootPlaceId,
}
end)
end)
end)
end
end
@@ -0,0 +1,50 @@
local CorePackages = game:GetService("CorePackages")
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
return function(users)
return function(store)
local friendCountOffset = 0
local updatedUsers = {}
for _, user in pairs(users) do
local needsUpdate = false
local userId = user.id
local isFriend = user.isFriend
local offset = 0
assert(typeof(isFriend) == "boolean")
local userInStore = store:getState().Users[userId]
if userInStore then
-- Mark user with needsUpdate if any of the field is different
-- from the existing user information in Store.
if not User.compare(userInStore, user) then
needsUpdate = true
if userInStore.isFriend ~= isFriend then
offset = isFriend and 1 or -1
end
end
else
needsUpdate = true
offset = isFriend and 1 or 0
end
if needsUpdate then
friendCountOffset = friendCountOffset + offset
updatedUsers[userId] = user
end
end
if next(updatedUsers) then
store:dispatch(AddUsers(updatedUsers))
end
if friendCountOffset ~= 0 then
local currentFriendCount = store:getState().FriendCount
store:dispatch(SetFriendCount(currentFriendCount + friendCountOffset))
end
end
end
@@ -0,0 +1,123 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount)
local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users)
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
local function UsersReducerMonitor (state, action)
state = state or {
numberOfAddUsersCalled = 0,
numberOfUsersPassedIn = 0,
}
if action.type == AddUsers.name then
state.numberOfAddUsersCalled = state.numberOfAddUsersCalled + 1
state.numberOfUsersPassedIn = 0
for _, _ in pairs(action.users) do
state.numberOfUsersPassedIn = state.numberOfUsersPassedIn + 1
end
end
return state
end
local function FriendCountReducerMonitor (state, action)
state = state or {
numberOfSetFriendCountCalled = 0,
}
if action.type == SetFriendCount.name then
state.numberOfSetFriendCountCalled = state.numberOfSetFriendCountCalled + 1
end
return state
end
local function CustomReducer(state, action)
state = state or {}
return {
Users = Users(state.Users, action),
UsersReducerMonitor = UsersReducerMonitor(state.UsersReducerMonitor, action),
FriendCount = FriendCount(state.FriendCount, action),
FriendCountReducerMonitor = FriendCountReducerMonitor(state.FriendCountReducerMonitor, action),
}
end
local listOfUsers = {
["1"] = User.fromData(1, "Hedonism Bot", true),
["2"] = User.fromData(2, "Hypno Toad", true),
["3"] = User.fromData(3, "John Zoidberg", false),
["4"] = User.fromData(4, "Pazuzu", true),
["5"] = User.fromData(5, "Ogden Wernstrom", true),
["6"] = User.fromData(6, "Lrrr", true),
}
it("should do nothing if empty list of users is provided", function()
local store = Rodux.Store.new(CustomReducer, {}, {
Rodux.thunkMiddleware,
})
store:dispatch(UpdateUsers({ }))
local state = store:getState()
expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(0)
expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(0)
end)
it("should update only the number of users with modified data", function()
local store = Rodux.Store.new(CustomReducer, {
Users = listOfUsers,
}, {
Rodux.thunkMiddleware,
})
local currentUsers = store:getState().Users
local listOfUsersWithPotentialUpdates = {
Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- changed
Immutable.Set(currentUsers["5"], "isFriend", false), -- changed
Immutable.Set(currentUsers["6"], "isFriend", true), -- did not change
}
store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates))
local state = store:getState()
expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(1)
expect(state.UsersReducerMonitor.numberOfUsersPassedIn).to.equal(2)
end)
it("should correctly update the number of friends", function()
local store = Rodux.Store.new(CustomReducer, {}, {
Rodux.thunkMiddleware,
})
store:dispatch(UpdateUsers(listOfUsers))
local state = store:getState()
expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(1)
expect(state.FriendCount).to.equal(5)
local currentUsers = store:getState().Users
local listOfUsersWithPotentialUpdates = {
Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- friendship didn't change
Immutable.Set(currentUsers["5"], "isFriend", false), -- friendship changed
Immutable.Set(currentUsers["6"], "isFriend", false), -- friendship changed
User.fromData(7, "Nibbler", true), -- new friend
}
store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates))
state = store:getState()
expect(state.FriendCount).to.equal(4)
end)
end
@@ -0,0 +1,10 @@
-- Helper function to throttle based on player Id:
return function(throttle, userId)
assert(type(throttle) == "number")
assert(type(userId) == "number")
-- Determine userRollout using last two digits of user ID:
-- (+1 to change range from 0-99 to 1-100 as 0 is off, 100 is full on):
local userRollout = (userId % 100) + 1
return userRollout <= throttle
end
@@ -0,0 +1,67 @@
return function()
local ThrottleUserId = require(script.Parent.ThrottleUserId)
describe("ThrottleUserId", function()
it("should always reject zero%", function()
local gating = ThrottleUserId(0, 10000)
expect(gating).to.equal(false)
gating = ThrottleUserId(0, 10001)
expect(gating).to.equal(false)
gating = ThrottleUserId(0, 10025)
expect(gating).to.equal(false)
gating = ThrottleUserId(0, 10075)
expect(gating).to.equal(false)
gating = ThrottleUserId(0, 10099)
expect(gating).to.equal(false)
gating = ThrottleUserId(0, 10100)
expect(gating).to.equal(false)
end)
it("should always accept 100%", function()
local gating = ThrottleUserId(100, 10000)
expect(gating).to.equal(true)
gating = ThrottleUserId(100, 10001)
expect(gating).to.equal(true)
gating = ThrottleUserId(100, 10025)
expect(gating).to.equal(true)
gating = ThrottleUserId(100, 10075)
expect(gating).to.equal(true)
gating = ThrottleUserId(100, 10099)
expect(gating).to.equal(true)
gating = ThrottleUserId(100, 10100)
expect(gating).to.equal(true)
end)
it("should reject IDs over throttle percent", function()
local gating = ThrottleUserId(25, 10050)
expect(gating).to.equal(false)
gating = ThrottleUserId(50, 10075)
expect(gating).to.equal(false)
gating = ThrottleUserId(75, 10099)
expect(gating).to.equal(false)
end)
it("should accept IDs under throttle percent", function()
local gating = ThrottleUserId(1, 10100)
expect(gating).to.equal(true)
gating = ThrottleUserId(10, 10109)
expect(gating).to.equal(true)
gating = ThrottleUserId(25, 10023)
expect(gating).to.equal(true)
end)
end)
end