This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(user)
return {
user = user,
}
end)
@@ -0,0 +1,14 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(id, participants, title, lastUpdated)
return {
conversationId = id,
participants = participants,
title = title,
lastUpdated = lastUpdated,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(alert)
return {
alert = alert,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(placeIds)
return {
placeIds = placeIds,
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(imageToken)
return {
imageToken = imageToken,
}
end)
@@ -0,0 +1,19 @@
local LuaChat = script.Parent.Parent
local WebApi = require(LuaChat.WebApi)
local SetChatEnabled = require(LuaChat.Actions.SetChatEnabled)
return function(onSuccess)
return function(store)
spawn(function()
local status, response = WebApi.GetChatSettings()
if status ~= WebApi.Status.OK then
warn("Failure in WebApi.GetChatSettings", status)
return
end
store:dispatch(SetChatEnabled(response.chatEnabled))
if onSuccess then
onSuccess(response.chatEnabled)
end
end)
end
end
@@ -0,0 +1,24 @@
local LuaChat = script.Parent.Parent
local WebApi = require(LuaChat.WebApi)
local FetchChatSettingsStarted = require(LuaChat.Actions.FetchChatSettingsStarted)
local FetchChatSettingsCompleted = require(LuaChat.Actions.FetchChatSettingsCompleted)
local FetchChatSettingsFailed = require(LuaChat.Actions.FetchChatSettingsFailed)
return function(onSuccess)
return function(store)
store:dispatch(FetchChatSettingsStarted())
spawn(function()
local status, response = WebApi.GetChatSettings()
if status ~= WebApi.Status.OK then
store:dispatch(FetchChatSettingsFailed(status))
warn("Failure in WebApi.GetChatSettings", status)
return
end
store:dispatch(FetchChatSettingsCompleted(response))
if onSuccess then
onSuccess(response)
end
end)
end
end
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(settings)
return {
settings = settings,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(status)
return {
status = status,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(isFetchedOldestConversation)
return {
value = isFetchedOldestConversation,
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, isFetchedOldestMessage)
return {
conversationId = conversationId,
fetchedOldestMessage = isFetchedOldestMessage,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, isFetchingOlderMessages)
return {
conversationId = conversationId,
fetchingOlderMessages = isFetchingOlderMessages,
}
end)
@@ -0,0 +1,13 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
local RetrievalStatus = require(Modules.LuaApp.Enum.RetrievalStatus)
return Action(script.Name, function(userId)
return {
userId = userId,
status = RetrievalStatus.Fetching,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,18 @@
local Modules = script.Parent.Parent
local WebApi = require(Modules.WebApi)
local SetFriendCount = require(Modules.Actions.SetFriendCount)
return function()
return function(store)
spawn(function()
local status, totalCount = WebApi.GetFriendCount()
if status ~= WebApi.Status.OK then
store:dispatch(SetFriendCount(0)) -- Remember to come back and add status instead of setting to zero
else
store:dispatch(SetFriendCount(totalCount))
end
end)
end
end
@@ -0,0 +1,47 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local LuaChat = Modules.LuaChat
local WebApi = require(LuaChat.WebApi)
local PlaceInfoModel = require(LuaChat.Models.PlaceInfoModel)
local RequestMultiplePlaceInfos = require(LuaChat.Actions.RequestMultiplePlaceInfos)
local FailedToFetchMultiplePlaceInfos = require(LuaChat.Actions.FailedToFetchMultiplePlaceInfos)
local ReceivedMultiplePlaceInfos = require(LuaChat.Actions.ReceivedMultiplePlaceInfos)
return function(placeIdList)
return function(store)
local state = store:getState()
local placesToFetch = {}
if state.ChatAppReducer.PlaceInfos then
for _, placeId in pairs(placeIdList) do
if not state.ChatAppReducer.PlaceInfosAsync[placeId] then
table.insert(placesToFetch, placeId)
end
end
if #placesToFetch == 0 then
return
end
end
store:dispatch(RequestMultiplePlaceInfos(placesToFetch))
spawn(function()
local status, result = WebApi.GetMultiplePlaceInfos(placesToFetch)
if status ~= WebApi.Status.OK then
warn("WebApi failure in GetMultiplePlaceInfos")
store:dispatch(FailedToFetchMultiplePlaceInfos(placesToFetch))
return
end
local placeInfos = {}
for _, placeInfoData in pairs(result) do
table.insert(placeInfos, PlaceInfoModel.fromWeb(placeInfoData))
end
store:dispatch(ReceivedMultiplePlaceInfos(placeInfos))
end)
end
end
@@ -0,0 +1,52 @@
local Modules = script.Parent.Parent
local WebApi = require(Modules.WebApi)
local ThumbnailModel = require(Modules.Models.ThumbnailModel)
local RequestPlaceThumbnail = require(Modules.Actions.RequestPlaceThumbnail)
local ReceivedPlaceThumbnail = require(Modules.Actions.ReceivedPlaceThumbnail)
local FailedToFetchPlaceThumbnail = require(Modules.Actions.FailedToFetchPlaceThumbnail)
local RETRY_COUNT = 3
local WAIT_TIME = 2
return function(imageToken, width, height)
return function(store)
local state = store:getState()
if state.ChatAppReducer.PlaceThumbnailsAsync[imageToken] then
return
end
store:dispatch(RequestPlaceThumbnail(imageToken))
spawn(function()
local thumbnail = ''
local retryCount = RETRY_COUNT
local waitTime = WAIT_TIME
while (retryCount > 0) do
local status, result = WebApi.GetPlaceThumbnail(imageToken, width, height)
if status ~= WebApi.Status.OK then
warn("WebApi failure in GetPlaceThumbnail")
store:dispatch(FailedToFetchPlaceThumbnail(imageToken))
break
else
local placeThumbnailData = result[1]
if placeThumbnailData.final == true then
thumbnail = placeThumbnailData.url
break
end
end
retryCount = retryCount - 1
if retryCount > 0 then
wait(waitTime)
waitTime = waitTime * 2
end
end
local thumbnailModel = ThumbnailModel.fromWeb(thumbnail)
store:dispatch(ReceivedPlaceThumbnail(imageToken, thumbnailModel))
end)
end
end
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, messageId)
return {
conversationId = conversationId,
messageId = messageId,
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, messageId)
return {
conversationId = conversationId,
messageId = messageId,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,185 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local LuaChat = Modules.LuaChat
local Actions = LuaChat.Actions
local Constants = require(LuaChat.Constants)
local PlaceInfoModel = require(LuaChat.Models.PlaceInfoModel)
local ToastModel = require(LuaChat.Models.ToastModel)
local WebApi = require(LuaChat.WebApi)
local FailedToFetchedMostRecentlyPlayedGames = require(Actions.FailedToFetchMostRecentlyPlayedGames)
local FailedToFetchMultiplePlaceInfos = require(Actions.FailedToFetchMultiplePlaceInfos)
local FetchedMostRecentlyPlayedGames = require(Actions.FetchedMostRecentlyPlayedGames)
local FetchingMostRecentlyPlayedGames = require(Actions.FetchingMostRecentlyPlayedGames)
local GameFailedToPin = require(Actions.GameFailedToPin)
local GameFailedToUnpin = require(Actions.GameFailedToUnpin)
local PinnedGame = require(Actions.PinnedGame)
local PinningGame = require(Actions.PinningGame)
local ReceivedMultiplePlaceInfos = require(Actions.ReceivedMultiplePlaceInfos)
local RequestMultiplePlaceInfos = require(Actions.RequestMultiplePlaceInfos)
local SetMostRecentlyPlayedGamesForUser = require(Actions.SetMostRecentlyPlayedGamesForUser)
local SetMostRecentlyPlayedPlayableGameForUser = require(Actions.SetMostRecentlyPlayedPlayableGameForUser)
local SetPinnedGameForConversation = require(Actions.SetPinnedGameForConversation)
local ShowToast = require(Actions.ShowToast)
local UnpinnedGame = require(Actions.UnpinnedGame)
local UnpinningGame = require(Actions.UnpinningGame)
local PlayTogetherActions = {}
local HOME_GAMES_SORTS = "HomeSorts"
local MOST_RECENTLY_PLAYED_GAMES = "MyRecent"
function PlayTogetherActions.PinGame(conversationId, universeId)
return function(store)
if universeId == store:getState().ChatAppReducer.Conversations[conversationId].pinnedGame.universeId then
local messageKey = "Feature.Chat.Message.AlreadyPinnedGame"
local toastModel = ToastModel.new(Constants.ToastIDs.PIN_PINNED_GAME, messageKey)
store:dispatch(ShowToast(toastModel))
return
end
if store:getState().ChatAppReducer.PlayTogetherAsync.pinningGames[conversationId] then
return
end
store:dispatch(PinningGame(conversationId))
spawn(function()
local status, _ = WebApi.PinGame(conversationId, universeId)
if status == WebApi.Status.OK then
store:dispatch(PinnedGame(conversationId))
else
store:dispatch(GameFailedToPin(conversationId))
local messageKey = "Feature.Chat.Message.PinFailed"
local toastModel = ToastModel.new(Constants.ToastIDs.PIN_GAME_FAILED, messageKey)
store:dispatch(ShowToast(toastModel))
warn("Game could not be pinned.")
end
end)
end
end
function PlayTogetherActions.UnpinGame(conversationId)
return function(store)
if store:getState().ChatAppReducer.PlayTogetherAsync.unPinningGames[conversationId] then
return
end
store:dispatch(UnpinningGame(conversationId))
spawn(function()
local status, _ = WebApi.UnpinGame(conversationId)
if status == WebApi.Status.OK then
store:dispatch(UnpinnedGame(conversationId))
else
store:dispatch(GameFailedToUnpin(conversationId))
local messageKey = "Feature.Chat.Message.UnpinFailed"
local toastModel = ToastModel.new(Constants.ToastIDs.UNPIN_GAME_FAILED, messageKey)
store:dispatch(ShowToast(toastModel))
warn("Game could not be unpinned.")
end
end)
end
end
function PlayTogetherActions.SetPinnedGameForConversation(universeId, rootPlaceId, conversationId)
return function(store)
spawn(function()
store:dispatch(SetPinnedGameForConversation(
universeId,
rootPlaceId,
conversationId
))
end)
end
end
function PlayTogetherActions.GetMostRecentlyPlayedGames()
return function(store)
spawn(function()
if store:getState().ChatAppReducer.PlayTogetherAsync.fetchingMostRecentlyPlayedGames then
return
end
store:dispatch(FetchingMostRecentlyPlayedGames())
local gameSorts = WebApi.GetGamesSorts(HOME_GAMES_SORTS)
if not gameSorts then
warn("Failed to get game sorts")
store:dispatch(FailedToFetchedMostRecentlyPlayedGames())
return
end
for _, sort in pairs(gameSorts) do
if sort.name == MOST_RECENTLY_PLAYED_GAMES then
local games = WebApi.GetMostRecentlyPlayedGames(sort.token)
if games then
store:dispatch(FetchedMostRecentlyPlayedGames())
store:dispatch(SetMostRecentlyPlayedGamesForUser(games))
else
warn("No most recently played games found")
store:dispatch(FailedToFetchedMostRecentlyPlayedGames())
end
return
end
end
warn("No most recently played game sort")
store:dispatch(FailedToFetchedMostRecentlyPlayedGames())
end)
end
end
function PlayTogetherActions.GetMostRecentlyPlayedPlayableGame()
return function(store)
local mostRecentlyPlayedGames = store:getState().ChatAppReducer.MostRecentlyPlayedGames.games
if not mostRecentlyPlayedGames or #mostRecentlyPlayedGames == 0 then
return
end
spawn(function()
local mostRecentlyPlayedPlayableGamePlaceId = nil
for _, game in pairs(mostRecentlyPlayedGames) do
local placeId = tostring(game.placeId)
local placeInfo = store:getState().ChatAppReducer.PlaceInfos[placeId]
if (placeInfo == nil) and (not store:getState().ChatAppReducer.PlaceInfosAsync[placeId]) then
local placeIds = { placeId }
store:dispatch(RequestMultiplePlaceInfos(placeIds))
local status, result = WebApi.GetMultiplePlaceInfos(placeIds)
if status ~= WebApi.Status.OK then
warn("WebApi failure in GetMostRecentlyPlayedPlayableGame")
store:dispatch(FailedToFetchMultiplePlaceInfos(placeIds))
else
local placeInfos = {}
for _, placeInfoData in pairs(result) do
table.insert(placeInfos, PlaceInfoModel.fromWeb(placeInfoData))
end
store:dispatch(ReceivedMultiplePlaceInfos(placeInfos))
end
break
end
if placeInfo and placeInfo.isPlayable then
mostRecentlyPlayedPlayableGamePlaceId = placeId
break
end
end
if mostRecentlyPlayedPlayableGamePlaceId then
store:dispatch(SetMostRecentlyPlayedPlayableGameForUser(mostRecentlyPlayedPlayableGamePlaceId))
end
end)
end
end
return PlayTogetherActions
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,7 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,9 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
--- Please put your changes in AppTempCommon. ---
--- NOTE: You will have to rebuild for changes to kick in ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedConversation)
@@ -0,0 +1,7 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,15 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, messages, shouldMarkConversationUnread, messageId)
return {
conversationId = conversationId,
messages = messages,
shouldMarkConversationUnread = shouldMarkConversationUnread,
exclusiveStartMessageId = messageId,
}
end
)
@@ -0,0 +1,9 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
--- Please put your changes in AppTempCommon. ---
--- NOTE: You will have to rebuild for changes to kick in ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedMultiplePlaceInfos)
@@ -0,0 +1,9 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function(value)
return {
value = value,
}
end)
@@ -0,0 +1,7 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(imageToken, thumbnail)
return {
imageToken = imageToken,
thumbnail = thumbnail,
}
end)
@@ -0,0 +1,9 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
--- Please put your changes in AppTempCommon. ---
--- NOTE: You will have to rebuild for changes to kick in ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
@@ -0,0 +1,24 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local LuaChat = Modules.LuaChat
local SetUserTyping = require(LuaChat.Actions.SetUserTyping)
local typingCount = 0
return function(conversationId, userId)
return function(store)
spawn(function()
typingCount = typingCount + 1
local thisTypingCount = typingCount
store:dispatch(SetUserTyping(conversationId, userId, true))
wait(5)
if typingCount == thisTypingCount then
store:dispatch(SetUserTyping(conversationId, userId, false))
end
end)
end
end
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(intent)
return {
intent = intent,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,14 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, title, isDefaultTitle, lastUpdated)
return {
conversationId = conversationId,
title = title,
isDefaultTitle = isDefaultTitle,
lastUpdated = lastUpdated,
}
end)
@@ -0,0 +1,7 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,7 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(placeIds)
return {
placeIds = placeIds,
}
end)
@@ -0,0 +1,7 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(imageToken)
return {
imageToken = imageToken,
}
end)
@@ -0,0 +1,9 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Action = require(Modules.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, message)
return {
conversationId = conversationId,
message = message
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, messageId)
return {
conversationId = conversationId,
messageId = messageId
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(isLoaded)
return {
value = isLoaded,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(isEnabled)
return {
value = isEnabled,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(connectionState)
return {
connectionState = connectionState;
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(convoId, conversationLoadingState)
return {
conversationId = convoId,
value = conversationLoadingState
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(isFetchingConversations)
return {
value = isFetchingConversations,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(formFactor)
return {
formFactor = formFactor,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(count)
return {
count = count,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(games)
return {
games = games,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(placeId)
return {
placeId = placeId,
}
end)
@@ -0,0 +1,13 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(universeId, rootPlaceId, conversationId)
return {
universeId = universeId,
rootPlaceId = rootPlaceId,
conversationId = conversationId,
}
end)
@@ -0,0 +1,13 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(intent, parameters, popToIntent)
return {
intent = intent,
parameters = parameters or {},
popToIntent = popToIntent,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(unreadConversationCount)
return {
count = unreadConversationCount,
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, isFriend)
return {
isFriend = isFriend,
userId = userId,
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, isLeaving)
return {
id = conversationId,
isLeaving = isLeaving,
}
end)
@@ -0,0 +1,14 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId, userId, isUserTyping)
return {
conversationId = conversationId,
userId = userId,
value = isUserTyping,
}
end)
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(gameSortName, placeIds)
return {
name = gameSortName,
placeIds = placeIds or {},
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(games)
return {
games = games,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(gameSortName)
return {
gameSortName = gameSortName,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(gameSortName)
return {
gameSortName = gameSortName,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(gameSortName)
return {
gameSortName = gameSortName,
}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,126 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local LuaApp = Modules.LuaApp
local LuaChat = Modules.LuaChat
local ShareGameToChatActions = LuaChat.Actions.ShareGameToChatFromChat
local AddGamesBySort = require(ShareGameToChatActions.AddGamesBySortShareGameToChatFromChat)
local AddGamesInformation = require(ShareGameToChatActions.AddGamesInformationShareGameToChatFromChat)
local Constants = require(LuaChat.Constants)
local ClearAllGamesInSorts = require(ShareGameToChatActions.ClearAllGamesInSortsShareGameToChatFromChat)
local FailedToFetchGamesBySort = require(ShareGameToChatActions.FailedToFetchGamesBySortShareGameToChatFromChat)
local FailedToShareGameToChat = require(ShareGameToChatActions.FailedToShareGameToChatFromChat)
local FetchedGamesBySort = require(ShareGameToChatActions.FetchedGamesBySortShareGameToChatFromChat)
local FetchingGamesBySort = require(ShareGameToChatActions.FetchingGamesBySortShareGameToChatFromChat)
local PopRoute = require(LuaChat.Actions.PopRoute)
local ResetShareGame = require(ShareGameToChatActions.ResetShareGameToChatFromChat)
local ResetShareGameToChatAsync = require(ShareGameToChatActions.ResetShareGameToChatFromChatAsync)
local SharedGameToChat = require(ShareGameToChatActions.SharedGameToChatFromChat)
local SharingGameToChat = require(ShareGameToChatActions.SharingGameToChatFromChat)
local ShowToast = require(LuaChat.Actions.ShowToast)
local ToastModel = require(LuaChat.Models.ToastModel)
local SetGameThumbnails = require(LuaApp.Actions.SetGameThumbnails)
local UpdateGameSortsTokens = require(ShareGameToChatActions.UpdateGameSortsTokensShareGameToChatFromChat)
local WebApi = require(LuaChat.WebApi)
local SHARED_GAMES_SORT = "GamesAllSorts"
local ShareGameToChatFromChatThunks = {}
function ShareGameToChatFromChatThunks.FetchGames(gameSortName, fetchedThumbnailSize)
return function(store)
spawn(function()
if store:getState().ChatAppReducer.ShareGameToChatAsync.fetchingGamesBySort[gameSortName] then
return
end
store:dispatch(FetchingGamesBySort(gameSortName))
if not store:getState().ChatAppReducer.SharedGameSorts[gameSortName]
or not store:getState().ChatAppReducer.SharedGameSorts[gameSortName].tokenExpiry
or store:getState().ChatAppReducer.SharedGameSorts[gameSortName].tokenExpiry < tick() then
local gameSorts = WebApi.GetGamesSorts(SHARED_GAMES_SORT)
if not gameSorts then
store:dispatch(FailedToFetchGamesBySort(gameSortName))
warn("Failed to get game sorts")
return
end
store:dispatch(UpdateGameSortsTokens(gameSorts))
end
local gamesList = nil
if store:getState().ChatAppReducer.SharedGameSorts[gameSortName] and
store:getState().ChatAppReducer.SharedGameSorts[gameSortName].token then
gamesList = WebApi.GetGamesInSortByToken(store:getState().ChatAppReducer.SharedGameSorts[gameSortName].token)
end
if gamesList then
local placeIds = {}
local newPlaceIds = {}
local games = {}
for _, game in pairs(gamesList) do
table.insert(placeIds, game.placeId)
if not store:getState().ChatAppReducer.SharedGamesInfo[game.placeId] then
games[game.placeId] = game
end
if not store:getState().ChatAppReducer.SharedGamesInfo[game.placeId] or
not store:getState().ChatAppReducer.SharedGamesInfo[game.placeId].url then
table.insert(newPlaceIds, game.placeId)
end
end
if #newPlaceIds > 0 then
local _, placesInfo = WebApi.GetMultiplePlaceInfos(newPlaceIds)
local imageTokens = {}
for _, placeInfo in pairs(placesInfo) do
games[placeInfo.placeId].url = placeInfo.url
games[placeInfo.placeId].isPlayable = placeInfo.isPlayable
table.insert(imageTokens, placeInfo.imageToken)
end
store:dispatch(AddGamesInformation(games))
local thumbnails = WebApi.GetPlacesThumbnails(imageTokens, fetchedThumbnailSize, fetchedThumbnailSize)
store:dispatch(SetGameThumbnails(thumbnails))
end
store:dispatch(FetchedGamesBySort(gameSortName))
store:dispatch(AddGamesBySort(gameSortName, placeIds))
else
store:dispatch(AddGamesBySort(gameSortName, nil))
store:dispatch(FailedToFetchGamesBySort(gameSortName))
warn("No " .. gameSortName .. " games found")
end
end)
end
end
function ShareGameToChatFromChatThunks.HasGameFetchRequestCompleted(sortName, shareGameToChatAsync)
return shareGameToChatAsync.fetchedGamesBySort[sortName] or
shareGameToChatAsync.failedToFetchGamesBySort[sortName]
end
function ShareGameToChatFromChatThunks.Sharing(store)
store:dispatch(SharingGameToChat())
end
function ShareGameToChatFromChatThunks.Shared(store)
store:dispatch(PopRoute())
store:dispatch(SharedGameToChat())
store:dispatch(ResetShareGame())
store:dispatch(ClearAllGamesInSorts())
store:dispatch(ResetShareGameToChatAsync())
end
function ShareGameToChatFromChatThunks.FailedToShare(store)
local messageKey = "Feature.Chat.ShareGameToChat.FailedToShareTheGame"
local toastModel = ToastModel.new(Constants.ToastIDs.GAME_NOT_SHAREABLE, messageKey)
store:dispatch(ShowToast(toastModel))
store:dispatch(FailedToShareGameToChat())
end
return ShareGameToChatFromChatThunks
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function()
return {}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(gameSorts)
return {
gameSorts = gameSorts,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(alert)
return {
alert = alert,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(toast)
return {
toast = toast,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(toast)
return {
toast = toast,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(isPaused)
return {
value = isPaused,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local Action = require(Common.Action)
return Action(script.Name, function(conversationId)
return {
conversationId = conversationId,
}
end)
@@ -0,0 +1,21 @@
local PlayerService = game:GetService("Players")
return function(eventStreamImpl, eventContext, conversationId, placeId)
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(conversationId) == "string", "Expected conversationId to be a string")
assert(type(placeId) == "string", "Expected placeId to be a string")
local eventName = "loadGameLinkCardInChat"
local player = PlayerService.LocalPlayer
local userId = "UNKNOWN"
if player then
userId = tostring(player.UserId)
end
eventStreamImpl:setRBXEventStream(eventContext, eventName, {
uid = userId,
cid = conversationId,
pid = placeId,
})
end
@@ -0,0 +1,15 @@
local Players = game:GetService("Players")
return function(eventStreamImpl, eventContext, conversationId)
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(conversationId) == "string", "Expected conversationId to be a string")
local eventName = "sendIcebreaker"
local player = Players.LocalPlayer
local userId = tostring(player.UserId)
eventStreamImpl:setRBXEventStream(eventContext, eventName, {
uid = userId,
cid = conversationId,
})
end
@@ -0,0 +1,19 @@
local PlayerService = game:GetService("Players")
return function(eventStreamImpl, eventContext, conversationId, placeId)
assert(type(eventContext) == "string", "Expected eventContext to be a string")
local eventName = "shareGameToChatFromChat"
local player = PlayerService.LocalPlayer
local userId = "UNKNOWN"
if player then
userId = tostring(player.UserId)
end
eventStreamImpl:setRBXEventStream(eventContext, eventName, {
uid = userId,
cid = conversationId,
pid = placeId,
})
end
@@ -0,0 +1,72 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local LuaChat = Modules.LuaChat
local ActiveConversationId = require(LuaChat.Reducers.ActiveConversationId)
local ShareGameToChatAsync = require(LuaChat.Reducers.ShareGameToChatAsync)
local SharedGameSorts = require(LuaChat.Reducers.SharedGameSorts)
local SharedGamesInfo = require(LuaChat.Reducers.SharedGamesInfo)
local AppLoaded = require(LuaChat.Reducers.AppLoaded)
local AppState = require(LuaChat.Reducers.AppState)
local Conversations = require(LuaChat.Reducers.Conversations)
local ConversationsAsync = require(LuaChat.Reducers.ConversationsAsync)
local Location = require(LuaChat.Reducers.Location)
local MostRecentlyPlayedGames = require(LuaChat.Reducers.MostRecentlyPlayedGames)
local PlaceInfos = require(Modules.LuaChat.Reducers.PlaceInfos)
local PlaceInfosAsync = require(LuaChat.Reducers.PlaceInfosAsync)
local PlaceThumbnails = require(Modules.LuaChat.Reducers.PlaceThumbnails)
local PlaceThumbnailsAsync = require(LuaChat.Reducers.PlaceThumbnailsAsync)
local PlayTogetherAsync = require(LuaChat.Reducers.PlayTogetherAsync)
local Toast = require(LuaChat.Reducers.Toast)
local ToggleChatPaused = require(LuaChat.Reducers.ToggleChatPaused)
local UnreadConversationCount = require(LuaChat.Reducers.UnreadConversationCount)
local FFlagLuaChatCheckWasUsedRecently = settings():GetFFlag("LuaChatCheckWasUsedRecently")
local ChatEnabled
local ChatSettings
if FFlagLuaChatCheckWasUsedRecently then
ChatSettings = require(LuaChat.Reducers.ChatSettings)
else
ChatEnabled = require(LuaChat.Reducers.ChatEnabled)
end
return function(state, action)
state = state or {}
local newState = {
-- Unique to Chat
ActiveConversationId = ActiveConversationId(state.ActiveConversationId, action),
AppState = AppState(state.AppState, action),
Location = Location(state.Location, action),
AppLoaded = AppLoaded(state.AppLoaded, action),
ToggleChatPaused = ToggleChatPaused(state.ToggleChatPaused, action),
-- TODO: update and move the following to the LuaApp state when WebApi is refactored:
-- See: https://jira.roblox.com/browse/SOC-1737
PlaceInfos = PlaceInfos(state.PlaceInfos, action),
PlaceInfosAsync = PlaceInfosAsync(state.PlaceInfosAsync, action),
PlaceThumbnails = PlaceThumbnails(state.PlaceThumbnails, action),
PlaceThumbnailsAsync = PlaceThumbnailsAsync(state.PlaceThumbnailsAsync, action),
-- May be able to be shared with other pages
Toast = Toast(state.Toast, action),
Conversations = Conversations(state.Conversations, action),
ConversationsAsync = ConversationsAsync(state.ConversationsAsync, action),
UnreadConversationCount = UnreadConversationCount(state.UnreadConversationCount, action),
MostRecentlyPlayedGames = MostRecentlyPlayedGames(state.MostRecentlyPlayedGames, action),
PlayTogetherAsync = PlayTogetherAsync(state.PlayTogetherAsync, action),
-- Share game to chat from chat
ShareGameToChatAsync = ShareGameToChatAsync(state.ShareGameToChatAsync, action),
SharedGameSorts = SharedGameSorts(state.SharedGameSorts, action),
SharedGamesInfo = SharedGamesInfo(state.SharedGamesInfo, action),
}
if FFlagLuaChatCheckWasUsedRecently then
newState.ChatSettings = ChatSettings(state.ChatSettings, action)
else
newState.ChatEnabled = ChatEnabled(state.ChatEnabled, action)
end
return newState
end
@@ -0,0 +1,45 @@
local CoreGui = game:GetService("CoreGui")
local LocalizationService = game:GetService("LocalizationService")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local request = require(Modules.LuaApp.Http.request)
local ScreenManager = require(Modules.LuaChat.ScreenManager)
local AppReducer = require(Modules.LuaApp.AppReducer)
local Localization = require(Modules.LuaApp.Localization)
local Analytics = require(Common.Analytics)
local Rodux = require(Common.Rodux)
local AppState = {}
local function appStateConstructor(chatGui, store, analyticsImpl)
local state = {}
state.store = store
state.localization = Localization.new(LocalizationService.RobloxLocaleId)
state.analytics = analyticsImpl
state.request = request
state.screenManager = ScreenManager.new(chatGui, state)
return state
end
function AppState.new(chatGui, store)
local analyticsImpl = Analytics.new()
return appStateConstructor(chatGui, store, analyticsImpl)
end
function AppState.mock(chatGui, store, analyticsImpl)
analyticsImpl = analyticsImpl or Analytics.mock()
chatGui = chatGui or CoreGui
store = store or Rodux.Store.new(AppReducer)
return appStateConstructor(chatGui, store, analyticsImpl)
end
function AppState:Destruct()
self.store:destruct()
end
return AppState
@@ -0,0 +1,117 @@
local Modules = script.Parent.Parent
local Create = require(Modules.Create)
local Constants = require(Modules.Constants)
local Text = require(Modules.Text)
local ListEntry = require(Modules.Components.ListEntry)
local ICON_CELL_WIDTH = 60
local HEIGHT = 48
local LABEL_SPACING = 12
local ActionEntry = {}
function ActionEntry.new(appState, icon, localizationKey, size)
local self = {}
size = size or 24
setmetatable(self, {__index = ActionEntry})
local text = appState.localization:Format(localizationKey)
local listEntry = ListEntry.new(appState, HEIGHT)
self.rbx = listEntry.rbx
local iconFrame = Create.new"Frame" {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
Create.new"ImageLabel" {
BackgroundTransparency = 1,
Size = UDim2.new(0, size, 0, size),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Image = icon,
BorderSizePixel = 0,
},
}
iconFrame.Parent = self.rbx
local label = Create.new"TextLabel" {
Name = "Label",
BackgroundTransparency = 1,
Size = UDim2.new(1, -ICON_CELL_WIDTH, 1, 0),
Position = UDim2.new(0, ICON_CELL_WIDTH, 0, 0),
TextSize = Constants.Font.FONT_SIZE_18,
TextColor3 = Constants.Color.GRAY1,
Font = Enum.Font.SourceSans,
Text = text,
TextXAlignment = Enum.TextXAlignment.Left,
}
label.Parent = self.rbx
local labelEffectiveWidth = Text.GetTextWidth(text, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_18) + LABEL_SPACING
local value = Create.new"TextLabel" {
Name = "Value",
BackgroundTransparency = 1,
Size = UDim2.new(1, -ICON_CELL_WIDTH-12-labelEffectiveWidth, 1, 0),
Position = UDim2.new(0, ICON_CELL_WIDTH+labelEffectiveWidth, 0, 0),
TextSize = Constants.Font.FONT_SIZE_18,
TextColor3 = Constants.Color.GRAY2,
Font = Enum.Font.SourceSans,
Text = "",
TextXAlignment = Enum.TextXAlignment.Right,
ClipsDescendants = true,
}
value.Parent = self.rbx
local divider = Create.new"Frame" {
Name = "Divider",
BackgroundColor3 = Constants.Color.GRAY4,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, -1),
}
divider.Parent = self.rbx
self.divider = divider
self.tapped = Instance.new("BindableEvent")
self.tapped.Parent = self.rbx
listEntry.tapped:connect(function()
self.tapped:fire()
end)
value:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:AdjustLayout()
end)
return self
end
function ActionEntry:SetDividerOffset(dividerOffset)
self.divider.Size = UDim2.new(1, -dividerOffset, 0, 1)
self.divider.Position = UDim2.new(0, dividerOffset, 1, -1)
end
function ActionEntry:AdjustLayout()
local text = self.rbx.Value.Text
local valueWidth = Text.GetTextWidth(text, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_18)
if valueWidth > self.rbx.Value.AbsoluteSize.X then
self.rbx.Value.TextXAlignment = Enum.TextXAlignment.Right
else
self.rbx.Value.TextXAlignment = Enum.TextXAlignment.Left
end
end
function ActionEntry:Update(state)
self.rbx.Value.Text = state
self:AdjustLayout()
end
return ActionEntry
@@ -0,0 +1,589 @@
local CoreGui = game:GetService("CoreGui")
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local TweenService = game:GetService("TweenService")
local HttpService = game:GetService("HttpService")
local UserInputService = game:GetService("UserInputService")
local Modules = CoreGui.RobloxGui.Modules
local LuaApp = Modules.LuaApp
local LuaChat = Modules.LuaChat
local Constants = require(LuaChat.Constants)
local Create = require(LuaChat.Create)
local FlagSettings = require(LuaChat.FlagSettings)
local GameParams = require(LuaChat.Models.GameParams)
local getInputEvent = require(LuaChat.Utils.getInputEvent)
local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos)
local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail)
local LoadingIndicator = require(LuaChat.Components.LoadingIndicator)
local NotificationType = require(LuaApp.Enum.NotificationType)
local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions)
local Text = require(LuaChat.Text)
local FormFactor = require(LuaApp.Enum.FormFactor)
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
local UseCppTextTruncation = FlagSettings.UseCppTextTruncation()
local UrlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI")
local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection")
local LuaChatFixAssetCardResizeTruncation = true or settings():GetFFlag("LuaChatFixAssetCardResizeTruncation")
local BUBBLE_PADDING = 10
local DEFAULT_THUMBNAIL = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png"
local EXTERIOR_PADDING = 3
local GAME_CARD_BUTTON_CLICKED_EVENT = "clickBtnFromLinkCardInChat"
local GAME_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/gr-mask-game-icon.png"
local GAME_PLAY_INTENT = "gamePlayIntent"
local GAME_PLAY_EVENT_CONTEXT = "PlayGameFromLinkCard"
local ICON_SIZE = 64
local INTERIOR_PADDING = 12
local LINK_CARD_CLICKED_EVENT = "clickLinkCardInChat"
local PIN_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png"
local PIN_ICON_HORIZONTAL_PADDING = 10
local PIN_ICON_SIZE = 20
local PIN_ICON_VERTICAL_PADDING = 8
local PIN_PRESSED_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pinpressed.png"
local PLACE_INFO_THUMBNAIL_SIZE = 50
local TOUCH_CONTEXT = "touch"
local ASSET_CARD_HORIZONTAL_MARGIN_PHONE = 108
local ASSET_CARD_HORIZONTAL_MARGIN_TABLET = 224
local function isOutgoingMessage(message)
local localUserId = tostring(Players.LocalPlayer.UserId)
return message.senderTargetId == localUserId
end
local FFlagLuaChatInfiniteRelayoutRecursionFix = settings():GetFFlag("LuaChatInfiniteRelayoutRecursionFix")
local AssetCard = {}
AssetCard.__index = AssetCard
function AssetCard.new(appState, message, assetId)
local self = {}
setmetatable(self, AssetCard)
local state = appState.store:getState()
local user = state.Users[message.senderTargetId]
local username = user and user.name or "unknown user"
self._analytics = appState.analytics
self.appState = appState
self.paddingObject = nil
self.message = message
self.bubbleType = "AssetCard"
self.connections = {}
if FFlagLuaChatToSplitRbxConnections then
self.rbx_connections = {}
end
self.cardBodyClick = nil
self.assetId = assetId
self.conversationId = message.conversationId
self.universeId = nil
self.pinButtonClick = nil
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
self.appState.store:getState().FormFactor)
self.tail = Create.new "ImageLabel" {
Name = "Tail",
Size = UDim2.new(0, 6, 0, 6),
BackgroundTransparency = 1,
}
self.actionLabel = Create.new "TextLabel" {
Name = "ActionLabel",
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(0.8, 0, 0.8, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
TextSize = Constants.Font.FONT_SIZE_20,
TextColor3 = Constants.Color.GRAY1,
Font = Enum.Font.SourceSans,
Text = self.appState.localization:Format("Feature.Chat.Action.ViewAssetDetails"),
}
self.actionButton = Create.new "ImageButton" {
Name = "Action",
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 1),
Position = UDim2.new(0.5, 0, 1, 0),
Size = UDim2.new(1, 0, 0, 32),
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(3,3,4,4),
Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png",
self.actionLabel,
}
local textLabelWidth
if self.luaChatPlayTogetherEnabled then
textLabelWidth = -(PIN_ICON_SIZE + (2 * INTERIOR_PADDING) + BUBBLE_PADDING)
else
textLabelWidth = -(INTERIOR_PADDING + BUBBLE_PADDING)
end
self.Title = Create.new "TextLabel" {
Name = "Title",
TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil,
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0, 0),
TextSize = Constants.Font.FONT_SIZE_20,
Size = UDim2.new(1, textLabelWidth, 0, 20),
Position = UDim2.new(0, 0, 0, 0),
TextColor3 = Constants.Color.GRAY1,
Font = Enum.Font.SourceSans,
TextXAlignment = Enum.TextXAlignment.Left
}
self.Icon = Create.new "ImageLabel" {
Name = "Icon",
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, self.Title.TextSize + INTERIOR_PADDING),
Size = UDim2.new(0, ICON_SIZE, 0, ICON_SIZE),
Create.new "ImageLabel" {
Name = "Mask",
BackgroundTransparency = 1,
BorderSizePixel = 0,
Image = GAME_MASK_IMAGE,
ImageColor3 = Constants.Color.WHITE,
ScaleType = Enum.ScaleType.Slice,
Size = UDim2.new(1, 0, 1, 0),
SliceCenter = Rect.new(3,3,4,4),
}
}
self.Details = Create.new "TextLabel" {
Name = "Details",
BackgroundTransparency = 1,
Size = UDim2.new(1, - INTERIOR_PADDING -ICON_SIZE, 0, ICON_SIZE),
Position = self.Icon.Position + UDim2.new(0 , INTERIOR_PADDING + ICON_SIZE, 0, 0),
TextColor3 = Constants.Color.GRAY2,
Font = Enum.Font.SourceSans,
TextSize = Constants.Font.FONT_SIZE_14,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextWrapped = true,
}
self.fadeScreen = Create.new "Frame" {
Name = "FadeScreen",
BackgroundTransparency = 0,
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
}
self.Content = Create.new "ImageButton" {
Name = "Content",
BackgroundTransparency = 1,
Size = UDim2.new(1, -BUBBLE_PADDING * 2, 1, -BUBBLE_PADDING * 2),
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Visible = false,
self.actionButton,
self.Title,
self.Icon,
self.Details,
self.fadeScreen,
}
if self.luaChatPlayTogetherEnabled then
self.PinIcon = Create.new "ImageLabel" {
Name = "PinIcon",
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(0, PIN_ICON_SIZE, 0, PIN_ICON_SIZE),
Image = PIN_ICON,
}
self.PinButton = Create.new "TextButton" {
Name = "PinButton",
Text = "",
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, BUBBLE_PADDING, 0, -BUBBLE_PADDING),
Size = UDim2.new(0, PIN_ICON_SIZE + 2 * PIN_ICON_HORIZONTAL_PADDING,
0, PIN_ICON_SIZE + 2 * PIN_ICON_VERTICAL_PADDING),
self.PinIcon
}
self.PinButton.Parent = self.Content
end
self.bubble = Create.new "ImageLabel" {
Name = "Bubble",
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(0, 267, 1, 0),
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(10, 10, 11, 11),
LayoutOrder = 2,
self.Content,
self.tail,
}
self.usernameLabel = Create.new "TextLabel" {
Name = "UsernameLabel",
Font = Enum.Font.SourceSans,
TextSize = Constants.Font.FONT_SIZE_12,
Visible = false,
BackgroundTransparency = 1,
Size = UDim2.new(1, -56, 0, 16),
Position = UDim2.new(0, 56, 0, 0),
TextColor3 = Constants.Color.GRAY2,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
Text = username,
}
self.bubbleContainer = Create.new "Frame" {
Name = "BubbleContainer",
BackgroundTransparency = 1,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 0),
self.bubble,
self.usernameLabel,
}
self.layout = Create.new "UIListLayout" {
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Center,
}
self.rbx = Create.new "Frame" {
Name = "AssetCard",
BackgroundTransparency = 1,
self.layout,
self.bubbleContainer,
}
-- 'isOutgoing' means "is sent by the local user". This function separates the tail position & color
if isOutgoingMessage(message) then
self.tail.AnchorPoint = Vector2.new(0, 0)
self.tail.Position = UDim2.new(1, 0, 0, 0)
self.tail.ImageColor3 = Color3.new(1, 1, 1)
self.bubble.ImageColor3 = Color3.new(1, 1, 1)
self.bubble.AnchorPoint = Vector2.new(1, 0)
self.bubble.Position = UDim2.new(1, -10, 0, 0)
self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Right
else
self.tail.AnchorPoint = Vector2.new(1, 0)
self.tail.Position = UDim2.new(0, 0, 0, 0)
self.tail.ImageColor3 = Color3.new(1, 1, 1)
self.bubble.ImageColor3 = Color3.new(1, 1, 1)
self.bubble.AnchorPoint = Vector2.new(0, 0)
self.bubble.Position = UDim2.new(0, 54, 0, 0)
self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left
end
self.appStateConnection = self.appState.store.changed:connect(function(state)
self:Update(state)
end)
table.insert(self.connections, self.appStateConnection)
if not FFlagLuaChatInfiniteRelayoutRecursionFix then
local connection = self.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:Resize()
end)
if FFlagLuaChatToSplitRbxConnections then
table.insert(self.rbx_connections, connection)
else
table.insert(self.connections, connection)
end
end
self:Update(state)
if FFlagLuaChatInfiniteRelayoutRecursionFix then
self:Resize()
end
return self
end
function AssetCard:_truncateText()
if LuaChatFixAssetCardResizeTruncation then
if not UseCppTextTruncation then
if self.placeInfo ~= nil then
self.Title.Text = self.placeInfo.name
end
Text.TruncateTextLabel(self.Title, "...")
end
else
if not UseCppTextTruncation then
Text.TruncateTextLabel(self.Title, "...")
end
end
end
function AssetCard:Resize()
local formFactor = self.appState.store:getState().FormFactor
local bubbleSizeOffsetY
if FFlagLuaChatInfiniteRelayoutRecursionFix then
bubbleSizeOffsetY = formFactor == FormFactor.PHONE and ASSET_CARD_HORIZONTAL_MARGIN_PHONE
or ASSET_CARD_HORIZONTAL_MARGIN_TABLET
else
bubbleSizeOffsetY = Constants:GetFormFactorSpecific(formFactor).ASSET_CARD_HORIZONTAL_MARGIN
end
local bubbleHeight = 92 + ICON_SIZE
self.bubble.Size = UDim2.new(1, -bubbleSizeOffsetY, 0, bubbleHeight)
local containerHeight = bubbleHeight
if self.usernameLabel.Visible then
containerHeight = containerHeight + self.usernameLabel.AbsoluteSize.Y
end
self.bubbleContainer.Size = UDim2.new(1, 0, 0, containerHeight)
local height = 0
for _, child in ipairs(self.rbx:GetChildren()) do
if child:IsA("GuiObject") then
height = height + child.AbsoluteSize.Y
end
end
height = height + EXTERIOR_PADDING * 2
if FFlagLuaChatInfiniteRelayoutRecursionFix then
-- FFlagLuaChatInfiniteRelayoutRecursionFix aims to seperate resize
-- and truncation logic so they may be called independantly
self.rbx.Size = UDim2.new(1, 0, 0, height)
self:_truncateText()
else
if LuaChatFixAssetCardResizeTruncation then
-- resize the frame before we truncate
self.rbx.Size = UDim2.new(1, 0, 0, height)
if not UseCppTextTruncation then
-- set the title text first in case the text is already truncated
-- from a Resize() call when the frame was a smaller width
if self.placeInfo ~= nil then
self.Title.Text = self.placeInfo.name
end
Text.TruncateTextLabel(self.Title, "...")
end
else
if not UseCppTextTruncation then
Text.TruncateTextLabel(self.Title, "...")
end
self.rbx.Size = UDim2.new(1, 0, 0, height)
end
end
end
function AssetCard:onPinPressed()
self.PinIcon.Image = PIN_PRESSED_ICON
self.userInputServiceCon = UserInputService.InputEnded:Connect(function()
self:onPinRelease()
end)
end
function AssetCard:onPinRelease()
if self.userInputServiceCon then
self.userInputServiceCon:Disconnect()
self.userInputServiceCon = nil
end
self.PinIcon.Image = PIN_ICON
end
function AssetCard:Update(newState)
local placeInfo = newState.ChatAppReducer.PlaceInfos[self.assetId]
if placeInfo == nil then
self:ShowLoadingIndicator(true)
self.appState.store:dispatch(GetMultiplePlaceInfos({self.assetId}))
else
self.placeInfo = placeInfo
self.Title.Text = placeInfo.name
if FFlagLuaChatInfiniteRelayoutRecursionFix then
self:_truncateText()
end
local description = placeInfo.description:gsub("%s", " ")
if description:gsub("^%s+$", "") == "" then
description = self.appState.localization:Format("Feature.Chat.Label.NoDescriptionYet")
end
self.Details.Text = description
self.universeId = placeInfo.universeId
if self.luaChatPlayTogetherEnabled then
if self.pinButtonClick then self.pinButtonClick:Disconnect() end
if self.pinButtonInputBegin then self.pinButtonInputBegin:Disconnect() end
self.pinButtonClick = self.PinButton.Activated:Connect(function()
self.appState.store:dispatch(PlayTogetherActions.PinGame(self.conversationId, self.universeId))
end)
self.pinButtonInputBegin = self.PinButton.InputBegan:Connect(function()
self:onPinPressed()
end)
end
if UrlSupportNewGamesAPI then
local thumbnail = newState.ChatAppReducer.PlaceThumbnails[placeInfo.imageToken]
if thumbnail == nil then
self.appState.store:dispatch(GetPlaceThumbnail(
placeInfo.imageToken, PLACE_INFO_THUMBNAIL_SIZE, PLACE_INFO_THUMBNAIL_SIZE
))
else
if thumbnail.image == '' then
self.thumbnail = DEFAULT_THUMBNAIL
else
self.thumbnail = thumbnail.image
end
self:FillThumbnail()
self:Show()
end
else
self.thumbnail = DEFAULT_THUMBNAIL
self:Show()
end
end
if self.cardBodyClick then self.cardBodyClick:Disconnect() end
if self.detailsButtonClick then self.detailsButtonClick:Disconnect() end
self:StyleViewDetailsAsPlay(self.placeInfo ~= nil and self.placeInfo.isPlayable)
self.cardBodyClick = getInputEvent(self.Content):Connect(function()
self:ReportAnEvent(LINK_CARD_CLICKED_EVENT, TOUCH_CONTEXT)
if self.placeInfo then
GuiService:BroadcastNotification(self.assetId,
NotificationType.VIEW_GAME_DETAILS)
end
end)
self.detailsButtonClick = getInputEvent(self.actionButton):Connect(function()
self:ReportAnEvent(GAME_CARD_BUTTON_CLICKED_EVENT, TOUCH_CONTEXT)
if self.placeInfo then
if self.placeInfo.isPlayable then
self:ReportAnEvent(GAME_PLAY_INTENT, GAME_PLAY_EVENT_CONTEXT)
local gameParams = GameParams.fromPlaceId(self.assetId)
local payload = HttpService:JSONEncode(gameParams)
GuiService:BroadcastNotification(payload,
NotificationType.LAUNCH_GAME)
else
GuiService:BroadcastNotification(self.assetId,
NotificationType.VIEW_GAME_DETAILS)
end
end
end)
if not FFlagLuaChatInfiniteRelayoutRecursionFix then
self:Resize()
end
end
function AssetCard:ReportAnEvent(eventName, eventContext)
local additionalArgs
if eventName == GAME_PLAY_INTENT then
additionalArgs = {
conversationId = self.conversationId,
rootPlaceId = self.assetId
}
else
additionalArgs = {
conversationId = self.conversationId,
placeId = self.assetId
}
end
self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
end
function AssetCard:StyleViewDetailsAsPlay(isShowingAsPlay)
if isShowingAsPlay then
self.actionButton.ImageColor3 = Constants.Color.GREEN_PRIMARY
self.actionLabel.Text = self.appState.localization:Format("Common.VisitGame.Label.Play")
self.actionLabel.TextColor3 = Constants.Color.WHITE
else
self.actionButton.ImageColor3 = Constants.Color.WHITE
self.actionLabel.Text = self.appState.localization:Format("Feature.Chat.Action.ViewAssetDetails")
self.actionLabel.TextColor3 = Constants.Color.GRAY1
end
end
function AssetCard:Show()
self.Content.Visible = true
spawn(function()
while (not self.Icon.IsLoaded) do wait() end
self:ShowLoadingIndicator(false)
local fadeInTween = TweenService:Create(
self.fadeScreen,
TweenInfo.new(0.4),
{BackgroundTransparency = 1}
)
fadeInTween:Play()
end)
if LuaChatAssetCardsSelfTerminateConnection then
if self.appStateConnection then
self.appStateConnection:disconnect()
end
end
end
function AssetCard:FillThumbnail()
self.Icon.Image = self.thumbnail or ""
end
function AssetCard:ShowLoadingIndicator(isVisible)
if isVisible then
if not self.loadingIndicator then
local loadingIndicator = LoadingIndicator.new(self.appState)
loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
loadingIndicator.rbx.Size = UDim2.new(0.5, 0, 0.25, 0)
loadingIndicator.rbx.Parent = self.bubble
loadingIndicator:SetVisible(true)
self.loadingIndicator = loadingIndicator
end
else
if self.loadingIndicator then
self.loadingIndicator:Destroy()
end
end
end
if not LuaChatAssetCardsSelfTerminateConnection then
function AssetCard:DisconnectUpdate()
if self.Content.Visible then
if self.appStateConnection then
self.appStateConnection:disconnect()
self.appStateConnection = nil
end
end
end
end
function AssetCard:Destruct()
for _, connection in pairs(self.connections) do
connection:disconnect()
end
self.connections = {}
if FFlagLuaChatToSplitRbxConnections then
for _, connection in pairs(self.rbx_connections) do
connection:Disconnect()
end
self.rbx_connections = {}
end
if self.pinButtonClick then self.pinButtonClick:Disconnect() end
if self.pinButtonInputBegin then self.pinButtonInputBegin:Disconnect() end
self.rbx:Destroy()
end
return AssetCard
@@ -0,0 +1,373 @@
local PlayerService = game:GetService("Players")
local CoreGui = game:GetService("CoreGui")
local TweenService = game:GetService("TweenService")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local LuaApp = Modules.LuaApp
local LuaChat = Modules.LuaChat
local Components = LuaChat.Components
local ChatGameDrawer = require(Components.ChatGameDrawer)
local Constants = require(LuaChat.Constants)
local DialogInfo = require(LuaChat.DialogInfo)
local PaddedImageButton = require(LuaChat.Components.PaddedImageButton)
local Roact = require(CoreGui.RobloxGui.Modules.Common.Roact)
local RoactAnalytics = require(LuaApp.Services.RoactAnalytics)
local RoactLocalization = require(LuaApp.Services.RoactLocalization)
local RoactRodux = require(CoreGui.RobloxGui.Modules.Common.RoactRodux)
local RoactServices = require(LuaApp.RoactServices)
local Text = require(Common.Text)
local FFlagLuaChatLoadGameLinkCardInChatAnalytics = settings():GetFFlag("LuaChatLoadGameLinkCardInChatAnalytics")
local FFlagGroupChatIconEnabled = settings():GetFFlag("LuaChatGroupChatIconEnabled")
local FFlagLuaChatFlexibleTitleWidth = settings():GetFFlag("LuaChatFlexibleTitleWidth")
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
local PLATFORM_SPECIFIC_CONSTANTS = {
[Enum.Platform.Android] = {
BACK_BUTTON_ASSET_ID = "rbxasset://textures/ui/LuaChat/icons/ic-back-android.png",
},
Default = {
BACK_BUTTON_ASSET_ID = "rbxasset://textures/ui/LuaChat/icons/ic-back.png",
},
}
local TITLE_LABEL_MAX_WIDTH = 200
local TITLE_LABEL_HEIGHT = 25
local function getPlatformSpecific(platform)
return PLATFORM_SPECIFIC_CONSTANTS[platform] or PLATFORM_SPECIFIC_CONSTANTS.Default
end
local function createRoactInstanceGameDrawer(self)
local newGameDrawer = Roact.createElement(ChatGameDrawer, {
conversationId = self.conversationId,
isGameDrawerOpen = self.isGameDrawerOpen,
Localization = self.appState.localization,
onSize = function(newSize, forceOpen)
self:SetGameDrawerSize(newSize, forceOpen)
end,
})
return Roact.createElement(RoactRodux.StoreProvider, {
store = self.appState.store,
}, {
serviceProvider = Roact.createElement(RoactServices.ServiceProvider, {
services = {
[RoactAnalytics] = self.appState.analytics,
[RoactLocalization] = self.appState.localization,
}
}, {
newGameDrawer
})
})
end
local BaseHeader = {}
--[[
This type of pseudo-inheritance for components is usually bad, but this is a special exception.
It is not recommended to do this.
]]
function BaseHeader:Template()
local class = {}
for key, value in pairs(self) do
class[key] = value
end
return class
end
function BaseHeader:SetPlatform(platform)
self.platform = platform
end
function BaseHeader:SetTitle(text)
local label = self.titleLabel
if label then
local labelFont = label.Font
local labelTextSize = label.TextSize
local maxTitleLabelWidth = self.innerTitles.AbsoluteSize.X
if FFlagGroupChatIconEnabled and self.groupChatIcon then
maxTitleLabelWidth = maxTitleLabelWidth
- (self.groupChatIcon.Visible and self.groupChatIcon.AbsoluteSize.X or 0)
end
if FFlagLuaChatFlexibleTitleWidth then
label.Text = Text.Truncate(text, labelFont, labelTextSize, maxTitleLabelWidth, "...")
else
label.Text = Text.Truncate(text, labelFont, labelTextSize, TITLE_LABEL_MAX_WIDTH, "...")
end
local titleTextLength = Text.GetTextWidth(label.Text, labelFont, labelTextSize)
self.titleLabel.Size = UDim2.new(0, titleTextLength, 0, TITLE_LABEL_HEIGHT)
end
self.title = text
end
function BaseHeader:SetDefaultSubtitle()
if (self.dialogType ~= DialogInfo.DialogType.Centered) and (self.dialogType ~= DialogInfo.DialogType.Left) then
self:SetSubtitle("")
return
end
local displayText = ""
local player = PlayerService.LocalPlayer
if player then
local userId = tostring(player.UserId)
local localUser = self.appState.store:getState().Users[userId]
if localUser and not localUser.isFetching then
if player:GetUnder13() then
displayText = string.format("%s: <13", localUser.name)
else
displayText = string.format("%s: 13+", localUser.name)
end
end
end
self:SetSubtitle(displayText)
end
--[[
Sets the Header's subtitle
Pass an empty string to hide the subtitle completely.
Otherwise pass nil to default to the userAge label.
]]
function BaseHeader:SetSubtitle(displayText)
assert(type(displayText) == "nil" or type(displayText) == "string",
"Invalid argument number #1 to SetSubtitle, expected string or nil.")
self.subtitle = displayText
if displayText == "" then
self.innerSubtitle.Visible = false
else
self.innerSubtitle.Visible = true
self.innerSubtitle.Text = displayText
end
end
function BaseHeader:SetBackButtonEnabled(enabled)
self.backButton.rbx.Visible = enabled
end
function BaseHeader:AddButton(button)
table.insert(self.buttons, button)
button.rbx.Parent = self.innerButtons
button.rbx.LayoutOrder = #self.buttons
end
function BaseHeader:AddContent(content)
content.rbx.Parent = self.innerContent
end
function BaseHeader:SetConnectionState(connectionState)
if not self.subtitle then
self:SetDefaultSubtitle()
end
if self.dialogType == DialogInfo.DialogType.Right then
return
end
if connectionState ~= self.connectionState and self.rbx.Parent ~= nil and self.rbx.Parent.Parent ~= nil then
if connectionState == Enum.ConnectionState.Disconnected then
self.isDisconnectedOpen = true
self:DoSizeTweening()
else
self.isDisconnectedOpen = false
self:DoSizeTweening()
end
self.connectionState = connectionState
end
end
function BaseHeader:GetNewBackButton(dialogType)
local backButton
if dialogType == DialogInfo.DialogType.Modal then
backButton = PaddedImageButton.new(self.appState, "Close", "rbxasset://textures/ui/LuaChat/icons/ic-close-gray2.png")
elseif dialogType == DialogInfo.DialogType.Popup then
backButton = PaddedImageButton.new(self.appState, "Close", "rbxasset://textures/ui/LuaChat/icons/ic-close-white.png")
else
backButton = PaddedImageButton.new(self.appState, "Back", getPlatformSpecific(self.platform).BACK_BUTTON_ASSET_ID)
end
backButton.rbx.Position = UDim2.new(0, 0, 0.5, 0)
backButton.rbx.AnchorPoint = Vector2.new(0, 0.5)
return backButton
end
function BaseHeader:Destroy()
for _, conn in pairs(self.connections) do
conn:disconnect()
end
self.connections = {}
if FFlagLuaChatToSplitRbxConnections then
for _, conn in pairs(self.rbx_connections) do
conn:Disconnect()
end
self.rbx_connections = {}
end
self.buttons = {}
-- Destroy an attached Roact Gamedrawer if we have one:
if self.roactInstanceGameDrawer ~= nil then
Roact.teardown(self.roactInstanceGameDrawer)
end
end
function BaseHeader:CreateGameDrawer(store, conversationId)
-- Create the game drawer. Note it's probably not shown yet:
self.conversationId = conversationId
self.heightOfGameDrawer = 0
self.hasFriendsInGame = false
self.isGameDrawerSized = false
self.isGameDrawerOpen = false
if FFlagLuaChatLoadGameLinkCardInChatAnalytics then
self.roactInstanceGameDrawer = Roact.mount(createRoactInstanceGameDrawer(self), self.rbx.GameDrawer, "ChatGameDrawer")
else
local newGameDrawer = Roact.createElement(ChatGameDrawer, {
AnchorPoint = Vector2.new(0, 0),
conversationId = conversationId,
Localization = self.appState.localization,
Position = UDim2.new(0, 0, 0, 0),
onSize = function(newSize, forceOpen)
self:SetGameDrawerSize(newSize, forceOpen)
end,
})
self.roactInstanceGameDrawer = Roact.mount(Roact.createElement(RoactRodux.StoreProvider, {
store = store,
}, {
serviceProvider = Roact.createElement(RoactServices.ServiceProvider, {
services = {
[RoactLocalization] = self.appState.localization,
}
}, {
newGameDrawer
})
}), self.rbx.GameDrawer, "ChatGameDrawer")
end
end
function BaseHeader:ToggleGameDrawer()
if self.isGameDrawerOpen == false then
self.isGameDrawerOpen = true
else
self.isGameDrawerOpen = false
end
if FFlagLuaChatLoadGameLinkCardInChatAnalytics then
self.roactInstanceGameDrawer = Roact.reconcile(self.roactInstanceGameDrawer, createRoactInstanceGameDrawer(self))
end
self:DoSizeTweening()
end
-- Note: Start is called whenever we return to a conversation:
function BaseHeader:Start()
self.lastHeight = -1
self.lastHeightDisconnected = -1
self.lastHeightDrawer = -1
if self.luaChatPlayTogetherEnabled then
if self.hasFriendsInGame then
self:OpenDrawer()
else
self:CloseDrawer()
end
end
end
function BaseHeader:OpenDrawer()
if self.isGameDrawerSized and (not self.isGameDrawerOpen) then
self.isGameDrawerOpen = true
self:DoSizeTweening()
end
end
function BaseHeader:CloseDrawer()
if self.isGameDrawerSized and self.isGameDrawerOpen then
self.isGameDrawerOpen = false
self:DoSizeTweening()
end
end
function BaseHeader:InputFocus()
if self.luaChatPlayTogetherEnabled then
self:CloseDrawer()
end
end
-- Set the open game drawer size - and tween it open:
function BaseHeader:SetGameDrawerSize(drawerSize, hasFriendsInGame)
if drawerSize > 0 and hasFriendsInGame and not self.isGameDrawerSized then
self.isGameDrawerOpen = true
end
self.heightOfGameDrawer = drawerSize
self.hasFriendsInGame = hasFriendsInGame
self.isGameDrawerSized = true
self:DoSizeTweening()
end
-- Do tweening for any drawers that might be open:
function BaseHeader:DoSizeTweening()
-- Base height of our header is the main contents:
local desiredHeight = self.heightOfHeader
-- Disconnected is the network error message:
if self.isDisconnectedOpen then
desiredHeight = desiredHeight + self.heightOfDisconnected
if not self.rbx.Disconnected.Visible or
(self.lastHeightDisconnected ~= self.heightOfDisconnected) then
local size = UDim2.new(1, 0, 0, self.heightOfDisconnected)
local resizeTween = TweenService:Create(
self.rbx.Disconnected,
TweenInfo.new(Constants.Tween.DEFAULT_TWEEN_TIME, Constants.Tween.DEFAULT_TWEEN_STYLE, Enum.EasingDirection.In),
{ Size = size }
)
resizeTween:Play()
self.rbx.Disconnected.Visible = true
self.lastHeightDisconnected = self.heightOfDisconnected
end
else
if self.rbx.Disconnected.Visible then
self.rbx.Disconnected.Size = UDim2.new(1, 0, 0, 0)
self.rbx.Disconnected.Visible = false
self.lastHeightDisconnected = 0
end
end
if self.luaChatPlayTogetherEnabled then
-- Game drawer is the list of games being played by participants of this conversation:
if self.isGameDrawerOpen and self.heightOfGameDrawer then
desiredHeight = desiredHeight + self.heightOfGameDrawer
if not self.rbx.GameDrawer.Visible or
(self.lastHeightDrawer ~= self.heightOfGameDrawer) then
self.rbx.GameDrawer.Visible = true
local size = UDim2.new(1, 0, 0, self.heightOfGameDrawer)
local resizeTween = TweenService:Create(
self.rbx.GameDrawer,
TweenInfo.new(Constants.Tween.DEFAULT_TWEEN_TIME, Constants.Tween.DEFAULT_TWEEN_STYLE, Enum.EasingDirection.In),
{ Size = size }
)
resizeTween:Play()
self.lastHeightDrawer = self.heightOfGameDrawer
end
else
if self.rbx.GameDrawer.Visible then
self.rbx.GameDrawer.Size = UDim2.new(1, 0, 0, 0)
self.rbx.GameDrawer.Visible = false
self.lastHeightDrawer = 0
end
end
end
-- Set the size of the main drawer:
if (self.lastHeight ~= desiredHeight) then
local size = UDim2.new(1, 0, 0, desiredHeight)
local resizeTween = TweenService:Create(
self.rbx,
TweenInfo.new(Constants.Tween.DEFAULT_TWEEN_TIME, Constants.Tween.DEFAULT_TWEEN_STYLE, Enum.EasingDirection.In),
{ Size = size }
)
resizeTween:Play()
self.lastHeight = desiredHeight
end
end
return BaseHeader
@@ -0,0 +1,187 @@
local CoreGui = game:GetService("CoreGui")
local NotificationService = game:GetService("NotificationService")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local LuaApp = Modules.LuaApp
local LuaChat = Modules.LuaChat
local AppNotificationService = require(LuaApp.Services.AppNotificationService)
local Constants = require(LuaChat.Constants)
local Create = require(LuaChat.Create)
local DialogInfo = require(LuaChat.DialogInfo)
local Intent = DialogInfo.Intent
local Signal = require(Common.Signal)
local Roact = require(Common.Roact)
local RoactAnalytics = require(LuaApp.Services.RoactAnalytics)
local RoactLocalization = require(LuaApp.Services.RoactLocalization)
local RoactNetworking = require(LuaApp.Services.RoactNetworking)
local RoactRodux = require(Common.RoactRodux)
local RoactServices = require(LuaApp.RoactServices)
local Components = LuaChat.Components
local HeaderLoader = require(Components.HeaderLoader)
local ResponseIndicator = require(Components.ResponseIndicator)
--[[
TODO: we would have a ticket "removing the fast flag LuaChatShareGameToChatFromChatV2".
When removing the flag LuaChatShareGameToChatFromChatV2, we need to delete the actions, reduces and store that
V1 share game to chat from chat used.
]]
-- V1 sharing game to chat from chat
local SharedGameList = require(Components.SharedGameList)
-- V2 sharing game to chat from chat
local SharedGamesList = require(Components.ShareGameToChatFromChat.SharedGamesList)
local TabBarView = require(LuaChat.TabBarView)
local TabPageParameters = require(LuaChat.Models.TabPageParameters)
-- Actions for V1 sharing game to chat from chat
local ClearAllGames = require(LuaChat.Actions.ShareGameToChatFromChat.ClearAllGamesInSortsShareGameToChatFromChat)
local ResetShareGameToChatAsync = require(LuaChat.Actions.ShareGameToChatFromChat.ResetShareGameToChatFromChatAsync)
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
local FFlagLuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
local BrowseGames = {}
BrowseGames.__index = BrowseGames
function BrowseGames.new(appState)
local self = {
appState = appState,
connections = {},
rbx_connections = {},
}
setmetatable(self, BrowseGames)
self._analytics = self.appState.analytics
self._localization = self.appState.localization
self._request = self.appState.request
self.responseIndicator = ResponseIndicator.new(appState)
self.responseIndicator:SetVisible(false)
self.header = HeaderLoader.GetHeader(appState, Intent.BrowseGames)
self.header:SetDefaultSubtitle()
self.header:SetTitle(self.appState.localization:Format("Feature.Chat.ShareGameToChat.BrowseGames"))
self.header:SetBackButtonEnabled(true)
self.header:SetConnectionState(Enum.ConnectionState.Disconnected)
local sharedGamesConfig = Constants.SharedGamesConfig
self.gamesPages = {}
local sharedGamesList = FFlagLuaChatShareGameToChatFromChatV2 and SharedGamesList
or SharedGameList
for _, sortName in ipairs(sharedGamesConfig.SortNames) do
table.insert(
self.gamesPages,
TabPageParameters(
self._localization:Format(sharedGamesConfig.SortsAttribute[sortName].TILE_LOCALIZATION_KEY),
sharedGamesList,
{
gameSort = sortName,
}
)
)
end
self.rbx = Create.new"Frame" {
Name = "BrowseGames",
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
Create.new("UIListLayout") {
Name = "ListLayout",
SortOrder = Enum.SortOrder.LayoutOrder,
},
self.header.rbx,
Create.new"Frame" {
Name = "Content",
BackgroundColor3 = Constants.Color.GRAY5,
BorderSizePixel = 0,
ClipsDescendants = true,
LayoutOrder = 1,
Size = UDim2.new(1, 0, 1, -self.header.heightOfHeader),
self.responseIndicator.rbx,
},
}
self.mainContent = Roact.mount(Roact.createElement(RoactRodux.StoreProvider, {
store = appState.store,
}, {
Roact.createElement(RoactServices.ServiceProvider, {
services = {
[AppNotificationService] = NotificationService,
[RoactAnalytics] = self._analytics,
[RoactLocalization] = self._localization,
[RoactNetworking] = self._request,
}
}, {
TabBarView = Roact.createElement(TabBarView, {
tabs = self.gamesPages,
}),
}),
}), self.rbx.Content, "MainContent")
self.BackButtonPressed = Signal.new()
self.header.BackButtonPressed:connect(function()
if not FFlagLuaChatShareGameToChatFromChatV2 then
self:CleanGamesInSorts()
end
self.BackButtonPressed:fire()
end)
local headerSizeConnection = self.header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:Resize()
end)
if FFlagLuaChatToSplitRbxConnections then
table.insert(self.rbx_connections, headerSizeConnection)
else
table.insert(self.connections, headerSizeConnection)
end
return self
end
function BrowseGames:CleanGamesInSorts()
self.appState.store:dispatch(ClearAllGames())
self.appState.store:dispatch(ResetShareGameToChatAsync())
end
function BrowseGames:Resize()
local sizeContent = UDim2.new(1, 0, 1, -self.header.rbx.AbsoluteSize.Y)
self.rbx.Content.Size = sizeContent
end
function BrowseGames:Update(current, previous)
self.header:SetConnectionState(current.ConnectionState)
local isSharing = self.appState.store:getState().ChatAppReducer.ShareGameToChatAsync.sharingGame or false
self.responseIndicator:SetVisible(isSharing)
end
function BrowseGames:Destruct()
for _, connection in pairs(self.connections) do
connection:Disconnect()
end
self.connections = {}
if FFlagLuaChatToSplitRbxConnections then
for _, connection in pairs(self.rbx_connections) do
connection:Disconnect()
end
self.rbx_connections = {}
end
self.header:Destroy()
self.responseIndicator:Destruct()
Roact.unmount(self.mainContent)
self.rbx:Destroy()
end
return BrowseGames
@@ -0,0 +1,35 @@
return function()
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local LuaChat = Modules.LuaChat
local AppState = require(LuaChat.AppState)
local BrowseGames = require(LuaChat.Components.BrowseGames)
local FormFactor = require(Modules.LuaApp.Enum.FormFactor)
local SetFormFactor = require(Modules.LuaApp.Actions.SetFormFactor)
describe("new", function()
it("should create and destruct BrowseGames page on mobile phone with no errors", function()
local appState = AppState.mock()
appState.store:dispatch(SetFormFactor(FormFactor.PHONE))
local browseGames = BrowseGames.new(appState)
expect(browseGames).to.be.ok()
browseGames:Destruct()
expect(browseGames).to.be.ok()
end)
it("should create and destruct BrowseGames page on tablet with no errors", function()
local appState = AppState.mock()
appState.store:dispatch(SetFormFactor(FormFactor.TABLET))
local browseGames = BrowseGames.new(appState)
expect(browseGames).to.be.ok()
browseGames:Destruct()
expect(browseGames).to.be.ok()
end)
end)
end
@@ -0,0 +1,323 @@
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local LuaChat = script.Parent.Parent
local UserThumbnail = require(script.Parent.UserThumbnail)
local TypingIndicator = require(script.Parent.TypingIndicator)
local UserChatBubble = require(script.Parent.UserChatBubble)
local AssetCard = require(script.Parent.AssetCard)
local Create = require(LuaChat.Create)
local Constants = require(LuaChat.Constants)
local WebApi = require(LuaChat.WebApi)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local LuaApp = Modules.LuaApp
local NotificationType = require(LuaApp.Enum.NotificationType)
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
local RECEIVED_BUBBLE = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble2.png"
local RECEIVED_BUBBLE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble.png"
local RECEIVED_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-tip.png"
local SENT_BUBBLE = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-self2.png"
local SENT_BUBBLE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-self.png"
local SENT_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-self-tip.png"
local SENT_BUBBLE_OUTLINE = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble2.png"
local SENT_BUBBLE_OUTLINE_WITH_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-right.png"
local SENT_OUTLINE_TAIL = "rbxasset://textures/ui/LuaChat/9-slice/chat-bubble-tip-right.png"
local FFlagLuaChatInfiniteRelayoutRecursionFix = settings():GetFFlag("LuaChatInfiniteRelayoutRecursionFix")
local function isOutgoingMessage(message)
local localUserId = tostring(Players.LocalPlayer.UserId)
return message.senderTargetId == localUserId
end
local function isMessageSending(conversation, message)
if conversation and conversation.sendingMessages then
return conversation.sendingMessages:Get(message.id) ~= nil
end
return false
end
local PROTOCOL_IDENTIFIERS = {
"https?://", ""
}
local RESOURCE_NAMES = {
"www%.", "web%.", ""
}
local WHITELISTED_DOMAINS = {
"roblox", "sitetest%d%.robloxlabs", "gametest%d%.robloxlabs"
}
local MESSAGE_CONTENT_PATTERNS = {
GAME_LINK = "%.com/games[^%d]*(%d+)/?",
}
local ChatBubble = {}
ChatBubble.__index = ChatBubble
ChatBubble.BubbleType = {
AssetCard = "AssetCard",
ChatBubble = "UserChatBubble",
}
local function getBubbleImages(message, bubbleType)
if isOutgoingMessage(message) and bubbleType ~= ChatBubble.BubbleType.AssetCard then
return SENT_BUBBLE, SENT_BUBBLE_WITH_TAIL, SENT_TAIL
elseif isOutgoingMessage(message) then
return SENT_BUBBLE_OUTLINE, SENT_BUBBLE_OUTLINE_WITH_TAIL, SENT_OUTLINE_TAIL
else
return RECEIVED_BUBBLE, RECEIVED_BUBBLE_WITH_TAIL, RECEIVED_TAIL
end
end
function ChatBubble.new(appState, message, width)
width = width or 0
local self = {}
setmetatable(self, ChatBubble)
local conversationId = message.conversationId
local isSending = isMessageSending(appState.store:getState().ChatAppReducer.Conversations[conversationId], message)
if FFlagLuaChatInfiniteRelayoutRecursionFix then
self.width = width
end
self.appState = appState
self.message = message
self.bubbles = {}
self.connections = {}
if FFlagLuaChatToSplitRbxConnections then
self.rbx_connections = {}
end
self.tailVisible = false
self.rbx = Create.new "Frame" {
Name = "ChatContainer",
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 0),
Create.new "UIListLayout" {
SortOrder = Enum.SortOrder.LayoutOrder,
},
}
if message.moderated or isSending then
self:AddBubble(UserChatBubble.new(appState, message, nil, self.width), 1)
-- Specifically whitelist strings with .com/games in the url
elseif message.content:lower():match(MESSAGE_CONTENT_PATTERNS.GAME_LINK) then
local text = self:FilterForLinks()
-- Flush remaining text if it is not empty
if text:gsub("%s+","") ~= "" then
self:AddBubble(UserChatBubble.new(appState, message, text, self.width))
end
else
self:AddBubble(UserChatBubble.new(appState, message, nil, self.width), 1)
end
return self
end
function ChatBubble:FilterForLinks()
local text = self.message.content
for _, protocol in pairs(PROTOCOL_IDENTIFIERS) do
for _, resource in pairs(RESOURCE_NAMES) do
for _, domain in pairs(WHITELISTED_DOMAINS) do
local constructedUrlPattern = protocol .. resource .. domain .. MESSAGE_CONTENT_PATTERNS.GAME_LINK
for assetId in text:lower():gmatch(constructedUrlPattern) do
local linkStart, endLink = text:lower():find("[^%s*]*" .. constructedUrlPattern .. "[^%s*]*")
if linkStart then
local textBefore = text:sub(1, linkStart - 1)
if textBefore:gsub("%s+","") ~= "" then
self:AddBubble(UserChatBubble.new(self.appState, self.message, textBefore, self.width))
end
self:AddBubble(AssetCard.new(self.appState, self.message, assetId))
text = text:sub(endLink + 1)
else
return text
end
end
end
end
end
return text
end
function ChatBubble:AddBubble(bubble, placement)
table.insert(self.bubbles, placement or #self.bubbles+1 ,bubble)
bubble.rbx.Parent = self.rbx
bubble.LayoutOrder = placement or #self.bubbles
for i=1,#self.bubbles do
self.bubbles[i].LayoutOrder = i
end
local connection = bubble.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:Resize()
end)
if FFlagLuaChatToSplitRbxConnections then
table.insert(self.rbx_connections, connection)
else
table.insert(self.connections, connection)
end
self:Update()
end
function ChatBubble:SetUsernameVisible(value)
local bubblePos = self.bubbles[1].bubble.Position
if value then
self.bubbles[1].usernameLabel.Visible = true
self.bubbles[1].bubble.Position = UDim2.new(
bubblePos.X.Scale,
bubblePos.X.Offset,
0,
16
)
else
self.bubbles[1].usernameLabel.Visible = false
self.bubbles[1].bubble.Position = UDim2.new(
bubblePos.X.Scale,
bubblePos.X.Offset,
0,
0
)
end
self:Resize()
end
function ChatBubble:SetTypingIndicatorVisible(value)
if value and not self.indicator then
local indicator = TypingIndicator.new(self.appState, .4)
indicator.rbx.AnchorPoint = Vector2.new(0,0.5)
indicator.rbx.Position = UDim2.new(0, self.bubbles[1].usernameLabel.TextBounds.X + 3, 0.5, 0)
indicator.rbx.Parent = self.bubbles[1].usernameLabel
self.indicator = indicator
elseif self.indicator and not value then
self.indicator:Destroy()
self.indicator = nil
end
end
function ChatBubble:SetThumbnailVisible(value)
if value then
self.thumbnail = UserThumbnail.new(self.appState, self.message.senderTargetId, true)
self.thumbnail.rbx.Position = UDim2.new(0, 10, 0, 0)
self.thumbnail.rbx.Overlay.ImageColor3 = Constants.Color.GRAY6
self.thumbnail.rbx.Parent = self.bubbles[1].bubbleContainer
self.thumbnail.clicked:connect(function()
local user = self.appState.store:getState().Users[self.message.senderTargetId]
local userId = user and user.id
if userId then
GuiService:BroadcastNotification(WebApi.MakeUserProfileUrl(userId),
NotificationType.VIEW_PROFILE)
end
end)
else
if self.thumbnail then
self.thumbnail:Destruct()
end
end
end
function ChatBubble:SetTailVisible(value)
self.tailVisible = value
if not self.bubbles[1] then return end
for i, bubble in pairs(self.bubbles) do
local bubbleImage, bubbleWithTail, tailImage = getBubbleImages(self.message, bubble.bubbleType)
if value and i == 1 then
bubble.bubble.Image = bubbleWithTail
bubble.tail.Image = tailImage
bubble.tail.Visible = true
else
bubble.bubble.Image = bubbleImage
bubble.tail.Visible = false
end
end
end
function ChatBubble:SetPaddingObject(object)
if not self.bubbles[1] then return end
if self.bubbles[1].paddingObject then
self.bubbles[1].paddingObject:Destroy()
end
object.LayoutOrder = 1
object.Parent = self.bubbles[1].rbx
self.bubbles[1].paddingObject = object
self.bubbles[1]:Resize()
end
function ChatBubble:Resize()
if not FFlagLuaChatInfiniteRelayoutRecursionFix then
for _,bubble in pairs(self.bubbles) do
bubble:Resize()
end
end
local height = 0
for _, child in ipairs(self.rbx:GetChildren()) do
if child:IsA("GuiObject") then
height = height + child.AbsoluteSize.Y
end
end
self.rbx.Size = UDim2.new(1, 0, 0, height)
end
function ChatBubble:Update()
self:SetTailVisible(self.tailVisible)
self:Resize()
end
function ChatBubble:Destruct()
for _, connection in ipairs(self.connections) do
connection:disconnect()
end
self.connections = {}
if FFlagLuaChatToSplitRbxConnections then
for _, connection in ipairs(self.rbx_connections) do
connection:Disconnect()
end
self.rbx_connections = {}
end
for _, bubble in ipairs(self.bubbles) do
bubble:Destruct()
end
if self.thumbnail then
self.thumbnail:Destruct()
end
self.thumbnail = nil
self.rbx:Destroy()
end
return ChatBubble
@@ -0,0 +1,464 @@
return function()
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local LuaChat = Modules.LuaChat
local MessageModel = require(LuaChat.Models.Message)
local ChatBubble = require(LuaChat.Components.ChatBubble)
local AppState = require(LuaChat.AppState)
local FFlagEnableChatMessageType = settings():GetFFlag("EnableChatMessageType")
describe("new", function()
it("should create with no errors", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "testing",
})
local chat = ChatBubble.new(appState, message)
expect(chat).to.be.ok()
end)
end)
describe("asset card creation", function()
describe("web protocol filtering", function()
it("should make a card with http:// prefix", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "http://www.roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with https:// prefix", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with no protocol prefix", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "www.roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
end)
describe("web resource names", function()
describe("www.", function()
it("should make a card with https:// protocol", function()
local appState = AppState.mock()
local message1 = MessageModel.mock({
content = "https://www.roblox.com/games/1818/Classic-Crossroads"
})
local chat1 = ChatBubble.new(appState, message1)
expect(#chat1.bubbles).to.equal(1)
expect(chat1.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat1.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with http:// protocol", function()
local appState = AppState.mock()
local message2 = MessageModel.mock({
content = "http://www.roblox.com/games/1818/Classic-Crossroads"
})
local chat2 = ChatBubble.new(appState, message2)
expect(#chat2.bubbles).to.equal(1)
expect(chat2.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat2.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with no protocol", function()
local appState = AppState.mock()
local message3 = MessageModel.mock({
content = "www.roblox.com/games/1818/Classic-Crossroads"
})
local chat3 = ChatBubble.new(appState, message3)
expect(#chat3.bubbles).to.equal(1)
expect(chat3.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat3.bubbles[1].assetId).to.equal("1818")
end)
end)
describe("web.", function()
it("should make a card with https:// protocol", function()
local appState = AppState.mock()
local message1 = MessageModel.mock({
content = "https://web.roblox.com/games/1818/Classic-Crossroads"
})
local chat1 = ChatBubble.new(appState, message1)
expect(#chat1.bubbles).to.equal(1)
expect(chat1.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat1.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with http:// protocol", function()
local appState = AppState.mock()
local message2 = MessageModel.mock({
content = "http://web.roblox.com/games/1818/Classic-Crossroads"
})
local chat2 = ChatBubble.new(appState, message2)
expect(#chat2.bubbles).to.equal(1)
expect(chat2.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat2.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with no protocol", function()
local appState = AppState.mock()
local message3 = MessageModel.mock({
content = "web.roblox.com/games/1818/Classic-Crossroads"
})
local chat3 = ChatBubble.new(appState, message3)
expect(#chat3.bubbles).to.equal(1)
expect(chat3.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat3.bubbles[1].assetId).to.equal("1818")
end)
end)
end)
describe("whitelisted domains", function()
describe("roblox.com", function()
it("should make a card with http protocol prefixes", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with only a resouce name", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "www.roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card without a http protocol prefixes", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
end)
describe("sitetest1.robloxlabs.com", function()
it("should make a card with http protocol prefixes", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.sitetest1.robloxlabs.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with only a resouce name", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "web.sitetest1.robloxlabs.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card without a http protocol prefixes", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "sitetest1.robloxlabs.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
end)
describe("gametest1.robloxlabs.com", function()
it("should make a card with http protocol prefixes", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.gametest1.robloxlabs.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card with only a resouce name", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "web.gametest1.robloxlabs.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should make a card without a http protocol prefixes", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "gametest1.robloxlabs.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
end)
describe("invalid domains", function()
it("should not create an asset card for non roblox domains", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.google.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.never.equal("AssetCard")
end)
end)
end)
describe("game link format", function()
it("should create an asset card with link without appended title text", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.roblox.com/games/1818/"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should ignore case when creating an asset card with link", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.ROBLOX.com/games/1818/"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should not create an asset card for non games", function()
local appState = AppState.mock()
local message1 = MessageModel.mock({
content = "https://www.roblox.com/users/1922632/profile"
})
local chat1 = ChatBubble.new(appState, message1)
expect(#chat1.bubbles).to.equal(1)
expect(chat1.bubbles[1].bubbleType).to.never.equal("AssetCard")
local message2 = MessageModel.mock({
content = "https://www.roblox.com/Groups/Group.aspx?gid=3475371"
})
local chat2 = ChatBubble.new(appState, message2)
expect(#chat2.bubbles).to.equal(1)
expect(chat2.bubbles[1].bubbleType).to.never.equal("AssetCard")
local message3 = MessageModel.mock({
content = "https://www.roblox.com/catalog/100929604/Green-Sparkle-Time-Fedora"
})
local chat3 = ChatBubble.new(appState, message3)
expect(#chat3.bubbles).to.equal(1)
expect(chat3.bubbles[1].bubbleType).to.never.equal("AssetCard")
end)
end)
describe("roblox links with text", function()
it("should create one UserChatBubble when presented with raw text", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "testing"
})
if FFlagEnableChatMessageType then
message.messageType = MessageModel.MessageTypes.PlainText
end
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[1].textContent.Text).to.equal("testing")
end)
if FFlagEnableChatMessageType then
it("should create one UserChatBubble with placeholder text if unknown message type is used", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "testing",
messageType = "SomeUnknownMessageTypeThatWillNeverExistInProduction"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[1].textContent.Text).to.equal("")
end)
end
it("should create one an AssetCard when presented with roblox link", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.roblox.com/games/1818/Classic-Crossroads",
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(1)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
end)
it("should create two cards when presented a link with text if the link is first", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.roblox.com/games/1818/Classic-Crossroads Play my game!"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(2)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
expect(chat.bubbles[2].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[2].textContent.Text).to.equal(" Play my game!")
end)
it("should create two cards when presented a link with text if the link is second", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "Play my game! https://www.roblox.com/games/1818/Classic-Crossroads"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(2)
expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[1].textContent.Text).to.equal("Play my game! ")
expect(chat.bubbles[2].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[2].assetId).to.equal("1818")
end)
it("should create three cards when presented a link with text", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "Play my game! https://www.roblox.com/games/1818/Classic-Crossroads Or dont."
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(3)
expect(chat.bubbles[1].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[1].textContent.Text).to.equal("Play my game! ")
expect(chat.bubbles[2].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[2].assetId).to.equal("1818")
expect(chat.bubbles[3].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[3].textContent.Text).to.equal(" Or dont.")
end)
it("should optionally accept the final / when parsing a roblox link", function()
local appState = AppState.mock()
local message1 = MessageModel.mock({
content = "https://www.roblox.com/games/1818/"
})
local chat1 = ChatBubble.new(appState, message1)
expect(#chat1.bubbles).to.equal(1)
expect(chat1.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat1.bubbles[1].assetId).to.equal("1818")
local message2 = MessageModel.mock({
content = "https://www.roblox.com/games/1337"
})
local chat2 = ChatBubble.new(appState, message2)
expect(#chat2.bubbles).to.equal(1)
expect(chat2.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat2.bubbles[1].assetId).to.equal("1337")
end)
it("should handle text in between two links", function()
local appState = AppState.mock()
local message = MessageModel.mock({
content = "https://www.roblox.com/games/1818/ or https://www.roblox.com/games/1337"
})
local chat = ChatBubble.new(appState, message)
expect(#chat.bubbles).to.equal(3)
expect(chat.bubbles[1].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[1].assetId).to.equal("1818")
expect(chat.bubbles[2].bubbleType).to.equal("UserChatBubble")
expect(chat.bubbles[2].textContent.Text).to.equal(" or ")
expect(chat.bubbles[3].bubbleType).to.equal("AssetCard")
expect(chat.bubbles[3].assetId).to.equal("1337")
end)
end)
end)
end

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