add gs
This commit is contained in:
@@ -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,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", 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", 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", 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", 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,123 @@
|
||||
local LuaUseUtf8TextTruncation = settings():GetFFlag("LuaUseUtf8TextTruncation")
|
||||
local TextMeasureTemporaryPatch = settings():GetFFlag("TextMeasureTemporaryPatch")
|
||||
|
||||
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
|
||||
|
||||
-- TODO(CLIPLAYEREX-1633): We can remove this padding patch after fixing TextService:GetTextSize sizing bug
|
||||
Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0)
|
||||
|
||||
if TextMeasureTemporaryPatch 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
|
||||
if LuaUseUtf8TextTruncation 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
|
||||
for len = #text, 1, -1 do
|
||||
local newText = string.sub(text, 1, len) .. overflowMarker
|
||||
if Text.GetTextWidth(newText, font, fontSize) <= widthInPixels then
|
||||
return newText
|
||||
end
|
||||
end
|
||||
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, 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", function()
|
||||
local trimmedInput = Text.Trim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly", function()
|
||||
local trimmedInput = Text.Trim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly", function()
|
||||
local trimmedInput = Text.Trim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly", function()
|
||||
local trimmedInput = Text.Trim(" ab ")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly", function()
|
||||
local trimmedInput = Text.Trim(" a b ")
|
||||
local expected = "a b"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly", 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", function()
|
||||
local trimmedInput = Text.Trim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly", 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", function()
|
||||
local trimmedInput = Text.RightTrim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly", function()
|
||||
local trimmedInput = Text.RightTrim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly", function()
|
||||
local trimmedInput = Text.RightTrim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly", function()
|
||||
local trimmedInput = Text.RightTrim(" ab ")
|
||||
local expected = " ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly", function()
|
||||
local trimmedInput = Text.RightTrim(" a b ")
|
||||
local expected = " a b"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly", 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", function()
|
||||
local trimmedInput = Text.RightTrim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = " 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly", 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", 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(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly", function()
|
||||
local trimmedInput = Text.LeftTrim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly", function()
|
||||
local trimmedInput = Text.LeftTrim(" ab ")
|
||||
local expected = "ab "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly", function()
|
||||
local trimmedInput = Text.LeftTrim(" a b ")
|
||||
local expected = " a b "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly", 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", 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,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)
|
||||
+8
@@ -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)
|
||||
+9
@@ -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)
|
||||
+8
@@ -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)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(placesInfos)
|
||||
return {
|
||||
placesInfos = placesInfos,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId)
|
||||
return {
|
||||
userId = userId,
|
||||
}
|
||||
end)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(deviceOrientation)
|
||||
return {
|
||||
deviceOrientation = deviceOrientation,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(count)
|
||||
return {
|
||||
count = count,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,26 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
--[[
|
||||
Passes a table that looks like this : { "universeId" : {json}, ... }
|
||||
|
||||
{
|
||||
"26034470" : {
|
||||
universeId : "26034470",
|
||||
placeId : "70542190",
|
||||
url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814,
|
||||
final : true
|
||||
}, {...}, ...
|
||||
}
|
||||
|
||||
]]
|
||||
|
||||
--TODO MOBLUAPP-778 Refactor improper Setter Actions.
|
||||
return Action(script.Name, function(thumbnailsTable)
|
||||
assert(type(thumbnailsTable) == "table",
|
||||
string.format("SetGameThumbnails action expects thumbnailsTable to be a table, was %s", type(thumbnailsTable)))
|
||||
|
||||
return {
|
||||
thumbnails = thumbnailsTable
|
||||
}
|
||||
end)
|
||||
+11
@@ -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)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, membershipType)
|
||||
return {
|
||||
userId = userId,
|
||||
membershipType = membershipType,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, presence, lastLocation)
|
||||
return {
|
||||
userId = tostring(userId),
|
||||
presence = presence,
|
||||
lastLocation = lastLocation,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,13 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize)
|
||||
return {
|
||||
userId = userId,
|
||||
image = image,
|
||||
thumbnailType = thumbnailType,
|
||||
thumbnailSize = thumbnailSize,
|
||||
}
|
||||
end)
|
||||
@@ -0,0 +1,51 @@
|
||||
local Workspace = game:GetService("Workspace")
|
||||
local RunService = game:GetService('RunService')
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3)
|
||||
local BAR_MAX_SIZE = 15
|
||||
local BAR_MAX_AMPLITUDE = 40
|
||||
local BAR_DIAMETER = 4
|
||||
local BAR_PERIOD = 1.25
|
||||
|
||||
local LoadingBar = Roact.Component:extend("LoadingBar")
|
||||
|
||||
function LoadingBar:init()
|
||||
self.barRef = Roact.createRef()
|
||||
end
|
||||
|
||||
function LoadingBar:render()
|
||||
local zIndex = self.props.ZIndex
|
||||
|
||||
return Roact.createElement("ImageLabel", {
|
||||
Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png",
|
||||
ScaleType = "Slice",
|
||||
SliceCenter = BAR_SLICE_CENTER,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = zIndex,
|
||||
[Roact.Ref] = self.barRef,
|
||||
})
|
||||
end
|
||||
|
||||
function LoadingBar:didMount()
|
||||
self.connection = RunService.RenderStepped:Connect(function()
|
||||
local t = Workspace.DistributedGameTime
|
||||
local instance = self.barRef.current
|
||||
local period = 2.0 * math.pi / BAR_PERIOD
|
||||
|
||||
local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period))
|
||||
instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER)
|
||||
|
||||
local x = BAR_MAX_AMPLITUDE * math.cos(t*period)
|
||||
instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0)
|
||||
end)
|
||||
end
|
||||
|
||||
function LoadingBar:willUnmount()
|
||||
self.connection:Disconnect()
|
||||
end
|
||||
|
||||
return LoadingBar
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
return function()
|
||||
local LoadingBar = require(script.Parent.LoadingBar)
|
||||
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
local element = Roact.createElement(LoadingBar, {
|
||||
Position = UDim2.new(0.5, 0, 0.5, 5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
local RetrievalStatus = {}
|
||||
|
||||
local EnumValues =
|
||||
{
|
||||
NotStarted = "NotStarted",
|
||||
Fetching = "Fetching",
|
||||
Done = "Done",
|
||||
Failed = "Failed",
|
||||
}
|
||||
|
||||
setmetatable(RetrievalStatus,
|
||||
{
|
||||
__newindex = function(t, key, index)
|
||||
end,
|
||||
__index = function(t, index)
|
||||
assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index)))
|
||||
return EnumValues[index]
|
||||
end
|
||||
})
|
||||
|
||||
return RetrievalStatus
|
||||
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
|
||||
return {
|
||||
[0] = User.PresenceType.OFFLINE,
|
||||
[1] = User.PresenceType.ONLINE,
|
||||
[2] = User.PresenceType.IN_GAME,
|
||||
[3] = User.PresenceType.IN_STUDIO,
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
|
||||
|
||||
local FIntLuaHomePageEnablePlacesListV1 = settings():GetFVariable("LuaHomePageEnablePlacesListV1V361")
|
||||
|
||||
-- Don't call this function globally because we cannot get the userId
|
||||
-- Reason: The LocalPlayer wouldn't be ready if we called it globally.
|
||||
return function()
|
||||
local throttleNumber = tonumber(FIntLuaHomePageEnablePlacesListV1)
|
||||
local userId = Players.LocalPlayer.UserId
|
||||
|
||||
return ThrottleUserId(throttleNumber, userId)
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local FFlagPlacesListV1 = require(CorePackages.AppTempCommon.LuaApp.Flags.IsPlacesListV1Enabled)
|
||||
|
||||
local FFlagLuaHomePeopleListV1V361 = settings():GetFFlag("LuaHomePeopleListV1V361")
|
||||
|
||||
return function()
|
||||
-- When we should fetch games data could change in the future
|
||||
-- Right now, we should fetch games data when people list on or place list on
|
||||
return FFlagLuaHomePeopleListV1V361 or FFlagPlacesListV1()
|
||||
end
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
return function(requestImpl, conversationId, messageText)
|
||||
local payload = HttpService:JSONEncode({
|
||||
conversationId = conversationId,
|
||||
message = messageText,
|
||||
})
|
||||
|
||||
local url = string.format("%s/send-message", Url.CHAT_URL)
|
||||
|
||||
return requestImpl(url, "POST", { postBody = payload })
|
||||
end
|
||||
+14
@@ -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
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
This endpoint returns a promise that resolves to:
|
||||
[
|
||||
{
|
||||
"final": true,
|
||||
"url": "string",
|
||||
"retryToken": "string",
|
||||
"universeId": 0,
|
||||
"placeId": 0
|
||||
}, {...}, ...
|
||||
]
|
||||
]]
|
||||
|
||||
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
|
||||
-- imageTokens - (array<long>) the placeIds of the places you want to get thumbnails for
|
||||
-- height - (int) the height of the asset to render
|
||||
-- width - (int) the width of the asset to render
|
||||
return function(requestImpl, imageTokens, height, width)
|
||||
local args = {}
|
||||
|
||||
if height then
|
||||
table.insert(args, string.format("height=%d", height))
|
||||
end
|
||||
|
||||
if width then
|
||||
table.insert(args, string.format("width=%d", width))
|
||||
end
|
||||
|
||||
-- append all of the thumbnail tokens
|
||||
local totalTokens = 0
|
||||
for _, value in pairs(imageTokens) do
|
||||
totalTokens = totalTokens + 1
|
||||
table.insert(args, string.format("imageTokens=%s", value))
|
||||
end
|
||||
if totalTokens == 0 then
|
||||
error("cannot fetch thumbnails without tokens")
|
||||
end
|
||||
|
||||
-- construct the url
|
||||
local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&"))
|
||||
|
||||
-- return a promise of the result listed above
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+33
@@ -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
|
||||
+17
@@ -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
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
--[[
|
||||
This endpoint returns a promise that resolves to:
|
||||
|
||||
[
|
||||
{
|
||||
"success:" true,
|
||||
"count": "0"
|
||||
},
|
||||
]
|
||||
]]--
|
||||
|
||||
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
|
||||
return function(requestImpl)
|
||||
|
||||
local argTable = {
|
||||
userId = Players.LocalPlayer.UserId,
|
||||
}
|
||||
|
||||
local args = Url:makeQueryString(argTable)
|
||||
local url = string.format("%s/user/get-friendship-count?%s",
|
||||
Url.API_URL, tostring(Players.LocalPlayer.UserId), args
|
||||
)
|
||||
|
||||
return requestImpl(url, "GET")
|
||||
end
|
||||
+10
@@ -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
|
||||
+25
@@ -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
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
|
||||
local THUMBNAIL_TYPE_BY_NAME = {
|
||||
AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail,
|
||||
HeadShot = Enum.ThumbnailType.HeadShot,
|
||||
}
|
||||
|
||||
local THUMBNAIL_SIZE_BY_NAME = {
|
||||
Size48x48 = Enum.ThumbnailSize.Size48x48,
|
||||
Size60x60 = Enum.ThumbnailSize.Size60x60,
|
||||
Size100x100 = Enum.ThumbnailSize.Size100x100,
|
||||
Size150x150 = Enum.ThumbnailSize.Size150x150,
|
||||
Size352x352 = Enum.ThumbnailSize.Size352x352
|
||||
}
|
||||
|
||||
return function(userId, thumbnailType, thumbnailSize)
|
||||
return Promise.new(function(resolve, reject)
|
||||
--Async methods will yield the thread
|
||||
spawn(function()
|
||||
local result = {success = false}
|
||||
local success, message = pcall(function()
|
||||
local image, isFinal = Players:GetUserThumbnailAsync(
|
||||
userId, THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize]
|
||||
)
|
||||
|
||||
result = {
|
||||
success = true,
|
||||
id = userId,
|
||||
thumbnailType = thumbnailType,
|
||||
thumbnailSize = thumbnailSize,
|
||||
|
||||
image = isFinal and image or nil,
|
||||
isFinal = isFinal,
|
||||
}
|
||||
end)
|
||||
|
||||
if success then
|
||||
resolve(result)
|
||||
else
|
||||
result.message = message
|
||||
reject(result)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
--[[
|
||||
Url Constructor
|
||||
|
||||
Provides a single location for base urls.
|
||||
|
||||
]]--
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
-- helper functions
|
||||
local function parseBaseUrlInformation()
|
||||
-- get the current base url from the current configuration
|
||||
local baseUrl = ContentProvider.BaseUrl
|
||||
|
||||
-- keep a copy of the base url (https://www.roblox.com/)
|
||||
-- append a trailing slash if there isn't one
|
||||
if baseUrl:sub(#baseUrl) ~= "/" then
|
||||
baseUrl = baseUrl .. "/"
|
||||
end
|
||||
|
||||
-- parse out scheme (http, https)
|
||||
local _, schemeEnd = baseUrl:find("://")
|
||||
|
||||
-- parse out the prefix (www, kyle, ying, etc.)
|
||||
local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1)
|
||||
local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1)
|
||||
|
||||
-- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.)
|
||||
local baseDomain = baseUrl:sub(prefixEnd + 1)
|
||||
|
||||
return baseUrl, basePrefix, baseDomain
|
||||
end
|
||||
local function preventTableModification(aTable, key, value)
|
||||
error("Attempt to modify read-only table")
|
||||
end
|
||||
local function createReadOnlyTable(aTable)
|
||||
return setmetatable({}, {
|
||||
__index = aTable,
|
||||
__newindex = preventTableModification,
|
||||
__metatable = false
|
||||
});
|
||||
end
|
||||
|
||||
|
||||
-- url construction building blocks
|
||||
local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation()
|
||||
|
||||
-- construct urls once
|
||||
local _baseApiUrl = string.format("https://api.%s", _baseDomain)
|
||||
local _baseAuthUrl = string.format("https://auth.%s", _baseDomain)
|
||||
local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain)
|
||||
local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain)
|
||||
local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain)
|
||||
local _baseGamesUrl = string.format("https://games.%s", _baseDomain)
|
||||
local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain)
|
||||
local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain)
|
||||
local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain)
|
||||
local _baseWebUrl = string.format("https://web.%s", _baseDomain)
|
||||
local _baseWwwUrl = string.format("https://www.%s", _baseDomain)
|
||||
local _baseAdsUrl = string.format("https://ads.%s", _baseDomain)
|
||||
|
||||
-- public api
|
||||
local Url = {
|
||||
DOMAIN = _baseDomain,
|
||||
PREFIX = _basePrefix,
|
||||
BASE_URL = _baseUrl,
|
||||
API_URL = _baseApiUrl,
|
||||
AUTH_URL = _baseAuthUrl,
|
||||
GAME_URL = _baseGamesUrl,
|
||||
GAME_ASSET_URL = _baseGameAssetUrl,
|
||||
CHAT_URL = _baseChatUrl,
|
||||
FRIEND_URL = _baseFriendUrl,
|
||||
PRESENCE_URL = _basePresenceUrl,
|
||||
NOTIFICATION_URL = _baseNotificationUrl,
|
||||
REALTIME_URL = _baseRealtimeUrl,
|
||||
WEB_URL = _baseWebUrl,
|
||||
WWW_URL = _baseWwwUrl,
|
||||
ADS_URL = _baseAdsUrl,
|
||||
}
|
||||
|
||||
function Url:getUserProfileUrl(userId)
|
||||
return string.format("%susers/%s/profile", self.BASE_URL, userId)
|
||||
end
|
||||
|
||||
function Url:isVanitySite()
|
||||
return self.PREFIX ~= "www"
|
||||
end
|
||||
|
||||
-- data - (table<string, string>) a table of key/value pairs to format
|
||||
function Url:makeQueryString(data)
|
||||
--NOTE - This function can be used to create a query string of parameters
|
||||
-- at the end of url query, or create a application/form-url-encoded post body string
|
||||
local params = {}
|
||||
|
||||
-- NOTE - Arrays are handled, but generally data is expected to be flat.
|
||||
for key, value in pairs(data) do
|
||||
if value ~= nil then --for optional params
|
||||
if type(value) == "table" then
|
||||
for i = 1, #value do
|
||||
table.insert(params, key .. "=" .. value[i])
|
||||
end
|
||||
else
|
||||
table.insert(params, key .. "=" .. tostring(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(params, "&")
|
||||
end
|
||||
|
||||
|
||||
-- prevent anyone from modifying this table:
|
||||
Url = createReadOnlyTable(Url)
|
||||
|
||||
return Url
|
||||
@@ -0,0 +1,12 @@
|
||||
return function()
|
||||
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
local Url = require(script.Parent.Url)
|
||||
|
||||
it("The base url has not been changed for debugging", function()
|
||||
local baseUrl = ContentProvider.BaseUrl
|
||||
|
||||
expect(baseUrl).to.equal(Url.BASE_URL)
|
||||
end)
|
||||
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
--[[
|
||||
A function to return a fake ID, used for testing
|
||||
]]
|
||||
|
||||
local lastId = 0
|
||||
|
||||
return function()
|
||||
lastId = lastId + 1
|
||||
return ("MOCK-%d"):format(lastId)
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
local ThumbnailRequest = {}
|
||||
|
||||
function ThumbnailRequest.new()
|
||||
local self = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ThumbnailRequest.fromData(thumbnailType, thumbnailSize)
|
||||
local self = ThumbnailRequest.new()
|
||||
|
||||
self.thumbnailType = thumbnailType
|
||||
self.thumbnailSize = thumbnailSize
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return ThumbnailRequest
|
||||
@@ -0,0 +1,117 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
|
||||
|
||||
local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss")
|
||||
|
||||
local User = {}
|
||||
|
||||
User.PresenceType = {
|
||||
OFFLINE = "OFFLINE",
|
||||
ONLINE = "ONLINE",
|
||||
IN_GAME = "IN_GAME",
|
||||
IN_STUDIO = "IN_STUDIO",
|
||||
}
|
||||
|
||||
function User.new()
|
||||
local self = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function User.mock()
|
||||
local self = User.new()
|
||||
|
||||
self.id = MockId()
|
||||
|
||||
self.isFetching = false
|
||||
self.isFriend = false
|
||||
self.lastLocation = nil
|
||||
self.name = "USER NAME"
|
||||
self.universeId = nil
|
||||
self.placeId = nil
|
||||
self.rootPlaceId = nil
|
||||
self.gameInstanceId = nil
|
||||
self.lastOnline = 0
|
||||
self.presence = User.PresenceType.OFFLINE
|
||||
self.membership = nil
|
||||
if not FFlagFixUsersReducerDataLoss then
|
||||
self.thumbnails = {}
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function User.fromData(id, name, isFriend)
|
||||
local self = User.new()
|
||||
|
||||
self.id = tostring(id)
|
||||
|
||||
self.isFetching = false
|
||||
self.isFriend = isFriend
|
||||
self.lastLocation = nil
|
||||
self.name = name
|
||||
self.universeId = nil
|
||||
self.placeId = nil
|
||||
self.rootPlaceId = nil
|
||||
self.gameInstanceId = nil
|
||||
self.lastOnline = 0
|
||||
if FFlagFixUsersReducerDataLoss then
|
||||
self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil
|
||||
else
|
||||
self.presence = (self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or
|
||||
User.PresenceType.OFFLINE
|
||||
self.thumbnails = {}
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function User.compare(user1, user2)
|
||||
assert(not(user1 == nil and user2 == nil))
|
||||
assert(user1 == nil or typeof(user1) == "table")
|
||||
assert(user2 == nil or typeof(user2) == "table")
|
||||
|
||||
-- Return false if any of the provided input is nil(empty).
|
||||
if not user1 or not user2 then
|
||||
return false
|
||||
end
|
||||
|
||||
for field, valueInUser2 in pairs(user2) do
|
||||
if user1[field] ~= valueInUser2 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for field, valueInUser1 in pairs(user1) do
|
||||
if user2[field] ~= valueInUser1 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function User.userPresenceToText(localization, user)
|
||||
local presence = user.presence
|
||||
local lastLocation = user.lastLocation
|
||||
|
||||
if not presence then
|
||||
return ''
|
||||
end
|
||||
|
||||
if presence == User.PresenceType.OFFLINE then
|
||||
return localization:Format("Common.Presence.Label.Offline")
|
||||
elseif presence == User.PresenceType.ONLINE then
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then
|
||||
if lastLocation ~= nil then
|
||||
return lastLocation
|
||||
else
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return User
|
||||
@@ -0,0 +1,68 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
|
||||
it("should detect if provided users are identical", function()
|
||||
local clone1 = User.fromData(1, "Andy", true)
|
||||
local clone2 = Immutable.Set(clone1, "isFriend", true)
|
||||
|
||||
local result = User.compare(clone1, clone2)
|
||||
expect(result).to.equal(true)
|
||||
|
||||
result = User.compare(clone2, clone1)
|
||||
expect(result).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should detect when there is one or more fields with different values", function()
|
||||
local andy = User.fromData(1, "Andy", true)
|
||||
local ollie = Immutable.Set(andy, "name", "Ollie")
|
||||
|
||||
local result = User.compare(andy, ollie)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(ollie, andy)
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should detect descrepancy when one user model contains more fields than the other", function()
|
||||
local andy = User.fromData(1, "Andy", true)
|
||||
local secretlyNotAndy = Immutable.Set(andy, "someDifferentField", "I'm Ollie!")
|
||||
|
||||
local result = User.compare(andy, secretlyNotAndy)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(secretlyNotAndy, andy)
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should throw if invalid input is provided", function()
|
||||
local aString = "I'm not a table."
|
||||
local teddy = User.fromData(1, "Teddy", true)
|
||||
|
||||
expect(function() User.compare(nil, nil) end).to.throw()
|
||||
expect(function() User.compare(aString, nil) end).to.throw()
|
||||
expect(function() User.compare(nil, aString) end).to.throw()
|
||||
expect(function() User.compare(aString, aString) end).to.throw()
|
||||
expect(function() User.compare(teddy, aString) end).to.throw()
|
||||
expect(function() User.compare(aString, teddy) end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return false if any one of the input is empty or nil)", function()
|
||||
local emptyTable = {}
|
||||
local teddy = User.fromData(1, "Teddy", true)
|
||||
|
||||
local result = User.compare(teddy, nil)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(nil, teddy)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(teddy, emptyTable)
|
||||
expect(result).to.equal(false)
|
||||
|
||||
result = User.compare(emptyTable, teddy)
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
local percentReporting = tonumber(settings():GetFVariable("PercentReportingNetworkProfileAfterStartup"))
|
||||
|
||||
local FEATURE_NAME = "NetworkProfileDuringStartup"
|
||||
local QUEUED_MEASURE_NAME = "Queued"
|
||||
local NAME_LOOKUP_MEASURE_NAME = "NameLookup"
|
||||
local CONNECT_MEASURE_NAME = "Connect"
|
||||
local SSL_HANDSHAKE_MEASURE_NAME = "SSLHandshake"
|
||||
local MAKE_REQUEST_MEASURE_NAME = "MakeRequest"
|
||||
local RECEIVE_RESPONSE_MEASURE_NAME = "ReceiveResponse"
|
||||
|
||||
local NetworkProfiler = {}
|
||||
NetworkProfiler.__index = NetworkProfiler
|
||||
|
||||
NetworkProfiler.aggregate = {
|
||||
queued = 0.0,
|
||||
nameLookup = 0.0,
|
||||
connect = 0.0,
|
||||
sslHandshake = 0.0,
|
||||
makeRequest = 0.0,
|
||||
receiveResponse = 0.0,
|
||||
}
|
||||
|
||||
function NetworkProfiler:track(timeProfile)
|
||||
self.aggregate.queued = self.aggregate.queued + timeProfile.queued
|
||||
if timeProfile.nameLookup >= 0 then
|
||||
self.aggregate.nameLookup = self.aggregate.nameLookup + timeProfile.nameLookup
|
||||
end
|
||||
if timeProfile.connect >= 0 then
|
||||
self.aggregate.connect = self.aggregate.connect + timeProfile.connect
|
||||
end
|
||||
if timeProfile.sslHandshake >= 0 then
|
||||
self.aggregate.sslHandshake = self.aggregate.sslHandshake + timeProfile.sslHandshake
|
||||
end
|
||||
if timeProfile.makeRequest >= 0 then
|
||||
self.aggregate.makeRequest = self.aggregate.makeRequest + timeProfile.makeRequest
|
||||
end
|
||||
if timeProfile.receiveResponse >= 0 then
|
||||
self.aggregate.receiveResponse = self.aggregate.receiveResponse + timeProfile.receiveResponse
|
||||
end
|
||||
end
|
||||
|
||||
function NetworkProfiler:report(reportToDiag)
|
||||
reportToDiag(FEATURE_NAME, QUEUED_MEASURE_NAME, self.aggregate.queued, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, NAME_LOOKUP_MEASURE_NAME, self.aggregate.nameLookup, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, CONNECT_MEASURE_NAME, self.aggregate.connect, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, SSL_HANDSHAKE_MEASURE_NAME, self.aggregate.sslHandshake, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, MAKE_REQUEST_MEASURE_NAME, self.aggregate.makeRequest, percentReporting)
|
||||
reportToDiag(FEATURE_NAME, RECEIVE_RESPONSE_MEASURE_NAME, self.aggregate.receiveResponse, percentReporting)
|
||||
end
|
||||
|
||||
return NetworkProfiler
|
||||
@@ -0,0 +1,343 @@
|
||||
--[[
|
||||
An implementation of Promises similar to Promise/A+.
|
||||
]]
|
||||
local TU = require(script.Parent.TableUtilities)
|
||||
|
||||
local PROMISE_DEBUG = true
|
||||
|
||||
-- If promise debugging is on, use a version of pcall that warns on failure.
|
||||
-- This is useful for finding errors that happen within Promise itself.
|
||||
local wpcall
|
||||
if PROMISE_DEBUG then
|
||||
wpcall = function(f, ...)
|
||||
local result = { pcall(f, ...) }
|
||||
|
||||
if not result[1] then
|
||||
warn(result[2])
|
||||
end
|
||||
|
||||
return unpack(result)
|
||||
end
|
||||
else
|
||||
wpcall = pcall
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a function that invokes a callback with correct error handling and
|
||||
resolution mechanisms.
|
||||
]]
|
||||
local function createAdvancer(callback, resolve, reject)
|
||||
return function(...)
|
||||
local result = { wpcall(callback, ...) }
|
||||
local ok = table.remove(result, 1)
|
||||
|
||||
if ok then
|
||||
resolve(unpack(result))
|
||||
else
|
||||
reject(unpack(result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function isEmpty(t)
|
||||
return next(t) == nil
|
||||
end
|
||||
|
||||
local Promise = {}
|
||||
Promise.__index = Promise
|
||||
|
||||
Promise.Status = {
|
||||
Started = "Started",
|
||||
Resolved = "Resolved",
|
||||
Rejected = "Rejected",
|
||||
}
|
||||
|
||||
--[[
|
||||
Constructs a new Promise with the given initializing callback.
|
||||
|
||||
This is generally only called when directly wrapping a non-promise API into
|
||||
a promise-based version.
|
||||
|
||||
The callback will receive 'resolve' and 'reject' methods, used to start
|
||||
invoking the promise chain.
|
||||
|
||||
For example:
|
||||
|
||||
local function get(url)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
resolve(HttpService:GetAsync(url))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
get("https://google.com")
|
||||
:andThen(function(stuff)
|
||||
print("Got some stuff!", stuff)
|
||||
end)
|
||||
]]
|
||||
function Promise.new(callback)
|
||||
local promise = {
|
||||
-- Used to locate where a promise was created
|
||||
_source = debug.traceback(),
|
||||
|
||||
-- A tag to identify us as a promise
|
||||
_type = "Promise",
|
||||
|
||||
_status = Promise.Status.Started,
|
||||
|
||||
-- A table containing a list of all results, whether success or failure.
|
||||
-- Only valid if _status is set to something besides Started
|
||||
_value = nil,
|
||||
|
||||
-- Queues representing functions we should invoke when we update!
|
||||
_queuedResolve = {},
|
||||
_queuedReject = {},
|
||||
}
|
||||
|
||||
setmetatable(promise, Promise)
|
||||
|
||||
local function resolve(...)
|
||||
promise:_resolve(...)
|
||||
end
|
||||
|
||||
local function reject(...)
|
||||
promise:_reject(...)
|
||||
end
|
||||
|
||||
local ok, err = wpcall(callback, resolve, reject)
|
||||
|
||||
if not ok and promise._status == Promise.Status.Started then
|
||||
reject(err)
|
||||
end
|
||||
|
||||
return promise
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately resolved value.
|
||||
]]
|
||||
function Promise.resolve(value)
|
||||
return Promise.new(function(resolve)
|
||||
resolve(value)
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately rejected value.
|
||||
]]
|
||||
function Promise.reject(value)
|
||||
return Promise.new(function(_, reject)
|
||||
reject(value)
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns a new promise that:
|
||||
* is resolved when all input promises resolve
|
||||
* is rejected if ANY input promises reject
|
||||
]]
|
||||
function Promise.all(...)
|
||||
local promises = {...}
|
||||
|
||||
-- check if we've been given a list of promises, not just a variable number of promises
|
||||
if type(promises[1]) == "table" and promises[1]._type ~= "Promise" then
|
||||
-- we've been given a table of promises already
|
||||
promises = promises[1]
|
||||
end
|
||||
|
||||
return Promise.new(function(resolve, reject)
|
||||
local isResolved = false
|
||||
local results = {}
|
||||
local totalCompleted = 0
|
||||
local function promiseCompleted(index, result)
|
||||
if isResolved then
|
||||
return
|
||||
end
|
||||
|
||||
results[index] = result
|
||||
totalCompleted = totalCompleted + 1
|
||||
|
||||
if totalCompleted == #promises then
|
||||
resolve(results)
|
||||
isResolved = true
|
||||
end
|
||||
end
|
||||
|
||||
if #promises == 0 then
|
||||
resolve(results)
|
||||
isResolved = true
|
||||
return
|
||||
end
|
||||
|
||||
for index, promise in ipairs(promises) do
|
||||
-- if a promise isn't resolved yet, add listeners for when it does
|
||||
if promise._status == Promise.Status.Started then
|
||||
promise:andThen(function(result)
|
||||
promiseCompleted(index, result)
|
||||
end):catch(function(reason)
|
||||
isResolved = true
|
||||
reject(reason)
|
||||
end)
|
||||
|
||||
-- if a promise is already resolved, move on
|
||||
elseif promise._status == Promise.Status.Resolved then
|
||||
promiseCompleted(index, unpack(promise._value))
|
||||
|
||||
-- if a promise is rejected, reject the whole chain
|
||||
else --if promise._status == Promise.Status.Rejected then
|
||||
isResolved = true
|
||||
reject(unpack(promise._value))
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Is the given object a Promise instance?
|
||||
]]
|
||||
function Promise.is(object)
|
||||
if type(object) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return object._type == "Promise"
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new promise that receives the result of this promise.
|
||||
|
||||
The given callbacks are invoked depending on that result.
|
||||
]]
|
||||
function Promise:andThen(successHandler, failureHandler)
|
||||
-- Create a new promise to follow this part of the chain
|
||||
return Promise.new(function(resolve, reject)
|
||||
-- Our default callbacks just pass values onto the next promise.
|
||||
-- This lets success and failure cascade correctly!
|
||||
|
||||
local successCallback = resolve
|
||||
if successHandler then
|
||||
successCallback = createAdvancer(successHandler, resolve, reject)
|
||||
end
|
||||
|
||||
local failureCallback = reject
|
||||
if failureHandler then
|
||||
failureCallback = createAdvancer(failureHandler, resolve, reject)
|
||||
end
|
||||
|
||||
if self._status == Promise.Status.Started then
|
||||
-- If we haven't resolved yet, put ourselves into the queue
|
||||
table.insert(self._queuedResolve, successCallback)
|
||||
table.insert(self._queuedReject, failureCallback)
|
||||
elseif self._status == Promise.Status.Resolved then
|
||||
-- This promise has already resolved! Trigger success immediately.
|
||||
successCallback(unpack(self._value))
|
||||
elseif self._status == Promise.Status.Rejected then
|
||||
-- This promise died a terrible death! Trigger failure immediately.
|
||||
failureCallback(unpack(self._value))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Used to catch any errors that may have occurred in the promise.
|
||||
]]
|
||||
function Promise:catch(failureCallback)
|
||||
return self:andThen(nil, failureCallback)
|
||||
end
|
||||
|
||||
--[[
|
||||
Yield until the promise is completed.
|
||||
|
||||
This matches the execution model of normal Roblox functions.
|
||||
]]
|
||||
function Promise:await()
|
||||
if self._status == Promise.Status.Started then
|
||||
local result
|
||||
local bindable = Instance.new("BindableEvent")
|
||||
|
||||
self:andThen(function(...)
|
||||
result = {...}
|
||||
bindable:Fire(true)
|
||||
end, function(...)
|
||||
result = {...}
|
||||
bindable:Fire(false)
|
||||
end)
|
||||
|
||||
local ok = bindable.Event:Wait()
|
||||
bindable:Destroy()
|
||||
|
||||
if not ok then
|
||||
error(tostring(result[1]), 2)
|
||||
end
|
||||
|
||||
return unpack(result)
|
||||
elseif self._status == Promise.Status.Resolved then
|
||||
return unpack(self._value)
|
||||
elseif self._status == Promise.Status.Rejected then
|
||||
error(tostring(self._value[1]), 2)
|
||||
end
|
||||
end
|
||||
|
||||
function Promise:_resolve(...)
|
||||
if self._status ~= Promise.Status.Started then
|
||||
return
|
||||
end
|
||||
|
||||
-- If the resolved value was a Promise, we chain onto it!
|
||||
if Promise.is((...)) then
|
||||
-- Without this warning, arguments sometimes mysteriously disappear
|
||||
if select("#", ...) > 1 then
|
||||
local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format(
|
||||
self._source
|
||||
)
|
||||
warn(message)
|
||||
end
|
||||
|
||||
(...):andThen(function(...)
|
||||
self:_resolve(...)
|
||||
end, function(...)
|
||||
self:_reject(...)
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self._status = Promise.Status.Resolved
|
||||
self._value = {...}
|
||||
|
||||
-- We assume that these callbacks will not throw errors.
|
||||
for _, callback in ipairs(self._queuedResolve) do
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
|
||||
function Promise:_reject(...)
|
||||
if self._status ~= Promise.Status.Started then
|
||||
return
|
||||
end
|
||||
|
||||
self._status = Promise.Status.Rejected
|
||||
self._value = {...}
|
||||
|
||||
-- If there are any rejection handlers, call those!
|
||||
if not isEmpty(self._queuedReject) then
|
||||
-- We assume that these callbacks will not throw errors.
|
||||
for _, callback in ipairs(self._queuedReject) do
|
||||
callback(...)
|
||||
end
|
||||
else
|
||||
-- At this point, no one was able to observe the error.
|
||||
-- An error handler might still be attached if the error occurred
|
||||
-- synchronously. We'll wait one tick, and if there are still no
|
||||
-- observers, then we should put a message in the console.
|
||||
|
||||
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
||||
TU.RecursiveToString((...)),
|
||||
self._source
|
||||
)
|
||||
warn(message)
|
||||
end
|
||||
end
|
||||
|
||||
return Promise
|
||||
@@ -0,0 +1,331 @@
|
||||
return function()
|
||||
local Promise = require(script.Parent.Promise)
|
||||
|
||||
describe("Promise.new", function()
|
||||
it("should instantiate with a callback", function()
|
||||
local promise = Promise.new(function() end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should invoke the given callback with resolve and reject", function()
|
||||
local callCount = 0
|
||||
local resolveArg
|
||||
local rejectArg
|
||||
|
||||
local promise = Promise.new(function(resolve, reject)
|
||||
callCount = callCount + 1
|
||||
resolveArg = resolve
|
||||
rejectArg = reject
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(resolveArg).to.be.a("function")
|
||||
expect(rejectArg).to.be.a("function")
|
||||
expect(promise._status).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
|
||||
it("should resolve promises on resolve()", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function(resolve)
|
||||
callCount = callCount + 1
|
||||
resolve()
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
|
||||
it("should reject promises on reject()", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function(resolve, reject)
|
||||
callCount = callCount + 1
|
||||
reject()
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
end)
|
||||
|
||||
it("should reject on error in callback", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function()
|
||||
callCount = callCount + 1
|
||||
error("hahah")
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]:find("hahah")).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.resolve", function()
|
||||
it("should immediately resolve with a value", function()
|
||||
local promise = Promise.resolve(5)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._value[1]).to.equal(5)
|
||||
end)
|
||||
|
||||
it("should chain onto passed promises", function()
|
||||
local promise = Promise.resolve(Promise.new(function(_, reject)
|
||||
reject(7)
|
||||
end))
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(7)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.reject", function()
|
||||
it("should immediately reject with a value", function()
|
||||
local promise = Promise.reject(6)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(6)
|
||||
end)
|
||||
|
||||
it("should pass a promise as-is as an error", function()
|
||||
local innerPromise = Promise.new(function(resolve)
|
||||
resolve(6)
|
||||
end)
|
||||
|
||||
local promise = Promise.reject(innerPromise)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(innerPromise)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise:andThen", function()
|
||||
it("should chain onto resolved promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local promise = Promise.resolve(5)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end, function()
|
||||
badCallCount = badCallCount + 1
|
||||
end)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(5)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._value[1]).to.equal(5)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should chain onto rejected promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local promise = Promise.reject(5)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function(...)
|
||||
badCallCount = badCallCount + 1
|
||||
end, function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(5)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(5)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should chain onto asynchronously resolved promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local startResolution
|
||||
local promise = Promise.new(function(resolve)
|
||||
startResolution = resolve
|
||||
end)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end, function()
|
||||
badCallCount = badCallCount + 1
|
||||
end)
|
||||
|
||||
expect(callCount).to.equal(0)
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
startResolution(6)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(6)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._value[1]).to.equal(6)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should chain onto asynchronously rejected promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local startResolution
|
||||
local promise = Promise.new(function(_, reject)
|
||||
startResolution = reject
|
||||
end)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function()
|
||||
badCallCount = badCallCount + 1
|
||||
end, function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end)
|
||||
|
||||
expect(callCount).to.equal(0)
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
startResolution(6)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(6)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(6)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.all", function()
|
||||
it("should resolve immediately with an empty table argument", function()
|
||||
local promise = Promise.all({})
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
|
||||
local result = unpack(promise._value)
|
||||
expect(type(result)).to.equal("table")
|
||||
expect(#result).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should resolve immediately with no arguments", function()
|
||||
local promise = Promise.all()
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
|
||||
local result = unpack(promise._value)
|
||||
expect(type(result)).to.equal("table")
|
||||
expect(#result).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should resolve with one result with one resolved argument", function()
|
||||
local promise = Promise.all(Promise.resolve(7))
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
|
||||
local result = unpack(promise._value)
|
||||
expect(type(result)).to.equal("table")
|
||||
expect(#result).to.equal(1)
|
||||
expect(result[1]).to.equal(7)
|
||||
end)
|
||||
|
||||
it("should resolve with multiple resolved arguments", function()
|
||||
local promise = Promise.all(Promise.resolve(7), Promise.resolve(4))
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
|
||||
local result = unpack(promise._value)
|
||||
expect(type(result)).to.equal("table")
|
||||
expect(#result).to.equal(2)
|
||||
expect(result[1]).to.equal(7)
|
||||
expect(result[2]).to.equal(4)
|
||||
end)
|
||||
|
||||
it("should reject with one rejected argument", function()
|
||||
local promise = Promise.all(Promise.reject(5))
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
|
||||
local result = unpack(promise._value)
|
||||
expect(result).to.equal(5)
|
||||
end)
|
||||
|
||||
it("should reject with one success followed by one rejected argument", function()
|
||||
local promise = Promise.all(Promise.resolve(7), Promise.reject(4))
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
|
||||
local result = unpack(promise._value)
|
||||
expect(result).to.equal(4)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
|
||||
|
||||
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
|
||||
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
|
||||
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
|
||||
|
||||
local function setFieldPerUser(state, fieldName, userId, value)
|
||||
local field = state[fieldName] or {}
|
||||
return Immutable.JoinDictionaries(state, {
|
||||
[fieldName] = Immutable.JoinDictionaries(field, {
|
||||
[userId] = value
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
local function setRetrievalStatus(state, userId, status)
|
||||
return setFieldPerUser(state, "retrievalStatus", userId, status)
|
||||
end
|
||||
|
||||
local function setRetrievalFailureResponse(state, userId, response)
|
||||
return setFieldPerUser(state, "retrievalFailureResponse", userId, response)
|
||||
end
|
||||
|
||||
return function(state, action)
|
||||
state = state or {
|
||||
retrievalStatus = {},
|
||||
retrievalFailureResponse = {},
|
||||
}
|
||||
|
||||
if action.type == FetchUserFriendsStarted.name then
|
||||
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Fetching)
|
||||
elseif action.type == FetchUserFriendsFailed.name then
|
||||
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Failed)
|
||||
state = setRetrievalFailureResponse(state, action.userId, action.response)
|
||||
elseif action.type == FetchUserFriendsCompleted.name then
|
||||
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Done)
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
|
||||
|
||||
return function(state, action)
|
||||
state = state or {}
|
||||
|
||||
if action.type == ReceivedPlacesInfos.name then
|
||||
for _, placeInfo in pairs(action.placesInfos) do
|
||||
state = Immutable.Set(state, tostring(placeInfo.universeId), placeInfo)
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
@@ -0,0 +1,117 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
|
||||
local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser)
|
||||
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
|
||||
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
|
||||
local RemoveUser = require(CorePackages.AppTempCommon.LuaApp.Actions.RemoveUser)
|
||||
local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend)
|
||||
local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType)
|
||||
local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence)
|
||||
local SetUserThumbnail = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserThumbnail)
|
||||
|
||||
local FFlagFixUsersReducerDataLoss = settings():GetFFlag("FixUsersReducerDataLoss")
|
||||
|
||||
return function(state, action)
|
||||
state = state or {}
|
||||
|
||||
if action.type == AddUser.name then
|
||||
local user = action.user
|
||||
state = Immutable.Set(state, user.id, user)
|
||||
elseif action.type == AddUsers.name then
|
||||
if FFlagFixUsersReducerDataLoss then
|
||||
local addedUsers = action.users
|
||||
local usersUpdate = {}
|
||||
for userId, addedUser in pairs(addedUsers) do
|
||||
local existingUser = state[userId]
|
||||
if existingUser then
|
||||
usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser)
|
||||
else
|
||||
usersUpdate[userId] = addedUser
|
||||
end
|
||||
end
|
||||
|
||||
state = Immutable.JoinDictionaries(state, usersUpdate)
|
||||
else
|
||||
local users = action.users
|
||||
state = Immutable.JoinDictionaries(state, users)
|
||||
end
|
||||
|
||||
elseif action.type == SetUserIsFriend.name then
|
||||
local user = state[action.userId]
|
||||
if user then
|
||||
local newUser = Immutable.Set(user, "isFriend", action.isFriend)
|
||||
state = Immutable.Set(state, user.id, newUser)
|
||||
else
|
||||
warn("Setting isFriend on user", action.userId, "who doesn't exist yet")
|
||||
end
|
||||
elseif action.type == SetUserPresence.name then
|
||||
local user = state[action.userId]
|
||||
if user then
|
||||
local newUser = Immutable.JoinDictionaries(user, {
|
||||
presence = action.presence,
|
||||
lastLocation = action.lastLocation,
|
||||
})
|
||||
state = Immutable.Set(state, user.id, newUser)
|
||||
else
|
||||
warn("Setting presence on user", action.userId, "who doesn't exist yet")
|
||||
end
|
||||
elseif action.type == ReceivedUserPresence.name then
|
||||
local user = state[action.userId]
|
||||
if user then
|
||||
state = Immutable.JoinDictionaries(state, {
|
||||
[action.userId] = Immutable.JoinDictionaries(user, {
|
||||
presence = action.presence,
|
||||
lastLocation = action.lastLocation,
|
||||
placeId = action.placeId,
|
||||
rootPlaceId = action.rootPlaceId,
|
||||
gameInstanceId = action.gameInstanceId,
|
||||
lastOnline = action.lastOnline,
|
||||
universeId = action.universeId,
|
||||
}),
|
||||
})
|
||||
end
|
||||
elseif action.type == SetUserThumbnail.name then
|
||||
local user = state[action.userId]
|
||||
if user then
|
||||
if FFlagFixUsersReducerDataLoss then
|
||||
local thumbnails = user.thumbnails or {}
|
||||
state = Immutable.JoinDictionaries(state, {
|
||||
[action.userId] = Immutable.JoinDictionaries(user, {
|
||||
thumbnails = Immutable.JoinDictionaries(thumbnails, {
|
||||
[action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, {
|
||||
[action.thumbnailSize] = action.image,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
else
|
||||
state = Immutable.JoinDictionaries(state, {
|
||||
[action.userId] = Immutable.JoinDictionaries(user, {
|
||||
thumbnails = Immutable.JoinDictionaries(user.thumbnails, {
|
||||
[action.thumbnailType] = Immutable.JoinDictionaries(user.thumbnails[action.thumbnailType] or {}, {
|
||||
[action.thumbnailSize] = action.image,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
end
|
||||
elseif action.type == SetUserMembershipType.name then
|
||||
local user = state[action.userId]
|
||||
if user then
|
||||
state = Immutable.JoinDictionaries(state, {
|
||||
[action.userId] = Immutable.JoinDictionaries(user, {
|
||||
membership = action.membershipType,
|
||||
}),
|
||||
})
|
||||
end
|
||||
elseif action.type == RemoveUser.name then
|
||||
if state[action.userId] then
|
||||
state = Immutable.RemoveFromDictionary(state, action.userId)
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
@@ -0,0 +1,106 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users)
|
||||
|
||||
local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser)
|
||||
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
|
||||
local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend)
|
||||
local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType)
|
||||
local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence)
|
||||
|
||||
describe("initial state", function()
|
||||
it("should return an initial table when passed nil", function()
|
||||
local state = Users(nil, {})
|
||||
expect(state).to.be.a("table")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("AddUser", function()
|
||||
it("should add a user to the store", function()
|
||||
local user = User.mock()
|
||||
local state = {}
|
||||
|
||||
state = Users(state, AddUser(user))
|
||||
|
||||
expect(state[user.id]).to.equal(user)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("SetUserIsFriend", function()
|
||||
it("should set isFriend on an existing user", function()
|
||||
local user = User.mock()
|
||||
local state = {
|
||||
[user.id] = user
|
||||
}
|
||||
|
||||
expect(state[user.id].isFriend).to.equal(false)
|
||||
|
||||
state = Users(state, SetUserIsFriend(user.id, true))
|
||||
expect(state[user.id].isFriend).to.equal(true)
|
||||
|
||||
state = Users(state, SetUserIsFriend(user.id, false))
|
||||
expect(state[user.id].isFriend).to.equal(false)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("SetUserPresence", function()
|
||||
it("should set presence on an existing user", function()
|
||||
local user = User.mock()
|
||||
local state = {
|
||||
[user.id] = user
|
||||
}
|
||||
|
||||
expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE)
|
||||
|
||||
state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE))
|
||||
expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE)
|
||||
|
||||
state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME))
|
||||
expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME)
|
||||
|
||||
state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO))
|
||||
expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("ReceivedUserPresence", function()
|
||||
it("should set presence on an existing user", function()
|
||||
local user = User.mock()
|
||||
local state = {
|
||||
[user.id] = user
|
||||
}
|
||||
|
||||
local existingPresence = user.presence
|
||||
local newPresence = 'ONLINE'
|
||||
local lastLocation = MockId()
|
||||
local newPlaceId = MockId()
|
||||
|
||||
state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId))
|
||||
|
||||
expect(user.presence).to.equal(existingPresence)
|
||||
expect(state[user.id].presence).to.equal(newPresence)
|
||||
expect(state[user.id].lastLocation).to.equal(lastLocation)
|
||||
expect(state[user.id].placeId).to.equal(newPlaceId)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("SetUserMembershipType", function()
|
||||
it("should set membership on an existing user", function()
|
||||
local user = User.mock()
|
||||
local state = {
|
||||
[user.id] = user
|
||||
}
|
||||
|
||||
local existingMembership = user.membership
|
||||
local newMembership = Enum.MembershipType.BuildersClub
|
||||
|
||||
state = Users(state, SetUserMembershipType(user.id, newMembership))
|
||||
|
||||
expect(user.membership).to.equal(existingMembership)
|
||||
expect(state[user.id].membership).to.equal(newMembership)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,100 @@
|
||||
local Result = {}
|
||||
Result.__index = Result
|
||||
|
||||
local ResultTypeSymbol = newproxy(true)
|
||||
|
||||
function Result.new(status, value)
|
||||
assert(typeof(status) == "boolean")
|
||||
|
||||
local result = {
|
||||
-- Used to locate where a result was created
|
||||
_source = debug.traceback(),
|
||||
|
||||
-- A tag to identify us as a result
|
||||
[ResultTypeSymbol] = true,
|
||||
|
||||
_status = status,
|
||||
|
||||
-- The value success value or error message.
|
||||
_value = value,
|
||||
}
|
||||
|
||||
setmetatable(result, Result)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function Result.success(value)
|
||||
return Result.new(true, value)
|
||||
end
|
||||
|
||||
function Result.error(value)
|
||||
return Result.new(false, value)
|
||||
end
|
||||
|
||||
--[[
|
||||
Is the given object a Result instance?
|
||||
]]
|
||||
function Result.is(object)
|
||||
if typeof(object) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return object[ResultTypeSymbol]
|
||||
end
|
||||
|
||||
--[[
|
||||
The given callbacks are invoked depending on that result.
|
||||
|
||||
Creates a new result that receives the output of the callback.
|
||||
]]
|
||||
function Result:match(successHandler, errorHandler)
|
||||
assert(typeof(successHandler) == "function" or typeof(successHandler) == "nil",
|
||||
string.format("Result:match expects successHandler to be a function or nil, got %s", typeof(successHandler)))
|
||||
assert(typeof(errorHandler) == "function" or typeof(errorHandler) == "nil",
|
||||
string.format("Result:match expects errorHandler to be a function or nil, got %s", typeof(errorHandler)))
|
||||
|
||||
local newResult
|
||||
if self._status then
|
||||
if successHandler ~= nil then
|
||||
newResult = successHandler(self._value)
|
||||
else
|
||||
return self
|
||||
end
|
||||
else
|
||||
if errorHandler ~= nil then
|
||||
newResult = errorHandler(self._value)
|
||||
else
|
||||
return self
|
||||
end
|
||||
end
|
||||
if Result.is(newResult) then
|
||||
return newResult
|
||||
else
|
||||
return Result.success(newResult)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
The given callback is invoked if the result is success.
|
||||
|
||||
Creates a new result that receives the output of the callback.
|
||||
]]
|
||||
function Result:matchSuccess(successHandler)
|
||||
return self:match(successHandler, nil)
|
||||
end
|
||||
|
||||
--[[
|
||||
The given callback is invoked if the result is error.
|
||||
|
||||
Creates a new result that receives the output of the callback.
|
||||
]]
|
||||
function Result:matchError(errorHandler)
|
||||
return self:match(nil, errorHandler)
|
||||
end
|
||||
|
||||
function Result:unwrap()
|
||||
return self._status, self._value
|
||||
end
|
||||
|
||||
return Result
|
||||
@@ -0,0 +1,217 @@
|
||||
return function()
|
||||
local Result = require(script.Parent.Result)
|
||||
|
||||
describe("Constructors", function()
|
||||
it("should return a success result from Result.new with a success value", function()
|
||||
local success, value = Result.new(true, "foo"):unwrap()
|
||||
expect(success).to.equal(true)
|
||||
expect(value).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should return an error result from Result.new with a failure value", function()
|
||||
local success, value = Result.new(false, "foo"):unwrap()
|
||||
expect(success).to.equal(false)
|
||||
expect(value).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should return a success result from Result.success", function()
|
||||
local success, value = Result.success("foo"):unwrap()
|
||||
expect(success).to.equal(true)
|
||||
expect(value).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should return an error result from Result.error", function()
|
||||
local success, value = Result.error("foo"):unwrap()
|
||||
expect(success).to.equal(false)
|
||||
expect(value).to.equal("foo")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Result:match", function()
|
||||
it("should call the first callback with the value if it's a success result", function()
|
||||
local called = false
|
||||
Result.success("foo"):match(
|
||||
function(value)
|
||||
called = true
|
||||
expect(value).to.equal("foo")
|
||||
end,
|
||||
function(value)
|
||||
assert(false)
|
||||
end
|
||||
)
|
||||
expect(called).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should call the second callback with the error if it's an error result", function()
|
||||
local called = false
|
||||
Result.error("foo"):match(
|
||||
function(value)
|
||||
assert(false)
|
||||
end,
|
||||
function(value)
|
||||
expect(value).to.equal("foo")
|
||||
called = true
|
||||
end
|
||||
)
|
||||
expect(called).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return the result of the first callback if it's a result", function()
|
||||
Result.success("foo"):match(
|
||||
function()
|
||||
return Result.success("bar")
|
||||
end,
|
||||
nil
|
||||
):match(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end,
|
||||
nil
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return the result of the second callback if it's a result", function()
|
||||
Result.error("foo"):match(
|
||||
nil,
|
||||
function()
|
||||
return Result.success("bar")
|
||||
end
|
||||
):match(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end,
|
||||
nil
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return self if it's success and the first callback isn't provided", function()
|
||||
local result1 = Result.success("foo")
|
||||
local result2 = result1:match(nil, nil)
|
||||
expect(result1).to.equal(result2)
|
||||
end)
|
||||
|
||||
it("should return self if it's error and the second callback isn't provided", function()
|
||||
local result1 = Result.error("foo")
|
||||
local result2 = result1:match(nil, nil)
|
||||
expect(result1).to.equal(result2)
|
||||
end)
|
||||
|
||||
it("should return success result wrapping value returned by first callback if not a result", function()
|
||||
Result.success("foo"):match(
|
||||
function()
|
||||
return "bar"
|
||||
end,
|
||||
nil
|
||||
):match(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end,
|
||||
nil
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return success result wrapping value returned by second callback if not a result", function()
|
||||
Result.error("foo"):match(
|
||||
nil,
|
||||
function()
|
||||
return "bar"
|
||||
end
|
||||
):match(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end,
|
||||
nil
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Result:matchSuccess", function()
|
||||
it("should call the callback with the value if it's a success result", function()
|
||||
local called = false
|
||||
Result.success("foo"):matchSuccess(
|
||||
function(value)
|
||||
expect(value).to.equal("foo")
|
||||
called = true
|
||||
end
|
||||
)
|
||||
expect(called).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should not call the callback if it's an error result", function()
|
||||
Result.error("foo"):matchSuccess(
|
||||
function(value)
|
||||
assert(false)
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return the result of the callback if it's a result", function()
|
||||
Result.success("foo"):matchSuccess(
|
||||
function()
|
||||
return Result.success("bar")
|
||||
end
|
||||
):matchSuccess(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return success result wrapping value returned by callback if not a result", function()
|
||||
Result.success("foo"):matchSuccess(
|
||||
function()
|
||||
return "bar"
|
||||
end
|
||||
):matchSuccess(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Result:matchError", function()
|
||||
it("should call the callback with the value if it's an error result", function()
|
||||
local called = false
|
||||
Result.error("foo"):matchError(
|
||||
function(value)
|
||||
expect(value).to.equal("foo")
|
||||
called = true
|
||||
end
|
||||
)
|
||||
expect(called).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should not call the callback if it's a success result", function()
|
||||
Result.success("foo"):matchError(
|
||||
function(value)
|
||||
assert(false)
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return the result of the callback if it's a result", function()
|
||||
Result.error("foo"):matchError(
|
||||
function()
|
||||
return Result.success("bar")
|
||||
end
|
||||
):matchSuccess(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return success result wrapping value returned by callback if not a result", function()
|
||||
Result.error("foo"):matchError(
|
||||
function()
|
||||
return "bar"
|
||||
end
|
||||
):matchSuccess(
|
||||
function(value)
|
||||
expect(value).to.equal("bar")
|
||||
end
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,184 @@
|
||||
--[[
|
||||
Provides functions for comparing and printing lua tables.
|
||||
]]
|
||||
|
||||
local TableUtilities = {}
|
||||
local defaultIgnore = {}
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns if they have the same key-value pairs
|
||||
Except ignored keys
|
||||
]]
|
||||
function TableUtilities.ShallowEqual(A, B, ignore)
|
||||
if not A or not B then
|
||||
return false
|
||||
elseif A == B then
|
||||
return true
|
||||
end
|
||||
|
||||
if not ignore then
|
||||
ignore = defaultIgnore
|
||||
end
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= value and not ignore[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, value in pairs(B) do
|
||||
if A[key] ~= value and not ignore[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A, B and a key, returns if two tables have the same value at key
|
||||
]]
|
||||
function TableUtilities.EqualKey(A, B, key)
|
||||
if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns a new table with elements of A
|
||||
which are either not keys in B or have a different value in B
|
||||
]]
|
||||
function TableUtilities.TableDifference(A, B)
|
||||
local new = {}
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= A[key] then
|
||||
new[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a list and returns a table whose
|
||||
keys are elements of the list and whose
|
||||
values are all true
|
||||
]]
|
||||
local function membershipTable(list)
|
||||
local result = {}
|
||||
for i = 1, #list do
|
||||
result[list[i]] = true
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a table and returns a list of keys in that table
|
||||
]]
|
||||
local function listOfKeys(t)
|
||||
local result = {}
|
||||
for key,_ in pairs(t) do
|
||||
table.insert(result, key)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes two lists A and B, returns a new list of elements of A
|
||||
which are not in B
|
||||
]]
|
||||
function TableUtilities.ListDifference(A, B)
|
||||
return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B)))
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
For debugging. Returns false if the given table has any of the following:
|
||||
- a key that is neither a number or a string
|
||||
- a mix of number and string keys
|
||||
- number keys which are not exactly 1..#t
|
||||
]]
|
||||
function TableUtilities.CheckListConsistency(t)
|
||||
local containsNumberKey = false
|
||||
local containsStringKey = false
|
||||
local numberConsistency = true
|
||||
|
||||
local index = 1
|
||||
for x, _ in pairs(t) do
|
||||
if type(x) == 'string' then
|
||||
containsStringKey = true
|
||||
elseif type(x) == 'number' then
|
||||
if index ~= x then
|
||||
numberConsistency = false
|
||||
end
|
||||
containsNumberKey = true
|
||||
else
|
||||
return false
|
||||
end
|
||||
|
||||
if containsStringKey and containsNumberKey then
|
||||
return false
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
if containsNumberKey then
|
||||
return numberConsistency
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
For debugging, serializes the given table to a reasonable string that might even interpret as lua.
|
||||
]]
|
||||
function TableUtilities.RecursiveToString(t, indent)
|
||||
indent = indent or ''
|
||||
|
||||
if type(t) == 'table' then
|
||||
local result = ""
|
||||
if not TableUtilities.CheckListConsistency(t) then
|
||||
result = result .. "-- WARNING: this table fails the list consistency test\n"
|
||||
end
|
||||
result = result .. "{\n"
|
||||
for k,v in pairs(t) do
|
||||
if type(k) == 'string' then
|
||||
result = result
|
||||
.. " "
|
||||
.. indent
|
||||
.. tostring(k)
|
||||
.. " = "
|
||||
.. TableUtilities.RecursiveToString(v, " "..indent)
|
||||
..";\n"
|
||||
end
|
||||
if type(k) == 'number' then
|
||||
result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n"
|
||||
end
|
||||
end
|
||||
result = result .. indent .. "}"
|
||||
return result
|
||||
else
|
||||
return tostring(t)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a table and returns the field count
|
||||
]]
|
||||
function TableUtilities.FieldCount(t)
|
||||
local fieldCount = 0
|
||||
for _ in pairs(t) do
|
||||
fieldCount = fieldCount + 1
|
||||
end
|
||||
return fieldCount
|
||||
end
|
||||
|
||||
return TableUtilities
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
return function()
|
||||
local TableUtilities = require(script.Parent.TableUtilities)
|
||||
|
||||
it("should return whether tables are equal to each other", function()
|
||||
local tableA = nil
|
||||
local tableB = nil
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
|
||||
tableA = nil
|
||||
tableB = {}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
|
||||
tableA = {}
|
||||
tableB = nil
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
|
||||
tableA = {}
|
||||
tableB = {}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key1 = "value1",
|
||||
}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(true)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key1 = "value2",
|
||||
}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key2 = "value1",
|
||||
}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key2 = "value2",
|
||||
}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key1 = "value1",
|
||||
key2 = "value2",
|
||||
}
|
||||
expect(TableUtilities.ShallowEqual(tableA, tableB)).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should return whether tables are equal to each other at key", function()
|
||||
local tableA = nil
|
||||
local tableB = nil
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = nil
|
||||
tableB = {}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = {}
|
||||
tableB = nil
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = {}
|
||||
tableB = {}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key1 = "value1",
|
||||
}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key1 = "value2",
|
||||
}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key2 = "value1",
|
||||
}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key2 = "value2",
|
||||
}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(false)
|
||||
|
||||
tableA = {
|
||||
key1 = "value1",
|
||||
}
|
||||
tableB = {
|
||||
key1 = "value1",
|
||||
key2 = "value2",
|
||||
}
|
||||
expect(TableUtilities.EqualKey(tableA, tableB)).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "")).to.equal(false)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key1")).to.equal(true)
|
||||
expect(TableUtilities.EqualKey(tableA, tableB, "key2")).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should return table's field count", function()
|
||||
local table = {}
|
||||
expect(TableUtilities.FieldCount(table)).to.equal(0)
|
||||
|
||||
table = {
|
||||
key1 = "value1",
|
||||
}
|
||||
expect(TableUtilities.FieldCount(table)).to.equal(1)
|
||||
|
||||
table = {
|
||||
key1 = "value1",
|
||||
key2 = "value2",
|
||||
}
|
||||
expect(TableUtilities.FieldCount(table)).to.equal(2)
|
||||
end)
|
||||
end
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local LuaApp = CorePackages.AppTempCommon.LuaApp
|
||||
|
||||
local GamesGetThumbnails = require(LuaApp.Http.Requests.GamesGetThumbnails)
|
||||
local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails)
|
||||
|
||||
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
|
||||
local Promise = require(LuaApp.Promise)
|
||||
local Result = require(LuaApp.Result)
|
||||
|
||||
local TableUtilities = require(LuaApp.TableUtilities)
|
||||
|
||||
local THUMBNAIL_PAGE_COUNT = 20
|
||||
local THUMBNAIL_SIZE = 150
|
||||
local RETRY_MAX_COUNT = math.max(0, settings():GetFVariable("LuaAppNonFinalThumbnailMaxRetries"))
|
||||
local RETRY_TIME_MULTIPLIER = 2 -- seconds
|
||||
|
||||
local function convertToId(value)
|
||||
if type(value) ~= "number" and type(value) ~= "string" then
|
||||
return Result.error("convertToId expects value passed in to be a number or a string")
|
||||
end
|
||||
|
||||
return Result.success(tostring(value))
|
||||
end
|
||||
|
||||
local function subdivideThumbnailTokenArray(thumbnailTokens, tokenLimit)
|
||||
local someTokens = {}
|
||||
for i = 1, #thumbnailTokens, tokenLimit do
|
||||
local subArray = Functional.Take(thumbnailTokens, tokenLimit, i)
|
||||
table.insert(someTokens, subArray)
|
||||
end
|
||||
|
||||
return someTokens
|
||||
end
|
||||
|
||||
local function fetchThumbnailBatch(networkImpl, store, thumbnailTokens)
|
||||
return GamesGetThumbnails(networkImpl, thumbnailTokens, THUMBNAIL_SIZE, THUMBNAIL_SIZE):andThen(function(result)
|
||||
local thumbnails = {}
|
||||
local unfinalizedThumbnails = {}
|
||||
|
||||
for _,image in pairs(result.responseBody) do
|
||||
local convertToIdResult = convertToId(image.universeId)
|
||||
|
||||
convertToIdResult:match(function(universeId)
|
||||
if image.final == false then
|
||||
unfinalizedThumbnails[universeId] = image.retryToken
|
||||
else
|
||||
-- index all of the thumbnails by universeId
|
||||
thumbnails[universeId] = image
|
||||
end
|
||||
end, function(convertToIdError)
|
||||
warn(convertToIdError)
|
||||
end)
|
||||
end
|
||||
|
||||
store:dispatch(SetGameThumbnails(thumbnails))
|
||||
return Promise.resolve(unfinalizedThumbnails)
|
||||
end)
|
||||
end
|
||||
|
||||
local function fetchSubdividedThumbnailsArray(networkImpl, store, thumbnailTokens)
|
||||
return fetchThumbnailBatch(networkImpl, store, thumbnailTokens):andThen(function(unfinalizedThumbnails)
|
||||
local remainingUnfinalizedThumbnails = unfinalizedThumbnails
|
||||
|
||||
for retryCount = 1, RETRY_MAX_COUNT do
|
||||
if TableUtilities.FieldCount(remainingUnfinalizedThumbnails) == 0 then
|
||||
return -- Bail out, we're done!
|
||||
end
|
||||
|
||||
wait(RETRY_TIME_MULTIPLIER * math.pow(2, retryCount - 1))
|
||||
remainingUnfinalizedThumbnails = fetchThumbnailBatch(networkImpl, store, remainingUnfinalizedThumbnails):await()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function fetchThumbnails(networkImpl, thumbnailTokens)
|
||||
return function(store)
|
||||
-- NOTE : because the size of each thumbnail token, me must limit the number we can fetch at a time.
|
||||
-- So break apart the array of tokens we get into smaller, more manageable pieces.
|
||||
local fetchPromises = {}
|
||||
local someTokens = subdivideThumbnailTokenArray(thumbnailTokens, THUMBNAIL_PAGE_COUNT)
|
||||
for _, thumbsArr in ipairs(someTokens) do
|
||||
local promise = fetchSubdividedThumbnailsArray(networkImpl, store, thumbsArr)
|
||||
table.insert(fetchPromises, promise)
|
||||
end
|
||||
|
||||
return Promise.all(fetchPromises)
|
||||
end
|
||||
end
|
||||
|
||||
return fetchThumbnails
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local ApiFetchGameThumbnails = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGameThumbnails)
|
||||
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
|
||||
local GamesMultigetPlaceDetails = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GamesMultigetPlaceDetails)
|
||||
local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel)
|
||||
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
|
||||
|
||||
return function(networkImpl, placeIds)
|
||||
return function(store)
|
||||
if not placeIds or #placeIds == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
return GamesMultigetPlaceDetails(networkImpl, placeIds):andThen(function(result)
|
||||
local data = result.responseBody
|
||||
|
||||
local imageTokens = {}
|
||||
local placeInfos = Functional.Map(data, function(placeInfoData)
|
||||
local placeInfo = PlaceInfoModel.fromWeb(placeInfoData)
|
||||
|
||||
local universePlaceInfos = store:getState().UniversePlaceInfos
|
||||
local universeId = placeInfo.universeRootPlaceId
|
||||
if not universePlaceInfos[universeId] or placeInfo.imageToken ~= universePlaceInfos[universeId].imageToken then
|
||||
table.insert(imageTokens, placeInfo.imageToken)
|
||||
end
|
||||
|
||||
return placeInfo
|
||||
end)
|
||||
|
||||
store:dispatch(ReceivedPlacesInfos(placeInfos))
|
||||
|
||||
if #imageTokens > 0 then
|
||||
store:dispatch(ApiFetchGameThumbnails(networkImpl, imageTokens))
|
||||
end
|
||||
|
||||
return placeInfos
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Functional = require(CorePackages.AppTempCommon.Common.Functional)
|
||||
local GetPlaceInfos = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.GetPlaceInfos)
|
||||
|
||||
-- LuaChat
|
||||
local PlaceInfoModel = require(CorePackages.AppTempCommon.LuaChat.Models.PlaceInfoModel)
|
||||
local ReceivedMultiplePlaceInfos = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos)
|
||||
|
||||
return function(networkImpl, placeIds)
|
||||
return function(store)
|
||||
return GetPlaceInfos(networkImpl, placeIds):andThen(function(result)
|
||||
local data = result.responseBody
|
||||
|
||||
local placeInfos = Functional.Map(data, function(placeInfoData)
|
||||
return PlaceInfoModel.fromWeb(placeInfoData)
|
||||
end)
|
||||
|
||||
store:dispatch(ReceivedMultiplePlaceInfos(placeInfos))
|
||||
|
||||
return placeInfos
|
||||
end)
|
||||
end
|
||||
end
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local UsersGetFriendCount = require(Requests.UsersGetFriendCount)
|
||||
local SetFriendCount = require(Actions.SetFriendCount)
|
||||
|
||||
return function(networkImpl)
|
||||
return function(store)
|
||||
return UsersGetFriendCount(networkImpl):andThen(function(result)
|
||||
local data = result.responseBody
|
||||
|
||||
if data.success and data.count then
|
||||
store:dispatch(SetFriendCount(data.count))
|
||||
end
|
||||
|
||||
return data.count
|
||||
end)
|
||||
end
|
||||
end
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
local ApiFetchUsersPresences = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersPresences)
|
||||
local ApiFetchUsersThumbnail = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersThumbnail)
|
||||
local ApiFetchUsersFriendCount = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchUsersFriendCount)
|
||||
local UsersGetFriends = require(Requests.UsersGetFriends)
|
||||
|
||||
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
|
||||
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
|
||||
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
|
||||
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
|
||||
local UserModel = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
|
||||
|
||||
local LuaAppRemoveGetFriendshipCountApiCalls = settings():GetFFlag("LuaAppRemoveGetFriendshipCountApiCalls")
|
||||
local homePageDataFetchRefactor = settings():GetFFlag('LuaHomePageDataFetchRefactor')
|
||||
|
||||
return function(requestImpl, userId, thumbnailRequest)
|
||||
return function(store)
|
||||
store:dispatch(FetchUserFriendsStarted(userId))
|
||||
if not LuaAppRemoveGetFriendshipCountApiCalls then
|
||||
store:dispatch(ApiFetchUsersFriendCount(requestImpl))
|
||||
end
|
||||
return UsersGetFriends(requestImpl, userId):andThen(function(response)
|
||||
local responseBody = response.responseBody
|
||||
|
||||
local userIds = {}
|
||||
local newUsers = {}
|
||||
for _, userData in pairs(responseBody.data) do
|
||||
local id = tostring(userData.id)
|
||||
local newUser = UserModel.fromData(id, userData.name, true)
|
||||
|
||||
table.insert(userIds, id)
|
||||
newUsers[newUser.id] = newUser
|
||||
end
|
||||
if LuaAppRemoveGetFriendshipCountApiCalls then
|
||||
store:dispatch(UpdateUsers(newUsers))
|
||||
else
|
||||
store:dispatch(AddUsers(newUsers))
|
||||
end
|
||||
return userIds
|
||||
end):andThen(function(userIds)
|
||||
-- Asynchronously fetch friend thumbnails so we don't block display of UI
|
||||
store:dispatch(ApiFetchUsersThumbnail(requestImpl, userIds, thumbnailRequest))
|
||||
return store:dispatch(ApiFetchUsersPresences(requestImpl, userIds))
|
||||
end):andThen(
|
||||
function(result)
|
||||
store:dispatch(FetchUserFriendsCompleted(userId))
|
||||
if homePageDataFetchRefactor then
|
||||
return Promise.resolve(result)
|
||||
end
|
||||
end,
|
||||
function(response)
|
||||
store:dispatch(FetchUserFriendsFailed(userId, response))
|
||||
if homePageDataFetchRefactor then
|
||||
return Promise.reject(response)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Utils = CorePackages.AppTempCommon.LuaChat.Utils
|
||||
|
||||
local getPlaceIds = require(Utils.getFriendsActiveGamesPlaceIdsFromUsersPresence)
|
||||
local receiveUsersPresence = require(Utils.receiveUsersPresence)
|
||||
|
||||
local ApiFetchGamesDataByPlaceIds = require(CorePackages.AppTempCommon.LuaApp.Thunks.ApiFetchGamesDataByPlaceIds)
|
||||
local UsersGetPresence = require(CorePackages.AppTempCommon.LuaApp.Http.Requests.UsersGetPresence)
|
||||
|
||||
local FFlagLuaHomeGetFriendsPlayingGamesInfo = settings():GetFFlag("LuaHomeGetFriendsPlayingGamesInfo")
|
||||
|
||||
return function(networkImpl, userIds)
|
||||
return function(store)
|
||||
return UsersGetPresence(networkImpl, userIds):andThen(function(result)
|
||||
local userPresences = result.responseBody.userPresences
|
||||
receiveUsersPresence(userPresences, store)
|
||||
|
||||
if FFlagLuaHomeGetFriendsPlayingGamesInfo then
|
||||
local placeIds = getPlaceIds(userPresences, store)
|
||||
|
||||
store:dispatch(ApiFetchGamesDataByPlaceIds(networkImpl, placeIds))
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Actions = CorePackages.AppTempCommon.LuaApp.Actions
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local UsersGetThumbnail = require(Requests.UsersGetThumbnail)
|
||||
local SetUserThumbnail = require(Actions.SetUserThumbnail)
|
||||
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
|
||||
|
||||
local function fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest)
|
||||
local fetchedPromises = {}
|
||||
|
||||
for _, userId in pairs(userIds) do
|
||||
table.insert(fetchedPromises,
|
||||
UsersGetThumbnail(userId, thumbnailRequest.thumbnailType, thumbnailRequest.thumbnailSize)
|
||||
)
|
||||
end
|
||||
|
||||
return Promise.all(fetchedPromises)
|
||||
end
|
||||
|
||||
return function(networkImpl, userIds, thumbnailRequests)
|
||||
return function(store)
|
||||
-- We currently cannot batch request user avatar thumbnails,
|
||||
-- so each thumbnailRequest has to be processed individually.
|
||||
|
||||
local fetchedPromises = {}
|
||||
for _, thumbnailRequest in pairs(thumbnailRequests) do
|
||||
table.insert(fetchedPromises,
|
||||
fetchThumbnailsBatch(networkImpl, userIds, thumbnailRequest):andThen(function(result)
|
||||
for _, data in pairs(result) do
|
||||
store:dispatch(SetUserThumbnail(data.id, data.image, data.thumbnailType, data.thumbnailSize))
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
return Promise.all(fetchedPromises)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local AppTempCommon = CorePackages.AppTempCommon
|
||||
local Requests = CorePackages.AppTempCommon.LuaApp.Http.Requests
|
||||
|
||||
local ChatSendMessage = require(Requests.ChatSendMessage)
|
||||
local ChatStartOneToOneConversation = require(Requests.ChatStartOneToOneConversation)
|
||||
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
|
||||
|
||||
local trimCharacterFromEndString = require(AppTempCommon.Temp.trimCharacterFromEndString)
|
||||
|
||||
local INVITE_TEXT_MESSAGE = "Come join me in %s"
|
||||
local INVITE_LINK_MESSAGE = "%s/games/%s"
|
||||
|
||||
return function(networkImpl, userId, placeInfo)
|
||||
local clientId = Players.LocalPlayer.UserId
|
||||
|
||||
-- Construct the invite messages based on place info
|
||||
local inviteTextMessage = string.format(INVITE_TEXT_MESSAGE, placeInfo.name)
|
||||
|
||||
local trimmedUrl = trimCharacterFromEndString(Url.WWW_URL, "/")
|
||||
local inviteLinkMessage = string.format(INVITE_LINK_MESSAGE, trimmedUrl, placeInfo.universeRootPlaceId)
|
||||
|
||||
return function(store)
|
||||
return ChatStartOneToOneConversation(networkImpl, userId, clientId):andThen(function(conversationResult)
|
||||
local conversation = conversationResult.responseBody.conversation
|
||||
|
||||
return ChatSendMessage(networkImpl, conversation.id, inviteTextMessage):andThen(function()
|
||||
return ChatSendMessage(networkImpl, conversation.id, inviteLinkMessage):andThen(function(inviteResult)
|
||||
local data = inviteResult.responseBody
|
||||
|
||||
return {
|
||||
resultType = data.resultType,
|
||||
conversationId = conversation.id,
|
||||
placeId = placeInfo.universeRootPlaceId,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
|
||||
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
|
||||
|
||||
return function(users)
|
||||
return function(store)
|
||||
local friendCountOffset = 0
|
||||
local updatedUsers = {}
|
||||
|
||||
for _, user in pairs(users) do
|
||||
local needsUpdate = false
|
||||
local userId = user.id
|
||||
local isFriend = user.isFriend
|
||||
local offset = 0
|
||||
|
||||
assert(typeof(isFriend) == "boolean")
|
||||
|
||||
local userInStore = store:getState().Users[userId]
|
||||
if userInStore then
|
||||
-- Mark user with needsUpdate if any of the field is different
|
||||
-- from the existing user information in Store.
|
||||
if not User.compare(userInStore, user) then
|
||||
needsUpdate = true
|
||||
if userInStore.isFriend ~= isFriend then
|
||||
offset = isFriend and 1 or -1
|
||||
end
|
||||
end
|
||||
else
|
||||
needsUpdate = true
|
||||
offset = isFriend and 1 or 0
|
||||
end
|
||||
|
||||
if needsUpdate then
|
||||
friendCountOffset = friendCountOffset + offset
|
||||
updatedUsers[userId] = user
|
||||
end
|
||||
end
|
||||
|
||||
if next(updatedUsers) then
|
||||
store:dispatch(AddUsers(updatedUsers))
|
||||
end
|
||||
|
||||
if friendCountOffset ~= 0 then
|
||||
local currentFriendCount = store:getState().FriendCount
|
||||
store:dispatch(SetFriendCount(currentFriendCount + friendCountOffset))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,123 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Rodux = require(CorePackages.Rodux)
|
||||
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
|
||||
|
||||
local UpdateUsers = require(CorePackages.AppTempCommon.LuaApp.Thunks.UpdateUsers)
|
||||
|
||||
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
|
||||
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
|
||||
|
||||
local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount)
|
||||
local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users)
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
|
||||
local function UsersReducerMonitor (state, action)
|
||||
state = state or {
|
||||
numberOfAddUsersCalled = 0,
|
||||
numberOfUsersPassedIn = 0,
|
||||
}
|
||||
|
||||
if action.type == AddUsers.name then
|
||||
state.numberOfAddUsersCalled = state.numberOfAddUsersCalled + 1
|
||||
state.numberOfUsersPassedIn = 0
|
||||
for _, _ in pairs(action.users) do
|
||||
state.numberOfUsersPassedIn = state.numberOfUsersPassedIn + 1
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local function FriendCountReducerMonitor (state, action)
|
||||
state = state or {
|
||||
numberOfSetFriendCountCalled = 0,
|
||||
}
|
||||
|
||||
if action.type == SetFriendCount.name then
|
||||
state.numberOfSetFriendCountCalled = state.numberOfSetFriendCountCalled + 1
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local function CustomReducer(state, action)
|
||||
state = state or {}
|
||||
|
||||
return {
|
||||
Users = Users(state.Users, action),
|
||||
UsersReducerMonitor = UsersReducerMonitor(state.UsersReducerMonitor, action),
|
||||
|
||||
FriendCount = FriendCount(state.FriendCount, action),
|
||||
FriendCountReducerMonitor = FriendCountReducerMonitor(state.FriendCountReducerMonitor, action),
|
||||
}
|
||||
end
|
||||
|
||||
local listOfUsers = {
|
||||
["1"] = User.fromData(1, "Hedonism Bot", true),
|
||||
["2"] = User.fromData(2, "Hypno Toad", true),
|
||||
["3"] = User.fromData(3, "John Zoidberg", false),
|
||||
["4"] = User.fromData(4, "Pazuzu", true),
|
||||
["5"] = User.fromData(5, "Ogden Wernstrom", true),
|
||||
["6"] = User.fromData(6, "Lrrr", true),
|
||||
}
|
||||
|
||||
it("should do nothing if empty list of users is provided", function()
|
||||
local store = Rodux.Store.new(CustomReducer, {}, {
|
||||
Rodux.thunkMiddleware,
|
||||
})
|
||||
store:dispatch(UpdateUsers({ }))
|
||||
|
||||
local state = store:getState()
|
||||
|
||||
expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(0)
|
||||
expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should update only the number of users with modified data", function()
|
||||
local store = Rodux.Store.new(CustomReducer, {
|
||||
Users = listOfUsers,
|
||||
}, {
|
||||
Rodux.thunkMiddleware,
|
||||
})
|
||||
|
||||
local currentUsers = store:getState().Users
|
||||
local listOfUsersWithPotentialUpdates = {
|
||||
Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- changed
|
||||
Immutable.Set(currentUsers["5"], "isFriend", false), -- changed
|
||||
Immutable.Set(currentUsers["6"], "isFriend", true), -- did not change
|
||||
}
|
||||
|
||||
store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates))
|
||||
|
||||
local state = store:getState()
|
||||
expect(state.UsersReducerMonitor.numberOfAddUsersCalled).to.equal(1)
|
||||
expect(state.UsersReducerMonitor.numberOfUsersPassedIn).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should correctly update the number of friends", function()
|
||||
local store = Rodux.Store.new(CustomReducer, {}, {
|
||||
Rodux.thunkMiddleware,
|
||||
})
|
||||
store:dispatch(UpdateUsers(listOfUsers))
|
||||
|
||||
local state = store:getState()
|
||||
expect(state.FriendCountReducerMonitor.numberOfSetFriendCountCalled).to.equal(1)
|
||||
expect(state.FriendCount).to.equal(5)
|
||||
|
||||
local currentUsers = store:getState().Users
|
||||
local listOfUsersWithPotentialUpdates = {
|
||||
Immutable.Set(currentUsers["2"], "presence", User.PresenceType.IN_GAME), -- friendship didn't change
|
||||
Immutable.Set(currentUsers["5"], "isFriend", false), -- friendship changed
|
||||
Immutable.Set(currentUsers["6"], "isFriend", false), -- friendship changed
|
||||
User.fromData(7, "Nibbler", true), -- new friend
|
||||
}
|
||||
|
||||
store:dispatch(UpdateUsers(listOfUsersWithPotentialUpdates))
|
||||
|
||||
state = store:getState()
|
||||
expect(state.FriendCount).to.equal(4)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Helper function to throttle based on player Id:
|
||||
return function(throttle, userId)
|
||||
assert(type(throttle) == "number")
|
||||
assert(type(userId) == "number")
|
||||
|
||||
-- Determine userRollout using last two digits of user ID:
|
||||
-- (+1 to change range from 0-99 to 1-100 as 0 is off, 100 is full on):
|
||||
local userRollout = (userId % 100) + 1
|
||||
return userRollout <= throttle
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
return function()
|
||||
local ThrottleUserId = require(script.Parent.ThrottleUserId)
|
||||
|
||||
describe("ThrottleUserId", function()
|
||||
it("should always reject zero%", function()
|
||||
local gating = ThrottleUserId(0, 10000)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(0, 10001)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(0, 10025)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(0, 10075)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(0, 10099)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(0, 10100)
|
||||
expect(gating).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should always accept 100%", function()
|
||||
local gating = ThrottleUserId(100, 10000)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(100, 10001)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(100, 10025)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(100, 10075)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(100, 10099)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(100, 10100)
|
||||
expect(gating).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should reject IDs over throttle percent", function()
|
||||
local gating = ThrottleUserId(25, 10050)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(50, 10075)
|
||||
expect(gating).to.equal(false)
|
||||
|
||||
gating = ThrottleUserId(75, 10099)
|
||||
expect(gating).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should accept IDs under throttle percent", function()
|
||||
local gating = ThrottleUserId(1, 10100)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(10, 10109)
|
||||
expect(gating).to.equal(true)
|
||||
|
||||
gating = ThrottleUserId(25, 10023)
|
||||
expect(gating).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
local Modules = game:GetService("CorePackages").AppTempCommon
|
||||
local Common = Modules.Common
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(convo)
|
||||
return {
|
||||
conversation = convo,
|
||||
}
|
||||
end)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local Modules = game:GetService("CorePackages").AppTempCommon
|
||||
|
||||
local Common = Modules.Common
|
||||
local Action = require(Common.Action)
|
||||
|
||||
return Action(script.Name, function(placeInfos)
|
||||
return {
|
||||
placeInfos = placeInfos,
|
||||
}
|
||||
end)
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
local Modules = game:GetService("CorePackages").AppTempCommon
|
||||
|
||||
local Action = require(Modules.Common.Action)
|
||||
local DateTime = require(Modules.LuaChat.DateTime)
|
||||
|
||||
local luaChatUseNewFriendsAndPresenceEndpoint = settings():GetFFlag("LuaChatUseNewFriendsAndPresenceEndpointV356")
|
||||
local luaChatPlayTogetherUseRootPresence = settings():GetFFlag("LuaChatPlayTogetherUseRootPresence")
|
||||
local luaChatRootPresenceEnabled = luaChatUseNewFriendsAndPresenceEndpoint and luaChatPlayTogetherUseRootPresence
|
||||
|
||||
if luaChatRootPresenceEnabled then
|
||||
return Action(script.Name, function(userId,
|
||||
presence, lastLocation,
|
||||
placeId, rootPlaceId,
|
||||
gameInstanceId, lastOnlineISO, universeId, previousUniverseId)
|
||||
|
||||
local lastOnline = 0
|
||||
if lastOnlineISO ~= nil then
|
||||
local lastDateTime = DateTime.fromIsoDate(lastOnlineISO)
|
||||
if lastDateTime ~= nil then
|
||||
lastOnline = lastDateTime:GetUnixTimestamp()
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
userId = userId,
|
||||
presence = presence,
|
||||
lastLocation = lastLocation,
|
||||
placeId = placeId,
|
||||
rootPlaceId = rootPlaceId,
|
||||
gameInstanceId = gameInstanceId,
|
||||
lastOnline = lastOnline,
|
||||
universeId = universeId,
|
||||
previousUniverseId = previousUniverseId,
|
||||
}
|
||||
end)
|
||||
else
|
||||
return Action(script.Name, function(userId,
|
||||
presence, lastLocation,
|
||||
placeId, gameInstanceId,
|
||||
universeId, previousUniverseId)
|
||||
|
||||
return {
|
||||
userId = userId,
|
||||
presence = presence,
|
||||
lastLocation = lastLocation,
|
||||
placeId = placeId,
|
||||
gameInstanceId = gameInstanceId,
|
||||
universeId = universeId,
|
||||
previousUniverseId = previousUniverseId,
|
||||
}
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,487 @@
|
||||
--[[
|
||||
This is a Lua implementation of the DateTime API proposal. It'll eventually
|
||||
be implemented in C++ and merged into the rest of the codebase if this model
|
||||
of working with dates ends up being useful.
|
||||
]]
|
||||
|
||||
local TimeZone = require(script.Parent.TimeZone)
|
||||
local TimeUnit = require(script.Parent.TimeUnit)
|
||||
|
||||
local DateTime = {}
|
||||
|
||||
local monthShortNames = {
|
||||
"Jan", "Feb", "Mar", "Apr",
|
||||
"May", "Jun", "Jul", "Aug",
|
||||
"Sep", "Oct", "Nov", "Dec"
|
||||
}
|
||||
|
||||
local monthLongNames = {
|
||||
"January", "February", "March", "April",
|
||||
"May", "June", "July", "August",
|
||||
"September", "October", "November", "December"
|
||||
}
|
||||
|
||||
local dayShortNames = {
|
||||
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
|
||||
}
|
||||
|
||||
local dayLongNames = {
|
||||
"Sunday", "Monday", "Tuesday", "Wednesday",
|
||||
"Thursday", "Friday", "Saturday"
|
||||
}
|
||||
|
||||
--[[
|
||||
We structure tokens like this to preserve order, since Lua associative
|
||||
arrays have no inherent order.
|
||||
]]
|
||||
local tokens = {
|
||||
{"YYYY", function(values)
|
||||
return tostring(values.Year)
|
||||
end},
|
||||
{"MMMM", function(values)
|
||||
return monthLongNames[values.Month]
|
||||
end},
|
||||
{"MMM", function(values)
|
||||
return monthShortNames[values.Month]
|
||||
end},
|
||||
{"MM", function(values)
|
||||
return ("%02d"):format(values.Month)
|
||||
end},
|
||||
{"M", function(values)
|
||||
return tostring(values.Month)
|
||||
end},
|
||||
{"DDDD", function(values)
|
||||
return dayLongNames[values.WeekDay]
|
||||
end},
|
||||
{"DDD", function(values)
|
||||
return dayShortNames[values.WeekDay]
|
||||
end},
|
||||
{"DD", function(values)
|
||||
return ("%02d"):format(values.Day)
|
||||
end},
|
||||
{"D", function(values)
|
||||
return tostring(values.Day)
|
||||
end},
|
||||
{"HH", function(values)
|
||||
local hour = values.Hour
|
||||
|
||||
return ("%02d"):format(hour)
|
||||
end},
|
||||
{"H", function(values)
|
||||
local hour = values.Hour
|
||||
|
||||
return tostring(hour)
|
||||
end},
|
||||
{"hh", function(values)
|
||||
local hour = values.Hour % 12
|
||||
if hour == 0 then
|
||||
hour = 12
|
||||
end
|
||||
|
||||
return ("%02d"):format(hour)
|
||||
end},
|
||||
{"h", function(values)
|
||||
local hour = values.Hour % 12
|
||||
if hour == 0 then
|
||||
hour = 12
|
||||
end
|
||||
|
||||
return tostring(hour)
|
||||
end},
|
||||
{"mm", function(values)
|
||||
return ("%02d"):format(values.Minute)
|
||||
end},
|
||||
{"m", function(values)
|
||||
return tostring(values.Minute)
|
||||
end},
|
||||
{"ss", function(values)
|
||||
return ("%02d"):format(values.Seconds)
|
||||
end},
|
||||
{"s", function(values)
|
||||
return tostring(values.Seconds)
|
||||
end},
|
||||
{"A", function(values)
|
||||
return values.Hour >= 12 and "PM" or "AM"
|
||||
end},
|
||||
{"a", function(values)
|
||||
return values.Hour >= 12 and "pm" or "am"
|
||||
end}
|
||||
}
|
||||
|
||||
local tokenKeys = {}
|
||||
for _, pair in ipairs(tokens) do
|
||||
table.insert(tokenKeys, pair[1])
|
||||
end
|
||||
|
||||
local tokenMap = {}
|
||||
for _, pair in ipairs(tokens) do
|
||||
tokenMap[pair[1]] = pair[2]
|
||||
end
|
||||
|
||||
--[[
|
||||
What's the next token in this source?
|
||||
]]
|
||||
local function getToken(source, i)
|
||||
local char = source:sub(i, i)
|
||||
|
||||
for _, token in ipairs(tokenKeys) do
|
||||
-- Only keep checking if the first character matches the token
|
||||
if token:sub(1, 1) == char then
|
||||
local match = source:sub(i, i + token:len() - 1)
|
||||
|
||||
if match == token then
|
||||
return token
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
An estimate of the current time zone's offset from UTC in seconds.
|
||||
|
||||
This might fail for weird timezones (UTC +/- 14), but we can fix that by
|
||||
picking a reference time that's further away from the Unix epoch.
|
||||
]]
|
||||
local function getTimeZoneOffset()
|
||||
local actualEpoch = 86400 + 43200
|
||||
local epoch = os.time({year = 1970, month = 1, day = 2, isdst = -1})
|
||||
if epoch then
|
||||
return actualEpoch - epoch
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a DateTime with the given values in UTC.
|
||||
|
||||
All values are optional!
|
||||
]]
|
||||
function DateTime.new(year, month, day, hour, minute, seconds)
|
||||
local tzOffset = getTimeZoneOffset()
|
||||
local timestamp = os.time({
|
||||
year = year or 1970,
|
||||
month = month or 1,
|
||||
day = day or 1,
|
||||
hour = hour or 0,
|
||||
min = minute or 0,
|
||||
sec = seconds or 0,
|
||||
isdst = -1
|
||||
})
|
||||
if timestamp == nil then
|
||||
timestamp = 0
|
||||
end
|
||||
|
||||
if seconds then
|
||||
local subseconds = seconds - math.floor(seconds)
|
||||
timestamp = timestamp + subseconds
|
||||
end
|
||||
|
||||
return DateTime.fromUnixTimestamp(timestamp + tzOffset)
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a DateTime representing now.
|
||||
]]
|
||||
function DateTime.now()
|
||||
return DateTime.fromUnixTimestamp(os.time())
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a Datetime from the given Unix timestamp.
|
||||
|
||||
Limited to the range [0, 2^32), which lets us represent dates out to about
|
||||
2038.
|
||||
]]
|
||||
function DateTime.fromUnixTimestamp(timestamp)
|
||||
assert(type(timestamp) == "number", "Invalid argument #1 to fromUnixTimestamp, expected number.")
|
||||
|
||||
local self = {}
|
||||
|
||||
self.value = timestamp
|
||||
|
||||
setmetatable(self, DateTime)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Attempt to create a DateTime from an ISO 8601 date-time string.
|
||||
|
||||
Will return nil on failure and output a warning to a console denoting what
|
||||
went wrong. This can probably turned into a second return value if we need
|
||||
to handle that data programmatically.
|
||||
]]
|
||||
function DateTime.fromIsoDate(isoDate)
|
||||
assert(type(isoDate) == "string", "Invalid argument #1 to DateTime.fromIsoDate, expected string.")
|
||||
|
||||
local datePattern = "^(%d+)%-(%d+)%-(%d+)" -- 0000-00-00
|
||||
local timePattern = "T(%d+):(%d+):(%d+%.?%d*)" -- T00:00:00
|
||||
local utcPattern = "Z$"
|
||||
local timeZonePattern = "([+-]%d+):(%d+)$" -- either Z or +/- followed by "00:00"
|
||||
|
||||
local timezone = 0
|
||||
local values = {1970, 1, 1, 0, 0, 0}
|
||||
local year, month, day = isoDate:match(datePattern)
|
||||
|
||||
if not year then
|
||||
warn(("Invalid ISO 8601 date: %q"):format(isoDate))
|
||||
return nil
|
||||
end
|
||||
|
||||
values[1] = tonumber(year)
|
||||
values[2] = tonumber(month)
|
||||
values[3] = tonumber(day)
|
||||
|
||||
local hour, minute, seconds = isoDate:match(timePattern)
|
||||
|
||||
if hour then
|
||||
values[4] = tonumber(hour)
|
||||
values[5] = tonumber(minute)
|
||||
values[6] = tonumber(seconds)
|
||||
|
||||
local isUtc = isoDate:match(utcPattern)
|
||||
|
||||
if not isUtc then
|
||||
local offsetHours, offsetMinutes = isoDate:match(timeZonePattern)
|
||||
|
||||
if not offsetHours then
|
||||
local offsetTotal = getTimeZoneOffset()
|
||||
offsetHours = offsetTotal / 3600
|
||||
offsetMinutes = 0
|
||||
|
||||
warn(("Invalid time zone in ISO 8601 date: %q -- falling back to local time"):format(isoDate))
|
||||
end
|
||||
|
||||
timezone = 3600 * tonumber(offsetHours) + 60 * tonumber(offsetMinutes)
|
||||
end
|
||||
end
|
||||
|
||||
local date = DateTime.new(unpack(values))
|
||||
date.value = date.value - timezone
|
||||
|
||||
return date
|
||||
end
|
||||
|
||||
--[[
|
||||
Format our current date using a formatting string. Look at the DateTime
|
||||
proposal to see information about the different formatting tokens.
|
||||
Generally, they try to resemble LDML and/or Moment.js-style formatting.
|
||||
|
||||
The time zone parameter is optional and defaults to the current time zone,
|
||||
TimeZone.Current.
|
||||
]]
|
||||
function DateTime:Format(formatString, tz)
|
||||
assert(type(formatString) == "string", "Invalid argument #1 to Format, expected string.")
|
||||
|
||||
tz = tz or TimeZone.Current
|
||||
|
||||
local values = self:GetValues(tz)
|
||||
|
||||
local buffer = {}
|
||||
|
||||
local i = 1
|
||||
while i <= formatString:len() do
|
||||
local char = formatString:sub(i, i)
|
||||
local token = getToken(formatString, i)
|
||||
|
||||
if token then
|
||||
table.insert(buffer, tokenMap[token](values))
|
||||
i = i + token:len()
|
||||
elseif char == "[" then
|
||||
-- Crawl forward until the next ] and interpret that text literally
|
||||
local j = i
|
||||
while j <= formatString:len() do
|
||||
j = j + 1
|
||||
|
||||
if formatString:sub(j, j) == "]" then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(buffer, formatString:sub(i + 1, j - 1))
|
||||
i = j + 1
|
||||
else
|
||||
table.insert(buffer, char)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local result = table.concat(buffer)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
Get a table of values representing the date-time in the given timezone.
|
||||
|
||||
The time zone parameter is optional and defaults to the current zime zone,
|
||||
TimeZone.Current.
|
||||
]]
|
||||
function DateTime:GetValues(tz)
|
||||
tz = tz or TimeZone.Current
|
||||
|
||||
local reference
|
||||
|
||||
if tz == TimeZone.Current then
|
||||
reference = os.date("*t", self.value)
|
||||
elseif tz == TimeZone.UTC then
|
||||
reference = os.date("!*t", self.value)
|
||||
end
|
||||
|
||||
if not reference then
|
||||
error(("Invalid TimeZone \"%s\""):format(tostring(tz)), 2)
|
||||
end
|
||||
|
||||
return {
|
||||
Year = reference.year,
|
||||
Month = reference.month,
|
||||
Day = reference.day,
|
||||
Hour = reference.hour,
|
||||
Minute = reference.min,
|
||||
Seconds = reference.sec,
|
||||
WeekDay = reference.wday
|
||||
}
|
||||
end
|
||||
|
||||
--[[
|
||||
Recover a Unix timestamp representing the DateTime's value.
|
||||
]]
|
||||
function DateTime:GetUnixTimestamp()
|
||||
return self.value
|
||||
end
|
||||
|
||||
--[[
|
||||
Format the DateTime as an ISO 8601 date string with time attached.
|
||||
|
||||
Always formats the time as UTC. There generally aren't many reasons to
|
||||
generate an ISO 8601 date in another time zone.
|
||||
]]
|
||||
function DateTime:GetIsoDate()
|
||||
return self:Format("YYYY-MM-DD[T]HH:mm:ss[Z]", TimeZone.UTC)
|
||||
end
|
||||
|
||||
-- Used by IsSame
|
||||
local descendingGranularityUnits = {
|
||||
{
|
||||
unit = TimeUnit.Years,
|
||||
key = "Year"
|
||||
},
|
||||
{
|
||||
unit = TimeUnit.Months,
|
||||
key = "Month"
|
||||
},
|
||||
{
|
||||
unit = TimeUnit.Days,
|
||||
key = "Day"
|
||||
},
|
||||
{
|
||||
unit = TimeUnit.Hours,
|
||||
key = "Hour"
|
||||
},
|
||||
{
|
||||
unit = TimeUnit.Minutes,
|
||||
key = "Minute"
|
||||
},
|
||||
{
|
||||
unit = TimeUnit.Seconds,
|
||||
key = "Seconds"
|
||||
}
|
||||
}
|
||||
|
||||
--[[
|
||||
Checks whether two DateTime values are the same, given a granularity and
|
||||
timezone value.
|
||||
|
||||
Granularity defaults to seconds and time zone defaults to the current local
|
||||
time zone.
|
||||
]]
|
||||
function DateTime:IsSame(other, granularity, timezone)
|
||||
granularity = granularity or TimeUnit.Seconds
|
||||
timezone = timezone or TimeZone.Current
|
||||
|
||||
local selfUnix = self:GetUnixTimestamp()
|
||||
local otherUnix = other:GetUnixTimestamp()
|
||||
|
||||
if selfUnix == otherUnix then
|
||||
return true
|
||||
end
|
||||
|
||||
local selfValues = self:GetValues(timezone)
|
||||
local otherValues = other:GetValues(timezone)
|
||||
|
||||
-- Week logic is special
|
||||
if granularity == TimeUnit.Weeks then
|
||||
local diff = math.abs(selfUnix - otherUnix)
|
||||
local diffDays = diff / (60 * 60 * 24)
|
||||
|
||||
-- Two dates separated by 7 or more whole days are never in the same week
|
||||
if diffDays >= 7 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Two dates separated by less than 7 days will be sorted monotonically
|
||||
-- if they're in the same week
|
||||
-- TODO: Use start-of-week value to shift WeekDay for locale
|
||||
if selfUnix > otherUnix then
|
||||
return selfValues.WeekDay >= otherValues.WeekDay
|
||||
else
|
||||
return selfValues.WeekDay <= otherValues.WeekDay
|
||||
end
|
||||
end
|
||||
|
||||
for _, unit in ipairs(descendingGranularityUnits) do
|
||||
local selfValue = selfValues[unit.key]
|
||||
local otherValue = otherValues[unit.key]
|
||||
|
||||
if selfValue ~= otherValue then
|
||||
return false
|
||||
end
|
||||
|
||||
if unit.unit == granularity then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Get a human-readable timestamp relative to the given epoch, which defaults
|
||||
to now. The format of the time is contextual to how far away the times are.
|
||||
]]
|
||||
function DateTime:GetLongRelativeTime(epoch)
|
||||
epoch = epoch or DateTime.now()
|
||||
|
||||
if self:IsSame(epoch, TimeUnit.Days) then
|
||||
return self:Format("h:mm A")
|
||||
elseif self:IsSame(epoch, TimeUnit.Weeks) then
|
||||
return self:Format("DDD | h:mm A")
|
||||
elseif self:IsSame(epoch, TimeUnit.Years) then
|
||||
return self:Format("MMM D | h:mm A")
|
||||
else
|
||||
return self:Format("MMM D, YYYY | h:mm A")
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Get a human-readable timestamp relative to the given epoch, which defaults
|
||||
to now. The format of the time is contextual to how far away the times are.
|
||||
]]
|
||||
function DateTime:GetShortRelativeTime(epoch)
|
||||
epoch = epoch or DateTime.now()
|
||||
|
||||
if self:IsSame(epoch, TimeUnit.Days) then
|
||||
return self:Format("h:mm A")
|
||||
elseif self:IsSame(epoch, TimeUnit.Weeks) then
|
||||
return self:Format("DDD")
|
||||
elseif self:IsSame(epoch, TimeUnit.Years) then
|
||||
return self:Format("MMM D")
|
||||
else
|
||||
return self:Format("MMM D, YYYY")
|
||||
end
|
||||
end
|
||||
|
||||
DateTime.__index = DateTime
|
||||
|
||||
return DateTime
|
||||
@@ -0,0 +1,355 @@
|
||||
return function()
|
||||
local DateTime = require(script.Parent.DateTime)
|
||||
local TimeZone = require(script.Parent.TimeZone)
|
||||
local TimeUnit = require(script.Parent.TimeUnit)
|
||||
|
||||
describe("Constructors", function()
|
||||
it("should construct with 'new'", function()
|
||||
expect(DateTime.new()).to.be.ok()
|
||||
expect(DateTime.new(2017)).to.be.ok()
|
||||
expect(DateTime.new(2017, 5)).to.be.ok()
|
||||
expect(DateTime.new(2017, 5, 3)).to.be.ok()
|
||||
expect(DateTime.new(2017, 5, 3, 12)).to.be.ok()
|
||||
expect(DateTime.new(2017, 5, 3, 12, 34)).to.be.ok()
|
||||
expect(DateTime.new(2017, 5, 3, 12, 34, 51)).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should construct with 'now'", function()
|
||||
expect(DateTime.now()).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should construct from a Unix timestamp", function()
|
||||
expect(DateTime.fromUnixTimestamp(0)).to.be.ok()
|
||||
expect(DateTime.fromUnixTimestamp(os.time())).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should construct from an ISO 8601 date", function()
|
||||
-- Basic date
|
||||
do
|
||||
local date = DateTime.fromIsoDate("1988-03-17")
|
||||
expect(date).to.be.ok()
|
||||
end
|
||||
|
||||
-- Date and time
|
||||
do
|
||||
local date = DateTime.fromIsoDate("2017-04-10T20:40:16Z")
|
||||
expect(date).to.be.ok()
|
||||
expect(date:GetUnixTimestamp()).to.equal(1491856816)
|
||||
end
|
||||
|
||||
-- Date and time with no time zone
|
||||
do
|
||||
local date = DateTime.fromIsoDate("2017-04-10T20:40:16")
|
||||
expect(date).to.be.ok()
|
||||
end
|
||||
|
||||
-- Date, time, and time zone offset
|
||||
do
|
||||
local date = DateTime.fromIsoDate("2017-04-10T20:40:16+01:00")
|
||||
expect(date).to.be.ok()
|
||||
expect(date:GetUnixTimestamp()).to.equal(1491856816 - 3600)
|
||||
end
|
||||
|
||||
-- Date, time, and negative time zone offset
|
||||
do
|
||||
local date = DateTime.fromIsoDate("2017-04-10T20:40:16-01:00")
|
||||
expect(date).to.be.ok()
|
||||
expect(date:GetUnixTimestamp()).to.equal(1491856816 + 3600)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Measurements", function()
|
||||
it("should get values in UTC", function()
|
||||
local date = DateTime.new()
|
||||
local values = date:GetValues(TimeZone.UTC)
|
||||
|
||||
expect(values).to.be.ok()
|
||||
expect(values.Year).to.be.a("number")
|
||||
expect(values.Month).to.be.a("number")
|
||||
expect(values.Day).to.be.a("number")
|
||||
expect(values.Hour).to.be.a("number")
|
||||
expect(values.Minute).to.be.a("number")
|
||||
expect(values.Seconds).to.be.a("number")
|
||||
|
||||
-- Locale specific!
|
||||
expect(values.WeekDay).to.be.a("number")
|
||||
end)
|
||||
|
||||
it("should get values in local time", function()
|
||||
local date = DateTime.new()
|
||||
local values = date:GetValues(TimeZone.Current)
|
||||
|
||||
expect(values).to.be.ok()
|
||||
expect(values.Year).to.be.a("number")
|
||||
expect(values.Month).to.be.a("number")
|
||||
expect(values.Day).to.be.a("number")
|
||||
expect(values.Hour).to.be.a("number")
|
||||
expect(values.Minute).to.be.a("number")
|
||||
expect(values.Seconds).to.be.a("number")
|
||||
|
||||
-- Locale specific!
|
||||
expect(values.WeekDay).to.be.a("number")
|
||||
end)
|
||||
|
||||
it("should preserve values from 'new' constructor", function()
|
||||
local date = DateTime.new(2017, 11, 3, 12, 34, 51)
|
||||
local values = date:GetValues(TimeZone.UTC)
|
||||
|
||||
expect(values.Year).to.equal(2017)
|
||||
expect(values.Month).to.equal(11)
|
||||
expect(values.Day).to.equal(3)
|
||||
expect(values.Hour).to.equal(12)
|
||||
expect(values.Minute).to.equal(34)
|
||||
expect(values.Seconds).to.equal(51)
|
||||
end)
|
||||
|
||||
it("should preserve Unix timestamp values", function()
|
||||
do
|
||||
local date = DateTime.fromUnixTimestamp(0)
|
||||
expect(date:GetUnixTimestamp()).to.equal(0)
|
||||
end
|
||||
|
||||
do
|
||||
local date = DateTime.fromUnixTimestamp(123456789)
|
||||
expect(date:GetUnixTimestamp()).to.equal(123456789)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Formatting", function()
|
||||
it("should have correct formatting tokens", function()
|
||||
local date = DateTime.new(2016, 1, 2, 15, 8, 9)
|
||||
|
||||
-- Shortcut time zone specification
|
||||
local function format(str)
|
||||
return date:Format(str, TimeZone.UTC)
|
||||
end
|
||||
|
||||
expect(format("YYYY")).to.equal("2016")
|
||||
expect(format("M")).to.equal("1")
|
||||
expect(format("MM")).to.equal("01")
|
||||
expect(format("D")).to.equal("2")
|
||||
expect(format("DD")).to.equal("02")
|
||||
expect(format("H")).to.equal("15")
|
||||
expect(format("HH")).to.equal("15")
|
||||
expect(format("h")).to.equal("3")
|
||||
expect(format("hh")).to.equal("03")
|
||||
expect(format("m")).to.equal("8")
|
||||
expect(format("mm")).to.equal("08")
|
||||
expect(format("s")).to.equal("9")
|
||||
expect(format("ss")).to.equal("09")
|
||||
|
||||
-- Locale-specific tests!
|
||||
expect(format("MMM")).to.equal("Jan")
|
||||
expect(format("MMMM")).to.equal("January")
|
||||
expect(format("A")).to.equal("PM")
|
||||
expect(format("a")).to.equal("pm")
|
||||
end)
|
||||
|
||||
it("should preserve text within brackets", function()
|
||||
local date = DateTime.new(2017, 1, 2, 15, 8, 9)
|
||||
|
||||
local function format(str)
|
||||
return date:Format(str, TimeZone.UTC)
|
||||
end
|
||||
|
||||
expect(format("[Hello, world!]")).to.equal("Hello, world!")
|
||||
expect(format("[YYYY-MM-DD]")).to.equal("YYYY-MM-DD")
|
||||
end)
|
||||
|
||||
it("should create identical ISO 8601 dates for UTC inputs", function()
|
||||
local date = DateTime.fromIsoDate("2017-04-10T20:40:16Z")
|
||||
expect(date:GetIsoDate()).to.equal("2017-04-10T20:40:16Z")
|
||||
end)
|
||||
|
||||
it("should handle dates around midnight", function()
|
||||
local date = DateTime.new(2015, 4, 20, 0, 0, 0)
|
||||
|
||||
expect(date:Format("H", TimeZone.UTC)).to.equal("0")
|
||||
expect(date:Format("HH", TimeZone.UTC)).to.equal("00")
|
||||
expect(date:Format("h", TimeZone.UTC)).to.equal("12")
|
||||
expect(date:Format("hh", TimeZone.UTC)).to.equal("12")
|
||||
expect(date:Format("a", TimeZone.UTC)).to.equal("am")
|
||||
end)
|
||||
|
||||
it("should handle dates around noon", function()
|
||||
local date = DateTime.new(2017, 5, 23, 12, 0, 0)
|
||||
|
||||
expect(date:Format("H", TimeZone.UTC)).to.equal("12")
|
||||
expect(date:Format("HH", TimeZone.UTC)).to.equal("12")
|
||||
expect(date:Format("h", TimeZone.UTC)).to.equal("12")
|
||||
expect(date:Format("hh", TimeZone.UTC)).to.equal("12")
|
||||
expect(date:Format("a", TimeZone.UTC)).to.equal("pm")
|
||||
end)
|
||||
|
||||
it("should return correct 24-hour clock sequences", function()
|
||||
local date = DateTime.new(2017, 9, 13, 0, 0, 0)
|
||||
|
||||
local formatString = "YYYY-MM-DD HH:mm:ss"
|
||||
|
||||
local expected = {
|
||||
"2017-09-13 00:00:00",
|
||||
"2017-09-13 01:00:00",
|
||||
"2017-09-13 02:00:00",
|
||||
"2017-09-13 03:00:00",
|
||||
"2017-09-13 04:00:00",
|
||||
"2017-09-13 05:00:00",
|
||||
"2017-09-13 06:00:00",
|
||||
"2017-09-13 07:00:00",
|
||||
"2017-09-13 08:00:00",
|
||||
"2017-09-13 09:00:00",
|
||||
"2017-09-13 10:00:00",
|
||||
"2017-09-13 11:00:00",
|
||||
"2017-09-13 12:00:00",
|
||||
"2017-09-13 13:00:00",
|
||||
"2017-09-13 14:00:00",
|
||||
"2017-09-13 15:00:00",
|
||||
"2017-09-13 16:00:00",
|
||||
"2017-09-13 17:00:00",
|
||||
"2017-09-13 18:00:00",
|
||||
"2017-09-13 19:00:00",
|
||||
"2017-09-13 20:00:00",
|
||||
"2017-09-13 21:00:00",
|
||||
"2017-09-13 22:00:00",
|
||||
"2017-09-13 23:00:00",
|
||||
"2017-09-14 00:00:00",
|
||||
"2017-09-14 01:00:00",
|
||||
}
|
||||
|
||||
for i = 1, #expected do
|
||||
local result = date:Format(formatString, TimeZone.UTC)
|
||||
expect(result).to.equal(expected[i])
|
||||
|
||||
-- Advance once hour
|
||||
date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should return correct 12-hour clock sequences", function()
|
||||
local date = DateTime.new(2017, 9, 13, 0, 0, 0)
|
||||
|
||||
local formatString = "YYYY-MM-DD hh:mm:ss a"
|
||||
|
||||
local expected = {
|
||||
"2017-09-13 12:00:00 am",
|
||||
"2017-09-13 01:00:00 am",
|
||||
"2017-09-13 02:00:00 am",
|
||||
"2017-09-13 03:00:00 am",
|
||||
"2017-09-13 04:00:00 am",
|
||||
"2017-09-13 05:00:00 am",
|
||||
"2017-09-13 06:00:00 am",
|
||||
"2017-09-13 07:00:00 am",
|
||||
"2017-09-13 08:00:00 am",
|
||||
"2017-09-13 09:00:00 am",
|
||||
"2017-09-13 10:00:00 am",
|
||||
"2017-09-13 11:00:00 am",
|
||||
"2017-09-13 12:00:00 pm",
|
||||
"2017-09-13 01:00:00 pm",
|
||||
"2017-09-13 02:00:00 pm",
|
||||
"2017-09-13 03:00:00 pm",
|
||||
"2017-09-13 04:00:00 pm",
|
||||
"2017-09-13 05:00:00 pm",
|
||||
"2017-09-13 06:00:00 pm",
|
||||
"2017-09-13 07:00:00 pm",
|
||||
"2017-09-13 08:00:00 pm",
|
||||
"2017-09-13 09:00:00 pm",
|
||||
"2017-09-13 10:00:00 pm",
|
||||
"2017-09-13 11:00:00 pm",
|
||||
"2017-09-14 12:00:00 am",
|
||||
"2017-09-14 01:00:00 am",
|
||||
}
|
||||
|
||||
for i = 1, #expected do
|
||||
local result = date:Format(formatString, TimeZone.UTC)
|
||||
expect(result).to.equal(expected[i])
|
||||
|
||||
-- Advance once hour
|
||||
date = date.fromUnixTimestamp(date:GetUnixTimestamp() + 3600)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should have LongRelativeTime", function()
|
||||
local date = DateTime.new(2015, 4, 20, 0, 0, 0)
|
||||
expect(date:GetLongRelativeTime()).to.be.a("string")
|
||||
end)
|
||||
|
||||
it("should have ShortRelativeTime", function()
|
||||
local date = DateTime.new(2015, 4, 20, 0, 0, 0)
|
||||
expect(date:GetShortRelativeTime()).to.be.a("string")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Comparisons", function()
|
||||
describe("IsSame", function()
|
||||
it("should equate dates with different granularity", function()
|
||||
local value = DateTime.new(2003, 6, 11, 15, 8, 9)
|
||||
local same = DateTime.new(2003, 6, 11, 15, 8, 9)
|
||||
|
||||
expect(value:IsSame(value)).to.equal(true)
|
||||
expect(value:IsSame(same)).to.equal(true)
|
||||
|
||||
local units = {TimeUnit.Years, TimeUnit.Months, TimeUnit.Days, TimeUnit.Hours, TimeUnit.Minutes}
|
||||
for _, unit in ipairs(units) do
|
||||
expect(value:IsSame(same, unit)).to.equal(true)
|
||||
end
|
||||
|
||||
local sameMinute = DateTime.new(2003, 6, 11, 15, 8, 10)
|
||||
|
||||
expect(value:IsSame(sameMinute)).to.equal(false)
|
||||
expect(value:IsSame(sameMinute, TimeUnit.Minutes)).to.equal(true)
|
||||
expect(value:IsSame(sameMinute, TimeUnit.Years)).to.equal(true)
|
||||
|
||||
local sameHour = DateTime.new(2003, 6, 11, 15, 9, 0)
|
||||
|
||||
expect(value:IsSame(sameHour)).to.equal(false)
|
||||
expect(value:IsSame(sameHour, TimeUnit.Hours)).to.equal(true)
|
||||
expect(value:IsSame(sameHour, TimeUnit.Years)).to.equal(true)
|
||||
|
||||
local sameDay = DateTime.new(2003, 6, 11, 14, 8, 9)
|
||||
|
||||
expect(value:IsSame(sameDay)).to.equal(false)
|
||||
expect(value:IsSame(sameDay, TimeUnit.Days)).to.equal(true)
|
||||
expect(value:IsSame(sameDay, TimeUnit.Years)).to.equal(true)
|
||||
|
||||
local sameMonth = DateTime.new(2003, 6, 12, 15, 8, 9)
|
||||
|
||||
expect(value:IsSame(sameMonth)).to.equal(false)
|
||||
expect(value:IsSame(sameMonth, TimeUnit.Months)).to.equal(true)
|
||||
expect(value:IsSame(sameMonth, TimeUnit.Years)).to.equal(true)
|
||||
|
||||
local sameYear = DateTime.new(2003, 7, 12, 15, 8, 9)
|
||||
|
||||
expect(value:IsSame(sameYear)).to.equal(false)
|
||||
expect(value:IsSame(sameYear, TimeUnit.Years)).to.equal(true)
|
||||
|
||||
local diffYear = DateTime.new(2004, 6, 11, 15, 8, 9)
|
||||
|
||||
expect(value:IsSame(diffYear)).to.equal(false)
|
||||
expect(value:IsSame(diffYear, TimeUnit.Years)).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should equate values using week boundaries", function()
|
||||
local sunday = DateTime.new(2017, 5, 7)
|
||||
local saturday = DateTime.new(2017, 5, 13)
|
||||
local monday = DateTime.new(2017, 5, 8)
|
||||
local tuesday = DateTime.new(2017, 5, 9)
|
||||
|
||||
-- TODO: Specify locale when that lands; default may break tests
|
||||
local function sameWeek(a, b)
|
||||
return a:IsSame(b, TimeUnit.Weeks, TimeZone.UTC)
|
||||
end
|
||||
|
||||
expect(sameWeek(monday, monday)).to.equal(true)
|
||||
|
||||
expect(sameWeek(sunday, monday)).to.equal(true)
|
||||
expect(sameWeek(tuesday, monday)).to.equal(true)
|
||||
expect(sameWeek(saturday, monday)).to.equal(true)
|
||||
|
||||
local nextSunday = DateTime.new(2017, 5, 14)
|
||||
|
||||
expect(sameWeek(nextSunday, monday)).to.equal(false)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
|
||||
|
||||
local PlaceInfoModel = {}
|
||||
|
||||
function PlaceInfoModel.new()
|
||||
local self = {}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function PlaceInfoModel.mock()
|
||||
local self = PlaceInfoModel.new()
|
||||
|
||||
self.builder = "builder"
|
||||
self.builderId = MockId()
|
||||
self.description = "description"
|
||||
self.imageToken = MockId()
|
||||
self.isPlayable = true
|
||||
self.name = "name"
|
||||
self.placeId = MockId()
|
||||
self.price = 0
|
||||
self.reasonProhibited = nil
|
||||
self.universeId = MockId()
|
||||
self.universeRootPlaceId = MockId()
|
||||
self.url = "url"
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function PlaceInfoModel.fromWeb(data)
|
||||
local self = data or {}
|
||||
self.placeId = tostring(self.placeId)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return PlaceInfoModel
|
||||
@@ -0,0 +1,12 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
|
||||
|
||||
return function(state, action)
|
||||
state = state or 0
|
||||
|
||||
if action.type == SetFriendCount.name then
|
||||
state = action.count
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local FriendCount = require(CorePackages.AppTempCommon.LuaChat.Reducers.FriendCount)
|
||||
local SetFriendCount = require(CorePackages.AppTempCommon.LuaApp.Actions.SetFriendCount)
|
||||
|
||||
it("should be zero by default", function()
|
||||
local state = FriendCount(nil, {})
|
||||
|
||||
expect(state).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should respond to SetFriendCount", function()
|
||||
local state = FriendCount(nil, {})
|
||||
state = FriendCount(state, SetFriendCount(520))
|
||||
|
||||
expect(state).to.equal(520)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Common = CorePackages.AppTempCommon.Common
|
||||
local LuaChat = CorePackages.AppTempCommon.LuaChat
|
||||
|
||||
local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos)
|
||||
|
||||
local Immutable = require(Common.Immutable)
|
||||
|
||||
return function(state, action)
|
||||
state = state or {}
|
||||
if action.type == ReceivedMultiplePlaceInfos.name then
|
||||
|
||||
local newInfos = {}
|
||||
for _, placeInfo in ipairs(action.placeInfos) do
|
||||
newInfos[placeInfo.placeId] = placeInfo
|
||||
end
|
||||
|
||||
state = Immutable.JoinDictionaries(state, newInfos)
|
||||
end
|
||||
return state
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
return function()
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local LuaApp = CorePackages.AppTempCommon.LuaApp
|
||||
local LuaChat = CorePackages.AppTempCommon.LuaChat
|
||||
|
||||
local MockId = require(LuaApp.MockId)
|
||||
local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos)
|
||||
|
||||
local PlaceInfosReducer = require(script.Parent.PlaceInfos)
|
||||
|
||||
describe("initial state", function()
|
||||
it("should return an initial table when passed nil", function()
|
||||
local state = PlaceInfosReducer(nil, {})
|
||||
expect(state).to.be.a("table")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("ReceivedMultiplePlaceInfos", function()
|
||||
it("should add place info to the store", function()
|
||||
local state = PlaceInfosReducer(nil, {})
|
||||
|
||||
local placeId = MockId()
|
||||
local returnedPlaceInfo = ReceivedMultiplePlaceInfos({
|
||||
{
|
||||
placeId = placeId,
|
||||
imageToken = "image-token",
|
||||
},
|
||||
})
|
||||
|
||||
state = PlaceInfosReducer(state, returnedPlaceInfo)
|
||||
|
||||
expect(state[placeId]).to.equal(returnedPlaceInfo.placeInfos[1])
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
local TimeUnit = setmetatable({}, {
|
||||
__index = function(self, key)
|
||||
error(("Invalid TimeUnit \"%s\""):format(tostring(key)), 2)
|
||||
end
|
||||
})
|
||||
|
||||
TimeUnit.Seconds = "Seconds"
|
||||
TimeUnit.Minutes = "Minutes"
|
||||
TimeUnit.Hours = "Hours"
|
||||
TimeUnit.Days = "Days"
|
||||
TimeUnit.Months = "Months"
|
||||
TimeUnit.Years = "Years"
|
||||
|
||||
-- Locale-specific
|
||||
TimeUnit.Weeks = "Weeks"
|
||||
|
||||
return TimeUnit
|
||||
@@ -0,0 +1,10 @@
|
||||
local TimeZone = setmetatable({}, {
|
||||
__index = function(self, key)
|
||||
error(("Invalid TimeZone \"%s\""):format(tostring(key)), 2)
|
||||
end
|
||||
})
|
||||
|
||||
TimeZone.UTC = -2
|
||||
TimeZone.Current = -1
|
||||
|
||||
return TimeZone
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
|
||||
local WebPresenceMap = require(CorePackages.AppTempCommon.LuaApp.Enum.WebPresenceMap)
|
||||
|
||||
return function(friendsPresence, store)
|
||||
local placeIds = {}
|
||||
|
||||
for _, presenceModel in pairs(friendsPresence) do
|
||||
if WebPresenceMap[presenceModel.userPresenceType] == User.PresenceType.IN_GAME
|
||||
and (not store:getState().UniversePlaceInfos[presenceModel.universeId]) then
|
||||
table.insert(placeIds, presenceModel.placeId)
|
||||
end
|
||||
end
|
||||
|
||||
return placeIds
|
||||
end
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
|
||||
local WebPresenceMap = require(CorePackages.AppTempCommon.LuaApp.Enum.WebPresenceMap)
|
||||
|
||||
local luaChatUseNewFriendsAndPresenceEndpoint = settings():GetFFlag("LuaChatUseNewFriendsAndPresenceEndpointV356")
|
||||
local luaChatPlayTogetherUseRootPresence = settings():GetFFlag("LuaChatPlayTogetherUseRootPresence")
|
||||
local luaChatRootPresenceEnabled = luaChatUseNewFriendsAndPresenceEndpoint and luaChatPlayTogetherUseRootPresence
|
||||
|
||||
return function(friendsPresence, store)
|
||||
for _, presenceModel in pairs(friendsPresence) do
|
||||
local userInStore = store:getState().Users[tostring(presenceModel.userId)]
|
||||
local previousUniverseId = userInStore and userInStore.universeId or nil
|
||||
|
||||
if luaChatRootPresenceEnabled then
|
||||
store:dispatch(ReceivedUserPresence(
|
||||
tostring(presenceModel.userId),
|
||||
WebPresenceMap[presenceModel.userPresenceType],
|
||||
presenceModel.lastLocation,
|
||||
presenceModel.placeId and tostring(presenceModel.placeId) or nil,
|
||||
presenceModel.rootPlaceId and tostring(presenceModel.rootPlaceId) or nil,
|
||||
presenceModel.gameId and tostring(presenceModel.gameId) or nil,
|
||||
presenceModel.lastOnline and tostring(presenceModel.lastOnline) or nil,
|
||||
presenceModel.universeId,
|
||||
previousUniverseId
|
||||
))
|
||||
else
|
||||
store:dispatch(ReceivedUserPresence(
|
||||
tostring(presenceModel.userId),
|
||||
WebPresenceMap[presenceModel.userPresenceType],
|
||||
presenceModel.lastLocation,
|
||||
presenceModel.placeId and tostring(presenceModel.placeId) or nil,
|
||||
presenceModel.gameId and tostring(presenceModel.gameId) or nil,
|
||||
presenceModel.universeId,
|
||||
previousUniverseId
|
||||
))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
local AnalyticsService = game:GetService("AnalyticsService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME = tonumber(settings():GetFVariable("SettingsHubInviteReleaseStreamTime"))
|
||||
or math.huge
|
||||
|
||||
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
|
||||
|
||||
function EventStream.new(overridePlatformTarget, overrideAnalyticsImpl)
|
||||
local self = {}
|
||||
setmetatable(self, EventStream)
|
||||
|
||||
self._analyticsImpl = overrideAnalyticsImpl or AnalyticsService
|
||||
self._platformTarget = overridePlatformTarget or getPlatformTarget()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
additionalArgs = additionalArgs or {}
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._analyticsImpl:SetRBXEventStream(self._platformTarget, eventContext, eventName, additionalArgs)
|
||||
|
||||
if not self.timerSteppedConnection then
|
||||
local lastGameTime = time()
|
||||
self.timerSteppedConnection = RunService.Stepped:Connect(function(gameTime)
|
||||
if gameTime - lastGameTime > SETTINGS_HUB_INVITE_RELEASE_STREAM_TIME then
|
||||
self:releaseRBXEventStream()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function EventStream:releaseRBXEventStream()
|
||||
self._analyticsImpl:ReleaseRBXEventStream(self._platformTarget)
|
||||
if self.timerSteppedConnection then
|
||||
self.timerSteppedConnection:Disconnect()
|
||||
self.timerSteppedConnection = nil
|
||||
end
|
||||
end
|
||||
|
||||
return EventStream
|
||||
@@ -0,0 +1,90 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local LuaApp = CorePackages.AppTempCommon.LuaApp
|
||||
|
||||
local Promise = require(LuaApp.Promise)
|
||||
|
||||
local DEFAULT_THROTTLING_PRIORITY = Enum.ThrottlingPriority.Extreme
|
||||
local DEFAULT_POST_ASYNC_CONTENT_TYPE = Enum.HttpContentType.ApplicationJson
|
||||
|
||||
-- httpRequest : (table, optional) an object that implements the same http functions as the data model
|
||||
return function(httpImpl)
|
||||
|
||||
local function doHttpPost(url, options)
|
||||
assert(options.postBody, "Expected a postBody to be specified with this request")
|
||||
assert(type(options.postBody) == "string", "Expected postBody to be a string")
|
||||
|
||||
if not options.contentType then
|
||||
options.contentType = DEFAULT_POST_ASYNC_CONTENT_TYPE
|
||||
end
|
||||
|
||||
if not options.throttlingPriority then
|
||||
options.throttlingPriority = DEFAULT_THROTTLING_PRIORITY
|
||||
end
|
||||
|
||||
return function()
|
||||
return httpImpl:PostAsyncFullUrl(
|
||||
url,
|
||||
options.postBody,
|
||||
options.throttlingPriority,
|
||||
options.contentType
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function doHttpGet(url)
|
||||
return function()
|
||||
return httpImpl:GetAsyncFullUrl(url, DEFAULT_THROTTLING_PRIORITY)
|
||||
end
|
||||
end
|
||||
|
||||
-- return the request function
|
||||
-- url : (string)
|
||||
-- requestMethod : (string) "GET", "POST"
|
||||
-- args : (table, optional)
|
||||
-- options.throttlingPriority : (Enum.ThrottlingPriority, optional)
|
||||
-- options.contentType : (Enum.HttpContentType, optional)
|
||||
-- options.postBody : (string, optional ("POST" only))
|
||||
-- RETURNS : (promise<HttpResponse or string>)
|
||||
return function(url, requestMethod, options)
|
||||
assert(type(url) == "string", "Expected url to be a string")
|
||||
assert(type(requestMethod) == "string", "Expected requestMethod to be a string")
|
||||
assert(not options or type(options) == "table", "Expected options to be a table")
|
||||
requestMethod = string.upper(requestMethod)
|
||||
|
||||
local httpFunction
|
||||
if requestMethod == "POST" then
|
||||
httpFunction = doHttpPost(url, options)
|
||||
elseif requestMethod == "GET" then
|
||||
httpFunction = doHttpGet(url)
|
||||
else
|
||||
error(string.format("Unsupported requestMethod : %s", requestMethod or "nil"))
|
||||
end
|
||||
|
||||
return Promise.new(function(resolve, reject)
|
||||
if httpFunction then
|
||||
spawn(function()
|
||||
local success, response = pcall(httpFunction)
|
||||
|
||||
if success then
|
||||
local jsonSuccess, decodedJson = pcall(function()
|
||||
return HttpService:JSONDecode(response)
|
||||
end)
|
||||
if jsonSuccess then
|
||||
resolve({
|
||||
responseBody = decodedJson,
|
||||
})
|
||||
else
|
||||
reject(decodedJson)
|
||||
end
|
||||
else
|
||||
reject(response)
|
||||
end
|
||||
end)
|
||||
else
|
||||
reject()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
return function()
|
||||
local httpRequest = require(script.Parent.httpRequest)
|
||||
|
||||
local function createTestRequestFunc(testResponse)
|
||||
local requestService = {}
|
||||
function requestService:GetAsyncFullUrl()
|
||||
return testResponse
|
||||
end
|
||||
function requestService:PostAsyncFullUrl()
|
||||
return testResponse
|
||||
end
|
||||
|
||||
return httpRequest(requestService)
|
||||
end
|
||||
|
||||
it("should return a function", function()
|
||||
expect(httpRequest()).to.be.ok()
|
||||
expect(type(httpRequest())).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should validate its inputs", function()
|
||||
local testRequest = createTestRequestFunc()
|
||||
local function testParams(url, requestMethod, args)
|
||||
return function()
|
||||
testRequest(url, requestMethod, args)
|
||||
end
|
||||
end
|
||||
|
||||
local validUrl = "friends.roblox.com"
|
||||
local validMethod = "GET"
|
||||
local validArgs = {}
|
||||
|
||||
-- url checks
|
||||
expect(testParams(nil, validMethod, validArgs)).to.throw()
|
||||
expect(testParams(123, validMethod, validArgs)).to.throw()
|
||||
expect(testParams({}, validMethod, validArgs)).to.throw()
|
||||
expect(testParams(true, validMethod, validArgs)).to.throw()
|
||||
expect(testParams(function() end, validMethod, validArgs)).to.throw()
|
||||
|
||||
-- request method checks
|
||||
expect(testParams(validUrl, nil, validArgs)).to.throw()
|
||||
expect(testParams(validUrl, 123, validArgs)).to.throw()
|
||||
expect(testParams(validUrl, {}, validArgs)).to.throw()
|
||||
expect(testParams(validUrl, true, validArgs)).to.throw()
|
||||
expect(testParams(validUrl, function() end, validArgs)).to.throw()
|
||||
|
||||
-- args checks
|
||||
expect(testParams(validUrl, validMethod, 123)).to.throw()
|
||||
expect(testParams(validUrl, validMethod, "Test")).to.throw()
|
||||
expect(testParams(validUrl, validMethod, true)).to.throw()
|
||||
expect(testParams(validUrl, validMethod, function() end)).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if the requestMethod isn't supported", function()
|
||||
local testRequest = createTestRequestFunc("foo")
|
||||
|
||||
expect(function()
|
||||
testRequest("testUrl", "GIVEANDTAKE")
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
return function(targetString, blacklistedCharacter)
|
||||
local charactersArray = {}
|
||||
local indexArray = {}
|
||||
for index, byte in utf8.codes(targetString) do
|
||||
local graphemeCharacter = utf8.char(byte)
|
||||
table.insert(charactersArray, 1, graphemeCharacter)
|
||||
table.insert(indexArray, 1, index)
|
||||
end
|
||||
for index, graphemeCharacter in ipairs(charactersArray) do
|
||||
if graphemeCharacter ~= blacklistedCharacter then
|
||||
return targetString:sub(1, indexArray[index])
|
||||
end
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
return function()
|
||||
local trimCharacterFromEndString = require(script.Parent.trimCharacterFromEndString)
|
||||
|
||||
describe("single byte characters", function()
|
||||
it("should not trim a string if it does not end with passed character", function()
|
||||
local passedString = "testing"
|
||||
local passedCharacter = "/"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString)
|
||||
end)
|
||||
|
||||
it("should trim a string if it ends with a single instance of the passed character", function()
|
||||
local passedString = "testing/"
|
||||
local passedCharacter = "/"
|
||||
local expectedString = "testing"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString)
|
||||
end)
|
||||
|
||||
it("should trim a string if it ends with multiple instances of the passed character", function()
|
||||
local passedString = "testing///"
|
||||
local passedCharacter = "/"
|
||||
local expectedString = "testing"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString)
|
||||
end)
|
||||
|
||||
it("should do nothing if the passed character is empty", function()
|
||||
local passedString = "hunter2"
|
||||
local passedCharacter = ""
|
||||
local expectedString = "hunter2"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("multiple byte characters", function()
|
||||
it("should not trim a string if it does not end with passed character", function()
|
||||
local passedString = "testing"
|
||||
local passedCharacter = "🐶"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(passedString)
|
||||
end)
|
||||
|
||||
it("should trim a string if it ends with a single instance of the passed character", function()
|
||||
local passedString = "testing🐶"
|
||||
local passedCharacter = "🐶"
|
||||
local expectedString = "testing"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString)
|
||||
end)
|
||||
|
||||
it("should trim a string if it ends with multiple instances of the passed character", function()
|
||||
local passedString = "testing🐶🐶🐶"
|
||||
local passedCharacter = "🐶"
|
||||
local expectedString = "testing"
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("a string with all blacklisted characters", function()
|
||||
it("should return a empty string", function()
|
||||
local passedString = "pppppppppppp"
|
||||
local passedCharacter = "p"
|
||||
local expectedString = ""
|
||||
|
||||
expect(trimCharacterFromEndString(passedString, passedCharacter)).to.equal(expectedString)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,193 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local Modalifier = require(script.Parent.Modalifier)
|
||||
|
||||
local Dropdown = Roact.Component:extend("Dropdown")
|
||||
|
||||
function Dropdown:init()
|
||||
self.state = {
|
||||
Open = false,
|
||||
Hovered = false,
|
||||
}
|
||||
end
|
||||
|
||||
function Dropdown:render()
|
||||
local listChildren = {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}),
|
||||
}
|
||||
|
||||
for index, listItem in ipairs(self.props.ListItems) do
|
||||
listChildren["ListObject"..index] = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, self.props.ListItemHeight),
|
||||
LayoutOrder = index,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Padding = Roact.createElement("UIPadding", {
|
||||
PaddingBottom = UDim.new(0, 4),
|
||||
}),
|
||||
|
||||
Button = Roact.createElement("TextButton", {
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
TextColor3 = self.props.TextColor3,
|
||||
Text = "",
|
||||
BackgroundColor3 = self.props.BackgroundColor3,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
listItem.OnActivated()
|
||||
self:setState({
|
||||
Open = false,
|
||||
})
|
||||
end,
|
||||
}, {
|
||||
Padding = Roact.createElement("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 4),
|
||||
}),
|
||||
|
||||
Text = Roact.createElement("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
TextColor3 = self.props.TextColor3,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Text = listItem.Text,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local visibleChildren
|
||||
|
||||
if self.state.Open then
|
||||
visibleChildren = {
|
||||
Button = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = 0,
|
||||
Text = "",
|
||||
BackgroundColor3 = self.props.ButtonDownColor3,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
Open = false,
|
||||
})
|
||||
end,
|
||||
|
||||
-- Empty functions here work around bug-147 in Roact
|
||||
[Roact.Event.InputBegan] = function()
|
||||
end,
|
||||
|
||||
[Roact.Event.InputEnded] = function()
|
||||
end,
|
||||
}, {
|
||||
Padding = Roact.createElement("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 4),
|
||||
}),
|
||||
|
||||
TextLabel = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
TextColor3 = self.props.TextColor3,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = self.props.CurrentText,
|
||||
}),
|
||||
|
||||
Arrow = Roact.createElement("ImageLabel", {
|
||||
Position = UDim2.new(1, -15, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Size = UDim2.new(0, 5, 0, 3),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/menuDownArrow.png",
|
||||
}),
|
||||
}),
|
||||
|
||||
Modalifier = Roact.createElement(Modalifier, {
|
||||
Window = self.props.Window,
|
||||
OnClosed = function()
|
||||
self:setState({
|
||||
Open = false,
|
||||
})
|
||||
end,
|
||||
Render = function(position)
|
||||
return Roact.createElement("Frame", {
|
||||
Position = UDim2.new(0, position.X, 0, position.Y + self.props.ListItemHeight),
|
||||
Size = UDim2.new(0, 160, 0, #(self.props.ListItems) * self.props.ListItemHeight),
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = self.props.BackgroundColor3,
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
}, listChildren)
|
||||
end,
|
||||
}),
|
||||
}
|
||||
else
|
||||
visibleChildren = {
|
||||
Button = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = self.state.Hovered and self.props.ButtonHoverColor3 or self.props.BackgroundColor3,
|
||||
Text = "",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
Open = true,
|
||||
Hovered = false,
|
||||
})
|
||||
end,
|
||||
|
||||
[Roact.Event.InputBegan] = function(gui, input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseMovement then
|
||||
self:setState({
|
||||
Hovered = true,
|
||||
})
|
||||
end
|
||||
end,
|
||||
|
||||
[Roact.Event.InputEnded] = function(gui, input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseMovement then
|
||||
self:setState({
|
||||
Hovered = false,
|
||||
})
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
Padding = Roact.createElement("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 4),
|
||||
}),
|
||||
|
||||
TextLabel = Roact.createElement("TextLabel", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
TextColor3 = self.props.TextColor3,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = self.props.CurrentText,
|
||||
}),
|
||||
|
||||
Arrow = Roact.createElement("ImageLabel", {
|
||||
Position = UDim2.new(1, -15, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Size = UDim2.new(0, 5, 0, 3),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/menuDownArrow.png",
|
||||
}),
|
||||
}),
|
||||
}
|
||||
end
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
}, visibleChildren)
|
||||
end
|
||||
|
||||
return Dropdown
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local LocaleSelector = require(script.Parent.LocaleSelector)
|
||||
|
||||
local LabeledLocaleSelector = Roact.Component:extend("LabeledLocaleSelector")
|
||||
|
||||
function LabeledLocaleSelector:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 300, 0, 25),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = self.props.LayoutOrder
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
|
||||
LocaleIdLabel = Roact.createElement("TextLabel", {
|
||||
Text = self.props.LabelText,
|
||||
TextXAlignment = "Right",
|
||||
TextYAlignment = "Center",
|
||||
TextColor3 = self.props.TextColor3,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, 50, 0, 25),
|
||||
LayoutOrder = 0,
|
||||
}),
|
||||
|
||||
LocaleSelectorGroup = Roact.createElement(LocaleSelector, {
|
||||
Window = self.props.Window,
|
||||
Size = UDim2.new(0, 200, 0, 25),
|
||||
BackgroundColor3 = self.props.BackgroundColor3,
|
||||
TextColor3 = self.props.TextColor3,
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
ButtonHoverColor3 = self.props.ButtonHoverColor3,
|
||||
ButtonDownColor3 = self.props.ButtonDownColor3,
|
||||
InitialLocaleId = self.props.InitialLocaleId,
|
||||
SetLocaleId = self.props.SetLocaleId,
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return LabeledLocaleSelector
|
||||
@@ -0,0 +1,127 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local Dropdown = require(script.Parent.Dropdown)
|
||||
|
||||
local customMenuItemText = "(Custom)"
|
||||
|
||||
local localeInfos = {
|
||||
{ localeId = "en-us", name = "English" },
|
||||
{ localeId = "fr-fr", name = "French" },
|
||||
{ localeId = "de-de", name = "German" },
|
||||
{ localeId = "pt-br", name = "Portuguese (Brazil)" },
|
||||
{ localeId = "es-es", name = "Spanish" },
|
||||
}
|
||||
|
||||
local localeNameMap = {}
|
||||
|
||||
for _, info in ipairs(localeInfos) do
|
||||
localeNameMap[info.localeId] = info.name
|
||||
end
|
||||
|
||||
local LocaleSelector = Roact.Component:extend("LocaleSelector")
|
||||
|
||||
function LocaleSelector:init()
|
||||
self.state = {
|
||||
LocaleId = self.props.InitialLocaleId
|
||||
}
|
||||
|
||||
self.textBoxRef = Roact.createRef()
|
||||
end
|
||||
|
||||
local function getMenuTextForLocale(localeId)
|
||||
if localeId == "" then
|
||||
return customMenuItemText
|
||||
end
|
||||
return localeNameMap[localeId] or customMenuItemText
|
||||
end
|
||||
|
||||
function LocaleSelector:render()
|
||||
local ListItems = {}
|
||||
|
||||
for index, item in ipairs(localeInfos) do
|
||||
ListItems[index] = {
|
||||
Text = item.name,
|
||||
OnActivated = function()
|
||||
self:setState({
|
||||
LocaleId = item.localeId,
|
||||
})
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
ListItems[#localeInfos + 1] = {
|
||||
Text = customMenuItemText,
|
||||
OnActivated = function()
|
||||
self:setState({
|
||||
LocaleId = "",
|
||||
})
|
||||
|
||||
self.textBoxRef.current:CaptureFocus()
|
||||
end
|
||||
}
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = self.props.Size,
|
||||
BackgroundTransparency = 1.0,
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
|
||||
LocaleNameDropdown = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 160, 0, 25),
|
||||
LayoutOrder = 1,
|
||||
}, {
|
||||
Dropdown = Roact.createElement(Dropdown, {
|
||||
Window = self.props.Window,
|
||||
BackgroundColor3 = self.props.BackgroundColor3,
|
||||
TextColor3 = self.props.TextColor3,
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
ButtonHoverColor3 = self.props.ButtonHoverColor3,
|
||||
ButtonDownColor3 = self.props.ButtonDownColor3,
|
||||
CurrentText = getMenuTextForLocale(self.state.LocaleId),
|
||||
ListItemHeight = 25,
|
||||
ListItems = ListItems,
|
||||
}),
|
||||
}),
|
||||
|
||||
LocaleIdTextBox = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 50, 0, 25),
|
||||
BorderSizePixel = 1,
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
BackgroundColor3 = self.props.BackgroundColor3,
|
||||
LayoutOrder = 2,
|
||||
}, {
|
||||
Padding = Roact.createElement("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 4),
|
||||
}),
|
||||
|
||||
TextboxInternal = Roact.createElement("TextBox", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
TextColor3 = self.props.TextColor3,
|
||||
BorderColor3 = self.props.BorderColor3,
|
||||
Text = self.state.LocaleId,
|
||||
TextXAlignment = "Left",
|
||||
ClearTextOnFocus = false,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Ref] = self.textBoxRef,
|
||||
|
||||
[Roact.Event.FocusLost] = function()
|
||||
self:setState({
|
||||
LocaleId = self.textBoxRef.current.Text,
|
||||
})
|
||||
end,
|
||||
})
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
function LocaleSelector:didUpdate()
|
||||
self.props.SetLocaleId(self.state.LocaleId)
|
||||
end
|
||||
|
||||
return LocaleSelector
|
||||
@@ -0,0 +1,53 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local Modalifier = Roact.Component:extend("Modalifier")
|
||||
|
||||
function Modalifier:init()
|
||||
self.state = {
|
||||
ContentAbsolutePosition = Vector2.new(0, 0),
|
||||
Showing = false,
|
||||
}
|
||||
end
|
||||
|
||||
function Modalifier:render()
|
||||
local visibleChildren = {}
|
||||
if self.state.Showing then
|
||||
visibleChildren = {
|
||||
content = self.props.Render(self.state.ContentAbsolutePosition),
|
||||
}
|
||||
end
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
[Roact.Change.AbsolutePosition] = function(instance)
|
||||
spawn(function()
|
||||
self:setState({
|
||||
ContentAbsolutePosition = instance.AbsolutePosition,
|
||||
Showing = true,
|
||||
})
|
||||
end)
|
||||
end,
|
||||
}, {
|
||||
Portal = Roact.createElement(Roact.Portal, {
|
||||
-- Future redesign strongly suggested here: https://jira.roblox.com/browse/CLILUACORE-287
|
||||
target = self.props.Window,
|
||||
}, {
|
||||
Curtain = Roact.createElement("TextButton", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Text = "",
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
self.props.OnClosed()
|
||||
end,
|
||||
}, visibleChildren),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Modalifier
|
||||
@@ -0,0 +1,76 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local LabeledLocaleSelector = require(script.Parent.LabeledLocaleSelector)
|
||||
|
||||
local PlayerLocaleView = Roact.Component:extend("PlayerLocaleView")
|
||||
local StudioEnableLuaAPIsForThemes = settings():GetFFlag("StudioEnableLuaAPIsForThemes")
|
||||
|
||||
local robloxLocaleLabelText = "Locale"
|
||||
|
||||
function PlayerLocaleView:init()
|
||||
if StudioEnableLuaAPIsForThemes then
|
||||
self.state = {
|
||||
Theme = settings().Studio.Theme,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function PlayerLocaleView:render()
|
||||
local TextColor3
|
||||
local BorderColor3
|
||||
local BackgroundColor3
|
||||
local ButtonHoverColor3
|
||||
local ButtonDownColor3
|
||||
|
||||
if StudioEnableLuaAPIsForThemes then
|
||||
TextColor3 = self.state.Theme:GetColor(Enum.StudioStyleGuideColor.BrightText)
|
||||
BorderColor3 = self.state.Theme:GetColor(Enum.StudioStyleGuideColor.Border)
|
||||
BackgroundColor3 = self.state.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground)
|
||||
ButtonHoverColor3 = self.state.Theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Hover)
|
||||
ButtonDownColor3 = self.state.Theme:GetColor(Enum.StudioStyleGuideColor.Button, Enum.StudioStyleGuideModifier.Pressed)
|
||||
else
|
||||
TextColor3 = Color3.new( 0, 0, 0 )
|
||||
BorderColor3 = Color3.new( 0.713726, 0.713726, 0.713726 )
|
||||
BackgroundColor3 = Color3.new( 1, 1, 1 )
|
||||
ButtonHoverColor3 = Color3.new( 0.894118, 0.933333, 0.996078 )
|
||||
ButtonDownColor3 = Color3.new( 0.858824, 0.858824, 0.858824 )
|
||||
end
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = BackgroundColor3,
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
Padding = Roact.createElement("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 5),
|
||||
}),
|
||||
|
||||
Roblox = Roact.createElement(LabeledLocaleSelector, {
|
||||
Window = self.props.Window,
|
||||
LabelText = robloxLocaleLabelText,
|
||||
TextColor3 = TextColor3,
|
||||
BorderColor3 = BorderColor3,
|
||||
BackgroundColor3 = BackgroundColor3,
|
||||
ButtonHoverColor3 = ButtonHoverColor3,
|
||||
ButtonDownColor3 = ButtonDownColor3,
|
||||
InitialLocaleId = self.props.InitialRobloxLocaleId,
|
||||
SetLocaleId = self.props.SetRobloxLocaleId,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
function PlayerLocaleView:didMount()
|
||||
if StudioEnableLuaAPIsForThemes then
|
||||
settings().Studio.ThemeChanged:Connect(
|
||||
function()
|
||||
self:setState({
|
||||
Theme = settings().Studio.Theme,
|
||||
})
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return PlayerLocaleView
|
||||
@@ -0,0 +1,100 @@
|
||||
local runService = game:GetService("RunService")
|
||||
local LocalizationService = game:GetService("LocalizationService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local enabled = runService:IsEdit()
|
||||
|
||||
local function createTextScraperControls(toolbar, plugin)
|
||||
function getTextScraperAsset()
|
||||
return LocalizationService.IsTextScraperRunning
|
||||
and "rbxasset://textures/localizationUIScrapingOn.png"
|
||||
or "rbxasset://textures/localizationUIScrapingOff.png"
|
||||
end
|
||||
|
||||
local captureButton = toolbar:CreateButton(
|
||||
"Text Capture",
|
||||
"Enable untranslated text capture",
|
||||
getTextScraperAsset()
|
||||
)
|
||||
|
||||
local exportButton = toolbar:CreateButton(
|
||||
"Export",
|
||||
"Export LocalizationTables under LocalizationService to CSV files",
|
||||
"rbxasset://textures/localizationExport.png"
|
||||
)
|
||||
|
||||
local importButton = toolbar:CreateButton(
|
||||
"Import",
|
||||
"Import CSV files to LocalizationTables under LocalizationService",
|
||||
"rbxasset://textures/localizationImport.png"
|
||||
)
|
||||
|
||||
captureButton.Enabled = enabled
|
||||
captureButton.Click:Connect(function()
|
||||
if not LocalizationService.IsTextScraperRunning then
|
||||
LocalizationService:StartTextScraper()
|
||||
else
|
||||
LocalizationService:StopTextScraper()
|
||||
end
|
||||
captureButton.Icon = getTextScraperAsset()
|
||||
end)
|
||||
|
||||
exportButton.Enabled = enabled
|
||||
exportButton.Click:Connect(function()
|
||||
LocalizationService:PromptExportToCSVs()
|
||||
end)
|
||||
|
||||
importButton.Enabled = enabled
|
||||
importButton.Click:Connect(function()
|
||||
LocalizationService:PromptImportFromCSVs()
|
||||
end)
|
||||
end
|
||||
|
||||
local function getInitialRobloxLocaleId()
|
||||
local forcedLocale = LocalizationService.RobloxForcePlayModeRobloxLocaleId
|
||||
|
||||
if forcedLocale ~= "" then
|
||||
return forcedLocale
|
||||
end
|
||||
|
||||
return LocalizationService.RobloxLocaleId
|
||||
end
|
||||
|
||||
local function createPlayerLocaleViewButton(toolbar, plugin)
|
||||
local PlayerLocaleView = require(script.Parent.Components.PlayerLocaleView)
|
||||
local Roact = require(CorePackages.Roact)
|
||||
|
||||
local Window = plugin:CreateDockWidgetPluginGui("LocalizationTesting",
|
||||
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Left))
|
||||
Window.Title = "Test Language"
|
||||
|
||||
local params = {
|
||||
Window = Window,
|
||||
SetRobloxLocaleId = function(localeId)
|
||||
LocalizationService.RobloxForcePlayModeRobloxLocaleId = localeId
|
||||
end,
|
||||
InitialRobloxLocaleId = getInitialRobloxLocaleId(),
|
||||
}
|
||||
|
||||
Roact.mount(Roact.createElement(PlayerLocaleView, params), Window)
|
||||
Window.Enabled = false
|
||||
|
||||
local button = toolbar:CreateButton(
|
||||
"Test Language",
|
||||
"Hide/show the Localization Testing view",
|
||||
"rbxasset://textures/localizationTestingIcon.png")
|
||||
|
||||
button.Enabled = enabled
|
||||
button.Click:Connect(
|
||||
function()
|
||||
Window.Enabled = not Window.Enabled
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
return function(plugin)
|
||||
local toolbar = plugin:CreateToolbar("Localization Tools")
|
||||
createTextScraperControls(toolbar, plugin)
|
||||
createPlayerLocaleViewButton(toolbar, plugin)
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
return function()
|
||||
local Roact = require(CorePackages.Roact)
|
||||
local Dropdown = require(script.Parent.Parent.Components.Dropdown)
|
||||
|
||||
it("mounts and unmounts", function()
|
||||
local handle = Roact.mount(Roact.createElement(Dropdown, {
|
||||
ListItems = {},
|
||||
}))
|
||||
|
||||
Roact.unmount(handle)
|
||||
end)
|
||||
|
||||
itFIXME("inits with an item list and includes the default value in the resulting UI", function()
|
||||
local container = Instance.new("Frame")
|
||||
|
||||
local element = Roact.createElement(Dropdown, {
|
||||
CurrentText = "Option 1 (default)",
|
||||
ListItems = {
|
||||
"Option 1",
|
||||
"Option 2"
|
||||
},
|
||||
})
|
||||
|
||||
local handle = Roact.mount(element, container)
|
||||
expect(container.Frame.button.textLabel.Text).to.equal("Option 1 (default)")
|
||||
Roact.unmount(handle)
|
||||
end)
|
||||
end
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
return function()
|
||||
local Roact = require(CorePackages.Roact)
|
||||
local LabeledLocaleSelector = require(script.Parent.Parent.Components.LabeledLocaleSelector)
|
||||
|
||||
it("mounts and unmounts", function()
|
||||
local element = Roact.createElement(LabeledLocaleSelector)
|
||||
local handle = Roact.mount(element)
|
||||
Roact.unmount(handle)
|
||||
end)
|
||||
|
||||
it("inits with label text and displays that label text", function()
|
||||
local container = Instance.new("Frame")
|
||||
|
||||
local element = Roact.createElement(LabeledLocaleSelector, {
|
||||
LabelText = "Choose your locale"
|
||||
})
|
||||
|
||||
local handle = Roact.mount(element, container)
|
||||
expect(container.Frame.LocaleIdLabel.Text).to.equal("Choose your locale")
|
||||
Roact.unmount(handle)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local function recursivePrint(node, indent)
|
||||
indent = indent or ""
|
||||
for _, child in pairs(node:GetChildren()) do
|
||||
print("|"..indent..tostring( child.Name ))
|
||||
recursivePrint(child, indent.." ")
|
||||
end
|
||||
end
|
||||
|
||||
return function()
|
||||
local Roact = require(CorePackages.Roact)
|
||||
local LocaleSelector = require(script.Parent.Parent.Components.LocaleSelector)
|
||||
|
||||
it("mounts and unmounts", function()
|
||||
local element = Roact.createElement(LocaleSelector)
|
||||
local handle = Roact.mount(element)
|
||||
Roact.unmount(handle)
|
||||
end)
|
||||
|
||||
it("inits with a selected locale and displays that locale", function()
|
||||
local container = Instance.new("Frame")
|
||||
|
||||
local element = Roact.createElement(LocaleSelector, {
|
||||
InitialLocaleId = "kw-gb"
|
||||
})
|
||||
|
||||
local handle = Roact.mount(element, container)
|
||||
expect(container.Frame.LocaleIdTextBox.TextboxInternal.Text).to.equal("kw-gb")
|
||||
Roact.unmount(handle)
|
||||
end)
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user