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,41 @@
stds.roblox = {
globals = {
"game"
},
read_globals = {
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof", "delay",
-- Types
"Vector2", "Vector3",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
}
std = "lua51+roblox"
files["**/*.spec.lua"] = {
std = "+testez",
}
@@ -0,0 +1,17 @@
{
"language": {
"mode": "noinfer"
},
"lint": {
"MultiLineStatement": "disabled",
"DeprecatedGlobal": "disabled",
"LocalShadow": "disabled",
"LocalUnused": "disabled",
"FunctionUnused": "fatal",
"ImportUnused": "disabled",
"SameLineStatement": "fatal",
"ImplicitReturn": "disabled",
"UnknownGlobal": "fatal",
"GlobalUsedAsLocal": "fatal"
}
}
@@ -0,0 +1,56 @@
--[[
A centralized hub for basic metrics reporting.
This class is designed to provide a baseline exposure to the reporters.
Context specific Analytics.lua objects should be created in sub-projects,
to report specific actions like chat interactions, or game page interactions.
Analytics.lua and the reporters here in Common should serve to cover the
most common interactions.
]]
local AnalyticsService = game:GetService("RbxAnalyticsService")
local Reporters = script.Parent.AnalyticsReporters
local DiagReporter = require(Reporters.Diag)
local EventStreamReporter = require(Reporters.EventStream)
local GoogleAnalyticsReporter = require(Reporters.GoogleAnalytics)
local InfluxDbReporter = require(Reporters.Influx)
local Analytics = {}
Analytics.__index = Analytics
-- reportingService : (Service, optional) an object that exposes the same functions as AnalyticsService
function Analytics.new(reportingService)
if not reportingService then
reportingService = AnalyticsService
end
-- All public reporting functions are exposed by the objects defined in the properties
local self = {}
self.Diag = DiagReporter.new(reportingService)
self.EventStream = EventStreamReporter.new(reportingService)
self.GoogleAnalytics = GoogleAnalyticsReporter.new(reportingService)
self.InfluxDb = InfluxDbReporter.new(reportingService)
setmetatable(self, Analytics)
return self
end
function Analytics.mock()
-- create a reporting service that does not fire any requests out to the world
local fakeReportingService = {}
function fakeReportingService.ReportCounter() end
function fakeReportingService.ReportInfluxSeries() end
function fakeReportingService.ReportStats() end
function fakeReportingService.SetRBXEvent() end
function fakeReportingService.SetRBXEventStream() end
function fakeReportingService.TrackEvent() end
function fakeReportingService.UpdateHeartbeatObject() end
return Analytics.new(fakeReportingService)
end
return Analytics
@@ -0,0 +1,69 @@
return function()
local Analytics = require(script.Parent.Analytics)
describe("new()", function()
it("should properly construct a new object", function()
local na = Analytics.new()
expect(na).to.be.ok()
end)
it("should accept a custom reporting service", function()
local fakeService = {}
local na = Analytics.new(fakeService)
expect(na).to.be.ok()
end)
it("should have a reporter specifically for Diag", function()
local na = Analytics.new()
expect(na.Diag).to.be.ok()
end)
it("should have a reporter specifically for RBXEventStream", function()
local na = Analytics.new()
expect(na.EventStream).to.be.ok()
end)
it("should have a reporter specifically for Google Analytics", function()
local na = Analytics.new()
expect(na.GoogleAnalytics).to.be.ok()
end)
it("should have a reporter specifically for Influx", function()
local na = Analytics.new()
expect(na.InfluxDb).to.be.ok()
end)
end)
describe("mock()", function()
it("should properly construct a new object", function()
local ma = Analytics.mock()
expect(ma).to.be.ok()
expect(ma.Diag).to.be.ok()
expect(ma.EventStream).to.be.ok()
expect(ma.GoogleAnalytics).to.be.ok()
expect(ma.InfluxDb).to.be.ok()
end)
it("should succeed for all function calls in Diag", function()
local ma = Analytics.mock()
ma.Diag:reportCounter("fakeCounter", 1)
ma.Diag:reportStats("fakeCategory", 1)
end)
it("should succeed for all function call in EventStream", function()
local ma = Analytics.mock()
ma.EventStream:setRBXEvent("fakeContext", "fakeEventName")
ma.EventStream:setRBXEventStream("fakeContext", "fakeEventName")
end)
it("should succeed for all function call in GoogleAnalytics", function()
local ma = Analytics.mock()
ma.GoogleAnalytics:trackEvent("fakeCategory", "fakeAction", "fakeLabel")
end)
it("should succeed for all function call in Influx", function()
local ma = Analytics.mock()
ma.InfluxDb:reportSeries("fakeSeries", {}, 1)
end)
end)
end
@@ -0,0 +1,64 @@
--[[
Specialized analytics reporter for ephemeral counters.
Useful for tracking at-a-glance health of a feature.
]]
local UserInputService = game:GetService("UserInputService")
local Diag = {}
Diag.__index = Diag
-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService
function Diag.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, Diag)
return self
end
-- isEnabled : (boolean)
function Diag:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- counterName : (string) the name of the ephemeral counter to increment
-- amount : (int) the value to increment the counter by
function Diag:reportCounter(counterName, amount)
assert(type(counterName) == "string", "Expected counterName to be a string")
assert(type(amount) == "number", "Expected amount to be a number")
assert(self._isEnabled, "This reporting service is disabled")
-- use special naming convention for Xbox counters
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
-- executes the scripts in the wrong authorization level
local platformID = Enum.Platform.None
pcall(function()
platformID = UserInputService:GetPlatform()
end)
if platformID == Enum.Platform.XBoxOne then
counterName = "Xbox-" .. tostring(counterName)
end
-- the AnalyticsService should automatically handle batch reporting
self._reporter:ReportCounter(counterName, amount)
end
-- category : (string) the name of the statistics buffer which to append the data
-- value : (number) any type of numeric value
function Diag:reportStats(category, value)
assert(type(category) == "string", "Expected category to be a string")
assert(type(value) == "number", "Expected value to be a number")
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:ReportStats(category, value)
end
return Diag
@@ -0,0 +1,176 @@
return function()
local Diag = require(script.Parent.Diag)
local testCounterName = "testCounter"
local testCounterAmount = 1
local testCategoryName = "testCategory"
local testCategoryValue = 98
local badTestCounterName = 5
local badTestCounterAmount = "hello"
local badTestCategoryName = {}
local badTestCategoryValue = {}
local DebugReportingService = {}
function DebugReportingService:ReportCounter(counterName, amount)
assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName)
assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount)
end
function DebugReportingService:ReportStats(categoryName, value)
assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName)
if value then
assert(value == testCategoryValue, "Unexpected value for value: " .. value)
end
end
describe("new()", function()
it("should construct with a Reporting Service", function()
local diag = Diag.new(DebugReportingService)
expect(diag).to.be.ok()
end)
it("should throw an error to be constructed without a Reporting Service", function()
expect(function()
Diag.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local diag = Diag.new(DebugReportingService)
diag:setEnabled(false)
diag:setEnabled(true)
end)
it("should disable the reporter", function()
local diag = Diag.new(DebugReportingService)
diag:setEnabled(false)
expect(function()
diag:reportCounter(testCounterName, testCounterAmount)
end).to.throw()
end)
end)
describe("reportCounter()", function()
it("should work when appropriately enabled / disabled", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:setEnabled(false)
diag:reportCounter(testCounterName, testCounterAmount)
end).to.throw()
diag:setEnabled(true)
diag:reportCounter(testCounterName, testCounterAmount)
end)
it("should succeed with valid input", function()
local diag = Diag.new(DebugReportingService)
diag:reportCounter(testCounterName, testCounterAmount)
end)
it("should throw an error with invalid input for the counter name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(badTestCounterName, testCounterAmount)
end).to.throw()
end)
it("should throw an error with invalid input for the amount", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(testCounterName, badTestCounterAmount)
end).to.throw()
end)
it("should throw an error with completely invalid input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(badTestCounterName, badTestCounterAmount)
end).to.throw()
end)
it("should throw an error if it is missing a counter name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(nil, testCounterAmount)
end).to.throw()
end)
it("should throw an error if it is missing an amount", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(testCounterName, nil)
end).to.throw()
end)
it("should throw an error if it is missing any input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(nil, nil)
end).to.throw()
end)
end)
describe("reportStats()", function()
it("should work when appropriately enabled / disabled", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:setEnabled(false)
diag:reportStats(testCategoryName, testCategoryValue)
end).to.throw()
diag:setEnabled(true)
diag:reportStats(testCategoryName, testCategoryValue)
end)
it("should succeed with valid input", function()
local diag = Diag.new(DebugReportingService)
diag:reportStats(testCategoryName, testCategoryValue)
end)
it("should throw an error with invalid input for the category name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(badTestCategoryName, testCategoryValue)
end).to.throw()
end)
it("should throw an error with invalid input for the value", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(testCategoryName, badTestCategoryValue)
end).to.throw()
end)
it("should throw an error with completely invalid input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(badTestCategoryName, badTestCategoryValue)
end).to.throw()
end)
it("should throw an error if it is missing a category name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(nil, testCategoryValue)
end).to.throw()
end)
it("should throw an error if it is missing a value", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(testCategoryName, nil)
end).to.throw()
end)
it("should throw an error if it is missing any input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(nil, nil)
end).to.throw()
end)
end)
end
@@ -0,0 +1,154 @@
--[[
Specialized reporter for RBX Event Ingest data.
Useful for tracking explicit user interactions with screens and guis.
]]
local UserInputService = game:GetService("UserInputService")
local function getPlatformTarget()
local platformTarget = "unknownLua"
local platformEnum = Enum.Platform.None
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
-- executes the scripts in the wrong authorization level
pcall(function()
platformEnum = UserInputService:GetPlatform()
end)
-- bucket the platform based on consumer platform
local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX)
local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android)
isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP)
local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne)
isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4)
isConsole = isConsole or (platformEnum == Enum.Platform.WiiU)
-- assign a target based on the form factor
if isDesktopClient then
platformTarget = "client"
elseif isMobileClient then
platformTarget = "mobile"
elseif isConsole then
platformTarget = "console"
else
-- if we don't have a name for the form factor, report it here so that we can eventually track it down
platformTarget = platformTarget .. tostring(platformEnum)
end
return platformTarget
end
local EventStream = {}
EventStream.__index = EventStream
-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService
function EventStream.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, EventStream)
return self
end
-- isEnabled : (boolean)
function EventStream:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) table for additional information to appear in the event stream.
function EventStream:setRBXEvent(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- This function fires reports to the server right away
self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs)
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- this function sends reports to the server in batches, not real-time
self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs)
end
function EventStream:releaseRBXEventStream()
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:ReleaseRBXEventStream(getPlatformTarget())
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
function EventStream:sendEventDeferred(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- this function sends reports to the server in batches, not real-time
self._reporter:SendEventDeferred(target, eventContext, eventName, additionalArgs)
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
function EventStream:sendEventImmediately(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- this function sends reports to the server in batches, not real-time
self._reporter:SendEventImmediately(target, eventContext, eventName, additionalArgs)
end
-- additionalArgs : (optional, map<string, string>) table for extra keys to appear in the event stream.
function EventStream:updateHeartbeatObject(additionalArgs)
additionalArgs = additionalArgs or {}
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:UpdateHeartbeatObject(additionalArgs)
end
return EventStream
@@ -0,0 +1,393 @@
return function()
local EventStream = require(script.Parent.EventStream)
local testArgs = {
testKey = "testValue"
}
local testContext = "testContext"
local testEvent = "testEventName"
local badTestArgs = "hello"
local badTestContext = {}
local badTestEvent = {}
local function isTableEqual(table1, table2)
if table1 == table2 then
return true
end
if type(table1) ~= "table" then
return false
end
if type(table2) ~= "table" then
return false
end
for key, _ in pairs(table1) do
if table1[key] ~= table2[key] then
return false
end
end
for key, _ in pairs(table2) do
if table2[key] ~= table1[key] then
return false
end
end
return true
end
local function createDebugReportingService()
local function validateInputs(eventTarget, eventContext, eventName, additionalArgs)
assert(eventTarget, "no value found for eventTarget")
assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext)
assert(eventName == testEvent, "unexpected value for eventName : " .. eventName)
if additionalArgs and not isTableEqual(additionalArgs, {}) then
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
end
end
local DebugReportingService = {}
function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:SendEventImmediately(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:SendEventDeferred(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:ReleaseRBXEventStream(eventTarget)
assert(eventTarget, "no value found for eventTarget")
end
function DebugReportingService:UpdateHeartbeatObject(additionalArgs)
if additionalArgs and not isTableEqual(additionalArgs, {}) then
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
end
end
return DebugReportingService
end
describe("new()", function()
it("should construct with a Reporting Service", function()
local es = EventStream.new(createDebugReportingService())
expect(es).to.be.ok()
end)
it("should not allow construction without a Reporting Service", function()
expect(function()
EventStream.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local reporter = EventStream.new(createDebugReportingService())
reporter:setEnabled(false)
reporter:setEnabled(true)
end)
it("should disable the reporter", function()
local reporter = EventStream.new(createDebugReportingService())
reporter:setEnabled(false)
expect(function()
reporter:updateHeartbeatObject()
end).to.throw()
end)
end)
describe("setRBXEvent()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEvent(testContext, testEvent, testArgs)
end)
it("should work when appropriately enabled / disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:setRBXEvent(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:setRBXEvent(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEvent(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("setRBXEventStream()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEventStream(testContext, testEvent, testArgs)
end)
it("should work when appropriately enabled / disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:setRBXEventStream(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:setRBXEventStream(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEventStream(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("sendEventImmediately()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventImmediately(testContext, testEvent, testArgs)
end)
it("should call without a fuss when enabled and throw when disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:sendEventImmediately(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:sendEventImmediately(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventImmediately(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("sendEventDeferred()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventDeferred(testContext, testEvent, testArgs)
end)
it("should call without a fuss when enabled and throw when disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:sendEventDeferred(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:sendEventDeferred(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventDeferred(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("releaseRBXEventStream()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:releaseRBXEventStream()
end)
it("should throw when disabled and succeed when enabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:releaseRBXEventStream()
end).to.throw()
es:setEnabled(true)
es:releaseRBXEventStream()
end)
end)
describe("updateHeartbeatObject()", function()
it("should work when appropriately enabled / disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:updateHeartbeatObject(testArgs)
end).to.throw()
expect(function()
es:setEnabled(true)
es:updateHeartbeatObject(testArgs)
end).never.to.throw()
end)
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:updateHeartbeatObject(testArgs)
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:updateHeartbeatObject(nil)
end)
it("should throw an error with invalid input", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:updateHeartbeatObject(badTestArgs)
end).to.throw()
end)
end)
end
@@ -0,0 +1,51 @@
--[[
Specialized reporter for sending data to GA.
Useful for creating a breadcrumb trail of user interactions.
Events in GA are aggregated and organized in order by category, action, label.
]]
local GoogleAnalytics = {}
GoogleAnalytics.__index = GoogleAnalytics
-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService
function GoogleAnalytics.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, GoogleAnalytics)
return self
end
-- isEnabled : (boolean)
function GoogleAnalytics:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc.
-- action : (string) a specific event to record, ex) ButtonPressed, GameExit
-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0
-- value : (integer, optional) the number of times this event has occurred
function GoogleAnalytics:trackEvent(category, action, label, value)
assert(type(category) == "string", "Expected category to be a string")
assert(type(action) == "string", "Expected action to be a string")
if label then
assert(type(label) == "string", "Expected label to be a string")
end
if value then
assert(type(value) == "number", "Expected value to be a number")
assert(value >= 0, "Expected value must not be a negative value")
end
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:TrackEvent(category, action, label, value)
end
return GoogleAnalytics
@@ -0,0 +1,140 @@
return function()
local GoogleAnalytics = require(script.Parent.GoogleAnalytics)
local testCategory = "testCategory"
local testAction = "testAction"
local testLabel = "testLabel"
local testValue = 6
local badTestCategory = 13141
local badTestAction = {}
local badTestLabel = {}
local badTestValue = "heyo"
local DebugReportingService = {}
function DebugReportingService:TrackEvent(category, action, label, value)
if category ~= testCategory then
error("unexpected value for category: " .. category)
end
if action ~= testAction then
error("unexpected value for action: " .. action)
end
if label then
if label ~= testLabel then
error("unexpected value for label: " .. label)
end
end
if value then
if value ~= testValue then
error("unexpected value for value: " .. value)
end
end
end
describe("new()", function()
it("should construct with a Reporting Service and Logging Service", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(ga).to.be.ok()
end)
it("should fail to be constructed without a Reporting Service", function()
expect(function()
GoogleAnalytics.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local reporter = GoogleAnalytics.new(DebugReportingService)
reporter:setEnabled(false)
reporter:setEnabled(true)
end)
it("should disable the reporter", function()
local reporter = GoogleAnalytics.new(DebugReportingService)
reporter:setEnabled(false)
expect(function()
reporter:trackEvent(testCategory, testAction, testLabel, testValue)
end).to.throw()
end)
end)
describe("trackEvent()", function()
it("should work when appropriately enabled / disabled", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:setEnabled(false)
ga:trackEvent(testCategory, testAction, testLabel)
end).to.throw()
ga:setEnabled(true)
ga:trackEvent(testCategory, testAction, testLabel)
end)
it("should succeed with valid input", function()
local ga = GoogleAnalytics.new(DebugReportingService)
ga:trackEvent(testCategory, testAction, testLabel, testValue)
end)
it("should throw an error if it is missing a category", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(nil, testAction, testLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is missing a testAction", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, nil, testLabel, testValue)
end).to.throw()
end)
it("should not throw an error if it is missing a label", function()
local ga = GoogleAnalytics.new(DebugReportingService)
ga:trackEvent(testCategory, testAction, nil, testValue)
end)
it("should not throw an error if it is missing a value", function()
local ga = GoogleAnalytics.new(DebugReportingService)
ga:trackEvent(testCategory, testAction, testLabel)
end)
it("should throw an error if it is given invalid input for category", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(badTestCategory, testAction, testLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is given invalid input for action", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, badTestAction, testLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is given invalid input for label", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, testAction, badTestLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is given invalid input for value", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, testAction, testLabel, badTestValue)
end).to.throw()
expect(function()
ga:trackEvent(testCategory, testAction, testLabel, -1)
end).to.throw()
end)
end)
end
@@ -0,0 +1,47 @@
--[[
Specialized reporter for sending data to InfluxDb.
Useful for very detailed information about specific errors.
Due to how Influx sends data, it is disallowed on XBox.
~Kyler Mulherin (9/12/2017)
]]
local Influx = {}
Influx.__index = Influx
-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService
function Influx.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, Influx)
return self
end
-- isEnabled : (boolean)
function Influx:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- seriesName : (string) the name of the series as it will appear in InfluxDb
-- additionalArgs : (map<string, string>) extra key/values to appear in each series
-- throttlingPercent : (int) the chance to actually report this series
function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent)
additionalArgs = additionalArgs or {}
assert(type(seriesName) == "string", "Expected seriesName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number")
assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000")
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent)
end
return Influx
@@ -0,0 +1,165 @@
return function()
local Influx = require(script.Parent.Influx)
local testSeriesName = "testSeries"
local testArgs = {
testKey = "testValue"
}
local testThrottlingPercentage = 1000
local badTestSeriesName = 114
local badTestArgs = "someString"
local badThrottlingPercentage1 = -15
local badThrottlingPercentage2 = 150000
local badThrottlingPercentage3 = "a lot"
local function isTableEqual(table1, table2)
if table1 == table2 then
return true
end
if type(table2) ~= "table" then
return false
end
for key, _ in pairs(table1) do
if table1[key] ~= table2[key] then
return false
end
end
for key, _ in pairs(table2) do
if table2[key] ~= table1[key] then
return false
end
end
return true
end
local DebugReportingService = {}
function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage)
if seriesName ~= seriesName then
error("Unexpected value for seriesName: " .. seriesName)
end
if throttlingPercentage ~= testThrottlingPercentage then
error("Unexpected value for throttlingPercentage: " .. throttlingPercentage)
end
if isTableEqual(additionalArgs, {}) == false then
if isTableEqual(additionalArgs, testArgs) == false then
error("Unexpected value for additionalArgs")
end
end
end
describe("new()", function()
it("should construct with a Reporting Service", function()
local influx = Influx.new(DebugReportingService)
expect(influx).to.be.ok()
end)
it("should fail to be constructed without a Reporting Service", function()
expect(function()
Influx.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local influx = Influx.new(DebugReportingService)
influx:setEnabled(false)
influx:setEnabled(true)
end)
it("should disable the reporter", function()
local influx = Influx.new(DebugReportingService)
influx:setEnabled(false)
expect(function()
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end).to.throw()
end)
end)
describe("reportSeries()", function()
it("should work when appropriately enabled / disabled", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:setEnabled(false)
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end).to.throw()
influx:setEnabled(true)
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end)
it("should succeed with valid input", function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end)
it("should succeed even if it is missing any additionalArgs", function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, nil, testThrottlingPercentage)
end)
it("should throw an error with invalid input for the seriesName", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage)
end).to.throw()
end)
it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function()
expect(function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1)
end).to.throw()
end)
it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function()
expect(function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2)
end).to.throw()
end)
it("should throw an error with invalid input for the throttlingPercentage - bad type", function()
expect(function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3)
end).to.throw()
end)
it("should throw an error with completely invalid input", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1)
end).to.throw()
end)
it("should throw an error if it is missing a seriesName", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(nil, testArgs, testThrottlingPercentage)
end).to.throw()
end)
it("should throw an error if it is missing a throttlingPercentage", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(testSeriesName, testArgs, nil)
end).to.throw()
end)
it("should throw an error if it is missing any input", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(nil, nil, nil)
end).to.throw()
end)
end)
end
@@ -0,0 +1,7 @@
{
"lint": {
"LocalUnused": "fatal",
"ImportUnused": "fatal",
"DeprecatedGlobal": "fatal"
}
}
@@ -0,0 +1,9 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"LocalShadow": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,8 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
--- Please put your changes in Analytics. ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.Analytics.AnalyticsReporters.Diag)
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,68 @@
--[[
A helper function to define a Rodux action creator with an associated name.
Normally when creating a Rodux action, you can just create a function:
return function(value)
return {
type = "MyAction",
value = value,
}
end
And then when you check for it in your reducer, you either use a constant,
or type out the string name:
if action.type == "MyAction" then
-- change some state
end
Typos here are a remarkably common bug. We also have the issue that there's
no link between reducers and the actions that they respond to!
`Action` (this helper) provides a utility that makes this a bit cleaner.
Instead, define your Rodux action like this:
return Action("MyAction", function(value)
return {
value = value,
}
end)
We no longer need to add the `type` field manually.
Additionally, the returned action creator now has a 'name' property that can
be checked by your reducer:
local MyAction = require(Reducers.MyAction)
...
if action.type == MyAction.name then
-- change some state!
end
Now we have a clear link between our reducers and the actions they use, and
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
at runtime!
]]
return function(name, fn)
assert(type(name) == "string", "A name must be provided to create an Action")
assert(type(fn) == "function", "A function must be provided to create an Action")
return setmetatable({
name = name,
}, {
__call = function(self, ...)
local result = fn(...)
assert(type(result) == "table", "An action must return a table")
result.type = name
return result
end
})
end
@@ -0,0 +1,85 @@
return function()
local Action = require(script.Parent.Action)
it("should return a table", function()
local action = Action("foo", function()
return {}
end)
expect(action).to.be.a("table")
end)
it("should set the name of the action", function()
local action = Action("foo", function()
return {}
end)
expect(action.name).to.equal("foo")
end)
it("should be able to be called as a function", function()
local action = Action("foo", function()
return {}
end)
expect(action).never.to.throw()
end)
it("should return a table when called as a function", function()
local action = Action("foo", function()
return {}
end)
expect(action()).to.be.a("table")
end)
it("should set the type of the action", function()
local action = Action("foo", function()
return {}
end)
expect(action().type).to.equal("foo")
end)
it("should set values", function()
local action = Action("foo", function(value)
return {
value = value
}
end)
expect(action(100).value).to.equal(100)
end)
it("should throw when passed a function", function()
local action = Action("foo", function()
return function() end
end)
expect(action).to.throw()
end)
it("should throw with a invalid name", function()
expect(function()
Action(nil, function()
return {}
end)
end).to.throw()
expect(function()
Action(100, function()
return {}
end)
end).to.throw()
end)
it("should throw when passed a invalid function", function()
expect(function()
Action("foo", nil)
end).to.throw()
expect(function()
Action("foo", {})
end).to.throw()
end)
end
@@ -0,0 +1,19 @@
local Color = {}
function Color.RgbFromHex(hexColor)
assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range")
local b = hexColor % 256
hexColor = (hexColor - b) / 256
local g = hexColor % 256
hexColor = (hexColor - g) / 256
local r = hexColor
return r, g, b
end
function Color.Color3FromHex(hexColor)
return Color3.fromRGB(Color.RgbFromHex(hexColor))
end
return Color
@@ -0,0 +1,33 @@
return function()
local Color = require(script.Parent.Color)
describe("RgbFromHex", function()
it("should convert a hex color to rgb correctly", function()
local r, g, b = Color.RgbFromHex(0x232527)
expect(r).to.equal(35)
expect(g).to.equal(37)
expect(b).to.equal(39)
r, g, b = Color.RgbFromHex(0x0)
expect(r).to.equal(0)
expect(g).to.equal(0)
expect(b).to.equal(0)
r, g, b = Color.RgbFromHex(0xffffff)
expect(r).to.equal(255)
expect(g).to.equal(255)
expect(b).to.equal(255)
end)
it("should assert if given a hex color out of range", function()
expect(function()
Color.RgbFromHex(-1)
end).to.throw()
expect(function()
Color.RgbFromHex(0x1000000)
end).to.throw()
end)
end)
end
@@ -0,0 +1,131 @@
--[[
Provides an implementation of functional programming primitives.
]]
local Functional = {}
--[[
Create a copy of a list with only values for which `callback` returns true
]]
function Functional.Filter(list, callback)
local new = {}
for key = 1, #list do
local value = list[key]
if callback(value, key) then
table.insert(new, value)
end
end
return new
end
--[[
Create a copy of a list where each value is transformed by `callback`
]]
function Functional.Map(list, callback)
local new = {}
for key = 1, #list do
new[key] = callback(list[key], key)
end
return new
end
--[[
Identical to Map, except that the result will be reversed.
]]
function Functional.MapReverse(list, callback)
local new = {}
for key = #list, 1, -1 do
new[key] = callback(list[key], key)
end
return new
end
--[[
Create a copy of a list doing a combination filter and map.
If callback returns nil for any item, it is considered filtered from the
list. Any other value is considered the result of the 'map' operation.
]]
function Functional.FilterMap(list, callback)
local new = {}
for key = 1, #list do
local value = list[key]
local result = callback(value, key)
if result ~= nil then
table.insert(new, result)
end
end
return new
end
--[[
Performs a left-fold of the list with the given initial value and callback.
]]
function Functional.Fold(list, initial, callback)
local accum = initial
for key = 1, #list do
accum = callback(accum, list[key], key)
end
return accum
end
--[[
Performs a fold over the entries in the given dictionary.
]]
function Functional.FoldDictionary(dictionary, initial, callback)
local accum = initial
for key, value in pairs(dictionary) do
accum = callback(accum, key, value)
end
return accum
end
--[[
Returns a list that contains at most `count` values from the given list.
]]
function Functional.Take(list, count, startingIndex)
startingIndex = startingIndex or 1
local maxIndex = count + (startingIndex - 1)
if maxIndex > #list then
maxIndex = #list
end
local new = {}
for i = startingIndex, maxIndex do
local value = list[i]
local newIndex = i - (startingIndex - 1)
new[newIndex] = value
end
return new
end
--[[
If the list contains the sought-after element, return its index, or nil otherwise.
]]
function Functional.Find(list, value)
for index, element in ipairs(list) do
if element == value then
return index
end
end
return nil
end
return Functional
@@ -0,0 +1,218 @@
return function()
local Functional = require(script.Parent.Functional)
local function identity(...)
return ...
end
local function add(a, b)
return a + b
end
describe("Filter", function()
it("should copy lists correctly", function()
local listA = {1, 2, 3}
local listB = Functional.Filter(listA, function()
return true
end)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the filter predicate", function()
local listA = {1, 2, 3, 4, 5}
local listB = Functional.Filter(listA, function(value, key)
expect(value).to.equal(key)
return value % 2 == 0
end)
expect(listB[1]).to.equal(2)
expect(listB[2]).to.equal(4)
end)
end)
describe("Map", function()
it("should copy lists correctly using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.Map(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the map predicate", function()
local listA = {1, 2, 3}
local listB = Functional.Map(listA, function(value, key)
expect(value).to.equal(key)
return value * 2
end)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i] * 2)
end
end)
end)
describe("MapReverse", function()
it("should copy lists correctly using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.MapReverse(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the map predicate", function()
local listA = {1, 2, 3}
local listB = Functional.MapReverse(listA, function(value, key)
expect(value).to.equal(key)
return value * 2
end)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i] * 2)
end
end)
it("should iterate backwards", function()
local list = {1, 2, 3}
local nextKey = 3
Functional.MapReverse(list, function(value, key)
expect(value).to.equal(nextKey)
expect(key).to.equal(nextKey)
nextKey = nextKey - 1
end)
expect(nextKey).to.equal(0)
end)
end)
describe("FilterMap", function()
it("should copy truthy lists using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.FilterMap(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the filter-map predicate", function()
local listA = {1, 2, 3, 4, 5}
-- Create a list containing only the odd numbers, and double those numbers
local listB = Functional.FilterMap(listA, function(value, key)
expect(value).to.equal(key)
if value % 2 == 0 then
return nil
end
return value * 2
end)
expect(listB[1]).to.equal(2)
expect(listB[2]).to.equal(6)
expect(listB[3]).to.equal(10)
end)
end)
describe("Fold", function()
it("should left-fold lists", function()
local list = {1, 2, 3, 4, 5}
local sum = Functional.Fold(list, 0, add)
expect(sum).to.equal(15)
end)
end)
describe("Take", function()
it("should take values from a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 2)
expect(#b).to.equal(2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
end)
it("should not take past the end of a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 4)
expect(#b).to.equal(3)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
end)
it("should copy all values when taking past the end of a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 4)
expect(#b).to.equal(#a)
expect(a[1]).to.equal(b[1])
expect(a[2]).to.equal(b[2])
expect(a[3]).to.equal(b[3])
end)
it("should take values from a starting index when provided", function()
local a = {1, 2, 3, 4}
local b = Functional.Take(a, 2, 2)
expect(#b).to.equal(2)
expect(b[1]).to.equal(2)
expect(b[2]).to.equal(3)
end)
it("should not take past the end of a list when the starting index is provided", function()
local a = {1, 2, 3, 4}
local b = Functional.Take(a, 3, 3)
expect(#b).to.equal(2)
expect(b[1]).to.equal(3)
expect(b[2]).to.equal(4)
end)
end)
describe("Find", function()
it("should return index of matched item", function()
local a = {"foo", "bar", "garply"}
local b = Functional.Find(a, "bar")
expect(b).to.equal(2)
end)
it("should find the first example in the case of duplicates", function()
local a = {"foo", "bar", "garply", "bar"}
local b = Functional.Find(a, "bar")
expect(b).to.equal(2)
end)
it("should return nil if item is not found", function()
local a = {"foo", "bar", "garply"}
local b = Functional.Find(a, "fleebledegoop")
expect(b).to.equal(nil)
end)
end)
end
@@ -0,0 +1,141 @@
--[[
Provides functions for manipulating immutable data structures.
]]
local Immutable = {}
--[[
Merges dictionary-like tables together.
]]
function Immutable.JoinDictionaries(...)
local result = {}
for i = 1, select("#", ...) do
local dictionary = select(i, ...)
for key, value in pairs(dictionary) do
result[key] = value
end
end
return result
end
--[[
Joins any number of lists together into a new list
]]
function Immutable.JoinLists(...)
local new = {}
for listKey = 1, select("#", ...) do
local list = select(listKey, ...)
local len = #new
for itemKey = 1, #list do
new[len + itemKey] = list[itemKey]
end
end
return new
end
--[[
Creates a new copy of the dictionary and sets a value inside it.
]]
function Immutable.Set(dictionary, key, value)
local new = {}
for key, value in pairs(dictionary) do
new[key] = value
end
new[key] = value
return new
end
--[[
Creates a new copy of the list with the given elements appended to it.
]]
function Immutable.Append(list, ...)
local new = {}
local len = #list
for key = 1, len do
new[key] = list[key]
end
for i = 1, select("#", ...) do
new[len + i] = select(i, ...)
end
return new
end
--[[
Remove elements from a dictionary
]]
function Immutable.RemoveFromDictionary(dictionary, ...)
local result = {}
for key, value in pairs(dictionary) do
local found = false
for listKey = 1, select("#", ...) do
if key == select(listKey, ...) then
found = true
break
end
end
if not found then
result[key] = value
end
end
return result
end
--[[
Remove the given key from the list.
]]
function Immutable.RemoveFromList(list, removeIndex)
local new = {}
for i = 1, #list do
if i ~= removeIndex then
table.insert(new, list[i])
end
end
return new
end
--[[
Remove the range from the list starting from the index.
]]
function Immutable.RemoveRangeFromList(list, index, count)
local new = {}
for i = 1, #list do
if i < index or i >= index + count then
table.insert(new, list[i])
end
end
return new
end
--[[
Creates a new list that has no occurrences of the given value.
]]
function Immutable.RemoveValueFromList(list, removeValue)
local new = {}
for i = 1, #list do
if list[i] ~= removeValue then
table.insert(new, list[i])
end
end
return new
end
return Immutable
@@ -0,0 +1,284 @@
return function()
local Immutable = require(script.Parent.Immutable)
describe("JoinDictionaries", function()
it("should preserve immutability", function()
local a = {}
local b = {}
local c = Immutable.JoinDictionaries(a, b)
expect(c).never.to.equal(a)
expect(c).never.to.equal(b)
end)
it("should treat list-like values like dictionary values", function()
local a = {
[1] = 1,
[2] = 2,
[3] = 3
}
local b = {
[1] = 11,
[2] = 22
}
local c = Immutable.JoinDictionaries(a, b)
expect(c[1]).to.equal(b[1])
expect(c[2]).to.equal(b[2])
expect(c[3]).to.equal(a[3])
end)
it("should merge dictionary values correctly", function()
local a = {
hello = "world",
foo = "bar"
}
local b = {
foo = "baz",
tux = "penguin"
}
local c = Immutable.JoinDictionaries(a, b)
expect(c.hello).to.equal(a.hello)
expect(c.foo).to.equal(b.foo)
expect(c.tux).to.equal(b.tux)
end)
it("should merge multiple dictionaries", function()
local a = {
foo = "yes"
}
local b = {
bar = "yup"
}
local c = {
baz = "sure"
}
local d = Immutable.JoinDictionaries(a, b, c)
expect(d.foo).to.equal(a.foo)
expect(d.bar).to.equal(b.bar)
expect(d.baz).to.equal(c.baz)
end)
end)
describe("JoinLists", function()
it("should preserve immutability", function()
local a = {}
local b = {}
local c = Immutable.JoinLists(a, b)
expect(c).never.to.equal(a)
expect(c).never.to.equal(b)
end)
it("should treat list-like values correctly", function()
local a = {1, 2, 3}
local b = {4, 5, 6}
local c = Immutable.JoinLists(a, b)
expect(#c).to.equal(6)
for i = 1, #c do
expect(c[i]).to.equal(i)
end
end)
it("should merge multiple lists", function()
local a = {1, 2}
local b = {3, 4}
local c = {5, 6}
local d = Immutable.JoinLists(a, b, c)
expect(#d).to.equal(6)
for i = 1, #d do
expect(d[i]).to.equal(i)
end
end)
end)
describe("Set", function()
it("should preserve immutability", function()
local a = {}
local b = Immutable.Set(a, "foo", "bar")
expect(b).never.to.equal(a)
end)
it("should treat numeric keys normally", function()
local a = {1, 2, 3}
local b = Immutable.Set(a, 2, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(4)
expect(b[3]).to.equal(3)
end)
it("should overwrite dictionary-like keys", function()
local a = {
foo = "bar",
baz = "qux"
}
local b = Immutable.Set(a, "foo", "hello there")
expect(b.foo).to.equal("hello there")
expect(b.baz).to.equal(a.baz)
end)
end)
describe("Append", function()
it("should preserve immutability", function()
local a = {}
local b = Immutable.Append(a, "another happy landing")
expect(b).never.to.equal(a)
end)
it("should append values", function()
local a = {1, 2, 3}
local b = Immutable.Append(a, 4, 5)
expect(#b).to.equal(5)
for i = 1, #b do
expect(b[i]).to.equal(i)
end
end)
end)
describe("RemoveFromDictionary", function()
it("should preserve immutability", function()
local a = { foo = "bar" }
local b = Immutable.RemoveFromDictionary(a, "foo")
expect(b).to.never.equal(a)
end)
it("should remove fields from the dictionary", function()
local a = {
foo = "bar",
baz = "qux",
boof = "garply",
}
local b = Immutable.RemoveFromDictionary(a, "foo", "boof")
expect(b.foo).to.never.be.ok()
expect(b.baz).to.equal("qux")
expect(b.boof).to.never.be.ok()
end)
end)
describe("RemoveFromList", function()
it("should preserve immutability", function()
local a = {1, 2, 3}
local b = Immutable.RemoveFromList(a, 2)
expect(b).never.to.equal(a)
end)
it("should remove elements from the list", function()
local a = {1, 2, 3}
local b = Immutable.RemoveFromList(a, 2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
end)
describe("RemoveRangeFromList", function()
it("should preserve immutability", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 1)
expect(b).never.to.equal(a)
end)
it("should remove elements properly from the list 1", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 1)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 2", function()
local a = {1, 2, 3, 4, 5, 6}
local b = Immutable.RemoveRangeFromList(a, 1, 4)
expect(b[1]).to.equal(5)
expect(b[2]).to.equal(6)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 3", function()
local a = {1, 2, 3, 4, 5, 6}
local b = Immutable.RemoveRangeFromList(a, 2, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(6)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 4", function()
local a = {1, 2, 3, 4, 5, 6, 7}
local b = Immutable.RemoveRangeFromList(a, 4, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
expect(b[4]).never.to.be.ok()
end)
it("should not remove any elements when count is 0 or less", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 0)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
local c = Immutable.RemoveRangeFromList(a, 2, -1)
expect(c[1]).to.equal(1)
expect(c[2]).to.equal(2)
expect(c[3]).to.equal(3)
end)
end)
describe("RemoveValueFromList", function()
it("should preserve immutability", function()
local a = {1, 1, 1}
local b = Immutable.RemoveValueFromList(a, 1)
expect(b).never.to.equal(a)
end)
it("should remove all elements from the list", function()
local a = {1, 2, 2, 3}
local b = Immutable.RemoveValueFromList(a, 2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
end)
end
@@ -0,0 +1,75 @@
--[[
A limited, simple implementation of a Signal.
Handlers are fired in order, and (dis)connections are properly handled when
executing an event.
Signal uses Immutable to avoid invalidating the 'Fire' loop iteration.
]]
local Immutable = require(script.Parent.Immutable)
local Signal = {}
Signal.__index = Signal
function Signal.new()
local self = {
_listeners = {}
}
setmetatable(self, Signal)
return self
end
function Signal:connect(callback)
local listener = {
callback = callback,
isConnected = true,
}
self._listeners = Immutable.Append(self._listeners, listener)
local function disconnect()
listener.isConnected = false
self._listeners = Immutable.RemoveValueFromList(self._listeners, listener)
end
return {
Disconnect = function()
warn(string.format(
"Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]",
debug.traceback()
))
disconnect()
end,
disconnect = disconnect,
}
end
function Signal:fire(...)
for _, listener in ipairs(self._listeners) do
if listener.isConnected then
listener.callback(...)
end
end
end
function Signal:Connect(...)
warn(string.format(
"Signal:Connect() has been deprecated, use Signal:connect()\n%s]",
debug.traceback()
))
return self:connect(...)
end
function Signal:Fire(...)
warn(string.format(
"Signal:Fire() has been deprecated, use Signal:fire()\n%s]",
debug.traceback()
))
self:fire(...)
end
return Signal
@@ -0,0 +1,114 @@
return function()
local Signal = require(script.Parent.Signal)
it("should construct from nothing", function()
local signal = Signal.new()
expect(signal).to.be.ok()
end)
it("should fire connected callbacks", function()
local callCount = 0
local value1 = "Hello World"
local value2 = 7
local callback = function(arg1, arg2)
expect(arg1).to.equal(value1)
expect(arg2).to.equal(value2)
callCount = callCount + 1
end
local signal = Signal.new()
local connection = signal:connect(callback)
signal:fire(value1, value2)
expect(callCount).to.equal(1)
connection:disconnect()
signal:fire(value1, value2)
expect(callCount).to.equal(1)
end)
it("should disconnect handlers", function()
local callback = function()
error("Callback was called after disconnect!")
end
local signal = Signal.new()
local connection = signal:connect(callback)
connection:disconnect()
signal:fire()
end)
it("should fire handlers in order", function()
local signal = Signal.new()
local x = 0
local y = 0
local callback1 = function()
expect(x).to.equal(0)
expect(y).to.equal(0)
x = x + 1
end
local callback2 = function()
expect(x).to.equal(1)
expect(y).to.equal(0)
y = y + 1
end
signal:connect(callback1)
signal:connect(callback2)
signal:fire()
expect(x).to.equal(1)
expect(y).to.equal(1)
end)
it("should continue firing despite mid-event disconnection", function()
local signal = Signal.new()
local countA = 0
local countB = 0
local connectionA
connectionA = signal:connect(function()
connectionA:disconnect()
countA = countA + 1
end)
signal:connect(function()
countB = countB + 1
end)
signal:fire()
expect(countA).to.equal(1)
expect(countB).to.equal(1)
end)
it("should skip listeners that were disconnected during event evaluation", function()
local signal = Signal.new()
local countA = 0
local countB = 0
local connectionB
signal:connect(function()
countA = countA + 1
connectionB:disconnect()
end)
connectionB = signal:connect(function()
countB = countB + 1
end)
signal:fire()
expect(countA).to.equal(1)
expect(countB).to.equal(0)
end)
end
@@ -0,0 +1,113 @@
local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp")
local TextService = game:GetService("TextService")
local Text = {}
-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results
local MAX_BOUND = 10000
-- Remove with EngineFeatureTextBoundsRoundUp
Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0)
if not EngineFeatureTextBoundsRoundUp then
Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2)
end
-- Wrapper function for GetTextSize
function Text.GetTextBounds(text, font, fontSize, bounds)
return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING
end
function Text.GetTextWidth(text, font, fontSize)
return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X
end
function Text.GetTextHeight(text, font, fontSize, widthCap)
return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y
end
-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation
function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker)
overflowMarker = overflowMarker or ""
if Text.GetTextWidth(text, font, fontSize) > widthInPixels then
-- A binary search may be more efficient
local lastText = ""
for _, stopIndex in utf8.graphemes(text) do
local newText = string.sub(text, 1, stopIndex) .. overflowMarker
if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then
return lastText
end
lastText = newText
end
else -- No truncation needed
return text
end
return ""
end
function Text.TruncateTextLabel(textLabel, overflowMarker)
textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font,
textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker)
end
-- Remove whitespace from the beginning and end of the string
function Text.Trim(str)
if type(str) ~= "string" then
error(string.format("Text.Trim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("^%s*(.-)%s*$", "%1"))
end
-- Remove whitespace from the end of the string
function Text.RightTrim(str)
if type(str) ~= "string" then
error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("%s+$", ""))
end
-- Remove whitespace from the beginning of the string
function Text.LeftTrim(str)
if type(str) ~= "string" then
error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("^%s+", ""))
end
-- Replace multiple whitespace with one; remove leading and trailing whitespace
function Text.SpaceNormalize(str)
if type(str) ~= "string" then
error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2)
end
return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , ""))
end
-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text.
function Text.Split(str, pattern)
if type(str) ~= "string" then
error(string.format("Text.Split called on non-string type %s.", type(str)), 2)
elseif type(pattern) ~= "string" then
error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2)
elseif pattern == "" then
error("Text.Split called with an empty pattern.", 2)
end
local result = {}
local currentPosition = 1
while true do
local patternStart, patternEnd = string.find(str, pattern, currentPosition, true)
if not patternStart or not patternEnd then break end
table.insert(result, string.sub(str, currentPosition, patternStart - 1))
currentPosition = patternEnd + 1
end
table.insert(result, string.sub(str, currentPosition, string.len(str)))
return result
end
return Text
@@ -0,0 +1,410 @@
return function()
local Text = require(script.Parent.Text)
describe("GetTextBounds", function()
it("should return a bounds of padding width and font-size height when the string is empty", function()
local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x)
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
it("should return the height and width of a string as one line with large bounds", function()
local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
it("should return the height of the string as multiple lines with short bounds", function()
local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000))
expect(bounds.Y > 18).to.equal(true)
end)
end)
describe("GetTextHeight", function()
it("should return height equal to font size when string is empty", function()
local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0)
expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
end)
describe("GetTextWidth", function()
it("should return width equal to 1 when string is empty", function()
local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18)
expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x)
end)
end)
describe("Truncate", function()
it("should return empty string", function()
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...")
expect(emptyQuery).to.be.a("string")
expect(emptyQuery).to.equal("")
end)
it("should return empty string for not empty box", function()
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...")
expect(emptyQuery).to.be.a("string")
expect(emptyQuery).to.equal("")
end)
it("should truncate with ...", function()
local reallyLongQuery = Text.Truncate(
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...")
expect(reallyLongQuery).to.equal("One Two Thre...")
end)
it("should truncate without a ...", function()
local reallyLongQueryNoOverflowMarker = Text.Truncate(
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100)
expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ")
end)
it("should not truncate", function()
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100)
expect(shouldFitQuery).to.equal("One Two")
end)
it("should not truncate, off by one check", function()
local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100)
expect(oneCharQuery).to.equal("O")
end)
it("should truncate, off by one check", function()
local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0)
expect(oneCharNoRoomQuery).to.equal("")
end)
it("should perform a negative width check", function()
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...")
expect(shouldFitQuery).to.equal("")
end)
itFIXME("should truncate long graphemes properly", function()
-- 11-byte rainbow flag grapheme
-- Flag, zero-space-joiner, rainbow
local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752)
local oneFlagWithinLimit = Text.Truncate(
rainbowFlag, Enum.Font.SourceSans, 18, 100, "...")
expect(oneFlagWithinLimit).to.equal(rainbowFlag)
local twoRainbowFlags = rainbowFlag .. rainbowFlag
local twoFlagsAreFine = Text.Truncate(
twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
expect(twoFlagsAreFine).to.equal(twoRainbowFlags)
local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags
local fourFlagsIsTooLong = Text.Truncate(
fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation
end)
end)
describe("TruncateTextLabel", function()
it("should use text label attributes to truncate text", function()
local screenGui = Instance.new("ScreenGui")
local textLabel = Instance.new("TextLabel")
textLabel.Size = UDim2.new(0, 100, 0, 32)
textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
textLabel.Font = Enum.Font.SourceSans
textLabel.TextSize = 18
textLabel.Parent = screenGui
Text.TruncateTextLabel(textLabel)
expect(textLabel.Text).to.equal("One Two Three ")
end)
end)
describe("TrimString", function()
it("Should trim the string properly 1", function()
local trimmedInput = Text.Trim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 2", function()
local trimmedInput = Text.Trim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 3", function()
local trimmedInput = Text.Trim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 4", function()
local trimmedInput = Text.Trim(" ab ")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 5", function()
local trimmedInput = Text.Trim(" a b ")
local expected = "a b"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 6", function()
local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ")
local expected = "a"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string with unicode characters properly", function()
local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 7", function()
local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 8", function()
local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("RightTrimString", function()
it("Should right trim the string properly 1", function()
local trimmedInput = Text.RightTrim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 2", function()
local trimmedInput = Text.RightTrim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 3", function()
local trimmedInput = Text.RightTrim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 4", function()
local trimmedInput = Text.RightTrim(" ab ")
local expected = " ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 5", function()
local trimmedInput = Text.RightTrim(" a b ")
local expected = " a b"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 6", function()
local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ")
local expected = "\r\n\t\f a"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string with unicode characters properly", function()
local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 7", function()
local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 8", function()
local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("LeftTrimString", function()
it("Should left trim the string properly 1", function()
local trimmedInput = Text.LeftTrim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 2", function()
local trimmedInput = Text.LeftTrim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 3", function()
local trimmedInput = Text.LeftTrim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 4", function()
local trimmedInput = Text.LeftTrim(" ab ")
local expected = "ab "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 5", function()
local trimmedInput = Text.LeftTrim(" a b ")
local expected = "a b "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 6", function()
local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ")
local expected = "a\r\n\t\f "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string with unicode characters properly", function()
local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 7", function()
local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly", function()
local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("SpaceNormalize", function()
it("should remove multiple spaces between words", function()
local a = "This is not a normal sentence."
expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.")
end)
it("should remove leading and trailing whitespace", function()
local a = " SpaceTabSpaceTab "
expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab")
end)
it("should not change a string with no whitespace", function()
local a = "There'sNo%Whit.e\\space--InThis."
expect(Text.SpaceNormalize(a)).to.equal(a)
end)
it("should remove all whitespace in a string that is nothing but whitespace", function()
local a = " "
expect(Text.SpaceNormalize(a)).to.equal("")
end)
it("should handle the case where the string is empty", function()
local a = ""
expect(Text.SpaceNormalize(a)).to.equal(a)
end)
it("should throw an error if called an a non-string type", function()
local a = { first = 1, second = 2 }
expect(function()
Text.SpaceNormalize(a)
end).to.throw()
end)
end)
describe("Split", function()
local function tableEquals(tb1, tb2)
local tables = { tb1, tb2 }
for _,tb in ipairs(tables) do
for key in pairs(tb) do
if tb1[key] ~= tb2[key] then
return false
end
end
end
return true
end
it("should return the correct table for your standard use case", function()
local a = "this,is,comma,separated"
local pattern = ","
local expectedResult = {
[1] = "this",
[2] = "is",
[3] = "comma",
[4] = "separated",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should not remove whitespace", function()
local a = " SpaceTab , , Space"
local pattern = ","
local expectedResult = {
[1] = " SpaceTab ",
[2] = " ",
[3] = " Space",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should treat regular expressions as plain text", function()
local a = "Notyour^%s+normalstring.Thisisasecondsentence."
local b = "."
local c = "^%s+"
local d = "%A"
local expectedB = {
[1] = "Notyour^%s+normalstring",
[2] = "Thisisasecondsentence",
[3] = "",
}
local expectedC = {
[1] = "Notyour",
[2] = "normalstring.Thisisasecondsentence."
}
local expectedD = {
[1] = "Notyour^%s+normalstring.Thisisasecondsentence."
}
expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true)
expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true)
expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true)
end)
it("should work when pattern is not in string", function()
local a = "The pattern you are looking for does not exist."
local pattern = ","
local expectedResult = {
[1] = "The pattern you are looking for does not exist.",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should work when called on an empty string", function()
local a = ""
local pattern = ","
local expectedResult = {
[1] = "",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should throw an error if called on an empty pattern", function()
local a = "The pattern definitely doesn't exist here."
local pattern = ""
expect(function()
Text.Split(a, pattern)
end).to.throw()
end)
it("should throw an error if called an a non-string type", function()
local a = { first = 1, second = 2 }
local b = "an actual string"
expect(function()
Text.Split(a, b)
end).to.throw()
expect(function()
Text.Split(b, a)
end).to.throw()
end)
end)
end
@@ -0,0 +1,68 @@
--[[␍
memoize creates a function as a wrapper that caches the last outputs of a function.␍
This is useful if you know that the function should return the same output every␍
time it is run with the same inputs. The function should only return an output, and␍
not have any side effects. These side effects are not cached.␍
Without memoize's caching, even though the function ouputs the same values, the␍
memory locations of the values are different; tables made in the function, even if␍
they have the same values, won't be the same tables.␍
memoize only caches the last set of inputs and ouputs. This means that it is only␍
helpful when the function is likely to be called with the same inputs multiple␍
times in a row. This is the case with most Roact use cases.␍
Note that memoize only does a ** shallow check on table inputs ** . This means␍
that if the same table is input but the elements of the table are different then␍
it will be assumed that the table has not changed.␍
In addition to all the previous warnings, memoize strips trailing nils. This means␍
that if foo is a memoized function and we call foo(), then foo(nil) will return a␍
cached value. This is opposed to how print handles input. print() only outputs a␍
new line, but print(nil) outputs "nil". This is because varargs can detect the␍
number of arguments passed in. So, be careful when using memoize with varargs.␍
Trailing nils will be stripped.␍
The wrapper can take any number of inputs and give any number of outputs.␍
Leading and interspersed nils are handled gracefully. Trailing nils on the input␍
are stripped.␍
]]
local function captureSize(...)
return {...}, select("#", ...)
end
local function memoize(func)
assert(type(func) == "function", "memoize requires a function to memoize")
local lastArgs
local lastNumArgs
local lastOutput
local lastNumOutput
return function(...)
local numArgs = select("#", ...)
while numArgs > 0 and select(numArgs, ...) == nil do
numArgs = numArgs - 1
end
if numArgs ~= lastNumArgs then
lastArgs = {...}
lastNumArgs = numArgs
lastOutput, lastNumOutput = captureSize(func(...))
return unpack(lastOutput, 1, lastNumOutput)
end
for i = 1, lastNumArgs do
if select(i, ...) ~= lastArgs[i] then
lastArgs = {...}
lastOutput, lastNumOutput = captureSize(func(...))
break
end
end
return unpack(lastOutput, 1, lastNumOutput)
end
end
return memoize
@@ -0,0 +1,213 @@
return function()
local memoize = require(script.Parent.memoize)
describe("memoize", function()
it("should handle arity 0", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
expect(identity()).to.equal(nil)
expect(identity(nil)).to.equal(nil)
expect(identity(nil, nil)).to.equal(nil)
expect(callCount).to.equal(1)
end)
it("should handle arity 1", function()
local callCount = 0
local identity = memoize(function(a)
callCount = callCount + 1
return a
end)
expect(identity(5)).to.equal(5)
expect(identity(5)).to.equal(5)
expect(callCount).to.equal(1)
expect(identity(6)).to.equal(6)
expect(callCount).to.equal(2)
expect(identity(5)).to.equal(5)
expect(callCount).to.equal(3)
end)
it("should handle arity 2", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(1)
a, b = identity(6, 5)
expect(a).to.equal(6)
expect(b).to.equal(5)
expect(callCount).to.equal(2)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(3)
end)
it("should handle mixed arity", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(1)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity()
expect(a).to.equal(nil)
expect(b).to.equal(nil)
a, b = identity()
expect(a).to.equal(nil)
expect(b).to.equal(nil)
expect(callCount).to.equal(3)
end)
it("should handle trailing nils", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, nil)
expect(a).to.equal(5)
expect(b).to.equal(nil)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(1)
a, b = identity(7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(3)
end)
it("should handle leading nils", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
expect(callCount).to.equal(1)
a, b = identity(7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
expect(callCount).to.equal(3)
end)
it("should handle interspersed nils", function()
local callCount = 0
local identity = memoize(function(a, b, c, d)
callCount = callCount + 1
return a, b, c, d
end)
local a, b, c, d
a, b, c, d = identity(7, nil, 7, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
-- Trailing nils can affect how interspersed nils are handled
a, b, c, d = identity(7, nil, 7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
expect(callCount).to.equal(1)
a, b, c, d = identity(7, nil, nil, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(nil)
expect(d).to.equal(nil)
expect(callCount).to.equal(2)
a, b, c, d = identity(7, nil, 7, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
expect(callCount).to.equal(3)
end)
end)
end
@@ -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,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, displayName)
return {
userId = tostring(userId),
displayName = displayName,
}
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(countryCode)
return {
countryCode = countryCode,
}
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,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,11 @@
local CorePackages = game:GetService("CorePackages")
local Rodux = require(CorePackages.Rodux)
local ReceivedUserCountryCode = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedUserCountryCode)
local DEFAULT_STATE = ""
return Rodux.createReducer(DEFAULT_STATE, {
[ReceivedUserCountryCode.name] = function(state, action)
return action.countryCode
end,
})
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local ReceivedUserCountryCode = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedUserCountryCode)
local CountryCodeReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.CountryCode)
describe("CountryCode", function()
it("should be and empty string by default", function()
local state = CountryCodeReducer(nil, {})
expect(state).to.equal("")
end)
it("should not be modified by other actions", function()
local oldState = CountryCodeReducer(nil, {})
local newState = CountryCodeReducer(oldState, { type = "not a real action" })
expect(newState).to.equal(oldState)
end)
it("should be changed using ReceivedUserCountryCode", function()
local state = CountryCodeReducer(nil, {})
state = CountryCodeReducer(state, ReceivedUserCountryCode("US"))
expect(state).to.equal("US")
state = CountryCodeReducer(state, ReceivedUserCountryCode(""))
expect(state).to.equal("")
end)
end)
end
@@ -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,107 @@
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 ReceivedDisplayName = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedDisplayName)
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
elseif action.type == ReceivedDisplayName.name then
local user = state[action.userId]
if user then
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
displayName = action.displayName,
}),
})
end
end
return state
end

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