add gs
This commit is contained in:
@@ -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
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local Text = require(LuaChat.Text)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local PADDING_HEIGHT = 32
|
||||
local TEXT_FONT = Enum.Font.SourceSans
|
||||
local TEXT_FONT_SIZE = Constants.Font.FONT_SIZE_18
|
||||
|
||||
local ChatDisabledIndicator = {}
|
||||
|
||||
ChatDisabledIndicator.__index = ChatDisabledIndicator
|
||||
|
||||
function ChatDisabledIndicator.new(appState)
|
||||
local self = {}
|
||||
|
||||
local imageButtonText = appState.localization:Format("Feature.Chat.Label.PrivacySettings")
|
||||
local ibtWidth = Text.GetTextWidth(imageButtonText, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_16)
|
||||
|
||||
local imageButton = Create.new "ImageButton" {
|
||||
Name = "PrivacySettings",
|
||||
AutoButtonColor = false,
|
||||
Size = UDim2.new(0, ibtWidth+6, 0, 36),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 3,
|
||||
BorderSizePixel = 0,
|
||||
ScaleType = "Slice",
|
||||
SliceCenter = Rect.new(3,3,4,4),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png",
|
||||
ImageColor3 = Constants.Color.GREEN_PRIMARY,
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Title",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
Text = imageButtonText,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
}
|
||||
}
|
||||
|
||||
local textLabelText = appState.localization:Format("Feature.Chat.Message.TurnOnChat")
|
||||
|
||||
local textLabel = Create.new "TextLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 2,
|
||||
Font = TEXT_FONT,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
TextSize = TEXT_FONT_SIZE,
|
||||
Text = textLabelText,
|
||||
TextWrapped = true
|
||||
}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "ChatDisabledIndicator",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 300),
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "IndicatorInner",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 160),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
},
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 72, 0, 72),
|
||||
LayoutOrder = 1,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-friends.png",
|
||||
},
|
||||
|
||||
textLabel,
|
||||
|
||||
imageButton,
|
||||
},
|
||||
}
|
||||
|
||||
local function rescaleFromParentSize()
|
||||
local parentSize = self.rbx.AbsoluteSize
|
||||
local tltHeight = Text.GetTextHeight(textLabelText, TEXT_FONT, TEXT_FONT_SIZE, parentSize.X)
|
||||
textLabel.Size = UDim2.new(0,parentSize.X,0,tltHeight+PADDING_HEIGHT)
|
||||
end
|
||||
|
||||
self.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(rescaleFromParentSize)
|
||||
rescaleFromParentSize()
|
||||
|
||||
self.openPrivacySettings = Signal.new()
|
||||
getInputEvent(self.rbx.IndicatorInner.PrivacySettings):Connect(function()
|
||||
self.openPrivacySettings:fire()
|
||||
end)
|
||||
|
||||
setmetatable(self, ChatDisabledIndicator)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return ChatDisabledIndicator
|
||||
@@ -0,0 +1,475 @@
|
||||
--
|
||||
-- ChatGameCard
|
||||
--
|
||||
-- This is a game that is shown (and possibly pinned) at the top of a chat
|
||||
-- conversation.
|
||||
--
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaApp.Constants)
|
||||
local ContextualMenu = require(LuaApp.Components.ContextualMenu)
|
||||
local FriendCarousel = require(LuaChat.Components.FriendCarousel)
|
||||
local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos)
|
||||
local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local RoactAnalyticsGameCardLoaded = require(LuaChat.Services.RoactAnalyticsGameCardLoaded)
|
||||
local RoactServices = require(LuaApp.RoactServices)
|
||||
|
||||
local ChatGameCard = Roact.PureComponent:extend("ChatGameCard")
|
||||
|
||||
local FFlagLuaChatLoadGameLinkCardInChatAnalytics = settings():GetFFlag("LuaChatLoadGameLinkCardInChatAnalytics")
|
||||
|
||||
local ICON_SIZE_SMALL = 36
|
||||
local ICON_SIZE_LARGE = 60
|
||||
local ICON_SIZE_SMALL_AMENDED = 48
|
||||
|
||||
local CARD_MARGINS = 12
|
||||
local CARD_HEIGHT_SMALL = ICON_SIZE_SMALL + (CARD_MARGINS * 2)
|
||||
local CARD_HEIGHT_LARGE = ICON_SIZE_LARGE + (CARD_MARGINS * 2)
|
||||
|
||||
local GAME_TEXT_COLOR = Constants.Color.GRAY1
|
||||
local GAME_TEXT_FONT = Enum.Font.SourceSans
|
||||
local GAME_TEXT_HEIGHT = 25
|
||||
local GAME_TEXT_HEIGHT_WITH_SUBTITLE = 20
|
||||
local GAME_TEXT_SIZE = 23
|
||||
local GAME_TEXT_SIZE_WITH_SUBTITLE = 20
|
||||
|
||||
local SUBTITLE_TEXT_SIZE = 18
|
||||
local SUBTITLE_TEXT_COLOR = Constants.Color.GRAY2
|
||||
|
||||
local CAROUSEL_SMALL_ICON_SIZE = 24
|
||||
local CAROUSEL_LARGE_ICON_SIZE = 32
|
||||
local CAROUSEL_SMALL_GAP = 3
|
||||
local CAROUSEL_LARGE_GAP = 9
|
||||
local CAROUSEL_SMALL_DOT_SIZE = 8
|
||||
local CAROUSEL_LARGE_DOT_SIZE = 10
|
||||
|
||||
local ACTION_BUTTON_WIDTH = 60
|
||||
local ACTION_BUTTON_HEIGHT = 32
|
||||
local ACTION_COLOR_PLAY = Constants.Color.GREEN_PRIMARY
|
||||
local ACTION_COLOR_TEXT = Constants.Color.WHITE
|
||||
|
||||
local DEBUG_OUTLINE = 0
|
||||
local DEBUG_TRANSPARENCY = 1
|
||||
|
||||
local DEFAULT_GAME_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png"
|
||||
local FADEOUT_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/friendmask.png"
|
||||
local FADEOUT_MASK_WIDTH = 10
|
||||
local GAME_MASK_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/gr-mask-game-icon.png"
|
||||
local ROUNDED_BUTTON = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png"
|
||||
|
||||
-- Set up some default state for this control:
|
||||
function ChatGameCard:init()
|
||||
self.state = {
|
||||
isMenuOpen = false,
|
||||
}
|
||||
|
||||
-- Localize strings. Needs to be done in context because of the way the localization object is being passed to us:
|
||||
local localization = self.props.Localization
|
||||
self.MenuInfoPlayGame = {
|
||||
displayIcon = "rbxasset://textures/ui/LuaApp/icons/ic-games.png",
|
||||
name = "PlayGameButton",
|
||||
displayName = localization:Format("Feature.Chat.Drawer.PlayGame"),
|
||||
}
|
||||
self.MenuInfoPinGame = {
|
||||
displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png",
|
||||
name = "PinGameButton",
|
||||
displayName = localization:Format("Feature.Chat.Drawer.PinGame")
|
||||
}
|
||||
self.MenuInfoUnpinGame = {
|
||||
displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-unpin-20x20.png",
|
||||
name = "UnpinGameButton",
|
||||
displayName = localization:Format("Feature.Chat.Drawer.UnpinGame")
|
||||
}
|
||||
self.MenuItemInfoViewGameDetail = {
|
||||
displayIcon = "rbxasset://textures/ui/LuaChat/icons/ic-viewdetails-20x20.png",
|
||||
name = "ViewDetailsButton",
|
||||
displayName = localization:Format("Feature.Chat.Drawer.ViewDetails"),
|
||||
}
|
||||
end
|
||||
|
||||
function ChatGameCard:render()
|
||||
-- Information about the game is passed in as properties. Action to take
|
||||
-- *on* the game should also be passed in, so we have containment and this
|
||||
-- module only knows the miniumum necessary.
|
||||
|
||||
local parentLayoutOrder = self.props.LayoutOrder
|
||||
|
||||
-- Visual properties of this game card:
|
||||
local isPinnedGame = self.props.isPinnedGame or false
|
||||
local isRecommendedGame = self.props.isRecommended or false
|
||||
|
||||
local game = self.props.game
|
||||
local getPlaceInfo = self.props.getPlaceInfo
|
||||
local getPlaceThumbnail = self.props.getPlaceThumbnail
|
||||
local localization = self.props.Localization
|
||||
local onGamePin = self.props.onGamePin
|
||||
local onGameStart = self.props.onGameStart
|
||||
local onGameUnpin = self.props.onGameUnpin
|
||||
local onViewDetails = self.props.onViewDetails
|
||||
local placeInfos = self.props.placeInfos
|
||||
local placeThumbnails = self.props.placeThumbnails or {}
|
||||
local renderWidth = self.props.renderWidth or 0
|
||||
|
||||
-- Unpack from our properties:
|
||||
local gameFriends = game.friends or {}
|
||||
local placeId = game.placeId
|
||||
|
||||
-- Read or retrieve information about our place:
|
||||
local placeInfo = placeInfos[placeId]
|
||||
local gameName
|
||||
local imageToken = nil
|
||||
local universeId = nil
|
||||
local isPlayable = false
|
||||
if (placeInfo == nil) then
|
||||
getPlaceInfo(placeId)
|
||||
gameName = "(" .. localization:Format("Feature.Chat.Drawer.Loading") .. ")"
|
||||
else
|
||||
gameName = placeInfo.name
|
||||
imageToken = placeInfo.imageToken
|
||||
universeId = placeInfo.universeId
|
||||
isPlayable = placeInfo.isPlayable
|
||||
end
|
||||
|
||||
-- Configure some dimensions based on properties, the GameInfo section in
|
||||
-- particular changes for a large (pinned) vs regular size card:
|
||||
local cardHeight = CARD_HEIGHT_SMALL
|
||||
local carouselItemDotSize = CAROUSEL_SMALL_DOT_SIZE
|
||||
local carouselItemGap = CAROUSEL_SMALL_GAP
|
||||
local carouselItemHeight = CAROUSEL_SMALL_ICON_SIZE
|
||||
local friendAlignment = Enum.HorizontalAlignment.Right
|
||||
local gameIconHeight = ICON_SIZE_SMALL
|
||||
local gameIconWidth = gameIconHeight
|
||||
local gameTextHeight = GAME_TEXT_HEIGHT
|
||||
local gameTextSize = GAME_TEXT_SIZE
|
||||
local infoFillDirection = Enum.FillDirection.Horizontal
|
||||
local subtitle = ""
|
||||
local subtitleVisibility = false
|
||||
|
||||
-- If we don't have an action button, zero the reserved width:
|
||||
local buttonWidth = ACTION_BUTTON_WIDTH
|
||||
if not isPlayable then
|
||||
buttonWidth = 0
|
||||
end
|
||||
|
||||
-- Scaling of elements:
|
||||
local friendsWidthOffset = 0
|
||||
local friendsWidthScale = 1
|
||||
local textWidthOffset = 0
|
||||
local textWidthScale = 1
|
||||
|
||||
if isPinnedGame then
|
||||
cardHeight = CARD_HEIGHT_LARGE
|
||||
carouselItemDotSize = CAROUSEL_LARGE_DOT_SIZE
|
||||
carouselItemGap = CAROUSEL_LARGE_GAP
|
||||
carouselItemHeight = CAROUSEL_LARGE_ICON_SIZE
|
||||
friendAlignment = Enum.HorizontalAlignment.Left
|
||||
gameIconHeight = ICON_SIZE_LARGE
|
||||
gameIconWidth = gameIconHeight
|
||||
infoFillDirection = Enum.FillDirection.Vertical
|
||||
elseif isRecommendedGame then
|
||||
carouselItemHeight = 0
|
||||
gameTextHeight = GAME_TEXT_HEIGHT_WITH_SUBTITLE
|
||||
gameTextSize = GAME_TEXT_SIZE_WITH_SUBTITLE
|
||||
infoFillDirection = Enum.FillDirection.Vertical
|
||||
subtitle = localization:Format("Feature.Chat.Drawer.Recommended")
|
||||
subtitleVisibility = true
|
||||
else
|
||||
friendsWidthScale = 0.5
|
||||
friendsWidthOffset = 0
|
||||
textWidthScale = 0.5
|
||||
textWidthOffset = 0
|
||||
end
|
||||
|
||||
-- This is how much space we have in the center of the card:
|
||||
local centerNegativeSpace = CARD_MARGINS + gameIconWidth + CARD_MARGINS + CARD_MARGINS
|
||||
local countFriends = #gameFriends
|
||||
|
||||
-- Default is Play if nobody else is in the game, Join if we have friends:
|
||||
local actionText
|
||||
if isPlayable then
|
||||
centerNegativeSpace = centerNegativeSpace + buttonWidth + CARD_MARGINS
|
||||
local actionTextKey = "Feature.Chat.Drawer.Play"
|
||||
if countFriends > 0 then
|
||||
actionTextKey = "Feature.Chat.Drawer.Join"
|
||||
end
|
||||
actionText = localization:Format(actionTextKey)
|
||||
end
|
||||
local actionColor = ACTION_COLOR_PLAY
|
||||
local actionTextColor = ACTION_COLOR_TEXT
|
||||
|
||||
-- ...and as a last metrics step, adjust the ratio of the game name and friend carousel:
|
||||
if not (isPinnedGame or isRecommendedGame) then
|
||||
local actualSpace = renderWidth - centerNegativeSpace
|
||||
if actualSpace > 0 then
|
||||
local friendSpace = carouselItemHeight * countFriends
|
||||
local textAdjust = (actualSpace * 0.5) - (friendSpace + CARD_MARGINS)
|
||||
if textAdjust > 0 then
|
||||
textWidthOffset = textWidthOffset + textAdjust
|
||||
friendsWidthOffset = friendsWidthOffset - textAdjust
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Obtain the thumbnail for this game:
|
||||
local gameIcon = DEFAULT_GAME_ICON
|
||||
if placeInfo then
|
||||
local thumbnail = placeThumbnails[imageToken]
|
||||
if thumbnail == nil then
|
||||
if imageToken and imageToken ~= "" then
|
||||
if gameIconWidth == ICON_SIZE_SMALL then
|
||||
getPlaceThumbnail(imageToken, ICON_SIZE_SMALL_AMENDED, ICON_SIZE_SMALL_AMENDED)
|
||||
else
|
||||
getPlaceThumbnail(imageToken, gameIconWidth, gameIconHeight)
|
||||
end
|
||||
end
|
||||
elseif thumbnail.image ~= "" then
|
||||
gameIcon = thumbnail.image
|
||||
end
|
||||
end
|
||||
|
||||
-- Build up a horizontal list of items for our card:
|
||||
local cardItems = {}
|
||||
cardItems["Layout"] = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, CARD_MARGINS)
|
||||
})
|
||||
|
||||
-- Game icon:
|
||||
local layoutOrder = 1
|
||||
cardItems["GameIcon"] = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
||||
BorderSizePixel = DEBUG_OUTLINE,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(0, gameIconHeight, 0, gameIconHeight),
|
||||
Image = gameIcon,
|
||||
}, {
|
||||
Mask = Roact.createElement("ImageLabel", {
|
||||
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),
|
||||
}),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
|
||||
cardItems["GameInfo"] = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
||||
BorderSizePixel = DEBUG_OUTLINE,
|
||||
ClipsDescendants = true,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, -centerNegativeSpace, 1, 0),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = infoFillDirection,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
|
||||
GameName = Roact.createElement("TextLabel", {
|
||||
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
||||
BorderSizePixel = DEBUG_OUTLINE,
|
||||
ClipsDescendants = true,
|
||||
Font = GAME_TEXT_FONT,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(textWidthScale, textWidthOffset, 0, gameTextHeight),
|
||||
Text = gameName,
|
||||
TextColor3 = GAME_TEXT_COLOR,
|
||||
TextSize = gameTextSize,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top
|
||||
},{
|
||||
MaskRight = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = FADEOUT_MASK_IMAGE,
|
||||
Position = UDim2.new(1, -FADEOUT_MASK_WIDTH, 0, 0),
|
||||
Size = UDim2.new(0, FADEOUT_MASK_WIDTH, 1, 0),
|
||||
ZIndex = 2,
|
||||
}),
|
||||
}),
|
||||
|
||||
Subtitle = Roact.createElement("TextLabel", {
|
||||
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
||||
BorderSizePixel = DEBUG_OUTLINE,
|
||||
ClipsDescendants = true,
|
||||
Font = GAME_TEXT_FONT,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(textWidthScale, textWidthOffset, 0, ICON_SIZE_SMALL - gameTextHeight),
|
||||
Text = subtitle,
|
||||
TextColor3 = SUBTITLE_TEXT_COLOR,
|
||||
TextSize = SUBTITLE_TEXT_SIZE,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Visible = subtitleVisibility,
|
||||
}),
|
||||
|
||||
GameFriends = Roact.createElement(FriendCarousel, {
|
||||
dotSize = carouselItemDotSize,
|
||||
friends = gameFriends,
|
||||
HorizontalAlignment = friendAlignment,
|
||||
itemGap = carouselItemGap,
|
||||
itemSize = carouselItemHeight,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(friendsWidthScale, friendsWidthOffset, 0, carouselItemHeight),
|
||||
}),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
|
||||
if isPlayable then
|
||||
cardItems["ActionButton"] = Roact.createElement("ImageButton", {
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = DEBUG_OUTLINE,
|
||||
Image = ROUNDED_BUTTON,
|
||||
ImageColor3 = actionColor,
|
||||
LayoutOrder = layoutOrder,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
Size = UDim2.new(0, ACTION_BUTTON_WIDTH, 0, ACTION_BUTTON_HEIGHT),
|
||||
SliceCenter = Rect.new(3,3,4,4),
|
||||
|
||||
[Roact.Event.Activated] = function(rbx)
|
||||
onGameStart()
|
||||
end
|
||||
},{
|
||||
ActionLabel = Roact.createElement("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = GAME_TEXT_FONT,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Text = actionText,
|
||||
TextColor3 = actionTextColor,
|
||||
TextSize = GAME_TEXT_SIZE,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
if self.state.isMenuOpen then
|
||||
local menuItems
|
||||
if isPinnedGame then
|
||||
menuItems = { self.MenuInfoUnpinGame }
|
||||
else
|
||||
menuItems = { self.MenuInfoPinGame }
|
||||
end
|
||||
|
||||
if isPlayable then
|
||||
table.insert(menuItems, 1, self.MenuInfoPlayGame)
|
||||
end
|
||||
table.insert(menuItems, self.MenuItemInfoViewGameDetail)
|
||||
|
||||
local callbackCancel = function()
|
||||
self:setState({ isMenuOpen = false })
|
||||
end
|
||||
|
||||
local callbackSelect = function(item)
|
||||
if item.name == self.MenuInfoPlayGame.name then
|
||||
onGameStart()
|
||||
elseif item.name == self.MenuInfoPinGame.name then
|
||||
onGamePin(universeId)
|
||||
elseif item.name == self.MenuInfoUnpinGame.name then
|
||||
onGameUnpin()
|
||||
elseif item.name == self.MenuItemInfoViewGameDetail.name then
|
||||
onViewDetails()
|
||||
end
|
||||
callbackCancel()
|
||||
end
|
||||
|
||||
cardItems["ContextMenu"] = Roact.createElement(ContextualMenu, {
|
||||
callbackCancel = callbackCancel,
|
||||
callbackSelect = callbackSelect,
|
||||
menuItems = menuItems,
|
||||
screenShape = self.state.screenShape,
|
||||
})
|
||||
end
|
||||
|
||||
-- Put a clickable wrapper around the entire card:
|
||||
return Roact.createElement("TextButton", {
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = DEBUG_TRANSPARENCY,
|
||||
BorderSizePixel = DEBUG_OUTLINE,
|
||||
LayoutOrder = parentLayoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, cardHeight),
|
||||
Text = "",
|
||||
[Roact.Event.Activated] = function(rbx)
|
||||
-- TODO: Move this screen size functionality into a helper component
|
||||
-- so that it doesn't get repeated everywhere (see: MOBLUAPP-241).
|
||||
|
||||
-- We need to know the size of the screen, so we can position the
|
||||
-- popout component appropriately. So we climb up the object
|
||||
-- heirachy until we find the current ScreenGui:
|
||||
local screenWidth = 0
|
||||
local screenHeight = 0
|
||||
local screenGui = rbx:FindFirstAncestorOfClass("ScreenGui")
|
||||
if screenGui ~= nil then
|
||||
screenWidth = screenGui.AbsoluteSize.X
|
||||
screenHeight = screenGui.AbsoluteSize.Y
|
||||
end
|
||||
|
||||
self:setState({
|
||||
isMenuOpen = true,
|
||||
screenShape = {
|
||||
x = rbx.AbsolutePosition.X,
|
||||
y = rbx.AbsolutePosition.Y,
|
||||
width = rbx.AbsoluteSize.X,
|
||||
height = rbx.AbsoluteSize.Y,
|
||||
parentWidth = screenWidth,
|
||||
parentHeight = screenHeight,
|
||||
},
|
||||
})
|
||||
end,
|
||||
|
||||
}, cardItems)
|
||||
end
|
||||
|
||||
function ChatGameCard:didUpdate(previousProps, previousState)
|
||||
local conversationId = self.props.conversationId
|
||||
local isGameDrawerOpen = self.props.isGameDrawerOpen
|
||||
local placeId = self.props.game.placeId
|
||||
if FFlagLuaChatLoadGameLinkCardInChatAnalytics then
|
||||
local sendAnalytics = false
|
||||
if isGameDrawerOpen ~= previousProps.isGameDrawerOpen and self.state == previousState then
|
||||
sendAnalytics = true
|
||||
end
|
||||
if isGameDrawerOpen and sendAnalytics then
|
||||
self.props.analytics.reportGameCardLoadedInLuaChat(tostring(conversationId), tostring(placeId))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ChatGameCard = RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
placeInfos = state.ChatAppReducer.PlaceInfos,
|
||||
placeThumbnails = state.ChatAppReducer.PlaceThumbnails,
|
||||
}
|
||||
end,
|
||||
function(dispatch)
|
||||
return {
|
||||
getPlaceInfo = function(placeId)
|
||||
dispatch(GetMultiplePlaceInfos({placeId}))
|
||||
end,
|
||||
getPlaceThumbnail = function(imageToken, iconWidth, iconHeight)
|
||||
dispatch(GetPlaceThumbnail(imageToken, iconWidth, iconHeight))
|
||||
end,
|
||||
}
|
||||
end
|
||||
)(ChatGameCard)
|
||||
|
||||
ChatGameCard = RoactServices.connect({
|
||||
analytics = RoactAnalyticsGameCardLoaded,
|
||||
})(ChatGameCard)
|
||||
|
||||
return ChatGameCard
|
||||
@@ -0,0 +1,41 @@
|
||||
return function()
|
||||
local LocalizationService = game:GetService("LocalizationService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local AppReducer = require(Modules.LuaApp.AppReducer)
|
||||
local Localization = require(Modules.LuaApp.Localization)
|
||||
local MockId = require(Modules.LuaApp.MockId)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local Rodux = require(Modules.Common.Rodux)
|
||||
|
||||
local ChatGameCard = require(Modules.LuaChat.Components.ChatGameCard)
|
||||
|
||||
local localization = Localization.new(LocalizationService.RobloxLocaleId)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
|
||||
local store = Rodux.Store.new(AppReducer)
|
||||
|
||||
local renderWidth = 500
|
||||
local game = {
|
||||
placeId = MockId(),
|
||||
friends = { { uid = MockId() }, { uid = MockId() },
|
||||
{ uid = MockId() }, { uid = MockId() }, { uid = MockId() } },
|
||||
}
|
||||
|
||||
local element = Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = store,
|
||||
}, {
|
||||
GameCard = Roact.createElement(ChatGameCard, {
|
||||
game = game,
|
||||
isPinnedGame = true,
|
||||
Localization = localization,
|
||||
renderWidth = renderWidth,
|
||||
})
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,673 @@
|
||||
--
|
||||
-- ChatGameDrawer
|
||||
--
|
||||
-- Contains ChatGameCard objects that represent pinned or in progress games.
|
||||
-- This lives at the top of a conversation window.
|
||||
--
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
local PlayerService = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local Common = Modules.Common
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Analytics = require(Common.Analytics)
|
||||
local ChatGameCard = require(LuaChat.Components.ChatGameCard)
|
||||
local Constants = require(LuaApp.Constants)
|
||||
local GameParams = require(LuaChat.Models.GameParams)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions)
|
||||
local Roact = require(Common.Roact)
|
||||
local RoactRodux = require(Common.RoactRodux)
|
||||
local SortedActivelyPlayedGames = require(LuaChat.SortedActivelyPlayedGames)
|
||||
local User = require(LuaApp.Models.User)
|
||||
|
||||
local ChatGameDrawer = Roact.PureComponent:extend("ChatGameDrawer")
|
||||
|
||||
local urlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI")
|
||||
|
||||
local luaChatPlayTogetherJoinGameInstance = FlagSettings.LuaChatPlayTogetherJoinGameInstance()
|
||||
|
||||
-- Drawer properties:
|
||||
local DRAWER_BACKGROUND_COLOR = Constants.Color.WHITE
|
||||
local BORDER_SIZE = 12
|
||||
local DRAWER_SHADOW_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/gr-overlay-shadow.png"
|
||||
local DRAWER_SHADOW_HEIGHT = 5
|
||||
|
||||
-- Pointer up to the image:
|
||||
local ICON_POINTER_HEIGHT = 6
|
||||
local ICON_POINTER_WIDTH = 12
|
||||
local ICON_POINTER_UP = "rbxasset://textures/ui/LuaApp/dropdown/gr-tip-up.png"
|
||||
local ICON_POINTER_FROMEDGE = 20
|
||||
|
||||
-- "Pinned Game" text properties:
|
||||
local PINNED_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-pin.png"
|
||||
local PINNED_ICON_SIZE = 12
|
||||
local PINNED_SPACER = 6
|
||||
|
||||
local PINNED_TEXT_COLOR = Constants.Color.GRAY2
|
||||
local PINNED_TEXT_FONT = Enum.Font.SourceSans
|
||||
local PINNED_TEXT_SIZE = 15
|
||||
|
||||
local PINNED_DIVIDER_COLOR = Constants.Color.GRAY4
|
||||
local PINNED_BOTTOM_BORDER = Constants.Color.GRAY4
|
||||
local PINNED_BOTTOM_BACKGROUND = Constants.Color.GRAY6
|
||||
|
||||
local SMALL_DIVIDER_OFFSET = 60
|
||||
local CARD_HEIGHT_SMALL = 60
|
||||
local CARD_HEIGHT_LARGE = 84
|
||||
|
||||
local MORE_TEXT_SIZE = 18
|
||||
local MORE_TEXT_PADDING = 9
|
||||
local MORE_TEXT_COLOR = Constants.Color.GRAY1
|
||||
|
||||
-- Set up some default state for this control:
|
||||
function ChatGameDrawer:init()
|
||||
self._analytics = Analytics.new()
|
||||
self.state = {
|
||||
isExpanded = false,
|
||||
pointerPosition = UDim2.new(1, -ICON_POINTER_FROMEDGE, 0, 0),
|
||||
pointerSet = false,
|
||||
renderWidth = 0,
|
||||
}
|
||||
|
||||
self.isGameDrawerSized = false
|
||||
end
|
||||
|
||||
function ChatGameDrawer:UnpinGame()
|
||||
local playTogetherUnpinGame = self.props.playTogetherUnpinGame
|
||||
playTogetherUnpinGame(self.props.conversationId)
|
||||
end
|
||||
|
||||
-- Games are pinned by universeId (but they're accessed using placeId elsewhere):
|
||||
function ChatGameDrawer:PinGame(universeId)
|
||||
local playTogetherPinGame = self.props.playTogetherPinGame
|
||||
playTogetherPinGame(self.props.conversationId, universeId)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:ViewGameDetails(placeId)
|
||||
GuiService:BroadcastNotification(placeId, GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:GameStart(game)
|
||||
local placeId = game.placeId
|
||||
|
||||
if luaChatPlayTogetherJoinGameInstance then
|
||||
local friends = game.friends
|
||||
if #friends == 0 then
|
||||
-- No friends are here, join naively:
|
||||
self:GamePlayPlace(placeId)
|
||||
else
|
||||
-- We have friends! Go join them:
|
||||
local friend = friends[1]
|
||||
if friend.placeId == friend.rootPlaceId then
|
||||
-- If our friend is in the root instance, we can join the same instance:
|
||||
self:GameJoinInstance(friend.placeId, friend.rootPlaceId, friend.gameInstanceId)
|
||||
else
|
||||
-- Otherwise, we must join to their playerID:
|
||||
self:GameJoinUser(friend.uid, friend.placeId, friend.rootPlaceId)
|
||||
end
|
||||
end
|
||||
else
|
||||
self:GamePlayPlace(placeId)
|
||||
end
|
||||
end
|
||||
|
||||
function ChatGameDrawer:GamePlayPlace(placeId)
|
||||
-- Report player start game via play together:
|
||||
self:ReportGamePlayIntent(placeId)
|
||||
self:ReportPlayerPlayGame(placeId)
|
||||
|
||||
-- Start a game:
|
||||
local gameParams = GameParams.fromPlaceId(placeId)
|
||||
local payload = HttpService:JSONEncode(gameParams)
|
||||
GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:GameJoinUser(userId, placeId, rootPlaceId)
|
||||
-- Report player join game via play together:
|
||||
self:ReportGamePlayIntent(rootPlaceId)
|
||||
self:ReportPlayerJoinGame(placeId, rootPlaceId, nil)
|
||||
|
||||
-- Join a game:
|
||||
local gameParams = GameParams.fromUserId(userId)
|
||||
local payload = HttpService:JSONEncode(gameParams)
|
||||
GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:GameJoinInstance(placeId, rootPlaceId, gameInstanceId)
|
||||
-- Report player join game via play together:
|
||||
self:ReportGamePlayIntent(rootPlaceId)
|
||||
self:ReportPlayerJoinGame(placeId, rootPlaceId, gameInstanceId)
|
||||
|
||||
-- Join a game:
|
||||
local gameParams = GameParams.fromPlaceInstance(placeId, gameInstanceId)
|
||||
local payload = HttpService:JSONEncode(gameParams)
|
||||
GuiService:BroadcastNotification(payload, GuiService:GetNotificationTypeList().LAUNCH_GAME)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:GetGamesFromConversation(conversationId)
|
||||
local conversations = self.props.conversations
|
||||
local mostRecentlyPlayedGames = self.props.mostRecentlyPlayedGames
|
||||
local users = self.props.users
|
||||
|
||||
-- Don't know if this is necessary:
|
||||
if not urlSupportNewGamesAPI then
|
||||
error("Server doesn't support new games API.")
|
||||
return { countFriendsInGames = 0, games = {}, }
|
||||
end
|
||||
|
||||
-- Early out if we have no conversation:
|
||||
if conversationId == nil or conversationId == "nil" then
|
||||
return { countFriendsInGames = 0, games = {}, }
|
||||
end
|
||||
|
||||
-- Find the specific conversation we're interested in:
|
||||
if not conversations then
|
||||
return { countFriendsInGames = 0, games = {}, }
|
||||
end
|
||||
|
||||
local conversation = conversations[conversationId]
|
||||
if not conversation then
|
||||
warn("ChatGameDrawer - Can't find conversation, id:" .. conversationId .. " t:" .. type(conversationId))
|
||||
return { countFriendsInGames = 0, games = {}, }
|
||||
end
|
||||
|
||||
local pinnedGameRootPlaceId = conversation.pinnedGame.rootPlaceId
|
||||
local inGameParticipants = {}
|
||||
local mostRecentPlayedPlayableGamePlaceId = mostRecentlyPlayedGames.playableGamePlaceId
|
||||
local localPlayerId = tostring(PlayerService.LocalPlayer.UserId)
|
||||
|
||||
for _, userId in pairs(conversation.participants) do
|
||||
if userId ~= localPlayerId then
|
||||
local user = users[userId]
|
||||
if user ~= nil then
|
||||
if (user.presence == User.PresenceType.IN_GAME) and user.placeId then
|
||||
table.insert(inGameParticipants, user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local countFriendsInGames = #inGameParticipants
|
||||
if countFriendsInGames > 0 then
|
||||
return {
|
||||
countFriendsInGames = countFriendsInGames,
|
||||
games = SortedActivelyPlayedGames.getSortedGamesPlusEmptyPinned(pinnedGameRootPlaceId, inGameParticipants),
|
||||
}
|
||||
end
|
||||
|
||||
if pinnedGameRootPlaceId then
|
||||
return {
|
||||
countFriendsInGames = 0,
|
||||
games = {
|
||||
{
|
||||
friends = {},
|
||||
pinned = true,
|
||||
placeId = pinnedGameRootPlaceId,
|
||||
recommended = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
if mostRecentPlayedPlayableGamePlaceId then
|
||||
return {
|
||||
countFriendsInGames = 0,
|
||||
games = {
|
||||
{
|
||||
friends = {},
|
||||
pinned = false,
|
||||
placeId = mostRecentPlayedPlayableGamePlaceId,
|
||||
recommended = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
countFriendsInGames = 0,
|
||||
games = {}
|
||||
}
|
||||
end
|
||||
|
||||
function ChatGameDrawer:ReportGamePlayIntent(rootPlaceId)
|
||||
local conversationId = self.props.conversationId
|
||||
local eventContext = "PlayGameFromPlayTogether"
|
||||
local eventName = "gamePlayIntent"
|
||||
|
||||
local player = PlayerService.LocalPlayer
|
||||
local userId = "UNKNOWN"
|
||||
if player then
|
||||
userId = tostring(player.UserId)
|
||||
end
|
||||
|
||||
local additionalArgs = {
|
||||
uid = userId,
|
||||
rootPlaceId = rootPlaceId,
|
||||
}
|
||||
self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:ReportPlayerPlayGame(placeId)
|
||||
local conversationId = self.props.conversationId
|
||||
local eventContext = "touch"
|
||||
local eventName = "clickPlayButtonInPlayTogether"
|
||||
|
||||
local player = PlayerService.LocalPlayer
|
||||
local userId = "UNKNOWN"
|
||||
if player then
|
||||
userId = tostring(player.UserId)
|
||||
end
|
||||
|
||||
local additionalArgs = {
|
||||
uid = userId,
|
||||
cid = conversationId,
|
||||
placeId = placeId
|
||||
}
|
||||
self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:ReportPlayerJoinGame(placeId, rootPlaceId, gameInstanceId)
|
||||
local conversationId = self.props.conversationId
|
||||
local eventContext = "touch"
|
||||
local eventName = "clickJoinButtonInPlayTogether"
|
||||
|
||||
local player = PlayerService.LocalPlayer
|
||||
local userId = "UNKNOWN"
|
||||
if player then
|
||||
userId = tostring(player.UserId)
|
||||
end
|
||||
|
||||
local additionalArgs = {
|
||||
uid = userId,
|
||||
conversationId = conversationId,
|
||||
placeId = placeId,
|
||||
rootPlaceId = rootPlaceId,
|
||||
gameInstanceId = gameInstanceId,
|
||||
}
|
||||
self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
function ChatGameDrawer:render()
|
||||
local anchorPoint = self.props.AnchorPoint
|
||||
local conversationId = self.props.conversationId
|
||||
local isGameDrawerOpen = self.props.isGameDrawerOpen
|
||||
local localization = self.props.Localization
|
||||
local onSize = self.props.onSize
|
||||
local parentLayoutOrder = self.props.layoutOrder
|
||||
local position = self.props.Position
|
||||
|
||||
local isExpanded = self.state.isExpanded
|
||||
local pointerPosition = self.state.pointerPosition or UDim2.new(1, -ICON_POINTER_FROMEDGE, 0, 0)
|
||||
local pointerSet = self.state.pointerSet
|
||||
local pointerTransparency = 1
|
||||
if pointerSet then
|
||||
pointerTransparency = 0
|
||||
end
|
||||
|
||||
local gameInfo = self:GetGamesFromConversation(conversationId)
|
||||
local countGames = #gameInfo.games
|
||||
|
||||
-- Early exit if we have nothing to display in the drawer:
|
||||
if countGames == 0 then
|
||||
spawn(function()
|
||||
onSize(0, false)
|
||||
end)
|
||||
return nil
|
||||
end
|
||||
|
||||
local hasFriendsActive = gameInfo.countFriendsInGames > 0
|
||||
|
||||
-- If we have active friends and this is the first time rendering, expand:
|
||||
if hasFriendsActive and not self.isGameDrawerSized then
|
||||
self.isGameDrawerSized = true
|
||||
if not isExpanded then
|
||||
spawn(function()
|
||||
self:setState({ isExpanded = true })
|
||||
end)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Build up our drop-down items here for display inside our main element:
|
||||
local gameItems = {}
|
||||
local drawerHeight = 0
|
||||
gameItems["Layout"] = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||
})
|
||||
|
||||
-- Display our pinned game (if we have one):
|
||||
local layoutOrder = 1
|
||||
local hasPinnedGame = false
|
||||
|
||||
for _, game in ipairs(gameInfo.games) do
|
||||
if game.pinned then
|
||||
hasPinnedGame = true
|
||||
local placeId = game.placeId
|
||||
|
||||
gameItems["PinnedTitle"] = Roact.createElement("TextButton", {
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, PINNED_ICON_SIZE + (BORDER_SIZE * 2)),
|
||||
Text = "",
|
||||
}, {
|
||||
Icon = Roact.createElement("ImageLabel", {
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = PINNED_ICON,
|
||||
Position = UDim2.new(0, BORDER_SIZE, 0.5, 0),
|
||||
Size = UDim2.new(0, PINNED_ICON_SIZE, 0, PINNED_ICON_SIZE),
|
||||
}),
|
||||
|
||||
Text = Roact.createElement("TextLabel", {
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = PINNED_TEXT_FONT,
|
||||
Position = UDim2.new(0, BORDER_SIZE + PINNED_ICON_SIZE + PINNED_SPACER, 0.5, 0),
|
||||
Size = UDim2.new(1, -(PINNED_ICON_SIZE + PINNED_SPACER + (BORDER_SIZE * 2)), 1, 0),
|
||||
Text = localization:Format("Feature.Chat.Drawer.PinnedGame"),
|
||||
TextColor3 = PINNED_TEXT_COLOR,
|
||||
TextSize = PINNED_TEXT_SIZE,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
}),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + PINNED_ICON_SIZE + (BORDER_SIZE * 2)
|
||||
|
||||
gameItems["Divider"] = Roact.createElement("Frame", {
|
||||
BackgroundColor3 = PINNED_DIVIDER_COLOR,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, -(BORDER_SIZE * 2), 0, 1),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + 1
|
||||
|
||||
-- Index by pinned status and placeId:
|
||||
gameItems["Pinned" .. placeId] = Roact.createElement(ChatGameCard, {
|
||||
game = game,
|
||||
conversationId = conversationId,
|
||||
isGameDrawerOpen = isGameDrawerOpen,
|
||||
isPinnedGame = true,
|
||||
LayoutOrder = layoutOrder,
|
||||
Localization = localization,
|
||||
renderWidth = self.state.renderWidth,
|
||||
onGameStart = function()
|
||||
self:GameStart(game)
|
||||
end,
|
||||
onGameUnpin = function()
|
||||
self:UnpinGame()
|
||||
end,
|
||||
onViewDetails = function()
|
||||
self:ViewGameDetails(placeId)
|
||||
end,
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + CARD_HEIGHT_LARGE
|
||||
|
||||
-- If we have more games to display, add a spacer between the pinned and regular games:
|
||||
if (countGames > 1) then
|
||||
gameItems["Spacer"] = Roact.createElement("Frame", {
|
||||
BackgroundColor3 = PINNED_BOTTOM_BACKGROUND,
|
||||
BackgroundTransparency = 0,
|
||||
BorderColor3 = PINNED_BOTTOM_BORDER,
|
||||
BorderSizePixel = 1,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, PINNED_SPACER),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + PINNED_SPACER
|
||||
end
|
||||
|
||||
-- Done, we found our pinned game in the list:
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Display all the other games in progress - but only if we don't have a
|
||||
-- pinned game or we're expanded:
|
||||
if (not hasPinnedGame) or isExpanded then
|
||||
local hasRegularGame = false
|
||||
for _, game in ipairs(gameInfo.games) do
|
||||
if not game.pinned then
|
||||
-- If we've already added a regular game, insert a spacer before the next:
|
||||
if hasRegularGame then
|
||||
gameItems[layoutOrder] = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
},{
|
||||
divider = Roact.createElement("Frame", {
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = PINNED_DIVIDER_COLOR,
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
Size = UDim2.new(1, -SMALL_DIVIDER_OFFSET, 0, 1),
|
||||
}),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + 1
|
||||
end
|
||||
|
||||
local placeId = game.placeId
|
||||
-- Index as an unpinned game and placeId:
|
||||
gameItems["Game" .. placeId] = Roact.createElement(ChatGameCard, {
|
||||
game = game,
|
||||
conversationId = conversationId,
|
||||
isGameDrawerOpen = isGameDrawerOpen,
|
||||
isPinnedGame = false,
|
||||
isRecommended = game.recommended,
|
||||
LayoutOrder = layoutOrder,
|
||||
Localization = localization,
|
||||
renderWidth = self.state.renderWidth,
|
||||
onGameStart = function()
|
||||
self:GameStart(game)
|
||||
end,
|
||||
onGamePin = function(universeId)
|
||||
self:PinGame(universeId)
|
||||
end,
|
||||
onViewDetails = function()
|
||||
self:ViewGameDetails(placeId)
|
||||
end,
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + CARD_HEIGHT_SMALL
|
||||
|
||||
-- If we're not expanded, we've hit our limit:
|
||||
if not isExpanded then
|
||||
break
|
||||
end
|
||||
|
||||
-- Next card will have a spacer before it.
|
||||
hasRegularGame = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The final item in the list should be the text to either show more or hide:
|
||||
local endTextDivider = false
|
||||
local endTextShow = false
|
||||
local endLocalizeText = ""
|
||||
if countGames == 0 then
|
||||
endTextShow = true
|
||||
endLocalizeText = localization:Format("Feature.Chat.Drawer.NoGames")
|
||||
elseif countGames > 1 then
|
||||
if isExpanded then
|
||||
endLocalizeText = localization:Format("Feature.Chat.Drawer.ShowLess")
|
||||
else
|
||||
endLocalizeText = localization:Format("Feature.Chat.Drawer.ShowMore") .. " (+" .. (countGames - 1) .. ")"
|
||||
end
|
||||
endTextDivider = true
|
||||
endTextShow = true
|
||||
end
|
||||
|
||||
if endTextShow then
|
||||
if endTextDivider then
|
||||
gameItems["DividerBottom"] = Roact.createElement("Frame", {
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = PINNED_DIVIDER_COLOR,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
})
|
||||
layoutOrder = layoutOrder + 1
|
||||
drawerHeight = drawerHeight + PINNED_SPACER
|
||||
end
|
||||
|
||||
gameItems["ShowButton"] = Roact.createElement("TextButton", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = PINNED_TEXT_FONT,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, MORE_TEXT_SIZE + (MORE_TEXT_PADDING * 2)),
|
||||
Text = endLocalizeText,
|
||||
TextColor3 = MORE_TEXT_COLOR,
|
||||
TextSize = MORE_TEXT_SIZE,
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({ isExpanded = not self.state.isExpanded })
|
||||
end
|
||||
})
|
||||
drawerHeight = drawerHeight + MORE_TEXT_SIZE + (MORE_TEXT_PADDING * 2)
|
||||
end
|
||||
|
||||
-- Define the shadow component to hang off the bottom of the list:
|
||||
-- Note: Do not count the height since this isn't inside the frame.
|
||||
local shadow = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Image = DRAWER_SHADOW_IMAGE,
|
||||
Size = UDim2.new(1, 0, 0, DRAWER_SHADOW_HEIGHT),
|
||||
Position = UDim2.new(0, 0, 1, 0),
|
||||
})
|
||||
|
||||
spawn(function()
|
||||
onSize(drawerHeight, hasFriendsActive)
|
||||
end)
|
||||
|
||||
-- Create and return the main control itself:
|
||||
return Roact.createElement("Frame", {
|
||||
AnchorPoint = anchorPoint,
|
||||
BackgroundColor3 = DRAWER_BACKGROUND_COLOR,
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = false,
|
||||
LayoutOrder = parentLayoutOrder,
|
||||
Position = position,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
[Roact.Ref] = function(rbx)
|
||||
if not rbx then
|
||||
return
|
||||
end
|
||||
spawn(function()
|
||||
self:resolveMetrics(rbx)
|
||||
end)
|
||||
end,
|
||||
}, {
|
||||
Pointer = Roact.createElement("ImageLabel", {
|
||||
AnchorPoint = Vector2.new(0.5, 1),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = ICON_POINTER_UP,
|
||||
ImageTransparency = pointerTransparency,
|
||||
Position = pointerPosition,
|
||||
Size = UDim2.new(0, ICON_POINTER_WIDTH, 0, ICON_POINTER_HEIGHT),
|
||||
}),
|
||||
Shadow = shadow,
|
||||
|
||||
Frame = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
},
|
||||
gameItems
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
function ChatGameDrawer:resolveMetrics(rbx)
|
||||
-- This function finds the ActiveGameIcon and positions an arrow pointing
|
||||
-- at it from our open drawer. The position of the icon can change depending
|
||||
-- on a number of factors so it can't be hardcoded.
|
||||
--
|
||||
-- Also retrieves the actual width of our drawer to pass to child components
|
||||
-- which need to be width-aware to render properly.
|
||||
--
|
||||
-- Note 1: I didn't want to attach this on the icon because it needs to line
|
||||
-- up with the edge of the drawer.)
|
||||
--
|
||||
-- Note 2: we can't examine the GameDrawer position here because on the
|
||||
-- initial pass through it is invisible with a position of 0,0 on screen.
|
||||
|
||||
-- Find the conversation header:
|
||||
local header = rbx:FindFirstAncestor("HeaderFrame")
|
||||
if header == nil then
|
||||
warn("Couldn't find header.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Find the "Play Together" icon:
|
||||
local iconPlayTogether = header:FindFirstChild("TopGameIcon", true)
|
||||
if iconPlayTogether == nil then
|
||||
warn("Couldn't find Play Together icon.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Figure out where on the screen iconPlayTogether is, so
|
||||
-- we can position our pointer directly underneath it:
|
||||
local iconFromEdge = (header.AbsolutePosition.X + header.AbsoluteSize.X) -
|
||||
(iconPlayTogether.AbsolutePosition.X + (iconPlayTogether.AbsoluteSize.X * 0.5))
|
||||
|
||||
-- Track our drawer's width for content-aware scaling:
|
||||
local renderWidth = rbx.AbsoluteSize.X
|
||||
|
||||
-- Prevent updating metrics if we already have the correct values:
|
||||
if (not self.state.pointerSet) or
|
||||
(self.state.renderWidth ~= renderWidth) or
|
||||
(self.state.pointerPosition.X.Offset ~= -iconFromEdge) then
|
||||
-- Update the pointer position state so it will render in the correct location:
|
||||
-- Yes, we're using another spawn call here - but we need the 1-frame delay.
|
||||
spawn(function()
|
||||
self:setState({
|
||||
pointerPosition = UDim2.new(1, -iconFromEdge, 0, 0),
|
||||
pointerSet = true,
|
||||
renderWidth = renderWidth,
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
ChatGameDrawer = RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
conversations = state.ChatAppReducer.Conversations,
|
||||
mostRecentlyPlayedGames = state.ChatAppReducer.MostRecentlyPlayedGames,
|
||||
users = state.Users,
|
||||
}
|
||||
end,
|
||||
function(dispatch)
|
||||
return {
|
||||
playTogetherUnpinGame = function(conversationId)
|
||||
dispatch(PlayTogetherActions.UnpinGame(conversationId))
|
||||
end,
|
||||
playTogetherPinGame = function(conversationId, universeId)
|
||||
dispatch(PlayTogetherActions.PinGame(conversationId, universeId))
|
||||
end,
|
||||
}
|
||||
end
|
||||
)(ChatGameDrawer)
|
||||
|
||||
return ChatGameDrawer
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
if not settings():GetFFlag("UrlSupportNewGamesAPI") then
|
||||
return function()
|
||||
it("those tests are not meaningful when flag \"UrlSupportNewGamesAPI\" is off!", function()
|
||||
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return function()
|
||||
local LocalizationService = game:GetService("LocalizationService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local AppReducer = require(Modules.LuaApp.AppReducer)
|
||||
local Localization = require(Modules.LuaApp.Localization)
|
||||
local MockId = require(Modules.LuaApp.MockId)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local Rodux = require(Modules.Common.Rodux)
|
||||
|
||||
local ChatGameDrawer = require(Modules.LuaChat.Components.ChatGameDrawer)
|
||||
|
||||
local localization = Localization.new(LocalizationService.RobloxLocaleId)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
|
||||
local store = Rodux.Store.new(AppReducer)
|
||||
|
||||
local element = Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = store,
|
||||
}, {
|
||||
GameDrawer = Roact.createElement(ChatGameDrawer, {
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
conversationId = MockId(),
|
||||
Localization = localization,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
onSize = function(newSize, forceOpen)
|
||||
-- pass
|
||||
end,
|
||||
}),
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,397 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local PlayerService = game:GetService("Players")
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
local Intent = DialogInfo.Intent
|
||||
local SetRoute = require(LuaChat.Actions.SetRoute)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
|
||||
local FFlagLuaChatInputBarRefactor = settings():GetFFlag("LuaChatInputBarRefactor")
|
||||
local FFlagLuaChatShareGameToChatFromChat = settings():GetFFlag("LuaChatShareGameToChatFromChat")
|
||||
|
||||
local GAME_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-game.png"
|
||||
local INPUT_FRAME_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png"
|
||||
local PRESSED_GAME_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-game-pressed-24x24.png"
|
||||
local SEND_BUTTON_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/send-white.png"
|
||||
local SEND_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-send.png"
|
||||
|
||||
local CHAT_BAR_HEIGHT_TABLET = 64
|
||||
local GAME_ICON_BOTTOM_PADDING = 8
|
||||
local LINE_CUTOFF_PHONE = 4.5 / 5
|
||||
local LINE_CUTOFF_TABLET = 0.95
|
||||
local RIGHT_BUTTON_HEIGHT = 48
|
||||
local RIGHT_BUTTON_WIDTH = 52
|
||||
local SEND_BUTTON_BOTTOM_PADDING = 8
|
||||
local MAX_CHARACTER_LENGTH = 160
|
||||
|
||||
local ChatInputBar = {}
|
||||
ChatInputBar.__index = ChatInputBar
|
||||
|
||||
function ChatInputBar.new(appState)
|
||||
local self = {}
|
||||
setmetatable(self, ChatInputBar)
|
||||
|
||||
self.appState = appState
|
||||
self.sendButtonEnabled = false
|
||||
self.SendButtonPressed = Signal.new()
|
||||
self.UserChangedText = Signal.new()
|
||||
self.blockUserChangedText = true
|
||||
self._analytics = appState.analytics
|
||||
|
||||
local isTablet = appState.store:getState().FormFactor == FormFactor.TABLET
|
||||
|
||||
local lineCutoff
|
||||
if isTablet then
|
||||
lineCutoff = LINE_CUTOFF_TABLET
|
||||
else
|
||||
lineCutoff = LINE_CUTOFF_PHONE
|
||||
end
|
||||
|
||||
local function getTextButtonHeight(text, font, textSize, textBoxAbsoluteSizeX)
|
||||
local textHeight = Text.GetTextHeight(text, font, textSize,
|
||||
textBoxAbsoluteSizeX)
|
||||
local maxTextHeight = Text.GetTextHeight("A\nB\nC\nD\nE", font, textSize,
|
||||
textBoxAbsoluteSizeX) * lineCutoff
|
||||
local textButtonHeight = 24 + math.min(textHeight, maxTextHeight)
|
||||
return textHeight, maxTextHeight, textButtonHeight
|
||||
end
|
||||
|
||||
local textButtonInputTextFont = Enum.Font.SourceSans
|
||||
local textButtonInputTextSize = Constants.Font.FONT_SIZE_18
|
||||
|
||||
local _, heightOfFourLines
|
||||
local textButtonHeight
|
||||
if FFlagLuaChatInputBarRefactor then
|
||||
_, _, heightOfFourLines = getTextButtonHeight("", textButtonInputTextFont, textButtonInputTextSize, 0)
|
||||
if isTablet then
|
||||
textButtonHeight = CHAT_BAR_HEIGHT_TABLET
|
||||
else
|
||||
textButtonHeight = heightOfFourLines
|
||||
end
|
||||
else
|
||||
_, _, textButtonHeight = getTextButtonHeight("", textButtonInputTextFont, textButtonInputTextSize, 0)
|
||||
end
|
||||
|
||||
local textBoxPosition
|
||||
if isTablet then
|
||||
textBoxPosition = UDim2.new(0, 0, 0, 0)
|
||||
else
|
||||
textBoxPosition = UDim2.new(0, 12, 0, 6)
|
||||
end
|
||||
|
||||
local textBoxInstance = Create.new "TextBox" {
|
||||
Name = "InputText",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = textBoxPosition,
|
||||
Text = "",
|
||||
Font = textButtonInputTextFont,
|
||||
TextSize = textButtonInputTextSize,
|
||||
TextColor3 = Constants.Text.INPUT,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextWrapped = true,
|
||||
OverlayNativeInput = true,
|
||||
ClearTextOnFocus = false,
|
||||
ManualFocusRelease = true,
|
||||
MultiLine = true,
|
||||
PlaceholderText = appState.localization:Format("Feature.Chat.Label.ChatInputPlaceholder"),
|
||||
PlaceholderColor3 = Constants.Text.INPUT_PLACEHOLDER,
|
||||
}
|
||||
|
||||
local inputBarInstance
|
||||
if isTablet then
|
||||
inputBarInstance = Create.new "ImageLabel" {
|
||||
Name = "InputBarFrame",
|
||||
Size = UDim2.new(1, -68, 1, -24),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
Position = UDim2.new(0, 12, 0, 12),
|
||||
BackgroundTransparency = 1,
|
||||
Image = "",
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(3, 3, 4, 4),
|
||||
Create.new "Frame" {
|
||||
Name = "InnerFrame",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = false,
|
||||
|
||||
textBoxInstance
|
||||
},
|
||||
}
|
||||
else
|
||||
inputBarInstance = Create.new "ImageLabel" {
|
||||
Name = "InputBarFrame",
|
||||
Size = UDim2.new(1, -62, 1, -10),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Position = UDim2.new(0, 10, 0.5, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Image = INPUT_FRAME_IMAGE,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(3, 3, 4, 4),
|
||||
Create.new "Frame" {
|
||||
Name = "InnerFrame",
|
||||
Size = UDim2.new(1, -24, 1, -12),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = false,
|
||||
|
||||
textBoxInstance
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local gameButtonAnchorPoint
|
||||
local gameButtonPosition
|
||||
if FFlagLuaChatInputBarRefactor then
|
||||
gameButtonAnchorPoint = Vector2.new(1, 0.5)
|
||||
gameButtonPosition = UDim2.new(1, 0, 0.5, 0)
|
||||
else
|
||||
gameButtonAnchorPoint = Vector2.new(0, 0)
|
||||
gameButtonPosition = UDim2.new(
|
||||
1,
|
||||
-RIGHT_BUTTON_WIDTH,
|
||||
1,
|
||||
-RIGHT_BUTTON_HEIGHT - (isTablet and GAME_ICON_BOTTOM_PADDING or 0)
|
||||
)
|
||||
end
|
||||
|
||||
self.rbx = Create.new "TextButton" {
|
||||
Name = "ChatInputBar",
|
||||
Text = "",
|
||||
AutoButtonColor = false,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, textButtonHeight),
|
||||
BorderSizePixel = 0,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "TopBorder",
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
BackgroundColor3 = Constants.Color.GRAY3,
|
||||
BorderSizePixel = 0,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
},
|
||||
|
||||
inputBarInstance,
|
||||
|
||||
Create.new "ImageButton" {
|
||||
Name = "GameButton",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, RIGHT_BUTTON_WIDTH, 0, RIGHT_BUTTON_HEIGHT),
|
||||
AnchorPoint = gameButtonAnchorPoint,
|
||||
Position = gameButtonPosition,
|
||||
Visible = FFlagLuaChatShareGameToChatFromChat,
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
Name = "GameIcon",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 24, 0, 24),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Image = GAME_ICON,
|
||||
}
|
||||
},
|
||||
|
||||
Create.new "ImageButton" {
|
||||
Name = "SendButton",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 32, 0, 32),
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
Position = UDim2.new(1, -42, 1, -SEND_BUTTON_BOTTOM_PADDING),
|
||||
Image = SEND_BUTTON_IMAGE,
|
||||
ImageColor3 = Constants.Color.GRAY3,
|
||||
Visible = not FFlagLuaChatShareGameToChatFromChat,
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
Name = "Icon",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
Position = UDim2.new(0.5, 1, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Image = SEND_ICON,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.rbx.TouchTap:Connect(function()
|
||||
--Sink this tap so the keyboard doesn't close
|
||||
end)
|
||||
|
||||
self.textBox = textBoxInstance
|
||||
|
||||
local function updateTextBoxSize()
|
||||
local textBoxText = self.textBox.Text
|
||||
if textBoxText:len() == 0 then
|
||||
textBoxText = self.textBox.PlaceholderText
|
||||
end
|
||||
|
||||
local textHeight, maxTextHeight, textButtonHeightUpdate = getTextButtonHeight(
|
||||
textBoxText,
|
||||
self.textBox.Font,
|
||||
self.textBox.TextSize,
|
||||
self.textBox.AbsoluteSize.X
|
||||
)
|
||||
self.rbx.Size = UDim2.new(1, 0, 0, textButtonHeightUpdate)
|
||||
|
||||
if not self.textBox:IsFocused() and textHeight > maxTextHeight then
|
||||
self.textBox.Size = UDim2.new(1, 0, 0, 24 + textHeight);
|
||||
self.rbx.InputBarFrame.InnerFrame.ClipsDescendants = true
|
||||
else
|
||||
self.textBox.Size = UDim2.new(1, 0, 1, 0);
|
||||
self.rbx.InputBarFrame.InnerFrame.ClipsDescendants = false
|
||||
end
|
||||
end
|
||||
|
||||
self.textBox.Focused:Connect(function()
|
||||
updateTextBoxSize()
|
||||
self.textBox.Size = UDim2.new(1, 0, 1, 0);
|
||||
self.blockUserChangedText = false
|
||||
end)
|
||||
|
||||
self.textBox.FocusLost:Connect(function()
|
||||
updateTextBoxSize()
|
||||
|
||||
if #self.textBox.Text <= 0 then
|
||||
self:Reset()
|
||||
end
|
||||
end)
|
||||
|
||||
self.textBox:GetPropertyChangedSignal("Text"):Connect(function()
|
||||
local text = self.textBox.Text
|
||||
|
||||
updateTextBoxSize()
|
||||
|
||||
if FFlagLuaChatShareGameToChatFromChat then
|
||||
self:RightButtonVisibility(string.len(text) == 0)
|
||||
end
|
||||
|
||||
if self:_isMessageValid(text) then
|
||||
self:SetSendButtonEnabled(true)
|
||||
else
|
||||
self:SetSendButtonEnabled(false)
|
||||
end
|
||||
|
||||
if not self.blockUserChangedText then
|
||||
self.UserChangedText:fire()
|
||||
end
|
||||
end)
|
||||
|
||||
getInputEvent(self.rbx.SendButton):Connect(function()
|
||||
self:SendMessage()
|
||||
end)
|
||||
|
||||
if FFlagLuaChatShareGameToChatFromChat then
|
||||
getInputEvent(self.rbx.GameButton):Connect(function()
|
||||
self:ReportBrowseGamesButtonTappedEvent()
|
||||
if self.textBox:IsFocused() then
|
||||
self.textBox:ReleaseFocus()
|
||||
end
|
||||
appState.store:dispatch(SetRoute(Intent.BrowseGames, {}))
|
||||
end)
|
||||
|
||||
self.rbx.GameButton.InputBegan:Connect(function()
|
||||
self:SetGameButtonIcon(true)
|
||||
end)
|
||||
|
||||
self.rbx.GameButton.InputEnded:Connect(function()
|
||||
self:SetGameButtonIcon(false)
|
||||
end)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ChatInputBar:_isMessageValid(text)
|
||||
if FFlagLuaChatInputBarRefactor then
|
||||
if #text > MAX_CHARACTER_LENGTH then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if #text >= MAX_CHARACTER_LENGTH then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Only whitespace
|
||||
if text:match("^%s*$") then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function ChatInputBar:RightButtonVisibility(isInputBoxEmpty)
|
||||
self.rbx.SendButton.Visible = not isInputBoxEmpty
|
||||
self.rbx.GameButton.Visible = isInputBoxEmpty
|
||||
end
|
||||
|
||||
function ChatInputBar:Reset()
|
||||
self.blockUserChangedText = true
|
||||
self.textBox.Text = ""
|
||||
self.textBox:ResetKeyboardMode()
|
||||
self.blockUserChangedText = false
|
||||
end
|
||||
|
||||
function ChatInputBar:SendMessage()
|
||||
local text = self.textBox.Text
|
||||
if not self:_isMessageValid(text) then
|
||||
return
|
||||
end
|
||||
|
||||
self:Reset()
|
||||
self.SendButtonPressed:fire(text)
|
||||
end
|
||||
|
||||
function ChatInputBar:SetSendButtonEnabled(value)
|
||||
if self.sendButtonEnabled == value then
|
||||
return
|
||||
end
|
||||
self.sendButtonEnabled = value
|
||||
|
||||
local color = value and Constants.Color.BLUE_PRIMARY or Constants.Color.GRAY3
|
||||
self.rbx.SendButton.ImageColor3 = color
|
||||
end
|
||||
|
||||
function ChatInputBar:SetGameButtonIcon(isPressed)
|
||||
if isPressed then
|
||||
self.rbx.GameButton.GameIcon.Image = PRESSED_GAME_ICON
|
||||
else
|
||||
self.rbx.GameButton.GameIcon.Image = GAME_ICON
|
||||
end
|
||||
end
|
||||
|
||||
function ChatInputBar:GetHeight()
|
||||
return self.rbx.Size.Y.Offset
|
||||
end
|
||||
|
||||
function ChatInputBar:ReportBrowseGamesButtonTappedEvent()
|
||||
local eventContext = "touch"
|
||||
local eventName = "chooseGameToShare"
|
||||
|
||||
local player = PlayerService.LocalPlayer
|
||||
local userId = "UNKNOWN"
|
||||
if player then
|
||||
userId = tostring(player.UserId)
|
||||
end
|
||||
|
||||
local additionalArgs = {
|
||||
uid = userId,
|
||||
cid = self.appState.store:getState().ChatAppReducer.ActiveConversationId
|
||||
}
|
||||
|
||||
self._analytics.EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
function ChatInputBar:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return ChatInputBar
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
return function()
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local AppState = require(LuaChat.AppState)
|
||||
local ChatInputBar = require(LuaChat.Components.ChatInputBar)
|
||||
|
||||
local SetFormFactor = require(LuaApp.Actions.SetFormFactor)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
|
||||
local FFlagLuaChatInputBarRefactor = settings():GetFFlag("LuaChatInputBarRefactor")
|
||||
|
||||
describe("new", function()
|
||||
it("should construct a new chat input bar with no errors", function()
|
||||
local appState = AppState.mock()
|
||||
appState.store:dispatch(SetFormFactor(FormFactor.TABLET))
|
||||
|
||||
local chatInputBar_Tablet = ChatInputBar.new(appState)
|
||||
|
||||
expect(chatInputBar_Tablet).to.be.ok()
|
||||
|
||||
chatInputBar_Tablet:Destruct()
|
||||
|
||||
appState.store:dispatch(SetFormFactor(FormFactor.PHONE))
|
||||
|
||||
local chatInputBar_Phone = ChatInputBar.new(appState)
|
||||
|
||||
expect(chatInputBar_Phone).to.be.ok()
|
||||
|
||||
chatInputBar_Phone:Destruct()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("isMessageValid", function()
|
||||
if FFlagLuaChatInputBarRefactor then
|
||||
it("should return false if string is longer than 160 characters", function()
|
||||
local appState = AppState.mock()
|
||||
local chatInputBar = ChatInputBar.new(appState)
|
||||
|
||||
local testString_Long = string.rep("b", 200)
|
||||
local isMessageValid_Long = chatInputBar:_isMessageValid(testString_Long)
|
||||
|
||||
local testString_160 = string.rep("b", 160)
|
||||
local isMessageValid_160 = chatInputBar:_isMessageValid(testString_160)
|
||||
|
||||
local testString_Short = "b"
|
||||
local isMessageValid_Short = chatInputBar:_isMessageValid(testString_Short)
|
||||
|
||||
expect(isMessageValid_Long).to.equal(false)
|
||||
expect(isMessageValid_160).to.equal(true)
|
||||
expect(isMessageValid_Short).to.equal(true)
|
||||
end)
|
||||
end
|
||||
|
||||
it("should return false if string is only white space characters", function()
|
||||
local appState = AppState.mock()
|
||||
local chatInputBar = ChatInputBar.new(appState)
|
||||
|
||||
local testString_Empty = ""
|
||||
local isMessageValid_Empty = chatInputBar:_isMessageValid(testString_Empty)
|
||||
|
||||
local testString_Space = " "
|
||||
local isMessageValid_Space = chatInputBar:_isMessageValid(testString_Space)
|
||||
|
||||
local testString_NewLines = "\n\r\n"
|
||||
local isMessageValid_NewLines = chatInputBar:_isMessageValid(testString_NewLines)
|
||||
|
||||
local testString_Hello = "hello"
|
||||
local isMessageValid_Hello = chatInputBar:_isMessageValid(testString_Hello)
|
||||
|
||||
expect(isMessageValid_Empty).to.equal(false)
|
||||
expect(isMessageValid_Space).to.equal(false)
|
||||
expect(isMessageValid_NewLines).to.equal(false)
|
||||
expect(isMessageValid_Hello).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("SendMessage", function()
|
||||
it("should fire SendButtonPressed when invoked", function()
|
||||
local appState = AppState.mock()
|
||||
local chatInputBar = ChatInputBar.new(appState)
|
||||
|
||||
local count = 0
|
||||
chatInputBar.SendButtonPressed:Connect(function()
|
||||
count = count + 1
|
||||
end)
|
||||
|
||||
chatInputBar.textBox.Text = "hello"
|
||||
chatInputBar:SendMessage()
|
||||
|
||||
expect(count).to.equal(1)
|
||||
|
||||
chatInputBar.textBox.Text = "goodbye"
|
||||
chatInputBar:SendMessage()
|
||||
|
||||
expect(count).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should not send messages when text is invalid", function()
|
||||
local appState = AppState.mock()
|
||||
local chatInputBar = ChatInputBar.new(appState)
|
||||
|
||||
local count = 0
|
||||
chatInputBar.SendButtonPressed:Connect(function()
|
||||
count = count + 1
|
||||
end)
|
||||
|
||||
chatInputBar.textBox.Text = ""
|
||||
chatInputBar:SendMessage()
|
||||
|
||||
expect(count).to.equal(0)
|
||||
|
||||
chatInputBar.textBox.Text = "\n\n"
|
||||
chatInputBar:SendMessage()
|
||||
|
||||
expect(count).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should reset textBox when invoked", function()
|
||||
local appState = AppState.mock()
|
||||
local chatInputBar = ChatInputBar.new(appState)
|
||||
|
||||
chatInputBar.textBox.Text = "hello"
|
||||
chatInputBar:SendMessage()
|
||||
|
||||
expect(chatInputBar.textBox.Text).to.equal("")
|
||||
end)
|
||||
|
||||
end)
|
||||
end
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
local LuaChat = script.Parent.Parent
|
||||
local Create = require(LuaChat.Create)
|
||||
local LoadingIndicator = require(script.Parent.LoadingIndicator)
|
||||
|
||||
local ChatLoadingIndicator = {}
|
||||
|
||||
function ChatLoadingIndicator.new(appState)
|
||||
local self = {}
|
||||
|
||||
local indicator = LoadingIndicator.new(appState, 2)
|
||||
self.super = indicator
|
||||
|
||||
self.rbx = Create "Frame" {
|
||||
Name = "ChatLoadingIndicator",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Create "Frame" {
|
||||
Name = "Inner",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 200),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Create "UIListLayout" {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
},
|
||||
|
||||
indicator.rbx,
|
||||
},
|
||||
}
|
||||
|
||||
setmetatable(self, ChatLoadingIndicator)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ChatLoadingIndicator:SetVisible(visible)
|
||||
self.rbx.Visible = visible
|
||||
self.super:SetVisible(visible)
|
||||
end
|
||||
|
||||
ChatLoadingIndicator.__index = ChatLoadingIndicator
|
||||
|
||||
return ChatLoadingIndicator
|
||||
@@ -0,0 +1,63 @@
|
||||
local LuaChat = script.Parent.Parent
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Text = require(LuaChat.Text)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
|
||||
local ChatTimestamp = {}
|
||||
|
||||
ChatTimestamp.__index = ChatTimestamp
|
||||
|
||||
function ChatTimestamp.new(appState, text)
|
||||
local self = {}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "ChatTimestamp",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 50),
|
||||
|
||||
Create.new "UIPadding" {
|
||||
PaddingTop = UDim.new(0, 20),
|
||||
PaddingBottom = UDim.new(0, 10),
|
||||
},
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Color3.fromRGB(224, 224, 224),
|
||||
Size = UDim2.new(0, 150, 0, 30),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/system-message.png",
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(3, 3, 4, 4),
|
||||
|
||||
Create.new "UIPadding" {
|
||||
PaddingTop = UDim.new(0, 4),
|
||||
PaddingBottom = UDim.new(0, 4),
|
||||
PaddingLeft = UDim.new(0, 6),
|
||||
PaddingRight = UDim.new(0, 6),
|
||||
},
|
||||
|
||||
Create.new "TextLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
TextColor3 = Color3.fromRGB(128, 128, 128),
|
||||
Text = text,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local textBounds = Text.GetTextBounds(text, Enum.Font.SourceSans, Constants.Font.FONT_SIZE_14, Vector2.new(1000, 1000))
|
||||
self.rbx.ImageLabel.Size = UDim2.new(0, textBounds.X + 12, 0, textBounds.Y + 8)
|
||||
|
||||
setmetatable(self, ChatTimestamp)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return ChatTimestamp
|
||||
@@ -0,0 +1,593 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Conversation = require(LuaChat.Models.Conversation)
|
||||
local ConversationActions = require(LuaChat.Actions.ConversationActions)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
local Signal = require(Common.Signal)
|
||||
local WebApi = require(LuaChat.WebApi)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
|
||||
local PlayTogetherGameIcon = require(Components.PlayTogetherGameIcon)
|
||||
local ChatInputBar = require(Components.ChatInputBar)
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
local Icebreaker = require(Components.Icebreaker)
|
||||
local LoadingIndicator = require(Components.LoadingIndicator)
|
||||
local MessageList = require(Components.MessageList)
|
||||
local PaddedImageButton = require(Components.PaddedImageButton)
|
||||
local UserTypingIndicator = require(Components.UserTypingIndicator)
|
||||
|
||||
local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle)
|
||||
|
||||
local SetActiveConversationId = require(LuaChat.Actions.SetActiveConversationId)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection")
|
||||
local LuaChatGroupChatIconEnabled = settings():GetFFlag("LuaChatGroupChatIconEnabled")
|
||||
local LuaChatActiveConversationId = settings():GetFFlag("LuaChatActiveConversationId")
|
||||
local FFlagShareGameToChatStatusAnalytics = settings():GetFFlag("ShareGameToChatStatusAnalytics")
|
||||
local FFlagLuaChatIcebreaker = settings():GetFFlag("LuaChatIcebreaker")
|
||||
|
||||
local ConversationView = {}
|
||||
|
||||
ConversationView.__index = ConversationView
|
||||
|
||||
local LayoutOrder = {
|
||||
HEADER = 10,
|
||||
INITIAL_LOADING_FRAME = 20,
|
||||
MESSAGE_LIST = 30,
|
||||
TYPING_INDICATOR = 1000,
|
||||
INPUT_BAR = 1000000,
|
||||
}
|
||||
|
||||
local function getNewestWithNilPreviousMessageId(messages)
|
||||
for id, message, _ in messages:CreateReverseIterator() do
|
||||
if message.previousMessageId == nil then
|
||||
return id
|
||||
end
|
||||
end
|
||||
return messages.keys[1]
|
||||
end
|
||||
|
||||
local function sendPreprocess(inputText)
|
||||
if inputText == "/shrug" then
|
||||
return "¯\\_(ツ)_/¯"
|
||||
end
|
||||
|
||||
-- Future chat commands will go here
|
||||
|
||||
return inputText
|
||||
end
|
||||
|
||||
function ConversationView.new(appState)
|
||||
local self = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
self.conversationId = nil
|
||||
self.appState = appState
|
||||
self.lastTypingTimestamp = 0
|
||||
self.BackButtonPressed = Signal.new()
|
||||
self.GroupDetailsButtonPressed = Signal.new()
|
||||
self.wasTouchingBottom = false
|
||||
self.oldConversation = nil
|
||||
|
||||
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
|
||||
self.appState.store:getState().FormFactor)
|
||||
|
||||
setmetatable(self, ConversationView)
|
||||
|
||||
self.rbx = Create.new "TextButton" {
|
||||
Name = "Conversation",
|
||||
Text = "",
|
||||
AutoButtonColor = false,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = Constants.Color.GRAY6,
|
||||
BorderSizePixel = 0,
|
||||
}
|
||||
-- Component Setup
|
||||
local header = HeaderLoader.GetHeader(appState, Intent.Conversation)
|
||||
header:SetDefaultSubtitle()
|
||||
if appState.store:getState().FormFactor == FormFactor.PHONE then
|
||||
header:SetBackButtonEnabled(true)
|
||||
else
|
||||
header:SetBackButtonEnabled(false)
|
||||
end
|
||||
self.header = header
|
||||
|
||||
header.rbx.Parent = self.rbx
|
||||
header.rbx.LayoutOrder = LayoutOrder.HEADER
|
||||
header.rbx.ZIndex = 2 -- Render on top of the conversation (which is a peer)
|
||||
|
||||
local groupDetailsButton
|
||||
groupDetailsButton = PaddedImageButton.new(appState, "GroupDetails",
|
||||
"rbxasset://textures/ui/LuaChat/icons/ic-info.png")
|
||||
header:AddButton(groupDetailsButton)
|
||||
groupDetailsButton.Pressed:connect(function()
|
||||
self.GroupDetailsButtonPressed:fire()
|
||||
end)
|
||||
|
||||
-- Play Together feature gating:
|
||||
if self.luaChatPlayTogetherEnabled then
|
||||
local playTogetherGameIcon = PlayTogetherGameIcon.new(appState, nil, PlayTogetherGameIcon.Size.SMALL)
|
||||
playTogetherGameIcon.Pressed:connect(function()
|
||||
self.header:ToggleGameDrawer()
|
||||
self.chatInputBar.textBox:ReleaseFocus()
|
||||
end)
|
||||
self.playTogetherGameIcon = playTogetherGameIcon
|
||||
|
||||
header:AddButton(playTogetherGameIcon)
|
||||
end
|
||||
|
||||
-- Conversation contents are now in this frame so the drawer can render
|
||||
-- on top of it as necessary. Note the "HeaderSpacer" element which
|
||||
-- copies the size from the real header above it.
|
||||
local contents = Create.new "Frame" {
|
||||
Name = "Contents",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = Constants.Color.GRAY6,
|
||||
BorderSizePixel = 0,
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = "LayoutOrder",
|
||||
},
|
||||
Create.new "Frame" {
|
||||
Name = "HeaderSpacer",
|
||||
Size = header.rbx.Size,
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
},
|
||||
}
|
||||
contents.Parent = self.rbx
|
||||
|
||||
local icebreaker
|
||||
if FFlagLuaChatIcebreaker then
|
||||
icebreaker = Icebreaker.new(self.appState)
|
||||
icebreaker.rbx.Parent = self.rbx.Contents
|
||||
icebreaker.rbx.Visible = false
|
||||
icebreaker.rbx.LayoutOrder = LayoutOrder.INPUT_BAR - 1
|
||||
self.icebreaker = icebreaker
|
||||
icebreaker:PlayFlashAnimation()
|
||||
|
||||
icebreaker.SendButtonPressed:connect(function(text)
|
||||
local messageSentLocalTime = tick()
|
||||
text = sendPreprocess(text)
|
||||
|
||||
if FFlagShareGameToChatStatusAnalytics then
|
||||
self.appState.store:dispatch(ConversationActions.SendMessage(self.conversationId,
|
||||
text, messageSentLocalTime, Constants.Decorators.ICEBREAKER))
|
||||
else
|
||||
self.appState.store:dispatch(ConversationActions.SendMessage(self.conversationId,
|
||||
text, nil, messageSentLocalTime, Constants.Decorators.ICEBREAKER))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local chatInputBar = ChatInputBar.new(appState)
|
||||
|
||||
--These now get initialized in Update, based on conversationId of CurrentRoute in store
|
||||
self.messageList = nil
|
||||
self.messageListConnection = nil
|
||||
self.typingIndicator = nil
|
||||
self.initialLoadingFrame = nil
|
||||
|
||||
chatInputBar.rbx.Parent = contents
|
||||
chatInputBar.rbx.Position = UDim2.new(0, 0, 1, -42)
|
||||
chatInputBar.rbx.LayoutOrder = LayoutOrder.INPUT_BAR
|
||||
self.chatInputBar = chatInputBar
|
||||
|
||||
--Close keyboard when tapping outside of both keyboard and input area
|
||||
--Per spec at: https://confluence.roblox.com/display/SOCIAL/Misc+Notes
|
||||
--This is a bit of a hack, but a tap that focuses self.chatInputBar.textBox
|
||||
--Can also, it seems, be interpreted as a tap of self.rbx
|
||||
--So if the self.chatInputBar.textBox was just focused, I won't release focus
|
||||
--on tap.
|
||||
local lastFocus = nil
|
||||
self.chatInputBar.textBox.Focused:Connect(function()
|
||||
lastFocus = tick()
|
||||
self.header:InputFocus()
|
||||
end)
|
||||
self.rbx.TouchTap:Connect(function()
|
||||
if (not lastFocus) or (tick() - lastFocus) > .3 then
|
||||
self.chatInputBar.textBox:ReleaseFocus()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Component Event Setup
|
||||
header.BackButtonPressed:connect(function()
|
||||
self.BackButtonPressed:fire()
|
||||
end)
|
||||
|
||||
header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:Rescale()
|
||||
end)
|
||||
|
||||
chatInputBar.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:Rescale()
|
||||
end)
|
||||
|
||||
chatInputBar.SendButtonPressed:connect(function(text)
|
||||
local messageSentLocalTime = tick()
|
||||
text = sendPreprocess(text)
|
||||
|
||||
if FFlagShareGameToChatStatusAnalytics then
|
||||
appState.store:dispatch(ConversationActions.SendMessage(self.conversationId, text, messageSentLocalTime))
|
||||
else
|
||||
appState.store:dispatch(ConversationActions.SendMessage(self.conversationId, text, nil, messageSentLocalTime))
|
||||
end
|
||||
end)
|
||||
|
||||
chatInputBar.UserChangedText:connect(function()
|
||||
if tick() - self.lastTypingTimestamp > Constants.Text.POST_TYPING_STATUS_INTERVAL then
|
||||
self.lastTypingTimestamp = tick()
|
||||
WebApi.PostTypingStatus(self.conversationId, true)
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConversationView:Start()
|
||||
self.header:Start()
|
||||
self.header:SetConnectionState(self.appState.store:getState().ConnectionState)
|
||||
|
||||
if self.messageList and self.messageList.isTouchingBottom then
|
||||
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
|
||||
end
|
||||
|
||||
-- initial sizing
|
||||
self:Rescale()
|
||||
|
||||
local propertyChangeSignal = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardVisible")
|
||||
local keyboardVisibleConnection = propertyChangeSignal:Connect(function()
|
||||
self:TweenRescale()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, keyboardVisibleConnection)
|
||||
else
|
||||
table.insert(self.connections, keyboardVisibleConnection)
|
||||
end
|
||||
|
||||
propertyChangeSignal = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardPosition")
|
||||
local keyboardSizeConnection = propertyChangeSignal:Connect(function()
|
||||
self:TweenRescale()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, keyboardSizeConnection)
|
||||
else
|
||||
table.insert(self.connections, keyboardSizeConnection)
|
||||
end
|
||||
propertyChangeSignal = self.rbx:GetPropertyChangedSignal("AbsoluteSize")
|
||||
local absoluteSizeConnection = propertyChangeSignal:Connect(function()
|
||||
self:TweenRescale()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, absoluteSizeConnection)
|
||||
else
|
||||
table.insert(self.connections, absoluteSizeConnection)
|
||||
end
|
||||
|
||||
local statusBarTappedConnection = UserInputService.StatusBarTapped:Connect(function()
|
||||
if self.appState.store:getState().ChatAppReducer.Location.current.intent ~= Intent.Conversation then
|
||||
return
|
||||
end
|
||||
if self.messageList then
|
||||
self.messageList.rbx:ScrollToTop()
|
||||
end
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, statusBarTappedConnection)
|
||||
else
|
||||
table.insert(self.connections, statusBarTappedConnection)
|
||||
end
|
||||
self:Update(self.appState.store:getState())
|
||||
end
|
||||
|
||||
function ConversationView:Stop()
|
||||
self.chatInputBar.textBox:ReleaseFocus()
|
||||
|
||||
if not LuaChatAssetCardsSelfTerminateConnection then
|
||||
if self.messageList then
|
||||
self.messageList:DisconnectChatBubbles()
|
||||
end
|
||||
end
|
||||
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
for _, connection in ipairs(self.rbx_connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
else
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
end
|
||||
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
|
||||
function ConversationView:Pause()
|
||||
self.chatInputBar.textBox:ReleaseFocus()
|
||||
end
|
||||
|
||||
function ConversationView:Resume()
|
||||
if self.messageList.isTouchingBottom then
|
||||
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function ConversationView:Update(state)
|
||||
self.header:SetConnectionState(state.ConnectionState)
|
||||
|
||||
local currentConversationId = state.ChatAppReducer.Location.current.parameters.conversationId
|
||||
|
||||
local conversation = state.ChatAppReducer.Conversations[currentConversationId]
|
||||
|
||||
if LuaChatActiveConversationId then
|
||||
if currentConversationId then
|
||||
local activeConversationId = tostring(currentConversationId)
|
||||
if state.ChatAppReducer.ActiveConversationId ~= activeConversationId then
|
||||
self.appState.store:dispatch(SetActiveConversationId(activeConversationId))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not conversation then
|
||||
return
|
||||
end
|
||||
|
||||
-- The game icon might not exist:
|
||||
if self.playTogetherGameIcon then
|
||||
self.playTogetherGameIcon:Update(conversation)
|
||||
end
|
||||
|
||||
if currentConversationId and currentConversationId ~= self.conversationId then
|
||||
|
||||
self.conversationId = currentConversationId
|
||||
|
||||
self.isFetchingOlderMessages = conversation.fetchingOlderMessages
|
||||
|
||||
self.header:SetTitle(getConversationDisplayTitle(conversation))
|
||||
|
||||
if self.messageList then
|
||||
self.messageList:Destruct()
|
||||
end
|
||||
|
||||
local messageList = MessageList.new(self.appState, conversation)
|
||||
messageList.rbx.LayoutOrder = LayoutOrder.MESSAGE_LIST
|
||||
messageList.rbx.Parent = self.rbx.Contents
|
||||
messageList:ResizeCanvas()
|
||||
self.messageList = messageList
|
||||
|
||||
if self.messageListConnection ~= nil then
|
||||
self.messageListConnection:Disconnect()
|
||||
end
|
||||
self.messageListConnection = self.messageList.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
if self.messageList.isTouchingBottom or self.wasTouchingBottom then
|
||||
self:TweenScrollToBottom()
|
||||
self.wasTouchingBottom = false
|
||||
end
|
||||
end)
|
||||
|
||||
local function onRequestOlderMessages()
|
||||
local conversationModel = self.appState.store:getState().ChatAppReducer.Conversations[self.conversationId]
|
||||
if conversationModel == nil then
|
||||
return
|
||||
end
|
||||
local messages = conversationModel.messages
|
||||
local exclusiveMessageStartId = getNewestWithNilPreviousMessageId(messages)
|
||||
if conversationModel.fetchingOlderMessages or conversationModel.fetchedOldestMessage then
|
||||
return
|
||||
end
|
||||
|
||||
self.messageList:StartLoadingMessageHistoryAnimation()
|
||||
|
||||
self.appState.store:dispatch(ConversationActions.GetOlderMessages(self.conversationId, exclusiveMessageStartId))
|
||||
end
|
||||
if self.requestOlderMessagesConnection then
|
||||
self.requestOlderMessagesConnection:disconnect()
|
||||
end
|
||||
self.requestOlderMessagesConnection = messageList.RequestOlderMessages:connect(onRequestOlderMessages)
|
||||
--Make sure this gets called at least once
|
||||
onRequestOlderMessages()
|
||||
|
||||
if self.readAllMessagesConnection then
|
||||
self.readAllMessagesConnection:disconnect()
|
||||
end
|
||||
self.readAllMessagesConnection = messageList.ReadAllMessages:connect(function()
|
||||
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
|
||||
end)
|
||||
|
||||
if conversation.conversationType == Conversation.Type.ONE_TO_ONE_CONVERSATION then
|
||||
if self.typingIndicator then
|
||||
self.typingIndicator:Destruct()
|
||||
end
|
||||
local typingIndicator = UserTypingIndicator.new(self.appState, conversation)
|
||||
typingIndicator.rbx.LayoutOrder = LayoutOrder.TYPING_INDICATOR
|
||||
typingIndicator.rbx.Parent = self.rbx.Contents
|
||||
self.typingIndicator = typingIndicator
|
||||
|
||||
typingIndicator.Resized:connect(function()
|
||||
self:Rescale()
|
||||
end)
|
||||
end
|
||||
|
||||
if self.initialLoadingFrame then
|
||||
self.initialLoadingFrame:Destroy()
|
||||
end
|
||||
local initialLoadingFrame = Create.new "Frame" {
|
||||
Name = "InitialLoadingFrame",
|
||||
Size = self.messageList.rbx.Size,
|
||||
Position = self.messageList.rbx.Position,
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = LayoutOrder.INITIAL_LOADING_FRAME,
|
||||
Visible = false
|
||||
}
|
||||
initialLoadingFrame.Parent = self.rbx.Contents
|
||||
self.initialLoadingFrame = initialLoadingFrame
|
||||
|
||||
if self.messageList.isTouchingBottom then
|
||||
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
|
||||
end
|
||||
|
||||
if self.luaChatPlayTogetherEnabled then
|
||||
self.header:CreateGameDrawer(self.appState.store, self.conversationId)
|
||||
end
|
||||
|
||||
self:Rescale()
|
||||
elseif conversation == self.oldConversation then
|
||||
return
|
||||
end
|
||||
self.oldConversation = conversation
|
||||
|
||||
if not conversation.fetchingOlderMessages then
|
||||
self.messageList:StopLoadingMessageHistoryAnimation()
|
||||
end
|
||||
|
||||
if conversation.initialLoadingStatus == Constants.ConversationLoadingState.LOADING then
|
||||
self:StartInitialLoadingAnimation()
|
||||
else
|
||||
self:StopInitialLoadingAnimation()
|
||||
end
|
||||
|
||||
self.messageList:Update(conversation)
|
||||
self.header:SetTitle(getConversationDisplayTitle(conversation))
|
||||
|
||||
if LuaChatGroupChatIconEnabled then
|
||||
if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then
|
||||
self.header:SetGroupChatIconVisibility(true)
|
||||
else
|
||||
self.header:SetGroupChatIconVisibility(false)
|
||||
end
|
||||
end
|
||||
|
||||
if self.typingIndicator then
|
||||
self.typingIndicator:Update(conversation)
|
||||
end
|
||||
|
||||
-- Only show icebreaker if there are no sent messages
|
||||
if FFlagLuaChatIcebreaker then
|
||||
if self.icebreaker then
|
||||
if conversation.messages then
|
||||
local hasNotPostedMessage = conversation.messages:Length() < 1
|
||||
local isNotSendingMessage = conversation.sendingMessages:Length() < 1
|
||||
self.icebreaker.rbx.Visible = hasNotPostedMessage and isNotSendingMessage
|
||||
self:Rescale()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationView:GetYOffset()
|
||||
local keyboardSize = 0
|
||||
if UserInputService.OnScreenKeyboardVisible and self.chatInputBar.textBox:IsFocused() then
|
||||
keyboardSize = self.rbx.AbsoluteSize.Y - UserInputService.OnScreenKeyboardPosition.Y
|
||||
end
|
||||
local offset = keyboardSize
|
||||
for _, child in ipairs(self.rbx.Contents:GetChildren()) do
|
||||
if child:IsA("GuiObject") and (self.messageList == nil or child ~= self.messageList.rbx)
|
||||
and child ~= self.initialLoadingFrame and child.Visible then
|
||||
offset = offset + child.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
|
||||
return offset
|
||||
end
|
||||
|
||||
function ConversationView:Rescale()
|
||||
|
||||
if not self.messageList then
|
||||
return
|
||||
end
|
||||
|
||||
local offset = self:GetYOffset()
|
||||
|
||||
local newSize = UDim2.new(1, 0, 1, -offset)
|
||||
|
||||
local wasTouchingBottom = self.messageList.isTouchingBottom
|
||||
self.messageList.rbx.Size = newSize
|
||||
if wasTouchingBottom then
|
||||
self.messageList:ScrollToBottom()
|
||||
end
|
||||
|
||||
self.initialLoadingFrame.Size = newSize
|
||||
end
|
||||
|
||||
function ConversationView:TweenRescale()
|
||||
if self.messageList == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local offset = self:GetYOffset()
|
||||
local newSize = UDim2.new(1, 0, 1, -offset)
|
||||
self.wasTouchingBottom = self.messageList.isTouchingBottom
|
||||
self.initialLoadingFrame.Size = newSize
|
||||
|
||||
local duration = UserInputService.OnScreenKeyboardAnimationDuration
|
||||
local tweenInfo = TweenInfo.new(duration)
|
||||
|
||||
local propertyGoals = {
|
||||
Size = newSize,
|
||||
}
|
||||
local tween = TweenService:Create(self.messageList.rbx, tweenInfo, propertyGoals)
|
||||
tween:Play()
|
||||
end
|
||||
|
||||
function ConversationView:TweenScrollToBottom()
|
||||
local offset = self:GetYOffset()
|
||||
local height = self.messageList.rbx.CanvasSize.Y.Offset - self.messageList.rbx.AbsoluteWindowSize.Y + offset
|
||||
|
||||
local duration = UserInputService.OnScreenKeyboardAnimationDuration
|
||||
local tweenInfo = TweenInfo.new(duration)
|
||||
|
||||
local propertyGoals =
|
||||
{
|
||||
CanvasPosition = Vector2.new(0, height)
|
||||
}
|
||||
local tween = TweenService:Create(self.messageList.rbx, tweenInfo, propertyGoals)
|
||||
tween:Play()
|
||||
end
|
||||
|
||||
function ConversationView:StartInitialLoadingAnimation()
|
||||
if not self.loadingAnimationRunning then
|
||||
self.loadingAnimationRunning = true
|
||||
|
||||
self.messageList.rbx.Visible = false
|
||||
self.initialLoadingFrame.Visible = true
|
||||
|
||||
local loadingIndicator = LoadingIndicator.new(self.appState, 3)
|
||||
loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
loadingIndicator.rbx.Parent = self.initialLoadingFrame
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationView:StopInitialLoadingAnimation()
|
||||
if self.loadingAnimationRunning then
|
||||
self.loadingAnimationRunning = false
|
||||
|
||||
self.messageList.rbx.Visible = true
|
||||
self.initialLoadingFrame.Visible = false
|
||||
|
||||
self.initialLoadingFrame:ClearAllChildren()
|
||||
end
|
||||
end
|
||||
|
||||
return ConversationView
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
local OrderedMap = require(LuaChat.OrderedMap)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
local Message = require(LuaChat.Models.Message)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local ConversationThumbnail = require(Components.ConversationThumbnail)
|
||||
local PlayTogetherGameIcon = require(Components.PlayTogetherGameIcon)
|
||||
|
||||
local UseCppTextTruncation = FlagSettings.UseCppTextTruncation()
|
||||
local FFlagEnableChatMessageType = settings():GetFFlag("EnableChatMessageType")
|
||||
|
||||
local UNREAD_COUNTER_ENABLED = false
|
||||
|
||||
local ConversationEntry = {}
|
||||
|
||||
ConversationEntry.__index = ConversationEntry
|
||||
|
||||
function ConversationEntry.new(appState, conversation)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
self.conversation = nil
|
||||
self.Tapped = Signal.new()
|
||||
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
|
||||
self.appState.store:getState().FormFactor)
|
||||
|
||||
local activeGameIconRbx = nil
|
||||
if self.luaChatPlayTogetherEnabled then
|
||||
self.activeGameIcon = PlayTogetherGameIcon.new(appState, conversation,
|
||||
PlayTogetherGameIcon.Size.LARGE, PlayTogetherGameIcon.Type.ACTIVE)
|
||||
self.activeGameIcon.rbx.AnchorPoint = Vector2.new(1, 0)
|
||||
self.activeGameIcon.rbx.Position = UDim2.new(1, -12, 0, 12)
|
||||
activeGameIconRbx = self.activeGameIcon.rbx
|
||||
|
||||
self.activeGameIcon.Pressed:connect(function()
|
||||
self.Tapped:fire()
|
||||
end)
|
||||
end
|
||||
|
||||
self.thumb = ConversationThumbnail.new(appState, conversation)
|
||||
self.thumb.rbx.Size = UDim2.new(0, 48, 0, 48)
|
||||
self.thumb.rbx.Position = UDim2.new(0, 12, 0, 12)
|
||||
|
||||
self.content = Create.new "TextLabel" {
|
||||
Name = "Content",
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -58, 0, Constants.Font.FONT_SIZE_16),
|
||||
Position = UDim2.new(0, 0, 1, -14),
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
ClipsDescendants = true,
|
||||
}
|
||||
|
||||
self.lastMessageTime = Create.new "TextLabel" {
|
||||
Name = "LastMessageTime",
|
||||
AnchorPoint = Vector2.new(1,0),
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 50, 0, Constants.Font.FONT_SIZE_14),
|
||||
Position = UDim2.new(1, -12, 0, Constants.Font.FONT_SIZE_18_POS_OFFSET + 20),
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
}
|
||||
|
||||
self.title = Create.new "TextLabel" {
|
||||
Name = "Title",
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -90, 0, Constants.Font.FONT_SIZE_18),
|
||||
Position = UDim2.new(0, 0, 0, Constants.Font.FONT_SIZE_18_POS_OFFSET + 20),
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
ClipsDescendants = true,
|
||||
}
|
||||
|
||||
self.unreadMessageIndicator = Create.new "ImageLabel" {
|
||||
Name = "UnreadMessageCount",
|
||||
AnchorPoint = Vector2.new(1,1),
|
||||
BackgroundTransparency = 1,
|
||||
BackgroundColor3 = Color3.fromRGB(226, 35, 26),
|
||||
Size = UDim2.new(0, 24, 0, 16),
|
||||
Position = UDim2.new(1, -12, 1, -14),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/new-message-indicator.png",
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(8, 8, 10, 10),
|
||||
Visible = false,
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Label",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = Constants.Font.FONT_SIZE_12,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = "1",
|
||||
Position = UDim2.new(0, 0, 0, -1),
|
||||
}
|
||||
}
|
||||
|
||||
self.rbx = Create.new "TextButton" {
|
||||
Name = "ConversationEntry",
|
||||
Size = UDim2.new(1, 0, 0, 72),
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
AutoButtonColor = false,
|
||||
BorderSizePixel = 0,
|
||||
Text = "",
|
||||
Font = Enum.Font.SourceSans,
|
||||
|
||||
self.thumb.rbx,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Body",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -72, 1, 0),
|
||||
Position = UDim2.new(0, 72, 0, 0),
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Inner",
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
self.content,
|
||||
self.lastMessageTime,
|
||||
self.title,
|
||||
self.unreadMessageIndicator,
|
||||
activeGameIconRbx,
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "BottomBorder",
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
Position = UDim2.new(0, 0, 1, -1),
|
||||
},
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "ImageContainer",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 72, 0, 72),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
},
|
||||
}
|
||||
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
self.Tapped:fire()
|
||||
end)
|
||||
|
||||
if self.thumb.clicked then
|
||||
self.thumb.clicked:connect(function()
|
||||
self.Tapped:fire()
|
||||
end)
|
||||
end
|
||||
|
||||
setmetatable(self, ConversationEntry)
|
||||
|
||||
self:Update(conversation)
|
||||
|
||||
self.title:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self.title.Text = self.conversation.title
|
||||
if not UseCppTextTruncation then
|
||||
Text.TruncateTextLabel(self.title, "...")
|
||||
end
|
||||
end)
|
||||
|
||||
if self.activeGameIcon then
|
||||
self:AdjustForGameIcon(self.activeGameIcon.rbx.Visible)
|
||||
self.activeGameIcon.rbx:GetPropertyChangedSignal("Visible"):Connect(function()
|
||||
self:AdjustForGameIcon(self.activeGameIcon.rbx.Visible)
|
||||
end)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConversationEntry:AdjustForGameIcon(isIconVisible)
|
||||
if isIconVisible then
|
||||
self.lastMessageTime.Visible = false
|
||||
self.content.Size = UDim2.new(1, -72, 0, 18)
|
||||
else
|
||||
self.lastMessageTime.Visible = true
|
||||
self.content.Size = UDim2.new(1, -58, 0, 18)
|
||||
end
|
||||
if not UseCppTextTruncation then
|
||||
Text.TruncateTextLabel(self.content, "...")
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationEntry:SetBackgroundColor(color3)
|
||||
self.rbx.BackgroundColor3 = color3
|
||||
if self.thumb.rbx:FindFirstChild("Mask") then
|
||||
self.thumb.rbx.Mask.ImageColor3 = color3
|
||||
elseif self.thumb.rbx:FindFirstChild("Overlay") then
|
||||
self.thumb.rbx.Overlay.ImageColor3 = color3
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationEntry:Update(conversation)
|
||||
|
||||
local state = self.appState.store:getState()
|
||||
if state.FormFactor == FormFactor.TABLET then
|
||||
local currentLocationParameters = state.ChatAppReducer.Location.current.parameters
|
||||
local currentConversationId = currentLocationParameters and currentLocationParameters.conversationId or nil
|
||||
if currentConversationId == conversation.id then
|
||||
self:SetBackgroundColor(Constants.Color.GRAY5)
|
||||
else
|
||||
self:SetBackgroundColor(Constants.Color.WHITE)
|
||||
end
|
||||
end
|
||||
|
||||
if conversation == self.conversation then
|
||||
return
|
||||
end
|
||||
|
||||
local oldConversationState = self.conversation
|
||||
self.conversation = conversation
|
||||
local lastMessageId = conversation.messages.keys[#conversation.messages.keys]
|
||||
local lastMessage = conversation.messages.values[lastMessageId]
|
||||
local lastMessageText = ""
|
||||
local lastMessageTime = ""
|
||||
|
||||
if lastMessage then
|
||||
if FFlagEnableChatMessageType then
|
||||
if lastMessage.messageType == Message.MessageTypes.PlainText then
|
||||
lastMessageText = Text.RightTrim(lastMessage.content)
|
||||
else
|
||||
lastMessageText = ""
|
||||
end
|
||||
else
|
||||
lastMessageText = Text.RightTrim(lastMessage.content)
|
||||
end
|
||||
lastMessageText = lastMessageText:gsub("%s", " ")
|
||||
lastMessageTime = conversation.lastUpdated:GetShortRelativeTime()
|
||||
elseif conversation.lastUpdated then
|
||||
lastMessageTime = conversation.lastUpdated:GetShortRelativeTime()
|
||||
end
|
||||
|
||||
local textColor = conversation.hasUnreadMessages and Constants.Color.GRAY1 or Constants.Color.GRAY2
|
||||
|
||||
self.content.Font = conversation.hasUnreadMessages and Constants.Font.TITLE or Enum.Font.SourceSans
|
||||
self.content.Text = lastMessageText
|
||||
|
||||
self.content.TextColor3 = textColor
|
||||
self.lastMessageTime.TextColor3 = textColor
|
||||
|
||||
self.lastMessageTime.Text = lastMessageTime
|
||||
|
||||
self.title.Text = getConversationDisplayTitle(conversation)
|
||||
if not UseCppTextTruncation then
|
||||
Text.TruncateTextLabel(self.title, "...")
|
||||
end
|
||||
|
||||
self.thumb:Update(conversation)
|
||||
if self.activeGameIcon then
|
||||
self.activeGameIcon:Update(conversation)
|
||||
self:AdjustForGameIcon(self.activeGameIcon.rbx.Visible)
|
||||
end
|
||||
|
||||
if UNREAD_COUNTER_ENABLED then
|
||||
self.unreadMessageIndicator.Visible = conversation.hasUnreadMessages
|
||||
if conversation.hasUnreadMessages and
|
||||
(not oldConversationState or oldConversationState.messages ~= conversation.messages) then
|
||||
local numUnread = 0
|
||||
for _, message in OrderedMap.CreateIterator(conversation.messages) do
|
||||
if not message.read then
|
||||
numUnread = numUnread + 1
|
||||
end
|
||||
end
|
||||
self.unreadMessageIndicator.Label.Text = tostring(numUnread)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ConversationEntry
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
|
||||
return function()
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local MessageModel = require(LuaChat.Models.Message)
|
||||
local ConversationModel = require(LuaChat.Models.Conversation)
|
||||
local ConversationEntry = require(LuaChat.Components.ConversationEntry)
|
||||
local AppState = require(LuaChat.AppState)
|
||||
local OrderedMap = require(LuaChat.OrderedMap)
|
||||
|
||||
local FFlagEnableChatMessageType = settings():GetFFlag("EnableChatMessageType")
|
||||
|
||||
describe("Conversation entry text", function()
|
||||
local function createConvEntry (appState, messageType)
|
||||
local message = MessageModel.mock({
|
||||
content = "testing",
|
||||
messageType = messageType
|
||||
})
|
||||
|
||||
local conversationModel = ConversationModel.mock()
|
||||
conversationModel.messages = OrderedMap.Insert(conversationModel.messages, unpack({ message }))
|
||||
expect(conversationModel).to.be.ok()
|
||||
return ConversationEntry.new(appState, conversationModel)
|
||||
end
|
||||
|
||||
it("should create ConversationEntry when presented with raw text", function()
|
||||
local appState = AppState.mock()
|
||||
|
||||
local convEntry
|
||||
if FFlagEnableChatMessageType then
|
||||
convEntry = createConvEntry(appState, MessageModel.MessageTypes.PlainText)
|
||||
else
|
||||
convEntry = createConvEntry(appState, "")
|
||||
end
|
||||
|
||||
expect(convEntry).to.be.ok()
|
||||
expect(convEntry.content).to.be.ok()
|
||||
expect(convEntry.content.Text).to.be.ok()
|
||||
expect(convEntry.content.Text).to.equal("testing")
|
||||
end)
|
||||
|
||||
if FFlagEnableChatMessageType then
|
||||
it("should create ConversationEntry with placeholder text if unknown message type is used", function()
|
||||
local appState = AppState.mock()
|
||||
|
||||
local convEntry = createConvEntry(appState, "SomeUnknownMessageTypeThatWillNeverExistInProduction")
|
||||
|
||||
expect(convEntry).to.be.ok()
|
||||
expect(convEntry.content).to.be.ok()
|
||||
expect(convEntry.content.Text).to.be.ok()
|
||||
expect(convEntry.content.Text).to.equal("")
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,452 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local LuaAppConstants = require(LuaApp.Constants)
|
||||
local Signal = require(Common.Signal)
|
||||
local NotificationType = require(LuaApp.Enum.NotificationType)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local ChatDisabledIndicator = require(Components.ChatDisabledIndicator)
|
||||
local ChatLoadingIndicator = require(Components.ChatLoadingIndicator)
|
||||
local ConversationList = require(Components.ConversationList)
|
||||
local ConversationSearchBox = require(Components.ConversationSearchBox)
|
||||
local ConversationEntry = require(Components.ConversationEntry)
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
local NoFriendsIndicator = require(Components.NoFriendsIndicator)
|
||||
local PaddedImageButton = require(Components.PaddedImageButton)
|
||||
local TokenRefreshComponent = require(LuaApp.Components.TokenRefreshComponent)
|
||||
|
||||
local ApiFetchSortTokens = require(LuaApp.Thunks.ApiFetchSortTokens)
|
||||
local ConversationActions = require(LuaChat.Actions.ConversationActions)
|
||||
local GetFriendCount = require(LuaChat.Actions.GetFriendCount)
|
||||
local SetActiveConversationId = require(LuaChat.Actions.SetActiveConversationId)
|
||||
local SetAppLoaded = require(LuaChat.Actions.SetAppLoaded)
|
||||
|
||||
local Roact = require(Common.Roact)
|
||||
local RoactRodux = require(Common.RoactRodux)
|
||||
|
||||
local appStageLoaded = require(LuaApp.Analytics.Events.appStageLoaded)
|
||||
local FetchChatData = require(LuaApp.Thunks.FetchChatData)
|
||||
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
|
||||
|
||||
local CREATE_CHAT_IMAGE = "rbxasset://textures/ui/LuaChat/icons/ic-createchat1-24x24.png"
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local ConversationHub = {}
|
||||
|
||||
ConversationHub.__index = ConversationHub
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local LuaChatNotificationButtonEnabled = settings():GetFFlag("LuaChatNotificationButtonEnabled")
|
||||
local LuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
|
||||
local LuaChatActiveConversationId = settings():GetFFlag("LuaChatActiveConversationId")
|
||||
local LuaChatCheckIsChatEnabled = settings():GetFFlag("LuaChatCheckIsChatEnabled")
|
||||
|
||||
local FFlagLuaChatCheckWasUsedRecently = settings():GetFFlag("LuaChatCheckWasUsedRecently")
|
||||
|
||||
local FetchChatSettings
|
||||
local FetchChatEnabled
|
||||
if FFlagLuaChatCheckWasUsedRecently then
|
||||
FetchChatSettings = require(LuaChat.Actions.FetchChatSettings)
|
||||
else
|
||||
FetchChatEnabled = require(LuaChat.Actions.FetchChatEnabled)
|
||||
end
|
||||
|
||||
local function requestOlderConversations(appState)
|
||||
-- Don't fetch older conversations if the oldest conversation has already been fetched.
|
||||
if appState.store:getState().ChatAppReducer.ConversationsAsync.oldestConversationIsFetched then
|
||||
return
|
||||
end
|
||||
|
||||
-- Don't fetch older conversations if the oldest conversation is fetched.
|
||||
if appState.store:getState().ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then
|
||||
return
|
||||
end
|
||||
|
||||
-- Ask for new conversations
|
||||
local convoCount = 0
|
||||
for _, _ in pairs(appState.store:getState().ChatAppReducer.Conversations) do
|
||||
convoCount = convoCount + 1
|
||||
end
|
||||
local pageSize = Constants.PageSize.GET_CONVERSATIONS
|
||||
local currentPage = math.floor(convoCount / pageSize)
|
||||
spawn(function()
|
||||
appState.store:dispatch(ConversationActions.GetLocalUserConversationsAsync(currentPage + 1, pageSize))
|
||||
end)
|
||||
end
|
||||
|
||||
local function refreshChatData(appState)
|
||||
local function refreshChatDataImpl()
|
||||
appState.store:dispatch(FetchChatData(function(chatEnabled)
|
||||
if chatEnabled and LuaChatShareGameToChatFromChatV2 then
|
||||
appState.store:dispatch(ApiFetchSortTokens(appState.request, LuaAppConstants.GameSortGroups.ChatGames))
|
||||
end
|
||||
end))
|
||||
end
|
||||
|
||||
if FFlagLuaChatCheckWasUsedRecently then
|
||||
local chatSettingsRetrievalStatus = appState.store:getState().ChatAppReducer.ChatSettings.retrievalStatus
|
||||
if chatSettingsRetrievalStatus ~= RetrievalStatus.Fetching then
|
||||
refreshChatDataImpl()
|
||||
end
|
||||
else
|
||||
spawn(refreshChatDataImpl)
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationHub.new(appState)
|
||||
local self = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
setmetatable(self, ConversationHub)
|
||||
|
||||
if LuaChatCheckIsChatEnabled then
|
||||
local state = appState.store:getState()
|
||||
|
||||
-- Only refresh here if we're not loaded:
|
||||
if not state.ChatAppReducer.AppLoaded then
|
||||
refreshChatData(appState)
|
||||
else
|
||||
local chatEnabled = FFlagLuaChatCheckWasUsedRecently and
|
||||
state.ChatAppReducer.ChatSettings.chatEnabled or state.ChatAppReducer.ChatEnabled
|
||||
|
||||
if chatEnabled and LuaChatShareGameToChatFromChatV2 then
|
||||
appState.store:dispatch(ApiFetchSortTokens(appState.request, LuaAppConstants.GameSortGroups.ChatGames))
|
||||
end
|
||||
end
|
||||
else
|
||||
spawn(function()
|
||||
if FFlagLuaChatCheckWasUsedRecently then
|
||||
appState.store:dispatch(FetchChatSettings())
|
||||
else
|
||||
appState.store:dispatch(FetchChatEnabled())
|
||||
end
|
||||
appState.store:dispatch(ConversationActions.GetUnreadConversationCountAsync())
|
||||
appState.store:dispatch(GetFriendCount())
|
||||
appState.store:dispatch(
|
||||
ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS)
|
||||
):andThen(function()
|
||||
appState.store:dispatch(SetAppLoaded(true))
|
||||
end)
|
||||
if LuaChatShareGameToChatFromChatV2 then
|
||||
appState.store:dispatch(ApiFetchSortTokens(appState.request, LuaAppConstants.GameSortGroups.ChatGames))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
self.appState = appState
|
||||
self._analytics = appState.analytics
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "ConversationHub",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}
|
||||
}
|
||||
|
||||
self.ConversationTapped = Signal.new()
|
||||
self.CreateChatButtonPressed = Signal.new()
|
||||
self.isSearchOpen = false
|
||||
|
||||
local header = HeaderLoader.GetHeader(appState, Intent.ConversationHub)
|
||||
header:SetTitle(appState.localization:Format("CommonUI.Features.Label.Chat"))
|
||||
header:SetDefaultSubtitle()
|
||||
|
||||
header.rbx.Parent = self.rbx
|
||||
header.rbx.LayoutOrder = 0
|
||||
self.header = header
|
||||
|
||||
local createChatButton = PaddedImageButton.new(self.appState, "CreateChat", CREATE_CHAT_IMAGE)
|
||||
|
||||
createChatButton:SetVisible(false)
|
||||
createChatButton.Pressed:connect(function()
|
||||
self.CreateChatButtonPressed:fire()
|
||||
end)
|
||||
|
||||
self.createChatButton = createChatButton
|
||||
|
||||
local searchConversationsButton = PaddedImageButton.new(self.appState,
|
||||
"SearchConversations", "rbxasset://textures/ui/LuaChat/icons/ic-search.png")
|
||||
|
||||
header:AddButton(searchConversationsButton)
|
||||
header:AddButton(createChatButton)
|
||||
|
||||
if LuaChatNotificationButtonEnabled then
|
||||
local notificationButton = PaddedImageButton.new(self.appState, "Notification",
|
||||
"rbxasset://textures/Icon_Stream_Off.png")
|
||||
notificationButton.Pressed:connect(function()
|
||||
GuiService:BroadcastNotification("", NotificationType.VIEW_NOTIFICATIONS)
|
||||
end)
|
||||
header:AddButton(notificationButton)
|
||||
end
|
||||
|
||||
local searchHeader = HeaderLoader.GetHeader(appState, Intent.ConversationHub)
|
||||
searchHeader:SetTitle("")
|
||||
searchHeader:SetSubtitle("")
|
||||
searchHeader.rbx.LayoutOrder = 1
|
||||
|
||||
local conversationSearchBox = ConversationSearchBox.new(self.appState)
|
||||
searchHeader:AddContent(conversationSearchBox)
|
||||
|
||||
local noFriendsIndicator = NoFriendsIndicator.new(appState)
|
||||
self.noFriendsIndicator = noFriendsIndicator
|
||||
noFriendsIndicator.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset)
|
||||
noFriendsIndicator.rbx.Parent = self.rbx
|
||||
noFriendsIndicator.rbx.LayoutOrder = 2
|
||||
|
||||
local chatDisabledIndicator = ChatDisabledIndicator.new(appState)
|
||||
self.chatDisabledIndicator = chatDisabledIndicator
|
||||
chatDisabledIndicator.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset)
|
||||
chatDisabledIndicator.rbx.Parent = self.rbx
|
||||
chatDisabledIndicator.rbx.LayoutOrder = 2
|
||||
|
||||
local chatLoadingIndicator = ChatLoadingIndicator.new(appState)
|
||||
self.chatLoadingIndicator = chatLoadingIndicator
|
||||
chatLoadingIndicator.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset)
|
||||
chatLoadingIndicator.rbx.Parent = self.rbx
|
||||
chatLoadingIndicator.rbx.LayoutOrder = 2
|
||||
|
||||
chatDisabledIndicator.openPrivacySettings:connect(function()
|
||||
GuiService:BroadcastNotification("", NotificationType.PRIVACY_SETTINGS)
|
||||
end)
|
||||
|
||||
local list = ConversationList.new(appState, appState.store:getState().ChatAppReducer.Conversations, ConversationEntry)
|
||||
self.list = list
|
||||
list.rbx.Size = UDim2.new(1, 0, 1, -header.rbx.Size.Y.Offset)
|
||||
list.rbx.Parent = self.rbx
|
||||
list.rbx.LayoutOrder = 2
|
||||
|
||||
list.ConversationTapped:connect(function(convoId)
|
||||
conversationSearchBox:Cancel()
|
||||
if not LuaChatActiveConversationId then
|
||||
appState.store:dispatch(SetActiveConversationId(convoId))
|
||||
end
|
||||
self.ConversationTapped:fire(convoId)
|
||||
end)
|
||||
|
||||
list.RequestOlderConversations:connect(function()
|
||||
requestOlderConversations(appState)
|
||||
end)
|
||||
|
||||
searchConversationsButton.Pressed:connect(function()
|
||||
self.rbx.Position = UDim2.new(0, 0, 0, -header.rbx.AbsoluteSize.Y)
|
||||
searchHeader.rbx.Parent = self.rbx
|
||||
self.rbx.BackgroundColor3 = Constants.Color.GRAY5
|
||||
list:SetFilterPredicate(conversationSearchBox.SearchFilterPredicate)
|
||||
conversationSearchBox.rbx.SearchBoxContainer.SearchBoxBackground.Search:CaptureFocus()
|
||||
self.isSearchOpen = true
|
||||
end)
|
||||
|
||||
conversationSearchBox.SearchChanged:connect(function()
|
||||
list:SetFilterPredicate(conversationSearchBox.SearchFilterPredicate)
|
||||
self:getOlderConversationsForSearchIfNecessary()
|
||||
end)
|
||||
|
||||
conversationSearchBox.Closed:connect(function()
|
||||
searchHeader.rbx.Parent = nil
|
||||
self.rbx.Position = UDim2.new(0, 0, 0, 0)
|
||||
self.rbx.BackgroundColor3 = Constants.Color.WHITE
|
||||
list:SetFilterPredicate(nil)
|
||||
self.isSearchOpen = false
|
||||
end)
|
||||
|
||||
if LuaChatShareGameToChatFromChatV2 then
|
||||
self.tokenRefreshComponent = Roact.mount(Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = appState.store,
|
||||
}, {
|
||||
TokenRefreshComponent = Roact.createElement(TokenRefreshComponent, {
|
||||
sortToRefresh = LuaAppConstants.GameSortGroups.ChatGames,
|
||||
}),
|
||||
}), self.rbx, "TokenRefreshComponent")
|
||||
end
|
||||
|
||||
appState.store.changed:connect(function(state, oldState)
|
||||
self:Update(state, oldState)
|
||||
|
||||
if state.ChatAppReducer.Conversations ~= oldState.ChatAppReducer.Conversations
|
||||
or state.ChatAppReducer.Location.current ~= oldState.ChatAppReducer.Location.current then
|
||||
list:Update(state, oldState)
|
||||
end
|
||||
end)
|
||||
|
||||
local state = appState.store:getState()
|
||||
self:Update(state, state)
|
||||
|
||||
local appRoutes = state.Navigation.history
|
||||
local currentRoute = appRoutes[#appRoutes]
|
||||
local currentSection = currentRoute[1]
|
||||
appStageLoaded(self._analytics.EventStream, currentSection.name, "chatRender")
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConversationHub:Start()
|
||||
local inputServiceConnection = UserInputService:GetPropertyChangedSignal('OnScreenKeyboardVisible'):Connect(function()
|
||||
self:TweenRescale()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, inputServiceConnection)
|
||||
else
|
||||
table.insert(self.connections, inputServiceConnection)
|
||||
end
|
||||
|
||||
local statusBarTappedConnection = UserInputService.StatusBarTapped:Connect(function()
|
||||
if self.appState.store:getState().ChatAppReducer.Location.current.intent ~= Intent.ConversationHub then
|
||||
return
|
||||
end
|
||||
self.list.rbx:ScrollToTop()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, statusBarTappedConnection)
|
||||
else
|
||||
table.insert(self.connections, statusBarTappedConnection)
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationHub:Stop()
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
for _, connection in ipairs(self.rbx_connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
if LuaChatShareGameToChatFromChatV2 then
|
||||
Roact.unmount(self.tokenRefreshComponent)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function ConversationHub:Update(state, oldState)
|
||||
self.header:SetConnectionState(state.ConnectionState)
|
||||
|
||||
local conversations = state.ChatAppReducer.Conversations
|
||||
local appLoaded = state.ChatAppReducer.AppLoaded
|
||||
|
||||
local haveConversations = next(conversations) ~= nil
|
||||
|
||||
local chatEnabled = FFlagLuaChatCheckWasUsedRecently and
|
||||
state.ChatAppReducer.ChatSettings.chatEnabled or state.ChatAppReducer.ChatEnabled
|
||||
|
||||
if chatEnabled then
|
||||
self.chatDisabledIndicator.rbx.Visible = false
|
||||
|
||||
local oldChatEnabled = FFlagLuaChatCheckWasUsedRecently and
|
||||
oldState.ChatAppReducer.ChatSettings.chatEnabled or oldState.ChatAppReducer.ChatEnabled
|
||||
|
||||
if chatEnabled ~= oldChatEnabled then
|
||||
if LuaChatCheckIsChatEnabled then
|
||||
refreshChatData(self.appState)
|
||||
else
|
||||
spawn(function()
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS)
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.chatDisabledIndicator.rbx.Visible = true
|
||||
self.list.rbx.Visible = false
|
||||
self.noFriendsIndicator.rbx.Visible = false
|
||||
self.chatLoadingIndicator:SetVisible(false)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if appLoaded then
|
||||
self.chatLoadingIndicator:SetVisible(false)
|
||||
else
|
||||
self.chatLoadingIndicator:SetVisible(true)
|
||||
self.list.rbx.Visible = false
|
||||
self.noFriendsIndicator.rbx.Visible = false
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if haveConversations then
|
||||
self.list.rbx.Visible = true
|
||||
self.noFriendsIndicator.rbx.Visible = false
|
||||
else
|
||||
self.list.rbx.Visible = false
|
||||
self.noFriendsIndicator.rbx.Visible = true
|
||||
end
|
||||
|
||||
if state.FriendCount < Constants.MIN_PARTICIPANT_COUNT then
|
||||
self.createChatButton:SetVisible(false)
|
||||
else
|
||||
self.createChatButton:SetVisible(true)
|
||||
end
|
||||
|
||||
if state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching
|
||||
~= oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then
|
||||
self.list:Update(state, oldState)
|
||||
self:getOlderConversationsForSearchIfNecessary()
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationHub:getOlderConversationsForSearchIfNecessary(appState)
|
||||
-- To Check:
|
||||
-- 1) Search is open
|
||||
-- 2) Not have loaded all oldest conversations
|
||||
-- 3) Not currently getting conversations
|
||||
-- 4) Has enough items to show
|
||||
-- Note that we already try to load more conversations if we scroll down to the bottom of the list
|
||||
local state = self.appState.store:getState()
|
||||
if not self.isSearchOpen
|
||||
or state.ChatAppReducer.ConversationsAsync.oldestConversationIsFetched
|
||||
or state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then
|
||||
return
|
||||
end
|
||||
|
||||
if self.list.rbx.CanvasSize.Y.Offset > self.list.rbx.AbsoluteSize.Y then
|
||||
return
|
||||
end
|
||||
|
||||
requestOlderConversations(self.appState)
|
||||
end
|
||||
|
||||
function ConversationHub:TweenRescale()
|
||||
local keyboardSize = 0
|
||||
if UserInputService.OnScreenKeyboardVisible then
|
||||
keyboardSize = self.rbx.AbsoluteSize.Y - UserInputService.OnScreenKeyboardPosition.Y
|
||||
end
|
||||
local newSize = UDim2.new(1, 0, 1, -(self.header.rbx.Size.Y.Offset + keyboardSize))
|
||||
|
||||
local duration = UserInputService.OnScreenKeyboardAnimationDuration
|
||||
local tweenInfo = TweenInfo.new(duration)
|
||||
|
||||
local propertyGoals = {
|
||||
Size = newSize
|
||||
}
|
||||
local tween = TweenService:Create(self.list.rbx, tweenInfo, propertyGoals)
|
||||
|
||||
tween:Play()
|
||||
end
|
||||
|
||||
return ConversationHub
|
||||
@@ -0,0 +1,418 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local PlayTogetherActions = require(LuaChat.Actions.PlayTogetherActions)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local LoadingIndicator = require(Components.LoadingIndicator)
|
||||
|
||||
local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle)
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
|
||||
local ConversationList = {}
|
||||
|
||||
local function conversationSortPredicate(a, b)
|
||||
--For conversations that faked based on friend relations,
|
||||
--for now there is no meaningful lastUpdated value to give them,
|
||||
--and this property is set to nil, so the sort predicate needs to
|
||||
--be able to handle that.
|
||||
if a.lastUpdated ~= nil and b.lastUpdated ~= nil then
|
||||
return a.lastUpdated:GetUnixTimestamp() > b.lastUpdated:GetUnixTimestamp()
|
||||
elseif a.lastUpdated ~= nil then
|
||||
return true
|
||||
elseif b.lastUpdated ~= nil then
|
||||
return false
|
||||
else
|
||||
return getConversationDisplayTitle(a) < getConversationDisplayTitle(b)
|
||||
end
|
||||
end
|
||||
|
||||
local function conversationEntrySortPredicate(a, b)
|
||||
--For conversations that faked based on friend relations,
|
||||
--for now there is no meaningful lastUpdated value to give them,
|
||||
--and this property is set to nil, so the sort predicate needs to
|
||||
--be able to handle that.
|
||||
if a.conversation.lastUpdated ~= nil and b.conversation.lastUpdated ~= nil then
|
||||
return a.conversation.lastUpdated:GetUnixTimestamp() > b.conversation.lastUpdated:GetUnixTimestamp()
|
||||
elseif a.conversation.lastUpdated ~= nil then
|
||||
return true
|
||||
elseif b.conversation.lastUpdated ~= nil then
|
||||
return false
|
||||
else
|
||||
return getConversationDisplayTitle(a.conversation) < getConversationDisplayTitle(b.conversation)
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationList.new(appState, conversations, entryCard)
|
||||
local self = {}
|
||||
self.appState = appState
|
||||
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
self.connections = {}
|
||||
self.conversations = {}
|
||||
self.conversationEntries = {}
|
||||
self.conversationUsers = {}
|
||||
self.isTouchingBottom = false
|
||||
self.RequestOlderConversations = Signal.new()
|
||||
self.noSearchResultsFound = false
|
||||
self.sortWithConversationEntry = false
|
||||
self.filterPredicate = nil
|
||||
|
||||
self.ConversationTapped = Signal.new()
|
||||
self.lastTappedConversationEntry = nil
|
||||
self._oldState = nil
|
||||
self.entryCard = entryCard
|
||||
|
||||
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
|
||||
self.appState.store:getState().FormFactor)
|
||||
|
||||
self.rbx = Create.new "ScrollingFrame" {
|
||||
Name = "ConversationList",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ScrollBarThickness = 5,
|
||||
ElasticBehavior = "Always",
|
||||
ScrollingDirection = "Y",
|
||||
BottomImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
MidImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
TopImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder
|
||||
}
|
||||
}
|
||||
|
||||
self.convoLoadingIndicatorFrame = Create.new "Frame" {
|
||||
Name = "LoadingIndicatorFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 72),
|
||||
Visible = false
|
||||
}
|
||||
self.convoLoadingIndicatorFrame.Parent = self.rbx
|
||||
|
||||
local canvasSizeConnection = self.rbx:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
|
||||
local canvasSizeYOffset = self.rbx.CanvasSize.Y.Offset
|
||||
if self.convoLoadingIndicatorFrame.Visible then
|
||||
canvasSizeYOffset = canvasSizeYOffset - self.convoLoadingIndicatorFrame.Size.Y.Offset
|
||||
end
|
||||
|
||||
if not self.isTouchingBottom and
|
||||
self.rbx.CanvasPosition.Y + self.rbx.AbsoluteSize.Y >= canvasSizeYOffset then
|
||||
self.isTouchingBottom = true
|
||||
self.RequestOlderConversations:fire()
|
||||
elseif self.rbx.CanvasPosition.Y + self.rbx.AbsoluteSize.Y < canvasSizeYOffset then
|
||||
self.isTouchingBottom = false
|
||||
end
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, canvasSizeConnection)
|
||||
else
|
||||
table.insert(self.connections, canvasSizeConnection)
|
||||
end
|
||||
|
||||
self.noResultsFrame = Create.new "Frame" {
|
||||
Name = "NoResultsFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 72),
|
||||
Visible = false,
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Content",
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
Font = Enum.Font.SourceSans,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 18),
|
||||
Position = UDim2.new(0, 0, 1, -18),
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY3,
|
||||
Text = appState.localization:Format("Feature.GameLeaderboard.Label.NoResults"),
|
||||
}
|
||||
}
|
||||
self.noResultsFrame.Parent = self.rbx
|
||||
|
||||
local appStateConnection = appState.store.changed:connect(function(state, oldState)
|
||||
if not oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching
|
||||
and state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then
|
||||
self:StartFetchingConversationsAnimation()
|
||||
elseif oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching
|
||||
and not state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then
|
||||
self:StopFetchingConversationsAnimation()
|
||||
end
|
||||
self:CheckToShowNoSearchResults()
|
||||
end)
|
||||
table.insert(self.connections, appStateConnection)
|
||||
|
||||
setmetatable(self, ConversationList)
|
||||
|
||||
if conversations then
|
||||
self:Update(self.appState.store:getState(), nil)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConversationList:Update(current, previous)
|
||||
local users = current.Users
|
||||
local conversations = current.ChatAppReducer.Conversations
|
||||
|
||||
for _, conversation in pairs(conversations) do
|
||||
local existing = self.conversations[conversation.id]
|
||||
local existingEntry = self.conversationEntries[conversation.id]
|
||||
local userCache = self.conversationUsers[conversation.id]
|
||||
|
||||
local doUpdate = false
|
||||
if previous == nil then
|
||||
doUpdate = true
|
||||
elseif conversation ~= existing
|
||||
or current.ChatAppReducer.Location.current ~= previous.ChatAppReducer.Location.current then
|
||||
doUpdate = true
|
||||
else
|
||||
if userCache then
|
||||
for _, id in ipairs(conversation.participants) do
|
||||
if userCache[id] ~= users[id] then
|
||||
doUpdate = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
doUpdate = true
|
||||
end
|
||||
end
|
||||
|
||||
if not userCache then
|
||||
userCache = {}
|
||||
self.conversationUsers[conversation.id] = userCache
|
||||
end
|
||||
|
||||
if doUpdate then
|
||||
if existingEntry then
|
||||
existingEntry:Update(conversation)
|
||||
else
|
||||
local entry = self.entryCard.new(self.appState, conversation)
|
||||
entry.rbx.Parent = self.rbx
|
||||
local tappedConnection = entry.Tapped:connect(function()
|
||||
self.ConversationTapped:fire(conversation.id)
|
||||
end)
|
||||
table.insert(self.connections, tappedConnection)
|
||||
|
||||
self.conversationEntries[conversation.id] = entry
|
||||
end
|
||||
|
||||
self.conversations[conversation.id] = conversation
|
||||
|
||||
-- TODO: May not correctly handle users leaving?
|
||||
for _, id in ipairs(conversation.participants) do
|
||||
userCache[id] = users[id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local toDelete = {}
|
||||
for _, conversation in pairs(self.conversations) do
|
||||
local hasBeenRemoved = conversations[conversation.id] == nil
|
||||
if hasBeenRemoved then
|
||||
table.insert(toDelete, conversation.id)
|
||||
end
|
||||
end
|
||||
for _, id in ipairs(toDelete) do
|
||||
local entry = self.conversationEntries[id]
|
||||
entry.rbx:Destroy()
|
||||
self.conversationEntries[id] = nil
|
||||
self.conversations[id] = nil
|
||||
end
|
||||
|
||||
self:Filter()
|
||||
self:Sort()
|
||||
self:ResizeCanvas()
|
||||
if self.luaChatPlayTogetherEnabled then
|
||||
self:FetchMostRecentlyPlayedGames()
|
||||
end
|
||||
|
||||
self._oldState = current
|
||||
end
|
||||
|
||||
function ConversationList:FetchMostRecentlyPlayedGames()
|
||||
local state = self.appState.store:getState()
|
||||
if state.ChatAppReducer.PlayTogetherAsync.fetchedMostRecentlyPlayedGames then
|
||||
self:GetMostRecentlyPlayedPlayableGame()
|
||||
return
|
||||
end
|
||||
|
||||
if self.luaChatPlayTogetherEnabled then
|
||||
self.appState.store:dispatch(PlayTogetherActions.GetMostRecentlyPlayedGames())
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationList:GetMostRecentlyPlayedPlayableGame()
|
||||
local state = self.appState.store:getState()
|
||||
if state.ChatAppReducer.MostRecentlyPlayedGames.setPlayableGame then
|
||||
return
|
||||
end
|
||||
|
||||
self.appState.store:dispatch(PlayTogetherActions.GetMostRecentlyPlayedPlayableGame())
|
||||
end
|
||||
|
||||
function ConversationList:SetFilterPredicate(filterPredicate)
|
||||
self.filterPredicate = filterPredicate
|
||||
|
||||
local state = self.appState.store:getState()
|
||||
self:Update(state, self._oldState or state)
|
||||
end
|
||||
|
||||
function ConversationList:Filter()
|
||||
local conversationCount = 0
|
||||
local visibleCount = 0
|
||||
|
||||
for _, conversationEntry in pairs(self.conversationEntries) do
|
||||
local visible
|
||||
local conversation = conversationEntry.conversation
|
||||
local filterPredicate = self.filterPredicate
|
||||
if filterPredicate and conversation then
|
||||
visible = filterPredicate(getConversationDisplayTitle(conversation))
|
||||
else
|
||||
visible = true
|
||||
end
|
||||
|
||||
conversationEntry.rbx.Visible = visible
|
||||
conversationCount = conversationCount + 1
|
||||
if visible then
|
||||
visibleCount = visibleCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
self.noSearchResultsFound = ((conversationCount > 0) and (visibleCount == 0))
|
||||
self:CheckToShowNoSearchResults()
|
||||
end
|
||||
|
||||
function ConversationList:SetSortWithConversationEntry(value)
|
||||
self.sortWithConversationEntry = value
|
||||
end
|
||||
|
||||
function ConversationList:Sort()
|
||||
if self.sortWithConversationEntry then
|
||||
self:SortWithConversationEntry()
|
||||
else
|
||||
self:SortWithConversation()
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationList:SortWithConversation()
|
||||
local sorted = {}
|
||||
for _, conversation in pairs(self.conversations) do
|
||||
table.insert(sorted, conversation)
|
||||
end
|
||||
|
||||
table.sort(sorted, conversationSortPredicate)
|
||||
|
||||
for key, conversation in ipairs(sorted) do
|
||||
local entry = self.conversationEntries[conversation.id]
|
||||
entry.rbx.LayoutOrder = key
|
||||
end
|
||||
|
||||
self.convoLoadingIndicatorFrame.LayoutOrder = #sorted + 1
|
||||
end
|
||||
|
||||
function ConversationList:SortWithConversationEntry()
|
||||
local sorted = {}
|
||||
for _, entry in pairs(self.conversationEntries) do
|
||||
table.insert(sorted, entry)
|
||||
end
|
||||
|
||||
table.sort(sorted, conversationEntrySortPredicate)
|
||||
|
||||
for key, conversationEntry in ipairs(sorted) do
|
||||
local entry = self.conversationEntries[conversationEntry.conversation.id]
|
||||
entry.rbx.LayoutOrder = key
|
||||
end
|
||||
|
||||
self.convoLoadingIndicatorFrame.LayoutOrder = #sorted + 1
|
||||
end
|
||||
|
||||
function ConversationList:ResizeCanvas()
|
||||
local height = 0
|
||||
for _, entry in pairs(self.conversationEntries) do
|
||||
if entry.rbx.Visible then
|
||||
height = height + entry.rbx.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
|
||||
if self.convoLoadingIndicatorFrame.Visible then
|
||||
height = height + self.convoLoadingIndicatorFrame.AbsoluteSize.Y
|
||||
end
|
||||
|
||||
self.rbx.CanvasSize = UDim2.new(1, 0, 0, height)
|
||||
end
|
||||
|
||||
function ConversationList:StartFetchingConversationsAnimation()
|
||||
if not self.currentFetchConvoIndicator then
|
||||
local loadingIndicator = LoadingIndicator.new(self.appState, 1)
|
||||
loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
loadingIndicator.rbx.Parent = self.convoLoadingIndicatorFrame
|
||||
|
||||
self.currentFetchConvoIndicator = loadingIndicator
|
||||
|
||||
self.convoLoadingIndicatorFrame.Visible = true
|
||||
|
||||
self:ResizeCanvas()
|
||||
self:Sort()
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationList:StopFetchingConversationsAnimation()
|
||||
if self.currentFetchConvoIndicator then
|
||||
self.currentFetchConvoIndicator:Destroy()
|
||||
self.currentFetchConvoIndicator = nil
|
||||
self.convoLoadingIndicatorFrame.Visible = false
|
||||
|
||||
self:ResizeCanvas()
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationList:CheckToShowNoSearchResults()
|
||||
local visible = false
|
||||
|
||||
if not self.convoLoadingIndicatorFrame.Visible then
|
||||
visible = self.noSearchResultsFound
|
||||
end
|
||||
|
||||
self.noResultsFrame.Visible = visible
|
||||
end
|
||||
|
||||
function ConversationList: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 _, entry in pairs(self.conversationEntries) do
|
||||
entry:Destruct()
|
||||
end
|
||||
self.conversations = {}
|
||||
self.conversationEntries = {}
|
||||
self.conversationUsers = {}
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
ConversationList.__index = ConversationList
|
||||
|
||||
return ConversationList
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local Text = require(LuaChat.Text)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local FFlagTextBoxOverrideManualFocusRelease = settings():GetFFlag("TextBoxOverrideManualFocusRelease")
|
||||
|
||||
local CANCEL_BUTTON_PADDING = 16
|
||||
|
||||
local ConversationSearchBox = {}
|
||||
|
||||
function ConversationSearchBox.new(appState)
|
||||
local self = {}
|
||||
setmetatable(self, {__index = ConversationSearchBox})
|
||||
|
||||
self.rbx = Create.new"Frame"
|
||||
{
|
||||
Name = "ConversationSearchBox",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Create.new "UIListLayout"
|
||||
{
|
||||
SortOrder = "LayoutOrder",
|
||||
FillDirection = "Horizontal",
|
||||
HorizontalAlignment = "Right",
|
||||
},
|
||||
}
|
||||
|
||||
local cancelButton = Create.new"TextButton"
|
||||
{
|
||||
Name = "Cancel",
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = appState.localization:Format("Feature.Chat.Action.Cancel"),
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
LayoutOrder = 1,
|
||||
}
|
||||
cancelButton.Size = UDim2.new(0,
|
||||
Text.GetTextWidth(cancelButton.Text, cancelButton.Font, cancelButton.TextSize) + CANCEL_BUTTON_PADDING * 2, 1, 0)
|
||||
cancelButton.Parent = self.rbx
|
||||
|
||||
local searchBoxContainer = Create.new"Frame"
|
||||
{
|
||||
Name = "SearchBoxContainer",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -cancelButton.Size.X.Offset, 1, 0),
|
||||
Position = UDim2.new(0, 8, 0, 0),
|
||||
LayoutOrder = 0,
|
||||
}
|
||||
searchBoxContainer.Parent = self.rbx
|
||||
|
||||
local searchBoxBackground = Create.new"ImageLabel"
|
||||
{
|
||||
Name = "SearchBoxBackground",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, -12),
|
||||
Position = UDim2.new(0, 8, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/search.png",
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(3,3,4,4),
|
||||
Create.new"ImageLabel"
|
||||
{
|
||||
Name = "SearchIcon",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
Position = UDim2.new(0, 8, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
ImageColor3 = Constants.Color.GRAY3,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-search.png",
|
||||
},
|
||||
}
|
||||
searchBoxBackground.Parent = searchBoxContainer
|
||||
|
||||
local clearSearchButton = Create.new"ImageButton"
|
||||
{
|
||||
Name = "ClearButton",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
Position = UDim2.new(1, -8, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(1, 0.5),
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png",
|
||||
Visible = false,
|
||||
}
|
||||
clearSearchButton.Parent = searchBoxBackground
|
||||
|
||||
local search = Create.new"TextBox"
|
||||
{
|
||||
Name = "Search",
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
Size = UDim2.new(1, -36 - 8 - 32, 1, 0),
|
||||
Position = UDim2.new(0, 36, 0, 0),
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = "",
|
||||
PlaceholderText = appState.localization:Format("Feature.Chat.Label.SearchWord"),
|
||||
PlaceholderColor3 = Constants.Color.GRAY3,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
OverlayNativeInput = true,
|
||||
ClearTextOnFocus = false,
|
||||
}
|
||||
search.Parent = searchBoxBackground
|
||||
if FFlagTextBoxOverrideManualFocusRelease then
|
||||
search.ManualFocusRelease = true
|
||||
end
|
||||
self.search = search
|
||||
|
||||
self.SearchChanged = Signal.new()
|
||||
self.Closed = Signal.new()
|
||||
|
||||
local searchText = ""
|
||||
|
||||
search:GetPropertyChangedSignal("Text"):Connect(function()
|
||||
searchText = string.lower(search.Text)
|
||||
clearSearchButton.Visible = searchText ~= ""
|
||||
self.SearchChanged:fire(search.Text)
|
||||
end)
|
||||
|
||||
search.FocusLost:Connect(function()
|
||||
if search.Text:len() == 0 then
|
||||
self:Cancel()
|
||||
end
|
||||
end)
|
||||
|
||||
getInputEvent(cancelButton):Connect(function()
|
||||
self:Cancel()
|
||||
end)
|
||||
|
||||
getInputEvent(clearSearchButton):Connect(function()
|
||||
search.Text = ""
|
||||
self:Cancel()
|
||||
end)
|
||||
|
||||
self.SearchFilterPredicate = function(other)
|
||||
if searchText == "" then
|
||||
return true
|
||||
end
|
||||
return string.find(string.lower(other), searchText, 1, true) ~= nil
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConversationSearchBox:Cancel()
|
||||
self.search:ReleaseFocus()
|
||||
self.search.Text = ""
|
||||
self.Closed:fire()
|
||||
end
|
||||
|
||||
function ConversationSearchBox:Update(participants)
|
||||
|
||||
end
|
||||
|
||||
return ConversationSearchBox
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Conversation = require(LuaChat.Models.Conversation)
|
||||
local Create = require(LuaChat.Create)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
local Functional = require(Common.Functional)
|
||||
local HeadshotLoader = require(LuaChat.HeadshotLoader)
|
||||
local UserThumbnail = require(LuaChat.Components.UserThumbnail)
|
||||
|
||||
local MAX_USERS_IN_THUMBNAIL = 4
|
||||
|
||||
local ConversationThumbnail = {}
|
||||
|
||||
ConversationThumbnail.__index = ConversationThumbnail
|
||||
|
||||
-- This lets us determine how to build the group thumbnail. Index represents how many people are in the thumbnail!
|
||||
local IMAGE_LAYOUT = {
|
||||
[1] = {
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(1, 0, 1, 0),
|
||||
FramePosition = UDim2.new(0, 0, 0, 0),
|
||||
},
|
||||
},
|
||||
[2] = {
|
||||
{
|
||||
Size = UDim2.new(2, 0, 1, 0),
|
||||
Position = UDim2.new(-0.5, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 1, 0),
|
||||
FramePosition = UDim2.new(0, 0, 0, 0),
|
||||
Border = {
|
||||
BorderPosition = UDim2.new(0.5, -1, 0, 0),
|
||||
BorderSize = UDim2.new(0, 2, 1, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
Size = UDim2.new(2, 0, 1, 0),
|
||||
Position = UDim2.new(-0.5, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 1, 0),
|
||||
FramePosition = UDim2.new(0.5, 1, 0, 0),
|
||||
},
|
||||
},
|
||||
[3] = {
|
||||
{
|
||||
Size = UDim2.new(2, 0, 1, 0),
|
||||
Position = UDim2.new(-0.5, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 1, 0),
|
||||
FramePosition = UDim2.new(0, 0, 0, 0),
|
||||
Border = {
|
||||
BorderPosition = UDim2.new(0.5, -1, 0, 0),
|
||||
BorderSize = UDim2.new(0, 2, 1, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
|
||||
FramePosition = UDim2.new(0.5, 1, 0, 0),
|
||||
Border = {
|
||||
BorderPosition = UDim2.new(0.5, 0, 0.5, -1),
|
||||
BorderSize = UDim2.new(0.5, 0, 0, 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
|
||||
FramePosition = UDim2.new(0.5, 1, 0.5, 1),
|
||||
},
|
||||
},
|
||||
[4] = {
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
|
||||
FramePosition = UDim2.new(0, 0, 0, 0),
|
||||
Border = {
|
||||
BorderPosition = UDim2.new(0, 0, 0.5, -1),
|
||||
BorderSize = UDim2.new(1, 0, 0, 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
|
||||
FramePosition = UDim2.new(0.5, 1, 0, 0),
|
||||
},
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
|
||||
FramePosition = UDim2.new(0, 0, 0.5, 1),
|
||||
Border = {
|
||||
BorderPosition = UDim2.new(0.5, -1, 0, 0),
|
||||
BorderSize = UDim2.new(0, 2, 1, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
FrameSize = UDim2.new(0.5, -1, 0.5, -1),
|
||||
FramePosition = UDim2.new(0.5, 1, 0.5, 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
function ConversationThumbnail.new(appState, conversation)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
|
||||
local localId = tostring(Players.LocalPlayer.UserId)
|
||||
local otherIds = Functional.Filter(conversation.participants, function(participantId)
|
||||
return participantId ~= localId
|
||||
end)
|
||||
|
||||
if conversation.conversationType == Conversation.Type.ONE_TO_ONE_CONVERSATION then
|
||||
return UserThumbnail.new(appState, otherIds[1])
|
||||
end
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "ConversationThumbnail",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
}
|
||||
|
||||
setmetatable(self, ConversationThumbnail)
|
||||
|
||||
self:Update(conversation)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConversationThumbnail:Update(conversation)
|
||||
self.rbx:ClearAllChildren()
|
||||
|
||||
local localId = tostring(Players.LocalPlayer.UserId)
|
||||
local otherIds = Functional.Filter(conversation.participants, function(participantId)
|
||||
return participantId ~= localId
|
||||
end)
|
||||
|
||||
local showUserIds
|
||||
|
||||
if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then
|
||||
local hasRoomForLocalUser = #conversation.participants <= MAX_USERS_IN_THUMBNAIL
|
||||
|
||||
if hasRoomForLocalUser then
|
||||
showUserIds = conversation.participants
|
||||
else
|
||||
showUserIds = Functional.Take(otherIds, MAX_USERS_IN_THUMBNAIL)
|
||||
end
|
||||
else
|
||||
showUserIds = otherIds
|
||||
end
|
||||
|
||||
for userIndex, userId in ipairs(showUserIds) do
|
||||
local layoutData = IMAGE_LAYOUT[#showUserIds][userIndex]
|
||||
local frame = Create.new "Frame" {
|
||||
Name = "AvatarHolder",
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
Size = layoutData.FrameSize,
|
||||
Position = layoutData.FramePosition,
|
||||
}
|
||||
local headshot = Create.new "ImageLabel" {
|
||||
Name = "Avatar",
|
||||
Size = layoutData.Size,
|
||||
Position = layoutData.Position,
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
headshot.Parent = frame
|
||||
|
||||
HeadshotLoader:Load(headshot, userId)
|
||||
|
||||
if layoutData.Border then
|
||||
Create.new "Frame" {
|
||||
Name = "Border",
|
||||
Size = layoutData.Border.BorderSize,
|
||||
Position = layoutData.Border.BorderPosition,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.GRAY4
|
||||
}.Parent = self.rbx
|
||||
end
|
||||
|
||||
frame.Parent = self.rbx
|
||||
end
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
Name = "Mask",
|
||||
Image = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-48x48.png",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
}.Parent = self.rbx
|
||||
|
||||
local state = self.appState.store:getState()
|
||||
if state.FormFactor == FormFactor.TABLET then
|
||||
local currentLocationParameters = state.ChatAppReducer.Location.current.parameters
|
||||
local currentConversationId = currentLocationParameters and currentLocationParameters.conversationId or nil
|
||||
if currentConversationId == conversation.id then
|
||||
self.rbx.Mask.ImageColor3 = Constants.Color.GRAY5
|
||||
else
|
||||
self.rbx.Mask.ImageColor3 = Constants.Color.WHITE
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ConversationThumbnail:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return ConversationThumbnail
|
||||
@@ -0,0 +1,192 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local ConversationActions = require(LuaChat.Actions.ConversationActions)
|
||||
local ConversationModel = require(LuaChat.Models.Conversation)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local Immutable = require(Common.Immutable)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local FriendSearchBoxComponent = require(Components.FriendSearchBox)
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
local ResponseIndicator = require(Components.ResponseIndicator)
|
||||
local SectionComponent = require(Components.ListSection)
|
||||
|
||||
local RemoveRoute = require(LuaChat.Actions.RemoveRoute)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
|
||||
local CreateChat = {}
|
||||
CreateChat.__index = CreateChat
|
||||
|
||||
function CreateChat.new(appState)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, CreateChat)
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
|
||||
self.conversation = ConversationModel.empty()
|
||||
|
||||
self.responseIndicator = ResponseIndicator.new(appState)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
|
||||
-- Header:
|
||||
self.header = HeaderLoader.GetHeader(appState, Intent.CreateChat)
|
||||
self.header:SetDefaultSubtitle()
|
||||
self.header:SetTitle(appState.localization:Format("Feature.Chat.Heading.ChatWithFriends"))
|
||||
self.header:SetBackButtonEnabled(true)
|
||||
self.header:SetConnectionState(Enum.ConnectionState.Disconnected)
|
||||
|
||||
-- Search for friends:
|
||||
self.searchComponent = FriendSearchBoxComponent.new(
|
||||
appState,
|
||||
self.conversation.participants,
|
||||
Constants.MAX_PARTICIPANT_COUNT,
|
||||
function(user)
|
||||
return user.isFriend and user.id ~= tostring(Players.LocalPlayer.UserId)
|
||||
end
|
||||
)
|
||||
self.searchComponent.rbx.LayoutOrder = 3
|
||||
local addParticipantConnection = self.searchComponent.addParticipant:connect(function(id)
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self:ChangeParticipants(Immutable.Set(self.conversation.participants, #self.conversation.participants+1, id))
|
||||
end)
|
||||
table.insert(self.connections, addParticipantConnection)
|
||||
|
||||
local removeParticipantConnection = self.searchComponent.removeParticipant:connect(function(id)
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self:ChangeParticipants(Immutable.RemoveValueFromList(self.conversation.participants, id))
|
||||
end)
|
||||
table.insert(self.connections, removeParticipantConnection)
|
||||
|
||||
-- Assemble the dialog from components we just made:
|
||||
self.sectionComponent = SectionComponent.new(appState, nil, 2)
|
||||
self.rbx = Create.new"Frame" {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
Create.new("UIListLayout") {
|
||||
Name = "ListLayout",
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
},
|
||||
self.header.rbx,
|
||||
Create.new"Frame" {
|
||||
Name = "Content",
|
||||
Size = UDim2.new(1, 0, 1, -(self.header.heightOfHeader)),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 1,
|
||||
ClipsDescendants = true,
|
||||
|
||||
Create.new"UIListLayout" {
|
||||
Name = "ListLayout",
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
},
|
||||
self.sectionComponent.rbx,
|
||||
self.searchComponent.rbx,
|
||||
},
|
||||
self.responseIndicator.rbx,
|
||||
}
|
||||
|
||||
-- Wire up the save button to actually create our new chat group:
|
||||
self.createChat = self.header:CreateHeaderButton("CreateChat", "Feature.Chat.Action.StartChatWithFriends")
|
||||
self.header.innerButtons.Position = UDim2.new(1, 0, 0, 0)
|
||||
self.createChat.rbx.Size = UDim2.new(0, 60, 1, 0)
|
||||
self.createChat:SetEnabled(false)
|
||||
local createChatConnection = self.createChat.Pressed:connect(function()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
if #self.conversation.participants >= Constants.MIN_PARTICIPANT_COUNT then
|
||||
self.responseIndicator:SetVisible(true)
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.CreateConversation(self.conversation, function(id)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
if id ~= nil then
|
||||
self.ConversationSaved:fire(id)
|
||||
end
|
||||
self.appState.store:dispatch(RemoveRoute(Intent.CreateChat))
|
||||
end)
|
||||
)
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, createChatConnection)
|
||||
|
||||
self.BackButtonPressed = Signal.new()
|
||||
self.header.BackButtonPressed:connect(function()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self.BackButtonPressed:fire()
|
||||
end)
|
||||
self.ConversationSaved = Signal.new()
|
||||
|
||||
spawn(function()
|
||||
self.appState.store:dispatch(ConversationActions.GetAllFriendsAsync())
|
||||
end)
|
||||
|
||||
self.tooManyFriendsAlertId = nil
|
||||
|
||||
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 CreateChat:Resize()
|
||||
-- Content frame must resize if the header changes size (which happens when it shows the "Connecting" message):
|
||||
local sizeContent = UDim2.new(1, 0, 1, -self.header.rbx.AbsoluteSize.Y)
|
||||
self.rbx.Content.Size = sizeContent
|
||||
|
||||
-- Friends Search frame must resize to fit properly with their peers:
|
||||
local sizeSearch = UDim2.new(1, 0, 1, -self.sectionComponent.rbx.AbsoluteSize.Y)
|
||||
self.searchComponent.rbx.Size = sizeSearch
|
||||
end
|
||||
|
||||
function CreateChat:ChangeParticipants(participants)
|
||||
self.conversation = Immutable.Set(self.conversation, "participants", participants)
|
||||
self.searchComponent:Update(participants)
|
||||
self.createChat:SetEnabled(#participants >= Constants.MIN_PARTICIPANT_COUNT)
|
||||
end
|
||||
|
||||
function CreateChat:Update(current)
|
||||
self.header:SetConnectionState(current.ConnectionState)
|
||||
self.searchComponent:Update(self.conversation.participants)
|
||||
end
|
||||
|
||||
function CreateChat: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()
|
||||
self.searchComponent:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return CreateChat
|
||||
@@ -0,0 +1,56 @@
|
||||
local LuaChat = script.Parent.Parent
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local DefaultScreen = {}
|
||||
|
||||
DefaultScreen.__index = DefaultScreen
|
||||
|
||||
function DefaultScreen.new(appState)
|
||||
local self = {}
|
||||
|
||||
setmetatable(self, DefaultScreen)
|
||||
|
||||
local header = HeaderLoader.GetHeader(appState, Intent.DefaultScreen)
|
||||
header:SetDefaultSubtitle()
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "DefaultScreen",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.GRAY6,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = "LayoutOrder",
|
||||
},
|
||||
|
||||
header.rbx,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Main",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1.0,
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
BackgroundTransparency = 1.0,
|
||||
Size = UDim2.new(0, 150, 0, 150),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-chat-large.png",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function DefaultScreen:Update(current, previous) end
|
||||
|
||||
return DefaultScreen
|
||||
@@ -0,0 +1,773 @@
|
||||
-- TweenService and UserInputService required by TextInputDialog
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local TextInputEntryComponent = require(Components.TextInputEntry)
|
||||
|
||||
local PopRoute = require(LuaChat.Actions.PopRoute)
|
||||
|
||||
local FFlagLuaChatContextualMenuTransition = settings():GetFFlag("LuaChatContextualMenuTransition")
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
|
||||
local OptionDialog = {}
|
||||
local TextInputDialog = {}
|
||||
local ConfirmationDialog = {}
|
||||
local AlertDialog = {}
|
||||
|
||||
local DialogComponents = {
|
||||
OptionDialog = OptionDialog,
|
||||
TextInputDialog = TextInputDialog,
|
||||
ConfirmationDialog = ConfirmationDialog,
|
||||
AlertDialog = AlertDialog,
|
||||
}
|
||||
|
||||
local DEFAULT_DIALOG_WIDTH = 400
|
||||
local OPTION_DIALOG_GAP_HEIGHT = 8
|
||||
|
||||
local TWEEN_TIME = Constants.Dialog.TWEEN_TIME
|
||||
|
||||
local function GetDialogWidth(appState)
|
||||
if appState.store:getState().FormFactor == FormFactor.TABLET then
|
||||
return UDim.new(0, DEFAULT_DIALOG_WIDTH)
|
||||
else
|
||||
return UDim.new(1, -Constants.ModalDialog.CLEARANCE_DIALOG_SIDE)
|
||||
end
|
||||
end
|
||||
|
||||
local function PushBackLayout(newObject, parentObject)
|
||||
parentObject = parentObject or newObject.Parent
|
||||
|
||||
local listLayout = parentObject:FindFirstChildOfClass('UIListLayout')
|
||||
local highestLayout = -1
|
||||
if listLayout then
|
||||
for _, child in pairs(parentObject:GetChildren()) do
|
||||
if child:IsA('GuiBase') then
|
||||
highestLayout = math.max(highestLayout, child.LayoutOrder)
|
||||
end
|
||||
end
|
||||
end
|
||||
newObject.LayoutOrder = highestLayout + 1
|
||||
return newObject
|
||||
end
|
||||
|
||||
local function makeDivider(height, color, transparency, layoutOrder)
|
||||
transparency = transparency or 0
|
||||
layoutOrder = layoutOrder or 0
|
||||
|
||||
return Create.new"Frame" {
|
||||
Name = "Divider",
|
||||
BackgroundColor3 = color,
|
||||
BackgroundTransparency = transparency,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, height),
|
||||
Position = UDim2.new(0, 0, 1, -height),
|
||||
LayoutOrder = layoutOrder,
|
||||
}
|
||||
end
|
||||
|
||||
local function tweenDialog(rbx, layout)
|
||||
rbx.Size = UDim2.new(1, 0, 2*rbx.Size.Y.Scale, 0)
|
||||
|
||||
spawn(function()
|
||||
local yTweenSize
|
||||
if layout then
|
||||
yTweenSize = layout.AbsoluteContentSize.Y
|
||||
else
|
||||
yTweenSize = rbx.AbsoluteSize.Y
|
||||
end
|
||||
|
||||
rbx.Position = UDim2.new(rbx.Position.X.Scale, rbx.Position.X.Offset, -1, yTweenSize)
|
||||
local tweenInfo = TweenInfo.new(TWEEN_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
||||
|
||||
local propertyGoals = {
|
||||
Position = UDim2.new(rbx.Position.X.Scale, rbx.Position.X.Offset, -1, 0),
|
||||
}
|
||||
|
||||
local tween = TweenService:Create(rbx, tweenInfo, propertyGoals)
|
||||
|
||||
tween:Play()
|
||||
end)
|
||||
end
|
||||
|
||||
local function outerFrameAndHeader(appState, title, color, event)
|
||||
color = color or Constants.Color.BLUE_PRIMARY
|
||||
local dialogWidth = GetDialogWidth(appState)
|
||||
|
||||
local dialogLayout = Create.new"UIListLayout" {
|
||||
Name = "ListLayout",
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}
|
||||
|
||||
local rbx = Create.new"TextButton" {
|
||||
Name = title,
|
||||
AutoButtonColor = false,
|
||||
Text = "",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1-Constants.Color.ALPHA_SHADOW_PRIMARY,
|
||||
BackgroundColor3 = Constants.Color.GRAY1,
|
||||
BorderSizePixel = 0,
|
||||
Visible = false,
|
||||
Active = true,
|
||||
Create.new"ImageButton" {
|
||||
Name = "Dialog",
|
||||
AutoButtonColor = false,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(5,5,6,6),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/modal.png",
|
||||
Size = UDim2.new(dialogWidth.Scale, dialogWidth.Offset, 0, 42),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
|
||||
dialogLayout,
|
||||
|
||||
Create.new"TextLabel" {
|
||||
Name = "Title",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Constants.Font.TITLE,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Text = title,
|
||||
Size = UDim2.new(1, 0, 0, 42),
|
||||
TextColor3 = color,
|
||||
LayoutOrder = 0,
|
||||
},
|
||||
makeDivider(3, color, nil, 1),
|
||||
},
|
||||
}
|
||||
|
||||
if not FFlagLuaChatContextualMenuTransition then
|
||||
--Seems to be necessary to make sure clicks get sunk and don't fall through when dialog is open
|
||||
getInputEvent(rbx):Connect(function()
|
||||
rbx.Visible = false
|
||||
appState.store:dispatch(PopRoute())
|
||||
if event ~= nil then
|
||||
event:fire()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
getInputEvent(rbx.Dialog):Connect(function() end)
|
||||
|
||||
return rbx
|
||||
end
|
||||
|
||||
local function addCancelButtonCallbacks(dialogComponent, cancelButton, event)
|
||||
local function onInputBegan(input)
|
||||
cancelButton.BackgroundColor3 = Constants.Color.GRAY5
|
||||
end
|
||||
cancelButton.InputBegan:Connect(onInputBegan)
|
||||
|
||||
local function onInputEnded(input)
|
||||
cancelButton.BackgroundColor3 = Constants.Color.WHITE
|
||||
end
|
||||
cancelButton.InputEnded:Connect(onInputEnded)
|
||||
|
||||
getInputEvent(cancelButton):Connect(function()
|
||||
dialogComponent:Close()
|
||||
if event ~= nil then
|
||||
event:fire()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function makeConfirmCancelButtons(cancelTitle, confirmTitle)
|
||||
return Create.new"Frame" {
|
||||
Name = "ConfirmAndCancelButtons",
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 48),
|
||||
BorderSizePixel = 0,
|
||||
Create.new"TextButton" {
|
||||
Name = "Cancel",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Text = cancelTitle,
|
||||
Size = UDim2.new(0.5, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
AutoButtonColor = false,
|
||||
},
|
||||
Create.new"Frame" {
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, 1, 1, 0),
|
||||
Position = UDim2.new(0.5, -1, 0, 0),
|
||||
},
|
||||
Create.new"TextButton" {
|
||||
Name = "Save",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.BLUE_PRIMARY,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Text = confirmTitle,
|
||||
Size = UDim2.new(0.5, 0, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
AutoButtonColor = false,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- Vertically resize the given frame so that it fits the contents:
|
||||
local function ResizeFrame(frame)
|
||||
local verticalSize = 0
|
||||
local horizontalSize = frame.Size.X
|
||||
for _, v in pairs(frame:GetChildren()) do
|
||||
if v:IsA("GuiObject") and (v.Visible == true) then
|
||||
verticalSize = verticalSize + v.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
|
||||
local sizeFrame = UDim2.new(horizontalSize.Scale, horizontalSize.Offset, 0, verticalSize)
|
||||
frame.Size = sizeFrame
|
||||
end
|
||||
|
||||
function AlertDialog.new(appState, titleKey, messageKey)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, {__index = AlertDialog})
|
||||
|
||||
local title = titleKey ~= nil and appState.localization:Format(titleKey) or ""
|
||||
local message = messageKey ~= nil and appState.localization:Format(messageKey) or ""
|
||||
|
||||
self.rbx = outerFrameAndHeader(appState, title, Constants.Color.RED_PRIMARY)
|
||||
|
||||
if FFlagLuaChatContextualMenuTransition then
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
self.rbx.Visible = false
|
||||
appState.store:dispatch(PopRoute())
|
||||
end)
|
||||
end
|
||||
|
||||
local messageFrame = Create.new"Frame" {
|
||||
Name = "MessageFrame",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, 100),
|
||||
Create.new"TextLabel" {
|
||||
Name = "Message",
|
||||
Text = message,
|
||||
TextWrapped = true,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -48, 1, -48),
|
||||
Position = UDim2.new(0, 24, 0, 24),
|
||||
}
|
||||
}
|
||||
messageFrame.Parent = self.rbx.Dialog
|
||||
|
||||
local divider = makeDivider(1, Constants.Color.GRAY4)
|
||||
divider.Parent = self.rbx.Dialog
|
||||
|
||||
local cancelButtonFrame = Create.new"Frame" {
|
||||
Name = "CancelFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 42),
|
||||
BorderSizePixel = 0,
|
||||
Create.new"TextButton" {
|
||||
Name = "Cancel",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
Font = Constants.Font.TITLE,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Text = appState.localization:Format("Feature.Chat.Action.Confirm"),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
AutoButtonColor = false,
|
||||
}
|
||||
}
|
||||
cancelButtonFrame.Parent = self.rbx.Dialog
|
||||
|
||||
self.accepted = Instance.new("BindableEvent")
|
||||
self.accepted.Parent = self.rbx
|
||||
|
||||
addCancelButtonCallbacks(self, cancelButtonFrame.Cancel, self.accepted)
|
||||
ResizeFrame(self.rbx.Dialog)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function AlertDialog:Update(alert)
|
||||
|
||||
local title = alert.titleKey ~= nil and self.appState.localization:Format(alert.titleKey) or ""
|
||||
local message = alert.messageKey ~= nil and
|
||||
self.appState.localization:Format(alert.messageKey, alert.messageArguments) or ""
|
||||
|
||||
self.rbx.Dialog.Title.Text = title
|
||||
self.rbx.Dialog.MessageFrame.Message.Text = message
|
||||
end
|
||||
|
||||
function AlertDialog:Prompt()
|
||||
self.rbx.Visible = true
|
||||
end
|
||||
|
||||
function AlertDialog:Close()
|
||||
self.rbx.Visible = false
|
||||
end
|
||||
|
||||
function ConfirmationDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, {__index = ConfirmationDialog})
|
||||
|
||||
local title = titleKey ~= nil and appState.localization:Format(titleKey) or ""
|
||||
local message = messageKey ~= nil and appState.localization:Format(messageKey) or ""
|
||||
local cancelTitle = cancelTitleKey ~= nil and appState.localization:Format(cancelTitleKey) or ""
|
||||
local confirmTitle = confirmTitleKey ~= nil and appState.localization:Format(confirmTitleKey) or ""
|
||||
|
||||
|
||||
self.rbx = outerFrameAndHeader(appState, title, Constants.Color.RED_PRIMARY)
|
||||
|
||||
if FFlagLuaChatContextualMenuTransition then
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
self.rbx.Visible = false
|
||||
appState.store:dispatch(PopRoute())
|
||||
end)
|
||||
end
|
||||
|
||||
local messageFrame = Create.new"Frame" {
|
||||
Name = "MessageFrame",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, 100),
|
||||
Create.new"TextButton" {
|
||||
Name = "Message",
|
||||
Text = message,
|
||||
TextWrapped = true,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -48, 1, -48),
|
||||
Position = UDim2.new(0, 24, 0, 24),
|
||||
AutoButtonColor = false,
|
||||
}
|
||||
}
|
||||
messageFrame.Parent = self.rbx.Dialog
|
||||
PushBackLayout(messageFrame)
|
||||
|
||||
local divider = makeDivider(1, Constants.Color.GRAY4)
|
||||
divider.Parent = self.rbx.Dialog
|
||||
PushBackLayout(divider)
|
||||
|
||||
local buttons = makeConfirmCancelButtons(cancelTitle, confirmTitle)
|
||||
buttons.Parent = self.rbx.Dialog
|
||||
PushBackLayout(buttons)
|
||||
|
||||
local cancelButton = buttons.Cancel
|
||||
addCancelButtonCallbacks(self, cancelButton)
|
||||
|
||||
local saveButton = buttons.Save
|
||||
buttons.Save.TextColor3 = Constants.Color.RED_PRIMARY
|
||||
|
||||
local function onInputBegan(input)
|
||||
saveButton.BackgroundColor3 = Constants.Color.GRAY5
|
||||
end
|
||||
|
||||
local function onInputEnded(input)
|
||||
saveButton.BackgroundColor3 = Constants.Color.WHITE
|
||||
end
|
||||
|
||||
saveButton.InputBegan:Connect(onInputBegan)
|
||||
saveButton.InputEnded:Connect(onInputEnded)
|
||||
|
||||
getInputEvent(saveButton):Connect(function()
|
||||
self.rbx.Visible = false
|
||||
appState.store:dispatch(PopRoute())
|
||||
self.saved:fire(self.data)
|
||||
end)
|
||||
|
||||
self.saved = Signal.new()
|
||||
ResizeFrame(self.rbx.Dialog)
|
||||
|
||||
self.data = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ConfirmationDialog:Close()
|
||||
self.appState.store:dispatch(PopRoute())
|
||||
end
|
||||
|
||||
function ConfirmationDialog:Update(messageKey, data, messageArguments)
|
||||
self.data = data
|
||||
local message = messageKey ~= nil and self.appState.localization:Format(messageKey, messageArguments) or ""
|
||||
|
||||
self.rbx.Dialog.MessageFrame.Message.Text = message
|
||||
end
|
||||
|
||||
function ConfirmationDialog:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
function TextInputDialog.new(appState, titleLocalizationKey, maxChar)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, {__index = TextInputDialog})
|
||||
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
local title = appState.localization:Format(titleLocalizationKey)
|
||||
|
||||
self.cancel = Signal.new()
|
||||
|
||||
if FFlagLuaChatContextualMenuTransition then
|
||||
self.rbx = outerFrameAndHeader(appState, title, Constants.Color.BLUE_PRIMARY)
|
||||
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
self.rbx.Visible = false
|
||||
appState.store:dispatch(PopRoute())
|
||||
self.cancel:fire()
|
||||
end)
|
||||
else
|
||||
self.rbx = outerFrameAndHeader(appState, title, Constants.Color.BLUE_PRIMARY, self.cancel)
|
||||
end
|
||||
|
||||
local placeholderText = appState.localization:Format("Feature.Chat.Description.NameGroupChat")
|
||||
local textInput = TextInputEntryComponent.new(appState, nil, placeholderText)
|
||||
textInput.rbx.BackgroundTransparency = 1
|
||||
textInput.rbx.Size = UDim2.new(1,-10,1,0)
|
||||
textInput.rbx.Position = UDim2.new(0,10,0,0)
|
||||
textInput:ShowDivider(false)
|
||||
self.textInputComponent = textInput
|
||||
|
||||
self.value = textInput.value
|
||||
|
||||
local textCount = Create.new"TextLabel"{
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 18),
|
||||
Position = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(1,0),
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
Text = "0/" .. maxChar,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_12,
|
||||
TextYAlignment = "Top",
|
||||
}
|
||||
|
||||
local textInputContainer = Create.new"Frame"{
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(1, 0, 0, 98),
|
||||
Create.new"ImageLabel"{
|
||||
Name = "TextBackground",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, -40, 0, 36),
|
||||
Position = UDim2.new(0.5, 0, 0.5, -5),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
ScaleType = "Slice",
|
||||
SliceCenter = Rect.new(3,3,4,4),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png",
|
||||
textInput.rbx, -- set parent
|
||||
textCount, -- set parent
|
||||
},
|
||||
}
|
||||
|
||||
textInputContainer.Parent = self.rbx.Dialog
|
||||
|
||||
local divider = makeDivider(1, Constants.Color.GRAY4)
|
||||
divider.LayoutOrder = 3
|
||||
divider.Parent = self.rbx.Dialog
|
||||
|
||||
local cancelTitle = appState.localization:Format("Feature.Chat.Action.Cancel")
|
||||
local confirmTitle = appState.localization:Format("Feature.Chat.Action.Save")
|
||||
|
||||
local buttons = makeConfirmCancelButtons(cancelTitle, confirmTitle)
|
||||
buttons.LayoutOrder = 4
|
||||
buttons.Parent = self.rbx.Dialog
|
||||
|
||||
local function isSubmitable()
|
||||
return self.value:len() <= maxChar
|
||||
end
|
||||
|
||||
local cancelButton = buttons.Cancel
|
||||
|
||||
addCancelButtonCallbacks(self, cancelButton, self.cancel)
|
||||
|
||||
local saveButton = buttons.Save
|
||||
local function onInputBegan(input)
|
||||
saveButton.BackgroundColor3 = Constants.Color.GRAY5
|
||||
end
|
||||
|
||||
local function onInputEnded(input)
|
||||
saveButton.BackgroundColor3 = Constants.Color.WHITE
|
||||
end
|
||||
|
||||
saveButton.MouseButton1Down:Connect(onInputBegan)
|
||||
saveButton.MouseButton1Up:Connect(onInputEnded)
|
||||
|
||||
getInputEvent(saveButton):Connect(function()
|
||||
if isSubmitable() then
|
||||
self:Close()
|
||||
self.saved:fire(self.value)
|
||||
end
|
||||
end)
|
||||
|
||||
textInput.textBoxChanged:connect(function()
|
||||
self.value = textInput.value
|
||||
|
||||
saveButton.TextColor3 = isSubmitable() and Constants.Color.BLUE_PRIMARY or Constants.Color.RED_PRIMARY
|
||||
textCount.TextColor3 = isSubmitable() and Constants.Color.GRAY2 or Constants.Color.RED_PRIMARY
|
||||
textCount.Text = string.format("%s/%s", tostring(self.value:len()), tostring(maxChar))
|
||||
end)
|
||||
|
||||
self.saved = Signal.new()
|
||||
ResizeFrame(self.rbx.Dialog)
|
||||
|
||||
self:SetupTweenForKeyboardEvents()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TextInputDialog:SetupTweenForKeyboardEvents()
|
||||
self.tweenGroupNameDialogUp = nil
|
||||
self.tweenGroupNameDialogDown = nil
|
||||
local inputServiceConnection = UserInputService:GetPropertyChangedSignal('OnScreenKeyboardVisible'):Connect(function()
|
||||
if UserInputService.OnScreenKeyboardVisible then
|
||||
if self.tweenGroupNameDialogUp == nil then
|
||||
local duration = UserInputService.OnScreenKeyboardAnimationDuration
|
||||
local yPos = UserInputService.OnScreenKeyboardPosition.Y / 2
|
||||
local tweenInfo = TweenInfo.new(duration)
|
||||
local propertyGoals =
|
||||
{
|
||||
Position = UDim2.new(0.5, 0, 0, yPos)
|
||||
}
|
||||
self.tweenGroupNameDialogUp = TweenService:Create(self.rbx.Dialog, tweenInfo, propertyGoals)
|
||||
end
|
||||
self.tweenGroupNameDialogUp:Play()
|
||||
else
|
||||
if self.tweenGroupNameDialogDown == nil then
|
||||
local duration = UserInputService.OnScreenKeyboardAnimationDuration
|
||||
local tweenInfo = TweenInfo.new(duration)
|
||||
local propertyGoals =
|
||||
{
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
}
|
||||
self.tweenGroupNameDialogDown = TweenService:Create(self.rbx.Dialog, tweenInfo, propertyGoals)
|
||||
end
|
||||
self.tweenGroupNameDialogDown:Play()
|
||||
end
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, inputServiceConnection)
|
||||
else
|
||||
table.insert(self.connections, inputServiceConnection)
|
||||
end
|
||||
end
|
||||
|
||||
function TextInputDialog:Close()
|
||||
self.textInputComponent.rbx.TextBox:ReleaseFocus()
|
||||
self.appState.store:dispatch(PopRoute())
|
||||
end
|
||||
|
||||
function TextInputDialog:Update(value)
|
||||
self.textInputComponent:Update(value)
|
||||
end
|
||||
|
||||
function TextInputDialog:Destruct()
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
for _, connection in ipairs(self.rbx_connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
self.textInputComponent:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
function OptionDialog.new(appState, titleKey, options, userId)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, {__index = OptionDialog})
|
||||
|
||||
local title = titleKey ~= nil and appState.localization:Format(titleKey) or ""
|
||||
|
||||
self.rbx = outerFrameAndHeader(appState, title)
|
||||
|
||||
if FFlagLuaChatContextualMenuTransition then
|
||||
local connection
|
||||
connection = getInputEvent(self.rbx):Connect(function()
|
||||
connection:Disconnect()
|
||||
self:Close()
|
||||
end)
|
||||
else
|
||||
self.rbx.Dialog.AnchorPoint = Vector2.new(0.5, 1)
|
||||
self.rbx.Dialog.Position = UDim2.new(0.5, 0, 1, -10)
|
||||
end
|
||||
|
||||
self.selected = Signal.new()
|
||||
|
||||
self.optionGuis = {}
|
||||
|
||||
local layout = Create.new"UIListLayout"{
|
||||
Name = "ListLayout",
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = "Center",
|
||||
VerticalAlignment = "Bottom",
|
||||
}
|
||||
layout.Parent = self.rbx
|
||||
|
||||
local optionSizeY = 42 + 1
|
||||
|
||||
for optionId, optionTitleKey in pairs(options) do
|
||||
|
||||
local optionTitle = appState.localization:Format(optionTitleKey)
|
||||
|
||||
local optionGui = Create.new"TextButton"{
|
||||
Name = optionTitle,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Text = optionTitle,
|
||||
Size = UDim2.new(1, 0, 0, optionSizeY),
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
AutoButtonColor = false,
|
||||
}
|
||||
|
||||
local divider = makeDivider(1, Constants.Color.GRAY4)
|
||||
divider.Position = UDim2.new(0, 0, 1, 0)
|
||||
divider.Parent = optionGui
|
||||
|
||||
local function onInputBegan(input)
|
||||
optionGui.BackgroundColor3 = Constants.Color.GRAY5
|
||||
end
|
||||
optionGui.InputBegan:Connect(onInputBegan)
|
||||
|
||||
local function onInputEnded(input)
|
||||
optionGui.BackgroundColor3 = Constants.Color.WHITE
|
||||
end
|
||||
optionGui.InputEnded:Connect(onInputEnded)
|
||||
|
||||
getInputEvent(optionGui):Connect(function()
|
||||
self.rbx.Visible = false
|
||||
appState.store:dispatch(PopRoute())
|
||||
self.selected:fire(optionId, self.data or {})
|
||||
end)
|
||||
|
||||
optionGui.Parent = self.rbx.Dialog
|
||||
PushBackLayout(optionGui)
|
||||
|
||||
self.optionGuis[optionId] = optionGui
|
||||
end
|
||||
|
||||
local firstDivider = makeDivider(OPTION_DIALOG_GAP_HEIGHT, Constants.Color.WHITE, 1, 1)
|
||||
firstDivider.BackgroundTransparency = 1
|
||||
firstDivider.Parent = self.rbx
|
||||
|
||||
local cancelButtonFrame = Create.new"ImageLabel" {
|
||||
Name = "CancelButton",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(self.rbx.Dialog.Size.X.Scale, self.rbx.Dialog.Size.X.Offset, 0, Constants.ModalDialog.BUTTON_HEIGHT),
|
||||
BorderSizePixel = 0,
|
||||
ScaleType = "Slice",
|
||||
SliceCenter = Rect.new(5,5,6,6),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/modal.png",
|
||||
LayoutOrder = 2,
|
||||
Create.new"TextButton" {
|
||||
Name = "Cancel",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Constants.Font.TITLE,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Text = appState.localization:Format("Feature.Chat.Action.Cancel"),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
AutoButtonColor = false,
|
||||
}
|
||||
}
|
||||
cancelButtonFrame.Parent = self.rbx
|
||||
addCancelButtonCallbacks(self, cancelButtonFrame.Cancel)
|
||||
|
||||
local secondDivider = makeDivider(OPTION_DIALOG_GAP_HEIGHT, Constants.Color.WHITE, 1, 3)
|
||||
secondDivider.BackgroundTransparency = 1
|
||||
secondDivider.Parent = self.rbx
|
||||
|
||||
self.data = userId
|
||||
self:Resize()
|
||||
|
||||
if FFlagLuaChatContextualMenuTransition then
|
||||
tweenDialog(self.rbx, layout)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Helper function so ResizeFrame doesn't have to be called externally:
|
||||
function OptionDialog:Resize()
|
||||
ResizeFrame(self.rbx.Dialog)
|
||||
end
|
||||
|
||||
function OptionDialog:Close()
|
||||
if FFlagLuaChatContextualMenuTransition then
|
||||
self.rbx.BackgroundTransparency = 1
|
||||
|
||||
local totalHeight = self.rbx.ListLayout.AbsoluteContentSize.Y
|
||||
local tweenInfo = TweenInfo.new(TWEEN_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
||||
|
||||
local propertyGoals = {
|
||||
Position = UDim2.new(0, 0, -1, totalHeight),
|
||||
}
|
||||
|
||||
local tween = TweenService:Create(self.rbx, tweenInfo, propertyGoals)
|
||||
tween:Play()
|
||||
|
||||
wait(TWEEN_TIME)
|
||||
end
|
||||
self.appState.store:dispatch(PopRoute())
|
||||
end
|
||||
|
||||
function OptionDialog:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return DialogComponents
|
||||
@@ -0,0 +1,189 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local ConversationActions = require(LuaChat.Actions.ConversationActions)
|
||||
local ConversationModel = require(LuaChat.Models.Conversation)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local Immutable = require(Common.Immutable)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local FriendSearchBoxComponent = require(Components.FriendSearchBox)
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
local ResponseIndicator = require(Components.ResponseIndicator)
|
||||
|
||||
local SetRoute = require(LuaChat.Actions.SetRoute)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local EditChatGroup = {}
|
||||
EditChatGroup.__index = EditChatGroup
|
||||
|
||||
function EditChatGroup.new(appState, maxSize, convoId)
|
||||
local self = {
|
||||
appState = appState,
|
||||
maxSize = maxSize,
|
||||
convoId = convoId,
|
||||
}
|
||||
self.connections = {}
|
||||
setmetatable(self, EditChatGroup)
|
||||
|
||||
self.conversation = ConversationModel.empty()
|
||||
self.alreadyParticipants = {}
|
||||
|
||||
local oldConversation = self.appState.store:getState().ChatAppReducer.Conversations[self.convoId]
|
||||
self.fromType = oldConversation.conversationType
|
||||
for _, userId in pairs(oldConversation.participants) do
|
||||
self.alreadyParticipants[userId] = true
|
||||
end
|
||||
|
||||
self.searchComponent = FriendSearchBoxComponent.new(
|
||||
appState,
|
||||
self.conversation.participants,
|
||||
self.maxSize,
|
||||
function(user)
|
||||
local isNotPlayer = user.id ~= tostring(Players.LocalPlayer.UserId)
|
||||
local isNotInList = not self.alreadyParticipants[user.id]
|
||||
return user.isFriend and isNotPlayer and isNotInList
|
||||
end
|
||||
)
|
||||
local addParticipantConnection = self.searchComponent.addParticipant:connect(function(id)
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self:ChangeParticipants(Immutable.Set(self.conversation.participants, #self.conversation.participants+1, id))
|
||||
end)
|
||||
table.insert(self.connections, addParticipantConnection)
|
||||
|
||||
local removeParticipantConnection = self.searchComponent.removeParticipant:connect(function(id)
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self:ChangeParticipants(Immutable.RemoveValueFromList(self.conversation.participants, id))
|
||||
end)
|
||||
table.insert(self.connections, removeParticipantConnection)
|
||||
|
||||
-- Header:
|
||||
self.header = HeaderLoader.GetHeader(appState, Intent.EditChatGroup)
|
||||
self.header:SetDefaultSubtitle()
|
||||
self.header:SetTitle(appState.localization:Format("Feature.Chat.Label.AddFriends"))
|
||||
self.header:SetBackButtonEnabled(true)
|
||||
|
||||
self.responseIndicator = ResponseIndicator.new(appState)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
self.header.rbx,
|
||||
Create.new"Frame" {
|
||||
Name = "Content",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
self.searchComponent.rbx,
|
||||
},
|
||||
self.responseIndicator.rbx,
|
||||
}
|
||||
|
||||
self.saveGroup = self.header:CreateHeaderButton("SaveGroup", "Feature.Chat.Action.Add")
|
||||
self.saveGroup:SetEnabled(false)
|
||||
|
||||
self.header:AddButton(self.saveGroup)
|
||||
local saveGroupConnection = self.saveGroup.Pressed:connect(function()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
if #self.conversation.participants == 0 then
|
||||
return
|
||||
end
|
||||
if self.fromType == ConversationModel.Type.MULTI_USER_CONVERSATION then
|
||||
self.responseIndicator:SetVisible(true)
|
||||
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.AddUsersToConversation(self.convoId, self.conversation.participants, function()
|
||||
self.responseIndicator:SetVisible(false)
|
||||
self.appState.store:dispatch(SetRoute(
|
||||
Intent.Conversation,
|
||||
{conversationId = self.convoId},
|
||||
Intent.ConversationHub
|
||||
))
|
||||
end)
|
||||
)
|
||||
elseif self.fromType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then
|
||||
self.responseIndicator:SetVisible(true)
|
||||
|
||||
local originalConvo = self.appState.store:getState().ChatAppReducer.Conversations[self.convoId]
|
||||
local allParticipants = Immutable.Append(self.conversation.participants)
|
||||
for _, userId in ipairs(originalConvo.participants) do
|
||||
if userId ~= tostring(Players.LocalPlayer.UserId) then
|
||||
table.insert(allParticipants, userId)
|
||||
end
|
||||
end
|
||||
local newConvo = Immutable.JoinDictionaries(originalConvo, {
|
||||
participants = allParticipants,
|
||||
clientId = self.conversation.clientId,
|
||||
title = "",
|
||||
})
|
||||
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.CreateConversation(newConvo, function(passedConversationId)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
self.appState.store:dispatch(
|
||||
SetRoute(Intent.Conversation, {conversationId = passedConversationId}, Intent.ConversationHub)
|
||||
)
|
||||
end)
|
||||
)
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, saveGroupConnection)
|
||||
|
||||
self.rbx.Content.Position = UDim2.new(0, 0, 0, self.header.rbx.Size.Y.Offset)
|
||||
self.rbx.Content.Size = UDim2.new(1, 0, 1, -self.header.rbx.Size.Y.Offset)
|
||||
|
||||
self.BackButtonPressed = Signal.new()
|
||||
self.header.BackButtonPressed:connect(function()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self.BackButtonPressed:fire()
|
||||
end)
|
||||
|
||||
spawn(function()
|
||||
self.appState.store:dispatch(ConversationActions.GetAllFriendsAsync())
|
||||
end)
|
||||
|
||||
self.tooManyFriendsAlertId = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function EditChatGroup:ChangeParticipants(participants)
|
||||
self.conversation = Immutable.Set(self.conversation, "participants", participants)
|
||||
self.searchComponent:Update(participants)
|
||||
self.saveGroup:SetEnabled(#participants > 0)
|
||||
end
|
||||
|
||||
function EditChatGroup:Update(current, previous)
|
||||
self.header:SetConnectionState(current.ConnectionState)
|
||||
self.searchComponent:Update(self.conversation.participants)
|
||||
|
||||
self.saveGroup:SetEnabled(#self.conversation.participants ~= #self.alreadyParticipants)
|
||||
end
|
||||
|
||||
function EditChatGroup:Destruct()
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
|
||||
self.header:Destroy()
|
||||
self.responseIndicator:Destruct()
|
||||
self.searchComponent:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return EditChatGroup
|
||||
@@ -0,0 +1,248 @@
|
||||
--
|
||||
-- FriendCarousel
|
||||
--
|
||||
-- This is a scrollable list of friend icons.
|
||||
--
|
||||
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local ApiFetchUsersThumbnail = require(LuaApp.Thunks.ApiFetchUsersThumbnail)
|
||||
local ChatConstants = require(Modules.LuaChat.Constants)
|
||||
local Constants = require(LuaApp.Constants)
|
||||
local FriendIcon = require(Modules.LuaApp.Components.FriendIcon)
|
||||
local Roact = require(Common.Roact)
|
||||
local RoactRodux = require(Common.RoactRodux)
|
||||
local UserModel = require(LuaApp.Models.User)
|
||||
|
||||
local FFlagLuaChatReplacePresenceIndicatorImages = settings():GetFFlag("LuaChatReplacePresenceIndicatorImages")
|
||||
local FFlagLuaChatFriendIconRefactor = settings():GetFFlag("LuaChatFriendIconRefactor")
|
||||
|
||||
local FriendCarousel = Roact.Component:extend("FriendCarousel")
|
||||
|
||||
local IMAGE_DOT_INGAME = "rbxasset://textures/ui/LuaApp/icons/ic-green-dot.png"
|
||||
local IMAGE_DOT_ONLINE = "rbxasset://textures/ui/LuaApp/icons/ic-blue-dot.png"
|
||||
local IMAGE_DOT_STUDIO = "rbxasset://textures/ui/LuaApp/icons/ic-orange-dot.png"
|
||||
local DEFAULT_DOT_SIZE = 8
|
||||
|
||||
local IMAGE_MASK = "rbxasset://textures/ui/LuaChat/graphic/friendmask.png"
|
||||
local MASK_WIDTH = 10
|
||||
|
||||
-- Frame around the profile image:
|
||||
local IMAGE_PROFILE_BORDER = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-36x36.png"
|
||||
local IMAGE_PROFILE_DEFAULT = "rbxasset://textures/ui/LuaChat/icons/ic-profile.png"
|
||||
|
||||
function FriendCarousel:init()
|
||||
self.state = {
|
||||
fadeScrollLeft = false,
|
||||
fadeScrollRight = false,
|
||||
}
|
||||
end
|
||||
|
||||
function FriendCarousel:onPositionChanged(rbx)
|
||||
-- Programatically show / hide the fade bars at either side of the carousel.
|
||||
-- This hides items that are partly visible, but completely reveals the
|
||||
-- first / last items when they're present.
|
||||
|
||||
-- Early return if we're not set up yet:
|
||||
if (rbx.CanvasSize.X.Offset == 0) or (rbx.CanvasSize.Y.Offset == 0) or
|
||||
(rbx.AbsoluteWindowSize.X == 0) or (rbx.AbsoluteWindowSize.Y == 0) then
|
||||
return
|
||||
end
|
||||
|
||||
local fadeLeft = (0 < rbx.CanvasPosition.X)
|
||||
local fadeRight = (rbx.CanvasSize.X.Offset - rbx.CanvasPosition.X) > rbx.AbsoluteWindowSize.X
|
||||
|
||||
if (fadeLeft ~= self.state.fadeScrollLeft) or
|
||||
(fadeRight ~= self.state.fadeScrollRight) then
|
||||
spawn(function()
|
||||
self:setState({
|
||||
fadeScrollLeft = fadeLeft,
|
||||
fadeScrollRight = fadeRight,
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function FriendCarousel:didMount()
|
||||
if self.rbxScroller then
|
||||
self:onPositionChanged(self.rbxScroller)
|
||||
end
|
||||
end
|
||||
|
||||
function FriendCarousel:render()
|
||||
-- Visual properties of this game card:
|
||||
local dotSize = self.props.dotSize or DEFAULT_DOT_SIZE
|
||||
local friends = self.props.friends or {}
|
||||
local getUserThumbnail = self.props.getUserThumbnail
|
||||
local horizontalAlignment = self.props.HorizontalAlignment or Enum.HorizontalAlignment.Left
|
||||
local itemGap = self.props.itemGap
|
||||
local itemSize = self.props.itemSize
|
||||
local layoutOrder = self.props.LayoutOrder
|
||||
local size = self.props.Size or UDim2.new(1, 0, 0, itemSize)
|
||||
local users = self.props.users
|
||||
|
||||
local presenceIndicatorSizeKey = ChatConstants:GetPresenceIndicatorSizeKey(dotSize)
|
||||
|
||||
-- Build up a horizontal list of items for our card:
|
||||
local friendItems = {}
|
||||
friendItems["Layout"] = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = horizontalAlignment,
|
||||
Padding = UDim.new(0, itemGap),
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
})
|
||||
|
||||
local countFriends = #friends
|
||||
if countFriends > 0 then
|
||||
for index, friend in ipairs(friends) do
|
||||
if FFlagLuaChatFriendIconRefactor then
|
||||
local userFriend = users[friend.uid]
|
||||
friendItems[index] = Roact.createElement(FriendIcon, {
|
||||
user = userFriend,
|
||||
dotSize = dotSize,
|
||||
itemSize = itemSize,
|
||||
layoutOrder = index,
|
||||
})
|
||||
else
|
||||
-- Look up the presence information:
|
||||
local imageFriend = nil
|
||||
local iconDot = not FFlagLuaChatReplacePresenceIndicatorImages and IMAGE_DOT_ONLINE or nil
|
||||
local userFriend = users[friend.uid]
|
||||
if userFriend then
|
||||
if FFlagLuaChatReplacePresenceIndicatorImages then
|
||||
iconDot = ChatConstants.PresenceIndicatorImagesBySize[presenceIndicatorSizeKey][userFriend.presence]
|
||||
else
|
||||
if userFriend.presence == UserModel.PresenceType.IN_GAME then
|
||||
iconDot = IMAGE_DOT_INGAME
|
||||
elseif userFriend.presence == UserModel.PresenceType.IN_STUDIO then
|
||||
iconDot = IMAGE_DOT_STUDIO
|
||||
end
|
||||
end
|
||||
|
||||
-- Find images for the friend portraits:
|
||||
if userFriend.thumbnails and userFriend.thumbnails.HeadShot
|
||||
and userFriend.thumbnails.HeadShot.Size48x48 then
|
||||
imageFriend = userFriend.thumbnails.HeadShot.Size48x48
|
||||
end
|
||||
|
||||
if imageFriend == nil then
|
||||
imageFriend = IMAGE_PROFILE_DEFAULT
|
||||
getUserThumbnail(friend.uid)
|
||||
end
|
||||
end
|
||||
|
||||
friendItems[index] = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = index,
|
||||
Size = UDim2.new(0, itemSize, 0, itemSize),
|
||||
}, {
|
||||
Profile = Roact.createElement("ImageButton", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = imageFriend,
|
||||
Size = UDim2.new(0, itemSize, 0, itemSize),
|
||||
ZIndex = 1,
|
||||
}),
|
||||
|
||||
Border = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = IMAGE_PROFILE_BORDER,
|
||||
Size = UDim2.new(0, itemSize, 0, itemSize),
|
||||
ZIndex = 2,
|
||||
}),
|
||||
|
||||
Dot = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = iconDot,
|
||||
Position = UDim2.new(1, -dotSize, 1, -dotSize),
|
||||
Size = UDim2.new(0, dotSize, 0, dotSize),
|
||||
ZIndex = 3,
|
||||
}),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local maskLeft = nil
|
||||
if self.state.fadeScrollLeft then
|
||||
maskLeft = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = IMAGE_MASK,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Rotation = 180,
|
||||
Size = UDim2.new(0, MASK_WIDTH, 1, 0),
|
||||
ZIndex = 2,
|
||||
})
|
||||
end
|
||||
|
||||
local maskRight = nil
|
||||
if self.state.fadeScrollRight then
|
||||
maskRight = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = IMAGE_MASK,
|
||||
Position = UDim2.new(1, -MASK_WIDTH, 0, 0),
|
||||
Size = UDim2.new(0, MASK_WIDTH, 1, 0),
|
||||
ZIndex = 2,
|
||||
})
|
||||
end
|
||||
|
||||
-- This frame arrangement adds a semi-transparent overlay to
|
||||
-- fade out items at the edge of the frame:
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = layoutOrder,
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = size,
|
||||
},{
|
||||
MaskLeft = maskLeft,
|
||||
|
||||
MaskRight = maskRight,
|
||||
|
||||
ScrollyFrame = Roact.createElement("ScrollingFrame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
CanvasSize = UDim2.new(0, (itemSize + itemGap) * countFriends, 0, itemSize),
|
||||
ClipsDescendants = true,
|
||||
ScrollBarThickness = 0,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
[Roact.Change.AbsolutePosition] = function(rbx, changed)
|
||||
self:onPositionChanged(rbx)
|
||||
end,
|
||||
[Roact.Ref] = function(rbx)
|
||||
self.rbxScroller = rbx
|
||||
end,
|
||||
}, friendItems)
|
||||
})
|
||||
end
|
||||
|
||||
FriendCarousel = RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
users = state.Users,
|
||||
}
|
||||
end,
|
||||
function(dispatch)
|
||||
return {
|
||||
getUserThumbnail = function(friendId)
|
||||
spawn(function()
|
||||
dispatch(ApiFetchUsersThumbnail(nil, { friendId },
|
||||
Constants.AvatarThumbnailRequests.FRIEND_CAROUSEL
|
||||
))
|
||||
end)
|
||||
end,
|
||||
}
|
||||
end
|
||||
)(FriendCarousel)
|
||||
|
||||
return FriendCarousel
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
return function()
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local AppReducer = require(Modules.LuaApp.AppReducer)
|
||||
local MockId = require(Modules.LuaApp.MockId)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local Rodux = require(Modules.Common.Rodux)
|
||||
|
||||
local FriendCarousel = require(Modules.LuaChat.Components.FriendCarousel)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
|
||||
local store = Rodux.Store.new(AppReducer)
|
||||
|
||||
local gameFriends = { { uid = MockId() }, { uid = MockId() },
|
||||
{ uid = MockId() }, { uid = MockId() }, { uid = MockId() } }
|
||||
local carouselItemGap = 9
|
||||
local carouselItemHeight = 32
|
||||
local carouselItemDotSize = 10
|
||||
|
||||
local element = Roact.createElement(RoactRodux.StoreProvider, {
|
||||
store = store,
|
||||
}, {
|
||||
GameFriends = Roact.createElement(FriendCarousel, {
|
||||
dotSize = carouselItemDotSize,
|
||||
friends = gameFriends,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
itemGap = carouselItemGap,
|
||||
itemSize = carouselItemHeight,
|
||||
Size = UDim2.new(1, 0, 1, carouselItemHeight),
|
||||
}),
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,359 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Functional = require(Common.Functional)
|
||||
local Signal = require(Common.Signal)
|
||||
local ToastModel = require(LuaChat.Models.ToastModel)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local ListSection = require(Components.ListSection)
|
||||
local UserList = require(Components.UserList)
|
||||
local UserThumbnailBar = require(Components.UserThumbnailBar)
|
||||
|
||||
local User = require(LuaApp.Models.User)
|
||||
|
||||
local ShowToast = require(LuaChat.Actions.ShowToast)
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local FFlagTextBoxOverrideManualFocusRelease = settings():GetFFlag("TextBoxOverrideManualFocusRelease")
|
||||
local FFlagLuaChatSortFriendsForConversation = settings():GetFFlag("LuaChatSortFriendsForConversation")
|
||||
|
||||
local CLEAR_TEXT_WIDTH = 44
|
||||
local ICON_CELL_WIDTH = 60
|
||||
local SEARCH_BOX_HEIGHT = 48
|
||||
|
||||
local FriendSearchBox = {}
|
||||
FriendSearchBox.__index = FriendSearchBox
|
||||
|
||||
local PRESENCE_WEIGHTS = {
|
||||
[User.PresenceType.IN_GAME] = 4,
|
||||
[User.PresenceType.ONLINE] = 3,
|
||||
[User.PresenceType.IN_STUDIO] = 2,
|
||||
[User.PresenceType.OFFLINE] = 1,
|
||||
}
|
||||
|
||||
local function friendSortPredicate(friend1, friend2)
|
||||
local friend1Weight = PRESENCE_WEIGHTS[friend1.presence] or 0
|
||||
local friend2Weight = PRESENCE_WEIGHTS[friend2.presence] or 0
|
||||
|
||||
if friend1Weight == friend2Weight then
|
||||
return friend1.name:lower() < friend2.name:lower()
|
||||
else
|
||||
return friend1Weight > friend2Weight
|
||||
end
|
||||
end
|
||||
|
||||
function FriendSearchBox.new(appState, participants, maxParticipantCount, filter)
|
||||
local self = {
|
||||
appState = appState,
|
||||
participants = participants,
|
||||
users = appState.store:getState().Users,
|
||||
maxParticipantCount = maxParticipantCount,
|
||||
}
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
setmetatable(self, FriendSearchBox)
|
||||
|
||||
self.friendThumbnails = UserThumbnailBar.new(appState, maxParticipantCount, 1)
|
||||
local removedConnection = self.friendThumbnails.removed:connect(function(id)
|
||||
self:RemoveParticipant(id)
|
||||
end)
|
||||
table.insert(self.connections, removedConnection)
|
||||
|
||||
self.userList = UserList.new(appState, nil, filter)
|
||||
local userSelectedConnection = self.userList.userSelected:connect(function(user)
|
||||
local selected = Functional.Find(self.participants, user.id)
|
||||
if selected then
|
||||
self:RemoveParticipant(user.id)
|
||||
else
|
||||
self:AddParticipant(user.id)
|
||||
end
|
||||
self:ClearText()
|
||||
end)
|
||||
table.insert(self.connections, userSelectedConnection)
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
Name = "FriendSearchBox",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
Create.new"UIListLayout" {
|
||||
Name = "ListLayout",
|
||||
SortOrder = "LayoutOrder",
|
||||
},
|
||||
self.friendThumbnails.rbx,
|
||||
Create.new"Frame" {
|
||||
Name = "Divider1",
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
LayoutOrder = 2,
|
||||
},
|
||||
ListSection.new(appState, nil, 3).rbx,
|
||||
Create.new"Frame" {
|
||||
Name = "SearchContainer",
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, SEARCH_BOX_HEIGHT),
|
||||
LayoutOrder = 4,
|
||||
|
||||
Create.new"ImageLabel" {
|
||||
Name = "SearchIcon",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 24, 0, 24),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH/2, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
ImageColor3 = Constants.Color.GRAY3,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-search.png",
|
||||
},
|
||||
Create.new"TextBox" {
|
||||
Name = "Search",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -CLEAR_TEXT_WIDTH-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 = "",
|
||||
PlaceholderText = appState.localization:Format("Feature.Friends.Label.SearchFriends"),
|
||||
PlaceholderColor3 = Constants.Color.GRAY3,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
OverlayNativeInput = true,
|
||||
ClearTextOnFocus = false,
|
||||
ClipsDescendants = true,
|
||||
},
|
||||
Create.new"ImageButton" {
|
||||
Name = "Clear",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
Position = UDim2.new(1, -(CLEAR_TEXT_WIDTH/2), 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
AutoButtonColor = false,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png",
|
||||
ImageTransparency = 1,
|
||||
},
|
||||
},
|
||||
Create.new"Frame" {
|
||||
Name = "Divider2",
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
LayoutOrder = 5,
|
||||
},
|
||||
Create.new"ScrollingFrame" {
|
||||
Name = "ScrollingFrame",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ScrollBarThickness = 5,
|
||||
BottomImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
MidImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
TopImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
LayoutOrder = 6,
|
||||
|
||||
self.userList.rbx,
|
||||
},
|
||||
}
|
||||
|
||||
self.searchContainer = self.rbx.SearchContainer
|
||||
local clearButton = self.searchContainer.Clear
|
||||
local search = self.searchContainer.Search
|
||||
self.search = search
|
||||
if FFlagTextBoxOverrideManualFocusRelease then
|
||||
search.ManualFocusRelease = true
|
||||
end
|
||||
|
||||
local clearButtonConnection = getInputEvent(clearButton):Connect(function()
|
||||
self:ClearText()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, clearButtonConnection)
|
||||
else
|
||||
table.insert(self.connections, clearButtonConnection)
|
||||
end
|
||||
|
||||
self:Resize()
|
||||
|
||||
local function updateClearButtonVisibility()
|
||||
-- If we were to set the visible property of the clear button on the textbox focus lost event
|
||||
-- it would disable the clear button, which in turn would stop the click event
|
||||
-- from being able to notify the button
|
||||
local visible = search:IsFocused() and (search.Text ~= "")
|
||||
clearButton.ImageTransparency = visible and 0 or 1
|
||||
end
|
||||
|
||||
self.searchChanged = Signal.new()
|
||||
self.addParticipant = Signal.new()
|
||||
self.removeParticipant = Signal.new()
|
||||
|
||||
local searchChangedConnection = search:GetPropertyChangedSignal("Text"):Connect(function()
|
||||
updateClearButtonVisibility()
|
||||
self.userList:ApplySearch(search.Text)
|
||||
self:Resize()
|
||||
self:ResizeCanvas()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, searchChangedConnection)
|
||||
else
|
||||
table.insert(self.connections, searchChangedConnection)
|
||||
end
|
||||
|
||||
local focusedConnection = search.Focused:Connect(updateClearButtonVisibility)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, focusedConnection)
|
||||
else
|
||||
table.insert(self.connections, focusedConnection)
|
||||
end
|
||||
local focusLostConnection = search.FocusLost:Connect(updateClearButtonVisibility)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, focusLostConnection)
|
||||
else
|
||||
table.insert(self.connections, focusLostConnection)
|
||||
end
|
||||
|
||||
self:UpdateFriends(appState.store:getState().Users, self.participants)
|
||||
|
||||
local userListAddConnection = self.userList.rbx.ChildAdded:Connect(function(child)
|
||||
self:ResizeCanvas();
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, userListAddConnection)
|
||||
else
|
||||
table.insert(self.connections, userListAddConnection)
|
||||
end
|
||||
|
||||
local userListRemoveConnection = self.userList.rbx.ChildRemoved:Connect(function(child)
|
||||
self:ResizeCanvas();
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, userListRemoveConnection)
|
||||
else
|
||||
table.insert(self.connections, userListRemoveConnection)
|
||||
end
|
||||
|
||||
local userListSizeConnection = self.userList.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:ResizeCanvas()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, userListSizeConnection)
|
||||
else
|
||||
table.insert(self.connections, userListSizeConnection)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function FriendSearchBox:Resize()
|
||||
local height = 0
|
||||
for _, element in pairs(self.rbx:GetChildren()) do
|
||||
if element:IsA("GuiObject") and element.Visible and element.Name ~= "ScrollingFrame" then
|
||||
height = height + element.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
|
||||
self.rbx.ScrollingFrame.Size = UDim2.new(1, 0, 1, -height)
|
||||
end
|
||||
|
||||
function FriendSearchBox:ResizeCanvas()
|
||||
local height = 0
|
||||
for _, element in pairs(self.userList.rbx:GetChildren()) do
|
||||
if element:IsA("GuiObject") and element.Visible then
|
||||
height = height + element.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
self.rbx.ScrollingFrame.CanvasSize = UDim2.new(1, 0, 0, height)
|
||||
end
|
||||
|
||||
function FriendSearchBox:AddParticipant(userId)
|
||||
if #self.participants >= self.maxParticipantCount then
|
||||
|
||||
if self.tooManyFriendsToastModel == nil then
|
||||
local messageKey = "Feature.Chat.Message.ToastText"
|
||||
local messageArguments = {
|
||||
friendNum = tostring(Constants.MAX_PARTICIPANT_COUNT),
|
||||
}
|
||||
local toastModel = ToastModel.new(Constants.ToastIDs.TOO_MANY_PEOPLE, messageKey, messageArguments)
|
||||
self.tooManyFriendsToastModel = toastModel
|
||||
end
|
||||
|
||||
self.appState.store:dispatch(ShowToast(self.tooManyFriendsToastModel))
|
||||
else
|
||||
self.addParticipant:fire(userId)
|
||||
end
|
||||
end
|
||||
|
||||
function FriendSearchBox:RemoveParticipant(userId)
|
||||
self.removeParticipant:fire(userId)
|
||||
end
|
||||
|
||||
function FriendSearchBox:UpdateFriends(users, selectedList)
|
||||
local friends = {}
|
||||
for _, user in pairs(users) do
|
||||
table.insert(friends, user)
|
||||
end
|
||||
if FFlagLuaChatSortFriendsForConversation then
|
||||
table.sort(friends, friendSortPredicate)
|
||||
for _,friend in pairs (friends) do
|
||||
if not PRESENCE_WEIGHTS[friend.presence] then
|
||||
warn("Invalid presence value for "..friend.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
self.userList:Update(friends, selectedList)
|
||||
self:Resize()
|
||||
end
|
||||
|
||||
function FriendSearchBox:ClearText()
|
||||
self.searchContainer.Search.Text = ""
|
||||
self:Resize()
|
||||
end
|
||||
|
||||
function FriendSearchBox:Update(participants)
|
||||
local state = self.appState.store:getState()
|
||||
local users = state.Users
|
||||
|
||||
if participants ~= self.participants then
|
||||
self.friendThumbnails:Update(participants)
|
||||
end
|
||||
|
||||
if participants ~= self.participants or users ~= self.users then
|
||||
self:UpdateFriends(users, participants)
|
||||
end
|
||||
|
||||
self.participants = participants
|
||||
self.users = users
|
||||
|
||||
self:Resize()
|
||||
end
|
||||
|
||||
function FriendSearchBox: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
|
||||
|
||||
self.userList:Destruct()
|
||||
self.friendThumbnails:Destruct()
|
||||
self.rbx.Parent = nil
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return FriendSearchBox
|
||||
@@ -0,0 +1,220 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local ConversationThumbnail = require(LuaChat.Components.ConversationThumbnail)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DateTime = require(LuaChat.DateTime)
|
||||
local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
local TextButton = require(LuaChat.Components.TextButton)
|
||||
local UserPresenceTextLabel = require(LuaChat.Components.UserPresenceTextLabel)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local UseCppTextTruncation = FlagSettings.UseCppTextTruncation()
|
||||
|
||||
local GameShareCard = {}
|
||||
|
||||
GameShareCard.__index = GameShareCard
|
||||
|
||||
local ICON_CELL_WIDTH = 60
|
||||
local HEIGHT = 54
|
||||
|
||||
local function getOneToOneConversationFriend(appState, conversation)
|
||||
local friend = nil
|
||||
if #conversation.participants == 2 then
|
||||
local friendId = nil
|
||||
for _, userId in ipairs(conversation.participants) do
|
||||
if userId ~= tostring(Players.LocalPlayer.UserId) then
|
||||
friendId = userId
|
||||
break
|
||||
end
|
||||
end
|
||||
if friendId then
|
||||
friend = appState.store:getState().Users[friendId]
|
||||
end
|
||||
end
|
||||
return friend
|
||||
end
|
||||
|
||||
function GameShareCard.new(appState, conversation)
|
||||
local self = {}
|
||||
self.appState = appState
|
||||
self.conversation = conversation
|
||||
self.conversationId = conversation.id
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
self.Tapped = Signal.new()
|
||||
self.startTime = DateTime.now()
|
||||
setmetatable(self, GameShareCard)
|
||||
|
||||
local friend = getOneToOneConversationFriend(appState, conversation)
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
Name = "GameShareCard",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, HEIGHT),
|
||||
}
|
||||
|
||||
local conversationThumbnail = ConversationThumbnail.new(appState, conversation)
|
||||
conversationThumbnail.rbx.Size = UDim2.new(0, 36, 0, 36)
|
||||
conversationThumbnail.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
conversationThumbnail.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
self.conversationThumbnail = conversationThumbnail
|
||||
|
||||
if friend then
|
||||
self.presenceSubLabel = UserPresenceTextLabel.new(appState, friend.id, {
|
||||
Name = "subLabel",
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH, 0.35, 0),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH, 0.55, 0),
|
||||
})
|
||||
self.presenceSubLabel.rbx.Parent = self.rbx
|
||||
end
|
||||
|
||||
self.sendButton = TextButton.new(self.appState, "SendButton", "Feature.Chat.Action.Send")
|
||||
local label = self.sendButton.rbx.Label
|
||||
label.TextColor3 = Constants.Color.GRAY1
|
||||
label.TextSize = Constants.Font.FONT_SIZE_16
|
||||
label.Font = Enum.Font.SourceSans
|
||||
self.sendButton:SetEnabled(true)
|
||||
self.sendButton.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
self.sendButton.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
local sendTextWidth = Text.GetTextWidth(label.Text, label.Font, label.TextSize)
|
||||
|
||||
local conversationThumbnailFrame = Create.new"Frame" {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
conversationThumbnail.rbx,
|
||||
}
|
||||
conversationThumbnailFrame.Parent = self.rbx
|
||||
|
||||
self.sentLabel = Create.new "TextLabel" {
|
||||
Name = "SentLabel",
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
Text = self.appState.localization:Format("Feature.Chat.Label.Sent"),
|
||||
Visible = false
|
||||
}
|
||||
|
||||
local sendButtonFrame = Create.new"Frame" {
|
||||
Name = "SendButtonFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, sendTextWidth + 14, 0, 32),
|
||||
Position = UDim2.new(1, -((sendTextWidth + 14) /2) - 12, .5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Create.new"ImageLabel"{
|
||||
Name = "SendImageLabel",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
ScaleType = "Slice",
|
||||
SliceCenter = Rect.new(3,3,4,4),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/btn-control-sm.png",
|
||||
self.sendButton.rbx,
|
||||
},
|
||||
self.sentLabel,
|
||||
}
|
||||
self.sendImageLabel = sendButtonFrame.SendImageLabel
|
||||
|
||||
sendButtonFrame.Parent = self.rbx
|
||||
local sendButtonConnection = self.sendButton.Pressed:connect(function()
|
||||
self.Tapped:fire()
|
||||
self.sendImageLabel.Visible = false
|
||||
self.sentLabel.Visible = true
|
||||
end)
|
||||
table.insert(self.connections, sendButtonConnection)
|
||||
|
||||
self.conversationTitle = Create.new"TextLabel" {
|
||||
Name = "ConversationTitle",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH - sendButtonFrame.Size.X.Offset - 20, 0.75, 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 = getConversationDisplayTitle(conversation),
|
||||
TextTruncate = UseCppTextTruncation and Enum.TextTruncate.AtEnd or nil,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
}
|
||||
|
||||
self.conversationTitle.Parent = self.rbx
|
||||
|
||||
local convoTitleChanged = self.conversationTitle:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self.conversationTitle.Text = getConversationDisplayTitle(conversation)
|
||||
if not UseCppTextTruncation then
|
||||
Text.TruncateTextLabel(self.conversationTitle, "...")
|
||||
end
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, convoTitleChanged)
|
||||
else
|
||||
table.insert(self.connections, convoTitleChanged)
|
||||
end
|
||||
|
||||
|
||||
local divider = Create.new"Frame" {
|
||||
Name = "Divider",
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1),
|
||||
}
|
||||
divider.Parent = self.rbx
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function GameShareCard:Update(conversation)
|
||||
if not conversation.lastUpdated then
|
||||
return
|
||||
end
|
||||
if conversation.lastUpdated:GetUnixTimestamp() < self.startTime:GetUnixTimestamp() then
|
||||
self.conversation = conversation
|
||||
end
|
||||
end
|
||||
|
||||
function GameShareCard: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
|
||||
|
||||
if self.conversationThumbnail then
|
||||
self.conversationThumbnail:Destruct()
|
||||
end
|
||||
if self.presenceSubLabel then
|
||||
self.presenceSubLabel:Destruct()
|
||||
end
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return GameShareCard
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local PlayerService = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local ConversationActions = require(LuaChat.Actions.ConversationActions)
|
||||
local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos)
|
||||
local HeaderLoader = require(LuaChat.Components.HeaderLoader)
|
||||
local PlaceInfoCard = require(LuaChat.Components.PlaceInfoCard)
|
||||
local GameShareCard = require(LuaChat.Components.GameShareCard)
|
||||
local ConversationList = require(LuaChat.Components.ConversationList)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
local truncateAssetLink = require(LuaChat.Utils.truncateAssetLink)
|
||||
local FlagSettings = require(LuaApp.FlagSettings)
|
||||
|
||||
local HttpStatus = require(LuaChat.WebApi).Status
|
||||
|
||||
local ToastModel = require(LuaChat.Models.ToastModel)
|
||||
local ShowToast = require(LuaChat.Actions.ShowToast)
|
||||
|
||||
local NavigateBack = require(Modules.LuaApp.Thunks.NavigateBack)
|
||||
local RemoveRoute = require(LuaChat.Actions.RemoveRoute)
|
||||
local SetAppLoaded = require(LuaChat.Actions.SetAppLoaded)
|
||||
|
||||
local NotificationType = require(LuaApp.Enum.NotificationType)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local FFlagShareGameToChatStatusAnalytics = settings():GetFFlag("ShareGameToChatStatusAnalytics")
|
||||
|
||||
local GameShareComponent = {}
|
||||
GameShareComponent.__index = GameShareComponent
|
||||
|
||||
local ICON_CELL_WIDTH = 60
|
||||
local SEARCH_BOX_HEIGHT = 48
|
||||
local CLEAR_TEXT_WIDTH = 44
|
||||
local PLACE_INFO_FRAME_HEIGHT = 84
|
||||
|
||||
local function requestOlderConversations(appState)
|
||||
-- Don't fetch older conversations if the oldest conversation has already been fetched.
|
||||
if appState.store:getState().ChatAppReducer.ConversationsAsync.oldestConversationIsFetched then
|
||||
return
|
||||
end
|
||||
|
||||
-- Ask for new conversations
|
||||
local convoCount = 0
|
||||
for _, _ in pairs(appState.store:getState().ChatAppReducer.Conversations) do
|
||||
convoCount = convoCount + 1
|
||||
end
|
||||
local pageSize = Constants.PageSize.GET_CONVERSATIONS
|
||||
local currentPage = math.floor(convoCount / pageSize)
|
||||
spawn(function()
|
||||
appState.store:dispatch(ConversationActions.GetLocalUserConversationsAsync(currentPage + 1, pageSize))
|
||||
end)
|
||||
end
|
||||
|
||||
function GameShareComponent.new(appState, placeId, innerFrame)
|
||||
local self = {}
|
||||
self._analytics = appState.analytics
|
||||
self.appState = appState
|
||||
self.placeId = placeId
|
||||
self.placeInfo = appState.store:getState().ChatAppReducer.PlaceInfos[placeId]
|
||||
setmetatable(self, GameShareComponent)
|
||||
|
||||
-- Header
|
||||
self.header = HeaderLoader.GetHeader(appState, Intent.GameShare)
|
||||
self.header:SetDefaultSubtitle()
|
||||
self.header:SetTitle(appState.localization:Format("Feature.Chat.Heading.ShareGameToChat"))
|
||||
self.header:SetBackButtonEnabled(true)
|
||||
|
||||
-- Place Info Card Frame
|
||||
self.placeInfoCardFrame = Create.new"Frame" {
|
||||
Name = "PlaceInfoCardFrame",
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, PLACE_INFO_FRAME_HEIGHT),
|
||||
LayoutOrder = 2,
|
||||
}
|
||||
|
||||
-- Search Container
|
||||
self.searchContainer = Create.new"Frame" {
|
||||
Name = "SearchContainer",
|
||||
BackgroundTransparency = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, SEARCH_BOX_HEIGHT),
|
||||
Visible = false,
|
||||
LayoutOrder = 3,
|
||||
|
||||
Create.new"ImageLabel" {
|
||||
Name = "SearchIcon",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 24, 0, 24),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH/2, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
ImageColor3 = Constants.Color.GRAY3,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-search.png",
|
||||
},
|
||||
Create.new"TextBox" {
|
||||
Name = "Search",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -CLEAR_TEXT_WIDTH-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 = "",
|
||||
PlaceholderText = appState.localization:Format("Feature.Chat.Label.SearchForFriendsAndChat"),
|
||||
PlaceholderColor3 = Constants.Color.GRAY3,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
OverlayNativeInput = true,
|
||||
ClearTextOnFocus = false,
|
||||
ClipsDescendants = true,
|
||||
},
|
||||
Create.new"ImageButton" {
|
||||
Name = "Clear",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
Position = UDim2.new(1, -(CLEAR_TEXT_WIDTH/2), 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
AutoButtonColor = false,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png",
|
||||
ImageTransparency = 1,
|
||||
},
|
||||
}
|
||||
self.clearSearchButton = self.searchContainer.Clear
|
||||
self.searchBox = self.searchContainer.Search
|
||||
self.SearchFilterPredicate = function(other)
|
||||
if self.searchBox.Text == "" then
|
||||
return true
|
||||
end
|
||||
return string.find(string.lower(other), string.lower(self.searchBox.Text), 1, true) ~= nil
|
||||
end
|
||||
|
||||
local divider = Create.new"Frame" {
|
||||
Name = "Divider",
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
LayoutOrder = 4,
|
||||
}
|
||||
|
||||
-- Conversation List Frame
|
||||
self.conversationListFrame = Create.new "Frame" {
|
||||
Name = "ConversationListFrame",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 5,
|
||||
Size = UDim2.new(1, 0, 1, - self.header.rbx.Size.Y.Offset - SEARCH_BOX_HEIGHT - PLACE_INFO_FRAME_HEIGHT),
|
||||
}
|
||||
|
||||
self.placeInfoCardFrame.Parent = innerFrame
|
||||
self.searchContainer.Parent = innerFrame
|
||||
divider.Parent = innerFrame
|
||||
self.conversationListFrame.Parent = innerFrame
|
||||
|
||||
self.rbx = Create.new"ImageButton" {
|
||||
Active = true,
|
||||
AutoButtonColor = false,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
Create.new("UIListLayout") {
|
||||
Name = "ListLayout",
|
||||
SortOrder = "LayoutOrder",
|
||||
HorizontalAlignment = "Center",
|
||||
},
|
||||
|
||||
self.header.rbx,
|
||||
innerFrame,
|
||||
}
|
||||
|
||||
if not appState.store:getState().ChatAppReducer.AppLoaded then
|
||||
spawn(function()
|
||||
appState.store:dispatch(
|
||||
ConversationActions.GetLocalUserConversationsAsync(1, Constants.PageSize.GET_CONVERSATIONS)
|
||||
):andThen(function()
|
||||
appState.store:dispatch(SetAppLoaded(true))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function GameShareComponent:Start()
|
||||
local isLuaAppStarterScriptEnabled = FlagSettings:IsLuaAppStarterScriptEnabled()
|
||||
|
||||
self.connections = {}
|
||||
|
||||
-- back button
|
||||
local backButtonConnection
|
||||
if isLuaAppStarterScriptEnabled then
|
||||
backButtonConnection = self.header.BackButtonPressed:connect(function()
|
||||
self.appState.store:dispatch(NavigateBack())
|
||||
GuiService:BroadcastNotification("", NotificationType.CLOSE_MODAL)
|
||||
end)
|
||||
else
|
||||
backButtonConnection = self.header.BackButtonPressed:connect(function()
|
||||
self.appState.store:dispatch(RemoveRoute(DialogInfo.Intent.GameShare))
|
||||
GuiService:BroadcastNotification("", NotificationType.CLOSE_MODAL)
|
||||
end)
|
||||
end
|
||||
table.insert(self.connections, backButtonConnection)
|
||||
|
||||
-- update
|
||||
local appStateConnection = self.appState.store.changed:connect(function(state, oldState)
|
||||
self:Update(state, oldState)
|
||||
end)
|
||||
table.insert(self.connections, appStateConnection)
|
||||
|
||||
-- search clear button
|
||||
local clearButtonConnection = getInputEvent(self.clearSearchButton):Connect(function()
|
||||
self.searchBox.Text = ""
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, clearButtonConnection)
|
||||
else
|
||||
table.insert(self.connections, clearButtonConnection)
|
||||
end
|
||||
|
||||
local function updateClearButtonVisibility()
|
||||
-- If we were to set the visible property of the clear button on the textbox focus lost event
|
||||
-- it would disable the clear button, which in turn would stop the click event
|
||||
-- from being able to notify the button
|
||||
local visible = self.searchBox:IsFocused() and (self.searchBox.Text ~= "")
|
||||
self.clearSearchButton.ImageTransparency = visible and 0 or 1
|
||||
end
|
||||
|
||||
-- search box
|
||||
local searchChangedConnection = self.searchBox:GetPropertyChangedSignal("Text"):Connect(function()
|
||||
if self.list then
|
||||
updateClearButtonVisibility()
|
||||
self.list:SetFilterPredicate(self.SearchFilterPredicate)
|
||||
self:getOlderConversationsForSearchIfNecessary()
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, searchChangedConnection)
|
||||
|
||||
local focusedConnection = self.searchBox.Focused:Connect(updateClearButtonVisibility)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, focusedConnection)
|
||||
else
|
||||
table.insert(self.connections, focusedConnection)
|
||||
end
|
||||
local focusLostConnection = self.searchBox.FocusLost:Connect(updateClearButtonVisibility)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, focusLostConnection)
|
||||
else
|
||||
table.insert(self.connections, focusLostConnection)
|
||||
end
|
||||
|
||||
if not self.placeInfo then
|
||||
self.appState.store:dispatch(GetMultiplePlaceInfos({self.placeId}))
|
||||
end
|
||||
|
||||
if self.appState.store:getState().ChatAppReducer.AppLoaded and self.placeInfo then
|
||||
self:FillContent()
|
||||
end
|
||||
end
|
||||
|
||||
function GameShareComponent:Update(newState, oldState)
|
||||
if (not oldState.ChatAppReducer.AppLoaded) and newState.ChatAppReducer.AppLoaded and self.placeInfo then
|
||||
self:FillContent()
|
||||
end
|
||||
|
||||
if (not self.placeInfo) and (newState.ChatAppReducer.PlaceInfos[self.placeId]) then
|
||||
self.placeInfo = newState.ChatAppReducer.PlaceInfos[self.placeId]
|
||||
if newState.ChatAppReducer.AppLoaded then
|
||||
self:FillContent()
|
||||
end
|
||||
end
|
||||
|
||||
local newPageConversationsIsFetching = newState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching
|
||||
local oldPageConversationsIsFetching = oldState.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching
|
||||
if newPageConversationsIsFetching ~= oldPageConversationsIsFetching and self.list then
|
||||
self.list:Update(newState, oldState)
|
||||
self:getOlderConversationsForSearchIfNecessary()
|
||||
end
|
||||
|
||||
if newState.ChatAppReducer.Conversations ~= oldState.ChatAppReducer.Conversations and self.list then
|
||||
self.list:Update(newState, oldState)
|
||||
end
|
||||
end
|
||||
|
||||
function GameShareComponent:FillContent()
|
||||
self.searchContainer.Visible = true
|
||||
self:FillPlaceInfo()
|
||||
self:FillConversations()
|
||||
end
|
||||
|
||||
function GameShareComponent:FillPlaceInfo()
|
||||
if self.placeInfoCard then
|
||||
return
|
||||
end
|
||||
|
||||
self.placeInfoCard = PlaceInfoCard.new(self.appState, self.placeInfo)
|
||||
self.placeInfoCard.rbx.Parent = self.placeInfoCardFrame
|
||||
end
|
||||
|
||||
function GameShareComponent:FillConversations()
|
||||
local conversations = self.appState.store:getState().ChatAppReducer.Conversations
|
||||
local list = ConversationList.new(self.appState, conversations, GameShareCard)
|
||||
list:SetSortWithConversationEntry(true)
|
||||
self.list = list
|
||||
list.rbx.Size = UDim2.new(1, 0, 1, 0)
|
||||
list.rbx.Parent = self.conversationListFrame
|
||||
|
||||
local tappedConnection = list.ConversationTapped:connect(function(convoId)
|
||||
local messageSentLocalTime = tick()
|
||||
|
||||
if FFlagShareGameToChatStatusAnalytics then
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.SendMessage(
|
||||
convoId,
|
||||
truncateAssetLink(self.placeInfo.url),
|
||||
messageSentLocalTime
|
||||
)):andThen(function(webStatus)
|
||||
self:ReportSendButtonTappedEvent(convoId, webStatus)
|
||||
|
||||
if webStatus == HttpStatus.MODERATED then
|
||||
local toastModel = ToastModel.new(Constants.ToastIDs.MESSAGE_WAS_MODERATED, "Feature.Chat.Message.GameLinkWasModerated")
|
||||
self.appState.store:dispatch(ShowToast(toastModel))
|
||||
end
|
||||
end)
|
||||
else
|
||||
self:ReportSendButtonTappedEvent(convoId)
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.SendMessage(
|
||||
convoId,
|
||||
truncateAssetLink(self.placeInfo.url),
|
||||
"Feature.Chat.Message.GameLinkWasModerated",
|
||||
messageSentLocalTime
|
||||
)
|
||||
)
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, tappedConnection)
|
||||
|
||||
local requestOlderConversationConnection = list.RequestOlderConversations:connect(function()
|
||||
requestOlderConversations(self.appState)
|
||||
end)
|
||||
table.insert(self.connections, requestOlderConversationConnection)
|
||||
end
|
||||
|
||||
function GameShareComponent:getOlderConversationsForSearchIfNecessary(appState)
|
||||
-- To Check:
|
||||
-- 1) Search is open
|
||||
-- 2) Not have loaded all conversations.
|
||||
-- 3) Not Ccrrently getting older conversations
|
||||
-- 4) Having enouth search items to show
|
||||
-- Note that we already try to load more conversations if we scroll down to the bottom of the list
|
||||
local state = self.appState.store:getState()
|
||||
local isSearchOpen = (self.searchBox.Text) ~= nil and (self.searchBox.Text ~= "")
|
||||
if (not isSearchOpen) or state.ChatAppReducer.ConversationsAsync.oldestConversationIsFetched
|
||||
or state.ChatAppReducer.ConversationsAsync.pageConversationsIsFetching then
|
||||
return
|
||||
end
|
||||
|
||||
if self.list.rbx.CanvasSize.Y.Offset > self.list.rbx.AbsoluteSize.Y then
|
||||
return
|
||||
end
|
||||
|
||||
requestOlderConversations(self.appState)
|
||||
end
|
||||
|
||||
function GameShareComponent:ReportSendButtonTappedEvent(convoId, httpStatus)
|
||||
local eventName = "clickSendBtnFromGameShareCard"
|
||||
local eventContext = "touch"
|
||||
|
||||
local player = PlayerService.LocalPlayer
|
||||
local userId = "UNKNOWN"
|
||||
if player then
|
||||
userId = tostring(player.UserId)
|
||||
end
|
||||
|
||||
local additionalArgs = {
|
||||
uid = userId,
|
||||
placeid = self.placeId,
|
||||
cid = convoId,
|
||||
httpStatus = httpStatus,
|
||||
}
|
||||
|
||||
self._analytics.EventStream:setRBXEvent(eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
function GameShareComponent:Stop()
|
||||
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
|
||||
end
|
||||
|
||||
function GameShareComponent:Destruct()
|
||||
if self.list then
|
||||
self.list:Destruct()
|
||||
end
|
||||
if self.header then
|
||||
self.header:Destroy()
|
||||
end
|
||||
if self.placeInfoCard then
|
||||
self.placeInfoCard:Destruct()
|
||||
end
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return GameShareComponent
|
||||
@@ -0,0 +1,344 @@
|
||||
local Players = game:GetService("Players")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Signal = require(Common.Signal)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Functional = require(Common.Functional)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
local SectionComponent = require(Components.ListSection)
|
||||
local ActionEntryComponent = require(Components.ActionEntry)
|
||||
local UserListComponent = require(Components.UserList)
|
||||
local ListEntryComponent = require(Components.ListEntry)
|
||||
local ResponseIndicator = require(Components.ResponseIndicator)
|
||||
local GenericDialogType = require(Components.GroupDetailDialogs.GenericDialogType)
|
||||
|
||||
local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle)
|
||||
|
||||
local ConversationModel = require(LuaChat.Models.Conversation)
|
||||
|
||||
local SetRoute = require(LuaChat.Actions.SetRoute)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
|
||||
local PARTICIPANT_VIEW = 1
|
||||
local PARTICIPANT_REPORT = 2
|
||||
local PARTICIPANT_REMOVE = 3
|
||||
|
||||
local function getAsset(name)
|
||||
return "rbxasset://textures/ui/LuaChat/"..name..".png"
|
||||
end
|
||||
|
||||
local SeeMoreButton = {}
|
||||
SeeMoreButton.__index = SeeMoreButton
|
||||
|
||||
function SeeMoreButton.new(appState)
|
||||
local self = {}
|
||||
setmetatable(self, SeeMoreButton)
|
||||
|
||||
local listEntry = ListEntryComponent.new(appState, 40)
|
||||
|
||||
self.rbx = listEntry.rbx
|
||||
self.tapped = listEntry.tapped
|
||||
|
||||
local label = Create.new"TextLabel" {
|
||||
Name = "Label",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -12, 1, 0),
|
||||
Position = UDim2.new(0, 12, 0, 0),
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
TextColor3 = Constants.Color.BLUE_PRIMARY,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
Text = appState.localization:Format("Feature.Chat.Action.SeeMoreFriends"),
|
||||
}
|
||||
label.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
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function SeeMoreButton:Update(text)
|
||||
self.rbx.Label.Text = text
|
||||
end
|
||||
|
||||
local GroupDetail = {}
|
||||
GroupDetail.__index = GroupDetail
|
||||
|
||||
function GroupDetail.new(appState, convoId)
|
||||
local self = {}
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
setmetatable(self, GroupDetail)
|
||||
|
||||
self.appState = appState
|
||||
self.conversationId = convoId
|
||||
self.AddFriendsPressed = Signal.new()
|
||||
|
||||
self.oldState = nil
|
||||
self.header = HeaderLoader.GetHeader(appState, Intent.GroupDetail)
|
||||
self.header:SetTitle(appState.localization:Format("Feature.Chat.Label.ChatDetails"))
|
||||
self.header:SetDefaultSubtitle()
|
||||
self.header:SetBackButtonEnabled(true)
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
self.header.rbx,
|
||||
Create.new "ScrollingFrame" {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ScrollBarThickness = 0,
|
||||
Create.new"Frame" {
|
||||
Name = "Content",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
Create.new"UIListLayout" {
|
||||
Name = "ListLayout",
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
local scrollingFrame = self.rbx.ScrollingFrame
|
||||
local content = scrollingFrame.Content
|
||||
|
||||
self.BackButtonPressed = self.header.BackButtonPressed
|
||||
|
||||
scrollingFrame.Position = UDim2.new(0, 0, 0, self.header.rbx.Size.Y.Offset)
|
||||
scrollingFrame.Size = UDim2.new(1, 0, 1, -self.header.rbx.Size.Y.Offset)
|
||||
|
||||
self.general = SectionComponent.new(appState, "Feature.Chat.Label.General")
|
||||
self.general.rbx.LayoutOrder = 1
|
||||
self.general.rbx.Parent = content
|
||||
|
||||
self.groupName = ActionEntryComponent.new(appState, getAsset("icons/ic-nametag"), "Feature.Chat.Label.ChatGroupName")
|
||||
self.groupName.rbx.LayoutOrder = 2
|
||||
self.groupName.rbx.Parent = content
|
||||
local groupNameConnection = self.groupName.tapped.Event:Connect(function()
|
||||
self.appState.store:dispatch(SetRoute(Intent.GenericDialog, {
|
||||
dialog = GenericDialogType.EditChatGroupNameDialog,
|
||||
dialogParameters = {
|
||||
titleLocalizationKey = "Feature.Chat.Label.ChatGroupName",
|
||||
maxChar = 150,
|
||||
conversation = self.conversation,
|
||||
}
|
||||
}
|
||||
))
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, groupNameConnection)
|
||||
else
|
||||
table.insert(self.connections, groupNameConnection)
|
||||
end
|
||||
|
||||
self.responseIndicator = ResponseIndicator.new(appState)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
self.responseIndicator.rbx.Parent = self.rbx
|
||||
|
||||
local members = SectionComponent.new(appState, "Feature.Chat.Label.Members")
|
||||
members.rbx.LayoutOrder = 5
|
||||
members.rbx.Parent = content
|
||||
|
||||
self.addFriends = ActionEntryComponent.new(appState, getAsset("icons/ic-add-friends"),
|
||||
"Feature.Chat.Label.AddFriends", 36)
|
||||
self.addFriends.rbx.LayoutOrder = 6
|
||||
self.addFriends:SetDividerOffset(60)
|
||||
self.addFriends.rbx.Parent = content
|
||||
|
||||
self.AddFriendsPressed = self.addFriends.tapped.Event
|
||||
|
||||
self.participantsList = UserListComponent.new(appState, getAsset("icons/ic-more"))
|
||||
self.participantsList.rbx.LayoutOrder = 7
|
||||
self.participantsList.rbx.Parent = content
|
||||
local userSelectedConnection = self.participantsList.userSelected:connect(function(user)
|
||||
if user.id ~= tostring(Players.LocalPlayer.UserId) then
|
||||
self.appState.store:dispatch(SetRoute(Intent.GenericDialog, {
|
||||
dialog = GenericDialogType.ParticipantDialog,
|
||||
dialogParameters = {
|
||||
titleKey = "Feature.Chat.Heading.Option",
|
||||
options = {
|
||||
[PARTICIPANT_VIEW] = "Feature.Chat.Label.ViewProfile",
|
||||
[PARTICIPANT_REPORT] = "Feature.Chat.Action.ReportUser",
|
||||
[PARTICIPANT_REMOVE] = "Feature.Chat.Action.RemoveFromGroup",
|
||||
},
|
||||
conversationId = self.conversationId,
|
||||
conversation = self.conversation,
|
||||
userId = user.id
|
||||
}
|
||||
}
|
||||
))
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, userSelectedConnection)
|
||||
|
||||
self.seeMore = SeeMoreButton.new(appState)
|
||||
self.seeMore.rbx.LayoutOrder = 9
|
||||
self.seeMore.rbx.Parent = content
|
||||
self.showAllParticipants = false
|
||||
local seeMoreConnection = self.seeMore.tapped:connect(function()
|
||||
if self.showAllParticipants then
|
||||
self.showAllParticipants = false
|
||||
self:Update(appState.store:getState())
|
||||
else
|
||||
self.showAllParticipants = true
|
||||
self:Update(appState.store:getState())
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, seeMoreConnection)
|
||||
|
||||
self.blankSection = SectionComponent.new(appState)
|
||||
self.blankSection.rbx.LayoutOrder = 10
|
||||
self.blankSection.rbx.Parent = content
|
||||
|
||||
self.leaveGroup = ActionEntryComponent.new(appState, getAsset("icons/ic-leave"), "Feature.Chat.Heading.LeaveGroup")
|
||||
self.leaveGroup.rbx.LayoutOrder = 11
|
||||
self.leaveGroup.rbx.Parent = content
|
||||
local leaveGroupConnection = self.leaveGroup.tapped.Event:Connect(function()
|
||||
self.appState.store:dispatch(SetRoute(Intent.GenericDialog, {
|
||||
dialog = GenericDialogType.LeaveGroupDialog,
|
||||
dialogParameters = {
|
||||
titleKey = "Feature.Chat.Heading.LeaveGroup",
|
||||
messageKey = "Feature.Chat.Message.LeaveGroup",
|
||||
cancelTitleKey = "Feature.Chat.Action.Stay",
|
||||
confirmationTitleKey = "Feature.Chat.Action.Leave",
|
||||
conversation = self.conversation
|
||||
}
|
||||
}
|
||||
))
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, leaveGroupConnection)
|
||||
else
|
||||
table.insert(self.connections, leaveGroupConnection)
|
||||
end
|
||||
|
||||
content.ListLayout:ApplyLayout()
|
||||
|
||||
self.conversation = ConversationModel.empty()
|
||||
|
||||
self:Update(appState.store:getState())
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function GroupDetail:Update(state)
|
||||
local conversationId = state.ChatAppReducer.Location.current.parameters.conversationId
|
||||
local conversation = state.ChatAppReducer.Conversations[conversationId]
|
||||
self.header:SetConnectionState(state.ConnectionState)
|
||||
if conversation ~= nil then --if conversation ~= self.conversation then
|
||||
if conversation.id ~= self.conversation.id then
|
||||
self.showAllParticipants = false
|
||||
end
|
||||
|
||||
if conversation.isUserLeaving ~= self.conversation.isUserLeaving then
|
||||
self.responseIndicator:SetVisible(conversation.isUserLeaving)
|
||||
end
|
||||
|
||||
if conversation.isDefaultTitle then
|
||||
local notSetLocalized = self.appState.localization:Format("Feature.Chat.Label.NotSet")
|
||||
self.groupName:Update(notSetLocalized)
|
||||
else
|
||||
self.groupName:Update(getConversationDisplayTitle(conversation))
|
||||
end
|
||||
|
||||
local count = 0
|
||||
local users = Functional.Map(conversation.participants, function(userId)
|
||||
count = count + 1
|
||||
return (count <= 3 or self.showAllParticipants) and state.Users[userId] or nil
|
||||
end)
|
||||
|
||||
if count > 3 and not self.showAllParticipants then
|
||||
local messageArguments = {
|
||||
NUMBER_OF_FRIENDS = tostring(count-3)
|
||||
}
|
||||
local message = self.appState.localization:Format("Feature.Chat.Action.SeeMoreFriends", messageArguments)
|
||||
self.seeMore:Update(message)
|
||||
self.seeMore.rbx.Visible = true
|
||||
elseif count > 3 then
|
||||
self.seeMore:Update(self.appState.localization:Format("Feature.Chat.Label.SeeLess"))
|
||||
self.seeMore.rbx.Visible = true
|
||||
else
|
||||
self.seeMore.rbx.Visible = false
|
||||
end
|
||||
|
||||
self.participantsList:Update(users)
|
||||
|
||||
if conversation.conversationType == ConversationModel.Type.MULTI_USER_CONVERSATION then
|
||||
self.general.rbx.Visible = true
|
||||
self.groupName.rbx.Visible = true
|
||||
self.leaveGroup.rbx.Visible = true
|
||||
self.blankSection.rbx.Visible = true
|
||||
elseif conversation.conversationType == ConversationModel.Type.ONE_TO_ONE_CONVERSATION then
|
||||
self.general.rbx.Visible = false
|
||||
self.groupName.rbx.Visible = false
|
||||
self.leaveGroup.rbx.Visible = false
|
||||
self.blankSection.rbx.Visible = false
|
||||
end
|
||||
|
||||
self.conversation = conversation
|
||||
end
|
||||
if self.oldState == nil or state.ChatAppReducer.Location.current ~= self.oldState.ChatAppReducer.Location.current then
|
||||
if self.oldState ~= nil and state.ChatAppReducer.Location.current.intent == Intent.GroupDetail then
|
||||
-- If any Dialog is mounted on ModalBase, close them.
|
||||
if self.oldState.ChatAppReducer.Location.current.intent == Intent.GenericDialog then
|
||||
self.oldState.ChatAppReducer.Location.current.parameters.dialog:Close()
|
||||
end
|
||||
end
|
||||
end
|
||||
local highestYValue = 0
|
||||
for _, element in pairs(self.rbx.ScrollingFrame.Content:GetChildren()) do
|
||||
if element:IsA("GuiObject") then
|
||||
highestYValue = highestYValue + element.Size.Y.Offset
|
||||
end
|
||||
end
|
||||
self.rbx.ScrollingFrame.CanvasSize = UDim2.new(1, 0, 0, highestYValue)
|
||||
self.rbx.ScrollingFrame.Content.Size = UDim2.new(1, 0, 0, highestYValue)
|
||||
|
||||
self.oldState = state
|
||||
end
|
||||
|
||||
function GroupDetail:Stop()
|
||||
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
|
||||
end
|
||||
|
||||
function GroupDetail:Destruct()
|
||||
self.responseIndicator:Destruct()
|
||||
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return GroupDetail
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
local Modules = script.Parent.Parent.Parent
|
||||
local Components = Modules.Components
|
||||
|
||||
local DialogComponents = require(Components.DialogComponents)
|
||||
local ConversationActions = require(Modules.Actions.ConversationActions)
|
||||
local ResponseIndicator = require(Components.ResponseIndicator)
|
||||
|
||||
local EditChatGroupNameDialog = {}
|
||||
|
||||
function EditChatGroupNameDialog.new(appState, titleLocalizationKey, maxChar, conversation)
|
||||
local self = {}
|
||||
setmetatable(self, {__index = EditChatGroupNameDialog})
|
||||
|
||||
self.dialog = DialogComponents.TextInputDialog.new(appState, titleLocalizationKey, maxChar)
|
||||
|
||||
-- Setup ResponseIndicator
|
||||
self.responseIndicator = ResponseIndicator.new(appState)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
self.responseIndicator.rbx.Parent = self.dialog.rbx
|
||||
|
||||
self.conversation = conversation
|
||||
|
||||
self:UpdateGroupName()
|
||||
|
||||
-- Define saved action
|
||||
self.savedConnection = self.dialog.saved:connect(function(newName)
|
||||
self.responseIndicator:SetVisible(true)
|
||||
local callback = function()
|
||||
self.responseIndicator:SetVisible(false)
|
||||
end
|
||||
local action = ConversationActions.RenameGroupConversation(self.conversation.id, newName, callback)
|
||||
appState.store:dispatch(action)
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function EditChatGroupNameDialog:UpdateGroupName()
|
||||
local groupName = ""
|
||||
|
||||
if not self.conversation.isDefaultTitle then
|
||||
groupName = self.conversation.title
|
||||
end
|
||||
|
||||
self.dialog:Update(groupName)
|
||||
end
|
||||
|
||||
function EditChatGroupNameDialog:Destruct()
|
||||
if self.savedConnection then
|
||||
self.savedConnection:disconnect()
|
||||
end
|
||||
self.responseIndicator:Destruct()
|
||||
self.dialog:Destruct()
|
||||
end
|
||||
|
||||
return EditChatGroupNameDialog
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
local GenericDialogType = {
|
||||
EditChatGroupNameDialog = "EditChatGroupNameDialog",
|
||||
LeaveGroupDialog = "LeaveGroupDialog",
|
||||
ParticipantDialog = "ParticipantDialog",
|
||||
RemoveUserDialog = "RemoveUserDialog",
|
||||
}
|
||||
|
||||
return GenericDialogType
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = script.Parent.Parent.Parent
|
||||
local Components = Modules.Components
|
||||
|
||||
local DialogComponents = require(Components.DialogComponents)
|
||||
local ConversationActions = require(Modules.Actions.ConversationActions)
|
||||
|
||||
local LeaveGroupDialog = {}
|
||||
|
||||
function LeaveGroupDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey, conversation)
|
||||
local self = {}
|
||||
setmetatable(self, {__index = LeaveGroupDialog})
|
||||
|
||||
self.dialog = DialogComponents.ConfirmationDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey)
|
||||
|
||||
self.conversation = conversation
|
||||
|
||||
self.dialogConnection = self.dialog.saved:connect(function()
|
||||
local userId = tostring(Players.LocalPlayer.UserId)
|
||||
local convoId = self.conversation.id
|
||||
local action = ConversationActions.RemoveUserFromConversation(userId, convoId)
|
||||
appState.store:dispatch(action)
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function LeaveGroupDialog:Destruct()
|
||||
if self.dialogConnection then
|
||||
self.dialogConnection:disconnect()
|
||||
end
|
||||
self.dialog:Destruct()
|
||||
end
|
||||
|
||||
return LeaveGroupDialog
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
local Players = game:GetService("Players")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local LuaApp = CoreGui.RobloxGui.Modules.LuaApp
|
||||
local LuaChat = CoreGui.RobloxGui.Modules.LuaChat
|
||||
|
||||
local GenericDialogType = require(LuaChat.Components.GroupDetailDialogs.GenericDialogType)
|
||||
local DialogComponents = require(LuaChat.Components.DialogComponents)
|
||||
local WebApi = require(LuaChat.WebApi)
|
||||
local ConversationModel = require(LuaChat.Models.Conversation)
|
||||
local SetRoute = require(LuaChat.Actions.SetRoute)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
|
||||
local NotificationType = require(LuaApp.Enum.NotificationType)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local PARTICIPANT_VIEW = 1
|
||||
local PARTICIPANT_REPORT = 2
|
||||
local PARTICIPANT_REMOVE = 3
|
||||
|
||||
local ParticipantDialog = {}
|
||||
|
||||
function ParticipantDialog.new(appState, titleKey, options, conversationId, conversation, userId)
|
||||
local self = {}
|
||||
setmetatable(self, {__index = ParticipantDialog})
|
||||
|
||||
self.appState = appState
|
||||
self.dialog = DialogComponents.OptionDialog.new(appState, titleKey, options, userId)
|
||||
|
||||
self.conversationId = conversationId
|
||||
self.conversation = conversation
|
||||
|
||||
if conversation ~= nil then
|
||||
if conversation.initiator == tostring(Players.LocalPlayer.UserId)
|
||||
and conversation.conversationType == ConversationModel.Type.MULTI_USER_CONVERSATION then
|
||||
self.dialog.optionGuis[PARTICIPANT_REMOVE].Visible = true
|
||||
else
|
||||
self.dialog.optionGuis[PARTICIPANT_REMOVE].Visible = false
|
||||
end
|
||||
self.dialog:Resize()
|
||||
end
|
||||
|
||||
self.dialogConnection = self.dialog.selected:connect(function(optionId, userIdSelected)
|
||||
local user = self.appState.store:getState().Users[userIdSelected]
|
||||
if user == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if optionId == PARTICIPANT_VIEW then
|
||||
if user and user.id and (type(user.id) == 'string' or type(user.id) == 'number') then
|
||||
GuiService:BroadcastNotification(WebApi.MakeUserProfileUrl(user.id),
|
||||
NotificationType.VIEW_PROFILE)
|
||||
else
|
||||
print("Bad input to RequestNativeView, show error prompt here")
|
||||
end
|
||||
elseif optionId == PARTICIPANT_REPORT then
|
||||
if user and user.id and (type(user.id) == 'string' or type(user.id) == 'number') then
|
||||
GuiService:BroadcastNotification(WebApi.MakeReportUserUrl(user.id, conversationId),
|
||||
NotificationType.REPORT_ABUSE)
|
||||
else
|
||||
print("Bad input to RequestNativeView, show error prompt here")
|
||||
end
|
||||
elseif optionId == PARTICIPANT_REMOVE then
|
||||
local messageArguments = {
|
||||
USERNAME = user.name,
|
||||
}
|
||||
self.appState.store:dispatch(SetRoute(Intent.GenericDialog, {
|
||||
dialog = GenericDialogType.RemoveUserDialog,
|
||||
dialogParameters = {
|
||||
titleKey = "Feature.Chat.Action.RemoveUser",
|
||||
messageKey = "Feature.Chat.Message.RemoveUser",
|
||||
cancelTitleKey = "Feature.Chat.Action.Cancel",
|
||||
confirmationTitleKey = "Feature.Chat.Action.Remove",
|
||||
conversation = self.conversation,
|
||||
user = user,
|
||||
messageArguments = messageArguments
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ParticipantDialog:Destruct()
|
||||
if self.dialogConnection then
|
||||
self.dialogConnection:disconnect()
|
||||
end
|
||||
self.dialog:Destruct()
|
||||
end
|
||||
|
||||
return ParticipantDialog
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
local Modules = script.Parent.Parent.Parent
|
||||
local Components = Modules.Components
|
||||
|
||||
local DialogComponents = require(Components.DialogComponents)
|
||||
local ConversationActions = require(Modules.Actions.ConversationActions)
|
||||
local ResponseIndicator = require(Components.ResponseIndicator)
|
||||
|
||||
local RemoveUserDialog = {}
|
||||
|
||||
function RemoveUserDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey, conversation)
|
||||
local self = {}
|
||||
setmetatable(self, {__index = RemoveUserDialog})
|
||||
|
||||
self.appState = appState
|
||||
self.dialog = DialogComponents.ConfirmationDialog.new(appState, titleKey, messageKey, cancelTitleKey, confirmTitleKey)
|
||||
|
||||
-- Setup ResponseIndicator
|
||||
self.responseIndicator = ResponseIndicator.new(appState)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
self.responseIndicator.rbx.Parent = self.dialog.rbx
|
||||
|
||||
self.conversation = conversation
|
||||
|
||||
self.dialogConnection = self.dialog.saved:connect(function(user)
|
||||
local userId = user.id
|
||||
local convoId = self.conversation.id
|
||||
|
||||
self.responseIndicator.rbx.Parent = self.rbx
|
||||
self.responseIndicator:SetVisible(true)
|
||||
|
||||
local action = ConversationActions.RemoveUserFromConversation(userId, convoId, function()
|
||||
self.responseIndicator:SetVisible(false)
|
||||
end)
|
||||
self.appState.store:dispatch(action)
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function RemoveUserDialog:Destruct()
|
||||
if self.dialogConnection then
|
||||
self.dialogConnection:disconnect()
|
||||
end
|
||||
self.responseIndicator:Destruct()
|
||||
self.dialog:Destruct()
|
||||
end
|
||||
|
||||
return RemoveUserDialog
|
||||
@@ -0,0 +1,384 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local LuaAppFlagSettings = require(LuaApp.FlagSettings)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local BaseHeader = require(Components.BaseHeader)
|
||||
local TextButton = require(Components.TextButton)
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local UseCppTextTruncation = FlagSettings.UseCppTextTruncation()
|
||||
local GroupChatIconEnabled = settings():GetFFlag("LuaChatGroupChatIconEnabled")
|
||||
local FFlagLuaChatFlexibleTitleWidth = settings():GetFFlag("LuaChatFlexibleTitleWidth")
|
||||
|
||||
local HEIGHT_OF_DISCONNECTED = 32
|
||||
|
||||
local PLATFORM_SPECIFIC_CONSTANTS = {
|
||||
[Enum.Platform.Android] = {
|
||||
HEADER_CONTENT_FRAME_Y_OFFSET = 0,
|
||||
HEADER_TITLE_FRAME_POSITION_NO_BACK_BUTTON = UDim2.new(0, 15, 0, 0),
|
||||
HEADER_TITLE_FRAME_POSITION = UDim2.new(0, 72, 0, 0),
|
||||
HEADER_TITLE_FRAME_ANCHOR_POINT = Vector2.new(0, 0),
|
||||
HEADER_VERTICAL_ALIGNMENT = Enum.VerticalAlignment.Center,
|
||||
HEADER_HORIZONTAL_ALIGNMENT = Enum.HorizontalAlignment.Left,
|
||||
HEADER_TEXT_X_ALIGNMENT = 0,
|
||||
},
|
||||
Default = {
|
||||
HEADER_CONTENT_FRAME_Y_OFFSET = 24,
|
||||
HEADER_TITLE_FRAME_POSITION_NO_BACK_BUTTON = UDim2.new(0.5, 0, 0, 0),
|
||||
HEADER_TITLE_FRAME_POSITION = UDim2.new(0.5, 0, 0, 0),
|
||||
HEADER_TITLE_FRAME_ANCHOR_POINT = Vector2.new(0.5, 0),
|
||||
HEADER_VERTICAL_ALIGNMENT = Enum.VerticalAlignment.Top,
|
||||
HEADER_HORIZONTAL_ALIGNMENT = Enum.HorizontalAlignment.Center,
|
||||
HEADER_TEXT_X_ALIGNMENT = 2,
|
||||
},
|
||||
}
|
||||
|
||||
local GROUP_CHAT_ICON_HEIGHT = 25
|
||||
local GROUP_CHAT_ICON_WIDTH = 25
|
||||
local GROUP_CHAT_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-group-16x16.png"
|
||||
local TITLE_LABEL_HEIGHT = 25
|
||||
local TITLE_LABEL_WIDTH = 200
|
||||
local TITLE_ITEM_PADDING = 5
|
||||
local SUBTITLE_LABEL_HEIGHT = 12
|
||||
local SUBTITLE_LABEL_WIDTH = 200
|
||||
|
||||
local function getPlatformSpecific(platform)
|
||||
return PLATFORM_SPECIFIC_CONSTANTS[platform] or PLATFORM_SPECIFIC_CONSTANTS.Default
|
||||
end
|
||||
|
||||
local Header = BaseHeader:Template()
|
||||
Header.__index = Header
|
||||
|
||||
function Header.new(appState, dialogType)
|
||||
local self = {}
|
||||
setmetatable(self, Header)
|
||||
|
||||
local platform = appState.store:getState().Platform
|
||||
|
||||
local isLuaAppStarterScriptEnabled = LuaAppFlagSettings:IsLuaAppStarterScriptEnabled()
|
||||
|
||||
self:SetPlatform(platform)
|
||||
local platformConstants = getPlatformSpecific(platform)
|
||||
|
||||
self.heightOfHeader = UserInputService.NavBarSize.Y + UserInputService.StatusBarSize.Y
|
||||
self.heightOfDisconnected = HEIGHT_OF_DISCONNECTED
|
||||
|
||||
self.buttons = {}
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
self.appState = appState
|
||||
self.dialogType = dialogType
|
||||
self.backButton = BaseHeader:GetNewBackButton(dialogType)
|
||||
self.backButton.rbx.Visible = false
|
||||
self.title = ""
|
||||
self.subtitle = nil
|
||||
self.connectionState = Enum.ConnectionState.Connected
|
||||
|
||||
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
|
||||
self.appState.store:getState().FormFactor)
|
||||
|
||||
self.BackButtonPressed = Signal.new()
|
||||
local backButtonConnection = self.backButton.Pressed:connect(function()
|
||||
self.BackButtonPressed:fire()
|
||||
end)
|
||||
table.insert(self.connections, backButtonConnection)
|
||||
|
||||
local titleLabelSize
|
||||
if FFlagLuaChatFlexibleTitleWidth then
|
||||
titleLabelSize = UDim2.new(1, 0, 0, TITLE_LABEL_HEIGHT)
|
||||
else
|
||||
titleLabelSize = UDim2.new(0, TITLE_LABEL_WIDTH, 0, TITLE_LABEL_HEIGHT)
|
||||
end
|
||||
|
||||
self.titleLabel = Create.new "TextLabel" {
|
||||
Name = "Title",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.SourceSansBold,
|
||||
LayoutOrder = 1,
|
||||
Size = titleLabelSize,
|
||||
Text = self.title,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = Constants.Font.FONT_SIZE_20,
|
||||
TextXAlignment = platformConstants.HEADER_TEXT_X_ALIGNMENT,
|
||||
}
|
||||
|
||||
if GroupChatIconEnabled then
|
||||
self.groupChatIcon = Create.new "ImageLabel" {
|
||||
Name = "GroupChatIcon",
|
||||
Visible = false,
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 0,
|
||||
Size = UDim2.new(0, GROUP_CHAT_ICON_WIDTH, 0, GROUP_CHAT_ICON_HEIGHT),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Image = GROUP_CHAT_ICON,
|
||||
}
|
||||
|
||||
local innerTitleFrameSize
|
||||
if FFlagLuaChatFlexibleTitleWidth then
|
||||
innerTitleFrameSize = UDim2.new(1, 0, 0, GROUP_CHAT_ICON_HEIGHT)
|
||||
else
|
||||
innerTitleFrameSize = UDim2.new(0, TITLE_LABEL_WIDTH + GROUP_CHAT_ICON_WIDTH, 0, GROUP_CHAT_ICON_HEIGHT)
|
||||
end
|
||||
|
||||
self.innerTitleFrame = Create.new "Frame" {
|
||||
Name = "InnerTitleFrame",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 0,
|
||||
Size = innerTitleFrameSize,
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, TITLE_ITEM_PADDING),
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = platformConstants.HEADER_HORIZONTAL_ALIGNMENT,
|
||||
},
|
||||
|
||||
self.groupChatIcon,
|
||||
self.titleLabel,
|
||||
}
|
||||
if FFlagLuaChatFlexibleTitleWidth then
|
||||
self.titleLabel.Size = UDim2.new(1, -(GROUP_CHAT_ICON_WIDTH + TITLE_ITEM_PADDING), 0, TITLE_LABEL_HEIGHT)
|
||||
end
|
||||
end
|
||||
|
||||
self.innerSubtitle = Create.new "TextLabel" {
|
||||
Name = "Subtitle",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(0, SUBTITLE_LABEL_WIDTH, 0, SUBTITLE_LABEL_HEIGHT),
|
||||
Text = "",
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = Constants.Font.FONT_SIZE_12,
|
||||
TextXAlignment = platformConstants.HEADER_TEXT_X_ALIGNMENT,
|
||||
}
|
||||
|
||||
self.innerTitles = Create.new "Frame" {
|
||||
Name = "Titles",
|
||||
AnchorPoint = platformConstants.HEADER_TITLE_FRAME_ANCHOR_POINT,
|
||||
BackgroundTransparency = 1,
|
||||
Position = self:GetHeaderTitleFramePosition(),
|
||||
Size = UDim2.new(0, TITLE_LABEL_WIDTH, 1, 0),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = isLuaAppStarterScriptEnabled and Enum.VerticalAlignment.Center
|
||||
or platformConstants.HEADER_VERTICAL_ALIGNMENT,
|
||||
HorizontalAlignment = platformConstants.HEADER_HORIZONTAL_ALIGNMENT,
|
||||
},
|
||||
}
|
||||
|
||||
if GroupChatIconEnabled then
|
||||
self.innerTitleFrame.Parent = self.innerTitles
|
||||
else
|
||||
self.titleLabel.Parent = self.innerTitles
|
||||
end
|
||||
|
||||
if not isLuaAppStarterScriptEnabled then
|
||||
self.innerSubtitle.Parent = self.innerTitles
|
||||
end
|
||||
|
||||
self.innerButtons = Create.new "Frame" {
|
||||
Name = "Buttons",
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(1, -5, 0, 0),
|
||||
Size = UDim2.new(0, 100, 1, 0),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = platformConstants.HEADER_VERTICAL_ALIGNMENT,
|
||||
},
|
||||
}
|
||||
|
||||
self.innerContent = Create.new "Frame" {
|
||||
Name = "Content",
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, UserInputService.StatusBarSize.Y),
|
||||
Size = UDim2.new(1, 0, 0, UserInputService.NavBarSize.Y),
|
||||
|
||||
self.backButton.rbx,
|
||||
self.innerTitles,
|
||||
self.innerButtons,
|
||||
}
|
||||
|
||||
self.innerHeader = Create.new "Frame" {
|
||||
Name = "Header",
|
||||
BackgroundColor3 = Constants.Color.BLUE_PRESSED,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, 0, 0, self.heightOfHeader),
|
||||
|
||||
self.innerContent,
|
||||
}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "HeaderFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, self.heightOfHeader),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||
},
|
||||
|
||||
self.innerHeader,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Disconnected",
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
BackgroundColor3 = Constants.Color.GRAY3,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately has zero vertical height, will be scaled when shown.
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Title",
|
||||
AnchorPoint = Vector2.new(0.5, 1),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
LayoutOrder = 0,
|
||||
Position = UDim2.new(0.5, 0, 1, 0),
|
||||
Size = UDim2.new(1, 0, 0, HEIGHT_OF_DISCONNECTED),
|
||||
Text = appState.localization:Format("Feature.Chat.Message.NoConnectionMsg"),
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
},
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "GameDrawer",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = false,
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately zero height, will be scaled open.
|
||||
Visible = false,
|
||||
},
|
||||
}
|
||||
|
||||
local rbxConnectionsList = self.connections
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
rbxConnectionsList = self.rbx_connections
|
||||
end
|
||||
|
||||
local parentChangedConnection = self.rbx:GetPropertyChangedSignal("Parent"):Connect(function()
|
||||
if self.rbx and self.rbx.Parent then
|
||||
if not UseCppTextTruncation then
|
||||
game:GetService("RunService").Stepped:wait() -- TextBounds isn't recalculated when this fires so we wait
|
||||
end
|
||||
self:SetTitle(self.title) -- Again, this can be much cleaner once we have proper truncation support
|
||||
end
|
||||
end)
|
||||
table.insert(rbxConnectionsList, parentChangedConnection)
|
||||
|
||||
local navBarSignal = UserInputService:GetPropertyChangedSignal("NavBarSize")
|
||||
local navBarConnection = navBarSignal:Connect(function()
|
||||
self:AdjustLayout()
|
||||
end)
|
||||
local statusBarSignal = UserInputService:GetPropertyChangedSignal("StatusBarSize")
|
||||
local statusBarConnection = statusBarSignal:Connect(function()
|
||||
self:AdjustLayout()
|
||||
end)
|
||||
self:AdjustLayout()
|
||||
table.insert(rbxConnectionsList, navBarConnection)
|
||||
table.insert(rbxConnectionsList, statusBarConnection)
|
||||
|
||||
if FFlagLuaChatFlexibleTitleWidth then
|
||||
table.insert(rbxConnectionsList, self.innerTitles:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:SetTitle(self.title) -- Call SetTitle to truncate text according to the new size.
|
||||
end))
|
||||
|
||||
table.insert(rbxConnectionsList, self.innerButtons:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:AdjustTitleHeader()
|
||||
end))
|
||||
|
||||
table.insert(rbxConnectionsList, self.backButton.rbx:GetPropertyChangedSignal("Visible"):Connect(function()
|
||||
self:AdjustTitleHeader()
|
||||
end))
|
||||
self:AdjustTitleHeader()
|
||||
end
|
||||
|
||||
do
|
||||
local connection = appState.store.changed:connect(function(state, oldState)
|
||||
self:SetPlatform(state.Platform)
|
||||
self:SetConnectionState(state.ConnectionState)
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Header:IsBackButtonVisible()
|
||||
return self.backButton and self.backButton.rbx and self.backButton.rbx.Visible
|
||||
end
|
||||
|
||||
function Header:AdjustLayout()
|
||||
self.heightOfHeader = UserInputService.NavBarSize.Y + UserInputService.StatusBarSize.Y
|
||||
self.rbx.Size = UDim2.new(1, 0, 0, self.heightOfHeader)
|
||||
self.innerHeader.Size = UDim2.new(1, 0, 0, self.heightOfHeader)
|
||||
|
||||
self.innerContent.Position = UDim2.new(0, 0, 0, UserInputService.StatusBarSize.Y)
|
||||
self.innerContent.Size = UDim2.new(1, 0, 0, UserInputService.NavBarSize.Y)
|
||||
end
|
||||
|
||||
function Header:AdjustTitleHeader()
|
||||
local backButtonWidth = self:IsBackButtonVisible() and self.backButton.rbx.AbsoluteSize.X or 0
|
||||
local buttonsWidth = self.innerButtons and self.innerButtons.AbsoluteSize.X or 0
|
||||
|
||||
local titleWidthOffset = math.max(backButtonWidth, buttonsWidth) * 2
|
||||
self.innerTitles.Size = UDim2.new(1, -titleWidthOffset, 1, 0)
|
||||
|
||||
self:SetTitle(self.title) -- Call SetTitle to truncate text according to the new size.
|
||||
end
|
||||
|
||||
function Header:CreateHeaderButton(name, textKey)
|
||||
local saveGroup = TextButton.new(self.appState, name, textKey)
|
||||
self:AddButton(saveGroup)
|
||||
return saveGroup
|
||||
end
|
||||
|
||||
function Header:SetBackButtonEnabled(enabled)
|
||||
self.backButton.rbx.Visible = enabled
|
||||
self.innerTitles.Position = self:GetHeaderTitleFramePosition()
|
||||
end
|
||||
|
||||
function Header:GetHeaderTitleFramePosition()
|
||||
if self:IsBackButtonVisible() then
|
||||
return getPlatformSpecific(self.platform).HEADER_TITLE_FRAME_POSITION
|
||||
end
|
||||
|
||||
return getPlatformSpecific(self.platform).HEADER_TITLE_FRAME_POSITION_NO_BACK_BUTTON
|
||||
end
|
||||
|
||||
function Header:SetGroupChatIconVisibility(enabled)
|
||||
if enabled then
|
||||
self.groupChatIcon.Visible = true
|
||||
else
|
||||
self.groupChatIcon.Visible = false
|
||||
end
|
||||
end
|
||||
|
||||
return Header
|
||||
@@ -0,0 +1,26 @@
|
||||
local LuaChat = script.Parent.Parent
|
||||
local Components = LuaChat.Components
|
||||
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
|
||||
local Header = require(Components.Header)
|
||||
local ModalHeader = require(Components.ModalHeader)
|
||||
|
||||
local HeaderLoader = {}
|
||||
|
||||
function HeaderLoader.GetHeader(appState, intent)
|
||||
if intent == nil then
|
||||
warn("intent passed to HeaderLoader.GetHeader is null.")
|
||||
return Header.new(appState, DialogInfo.DialogType.Centered)
|
||||
end
|
||||
|
||||
local dialogType = DialogInfo.GetTypeBasedOnIntent(appState.store:getState().FormFactor, intent)
|
||||
|
||||
if dialogType == DialogInfo.DialogType.Modal then
|
||||
return ModalHeader.new(appState, dialogType)
|
||||
else
|
||||
return Header.new(appState, dialogType)
|
||||
end
|
||||
end
|
||||
|
||||
return HeaderLoader
|
||||
@@ -0,0 +1,174 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
local sendIceBreakerAnalytics = require(LuaChat.Analytics.Events.sendIcebreaker)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
|
||||
local HELLO_BUTTON = "rbxasset://textures/ui/LuaChat/9-slice/hello-button.png"
|
||||
local HELLO_MESSAGE_KEY = "Feature.Chat.Action.Hello"
|
||||
local WAVE_EMOJI = "👋"
|
||||
|
||||
local TextMeasureTemporaryPatch = settings():GetFFlag("TextMeasureTemporaryPatch")
|
||||
|
||||
local FLASH_TWEEN_ANIMATION_DELAY_TIME = 0.6
|
||||
local FLASH_TWEEN_ANIMATION_DOES_REVERSES = false
|
||||
local FLASH_TWEEN_ANIMATION_REPEAT_COUNT = 2
|
||||
local FLASH_TWEEN_ANIMATION_SCALE_GOAL = 1.2
|
||||
local FLASH_TWEEN_ANIMATION_TIME = 0.6
|
||||
|
||||
local BUTTON_PADDING_HEIGHT = 8
|
||||
local BUTTON_PADDING_WIDTH = 18
|
||||
|
||||
local BUTTON_MARGIN_BOTTOM = 12
|
||||
local BUTTON_MARGIN_TOP = 10
|
||||
|
||||
local Icebreaker = {}
|
||||
Icebreaker.__index = Icebreaker
|
||||
|
||||
function Icebreaker.new(appState)
|
||||
local self = {}
|
||||
setmetatable(self, Icebreaker)
|
||||
self.appState = appState
|
||||
self.SendButtonPressed = Signal.new()
|
||||
self._analytics = appState.analytics
|
||||
|
||||
local helloTextWithEmoji = string.format(
|
||||
"%s %s",
|
||||
WAVE_EMOJI,
|
||||
self.appState.localization:Format(HELLO_MESSAGE_KEY)
|
||||
)
|
||||
local textBounds = Text.GetTextBounds(
|
||||
helloTextWithEmoji,
|
||||
Enum.Font.SourceSans,
|
||||
Constants.Font.FONT_SIZE_16,
|
||||
Vector2.new(10000, 10000)
|
||||
)
|
||||
|
||||
if TextMeasureTemporaryPatch then
|
||||
textBounds = textBounds + Vector2.new(-2, -2)
|
||||
end
|
||||
|
||||
self.animatorScale = Create.new "UIScale" {
|
||||
Scale = 1,
|
||||
}
|
||||
|
||||
self.icebreakerAnimator = Create.new "ImageLabel" {
|
||||
Name = "HelloButtonAnimator",
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 0,
|
||||
Size = UDim2.new(0, textBounds.X + BUTTON_PADDING_WIDTH * 2, 1, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Image = HELLO_BUTTON,
|
||||
ImageColor3 = Color3.new(0, 0, 0),
|
||||
ImageTransparency = 0.5,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(16, 16, 16, 16),
|
||||
ZIndex = 1,
|
||||
|
||||
self.animatorScale,
|
||||
}
|
||||
|
||||
self.helloButton = Create.new "ImageButton" {
|
||||
Name = "HelloButton",
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Image = HELLO_BUTTON,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(16, 16, 16, 16),
|
||||
ZIndex = 2,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Content",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
|
||||
Create.new "UIPadding" {
|
||||
PaddingBottom = UDim.new(0, BUTTON_PADDING_HEIGHT),
|
||||
PaddingLeft = UDim.new(0, BUTTON_PADDING_WIDTH),
|
||||
PaddingRight = UDim.new(0, BUTTON_PADDING_WIDTH),
|
||||
PaddingTop = UDim.new(0, BUTTON_PADDING_HEIGHT),
|
||||
},
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "HelloText",
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(0, textBounds.X, 1, 0),
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = helloTextWithEmoji,
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "IcebreakerContainer",
|
||||
BackgroundTransparency = 1,
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
Size = UDim2.new(1, 0, 0, BUTTON_MARGIN_BOTTOM + BUTTON_PADDING_HEIGHT * 2 + textBounds.Y + BUTTON_MARGIN_TOP),
|
||||
|
||||
Create.new "UIPadding" {
|
||||
PaddingBottom = UDim.new(0, BUTTON_MARGIN_BOTTOM),
|
||||
PaddingTop = UDim.new(0, BUTTON_MARGIN_TOP),
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "IceBreaker",
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(0, textBounds.X + BUTTON_PADDING_WIDTH * 2, 1, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
|
||||
self.helloButton,
|
||||
self.icebreakerAnimator,
|
||||
}
|
||||
}
|
||||
|
||||
getInputEvent(self.helloButton):Connect(function()
|
||||
self:SendMessage(helloTextWithEmoji)
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Icebreaker:SendMessage(text)
|
||||
local conversationId = tostring(self.appState.store:getState().ChatAppReducer.ActiveConversationId)
|
||||
local eventContext = "luaChat"
|
||||
local eventStreamImpl = self._analytics.EventStream
|
||||
|
||||
self.SendButtonPressed:fire(text)
|
||||
sendIceBreakerAnalytics(eventStreamImpl, eventContext, conversationId)
|
||||
end
|
||||
|
||||
function Icebreaker:PlayFlashAnimation()
|
||||
local tweenInfo = TweenInfo.new(
|
||||
FLASH_TWEEN_ANIMATION_TIME,
|
||||
Enum.EasingStyle.Sine,
|
||||
Enum.EasingDirection.Out,
|
||||
FLASH_TWEEN_ANIMATION_REPEAT_COUNT,
|
||||
FLASH_TWEEN_ANIMATION_DOES_REVERSES,
|
||||
FLASH_TWEEN_ANIMATION_DELAY_TIME
|
||||
)
|
||||
|
||||
TweenService:Create(self.icebreakerAnimator, tweenInfo, {
|
||||
ImageTransparency = 1,
|
||||
}):Play()
|
||||
TweenService:Create(self.animatorScale, tweenInfo, {
|
||||
Scale = FLASH_TWEEN_ANIMATION_SCALE_GOAL,
|
||||
}):Play()
|
||||
end
|
||||
|
||||
return Icebreaker
|
||||
@@ -0,0 +1,79 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local ListEntry = {}
|
||||
|
||||
ListEntry.__index = ListEntry
|
||||
|
||||
function ListEntry.new(appState, height)
|
||||
local self = {}
|
||||
self.connections = {}
|
||||
setmetatable(self, ListEntry)
|
||||
|
||||
self.rbx = Create.new"TextButton" {
|
||||
Name = "Entry",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, height),
|
||||
AutoButtonColor = false,
|
||||
Text = "",
|
||||
}
|
||||
|
||||
self.tapped = Signal.new()
|
||||
self.beginHover = Signal.new()
|
||||
self.endHover = Signal.new()
|
||||
|
||||
local beginningInput = nil
|
||||
local function onInputBegan(input)
|
||||
if input.UserInputType ~= Enum.UserInputType.Touch or
|
||||
(input.UserInputState ~= Enum.UserInputState.Begin and input ~= beginningInput) then
|
||||
return
|
||||
end
|
||||
beginningInput = input
|
||||
self.rbx.BackgroundColor3 = Constants.Color.GRAY5
|
||||
self.beginHover:fire()
|
||||
return
|
||||
end
|
||||
|
||||
local function onInputEnded(input, processed)
|
||||
if input.UserInputType ~= Enum.UserInputType.Touch then
|
||||
return
|
||||
end
|
||||
beginningInput = nil
|
||||
self.rbx.BackgroundColor3 = Constants.Color.WHITE
|
||||
self.endHover:fire()
|
||||
return
|
||||
end
|
||||
|
||||
local inputBeganConnection = self.rbx.InputBegan:Connect(onInputBegan)
|
||||
table.insert(self.connections, inputBeganConnection)
|
||||
local inputEndedConnection = self.rbx.InputEnded:Connect(onInputEnded)
|
||||
table.insert(self.connections, inputEndedConnection)
|
||||
|
||||
local mouseClickedConnection = getInputEvent(self.rbx):Connect(function()
|
||||
self.tapped:fire()
|
||||
end)
|
||||
table.insert(self.connections, mouseClickedConnection)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ListEntry:Destruct()
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return ListEntry
|
||||
@@ -0,0 +1,61 @@
|
||||
local function findAncestor(child, name)
|
||||
local parent = child.Parent
|
||||
while parent and parent.Name ~= name do
|
||||
parent = parent.Parent
|
||||
end
|
||||
return parent
|
||||
end
|
||||
|
||||
local DIVIDER_SIZE_LARGE = 26
|
||||
local DIVIDER_SIZE_SMALL = 12
|
||||
|
||||
local Modules = findAncestor(script, "LuaChat")
|
||||
local Create = require(Modules.Create)
|
||||
local Constants = require(Modules.Constants)
|
||||
|
||||
local ListSection = {}
|
||||
|
||||
ListSection.__index = ListSection
|
||||
|
||||
function ListSection.new(appState, labelKey, layoutOrder)
|
||||
local self = {}
|
||||
setmetatable(self, ListSection)
|
||||
|
||||
local labelHeight = labelKey and DIVIDER_SIZE_LARGE or DIVIDER_SIZE_SMALL
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, labelHeight),
|
||||
LayoutOrder = layoutOrder,
|
||||
}
|
||||
|
||||
if labelKey then
|
||||
local labelText = appState.localization:Format(labelKey)
|
||||
local label = Create.new"TextLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 12, 1, 0),
|
||||
BorderSizePixel = 0,
|
||||
Text = labelText,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
AnchorPoint = Vector2.new(0,1),
|
||||
}
|
||||
label.Parent = self.rbx
|
||||
end
|
||||
|
||||
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
|
||||
return self
|
||||
end
|
||||
|
||||
return ListSection
|
||||
@@ -0,0 +1,146 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local FlagSettings = require(LuaApp.FlagSettings)
|
||||
local LoadingBar = require(LuaApp.Components.LoadingBar)
|
||||
local Roact = require(Common.Roact)
|
||||
|
||||
local LOADING_INDICATOR_WIDTH = 70
|
||||
local LOADING_INDICATOR_HEIGHT = 16
|
||||
local DOT_COUNT = 3
|
||||
local DOT_ANIMATION_SPEED_MULTIPLIER = 1.75
|
||||
local DOT_BASE_RELATIVE_HEIGHT = 0.7
|
||||
local DOT_BASE_RELATIVE_WIDTH = 0.7
|
||||
local DOT_HEIGHT_LERP_AMPLITUDE = 0.3
|
||||
|
||||
local LoadingIndicator = {}
|
||||
|
||||
local function makeDot()
|
||||
return Create.new "ImageLabel" {
|
||||
Name = "DotContainer",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
SizeConstraint = Enum.SizeConstraint.RelativeYY,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Dot",
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
SizeConstraint = Enum.SizeConstraint.RelativeYY,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local function renderLoadingDots(self)
|
||||
if not self.rbx.Visible then
|
||||
return
|
||||
end
|
||||
local dotsTime = (time() * DOT_ANIMATION_SPEED_MULTIPLIER) % #self.dots
|
||||
for i, dot in ipairs(self.dots) do
|
||||
local dotHeight = DOT_BASE_RELATIVE_HEIGHT
|
||||
if dotsTime >= i - 1 and dotsTime <= i then
|
||||
dotHeight = DOT_BASE_RELATIVE_HEIGHT + DOT_HEIGHT_LERP_AMPLITUDE * math.sin(math.pi * (dotsTime % 1))
|
||||
local colorAlpha = math.sin(math.pi * (dotsTime % 1))
|
||||
dot.Dot.BackgroundColor3 = Constants.Color.GRAY3:lerp(Constants.Color.BLUE_PRIMARY, colorAlpha)
|
||||
else
|
||||
dot.Dot.BackgroundColor3 = Constants.Color.GRAY3
|
||||
end
|
||||
dot.Dot.Size = UDim2.new(DOT_BASE_RELATIVE_WIDTH, 0, dotHeight, 0)
|
||||
end
|
||||
end
|
||||
|
||||
function LoadingIndicator.new(appState, scale)
|
||||
scale = scale or 1
|
||||
|
||||
local self = {}
|
||||
self.connections = {}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "LoadingIndicator",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, scale * LOADING_INDICATOR_WIDTH, 0, scale * LOADING_INDICATOR_HEIGHT)
|
||||
}
|
||||
|
||||
local platform = appState.store:getState().Platform
|
||||
local isLuaHomePageEnabled = FlagSettings.IsLuaHomePageEnabled(platform)
|
||||
local isLuaGamesPageEnabled = FlagSettings.IsLuaGamesPageEnabled(platform)
|
||||
self.isLoadingBarEnabled = isLuaHomePageEnabled and isLuaGamesPageEnabled
|
||||
|
||||
if self.isLoadingBarEnabled then
|
||||
self.loadingBar = Roact.mount(Roact.createElement(LoadingBar), self.rbx)
|
||||
else -- use dots loading indicator for non-lua pages
|
||||
self.dots = {}
|
||||
for i = 1, DOT_COUNT do
|
||||
local value = (i - 1) / (DOT_COUNT - 1)
|
||||
|
||||
local dot = makeDot()
|
||||
dot.Position = UDim2.new(value, 0, 0.5, 0)
|
||||
dot.AnchorPoint = Vector2.new(value, 0.5)
|
||||
dot.Parent = self.rbx
|
||||
|
||||
table.insert(self.dots, dot)
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(self, LoadingIndicator)
|
||||
|
||||
do
|
||||
local connection = self.rbx.AncestryChanged:Connect(function(object, parent)
|
||||
if object == self.rbx and parent == nil then
|
||||
self:Destroy()
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
if not self.isLoadingBarEnabled then
|
||||
local connection = RunService.RenderStepped:Connect(function()
|
||||
renderLoadingDots(self)
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function LoadingIndicator:SetZIndex(index)
|
||||
self.rbx.ZIndex = index
|
||||
if self.isLoadingBarEnabled then
|
||||
self.loadingBar.ZIndex = index
|
||||
else
|
||||
for _, dot in ipairs(self.dots) do
|
||||
dot.ZIndex = index
|
||||
dot.Dot.ZIndex = index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LoadingIndicator:SetVisible(visible)
|
||||
self.rbx.Visible = visible
|
||||
end
|
||||
|
||||
function LoadingIndicator:Destroy()
|
||||
if self.isLoadingBarEnabled then
|
||||
Roact.unmount(self.loadingBar)
|
||||
end
|
||||
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
self.rbx:Destroy()
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
LoadingIndicator.__index = LoadingIndicator
|
||||
|
||||
return LoadingIndicator
|
||||
@@ -0,0 +1,474 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local ChatBubble = require(LuaChat.Components.ChatBubble)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local OrderedMap = require(LuaChat.OrderedMap)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local ChatTimestamp = require(LuaChat.Components.ChatTimestamp)
|
||||
local Conversation = require(LuaChat.Models.Conversation)
|
||||
local LoadingIndicator = require(LuaChat.Components.LoadingIndicator)
|
||||
|
||||
local IS_BOTTOM_BUFFER = 5
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection")
|
||||
local FFlagLuaChatInfiniteRelayoutRecursionFix = settings():GetFFlag("LuaChatInfiniteRelayoutRecursionFix")
|
||||
|
||||
local MessageList = {}
|
||||
MessageList.__index = MessageList
|
||||
|
||||
local function didLocalUserSend(message)
|
||||
local localUserId = tostring(Players.LocalPlayer.UserId)
|
||||
|
||||
return message.senderTargetId == localUserId
|
||||
end
|
||||
|
||||
local function getSpacer(height)
|
||||
return Create.new "Frame" {
|
||||
Name = "Spacer",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, height),
|
||||
LayoutOrder = 1,
|
||||
}
|
||||
end
|
||||
|
||||
--[[
|
||||
Updates the spacing and timestamp display of message identified by
|
||||
messages and messageIndex.
|
||||
]]
|
||||
|
||||
local function isFirstMessageInCluster(messages, messageIndex)
|
||||
local message = messages:GetByIndex(messageIndex)
|
||||
local previousMessage = messages:GetByIndex(messageIndex - 1)
|
||||
|
||||
if not previousMessage or message.sent:GetUnixTimestamp() - previousMessage.sent:GetUnixTimestamp() > 5 * 60 then
|
||||
return true
|
||||
elseif previousMessage and message.senderTargetId == previousMessage.senderTargetId then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function updateBubblePadding(appState, conversationType, bubble, messages, messageIndex)
|
||||
local message = messages:GetByIndex(messageIndex)
|
||||
local previousMessage = messages:GetByIndex(messageIndex - 1)
|
||||
|
||||
local padding
|
||||
local extraInfoVisible = true
|
||||
|
||||
if previousMessage then
|
||||
if message.sent:GetUnixTimestamp() - previousMessage.sent:GetUnixTimestamp() > 5 * 60 then
|
||||
local relativeTime = message.sent:GetLongRelativeTime()
|
||||
padding = ChatTimestamp.new(appState, relativeTime).rbx
|
||||
else
|
||||
if message.senderTargetId == previousMessage.senderTargetId then
|
||||
padding = getSpacer(2)
|
||||
extraInfoVisible = false
|
||||
else
|
||||
padding = getSpacer(10)
|
||||
end
|
||||
end
|
||||
else
|
||||
local relativeTime = message.sent:GetLongRelativeTime()
|
||||
padding = ChatTimestamp.new(appState, relativeTime).rbx
|
||||
end
|
||||
|
||||
bubble:SetTailVisible(extraInfoVisible)
|
||||
|
||||
if not didLocalUserSend(message) then
|
||||
bubble:SetThumbnailVisible(extraInfoVisible)
|
||||
|
||||
if conversationType == Conversation.Type.MULTI_USER_CONVERSATION then
|
||||
bubble:SetUsernameVisible(extraInfoVisible)
|
||||
end
|
||||
end
|
||||
|
||||
bubble:SetPaddingObject(padding)
|
||||
end
|
||||
|
||||
local function createChatHistoryMessagePadding()
|
||||
local padding = Create.new "Frame" {
|
||||
Name = "LoadingIndicatorPadding",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 50),
|
||||
|
||||
Create.new "UIPadding" {
|
||||
PaddingBottom = UDim.new(0, 10),
|
||||
PaddingTop = UDim.new(0, 20)
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Container",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Size = UDim2.new(0, 84, 0, 22),
|
||||
}
|
||||
}
|
||||
|
||||
return padding
|
||||
end
|
||||
|
||||
function MessageList.new(appState, conversation)
|
||||
local self = {}
|
||||
setmetatable(self, MessageList)
|
||||
|
||||
self.appState = appState
|
||||
self.conversation = Conversation.mock()
|
||||
self.messageIdToBubble = {}
|
||||
self.RequestOlderMessages = Signal.new()
|
||||
self.ReadAllMessages = Signal.new()
|
||||
self.isTouchingBottom = true
|
||||
self.lastThumbnailLocation = {}
|
||||
self.loadingIndicatorPadding = createChatHistoryMessagePadding()
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
self.layout = Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
}
|
||||
|
||||
self.rbx = Create.new "ScrollingFrame" {
|
||||
Name = "MessageList",
|
||||
ElasticBehavior = "Always",
|
||||
ScrollingDirection = "Y",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.GRAY6,
|
||||
Size = UDim2.new(1, 0, 0, 0),
|
||||
ScrollBarThickness = 5,
|
||||
BottomImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
MidImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
TopImage = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png",
|
||||
|
||||
self.layout,
|
||||
|
||||
Create.new "UIPadding" {
|
||||
PaddingBottom = UDim.new(0, 10),
|
||||
},
|
||||
}
|
||||
|
||||
self.loadingIndicatorPadding.Parent = self.rbx
|
||||
self.loadingIndicatorPadding.Visible = false
|
||||
|
||||
self.rbx:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
local currentCanvasPositionY = self.rbx.CanvasPosition.Y
|
||||
if currentCanvasPositionY + self.rbx.AbsoluteWindowSize.Y + IS_BOTTOM_BUFFER >= self.rbx.CanvasSize.Y.Offset then
|
||||
self.isTouchingBottom = true
|
||||
self.ReadAllMessages:fire()
|
||||
else
|
||||
self.isTouchingBottom = false
|
||||
end
|
||||
if self.rbx.CanvasSize.Y.Offset > self.rbx.AbsoluteSize.Y then
|
||||
if self.lastCanvasPosition then
|
||||
if self.lastCanvasPosition < 0 and currentCanvasPositionY >= 0 then
|
||||
self.RequestOlderMessages:fire()
|
||||
|
||||
elseif currentCanvasPositionY == 0 then
|
||||
self.RequestOlderMessages:fire()
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
self.lastCanvasPosition = currentCanvasPositionY
|
||||
end
|
||||
else
|
||||
-- FFlagLuaChatInfiniteRelayoutRecursionFix caches CanvasPosition.Y
|
||||
if self.rbx.CanvasPosition.Y + self.rbx.AbsoluteWindowSize.Y + IS_BOTTOM_BUFFER >= self.rbx.CanvasSize.Y.Offset then
|
||||
self.isTouchingBottom = true
|
||||
self.ReadAllMessages:fire()
|
||||
else
|
||||
self.isTouchingBottom = false
|
||||
end
|
||||
if self.rbx.CanvasSize.Y.Offset > self.rbx.AbsoluteSize.Y then
|
||||
if self.lastCanvasPosition then
|
||||
if self.lastCanvasPosition < 0 and self.rbx.CanvasPosition.Y >= 0 then
|
||||
self.RequestOlderMessages:fire()
|
||||
elseif self.rbx.CanvasPosition.Y == 0 then
|
||||
self.RequestOlderMessages:fire()
|
||||
end
|
||||
end
|
||||
self.lastCanvasPosition = self.rbx.CanvasPosition.Y
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
self:Update(conversation)
|
||||
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
table.insert(self.connections, self.layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
|
||||
self:_resize()
|
||||
end))
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function MessageList:Update(conversation)
|
||||
if conversation == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- NOTE The following loop is currently used to set the typing indiactor visiblity
|
||||
-- to true. We currently pass an empty table for usersTyping when receiving a message.
|
||||
for userId, typing in pairs(conversation.usersTyping) do
|
||||
if self.lastThumbnailLocation[userId] then
|
||||
local bubble = self.messageIdToBubble[self.lastThumbnailLocation[userId].id]
|
||||
if bubble then
|
||||
bubble:SetTypingIndicatorVisible(typing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if conversation.messages == self.conversation.messages and
|
||||
conversation.sendingMessages == self.conversation.sendingMessages then
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove sending messages that have been moved to sent
|
||||
for id, _ in self.conversation.sendingMessages:CreateIterator() do
|
||||
if conversation.sendingMessages:Get(id) == nil then
|
||||
if self.messageIdToBubble[id] then
|
||||
self.messageIdToBubble[id].rbx:Destroy()
|
||||
self.messageIdToBubble[id] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local combinedMessages = OrderedMap.Merge(conversation.messages, conversation.sendingMessages)
|
||||
|
||||
-- Map containing whether a message has been adjusted, indexed by numeric keys
|
||||
-- into the combinedMessages map.
|
||||
local adjustedMessages = {}
|
||||
|
||||
local oldThumbnailLocation = self.lastThumbnailLocation
|
||||
self.lastThumbnailLocation = {}
|
||||
|
||||
local havePassedNewestMessage = false
|
||||
local havePassedDiscontiguousMessage = false
|
||||
for id, message, index in combinedMessages:CreateReverseIterator() do
|
||||
local existingMessage = self.conversation.messages:Get(id) or self.conversation.sendingMessages:Get(id)
|
||||
local bubble = self.messageIdToBubble[id]
|
||||
local senderId = message.senderTargetId
|
||||
|
||||
|
||||
local isSending = conversation.sendingMessages:Get(id) ~= nil
|
||||
|
||||
if message ~= existingMessage then
|
||||
if bubble then
|
||||
bubble:Destruct()
|
||||
end
|
||||
|
||||
local bubbleWidth
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
bubbleWidth = self.rbx.AbsoluteSize.X
|
||||
end
|
||||
bubble = ChatBubble.new(self.appState, message, bubbleWidth)
|
||||
|
||||
if not FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
bubble.rbx.Parent = self.rbx
|
||||
end
|
||||
|
||||
self.messageIdToBubble[id] = bubble
|
||||
|
||||
-- When a message updates, we need to recalculate layout on it and
|
||||
-- the message immediately after it.
|
||||
|
||||
updateBubblePadding(self.appState, conversation.conversationType, bubble, combinedMessages, index)
|
||||
|
||||
if not adjustedMessages[index + 1] then
|
||||
local nextMessage = combinedMessages:GetByIndex(index + 1)
|
||||
|
||||
if nextMessage then
|
||||
local nextBubble = self.messageIdToBubble[nextMessage.id]
|
||||
updateBubblePadding(self.appState, conversation.conversationType, nextBubble, combinedMessages, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
adjustedMessages[index] = true
|
||||
adjustedMessages[index + 1] = true
|
||||
end
|
||||
|
||||
if not self.lastThumbnailLocation[senderId] and isFirstMessageInCluster(combinedMessages, index) then
|
||||
self.lastThumbnailLocation[senderId] = message
|
||||
|
||||
if ( oldThumbnailLocation and oldThumbnailLocation[senderId] and
|
||||
oldThumbnailLocation[senderId] ~= message ) and self.messageIdToBubble[oldThumbnailLocation[senderId].id] then
|
||||
self.messageIdToBubble[oldThumbnailLocation[senderId].id]:SetTypingIndicatorVisible(false)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- When the MessageList updates, we assume the most recent message has
|
||||
-- been received from a previously typing user for Group Conversations.
|
||||
if havePassedNewestMessage == false and isSending == false and self.lastThumbnailLocation[senderId] then
|
||||
havePassedNewestMessage = true
|
||||
local thumbnailBubble = self.messageIdToBubble[self.lastThumbnailLocation[senderId].id]
|
||||
if thumbnailBubble then
|
||||
thumbnailBubble:SetTypingIndicatorVisible(false)
|
||||
end
|
||||
end
|
||||
|
||||
bubble.rbx.LayoutOrder = index
|
||||
bubble.rbx.Visible = not havePassedDiscontiguousMessage
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
bubble.rbx.Parent = self.rbx
|
||||
end
|
||||
|
||||
if (not isSending) and message.previousMessageId == nil then
|
||||
havePassedDiscontiguousMessage = true
|
||||
end
|
||||
if not FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
local connection = bubble.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
local wasTouchingBottom = self.isTouchingBottom
|
||||
self:ResizeCanvas(wasTouchingBottom)
|
||||
if wasTouchingBottom then
|
||||
self:ScrollToBottom()
|
||||
end
|
||||
end)
|
||||
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, connection)
|
||||
else
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.conversation = conversation
|
||||
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
self:_resize()
|
||||
else
|
||||
local wasTouchingBottom = self.isTouchingBottom
|
||||
self:ResizeCanvas(wasTouchingBottom)
|
||||
if wasTouchingBottom then
|
||||
self:ScrollToBottom()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MessageList:_resize()
|
||||
local wasTouchingBottom = self.isTouchingBottom
|
||||
self:ResizeCanvas(wasTouchingBottom)
|
||||
if wasTouchingBottom then
|
||||
self:ScrollToBottom()
|
||||
end
|
||||
end
|
||||
|
||||
function MessageList:CalculateCanvasSize()
|
||||
local height = 0
|
||||
|
||||
for _, child in pairs(self.messageIdToBubble) do
|
||||
if child.rbx.Visible then
|
||||
height = height + child.rbx.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
|
||||
if self.loadingIndicatorPadding.Visible then
|
||||
height = height + self.loadingIndicatorPadding.Size.Y.Offset
|
||||
end
|
||||
|
||||
return UDim2.new(1, 0, 0, height)
|
||||
end
|
||||
|
||||
function MessageList:ResizeCanvas(wasTouchingBottom)
|
||||
local canvas = self.rbx
|
||||
local oldHeight = canvas.CanvasSize.Y.Offset
|
||||
|
||||
canvas.CanvasSize = self:CalculateCanvasSize()
|
||||
if not FFlagLuaChatInfiniteRelayoutRecursionFix or not wasTouchingBottom then
|
||||
--Restore distance to bottom
|
||||
canvas.CanvasPosition = canvas.CanvasPosition + Vector2.new(0, canvas.CanvasSize.Y.Offset - oldHeight)
|
||||
end
|
||||
end
|
||||
|
||||
function MessageList:ScrollToBottom()
|
||||
local height = self.rbx.CanvasSize.Y.Offset - self.rbx.AbsoluteWindowSize.Y
|
||||
self.rbx.CanvasPosition = Vector2.new(0, height)
|
||||
end
|
||||
|
||||
function MessageList:TweenScrollToBottom(offsetY, tweenInfo)
|
||||
local height = self.rbx.CanvasSize.Y.Offset - self.rbx.AbsoluteWindowSize.Y + offsetY
|
||||
local propertyGoals =
|
||||
{
|
||||
CanvasPosition = Vector2.new(0, height)
|
||||
}
|
||||
local tween = TweenService:Create(self.rbx, tweenInfo, propertyGoals)
|
||||
tween:Play()
|
||||
end
|
||||
|
||||
function MessageList:StartLoadingMessageHistoryAnimation()
|
||||
if self.loadingIndicator == nil then
|
||||
self.loadingIndicator = LoadingIndicator.new(self.appState, 1)
|
||||
end
|
||||
|
||||
local padding = self.loadingIndicatorPadding
|
||||
|
||||
self.loadingIndicator.rbx.Parent = padding.Container
|
||||
padding.Visible = true
|
||||
|
||||
self.rbx.CanvasSize = self:CalculateCanvasSize()
|
||||
end
|
||||
|
||||
function MessageList:StopLoadingMessageHistoryAnimation()
|
||||
local loadingIndicator = self.loadingIndicator
|
||||
if loadingIndicator then
|
||||
|
||||
local padding = self.loadingIndicatorPadding
|
||||
padding.Visible = false
|
||||
|
||||
self.rbx.CanvasSize = self:CalculateCanvasSize()
|
||||
|
||||
self.loadingIndicator:Destroy()
|
||||
self.loadingIndicator = nil
|
||||
end
|
||||
end
|
||||
|
||||
if not LuaChatAssetCardsSelfTerminateConnection then
|
||||
function MessageList:DisconnectChatBubbles()
|
||||
for _, chatEntry in pairs(self.messageIdToBubble) do
|
||||
for _, childBubble in pairs(chatEntry.bubbles) do
|
||||
if childBubble.bubbleType == "AssetCard" then
|
||||
childBubble:DisconnectUpdate()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MessageList:Destruct()
|
||||
for _, bubble in pairs(self.messageIdToBubble) do
|
||||
bubble:Destruct()
|
||||
end
|
||||
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
for _, connection in ipairs(self.rbx_connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.rbx_connections = {}
|
||||
else
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
self.messageIdToBubble = {}
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return MessageList
|
||||
@@ -0,0 +1,233 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
|
||||
local BaseHeader = require(Components.BaseHeader)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local ModalTextButton = require(Components.ModalTextButton)
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local UseCppTextTruncation = FlagSettings.UseCppTextTruncation()
|
||||
|
||||
local HEIGHT_OF_BORDER = 1
|
||||
local HEIGHT_OF_DISCONNECTED = 32
|
||||
local HEIGHT_OF_HEADER = 44
|
||||
local ICON_CELL_WIDTH = 60
|
||||
|
||||
local TOTAL_HEIGHT_OF_HEADER = HEIGHT_OF_HEADER + HEIGHT_OF_BORDER
|
||||
|
||||
local ModalHeader = BaseHeader:Template()
|
||||
ModalHeader.__index = ModalHeader
|
||||
|
||||
function ModalHeader.new(appState, dialogType)
|
||||
local self = {}
|
||||
|
||||
self.heightOfHeader = TOTAL_HEIGHT_OF_HEADER
|
||||
self.heightOfDisconnected = HEIGHT_OF_DISCONNECTED
|
||||
|
||||
self.buttons = {}
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
self.appState = appState
|
||||
self.dialogType = dialogType
|
||||
self.backButton = BaseHeader:GetNewBackButton(dialogType)
|
||||
self.backButton.rbx.Visible = false
|
||||
self.backButton.rbx.Size = UDim2.new(0, 24, 0, 24)
|
||||
self.backButton.rbx.Position = UDim2.new(0.5, -2, 0.5, 0)
|
||||
self.backButton.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
self.title = ""
|
||||
self.subtitle = nil
|
||||
self.connectionState = Enum.ConnectionState.Connected
|
||||
|
||||
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
|
||||
self.appState.store:getState().FormFactor)
|
||||
|
||||
self.BackButtonPressed = Signal.new()
|
||||
local backButtonConnection = self.backButton.Pressed:connect(function()
|
||||
self.BackButtonPressed:fire()
|
||||
end)
|
||||
table.insert(self.connections, backButtonConnection)
|
||||
|
||||
self.titleLabel = Create.new "TextLabel" {
|
||||
Name = "Title",
|
||||
BackgroundTransparency = 1,
|
||||
TextSize = Constants.Font.FONT_SIZE_20,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Size = UDim2.new(0, 200, 0, 18),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Text = "Title",
|
||||
Font = Enum.Font.SourceSans,
|
||||
LayoutOrder = 0,
|
||||
}
|
||||
|
||||
self.innerSubtitle = Create.new "TextLabel" {
|
||||
Name = "Subtitle",
|
||||
BackgroundTransparency = 1,
|
||||
TextSize = Constants.Font.FONT_SIZE_12,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Size = UDim2.new(0, 200, 0, 18),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Text = "",
|
||||
Font = Enum.Font.SourceSans,
|
||||
LayoutOrder = 1,
|
||||
}
|
||||
|
||||
self.innerTitles = Create.new "Frame" {
|
||||
Name = "Titles",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 200, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center
|
||||
},
|
||||
|
||||
self.titleLabel,
|
||||
self.innerSubtitle,
|
||||
}
|
||||
|
||||
self.innerButtons = Create.new "Frame" {
|
||||
Name = "Buttons",
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(1, -12, 0, 0),
|
||||
Size = UDim2.new(0, 100, 1, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
},
|
||||
}
|
||||
|
||||
self.innerContent = Create.new "Frame" {
|
||||
Name = "Content",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, -4),
|
||||
Position = UDim2.new(0, 0, 0, 4),
|
||||
|
||||
Create.new"Frame" {
|
||||
Name = "Icon",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
|
||||
self.backButton.rbx,
|
||||
},
|
||||
|
||||
self.innerTitles,
|
||||
self.innerButtons,
|
||||
}
|
||||
|
||||
self.innerHeader = Create.new "Frame" {
|
||||
Name = "Header",
|
||||
BackgroundTransparency = 1, -- Set transparent so the rounded corners will show.
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, HEIGHT_OF_HEADER),
|
||||
LayoutOrder = 0,
|
||||
|
||||
self.innerContent,
|
||||
}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "HeaderFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, TOTAL_HEIGHT_OF_HEADER),
|
||||
|
||||
Create.new("UIListLayout") {
|
||||
Name = "ModalHeaderListLayout",
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
},
|
||||
|
||||
self.innerHeader,
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "BottomBorder",
|
||||
BackgroundColor3 = Constants.Color.GRAY3,
|
||||
Size = UDim2.new(1, 0, 0, HEIGHT_OF_BORDER),
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 1,
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "Disconnected",
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
BackgroundColor3 = Constants.Color.GRAY3,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately has zero vertical height, will be scaled.
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Title",
|
||||
AnchorPoint = Vector2.new(0.5, 1),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Constants.Font.TITLE,
|
||||
LayoutOrder = 0,
|
||||
Position = UDim2.new(0.5, 0, 1, 0),
|
||||
Size = UDim2.new(1, 0, 0, HEIGHT_OF_DISCONNECTED),
|
||||
Text = appState.localization:Format("Feature.Chat.Message.NoConnectionMsg"),
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
},
|
||||
},
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "GameDrawer",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = false,
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, 0), -- Note: Deliberately zero height, will be scaled open.
|
||||
Visible = false,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
local parentChangedConnection = self.rbx:GetPropertyChangedSignal("Parent"):Connect(function()
|
||||
if self.rbx and self.rbx.Parent then
|
||||
if not UseCppTextTruncation then
|
||||
game:GetService("RunService").Stepped:wait() -- TextBounds isn't recalculated when this fires so we wait
|
||||
end
|
||||
self:SetTitle(self.title) -- Again, this can be much cleaner once we have proper truncation support
|
||||
end
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, parentChangedConnection)
|
||||
else
|
||||
table.insert(self.connections, parentChangedConnection)
|
||||
end
|
||||
|
||||
do
|
||||
local connection = appState.store.changed:connect(function(state, oldState)
|
||||
self:SetConnectionState(state.ConnectionState)
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
setmetatable(self, ModalHeader)
|
||||
return self
|
||||
end
|
||||
|
||||
function ModalHeader:CreateHeaderButton(name, textKey)
|
||||
local saveGroup = ModalTextButton.new(self.appState, name, textKey)
|
||||
self:AddButton(saveGroup)
|
||||
return saveGroup
|
||||
end
|
||||
|
||||
return ModalHeader
|
||||
@@ -0,0 +1,76 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local FONT = Enum.Font.SourceSans
|
||||
local TEXT_SIZE = Constants.Font.FONT_SIZE_18
|
||||
local X_PADDING = 8
|
||||
|
||||
local TextButton = {}
|
||||
|
||||
TextButton.__index = TextButton
|
||||
|
||||
function TextButton.new(appState, name, textKey)
|
||||
local self = {}
|
||||
|
||||
self.enabled = true
|
||||
|
||||
|
||||
local text = appState.localization:Format(textKey)
|
||||
|
||||
local textWidth = Text.GetTextWidth(text, FONT, TEXT_SIZE)
|
||||
|
||||
self.rbx = Create.new "TextButton" {
|
||||
Name = name,
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
Size = UDim2.new(0, textWidth + X_PADDING, 1, 0),
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Label",
|
||||
Size = UDim2.new(0, textWidth, 0, TEXT_SIZE),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Text = text,
|
||||
Font = FONT,
|
||||
TextSize = TEXT_SIZE,
|
||||
TextColor3 = Constants.Color.BLUE_PRIMARY,
|
||||
},
|
||||
}
|
||||
|
||||
self.Pressed = Signal.new()
|
||||
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
if not self.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
self.Pressed:fire()
|
||||
end)
|
||||
|
||||
setmetatable(self, TextButton)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TextButton:SetEnabled(value)
|
||||
if value then
|
||||
self.rbx.Label.TextColor3 = Constants.Color.BLUE_PRIMARY
|
||||
self.rbx.Label.TextTransparency = 0
|
||||
else
|
||||
self.rbx.Label.TextColor3 = Constants.Color.GRAY3
|
||||
end
|
||||
|
||||
self.enabled = value
|
||||
end
|
||||
|
||||
return TextButton
|
||||
@@ -0,0 +1,229 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local ConversationActions = require(LuaChat.Actions.ConversationActions)
|
||||
local ConversationModel = require(LuaChat.Models.Conversation)
|
||||
local Create = require(LuaChat.Create)
|
||||
local DialogInfo = require(LuaChat.DialogInfo)
|
||||
local Immutable = require(Common.Immutable)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local FriendSearchBoxComponent = require(Components.FriendSearchBox)
|
||||
local HeaderLoader = require(Components.HeaderLoader)
|
||||
local ResponseIndicator = require(Components.ResponseIndicator)
|
||||
local SectionComponent = require(Components.ListSection)
|
||||
local TextInputEntry = require(Components.TextInputEntry)
|
||||
|
||||
local RemoveRoute = require(LuaChat.Actions.RemoveRoute)
|
||||
|
||||
local Intent = DialogInfo.Intent
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
|
||||
local NewChatGroup = {}
|
||||
NewChatGroup.__index = NewChatGroup
|
||||
|
||||
local function getAsset(name)
|
||||
return "rbxasset://textures/ui/LuaChat/" .. name .. ".png"
|
||||
end
|
||||
|
||||
function NewChatGroup.new(appState)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, NewChatGroup)
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
|
||||
self.conversation = ConversationModel.empty()
|
||||
|
||||
self.responseIndicator = ResponseIndicator.new(appState)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
|
||||
-- Header:
|
||||
self.header = HeaderLoader.GetHeader(appState, Intent.NewChatGroup)
|
||||
self.header:SetDefaultSubtitle()
|
||||
self.header:SetTitle(appState.localization:Format("Feature.Chat.Heading.NewChatGroup"))
|
||||
self.header:SetBackButtonEnabled(true)
|
||||
self.header:SetConnectionState(Enum.ConnectionState.Disconnected)
|
||||
|
||||
-- Name the group:
|
||||
local placeholderText = appState.localization:Format("Feature.Chat.Description.NameGroupChat")
|
||||
self.groupName = TextInputEntry.new(appState, getAsset("icons/ic-nametag"), placeholderText)
|
||||
self.groupName.rbx.LayoutOrder = 1
|
||||
|
||||
local sanitizeGroupName = function(input)
|
||||
return input:gsub("\n", "")
|
||||
end
|
||||
local textboxChangedConnection = self.groupName.textBoxChanged:connect(function(newGroupName)
|
||||
self.conversation.title = sanitizeGroupName(newGroupName)
|
||||
end)
|
||||
table.insert(self.connections, textboxChangedConnection)
|
||||
|
||||
local textboxFocusLostConnection = self.groupName.textBoxFocusLost:connect(function()
|
||||
self.groupName:SanitizeInput(sanitizeGroupName)
|
||||
end)
|
||||
table.insert(self.connections, textboxFocusLostConnection)
|
||||
|
||||
-- Search for friends:
|
||||
self.searchComponent = FriendSearchBoxComponent.new(
|
||||
appState,
|
||||
self.conversation.participants,
|
||||
Constants.MAX_PARTICIPANT_COUNT,
|
||||
function(user)
|
||||
return user.isFriend and user.id ~= tostring(Players.LocalPlayer.UserId)
|
||||
end
|
||||
)
|
||||
self.searchComponent.rbx.LayoutOrder = 3
|
||||
local addParticipantConnection = self.searchComponent.addParticipant:connect(function(id)
|
||||
self.groupName:ReleaseFocus()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self:ChangeParticipants(Immutable.Set(self.conversation.participants, #self.conversation.participants+1, id))
|
||||
end)
|
||||
table.insert(self.connections, addParticipantConnection)
|
||||
|
||||
local removeParticipantConnection = self.searchComponent.removeParticipant:connect(function(id)
|
||||
self.groupName:ReleaseFocus()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self:ChangeParticipants(Immutable.RemoveValueFromList(self.conversation.participants, id))
|
||||
end)
|
||||
table.insert(self.connections, removeParticipantConnection)
|
||||
|
||||
-- Assemble the dialog from components we just made:
|
||||
self.sectionComponent = SectionComponent.new(appState, nil, 2)
|
||||
self.rbx = Create.new"Frame" {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
|
||||
Create.new("UIListLayout") {
|
||||
Name = "ListLayout",
|
||||
SortOrder = "LayoutOrder",
|
||||
},
|
||||
self.header.rbx,
|
||||
Create.new"Frame" {
|
||||
Name = "Content",
|
||||
Size = UDim2.new(1, 0, 1, -(self.header.heightOfHeader)),
|
||||
BackgroundColor3 = Constants.Color.GRAY5,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 1,
|
||||
ClipsDescendants = true,
|
||||
|
||||
Create.new"UIListLayout" {
|
||||
Name = "ListLayout",
|
||||
SortOrder = "LayoutOrder",
|
||||
},
|
||||
self.groupName.rbx,
|
||||
self.sectionComponent.rbx,
|
||||
self.searchComponent.rbx,
|
||||
},
|
||||
self.responseIndicator.rbx,
|
||||
}
|
||||
|
||||
-- Wire up the save button to actually create our new chat group:
|
||||
self.saveGroup = self.header:CreateHeaderButton("SaveGroup", "Feature.Chat.Action.Create")
|
||||
self.saveGroup:SetEnabled(false)
|
||||
local saveGroupConnection = self.saveGroup.Pressed:connect(function()
|
||||
self.groupName:ReleaseFocus()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
if #self.conversation.participants >= Constants.MIN_PARTICIPANT_COUNT then
|
||||
self.responseIndicator:SetVisible(true)
|
||||
self.appState.store:dispatch(
|
||||
ConversationActions.CreateConversation(self.conversation, function(id)
|
||||
self.responseIndicator:SetVisible(false)
|
||||
if id ~= nil then
|
||||
self.ConversationSaved:fire(id)
|
||||
end
|
||||
self.appState.store:dispatch(RemoveRoute(Intent.NewChatGroup))
|
||||
end)
|
||||
)
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, saveGroupConnection)
|
||||
|
||||
self.BackButtonPressed = Signal.new()
|
||||
self.header.BackButtonPressed:connect(function()
|
||||
self.groupName:ReleaseFocus()
|
||||
self.searchComponent.search:ReleaseFocus()
|
||||
self.BackButtonPressed:fire()
|
||||
end)
|
||||
self.ConversationSaved = Signal.new()
|
||||
|
||||
spawn(function()
|
||||
self.appState.store:dispatch(ConversationActions.GetAllFriendsAsync())
|
||||
end)
|
||||
|
||||
self.tooManyFriendsAlertId = nil
|
||||
|
||||
-- Monitor for several size changes to properly scale dialog elements:
|
||||
local groupNameConnection = self.groupName.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:Resize()
|
||||
end)
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
table.insert(self.rbx_connections, groupNameConnection)
|
||||
else
|
||||
table.insert(self.connections, groupNameConnection)
|
||||
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 NewChatGroup:Resize()
|
||||
-- Content frame must resize if the header changes size (which happens when it shows the "Connecting" message):
|
||||
local sizeContent = UDim2.new(1, 0, 1, -self.header.rbx.AbsoluteSize.Y)
|
||||
self.rbx.Content.Size = sizeContent
|
||||
|
||||
-- Friends Search frame must resize to fit properly with their peers:
|
||||
local sizeSearch = UDim2.new(1, 0, 1, -(self.groupName.rbx.AbsoluteSize.Y + self.sectionComponent.rbx.AbsoluteSize.Y))
|
||||
self.searchComponent.rbx.Size = sizeSearch
|
||||
end
|
||||
|
||||
function NewChatGroup:ChangeParticipants(participants)
|
||||
self.conversation = Immutable.Set(self.conversation, "participants", participants)
|
||||
self.searchComponent:Update(participants)
|
||||
self.saveGroup:SetEnabled(#participants >= Constants.MIN_PARTICIPANT_COUNT)
|
||||
end
|
||||
|
||||
function NewChatGroup:Update(current, previous)
|
||||
self.header:SetConnectionState(current.ConnectionState)
|
||||
self.searchComponent:Update(self.conversation.participants)
|
||||
end
|
||||
|
||||
function NewChatGroup: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
|
||||
|
||||
self.header:Destroy()
|
||||
self.groupName:Destruct()
|
||||
self.responseIndicator:Destruct()
|
||||
self.searchComponent:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return NewChatGroup
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
|
||||
local NoFriendsIndicator = {}
|
||||
|
||||
NoFriendsIndicator.__index = NoFriendsIndicator
|
||||
|
||||
function NoFriendsIndicator.new(appState)
|
||||
local self = {}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "NoFriendsIndicator",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 300),
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "IndicatorInner",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 160),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
},
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 72, 0, 72),
|
||||
LayoutOrder = 1,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-friends.png",
|
||||
},
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Size = UDim2.new(1, -32, 0, 66),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 2,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextWrapped = true,
|
||||
Text = appState.localization:Format("Feature.Chat.Message.MakeFriendsToChat"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setmetatable(self, NoFriendsIndicator)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return NoFriendsIndicator
|
||||
@@ -0,0 +1,49 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local PaddedImageButton = {}
|
||||
|
||||
function PaddedImageButton.new(appState, name, imageUrl)
|
||||
local self = {}
|
||||
|
||||
self.rbx = Create.new "ImageButton" {
|
||||
Name = name,
|
||||
Size = UDim2.new(0, 40, 0, 40),
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
Name = "ImageLabel",
|
||||
Size = UDim2.new(0, 24, 0, 24),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Image = imageUrl
|
||||
},
|
||||
}
|
||||
|
||||
self.Pressed = Signal.new()
|
||||
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
self.Pressed:fire()
|
||||
end)
|
||||
|
||||
setmetatable(self, PaddedImageButton)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
function PaddedImageButton:SetVisible(value)
|
||||
self.rbx.Visible = value
|
||||
end
|
||||
|
||||
PaddedImageButton.__index = PaddedImageButton
|
||||
|
||||
return PaddedImageButton
|
||||
@@ -0,0 +1,114 @@
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail)
|
||||
|
||||
local PlaceInfoCard = {}
|
||||
PlaceInfoCard.__index = PlaceInfoCard
|
||||
|
||||
local PLACE_INFO_CARD_HEIGHT = 72
|
||||
local PLACE_INFO_THUMBNAIL_SIZE = 50
|
||||
|
||||
local UrlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI")
|
||||
|
||||
function PlaceInfoCard.new(appState, placeInfo)
|
||||
local self = {}
|
||||
self.appState = appState
|
||||
self.placeInfo = placeInfo
|
||||
self.thumbnail = appState.store:getState().ChatAppReducer.PlaceThumbnails[placeInfo.imageToken]
|
||||
self.connections = {}
|
||||
setmetatable(self, PlaceInfoCard)
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "PlaceInfoCardFrame",
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, PLACE_INFO_CARD_HEIGHT),
|
||||
|
||||
Create.new "Frame" {
|
||||
Name = "PlaceThumbnailFrame",
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, PLACE_INFO_CARD_HEIGHT, 0, PLACE_INFO_CARD_HEIGHT),
|
||||
Position = UDim2.new(0, 0, 0, 0)
|
||||
},
|
||||
|
||||
Create.new"TextLabel" {
|
||||
Name = "PlaceTitle",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -PLACE_INFO_CARD_HEIGHT, 0.75, 0),
|
||||
Position = UDim2.new(0, PLACE_INFO_CARD_HEIGHT, 0, 0),
|
||||
TextSize = Constants.Font.FONT_SIZE_16,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = placeInfo.name,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
},
|
||||
|
||||
Create.new"TextLabel" {
|
||||
Name = "BuilderLabel",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -PLACE_INFO_CARD_HEIGHT, 0.35, 0),
|
||||
Position = UDim2.new(0, PLACE_INFO_CARD_HEIGHT, 0.5, 0),
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = appState.localization:Format("Feature.Chat.Label.ByBuilder", { USERNAME = placeInfo.builder }),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
},
|
||||
}
|
||||
|
||||
if not UrlSupportNewGamesAPI then
|
||||
self.thumbnail = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png"
|
||||
end
|
||||
|
||||
if not self.thumbnail then
|
||||
self.appState.store:dispatch(GetPlaceThumbnail(self.placeInfo.imageToken,
|
||||
PLACE_INFO_THUMBNAIL_SIZE, PLACE_INFO_THUMBNAIL_SIZE))
|
||||
|
||||
local appStateConnection = self.appState.store.changed:connect(function(state, oldState)
|
||||
self:Update(state, oldState)
|
||||
end)
|
||||
table.insert(self.connections, appStateConnection)
|
||||
else
|
||||
self:FillThumbnail()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function PlaceInfoCard:Update(newState, oldState)
|
||||
local thumbnail = newState.ChatAppReducer.PlaceThumbnails[self.placeInfo.imageToken]
|
||||
if (not self.thumbnail) and thumbnail then
|
||||
if thumbnail == '' then
|
||||
self.thumbnail = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png"
|
||||
else
|
||||
self.thumbnail = thumbnail
|
||||
end
|
||||
self:FillThumbnail()
|
||||
end
|
||||
end
|
||||
|
||||
function PlaceInfoCard:FillThumbnail()
|
||||
self.placeThumbnail = Create.new "ImageLabel" {
|
||||
Name = "PlaceThumbnail",
|
||||
Image = self.thumbnail.image,
|
||||
Size = UDim2.new(0, 48, 0, 48),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
self.placeThumbnail.Parent = self.rbx.PlaceThumbnailFrame
|
||||
end
|
||||
|
||||
function PlaceInfoCard:Destruct()
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return PlaceInfoCard
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local PlayerService = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local FlagSettings = require(LuaChat.FlagSettings)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
local GetMultiplePlaceInfos = require(LuaChat.Actions.GetMultiplePlaceInfos)
|
||||
local GetPlaceThumbnail = require(LuaChat.Actions.GetPlaceThumbnail)
|
||||
local Signal = require(Modules.Common.Signal)
|
||||
local SortedActivelyPlayedGames = require(LuaChat.SortedActivelyPlayedGames)
|
||||
local User = require(LuaApp.Models.User)
|
||||
|
||||
local LuaChatPlayTogetherUseRootPresence = FlagSettings.LuaChatPlayTogetherUseRootPresence()
|
||||
|
||||
local ICON_MASK = "rbxasset://textures/ui/LuaChat/graphic/gr-mask-game-icon-48x48.png"
|
||||
local STACKED_ICON_MASK = "rbxasset://textures/ui/LuaChat/graphic/gr-gamealbum-icon-52x52.png"
|
||||
local BORDER_ICON_MASK = "rbxasset://textures/ui/LuaChat/graphic/gr-game-border-24x24.png"
|
||||
local DEFAULT_THUMBNAIL = "rbxasset://textures/ui/LuaChat/icons/share-game-thumbnail.png"
|
||||
|
||||
local LARGE_GAME_ICON_OUTER_SIZE = UDim2.new(0, 48, 0, 48)
|
||||
local LARGE_GAME_STACK_ICON_SIZE = UDim2.new(0, 52, 0, 52)
|
||||
|
||||
local SMALL_GAME_ICON_OUTER_SIZE = UDim2.new(0, 40, 0, 40)
|
||||
local SMALL_GAME_ICON_CHILD_SIZE = UDim2.new(0, 24, 0, 24)
|
||||
|
||||
local PLACE_INFO_THUMBNAIL_SIZE = 48
|
||||
|
||||
local UrlSupportNewGamesAPI = settings():GetFFlag("UrlSupportNewGamesAPI")
|
||||
|
||||
local PlayTogetherGameIcon = {}
|
||||
PlayTogetherGameIcon.__index = PlayTogetherGameIcon
|
||||
|
||||
PlayTogetherGameIcon.Size = {
|
||||
SMALL = "SMALL",
|
||||
LARGE = "LARGE",
|
||||
}
|
||||
|
||||
PlayTogetherGameIcon.Type = {
|
||||
DEFAULT = "DEFAULT",
|
||||
ACTIVE = "ACTIVE"
|
||||
}
|
||||
|
||||
function PlayTogetherGameIcon.new(appState, conversation, iconSize, iconType)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
self.cachedConversation = nil
|
||||
self.cachedThumbnailPlaceInfo = nil
|
||||
self.conversationId = nil
|
||||
self.conversationUsers = {}
|
||||
self.fetchedMostRecentlyPlayedGames = false
|
||||
self.setIconPending = false
|
||||
self.type = iconType or PlayTogetherGameIcon.Type.DEFAULT
|
||||
self.updateConnection = nil
|
||||
|
||||
local childImage = BORDER_ICON_MASK
|
||||
local childSize = SMALL_GAME_ICON_CHILD_SIZE
|
||||
local size = SMALL_GAME_ICON_CHILD_SIZE
|
||||
local outerSize = SMALL_GAME_ICON_OUTER_SIZE
|
||||
if iconSize == PlayTogetherGameIcon.Size.LARGE then
|
||||
childImage = ICON_MASK
|
||||
childSize = LARGE_GAME_STACK_ICON_SIZE
|
||||
size = LARGE_GAME_ICON_OUTER_SIZE
|
||||
outerSize = LARGE_GAME_ICON_OUTER_SIZE
|
||||
end
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "Frame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = outerSize,
|
||||
|
||||
Create.new "ImageButton" {
|
||||
Name = "TopGameIcon",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Image = DEFAULT_THUMBNAIL,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Size = size,
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
Name = "Mask",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Image = childImage,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Size = childSize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.iconSize = iconSize
|
||||
self.mask = self.rbx.TopGameIcon.Mask
|
||||
|
||||
setmetatable(self, PlayTogetherGameIcon)
|
||||
|
||||
self:SetVisible(false)
|
||||
|
||||
if conversation ~= nil then
|
||||
self.conversationId = conversation.id
|
||||
self.cachedConversation = conversation
|
||||
self:Update(conversation)
|
||||
|
||||
self.updateConnection = appState.store.changed:connect(function(state, oldState)
|
||||
-- Update if there's a change in which conversation we're viewing:
|
||||
local newConversation = state.ChatAppReducer.Conversations[self.conversationId]
|
||||
|
||||
-- Note conversations can be nil if we're a dummy 1:1 conversation
|
||||
-- which was removed and replaced by a server conversation:
|
||||
if newConversation == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if self.cachedConversation ~= newConversation then
|
||||
self.cachedConversation = newConversation
|
||||
self:Update(self.cachedConversation)
|
||||
return
|
||||
end
|
||||
|
||||
-- Update if there's any change in our conversation participants:
|
||||
local users = state.Users
|
||||
local localPlayerId = tostring(PlayerService.LocalPlayer.UserId)
|
||||
local participantsCache = self.cachedConversation.participants
|
||||
for _, id in ipairs(conversation.participants) do
|
||||
if id ~= localPlayerId then
|
||||
if participantsCache[id] ~= users[id] then
|
||||
self:Update(self.cachedConversation)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Update if we were waiting for our icon to load:
|
||||
if self.setIconPending then
|
||||
self:Update(self.cachedConversation)
|
||||
return
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
self.Pressed = Signal.new()
|
||||
getInputEvent(self.rbx.TopGameIcon):Connect(function()
|
||||
self.Pressed:fire()
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function PlayTogetherGameIcon:Update(conversation)
|
||||
if not UrlSupportNewGamesAPI then
|
||||
self.rbx.TopGameIcon.Image = DEFAULT_THUMBNAIL
|
||||
return
|
||||
end
|
||||
|
||||
local state = self.appState.store:getState()
|
||||
|
||||
local pinnedGameRootPlaceId = nil
|
||||
if conversation.pinnedGame and conversation.pinnedGame.rootPlaceId then
|
||||
pinnedGameRootPlaceId = conversation.pinnedGame.rootPlaceId
|
||||
end
|
||||
|
||||
local inGameParticipants = {}
|
||||
local mostRecentPlayedPlayableGamePlaceId =
|
||||
self.appState.store:getState().ChatAppReducer.MostRecentlyPlayedGames.playableGamePlaceId
|
||||
|
||||
local localPlayerId = tostring(PlayerService.LocalPlayer.UserId)
|
||||
for _, userId in pairs(conversation.participants) do
|
||||
if userId ~= localPlayerId then
|
||||
local user = state.Users[userId]
|
||||
if user ~= nil then
|
||||
if LuaChatPlayTogetherUseRootPresence then
|
||||
if (user.presence == User.PresenceType.IN_GAME) and user.rootPlaceId then
|
||||
table.insert(inGameParticipants, user)
|
||||
end
|
||||
else
|
||||
if (user.presence == User.PresenceType.IN_GAME) and user.placeId then
|
||||
table.insert(inGameParticipants, user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.type == PlayTogetherGameIcon.Type.ACTIVE then
|
||||
if #inGameParticipants > 0 then
|
||||
local activelyPlayedGames = SortedActivelyPlayedGames.getSortedGames(pinnedGameRootPlaceId,
|
||||
inGameParticipants)
|
||||
local isMultiple = #activelyPlayedGames > 1
|
||||
self:SetThumbnail(state, activelyPlayedGames[1].placeId, isMultiple)
|
||||
else
|
||||
self:SetVisible(false)
|
||||
end
|
||||
else
|
||||
if #inGameParticipants > 0 then
|
||||
local activelyPlayedGames = SortedActivelyPlayedGames.getSortedGames(pinnedGameRootPlaceId,
|
||||
inGameParticipants)
|
||||
self:SetThumbnail(state, activelyPlayedGames[1].placeId, false)
|
||||
elseif pinnedGameRootPlaceId then
|
||||
self:SetThumbnail(state, pinnedGameRootPlaceId, false)
|
||||
elseif mostRecentPlayedPlayableGamePlaceId then
|
||||
self:SetThumbnail(state, mostRecentPlayedPlayableGamePlaceId, false)
|
||||
else
|
||||
self:SetVisible(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PlayTogetherGameIcon:SetThumbnail(state, chosenPlaceId, isMultiple)
|
||||
-- Always set the default icon while loading, so we don't see the old one:
|
||||
self.rbx.TopGameIcon.Image = DEFAULT_THUMBNAIL
|
||||
|
||||
local placeInfo = state.ChatAppReducer.PlaceInfos[chosenPlaceId]
|
||||
if placeInfo == nil then
|
||||
self.appState.store:dispatch(GetMultiplePlaceInfos({chosenPlaceId}))
|
||||
self.setIconPending = true
|
||||
else
|
||||
self.cachedThumbnailPlaceInfo = placeInfo
|
||||
local thumbnail = state.ChatAppReducer.PlaceThumbnails[placeInfo.imageToken]
|
||||
if thumbnail == nil then
|
||||
self.appState.store:dispatch(GetPlaceThumbnail(
|
||||
placeInfo.imageToken, PLACE_INFO_THUMBNAIL_SIZE, PLACE_INFO_THUMBNAIL_SIZE
|
||||
))
|
||||
self.setIconPending = true
|
||||
else
|
||||
self:SetIsMultipleGames(isMultiple)
|
||||
self.setIconPending = false
|
||||
if thumbnail.image ~= '' then
|
||||
self.rbx.TopGameIcon.Image = thumbnail.image
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:SetVisible(true)
|
||||
end
|
||||
|
||||
function PlayTogetherGameIcon:SetVisible(value)
|
||||
self.rbx.Visible = value
|
||||
end
|
||||
|
||||
function PlayTogetherGameIcon:SetIsMultipleGames(isMultiple)
|
||||
if self.iconSize == PlayTogetherGameIcon.Size.SMALL then
|
||||
return
|
||||
end
|
||||
|
||||
if isMultiple then
|
||||
self.mask.Image = STACKED_ICON_MASK
|
||||
self.mask.Size = LARGE_GAME_STACK_ICON_SIZE
|
||||
else
|
||||
self.mask.Image = ICON_MASK
|
||||
self.mask.Size = LARGE_GAME_ICON_OUTER_SIZE
|
||||
end
|
||||
end
|
||||
|
||||
function PlayTogetherGameIcon:Destruct()
|
||||
if self.updateConnection then
|
||||
self.updateConnection:Disconnect()
|
||||
end
|
||||
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return PlayTogetherGameIcon
|
||||
@@ -0,0 +1,65 @@
|
||||
local LuaChat = script.Parent.Parent
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local LoadingIndicatorComponent = require(LuaChat.Components.LoadingIndicator)
|
||||
|
||||
local PADDING = 48
|
||||
|
||||
local ResponseIndicator = {}
|
||||
|
||||
function ResponseIndicator.new(appState)
|
||||
local self = {}
|
||||
|
||||
setmetatable(self, {__index = ResponseIndicator})
|
||||
|
||||
self.connections = {}
|
||||
|
||||
self.indicator = LoadingIndicatorComponent.new(appState)
|
||||
self.indicator:SetZIndex(3)
|
||||
|
||||
self.indicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
self.indicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
|
||||
self.rbx = Create.new"ImageButton" { -- So you can't select buttons underneath
|
||||
Name = "ResponseIndicator",
|
||||
BackgroundTransparency = Constants.Color.ALPHA_SHADOW_HOVER,
|
||||
BackgroundColor3 = Constants.Color.GRAY1,
|
||||
AutoButtonColor = false,
|
||||
BorderSizePixel = 0,
|
||||
Active = false,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
ZIndex = 3,
|
||||
Create.new"ImageLabel" {
|
||||
BackgroundTransparency = 1,
|
||||
Size = self.indicator.rbx.Size + UDim2.new(0, PADDING, 0, PADDING),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(3,3,4,4),
|
||||
Image = "rbxasset://textures/ui/LuaChat/9-slice/input-default.png",
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 3,
|
||||
self.indicator.rbx,
|
||||
},
|
||||
}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ResponseIndicator:SetVisible(value)
|
||||
self.rbx.Visible = value
|
||||
self.indicator:SetVisible(value)
|
||||
end
|
||||
|
||||
function ResponseIndicator:Destruct()
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
self.connections = {}
|
||||
self.indicator:Destroy()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return ResponseIndicator
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local Constants = require(Modules.LuaChat.Constants)
|
||||
local formatInteger = require(Modules.LuaChat.Utils.formatInteger)
|
||||
|
||||
local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel)
|
||||
|
||||
local TEXT_FONT = Enum.Font.SourceSans
|
||||
|
||||
local GAME_NAME_LABEL_HEIGHT = 20
|
||||
local GAME_NAME_LABEL_TEXT_SIZE = 23
|
||||
local GAME_NAME_LABEL_TEXT_COLOR = Constants.Color.GRAY1
|
||||
|
||||
local CREATOR_NAME_LABEL_HEIGHT = 14
|
||||
local CREATOR_NAME_LABEL_TEXT_SIZE = 15
|
||||
local CREATOR_NAME_LABEL_TOP_PADDING = 6
|
||||
local CREATOR_NAME_LABEL_COLOR = Constants.Color.GRAY2
|
||||
|
||||
local SUBTITLE_LABEL_HEIGHT = 14
|
||||
local SUBTITLE_LABEL_TEXT_SIZE = 14
|
||||
local SUBTITLE_LABEL_TOP_PADDING = 3
|
||||
local SUBTITLE_FRAME_HEIGHT = SUBTITLE_LABEL_HEIGHT + SUBTITLE_LABEL_TOP_PADDING
|
||||
|
||||
local ROBUX_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-robux.png"
|
||||
local ROBUX_ICON_SIZE = 12
|
||||
local ROBUX_TO_PRICE_PADDING = 3
|
||||
|
||||
local PRICE_COLOR = Constants.Color.GREEN_PRIMARY
|
||||
|
||||
local FFlagLuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
|
||||
|
||||
local GameInformation = Roact.PureComponent:extend("GameInformation")
|
||||
|
||||
function GameInformation:render()
|
||||
local gameModel = self.props.gameModel
|
||||
if not gameModel then
|
||||
return nil
|
||||
end
|
||||
|
||||
local gameTitle = gameModel.name
|
||||
local creatorName = gameModel.creatorName
|
||||
-- TODO wait lua app team store the playability of the game, default value is true
|
||||
-- Ticket: MOBLUAPP-683, Review http://swarm.roblox.local/reviews/226723/
|
||||
-- TODO: SOC-3779 This ticket has been committed since this comment.
|
||||
-- * New ticket: https://jira.roblox.com/browse/SOC-3779
|
||||
local playable = FFlagLuaChatShareGameToChatFromChatV2 or self.props.gameModel.isPlayable
|
||||
local gamePrice = gameModel.price
|
||||
local showPrice = gamePrice and playable
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
|
||||
Name = Roact.createElement("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Font = TEXT_FONT,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, 0, 0, GAME_NAME_LABEL_HEIGHT),
|
||||
Text = gameTitle,
|
||||
TextColor3 = GAME_NAME_LABEL_TEXT_COLOR,
|
||||
TextSize = GAME_NAME_LABEL_TEXT_SIZE,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
}),
|
||||
|
||||
Creator = Roact.createElement(LocalizedTextLabel, {
|
||||
BackgroundTransparency = 1,
|
||||
Font = TEXT_FONT,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(1, 0, 0, CREATOR_NAME_LABEL_HEIGHT + CREATOR_NAME_LABEL_TOP_PADDING),
|
||||
Text = {"Feature.Chat.ShareGameToChat.By", creatorName = creatorName},
|
||||
TextColor3 = CREATOR_NAME_LABEL_COLOR,
|
||||
TextSize = CREATOR_NAME_LABEL_TEXT_SIZE,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
}),
|
||||
|
||||
NotAvailableTip = not playable and Roact.createElement(LocalizedTextLabel, {
|
||||
BackgroundTransparency = 1,
|
||||
Font = TEXT_FONT,
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, SUBTITLE_FRAME_HEIGHT),
|
||||
Text = "Feature.Chat.ShareGameToChat.GameNotAvailable",
|
||||
TextColor3 = CREATOR_NAME_LABEL_COLOR,
|
||||
TextSize = SUBTITLE_LABEL_TEXT_SIZE,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
}),
|
||||
|
||||
GamePrice = showPrice and Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, SUBTITLE_FRAME_HEIGHT),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, ROBUX_TO_PRICE_PADDING),
|
||||
}),
|
||||
|
||||
RobuxIcon = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Image = ROBUX_ICON,
|
||||
LayoutOrder = 1,
|
||||
ScaleType = Enum.ScaleType.Fit,
|
||||
Size = UDim2.new(0, ROBUX_ICON_SIZE, 0, ROBUX_ICON_SIZE),
|
||||
}),
|
||||
|
||||
Price = Roact.createElement("TextLabel",{
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -(ROBUX_ICON_SIZE + ROBUX_TO_PRICE_PADDING), 1, 0),
|
||||
Font = TEXT_FONT,
|
||||
LayoutOrder = 2,
|
||||
Text = formatInteger(gamePrice),
|
||||
TextColor3 = PRICE_COLOR,
|
||||
TextSize = SUBTITLE_LABEL_TEXT_SIZE,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return GameInformation
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
return function()
|
||||
local GameInformation = require(script.Parent.GameInformation)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
|
||||
|
||||
local FFlagLuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
|
||||
|
||||
-- TODO: SOC-3805 `gameModel` should be using an actual model.
|
||||
local function createValidGameModel(isPlayable, price)
|
||||
return {
|
||||
imageToken = "mock-token",
|
||||
name = "mock-name",
|
||||
universeId = "mock-id",
|
||||
isPlayable = isPlayable,
|
||||
price = price or nil,
|
||||
creatorName = "mock-creator",
|
||||
}
|
||||
end
|
||||
|
||||
describe("SHOULD create and destroy without errors", function()
|
||||
it("WHEN passed no props", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation)
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("WHEN passed a valid gameModel", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(),
|
||||
})
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
|
||||
if not FFlagLuaChatShareGameToChatFromChatV2 then
|
||||
describe("SHOULD determine if the game is available", function()
|
||||
describe("SHOULD display playability status visible", function()
|
||||
it("WHEN the game is not playable and free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(false, nil),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local notAvailableTip = folder:FindFirstChild("NotAvailableTip", true)
|
||||
expect(notAvailableTip).to.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
it("WHEN the game is not playable and not free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(false, 1000),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local notAvailableTip = folder:FindFirstChild("NotAvailableTip", true)
|
||||
expect(notAvailableTip).to.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
describe("SHOULD display playability status as not visible", function()
|
||||
it("WHEN the game is playable and free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(true, nil),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local notAvailableTip = folder:FindFirstChild("NotAvailableTip", true)
|
||||
expect(notAvailableTip).to.never.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("SHOULD determine to show price", function()
|
||||
describe("SHOULD display price as visible", function()
|
||||
it("WHEN the game is playable and not free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(true, 1000),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local gamePrice = folder:FindFirstChild("GamePrice", true)
|
||||
expect(gamePrice).to.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
describe("SHOULD display price as not visible", function()
|
||||
it("WHEN the game is playable and free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(true, nil),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local gamePrice = folder:FindFirstChild("GamePrice", true)
|
||||
expect(gamePrice).to.never.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
it("WHEN the game is not playable and free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(false, nil),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local gamePrice = folder:FindFirstChild("GamePrice", true)
|
||||
expect(gamePrice).to.never.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
it("WHEN the game is not playable and not free", function()
|
||||
local element = mockServices({
|
||||
GameInformation = Roact.createElement(GameInformation, {
|
||||
gameModel = createValidGameModel(false, 1000),
|
||||
})
|
||||
})
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local gamePrice = folder:FindFirstChild("GamePrice", true)
|
||||
expect(gamePrice).to.never.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
local GAME_BORDER_ICON = "rbxasset://textures/ui/LuaChat/graphic/gr-game-border-60x60.png"
|
||||
|
||||
local RoundedIcon = Roact.PureComponent:extend("RoundedIcon")
|
||||
|
||||
function RoundedIcon:render()
|
||||
local image = self.props.Image
|
||||
local size = self.props.Size
|
||||
local layoutOrder = self.props.LayoutOrder
|
||||
|
||||
return Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Image = image,
|
||||
Size = size,
|
||||
LayoutOrder = layoutOrder,
|
||||
}, {
|
||||
RoundCornerOverlay = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Image = GAME_BORDER_ICON,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return RoundedIcon
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
return function()
|
||||
local RoundedIcon = require(script.Parent.RoundedIcon)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
describe("SHOULD create and destroy without errors", function()
|
||||
it("WHEN passed no props", function()
|
||||
local element = Roact.createElement(RoundedIcon)
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
it("WHEN passed an Image", function()
|
||||
local element = Roact.createElement(RoundedIcon, {
|
||||
Image = "mock-id",
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
it("WHEN passed a valid Size", function()
|
||||
local element = Roact.createElement(RoundedIcon, {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local isInputTypeTouchOrMouseDown = require(Modules.LuaChat.Utils.isInputTypeTouchOrMouseDown)
|
||||
|
||||
local SEND_BUTTON_ICON = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-24x24.png"
|
||||
local SEND_BUTTON_ICON_PRESSED = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png"
|
||||
|
||||
local SEND_BUTTON_ICON_SIZE = 24
|
||||
local SEND_BUTTON_LEFT_PADDING = 12
|
||||
local SEND_BUTTON_RIGHT_PADDING = 25
|
||||
local SEND_BUTTON_FULL_WIDTH = SEND_BUTTON_ICON_SIZE + SEND_BUTTON_LEFT_PADDING + SEND_BUTTON_RIGHT_PADDING
|
||||
|
||||
|
||||
local SendToChatButton = Roact.PureComponent:extend("SendToChatButton")
|
||||
|
||||
function SendToChatButton.getWidth()
|
||||
return SEND_BUTTON_FULL_WIDTH
|
||||
end
|
||||
|
||||
function SendToChatButton:init()
|
||||
self.state = {
|
||||
isButtonPressed = false,
|
||||
}
|
||||
|
||||
self.onSendButtonInputBegan = function(_, inputObject)
|
||||
if isInputTypeTouchOrMouseDown(inputObject) then
|
||||
self:setState({
|
||||
isButtonPressed = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
self.onSendButtonInputEnded = function(_, inputObject)
|
||||
if isInputTypeTouchOrMouseDown(inputObject) then
|
||||
self:setState({
|
||||
isButtonPressed = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SendToChatButton:render()
|
||||
local onActivated = self.props.onActivated
|
||||
local layoutOrder = self.props.LayoutOrder
|
||||
|
||||
local isButtonPressed = self.state.isButtonPressed
|
||||
|
||||
local sendButtonImage = SEND_BUTTON_ICON
|
||||
if isButtonPressed then
|
||||
sendButtonImage = SEND_BUTTON_ICON_PRESSED
|
||||
end
|
||||
|
||||
return Roact.createElement("ImageButton", {
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(0, SEND_BUTTON_FULL_WIDTH, 1, 0),
|
||||
|
||||
[Roact.Event.InputBegan] = self.onSendButtonInputBegan,
|
||||
[Roact.Event.InputEnded] = self.onSendButtonInputEnded,
|
||||
[Roact.Event.Activated] = onActivated,
|
||||
},{
|
||||
SendButton = Roact.createElement("ImageLabel", {
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
Image = sendButtonImage,
|
||||
Position = UDim2.new(0, SEND_BUTTON_LEFT_PADDING, 0.5, 0),
|
||||
Size = UDim2.new(0, SEND_BUTTON_ICON_SIZE, 0, SEND_BUTTON_ICON_SIZE),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return SendToChatButton
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
return function()
|
||||
local SendToChatButton = require(script.Parent.SendToChatButton)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
|
||||
describe("SHOULD create and destroy without errors", function()
|
||||
it("WHEN passed no props", function()
|
||||
local element = Roact.createElement(SendToChatButton)
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
|
||||
it("SHOULD return a number from getWidth", function()
|
||||
local width = SendToChatButton.getWidth()
|
||||
expect(type(width)).to.equal("number")
|
||||
end)
|
||||
end
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactAnalyticsSharedGameItem = require(Modules.LuaChat.Services.RoactAnalyticsSharedGameItem)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local RoactServices = require(Modules.LuaApp.RoactServices)
|
||||
|
||||
local GameInformation = require(Modules.LuaChat.Components.ShareGameToChatFromChat.GameInformation)
|
||||
local RoundedIcon = require(Modules.LuaChat.Components.ShareGameToChatFromChat.RoundedIcon)
|
||||
local SendToChatButton = require(Modules.LuaChat.Components.ShareGameToChatFromChat.SendToChatButton)
|
||||
|
||||
local ConversationActions = require(Modules.LuaChat.Actions.ConversationActions)
|
||||
|
||||
local Constants = require(Modules.LuaChat.Constants)
|
||||
local isInputTypeTouchOrMouseDown = require(Modules.LuaChat.Utils.isInputTypeTouchOrMouseDown)
|
||||
|
||||
local UNPRESSED_BACKGROUND_COLOR = Constants.Color.WHITE
|
||||
local PRESSED_BACKGROUND_COLOR = Constants.Color.GRAY5
|
||||
local DEFAULT_SEPARATOR_LINE_COLOR = Constants.Color.GRAY4
|
||||
|
||||
local TOTAL_HEIGHT = 84
|
||||
|
||||
local GAME_ICON_SIZE = Constants.SharedGamesConfig.Thumbnail.SHOWN_SIZE
|
||||
local GAME_ICON_LEFT_PADDING = 15
|
||||
local GAME_ICON_RIGHT_PADDING = 12
|
||||
local GAME_ICON_TOP_PADDING = 12
|
||||
local GAME_ICON_FULL_WIDTH = GAME_ICON_SIZE + GAME_ICON_LEFT_PADDING + GAME_ICON_RIGHT_PADDING
|
||||
|
||||
local GAME_LOADING_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png"
|
||||
|
||||
local SEND_BUTTON_WIDTH = SendToChatButton.getWidth()
|
||||
|
||||
local function verticalSpacer(props)
|
||||
local height = props.height
|
||||
local layoutOrder = props.LayoutOrder
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, height),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = layoutOrder,
|
||||
})
|
||||
end
|
||||
|
||||
local function horizontalSpacer(props)
|
||||
local width = props.width
|
||||
local layoutOrder = props.LayoutOrder
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, width, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = layoutOrder,
|
||||
})
|
||||
end
|
||||
|
||||
local SharedGameItem = Roact.PureComponent:extend("SharedGameItem")
|
||||
|
||||
function SharedGameItem:init()
|
||||
self.state = {
|
||||
isGameInputDown = false,
|
||||
}
|
||||
|
||||
self.onGameButtonActivated = function()
|
||||
-- TODO: SOC-3805 Can remove string coercion when using proper model
|
||||
local universeId = self.props.game and tostring(self.props.game.universeId)
|
||||
if universeId then
|
||||
local notificationType = GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED
|
||||
GuiService:BroadcastNotification(string.format("%s", universeId), notificationType)
|
||||
end
|
||||
end
|
||||
|
||||
self.onGameItemInputBegan = function(_, inputObject)
|
||||
if isInputTypeTouchOrMouseDown(inputObject) then
|
||||
self:setState({
|
||||
isGameInputDown = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
self.onGameItemInputEnded = function(_, inputObject)
|
||||
if isInputTypeTouchOrMouseDown(inputObject) then
|
||||
self:setState({
|
||||
isGameInputDown = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
self.onSendButtonActivated = function()
|
||||
local gameModel = self.props.game
|
||||
|
||||
local activeConversationId = self.props.activeConversationId
|
||||
local analytics = self.props.analytics
|
||||
local gameUrl = self.props.gameUrl
|
||||
local isSharing = self.props.isSharing
|
||||
|
||||
local universeId = gameModel and tostring(gameModel.universeId)
|
||||
|
||||
if universeId then
|
||||
if not isSharing then
|
||||
self.props.shareGameToChat(activeConversationId, analytics, universeId, gameUrl)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGameItem:render()
|
||||
local gameModel = self.props.game
|
||||
local layoutOrder = self.props.layoutOrder
|
||||
|
||||
local gameThumbnails = self.props.gameThumbnails
|
||||
|
||||
-- TODO: SOC-3805 `gameModel` should be using an actual model.
|
||||
-- Currently this is the raw data derived from the WebApi endpoint
|
||||
-- (called from ShareGameToChatFromChatThunks:FetchGames)
|
||||
-- This means we have to coerce all ids into strings manually here
|
||||
local gameId = gameModel and tostring(gameModel.universeId)
|
||||
local gameIcon = gameId and gameThumbnails[gameId] or GAME_LOADING_ICON
|
||||
|
||||
local isGameInputDown = self.state.isGameInputDown
|
||||
local backgroundColor = UNPRESSED_BACKGROUND_COLOR
|
||||
if isGameInputDown then
|
||||
backgroundColor = PRESSED_BACKGROUND_COLOR
|
||||
end
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundColor3 = backgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = layoutOrder,
|
||||
Size = UDim2.new(1, 0, 0, TOTAL_HEIGHT),
|
||||
},{
|
||||
Content = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
GameButtonContainer = Roact.createElement("ImageButton", {
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, -SEND_BUTTON_WIDTH, 1, 0),
|
||||
|
||||
[Roact.Event.Activated] = self.onGameButtonActivated,
|
||||
[Roact.Event.InputBegan] = self.onGameItemInputBegan,
|
||||
[Roact.Event.InputEnded] = self.onGameItemInputEnded,
|
||||
},{
|
||||
LayoutHorizontal = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}),
|
||||
LeftMargin = Roact.createElement(horizontalSpacer, {
|
||||
width = GAME_ICON_LEFT_PADDING,
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
GameIconSection = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(0, GAME_ICON_SIZE, 1, 0),
|
||||
}, {
|
||||
LayoutVertical = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
}),
|
||||
TopMargin = Roact.createElement(verticalSpacer, {
|
||||
height = GAME_ICON_TOP_PADDING,
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
GameIcon = Roact.createElement(RoundedIcon, {
|
||||
Image = gameIcon,
|
||||
Size = UDim2.new(0, GAME_ICON_SIZE, 0, GAME_ICON_SIZE),
|
||||
LayoutOrder = 2,
|
||||
}),
|
||||
}),
|
||||
IconToInfoPadding = Roact.createElement(horizontalSpacer, {
|
||||
width = GAME_ICON_RIGHT_PADDING,
|
||||
LayoutOrder = 3,
|
||||
}),
|
||||
GameInfoAntiPadding = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -GAME_ICON_FULL_WIDTH, 1, 0),
|
||||
LayoutOrder = 3,
|
||||
}, {
|
||||
GameInfo = gameModel and Roact.createElement(GameInformation, {
|
||||
gameModel = gameModel,
|
||||
}),
|
||||
})
|
||||
}),
|
||||
|
||||
SendButtonContainer = Roact.createElement(SendToChatButton, {
|
||||
onActivated = self.onSendButtonActivated,
|
||||
LayoutOrder = 2,
|
||||
}),
|
||||
}),
|
||||
|
||||
Separator = Roact.createElement("Frame", {
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
BackgroundColor3 = DEFAULT_SEPARATOR_LINE_COLOR,
|
||||
BorderSizePixel = 0,
|
||||
Position = UDim2.new(0, GAME_ICON_FULL_WIDTH, 1, 0),
|
||||
Size = UDim2.new(1, -GAME_ICON_FULL_WIDTH, 0, 1),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
SharedGameItem = RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
-- TODO: SOC-3392 Kill activeConversationId and pass conversationId as routing prop
|
||||
activeConversationId = state.ChatAppReducer.ActiveConversationId,
|
||||
gameThumbnails = state.GameThumbnails,
|
||||
-- TODO: SOC-3896 Allow this to come from parent component and not in store
|
||||
isSharing = state.ChatAppReducer.ShareGameToChatAsync.sharingGame,
|
||||
}
|
||||
end,
|
||||
function(dispatch)
|
||||
return {
|
||||
shareGameToChat = function(activeConversationId, analytics, universeId)
|
||||
analytics.reportShareGameToChatFromChat(activeConversationId, tostring(universeId))
|
||||
return dispatch(ConversationActions.ShareGame(activeConversationId, universeId))
|
||||
end,
|
||||
}
|
||||
end
|
||||
)(SharedGameItem)
|
||||
|
||||
SharedGameItem = RoactServices.connect({
|
||||
analytics = RoactAnalyticsSharedGameItem,
|
||||
})(SharedGameItem)
|
||||
|
||||
return SharedGameItem
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
return function()
|
||||
local SharedGameItem = require(script.Parent.SharedGameItem)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local AppReducer = require(Modules.LuaApp.AppReducer)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local Rodux = require(Modules.Common.Rodux)
|
||||
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
|
||||
|
||||
local MOCK_UNIVERSE_ID = "MOCK_UNIVERSE_ID"
|
||||
local MOCK_THUMBNAIL = "MOCK_THUMBNAIL"
|
||||
|
||||
-- TODO: SOC-3805 `gameModel` should be using an actual model.
|
||||
local function createValidGameModel(universeId)
|
||||
return {
|
||||
imageToken = "mock-token",
|
||||
name = "mock-name",
|
||||
universeId = universeId,
|
||||
isPlayable = true,
|
||||
price = 1000,
|
||||
creatorName = "mock-creator",
|
||||
}
|
||||
end
|
||||
|
||||
local storeWithGameThumbnails = Rodux.Store.new(AppReducer, {
|
||||
GameThumbnails = {
|
||||
[MOCK_UNIVERSE_ID] = MOCK_THUMBNAIL,
|
||||
},
|
||||
})
|
||||
|
||||
local function createSharedGameItemTest(universeId)
|
||||
local element = mockServices({
|
||||
SharedGameItem = Roact.createElement(SharedGameItem, {
|
||||
game = createValidGameModel(universeId),
|
||||
}),
|
||||
}, {
|
||||
includeStoreProvider = true,
|
||||
store = storeWithGameThumbnails,
|
||||
})
|
||||
|
||||
return element
|
||||
end
|
||||
|
||||
describe("SHOULD create and destroy without errors", function()
|
||||
it("WHEN passed no props", function()
|
||||
local element = mockServices({
|
||||
SharedGameItem = Roact.createElement(SharedGameItem),
|
||||
}, {
|
||||
includeStoreProvider = true,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("WHEN passed a valid gameModel prop", function()
|
||||
local element = createSharedGameItemTest(MOCK_UNIVERSE_ID)
|
||||
|
||||
local folder = Instance.new("Folder")
|
||||
local instance = Roact.mount(element, folder)
|
||||
|
||||
local gameInfoInstance = folder:FindFirstChild("GameInfo", true)
|
||||
expect(gameInfoInstance).to.be.ok()
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Immutable = require(Modules.Common.Immutable)
|
||||
local memoize = require(Modules.Common.memoize)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local RoactServices = require(Modules.LuaApp.RoactServices)
|
||||
local RoactNetworking = require(Modules.LuaApp.Services.RoactNetworking)
|
||||
|
||||
local LuaAppConstants = require(LuaApp.Constants)
|
||||
local LuaChatConstants = require(LuaChat.Constants)
|
||||
|
||||
local LocalizedTextLabel = require(LuaApp.Components.LocalizedTextLabel)
|
||||
local LoadingBar = require(LuaApp.Components.LoadingBar)
|
||||
local RefreshScrollingFrame = require(LuaApp.Components.RefreshScrollingFrame)
|
||||
local SharedGameItem = require(LuaChat.Components.SharedGameItem)
|
||||
|
||||
local ApiFetchGamesData = require(LuaApp.Thunks.ApiFetchGamesData)
|
||||
local ApiFetchGamesInSort = require(LuaApp.Thunks.ApiFetchGamesInSort)
|
||||
local RetrievalStatus = require(LuaApp.Enum.RetrievalStatus)
|
||||
|
||||
local GAME_ITEM_HEIGHT = 84
|
||||
local NO_GAMES_TIPS_TOP_PADDING = 30
|
||||
local NO_GAMES_TIPS_LABEL_HEIGHT = 25
|
||||
|
||||
local SharedGamesList = Roact.PureComponent:extend("SharedGamesList")
|
||||
|
||||
SharedGamesList.defaultProps = {
|
||||
visible = false
|
||||
}
|
||||
|
||||
function SharedGamesList:init()
|
||||
self.loadMoreGames = function(count)
|
||||
self.isLoadingMore = true
|
||||
local loadCount = count or LuaChatConstants.DEFAULT_GAME_FETCH_COUNT
|
||||
local sorts = self.props.sorts
|
||||
local selectedSortName = self.props.gameSort
|
||||
local dispatchLoadMoreGames = self.props.dispatchLoadMoreGames
|
||||
local networking = self.props.networking
|
||||
|
||||
local selectedSort = sorts[selectedSortName]
|
||||
|
||||
return dispatchLoadMoreGames(networking, selectedSort, selectedSort.rowsRequested, loadCount,
|
||||
selectedSort.nextPageExclusiveStartId)
|
||||
end
|
||||
|
||||
self.refresh = function()
|
||||
return self.props.dispatchRefresh(self.props.networking, self.props.gameSort)
|
||||
end
|
||||
|
||||
if self.props.visible then
|
||||
self.refresh()
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGamesList:willUpdate()
|
||||
if not self.props.fetchingSortTokenStatus or
|
||||
self.props.fetchingSortTokenStatus == RetrievalStatus.Fetching then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.props.fetchingGamesStatus then
|
||||
self.refresh()
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGamesList:render()
|
||||
if not self.props.visible then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not self.props.fetchingSortTokenStatus or
|
||||
self.props.fetchingSortTokenStatus == RetrievalStatus.Fetching then
|
||||
return Roact.createElement(LoadingBar)
|
||||
end
|
||||
|
||||
local games = self.props.games
|
||||
local sortName = self.props.gameSort
|
||||
local sorts = self.props.sorts
|
||||
|
||||
local selectedSort = sorts[sortName]
|
||||
local gameCount = #selectedSort.entries
|
||||
local hasMoreRows = selectedSort.hasMoreRows
|
||||
|
||||
local gamesItems = {}
|
||||
gamesItems["Layout"] = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
})
|
||||
|
||||
for index = 1, gameCount do
|
||||
local uid = selectedSort.entries[index].universeId
|
||||
gamesItems[index] = Roact.createElement(SharedGameItem, {
|
||||
game = games[uid],
|
||||
itemHeight = GAME_ITEM_HEIGHT,
|
||||
layoutOrder = index,
|
||||
})
|
||||
end
|
||||
|
||||
if (not self.props.fetchingGamesStatus or self.props.fetchingGamesStatus == RetrievalStatus.Fetching)
|
||||
and gameCount == 0 then
|
||||
return Roact.createElement(LoadingBar)
|
||||
end
|
||||
|
||||
return Roact.createElement(RefreshScrollingFrame, {
|
||||
BackgroundColor3 = LuaChatConstants.Color.GRAY6,
|
||||
CanvasSize = UDim2.new(1, 0, 0, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
onLoadMore = hasMoreRows and self.loadMoreGames,
|
||||
refresh = self.refresh,
|
||||
}, {
|
||||
SharedGamesList = gameCount > 0 and Roact.createElement("Frame", {
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, gameCount * GAME_ITEM_HEIGHT),
|
||||
LayoutOrder = 3,
|
||||
}, gamesItems),
|
||||
|
||||
NoGamesTip = (gameCount == 0) and Roact.createElement(LocalizedTextLabel, {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Position = UDim2.new(0, 0, 0, NO_GAMES_TIPS_TOP_PADDING),
|
||||
Size = UDim2.new(1, 0, 0, NO_GAMES_TIPS_LABEL_HEIGHT),
|
||||
Text = LuaChatConstants.SharedGamesConfig.SortsAttribute[sortName].ERROR_TIP_LOCALIZATION_KEY,
|
||||
TextColor3 = LuaChatConstants.Color.GRAY2,
|
||||
TextSize = 20,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local function sortSorts(a, b)
|
||||
return a.displayName:lower() < b.displayName:lower()
|
||||
end
|
||||
|
||||
local selectSorts = memoize(function(gameSorts, gameSortsContents)
|
||||
local sorts = {}
|
||||
|
||||
for _, sortInfo in pairs(gameSorts) do
|
||||
local gameSortContents = gameSortsContents[sortInfo.name]
|
||||
|
||||
local sort = Immutable.JoinDictionaries(sortInfo, {
|
||||
entries = gameSortContents.entries,
|
||||
rowsRequested = gameSortContents.rowsRequested,
|
||||
hasMoreRows = gameSortContents.hasMoreRows,
|
||||
nextPageExclusiveStartId = gameSortContents.nextPageExclusiveStartId,
|
||||
})
|
||||
|
||||
table.insert(sorts, sort)
|
||||
sorts[sort.name] = sort
|
||||
end
|
||||
|
||||
table.sort(sorts, sortSorts)
|
||||
return sorts
|
||||
end)
|
||||
|
||||
SharedGamesList = RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
fetchingSortTokenStatus = state.RequestsStatus.GameSortTokenFetchingStatus[LuaAppConstants.GameSortGroups.ChatGames],
|
||||
fetchingGamesStatus = state.RequestsStatus.GameSortsStatus[props.gameSort],
|
||||
games = state.Games,
|
||||
sorts = selectSorts(state.GameSorts, state.GameSortsContents),
|
||||
}
|
||||
end,
|
||||
|
||||
function(dispatch)
|
||||
return {
|
||||
dispatchRefresh = function(networking, sortName)
|
||||
return dispatch(ApiFetchGamesData(networking, nil, sortName))
|
||||
end,
|
||||
|
||||
dispatchLoadMoreGames = function(networking, sort, startRows, maxRows, nextPageExclusiveStartId)
|
||||
return dispatch(ApiFetchGamesInSort(networking, sort, true, {
|
||||
startRows = startRows,
|
||||
maxRows = maxRows,
|
||||
exclusiveStartId = nextPageExclusiveStartId
|
||||
}))
|
||||
end
|
||||
}
|
||||
end
|
||||
)(SharedGamesList)
|
||||
|
||||
return RoactServices.connect({
|
||||
networking = RoactNetworking,
|
||||
})(SharedGamesList)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
return function()
|
||||
local SharedGamesList = require(script.Parent.SharedGamesList)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
local element = mockServices({
|
||||
SharedGamesList = Roact.createElement(SharedGamesList, {
|
||||
gameSort = "Popular",
|
||||
}),
|
||||
}, {
|
||||
includeStoreProvider = true,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,473 @@
|
||||
local GuiService = game:GetService("GuiService")
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local Constants = require(Modules.LuaChat.Constants)
|
||||
local ConversationActions = require(Modules.LuaChat.Actions.ConversationActions)
|
||||
local formatInteger = require(Modules.LuaChat.Utils.formatInteger)
|
||||
local getGameUrlByPlaceId = require(Modules.LuaChat.Utils.getGameUrlByPlaceId)
|
||||
local LocalizedTextLabel = require(Modules.LuaApp.Components.LocalizedTextLabel)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactAnalyticsSharedGameItem = require(Modules.LuaChat.Services.RoactAnalyticsSharedGameItem)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local RoactServices = require(Modules.LuaApp.RoactServices)
|
||||
local Text = require(Modules.Common.Text)
|
||||
|
||||
local DEFAULT_BACKGROUND_COLOR = Constants.Color.WHITE
|
||||
local DEFAULT_GAME_ADDITIONAL_INFO_LABEL_HEIGHT = 14
|
||||
local DEFAULT_GAME_ADDITIONAL_INFO_LABEL_TEXT_SIZE = 14
|
||||
local DEFAULT_GAME_CREATOR_LABEL_BOTTOM_PADDING = 3
|
||||
local DEFAULT_GAME_CREATOR_LABEL_COLOR = Constants.Color.GRAY2
|
||||
local DEFAULT_GAME_CREATOR_LABEL_HEIGHT = 14
|
||||
local DEFAULT_GAME_CREATOR_LABEL_TEXT_SIZE = 15
|
||||
local DEFAULT_GAME_CREATOR_LABEL_TOP_PADDING = 6
|
||||
local DEFAULT_GAME_NAME_LABEL_COLOR = Constants.Color.GRAY1
|
||||
local DEFAULT_GAME_NAME_LABEL_HEIGHT = 20
|
||||
local DEFAULT_GAME_NAME_LABEL_TEXT_SIZE = 23
|
||||
local DEFAULT_GAME_ICON_LEFT_PADDING = 15
|
||||
local DEFAULT_GAME_ICON_RIGHT_PADDING = 12
|
||||
local DEFAULT_GAME_ICON_SIZE = Constants.SharedGamesConfig.Thumbnail.SHOWN_SIZE
|
||||
local DEFAULT_GAME_ICON_TOP_PADDING = 12
|
||||
local DEFAULT_ITEM_HEIGHT = 84
|
||||
local DEFAULT_ITEM_PRESSED_BACKGROUND_COLOR = Constants.Color.GRAY5
|
||||
local DEFAULT_PRICE_COLOR = Constants.Color.GREEN_PRIMARY
|
||||
local DEFAULT_ROBUX_ICON_SIZE = 12
|
||||
local DEFAULT_SEND_BUTTON_ICON_SIZE = 24
|
||||
local DEFAULT_SEND_BUTTON_LEFT_PADDING = 12
|
||||
local DEFAULT_SEND_BUTTON_RIGHT_PADDING = 25
|
||||
local DEFAULT_SEPARATOR_LINE_COLOR = Constants.Color.GRAY4
|
||||
local DEFAULT_TEXT_FONT = Enum.Font.SourceSans
|
||||
|
||||
local GAME_LOADING_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-game.png"
|
||||
local GAME_BORDER_ICON = "rbxasset://textures/ui/LuaChat/graphic/gr-game-border-60x60.png"
|
||||
local ROBUX_ICON = "rbxasset://textures/ui/LuaChat/icons/ic-robux.png"
|
||||
local SEND_BUTTON_ICON = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-24x24.png"
|
||||
local SENT_BUTTON_ICON = "rbxasset://textures/ui/LuaChat/icons/icon-share-game-pressed-24x24.png"
|
||||
|
||||
local FFlagLuaChatShareGameToChatFromChatV2 = settings():GetFFlag("LuaChatShareGameToChatFromChatV2")
|
||||
|
||||
local SharedGameItem = Roact.PureComponent:extend("SharedGameItem")
|
||||
|
||||
SharedGameItem.defaultProps = {
|
||||
backgroundColor = DEFAULT_BACKGROUND_COLOR,
|
||||
gameAdditionalLabelHeight = DEFAULT_GAME_ADDITIONAL_INFO_LABEL_HEIGHT,
|
||||
gameAdditionalLabelTextSize = DEFAULT_GAME_ADDITIONAL_INFO_LABEL_TEXT_SIZE,
|
||||
gameCreatorLabelBottomPadding = DEFAULT_GAME_CREATOR_LABEL_BOTTOM_PADDING,
|
||||
gameCreatorLabelColor = DEFAULT_GAME_CREATOR_LABEL_COLOR,
|
||||
gameCreatorLabelHeight = DEFAULT_GAME_CREATOR_LABEL_HEIGHT,
|
||||
gameCreatorLabelTopPadding = DEFAULT_GAME_CREATOR_LABEL_TOP_PADDING,
|
||||
gameCreatorLabelTextSize = DEFAULT_GAME_CREATOR_LABEL_TEXT_SIZE,
|
||||
gameIconSize = DEFAULT_GAME_ICON_SIZE,
|
||||
gameIconLeftPadding = DEFAULT_GAME_ICON_LEFT_PADDING,
|
||||
gameIconRightPadding = DEFAULT_GAME_ICON_RIGHT_PADDING,
|
||||
gameIconTopPadding = DEFAULT_GAME_ICON_TOP_PADDING,
|
||||
gameNameLabelColor = DEFAULT_GAME_NAME_LABEL_COLOR,
|
||||
gameNameLabelHeight = DEFAULT_GAME_NAME_LABEL_HEIGHT,
|
||||
gameNameLabelTextSize = DEFAULT_GAME_NAME_LABEL_TEXT_SIZE,
|
||||
itemPressedBackgroundColor = DEFAULT_ITEM_PRESSED_BACKGROUND_COLOR,
|
||||
itemHeight = DEFAULT_ITEM_HEIGHT,
|
||||
priceColor = DEFAULT_PRICE_COLOR,
|
||||
robuxIconSize = DEFAULT_ROBUX_ICON_SIZE,
|
||||
sendButtonIconSize = DEFAULT_SEND_BUTTON_ICON_SIZE,
|
||||
sendButtonLeftPadding = DEFAULT_SEND_BUTTON_LEFT_PADDING,
|
||||
sendButtonRightPadding = DEFAULT_SEND_BUTTON_RIGHT_PADDING,
|
||||
separatorLineColor = DEFAULT_SEPARATOR_LINE_COLOR,
|
||||
textFont = DEFAULT_TEXT_FONT
|
||||
}
|
||||
|
||||
function SharedGameItem:init()
|
||||
self.state = {
|
||||
gameItemDown = false,
|
||||
gameItemActivated = false,
|
||||
sendButtonDown = false,
|
||||
sendButtonActivated = false,
|
||||
}
|
||||
|
||||
self.creatorName = nil
|
||||
self.gameNameTextLabel = nil
|
||||
self.gameCreatorTextLabel = nil
|
||||
|
||||
self.onGameButtonActivated = function()
|
||||
self:openGameDetails()
|
||||
end
|
||||
|
||||
self.onSendButtonInputBegan = function(_, inputObject)
|
||||
if (inputObject.UserInputType == Enum.UserInputType.Touch or
|
||||
inputObject.UserInputType == Enum.UserInputType.MouseButton1) and
|
||||
inputObject.UserInputState == Enum.UserInputState.Begin then
|
||||
self:onSendButtonDown()
|
||||
end
|
||||
end
|
||||
|
||||
self.onSendButtonInputEnded = function(_, inputObject)
|
||||
self:onSendButtonUp()
|
||||
end
|
||||
|
||||
self.onGameItemInputBegan = function(_, inputObject)
|
||||
if (inputObject.UserInputType == Enum.UserInputType.Touch or
|
||||
inputObject.UserInputType == Enum.UserInputType.MouseButton1) and
|
||||
inputObject.UserInputState == Enum.UserInputState.Begin then
|
||||
self:onGameItemDown()
|
||||
end
|
||||
end
|
||||
|
||||
self.onGameItemInputEnded = function(_, inputObject)
|
||||
self:onGameItemUp()
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGameItem:onSendButtonDown()
|
||||
if not self.state.sendButtonDown then
|
||||
self:eventDisconnect()
|
||||
|
||||
self.userInputServiceCon = UserInputService.InputEnded:Connect(function()
|
||||
self:onSendButtonUp()
|
||||
end)
|
||||
|
||||
self:setState({
|
||||
sendButtonDown = true,
|
||||
sendButtonActivated = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGameItem:onSendButtonUp(buttonActivated)
|
||||
if self.state.sendButtonDown or self.state.sendButtonActivated ~= buttonActivated then
|
||||
self:setState({
|
||||
sendButtonDown = false,
|
||||
sendButtonActivated = buttonActivated,
|
||||
})
|
||||
end
|
||||
|
||||
self:eventDisconnect()
|
||||
end
|
||||
|
||||
function SharedGameItem:onGameItemDown()
|
||||
if not self.state.gameItemDown then
|
||||
self:eventDisconnect()
|
||||
|
||||
self.userInputServiceCon = UserInputService.InputEnded:Connect(function()
|
||||
self:onGameItemUp()
|
||||
end)
|
||||
|
||||
self:setState({
|
||||
gameItemDown = true,
|
||||
gameItemActivated = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGameItem:onGameItemUp(buttonActivated)
|
||||
if self.state.gameItemDown or self.state.gameItemActivated ~= buttonActivated then
|
||||
self:setState({
|
||||
gameItemDown = false,
|
||||
gameItemActivated = buttonActivated,
|
||||
})
|
||||
end
|
||||
|
||||
self:eventDisconnect()
|
||||
end
|
||||
|
||||
function SharedGameItem:openGameDetails()
|
||||
local notificationType = GuiService:GetNotificationTypeList().VIEW_GAME_DETAILS_ANIMATED
|
||||
GuiService:BroadcastNotification(string.format("%d", self.props.game.placeId), notificationType)
|
||||
end
|
||||
|
||||
function SharedGameItem:render()
|
||||
local activeConversationId = self.props.activeConversationId
|
||||
local analytics = self.props.analytics
|
||||
local backgroundColor = self.props.backgroundColor
|
||||
local gameAdditionalLabelHeight = self.props.gameAdditionalLabelHeight
|
||||
local gameAdditionalLabelTextSize = self.props.gameAdditionalLabelTextSize
|
||||
local gameCreatorLabelBottomPadding = self.props.gameCreatorLabelBottomPadding
|
||||
local gameCreatorLabelColor = self.props.gameCreatorLabelColor
|
||||
local gameCreatorLabelHeight = self.props.gameCreatorLabelHeight
|
||||
local gameCreatorLabelTextSize = self.props.gameCreatorLabelTextSize
|
||||
local gameCreatorLabelTopPadding = self.props.gameCreatorLabelTopPadding
|
||||
local gameIconLeftPadding = self.props.gameIconLeftPadding
|
||||
local gameIconRightPadding = self.props.gameIconRightPadding
|
||||
local gameIconSize = self.props.gameIconSize
|
||||
local gameIconTopPadding = self.props.gameIconTopPadding
|
||||
local gameIconWidth = gameIconSize + gameIconLeftPadding + gameIconRightPadding
|
||||
local gameNameLabelHeight = self.props.gameNameLabelHeight
|
||||
local gameNameLabelColor = self.props.gameNameLabelColor
|
||||
local gameNameLabelTextSize = self.props.gameNameLabelTextSize
|
||||
local gameThumbnails = self.props.gameThumbnails
|
||||
local itemHeight = self.props.itemHeight
|
||||
local itemPressedBackgroundColor = self.props.itemPressedBackgroundColor
|
||||
local isSharing = self.props.isSharing
|
||||
local layoutOrder = self.props.layoutOrder
|
||||
local placeId = tostring(self.props.game.placeId)
|
||||
local universeId = self.props.game.universeId
|
||||
-- TODO wait lua app team store the playability of the game, default value is true
|
||||
-- Ticket: MOBLUAPP-683, Review http://swarm.roblox.local/reviews/226723/
|
||||
local playable = FFlagLuaChatShareGameToChatFromChatV2 or self.props.game.isPlayable
|
||||
local priceColor = self.props.priceColor
|
||||
local robuxIconSize = self.props.robuxIconSize
|
||||
local sendButtonIconSize = self.props.sendButtonIconSize
|
||||
local sendButtonLeftPadding = self.props.sendButtonLeftPadding
|
||||
local sendButtonRightPadding = self.props.sendButtonRightPadding
|
||||
local sendButtonWidth = sendButtonIconSize + sendButtonLeftPadding + sendButtonRightPadding
|
||||
local separatorLineColor = self.props.separatorLineColor
|
||||
local textFont = self.props.textFont
|
||||
|
||||
local gameId = FFlagLuaChatShareGameToChatFromChatV2 and universeId or placeId
|
||||
|
||||
local additionalInfoFrameHeight = gameAdditionalLabelHeight + gameCreatorLabelBottomPadding
|
||||
local showPrice = self.props.game.price ~= nil and playable
|
||||
local showAdditionalInfo = showPrice or (not playable)
|
||||
local gameIcon = gameThumbnails[gameId] or GAME_LOADING_ICON
|
||||
local gameInfoHeight = showAdditionalInfo and
|
||||
(gameNameLabelHeight + gameCreatorLabelHeight + 2 * gameCreatorLabelBottomPadding + gameAdditionalLabelHeight) or
|
||||
(gameNameLabelHeight + gameCreatorLabelHeight + gameCreatorLabelBottomPadding)
|
||||
local gameUrl = FFlagLuaChatShareGameToChatFromChatV2 and getGameUrlByPlaceId(placeId)
|
||||
or self.props.game.url
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundColor3 = self.state.gameItemDown and itemPressedBackgroundColor or backgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = FFlagLuaChatShareGameToChatFromChatV2 and layoutOrder or 0,
|
||||
Size = UDim2.new(1, 0, 0, itemHeight),
|
||||
},{
|
||||
Separator = Roact.createElement("Frame", {
|
||||
AnchorPoint = Vector2.new(0, 1),
|
||||
BackgroundColor3 = separatorLineColor,
|
||||
BorderSizePixel = 0,
|
||||
Position = UDim2.new(0, gameIconWidth, 1, 0),
|
||||
Size = UDim2.new(1, -gameIconWidth, 0, 1),
|
||||
}),
|
||||
|
||||
Game = Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 0, itemHeight),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
|
||||
GameButtonContainer = Roact.createElement("TextButton", {
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, -sendButtonWidth, 1, 0),
|
||||
Text = "",
|
||||
|
||||
[Roact.Event.Activated] = self.onGameButtonActivated,
|
||||
[Roact.Event.InputBegan] = self.onGameItemInputBegan,
|
||||
[Roact.Event.InputEnded] = self.onGameItemInputEnded,
|
||||
},{
|
||||
Icon = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = gameIcon,
|
||||
Position = UDim2.new(0, gameIconLeftPadding, 0, gameIconTopPadding),
|
||||
Size = UDim2.new(0, gameIconSize, 0, gameIconSize),
|
||||
}, {
|
||||
RoundCornerOverlay = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = GAME_BORDER_ICON,
|
||||
Size = UDim2.new(0, gameIconSize, 0, gameIconSize),
|
||||
}),
|
||||
}),
|
||||
|
||||
GameInfo = Roact.createElement("Frame", {
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Position = UDim2.new(0, gameIconWidth, 0.5, 0),
|
||||
Size = UDim2.new(1, -gameIconWidth, 0, gameInfoHeight),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
|
||||
Name = Roact.createElement("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = textFont,
|
||||
LayoutOrder = 1,
|
||||
Size = UDim2.new(1, 0, 0, gameNameLabelHeight),
|
||||
Text = self.props.game.name,
|
||||
TextColor3 = gameNameLabelColor,
|
||||
TextSize = gameNameLabelTextSize,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
[Roact.Ref] = function(rbx)
|
||||
if rbx then
|
||||
self.gameNameTextLabel = rbx
|
||||
end
|
||||
end,
|
||||
}),
|
||||
|
||||
Creator = Roact.createElement(LocalizedTextLabel, {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = textFont,
|
||||
LayoutOrder = 2,
|
||||
Size = UDim2.new(1, 0, 0, gameCreatorLabelHeight + gameCreatorLabelTopPadding),
|
||||
Text = {"Feature.Chat.ShareGameToChat.By", creatorName = self.props.game.creatorName},
|
||||
TextColor3 = gameCreatorLabelColor,
|
||||
TextSize = gameCreatorLabelTextSize,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
|
||||
[Roact.Ref] = function(rbx)
|
||||
if rbx then
|
||||
self.gameCreatorTextLabel = rbx
|
||||
self.creatorName = rbx.Text
|
||||
end
|
||||
end
|
||||
}),
|
||||
|
||||
NotAvailableTip = not playable and Roact.createElement(LocalizedTextLabel, {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = textFont,
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, additionalInfoFrameHeight),
|
||||
Text = "Feature.Chat.ShareGameToChat.GameNotAvailable",
|
||||
TextColor3 = gameCreatorLabelColor,
|
||||
TextSize = gameAdditionalLabelTextSize,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
}),
|
||||
|
||||
GamePrice = showPrice and Roact.createElement("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = 3,
|
||||
Size = UDim2.new(1, 0, 0, additionalInfoFrameHeight),
|
||||
}, {
|
||||
Layout = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 3),
|
||||
}),
|
||||
|
||||
RobuxIcon = Roact.createElement("ImageLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = ROBUX_ICON,
|
||||
LayoutOrder = 1,
|
||||
ScaleType = Enum.ScaleType.Fit,
|
||||
Size = UDim2.new(0, robuxIconSize, 0, robuxIconSize),
|
||||
}),
|
||||
|
||||
Price = Roact.createElement("TextLabel",{
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -15, 1, 0),
|
||||
Font = DEFAULT_TEXT_FONT,
|
||||
LayoutOrder = 2,
|
||||
Text = formatInteger(self.props.game.price),
|
||||
TextColor3 = priceColor,
|
||||
TextSize = gameAdditionalLabelTextSize,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
}, {
|
||||
padding = Roact.createElement("UIPadding", {
|
||||
PaddingLeft = UDim.new(3, 0),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
|
||||
SendButtonContainer = Roact.createElement("TextButton", {
|
||||
Active = not isSharing,
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
LayoutOrder = 2,
|
||||
Position = UDim2.new(1, -sendButtonWidth, 0, 0),
|
||||
Size = UDim2.new(0, sendButtonWidth, 1, 0),
|
||||
Text = "",
|
||||
|
||||
[Roact.Event.InputBegan] = self.onSendButtonInputBegan,
|
||||
[Roact.Event.InputEnded] = self.onSendButtonInputEnded,
|
||||
[Roact.Event.Activated] = function()
|
||||
if not isSharing then
|
||||
self.props.shareGameToChat(activeConversationId, analytics, placeId, gameUrl)
|
||||
end
|
||||
end,
|
||||
},{
|
||||
SendButton = Roact.createElement("ImageLabel", {
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = false,
|
||||
Image = self.state.sendButtonDown and SENT_BUTTON_ICON or SEND_BUTTON_ICON,
|
||||
Position = UDim2.new(0, sendButtonLeftPadding, 0.5, 0),
|
||||
Size = UDim2.new(0, sendButtonIconSize, 0, sendButtonIconSize),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
function SharedGameItem:didMount()
|
||||
local function resizeGameName()
|
||||
self.gameNameTextLabel.Text = Text.Truncate(self.props.game.name, self.props.textFont,
|
||||
self.gameNameTextLabel.TextSize, self.gameNameTextLabel.AbsoluteSize.X, "...")
|
||||
end
|
||||
|
||||
local function resizeGameCreator()
|
||||
self.gameCreatorTextLabel.Text = Text.Truncate(self.creatorName, self.props.textFont,
|
||||
self.gameCreatorTextLabel.TextSize, self.gameCreatorTextLabel.AbsoluteSize.X, "...")
|
||||
end
|
||||
|
||||
resizeGameName()
|
||||
resizeGameCreator()
|
||||
|
||||
self.connections = {}
|
||||
table.insert(self.connections, self.gameNameTextLabel:GetPropertyChangedSignal("Text"):Connect(resizeGameName))
|
||||
table.insert(self.connections, self.gameNameTextLabel:GetPropertyChangedSignal("AbsoluteSize"):Connect(resizeGameName))
|
||||
table.insert(self.connections, self.gameCreatorTextLabel:GetPropertyChangedSignal("Text"):Connect(resizeGameCreator))
|
||||
table.insert(
|
||||
self.connections,
|
||||
self.gameCreatorTextLabel:GetPropertyChangedSignal("AbsoluteSize"):Connect(resizeGameCreator)
|
||||
)
|
||||
end
|
||||
|
||||
function SharedGameItem:willUnmount()
|
||||
for _, connection in pairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
self:eventDisconnect()
|
||||
end
|
||||
|
||||
function SharedGameItem:eventDisconnect()
|
||||
if self.userInputServiceCon then
|
||||
self.userInputServiceCon:Disconnect()
|
||||
self.userInputServiceCon = nil
|
||||
end
|
||||
end
|
||||
|
||||
SharedGameItem = RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
activeConversationId = state.ChatAppReducer.ActiveConversationId,
|
||||
gameThumbnails = state.GameThumbnails,
|
||||
isSharing = state.ChatAppReducer.ShareGameToChatAsync.sharingGame,
|
||||
}
|
||||
end,
|
||||
function(dispatch)
|
||||
return {
|
||||
shareGameToChat = function(activeConversationId, analytics, placeId, url)
|
||||
analytics.reportShareGameToChatFromChat(activeConversationId, tostring(placeId))
|
||||
return dispatch(ConversationActions.ShareGame(activeConversationId, url))
|
||||
end,
|
||||
}
|
||||
end
|
||||
)(SharedGameItem)
|
||||
|
||||
SharedGameItem = RoactServices.connect({
|
||||
analytics = RoactAnalyticsSharedGameItem,
|
||||
})(SharedGameItem)
|
||||
|
||||
return SharedGameItem
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
return function()
|
||||
local SharedGameItem = require(script.Parent.SharedGameItem)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Game = require(Modules.LuaApp.Models.Game)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
local game = Game.mock()
|
||||
game.url = "http://www.roblox.com/game/10395446"
|
||||
game.isPlayable = true
|
||||
game.price = 0
|
||||
|
||||
local element = mockServices({
|
||||
SharedGameItem = Roact.createElement(SharedGameItem, {
|
||||
itemHeight = 84,
|
||||
game = game,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
}, {
|
||||
includeStoreProvider = true,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,161 @@
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local LocalizedTextLabel = require(LuaApp.Components.LocalizedTextLabel)
|
||||
local LoadingIndicator = require(LuaApp.Components.LoadingIndicator)
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local RoactRodux = require(Modules.Common.RoactRodux)
|
||||
local SharedGameItem = require(LuaChat.Components.SharedGameItem)
|
||||
local ShareGameToChatThunks = require(LuaChat.Actions.ShareGameToChatFromChat.ShareGameToChatFromChatThunks)
|
||||
|
||||
local DEFAULT_BACKGROUND_COLOR = Constants.Color.GRAY6
|
||||
local DEFAULT_ITEM_HEIGHT = 84
|
||||
local DEFAULT_TIPS_LABEL_COLOR = Constants.Color.GRAY2
|
||||
local DEFAULT_TIPS_LABEL_FONT = Enum.Font.SourceSans
|
||||
local DEFAULT_TIPS_LABEL_HEIGHT = 25
|
||||
local DEFAULT_TIPS_LABEL_TEXT_SIZE = 20
|
||||
local DEFAULT_TIPS_LABEL_TOP_MARGIN = 30
|
||||
|
||||
local SCROLLING_FRAME_IMAGE = "rbxasset://textures/ui/LuaChat/9-slice/scroll-bar.png"
|
||||
|
||||
local SharedGameList = Roact.PureComponent:extend("SharedGameList")
|
||||
|
||||
SharedGameList.defaultProps = {
|
||||
backgroundColor = DEFAULT_BACKGROUND_COLOR,
|
||||
itemHeight = DEFAULT_ITEM_HEIGHT,
|
||||
tipsLabelColor = DEFAULT_TIPS_LABEL_COLOR,
|
||||
tipsLabelFont = DEFAULT_TIPS_LABEL_FONT,
|
||||
tipsLabelHeight = DEFAULT_TIPS_LABEL_HEIGHT,
|
||||
tipsLabelTextSize = DEFAULT_TIPS_LABEL_TEXT_SIZE,
|
||||
tipsLabelTopMargin = DEFAULT_TIPS_LABEL_TOP_MARGIN,
|
||||
}
|
||||
|
||||
function SharedGameList:init()
|
||||
self.gamesList = nil
|
||||
self.isLoading = false
|
||||
|
||||
self.onScrollingFrameRef = function(rbx)
|
||||
if rbx then
|
||||
self.gamesList = rbx
|
||||
else
|
||||
warn("can not capture scrolling frame")
|
||||
end
|
||||
end
|
||||
|
||||
self:ShouldFetchGames(self.props)
|
||||
end
|
||||
|
||||
function SharedGameList:render()
|
||||
local backgroundColor = self.props.backgroundColor
|
||||
local frameHeight = self.props.frameHeight
|
||||
local gamesDetailInfos = self.props.gamesInfo
|
||||
local gameSorts = self.props.gameSorts
|
||||
local itemHeight = self.props.itemHeight
|
||||
local sortName = self.props.gameSort
|
||||
local tipsLabelColor = self.props.tipsLabelColor
|
||||
local tipsLabelHeight = self.props.tipsLabelHeight
|
||||
local tipsLabelFont = self.props.tipsLabelFont
|
||||
local tipsLabelTextSize = self.props.tipsLabelTextSize
|
||||
local tipsLabelTopMargin = self.props.tipsLabelTopMargin
|
||||
|
||||
local itemsCount = 0
|
||||
local gamesListItems = {}
|
||||
|
||||
if not self.isLoading then
|
||||
gamesListItems["Layout"] = Roact.createElement("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
})
|
||||
|
||||
local games = gameSorts[sortName]
|
||||
itemsCount = #games.placeIds
|
||||
for index, placeId in ipairs(games.placeIds) do
|
||||
gamesListItems[index] = Roact.createElement(SharedGameItem, {
|
||||
itemHeight = itemHeight,
|
||||
game = gamesDetailInfos[placeId],
|
||||
layoutOrder = index,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local contentHeight = itemsCount * itemHeight
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
BackgroundColor3 = backgroundColor,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
GamesList = itemsCount ~= 0 and Roact.createElement("ScrollingFrame", {
|
||||
BackgroundTransparency = 1,
|
||||
BottomImage = SCROLLING_FRAME_IMAGE,
|
||||
BorderSizePixel = 0,
|
||||
CanvasSize = UDim2.new(1, 0, 0, contentHeight),
|
||||
MidImage = SCROLLING_FRAME_IMAGE,
|
||||
Size = contentHeight > frameHeight and UDim2.new(1, 0, 1, 0) or UDim2.new(1, 0, 0, contentHeight),
|
||||
ScrollBarThickness = 5,
|
||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||
TopImage = SCROLLING_FRAME_IMAGE,
|
||||
|
||||
[Roact.Ref] = self.onScrollingFrameRef,
|
||||
}, gamesListItems),
|
||||
|
||||
Indicator = self.isLoading and Roact.createElement(LoadingIndicator, {
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, tipsLabelTopMargin),
|
||||
}),
|
||||
|
||||
NoGamesTip = ((not self.isLoading) and itemsCount == 0) and Roact.createElement(LocalizedTextLabel, {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = tipsLabelFont,
|
||||
Position = UDim2.new(0, 0, 0, tipsLabelTopMargin),
|
||||
Size = UDim2.new(1, 0, 0, tipsLabelHeight),
|
||||
Text = Constants.SharedGamesConfig.SortsAttribute[sortName].ERROR_TIP_LOCALIZATION_KEY,
|
||||
TextColor3 = tipsLabelColor,
|
||||
TextSize = tipsLabelTextSize,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
function SharedGameList:didUpdate()
|
||||
if self.gamesList then
|
||||
self.gamesList.CanvasPosition = Vector2.new(0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
function SharedGameList:willUpdate(newProps)
|
||||
self:ShouldFetchGames(newProps)
|
||||
end
|
||||
|
||||
function SharedGameList:ShouldFetchGames(props)
|
||||
local gameSorts = props.gameSorts
|
||||
local sortName = props.gameSort
|
||||
local games = gameSorts[sortName]
|
||||
|
||||
self.isLoading = false
|
||||
if games == nil or games.placeIds == nil then
|
||||
if not ShareGameToChatThunks.HasGameFetchRequestCompleted(sortName, props.shareGameToChatAsync) then
|
||||
self.isLoading = true
|
||||
self.props.fetchGames(sortName, Constants.SharedGamesConfig.Thumbnail.FETCHED_SIZE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return RoactRodux.UNSTABLE_connect2(
|
||||
function(state, props)
|
||||
return {
|
||||
gamesInfo = state.ChatAppReducer.SharedGamesInfo,
|
||||
gameSorts = state.ChatAppReducer.SharedGameSorts,
|
||||
shareGameToChatAsync = state.ChatAppReducer.ShareGameToChatAsync,
|
||||
}
|
||||
end,
|
||||
function(dispatch)
|
||||
return {
|
||||
fetchGames = function(gameSortName, fetchedThumbnailSize)
|
||||
return dispatch(ShareGameToChatThunks.FetchGames(gameSortName, fetchedThumbnailSize))
|
||||
end,
|
||||
}
|
||||
end
|
||||
)(SharedGameList)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
return function()
|
||||
local SharedGameList = require(script.Parent.SharedGameList)
|
||||
|
||||
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
||||
|
||||
local Roact = require(Modules.Common.Roact)
|
||||
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
|
||||
|
||||
it("should create and destroy without errors", function()
|
||||
local element = mockServices({
|
||||
SharedGameList = Roact.createElement(SharedGameList, {
|
||||
frameHeight = 100,
|
||||
gameSort = "Popular",
|
||||
}),
|
||||
}, {
|
||||
includeStoreProvider = true,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local FONT = Constants.Font.TITLE
|
||||
local TEXT_SIZE = Constants.Font.FONT_SIZE_18
|
||||
local X_PADDING = 8
|
||||
|
||||
local TextButton = {}
|
||||
|
||||
TextButton.__index = TextButton
|
||||
|
||||
function TextButton.new(appState, name, textKey)
|
||||
local self = {}
|
||||
|
||||
self.enabled = true
|
||||
|
||||
local text = appState.localization:Format(textKey)
|
||||
|
||||
local textWidth = Text.GetTextWidth(text, FONT, TEXT_SIZE)
|
||||
|
||||
self.rbx = Create.new "TextButton" {
|
||||
Name = name,
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
Size = UDim2.new(0, textWidth + X_PADDING, 1, 0),
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "Label",
|
||||
Size = UDim2.new(0, textWidth, 0, TEXT_SIZE),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Text = text,
|
||||
Font = FONT,
|
||||
TextSize = TEXT_SIZE,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
},
|
||||
}
|
||||
|
||||
self.Pressed = Signal.new()
|
||||
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
if not self.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
self.Pressed:fire()
|
||||
end)
|
||||
|
||||
setmetatable(self, TextButton)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TextButton:SetEnabled(value)
|
||||
if value then
|
||||
self.rbx.Label.TextTransparency = 0
|
||||
else
|
||||
self.rbx.Label.TextTransparency = 0.7
|
||||
end
|
||||
|
||||
self.enabled = value
|
||||
end
|
||||
|
||||
return TextButton
|
||||
@@ -0,0 +1,178 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local ListEntry = require(LuaChat.Components.ListEntry)
|
||||
|
||||
local FFlagTextBoxOverrideManualFocusRelease = settings():GetFFlag("TextBoxOverrideManualFocusRelease")
|
||||
|
||||
local ICON_CELL_WIDTH = 60
|
||||
local CLEAR_TEXT_WIDTH = 48
|
||||
local HEIGHT = 48
|
||||
|
||||
local TextInputEntry = {}
|
||||
|
||||
function TextInputEntry.new(appState, icon, placeholder)
|
||||
local self = {}
|
||||
self.connections = {}
|
||||
setmetatable(self, {__index = TextInputEntry})
|
||||
|
||||
local size = 24
|
||||
local iconWidth = 0
|
||||
|
||||
local listEntry = ListEntry.new(appState, HEIGHT)
|
||||
self.listEntry = listEntry
|
||||
self.rbx = listEntry.rbx
|
||||
self.placeholder = placeholder
|
||||
|
||||
if icon then
|
||||
iconWidth = ICON_CELL_WIDTH
|
||||
local iconImageLabel = Create.new"Frame" {
|
||||
Name = "Icon",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, iconWidth, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Create.new"ImageLabel" {
|
||||
Name = "IconImage",
|
||||
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,
|
||||
},
|
||||
}
|
||||
iconImageLabel.Parent = self.rbx
|
||||
end
|
||||
|
||||
local textBox = Create.new"TextBox" {
|
||||
Name = "TextBox",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -iconWidth - CLEAR_TEXT_WIDTH, 1, 0),
|
||||
Position = UDim2.new(0, iconWidth, 0, 0),
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = "",
|
||||
PlaceholderText = placeholder or "",
|
||||
PlaceholderColor3 = Constants.Color.GRAY3,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
OverlayNativeInput = true,
|
||||
ClearTextOnFocus = false,
|
||||
ClipsDescendants = true,
|
||||
}
|
||||
if FFlagTextBoxOverrideManualFocusRelease then
|
||||
textBox.ManualFocusRelease = true
|
||||
end
|
||||
textBox.Parent = self.rbx
|
||||
|
||||
local clearButton = Create.new"ImageButton" {
|
||||
Name = "Clear",
|
||||
BackgroundTransparency = 1,
|
||||
ImageTransparency = 1,
|
||||
Size = UDim2.new(0, CLEAR_TEXT_WIDTH, 0, CLEAR_TEXT_WIDTH),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(1, -CLEAR_TEXT_WIDTH/2, 0.5, 0),
|
||||
AutoButtonColor = false,
|
||||
|
||||
Create.new"ImageLabel" {
|
||||
Name = "ClearImage",
|
||||
BackgroundTransparency = 1,
|
||||
ImageTransparency = 0,
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-clear-solid.png",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
clearButton.Parent = self.rbx
|
||||
|
||||
local clearButtonConnection = getInputEvent(clearButton):Connect(function()
|
||||
self.textBoxComponent.Text = ""
|
||||
end)
|
||||
table.insert(self.connections, clearButtonConnection)
|
||||
|
||||
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.textBoxComponent = textBox
|
||||
self.value = textBox.Text
|
||||
self.textBoxChanged = Signal.new()
|
||||
self.textBoxFocusLost = Signal.new()
|
||||
|
||||
local function updateClearButtonVisibility()
|
||||
local visible = (self.textBoxComponent.Text ~= "")
|
||||
clearButton.Visible = visible
|
||||
end
|
||||
|
||||
updateClearButtonVisibility()
|
||||
|
||||
local textChangedConnection = textBox:GetPropertyChangedSignal("Text"):Connect(function()
|
||||
self.value = self.textBoxComponent.Text
|
||||
self.textBoxChanged:fire(self.value)
|
||||
updateClearButtonVisibility()
|
||||
end)
|
||||
table.insert(self.connections, textChangedConnection)
|
||||
|
||||
local focusedConnection = textBox.Focused:Connect(updateClearButtonVisibility)
|
||||
table.insert(self.connections, focusedConnection)
|
||||
local focusLostConnection = textBox.FocusLost:Connect(function()
|
||||
self.textBoxFocusLost:fire()
|
||||
updateClearButtonVisibility()
|
||||
end)
|
||||
table.insert(self.connections, focusLostConnection)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TextInputEntry:SanitizeInput(sanitizeFunc)
|
||||
self.value = sanitizeFunc(self.value)
|
||||
self.textBoxComponent.Text = self.value
|
||||
return self.value
|
||||
end
|
||||
|
||||
function TextInputEntry:ReleaseFocus()
|
||||
self.textBoxComponent:ReleaseFocus()
|
||||
end
|
||||
|
||||
function TextInputEntry:ShowDivider(show)
|
||||
self.rbx.Divider.Visible = show
|
||||
end
|
||||
|
||||
function TextInputEntry:Update(value)
|
||||
if value ~= self.value then
|
||||
self.rbx.TextBox.Text = value
|
||||
if self.placeholder == nil and value ~= "" then
|
||||
self.placeholder = value
|
||||
self.rbx.TextBox.PlaceholderText = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TextInputEntry:Destruct()
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
|
||||
self.listEntry:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return TextInputEntry
|
||||
@@ -0,0 +1,202 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
local Text = require(Modules.Common.Text)
|
||||
local ToastComplete = require(LuaChat.Actions.ToastComplete)
|
||||
|
||||
local INITIAL_SIZE = UDim2.new(1, -96, 0, 56)
|
||||
local POSITION_HIDE = UDim2.new(0.5, 0, 1, 72)
|
||||
local POSITION_SHOW = UDim2.new(0.5, 0, 1, -56-48)
|
||||
|
||||
local TEXT_SIZE = Constants.Font.FONT_SIZE_16
|
||||
local TEXT_FONT = Enum.Font.SourceSans
|
||||
|
||||
local ANIMATION_DURATION = 2
|
||||
local NORMAL_PHONE_MINIMUM_WIDTH = 360
|
||||
local PADDING = 12
|
||||
local PHONE_MARGIN = 48
|
||||
local SMALL_PHONE_MARGIN = 24
|
||||
local SINGLE_LINE_HEIGHT = 22
|
||||
local TABLET_MAXIMUM_WIDTH = 400
|
||||
|
||||
local TOAST_BACKGROUND = "rbxasset://textures/ui/LuaChat/9-slice/error-toast.png"
|
||||
|
||||
local FFlagLuaChatToastRefactor = settings():GetFFlag("LuaChatToastRefactor")
|
||||
|
||||
local ToastComponent = {}
|
||||
ToastComponent.__index = ToastComponent
|
||||
|
||||
function ToastComponent.new(appState, route)
|
||||
local self = {}
|
||||
self.appState = appState
|
||||
self.route = route
|
||||
self.positionHide = POSITION_HIDE
|
||||
setmetatable(self, ToastComponent)
|
||||
|
||||
if FFlagLuaChatToastRefactor then
|
||||
self.rbx = Create.new"ImageLabel" {
|
||||
Name = "ToastComponent",
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Image = TOAST_BACKGROUND,
|
||||
Position = self.positionHide,
|
||||
Size = INITIAL_SIZE,
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(5, 5, 6, 6),
|
||||
Visible = false,
|
||||
|
||||
Create.new"TextLabel" {
|
||||
Name = "Message",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = TEXT_FONT,
|
||||
Position = UDim2.new(0, PADDING, 0, PADDING),
|
||||
Size = UDim2.new(1, -2 * PADDING, 1, -2 * PADDING),
|
||||
Text = "",
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = TEXT_SIZE,
|
||||
TextWrapped = true,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
}
|
||||
}
|
||||
else
|
||||
self.rbx = Create.new"Frame" {
|
||||
Name = "ToastComponent",
|
||||
Size = INITIAL_SIZE,
|
||||
Position = POSITION_HIDE,
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
BackgroundTransparency = 0.1,
|
||||
BackgroundColor3 = Constants.Color.GRAY1,
|
||||
BorderSizePixel = 0,
|
||||
Visible = true,
|
||||
Create.new"TextLabel" {
|
||||
Name = "Message",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextSize = TEXT_SIZE,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
Text = "",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
self.appState.store.changed:connect(function(current, previous)
|
||||
if current ~= previous then
|
||||
self:Update(current.ChatAppReducer.Toast)
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function ToastComponent:Update(toast)
|
||||
if toast == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- We don't want to show the toast if another one with the same id is being shown.
|
||||
if self.toast and (self.toast.id == toast.id) then
|
||||
return
|
||||
end
|
||||
|
||||
self.toast = toast
|
||||
self:Show(toast)
|
||||
end
|
||||
|
||||
function ToastComponent:Hide()
|
||||
self.rbx:TweenPosition(
|
||||
self.positionHide,
|
||||
Enum.EasingDirection.In,
|
||||
Enum.EasingStyle.Quad,
|
||||
Constants.Tween.DEFAULT_TWEEN_TIME,
|
||||
false,
|
||||
function(status)
|
||||
self.appState.store:dispatch(ToastComplete(self.toast))
|
||||
self.toast = nil
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
function ToastComponent:Show(toast)
|
||||
|
||||
local message = toast.messageKey ~= nil and
|
||||
self.appState.localization:Format(toast.messageKey, toast.messageArguments) or ""
|
||||
self.rbx.Message.Text = message
|
||||
|
||||
local positionShown
|
||||
if FFlagLuaChatToastRefactor then
|
||||
local isTablet = self.appState.store:getState().FormFactor == FormFactor.TABLET
|
||||
local screenGui = self.rbx:FindFirstAncestorOfClass("ScreenGui")
|
||||
|
||||
local screenWidth
|
||||
if isTablet then
|
||||
screenWidth = screenGui.AbsoluteSize.Y
|
||||
else
|
||||
screenWidth = screenGui.AbsoluteSize.X
|
||||
end
|
||||
|
||||
local margin, maxWidth, width, height
|
||||
if screenWidth < NORMAL_PHONE_MINIMUM_WIDTH then
|
||||
margin = SMALL_PHONE_MARGIN
|
||||
else
|
||||
margin = PHONE_MARGIN
|
||||
end
|
||||
|
||||
if isTablet then
|
||||
maxWidth = TABLET_MAXIMUM_WIDTH
|
||||
else
|
||||
maxWidth = screenWidth - 2 * margin - PADDING * 2
|
||||
end
|
||||
|
||||
local textHeight = Text.GetTextHeight(message, TEXT_FONT, TEXT_SIZE, maxWidth)
|
||||
if textHeight > SINGLE_LINE_HEIGHT then
|
||||
width = isTablet and TABLET_MAXIMUM_WIDTH or (screenWidth - margin * 2)
|
||||
else
|
||||
width = self.rbx.Message.TextBounds.X + PADDING * 2
|
||||
end
|
||||
height = textHeight + 2 * PADDING
|
||||
|
||||
positionShown = UDim2.new(0.5, 0, 1, -height - margin)
|
||||
self.positionHide = UDim2.new(0.5, 0, 1, height + margin)
|
||||
|
||||
self.rbx.Size = UDim2.new(0, width, 0, height)
|
||||
self.rbx.Position = self.positionHide
|
||||
self.rbx.Visible = true
|
||||
else
|
||||
local textWidth = self.rbx.Message.TextBounds.X
|
||||
|
||||
self.rbx.Size = UDim2.new(0, textWidth + PADDING * 2, 0, 56)
|
||||
self.rbx.Position = POSITION_HIDE
|
||||
|
||||
positionShown = POSITION_SHOW
|
||||
end
|
||||
|
||||
self.rbx:TweenPosition(
|
||||
positionShown,
|
||||
Enum.EasingDirection.Out,
|
||||
Enum.EasingStyle.Quad,
|
||||
Constants.Tween.DEFAULT_TWEEN_TIME,
|
||||
false,
|
||||
function(status)
|
||||
wait(ANIMATION_DURATION)
|
||||
if self.toast.id == toast.id then
|
||||
self:Hide()
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
return ToastComponent
|
||||
@@ -0,0 +1,105 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local LuaChat = script.Parent.Parent
|
||||
local Create = require(LuaChat.Create)
|
||||
|
||||
local INDICATOR_WIDTH = 60
|
||||
local INDICATOR_HEIGHT = 16
|
||||
local DOT_COUNT = 3
|
||||
local ANIMATION_SPEED_MULTIPLIER = 1.75
|
||||
|
||||
local TypingIndicator = {}
|
||||
|
||||
local function makeDot()
|
||||
return Create.new "ImageLabel" {
|
||||
Name = "DotContainer",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
SizeConstraint = Enum.SizeConstraint.RelativeYY,
|
||||
|
||||
Create.new "ImageLabel" {
|
||||
Name = "Dot",
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/ui/LuaChat/graphic/send-white.png",
|
||||
ImageColor3 = Color3.new(0.5, 0.5, 0.5),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
SizeConstraint = Enum.SizeConstraint.RelativeYY,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function TypingIndicator.new(appState, scale)
|
||||
scale = scale or 1
|
||||
|
||||
local self = {}
|
||||
self.connections = {}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "TypingIndicator",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, scale * INDICATOR_WIDTH, 0, scale * INDICATOR_HEIGHT)
|
||||
}
|
||||
|
||||
self.dots = {}
|
||||
|
||||
for i = 1, DOT_COUNT do
|
||||
local value = (i - 1) / (DOT_COUNT - 1)
|
||||
|
||||
local dot = makeDot()
|
||||
dot.Position = UDim2.new(value, 0, 0, 0)
|
||||
dot.AnchorPoint = Vector2.new(value, 0)
|
||||
dot.Parent = self.rbx
|
||||
|
||||
table.insert(self.dots, dot)
|
||||
end
|
||||
|
||||
setmetatable(self, TypingIndicator)
|
||||
|
||||
do
|
||||
local connection = self.rbx.AncestryChanged:Connect(function(object, parent)
|
||||
if object == self.rbx and parent == nil then
|
||||
self:Destroy()
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
do
|
||||
local connection = RunService.RenderStepped:Connect(function()
|
||||
if not self.rbx.Visible then
|
||||
return
|
||||
end
|
||||
|
||||
local time = (Workspace.DistributedGameTime * ANIMATION_SPEED_MULTIPLIER) % #self.dots
|
||||
|
||||
for i, dot in ipairs(self.dots) do
|
||||
local size = 0.5
|
||||
if time >= i - 1 and time <= i then
|
||||
size = 0.5 + 0.5 * math.sin(math.pi * (time % 1))
|
||||
end
|
||||
|
||||
dot.Dot.Size = UDim2.new(size, 0, size, 0)
|
||||
end
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TypingIndicator:Destroy()
|
||||
self.rbx:Destroy()
|
||||
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
self.connections = {}
|
||||
end
|
||||
|
||||
TypingIndicator.__index = TypingIndicator
|
||||
|
||||
return TypingIndicator
|
||||
@@ -0,0 +1,303 @@
|
||||
local Players = game:GetService("Players")
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local LuaApp = CoreGui.RobloxGui.Modules.LuaApp
|
||||
|
||||
local LuaChat = script.Parent.Parent
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Text = require(LuaChat.Text)
|
||||
local FormFactor = require(LuaApp.Enum.FormFactor)
|
||||
|
||||
local FFlagEnableChatMessageType = settings():GetFFlag("EnableChatMessageType")
|
||||
local Message = FFlagEnableChatMessageType and require(LuaChat.Models.Message)
|
||||
|
||||
local BUBBLE_PADDING = 10
|
||||
local TEXT_BUBBLE_X_PADDING = 54
|
||||
local TEXT_BUBBLE_X_TABLET_ADDITIONAL_PADDING = 112
|
||||
|
||||
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 UserChatBubble = {}
|
||||
|
||||
UserChatBubble.__index = UserChatBubble
|
||||
function UserChatBubble.new(appState, message, newContent, width)
|
||||
local self = {}
|
||||
setmetatable(self, UserChatBubble)
|
||||
|
||||
local conversationId = message.conversationId
|
||||
local isSending = isMessageSending(appState.store:getState().ChatAppReducer.Conversations[conversationId], message)
|
||||
|
||||
local state = appState.store:getState()
|
||||
local user = state.Users[message.senderTargetId]
|
||||
local username = user and user.name or "unknown user"
|
||||
|
||||
self.appState = appState
|
||||
self.paddingObject = nil
|
||||
self.message = message
|
||||
self.bubbleType = "UserChatBubble"
|
||||
self.connections = {}
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
self.width = width
|
||||
end
|
||||
|
||||
if FFlagEnableChatMessageType then
|
||||
if newContent then
|
||||
self.displayMessage = newContent
|
||||
elseif self.message.messageType == Message.MessageTypes.PlainText then
|
||||
self.displayMessage = self.message.content
|
||||
else
|
||||
self.displayMessage = ""
|
||||
end
|
||||
else
|
||||
self.displayMessage = newContent and newContent or self.message.content
|
||||
end
|
||||
|
||||
self.textContent = Create.new "TextLabel" {
|
||||
Name = "TextContent",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
TextColor3 = Constants.Color.WHITE,
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
Text = "",
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextWrapped = true,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
}
|
||||
|
||||
self.tail = Create.new "ImageLabel" {
|
||||
Name = "Tail",
|
||||
Size = UDim2.new(0, 6, 0, 6),
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
|
||||
self.bubble = Create.new "ImageLabel" {
|
||||
Name = "Bubble",
|
||||
BackgroundTransparency = 1,
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10, 10, 11, 11),
|
||||
LayoutOrder = 2,
|
||||
|
||||
Create.new "Frame" {
|
||||
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),
|
||||
|
||||
self.textContent,
|
||||
},
|
||||
|
||||
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, 1),
|
||||
|
||||
self.bubble,
|
||||
self.usernameLabel,
|
||||
}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "Message",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 0, 0, 0),
|
||||
|
||||
Create.new "UIListLayout" {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
},
|
||||
|
||||
self.bubbleContainer,
|
||||
}
|
||||
|
||||
-- 'isOutgoing' means "is sent by the local user". This function separates the tail position & color
|
||||
if isOutgoingMessage(message) then
|
||||
local bubbleColor = isSending and Constants.Color.BLUE_DISABLED or Constants.Color.BLUE_PRIMARY
|
||||
local textTransparency = isSending and Constants.Color.ALPHA_SHADOW_PRIMARY or 0
|
||||
|
||||
self.tail.AnchorPoint = Vector2.new(0, 0)
|
||||
self.tail.Position = UDim2.new(1, 0, 0, 0)
|
||||
self.tail.ImageColor3 = bubbleColor
|
||||
|
||||
self.bubble.ImageColor3 = bubbleColor
|
||||
self.bubble.AnchorPoint = Vector2.new(1, 0)
|
||||
self.bubble.Position = UDim2.new(1, -10, 0, 0)
|
||||
|
||||
self.textContent.TextColor3 = Constants.Color.WHITE
|
||||
self.textContent.TextTransparency = textTransparency
|
||||
|
||||
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, TEXT_BUBBLE_X_PADDING, 0, 0)
|
||||
|
||||
self.textContent.TextColor3 = Constants.Color.GRAY1
|
||||
|
||||
self.rbx.UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Left
|
||||
end
|
||||
self:Update(message)
|
||||
|
||||
if not FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
local connection = self.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
|
||||
self:Resize()
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserChatBubble:Resize()
|
||||
local padding = (TEXT_BUBBLE_X_PADDING + BUBBLE_PADDING) * 2 -- * 2 for both sides
|
||||
if self.appState.store:getState().FormFactor == FormFactor.TABLET then
|
||||
padding = padding + TEXT_BUBBLE_X_TABLET_ADDITIONAL_PADDING
|
||||
end
|
||||
|
||||
local viewportWidth
|
||||
if FFlagLuaChatInfiniteRelayoutRecursionFix then
|
||||
viewportWidth = self.width
|
||||
else
|
||||
viewportWidth = self.rbx.AbsoluteSize.X
|
||||
end
|
||||
|
||||
local textWidth = viewportWidth - padding
|
||||
|
||||
local textBounds = Text.GetTextBounds(
|
||||
self.textContent.Text,
|
||||
self.textContent.Font,
|
||||
self.textContent.TextSize,
|
||||
Vector2.new(textWidth, 10000)
|
||||
)
|
||||
|
||||
local doublePadding = BUBBLE_PADDING * 2
|
||||
self.bubble.Size = UDim2.new(0, textBounds.X + doublePadding, 0, textBounds.Y + doublePadding)
|
||||
|
||||
local containerHeight = self.bubble.AbsoluteSize.Y
|
||||
|
||||
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
|
||||
self.rbx.Size = UDim2.new(1, 0, 0, height)
|
||||
end
|
||||
|
||||
local function CreateFilteringText(appState, filteringTextKey, textColor)
|
||||
return Create.new "Frame" {
|
||||
Name = "ModeratedNotice",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 18),
|
||||
LayoutOrder = 3,
|
||||
|
||||
Create.new "TextLabel" {
|
||||
Name = "ModeratedNoticeText",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(1, -10, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
TextColor3 = textColor,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
Text = appState.localization:Format(filteringTextKey),
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function UserChatBubble:Update(message)
|
||||
self.message = message
|
||||
|
||||
if message.moderated then
|
||||
self.displayMessage = string.gsub(self.displayMessage, "[^%s]", "#")
|
||||
end
|
||||
|
||||
self.textContent.Text = self.displayMessage
|
||||
|
||||
local textColor
|
||||
local filteringTextKey = nil
|
||||
if message.moderated then
|
||||
filteringTextKey = "Feature.Chat.Message.MessageContentModerated"
|
||||
textColor = Constants.Color.RED_NEGATIVE
|
||||
elseif message.filteredForReceivers then
|
||||
filteringTextKey = "Feature.Chat.Message.Default"
|
||||
textColor = Constants.Color.GRAY3
|
||||
end
|
||||
|
||||
-- Either the message was moderated or it was filtered for some users (minors).
|
||||
if filteringTextKey and (not self.moderatedText) then
|
||||
self.moderatedText = CreateFilteringText(self.appState, filteringTextKey, textColor)
|
||||
self.moderatedText.Parent = self.rbx
|
||||
end
|
||||
|
||||
-- There was an error and the message was not sent.
|
||||
if (message.failed or message.moderated) and (not self.alertIcon) then
|
||||
self.alertIcon = Create.new "ImageLabel" {
|
||||
Name = "FailedIcon",
|
||||
Size = UDim2.new(0, 18, 0, 18),
|
||||
AnchorPoint = Vector2.new(1, 0.5),
|
||||
Position = UDim2.new(0, -10, 0.5, 0),
|
||||
ImageColor3 = Constants.Color.RED_PRIMARY,
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-alert.png"
|
||||
}
|
||||
self.alertIcon.Parent = self.bubble
|
||||
end
|
||||
|
||||
self:Resize()
|
||||
end
|
||||
|
||||
function UserChatBubble:Destruct()
|
||||
for _, connection in ipairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.connections = {}
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return UserChatBubble
|
||||
@@ -0,0 +1,168 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaApp = Modules.LuaApp
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local User = require(LuaApp.Models.User)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local ListEntry = require(Components.ListEntry)
|
||||
local UserThumbnail = require(Components.UserThumbnail)
|
||||
|
||||
local ICON_CELL_WIDTH = 60
|
||||
local HEIGHT = 56
|
||||
|
||||
local function userPresenceToText(localization, presence, lastLocation)
|
||||
if presence == User.PresenceType.OFFLINE then
|
||||
return localization:Format("Common.Presence.Label.Offline")
|
||||
elseif presence == User.PresenceType.ONLINE then
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
elseif presence == User.PresenceType.IN_GAME then
|
||||
if lastLocation ~= nil then
|
||||
return lastLocation
|
||||
else
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
end
|
||||
elseif presence == User.PresenceType.IN_STUDIO then
|
||||
if lastLocation ~= nil then
|
||||
return lastLocation
|
||||
else
|
||||
return localization:Format("Common.Presence.Label.Online")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local UserEntry = {}
|
||||
UserEntry.__index = UserEntry
|
||||
|
||||
function UserEntry.new(appState, user, icon, selected)
|
||||
local self = {
|
||||
user = user,
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, UserEntry)
|
||||
|
||||
self.icon = icon or "rbxasset://textures/ui/LuaChat/graphic/ic-checkbox.png"
|
||||
|
||||
local listEntry = ListEntry.new(appState, HEIGHT)
|
||||
self.rbx = listEntry.rbx
|
||||
|
||||
local userThumbnail = UserThumbnail.new(appState, user.id, true)
|
||||
userThumbnail.rbx.Size = UDim2.new(0, 36, 0, 36)
|
||||
userThumbnail.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
userThumbnail.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
self.userThumbnail = userThumbnail
|
||||
|
||||
local userThumbnailFrame = Create.new"Frame" {
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, ICON_CELL_WIDTH, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
userThumbnail.rbx,
|
||||
}
|
||||
userThumbnailFrame.Parent = self.rbx
|
||||
|
||||
local label = Create.new"TextLabel" {
|
||||
Name = "Label",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH, 0.75, 0),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH, 0.5, 1 - Constants.Font.FONT_SIZE_18),
|
||||
TextSize = Constants.Font.FONT_SIZE_18,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = user.name,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
}
|
||||
label.Parent = self.rbx
|
||||
|
||||
local sublabel = Create.new"TextLabel" {
|
||||
Name = "SubLabel",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH, 0.35, 0),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH, 0.5, 1),
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = userPresenceToText(appState.localization, user.presence, user.lastLocation),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
}
|
||||
sublabel.Parent = self.rbx
|
||||
|
||||
local value = Create.new"ImageLabel" {
|
||||
Name = "Icon",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 20, 0, 20),
|
||||
Position = UDim2.new(1, -32, .5, 0),
|
||||
Image = self.icon,
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Visible = user.id ~= tostring(Players.LocalPlayer.UserId),
|
||||
}
|
||||
value.Parent = self.rbx
|
||||
|
||||
local divider = Create.new"Frame" {
|
||||
Name = "Divider",
|
||||
BackgroundColor3 = Constants.Color.GRAY4,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1),
|
||||
}
|
||||
divider.Parent = self.rbx
|
||||
|
||||
self:SetIsSelected(selected)
|
||||
|
||||
self.tapped = Signal.new()
|
||||
|
||||
listEntry.tapped:connect(function()
|
||||
self.tapped:fire()
|
||||
end)
|
||||
|
||||
listEntry.beginHover:connect(function()
|
||||
userThumbnail.rbx.Overlay.ImageColor3 = Constants.Color.GRAY5
|
||||
end)
|
||||
|
||||
listEntry.endHover:connect(function()
|
||||
userThumbnail.rbx.Overlay.ImageColor3 = Constants.Color.WHITE
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserEntry:SetIsSelected(selected)
|
||||
if selected then
|
||||
self.rbx.Icon.Image = "rbxasset://textures/ui/LuaChat/graphic/ic-checkbox-on.png"
|
||||
else
|
||||
self.rbx.Icon.Image = self.icon
|
||||
end
|
||||
end
|
||||
|
||||
function UserEntry:Update(user, selected)
|
||||
self.user = user
|
||||
self.rbx.Label.Text = user.name
|
||||
self.rbx.SubLabel.Text = userPresenceToText(self.appState.localization, user.presence, user.lastLocation)
|
||||
|
||||
self:SetIsSelected(selected)
|
||||
|
||||
if user.id == tostring(Players.LocalPlayer.UserId) then
|
||||
self.rbx.Icon.Visible = false
|
||||
else
|
||||
self.rbx.Icon.Visible = true
|
||||
end
|
||||
|
||||
self.userThumbnail:Update(user)
|
||||
end
|
||||
|
||||
function UserEntry:Destruct()
|
||||
self.userThumbnail:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return UserEntry
|
||||
@@ -0,0 +1,150 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Create = require(LuaChat.Create)
|
||||
local Functional = require(Common.Functional)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local UserEntry = require(Components.UserEntry)
|
||||
|
||||
local ICON_CELL_WIDTH = 60
|
||||
|
||||
local UserList = {}
|
||||
UserList.__index = UserList
|
||||
|
||||
function UserList.new(appState, icon, filter)
|
||||
local self = {
|
||||
appState = appState,
|
||||
icon = icon,
|
||||
filter = filter,
|
||||
}
|
||||
setmetatable(self, UserList)
|
||||
|
||||
self.users = nil
|
||||
self.userEntries = {}
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
Size = UDim2.new(1, 0, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Create.new"UIListLayout" {
|
||||
Name = "ListLayout",
|
||||
SortOrder = "LayoutOrder",
|
||||
Padding = UDim.new(0, 0),
|
||||
},
|
||||
}
|
||||
|
||||
self.userSelected = Signal.new()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserList:ResetFinalDivider()
|
||||
local children = self.rbx:GetChildren()
|
||||
local lastChild = nil
|
||||
for i = 1, #children do
|
||||
local child = children[#children-i+1]
|
||||
if child:IsA("GuiObject") and child.Visible then
|
||||
if lastChild == nil or lastChild.LayoutOrder < child.LayoutOrder then
|
||||
lastChild = child
|
||||
end
|
||||
end
|
||||
end
|
||||
if lastChild then
|
||||
local divider = lastChild.Divider
|
||||
divider.Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1)
|
||||
divider.Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
function UserList:FormatFinalDivider()
|
||||
local children = self.rbx:GetChildren()
|
||||
local lastChild = nil
|
||||
for i = 1, #children do
|
||||
local child = children[#children-i+1]
|
||||
if child:IsA("GuiObject") and child.Visible then
|
||||
if lastChild == nil or lastChild.LayoutOrder < child.LayoutOrder then
|
||||
lastChild = child
|
||||
end
|
||||
end
|
||||
end
|
||||
if lastChild then
|
||||
local divider = lastChild.Divider
|
||||
divider.Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, 1)
|
||||
divider.Position = UDim2.new(0, ICON_CELL_WIDTH, 1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
function UserList:ApplySearch(searchTerm)
|
||||
searchTerm = searchTerm:upper()
|
||||
self:ResetFinalDivider()
|
||||
for _, userEntry in pairs(self.userEntries) do
|
||||
local name = userEntry.rbx.Label.Text:upper()
|
||||
local first = name:find(searchTerm)
|
||||
if first ~= nil then
|
||||
userEntry.rbx.LayoutOrder = first
|
||||
userEntry.rbx.Visible = true
|
||||
else
|
||||
userEntry.rbx.Visible = false
|
||||
end
|
||||
end
|
||||
self.rbx.ListLayout:ApplyLayout()
|
||||
self:FormatFinalDivider()
|
||||
end
|
||||
|
||||
function UserList:Update(users, selectedList)
|
||||
if self.users == users then
|
||||
return
|
||||
end
|
||||
self.users = users
|
||||
|
||||
for _, userEntry in pairs(self.userEntries) do
|
||||
userEntry.rbx.Visible = false
|
||||
end
|
||||
|
||||
local height = 0
|
||||
for _, user in pairs(users) do
|
||||
if self.filter == nil or self.filter(user) then
|
||||
local userEntry = self.userEntries[user.id]
|
||||
local selected = selectedList and Functional.Find(selectedList, user.id) or false
|
||||
|
||||
if userEntry == nil then
|
||||
self:ResetFinalDivider()
|
||||
|
||||
userEntry = UserEntry.new(self.appState, user, self.icon, selected)
|
||||
self.tappedConnection = userEntry.tapped:connect(function()
|
||||
userEntry.selected = not userEntry.selected
|
||||
self.userSelected:fire(user)
|
||||
end)
|
||||
userEntry.rbx.Parent = self.rbx
|
||||
self.userEntries[user.id] = userEntry
|
||||
|
||||
self:FormatFinalDivider()
|
||||
else
|
||||
userEntry.rbx.Visible = true
|
||||
userEntry:Update(user, selected)
|
||||
end
|
||||
height = height + userEntry.rbx.AbsoluteSize.Y
|
||||
end
|
||||
end
|
||||
self.rbx.Size = UDim2.new(1, 0, 0, height)
|
||||
end
|
||||
|
||||
function UserList:Destruct()
|
||||
for _, userEntry in pairs(self.userEntries) do
|
||||
userEntry:Destruct()
|
||||
end
|
||||
|
||||
if self.tappedConnection then
|
||||
self.tappedConnection:disconnect()
|
||||
end
|
||||
|
||||
self.users = {}
|
||||
self.userEntries = {}
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return UserList
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local LuaChat = Modules.LuaChat
|
||||
local LuaApp = Modules.LuaApp
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local User = require(LuaApp.Models.User)
|
||||
|
||||
local UserPresenceTextLabel = {}
|
||||
UserPresenceTextLabel.__index = UserPresenceTextLabel
|
||||
|
||||
function UserPresenceTextLabel.new(appState, userId, additionalProps)
|
||||
local self = {
|
||||
appState = appState,
|
||||
connections = {},
|
||||
lastUserModel = nil,
|
||||
userId = userId,
|
||||
}
|
||||
setmetatable(self, UserPresenceTextLabel)
|
||||
|
||||
self.rbx = Create.new("TextLabel")(
|
||||
{
|
||||
BackgroundTransparency = 1,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
TextColor3 = Constants.Color.GRAY3,
|
||||
Font = Enum.Font.SourceSans,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
},
|
||||
additionalProps
|
||||
)
|
||||
|
||||
table.insert(self.connections, appState.store.changed:connect(function(newState)
|
||||
self:Update(newState)
|
||||
end))
|
||||
self:Update(appState.store:getState())
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserPresenceTextLabel:RenderPresenceText(user)
|
||||
self.rbx.Text = User.userPresenceToText(self.appState.localization, user)
|
||||
end
|
||||
|
||||
function UserPresenceTextLabel:Update(state)
|
||||
local user = state.Users[self.userId]
|
||||
|
||||
if not user then
|
||||
return
|
||||
end
|
||||
|
||||
if user == self.lastUserModel then
|
||||
return
|
||||
end
|
||||
self.lastUserModel = user
|
||||
|
||||
self:RenderPresenceText(user)
|
||||
end
|
||||
|
||||
function UserPresenceTextLabel:Destruct()
|
||||
for _, connection in pairs(self.connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return UserPresenceTextLabel
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local HeadshotLoader = require(LuaChat.HeadshotLoader)
|
||||
local Signal = require(Common.Signal)
|
||||
local getInputEvent = require(LuaChat.Utils.getInputEvent)
|
||||
|
||||
local FFlagLuaChatToSplitRbxConnections = settings():GetFFlag("LuaChatToSplitRbxConnections")
|
||||
local FFlagLuaChatReplacePresenceIndicatorImages = settings():GetFFlag("LuaChatReplacePresenceIndicatorImages")
|
||||
|
||||
local OVERLAY_IMAGE_BIG = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-48x48.png"
|
||||
local OVERLAY_IMAGE_SMALL = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-36x36.png"
|
||||
local PRESENCE_DEFAULT_IMAGE = "rbxasset://textures/ui/LuaChat/graphic/indicator-background.png"
|
||||
|
||||
local UserThumbnail = {}
|
||||
|
||||
UserThumbnail.__index = UserThumbnail
|
||||
|
||||
function UserThumbnail.new(appState, userId, small)
|
||||
local self = {}
|
||||
self.connections = {}
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
self.rbx_connections = {}
|
||||
end
|
||||
self.appState = appState
|
||||
self.userId = userId
|
||||
self.clicked = Signal.new()
|
||||
|
||||
local size = small and 36 or 48
|
||||
local overlayImage = small and OVERLAY_IMAGE_SMALL or OVERLAY_IMAGE_BIG
|
||||
|
||||
if FFlagLuaChatReplacePresenceIndicatorImages then
|
||||
self.presenceIndicatorSize = small and 12 or 14
|
||||
self.presenceIndicatorSizeKey = Constants:GetPresenceIndicatorSizeKey(self.presenceIndicatorSize)
|
||||
end
|
||||
|
||||
self.headshot = Create.new "ImageLabel" {
|
||||
Name = "Avatar",
|
||||
Image = "",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
|
||||
local mask = Create.new "ImageLabel" {
|
||||
Name = "Overlay",
|
||||
Image = overlayImage,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}
|
||||
if FFlagLuaChatReplacePresenceIndicatorImages then
|
||||
self.pIndicatorBg = Create.new "ImageLabel" {
|
||||
Name = "Presence",
|
||||
Size = UDim2.new(0, self.presenceIndicatorSize, 0, self.presenceIndicatorSize),
|
||||
BackgroundTransparency = 1,
|
||||
Image = PRESENCE_DEFAULT_IMAGE,
|
||||
Position = UDim2.new(1, -self.presenceIndicatorSize, 1, -self.presenceIndicatorSize),
|
||||
Visible = false,
|
||||
}
|
||||
else
|
||||
self.pIndicatorBg = Create.new "ImageLabel" {
|
||||
Name = "Presence",
|
||||
Size = UDim2.new(0, 14, 0, 14),
|
||||
BackgroundTransparency = 1,
|
||||
Image = PRESENCE_DEFAULT_IMAGE,
|
||||
Position = UDim2.new(1, -14, 1, -14),
|
||||
Visible = false,
|
||||
}
|
||||
end
|
||||
self.rbx = Create.new "ImageButton" {
|
||||
Name = "UserThumbnail",
|
||||
BackgroundTransparency = 1,
|
||||
ImageTransparency = 1,
|
||||
Image = "",
|
||||
Size = UDim2.new(0, size, 0, size),
|
||||
AutoButtonColor = false,
|
||||
|
||||
self.headshot,
|
||||
mask,
|
||||
self.pIndicatorBg,
|
||||
}
|
||||
|
||||
setmetatable(self, UserThumbnail)
|
||||
|
||||
self:Update()
|
||||
|
||||
self.rbx.AncestryChanged:Connect(function(rbx, parent)
|
||||
if rbx == self.rbx and parent == nil then
|
||||
self:Destruct()
|
||||
end
|
||||
end)
|
||||
|
||||
do
|
||||
local connection = appState.store.changed:connect(function(state, oldState)
|
||||
if state.Users == oldState.Users then
|
||||
return
|
||||
end
|
||||
|
||||
if state.Users[userId] == oldState.Users[userId] then
|
||||
return
|
||||
end
|
||||
|
||||
self:Update()
|
||||
end)
|
||||
table.insert(self.connections, connection)
|
||||
end
|
||||
|
||||
local rbxConnectionList = self.connections
|
||||
if FFlagLuaChatToSplitRbxConnections then
|
||||
rbxConnectionList = self.rbx_connections
|
||||
end
|
||||
table.insert(rbxConnectionList,
|
||||
getInputEvent(self.rbx):Connect(function()
|
||||
self.clicked:fire(self.user)
|
||||
end)
|
||||
)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserThumbnail: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.rbx:Destroy()
|
||||
end
|
||||
|
||||
function UserThumbnail:Update()
|
||||
local user = self.appState.store:getState().Users[self.userId]
|
||||
|
||||
if not user then
|
||||
return
|
||||
end
|
||||
|
||||
if user == self.user then
|
||||
return
|
||||
end
|
||||
|
||||
self.user = user
|
||||
|
||||
HeadshotLoader:Load(self.headshot, self.userId)
|
||||
|
||||
local presenceImage
|
||||
if FFlagLuaChatReplacePresenceIndicatorImages then
|
||||
presenceImage = Constants.PresenceIndicatorImagesBySize[self.presenceIndicatorSizeKey][user.presence]
|
||||
else
|
||||
presenceImage = Constants.PresenceIndicatorImages[user.presence]
|
||||
end
|
||||
|
||||
if presenceImage then
|
||||
self.pIndicatorBg.Visible = true
|
||||
self.pIndicatorBg.Image = presenceImage
|
||||
else
|
||||
self.pIndicatorBg.Visible = false
|
||||
end
|
||||
end
|
||||
|
||||
return UserThumbnail
|
||||
@@ -0,0 +1,256 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
|
||||
local UserThumbnail = require(LuaChat.Components.UserThumbnail)
|
||||
|
||||
local LuaChatCreateChatUserScrolling = settings():GetFFlag("LuaChatCreateChatUserScrolling2")
|
||||
|
||||
local GROUP_ICON_SIZE = 24
|
||||
local ICON_CELL_WIDTH = 60
|
||||
local REMOVE_BUTTON_SIZE = 16
|
||||
local THUMBNAIL_LABEL_HEIGHT = 15
|
||||
local THUMBNAIL_PADDING_HEIGHT = 10
|
||||
local THUMBNAIL_PADDING_WIDTH = 16
|
||||
local THUMBNAIL_SIZE = 48
|
||||
|
||||
local EXTRA_END_PADDING = .5 * (ICON_CELL_WIDTH - GROUP_ICON_SIZE)
|
||||
local THUMBNAIL_PLUS_HEIGHT = THUMBNAIL_SIZE + THUMBNAIL_LABEL_HEIGHT + THUMBNAIL_PADDING_HEIGHT * 2
|
||||
local THUMBNAIL_PLUS_WIDTH = THUMBNAIL_SIZE + THUMBNAIL_PADDING_WIDTH
|
||||
|
||||
local UserThumbnailPlus = {}
|
||||
UserThumbnailPlus.__index = UserThumbnailPlus
|
||||
|
||||
function UserThumbnailPlus.new(appState)
|
||||
local self = {
|
||||
appState = appState,
|
||||
}
|
||||
setmetatable(self, UserThumbnailPlus)
|
||||
|
||||
self.rbx = Create.new"Frame" {
|
||||
Name = "ThumbnailFrame",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, THUMBNAIL_PLUS_WIDTH, 0, THUMBNAIL_PLUS_HEIGHT),
|
||||
}
|
||||
|
||||
self:SetEmptyThumbnail()
|
||||
|
||||
self.removed = Signal.new()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserThumbnailPlus:Update(user)
|
||||
if user == self.user then
|
||||
return
|
||||
end
|
||||
self.user = user
|
||||
|
||||
if user == nil then
|
||||
self:SetEmptyThumbnail(user)
|
||||
else
|
||||
self:SetThumbnail(user)
|
||||
end
|
||||
end
|
||||
|
||||
function UserThumbnailPlus:SetEmptyThumbnail()
|
||||
if self.userThumbnailFrame ~= nil then
|
||||
self.userThumbnailFrame.Parent = nil
|
||||
end
|
||||
self.userThumbnailFrame = Create.new"ImageLabel" {
|
||||
Name = "UserThumbnail",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, THUMBNAIL_SIZE, 0, THUMBNAIL_SIZE),
|
||||
Position = UDim2.new(0, 0, 0, THUMBNAIL_PADDING_HEIGHT),
|
||||
Image = "rbxasset://textures/ui/LuaChat/graphic/gr-profile-border-48x48-dotted.png",
|
||||
}
|
||||
self.userThumbnailFrame.Parent = self.rbx
|
||||
end
|
||||
|
||||
function UserThumbnailPlus:SetThumbnail(user)
|
||||
if self.userThumbnailFrame ~= nil then
|
||||
self.userThumbnailFrame.Parent = nil
|
||||
end
|
||||
|
||||
self.userThumbnail = UserThumbnail.new(self.appState, user.id, false)
|
||||
self.userThumbnail.rbx.Size = UDim2.new(0, THUMBNAIL_SIZE, 0, THUMBNAIL_SIZE)
|
||||
|
||||
self.userThumbnailFrame = Create.new"Frame" {
|
||||
Name = "UserThumbnail",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, THUMBNAIL_SIZE, 1, -THUMBNAIL_PADDING_HEIGHT*2),
|
||||
Position = UDim2.new(0, 0, 0, THUMBNAIL_PADDING_HEIGHT),
|
||||
|
||||
self.userThumbnail.rbx,
|
||||
Create.new"TextLabel" {
|
||||
Name = "UserName",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, THUMBNAIL_LABEL_HEIGHT),
|
||||
Position = UDim2.new(0, 0, 1, -THUMBNAIL_LABEL_HEIGHT),
|
||||
TextSize = Constants.Font.FONT_SIZE_12,
|
||||
TextColor3 = Constants.Color.GRAY1,
|
||||
Font = Enum.Font.SourceSans,
|
||||
Text = user.name,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
ClipsDescendants = true,
|
||||
},
|
||||
Create.new"ImageLabel" {
|
||||
Name = "RemoveImage",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, REMOVE_BUTTON_SIZE, 0, REMOVE_BUTTON_SIZE),
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-remove.png",
|
||||
},
|
||||
}
|
||||
|
||||
self.userThumbnailFrame.Parent = self.rbx
|
||||
|
||||
self.tapped = self.userThumbnail.clicked
|
||||
self.tapped:connect(function()
|
||||
self.removed:fire(self.user.id)
|
||||
end)
|
||||
end
|
||||
|
||||
function UserThumbnailPlus:Destruct()
|
||||
if self.userThumbnail then
|
||||
self.userThumbnail:Destruct()
|
||||
end
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
local UserThumbnailBar = {}
|
||||
UserThumbnailBar.__index = UserThumbnailBar
|
||||
|
||||
function UserThumbnailBar.new(appState, maxSize, layoutOrder)
|
||||
local self = {
|
||||
appState = appState,
|
||||
maxSize = maxSize,
|
||||
connections = {},
|
||||
}
|
||||
setmetatable(self, UserThumbnailBar)
|
||||
|
||||
local friendsIcon = Create.new"Frame" {
|
||||
Name = "GroupIcon",
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
Size = UDim2.new(0, ICON_CELL_WIDTH, 0, THUMBNAIL_SIZE),
|
||||
Position = UDim2.new(0, 0, 0, THUMBNAIL_PADDING_HEIGHT),
|
||||
Create.new"ImageLabel" {
|
||||
Name = "IconImage",
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, GROUP_ICON_SIZE, 0, GROUP_ICON_SIZE),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Image = "rbxasset://textures/ui/LuaChat/icons/ic-group.png",
|
||||
},
|
||||
Create.new"TextLabel" {
|
||||
Name = "FriendCount",
|
||||
BackgroundTransparency = 1,
|
||||
TextSize = Constants.Font.FONT_SIZE_14,
|
||||
TextColor3 = Constants.Color.GRAY2,
|
||||
Size = UDim2.new(1, 0, 0, THUMBNAIL_LABEL_HEIGHT),
|
||||
Position = UDim2.new(0, 0, 1, -THUMBNAIL_LABEL_HEIGHT),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
Text = "0/" .. maxSize,
|
||||
Font = "SourceSans",
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Bottom,
|
||||
},
|
||||
}
|
||||
|
||||
local canvasSizeX = EXTRA_END_PADDING + THUMBNAIL_PLUS_WIDTH * self.maxSize - THUMBNAIL_PADDING_WIDTH
|
||||
|
||||
if LuaChatCreateChatUserScrolling then
|
||||
self.rbx = Create.new"Frame" {
|
||||
Name = "UserThumbnailBar",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, THUMBNAIL_PLUS_HEIGHT),
|
||||
LayoutOrder = layoutOrder,
|
||||
friendsIcon,
|
||||
Create.new"ScrollingFrame" {
|
||||
Name = "UserScrollingBar",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, -ICON_CELL_WIDTH, 0, THUMBNAIL_PLUS_HEIGHT),
|
||||
Position = UDim2.new(0, ICON_CELL_WIDTH, 0, 0),
|
||||
LayoutOrder = layoutOrder,
|
||||
ScrollBarThickness = 0,
|
||||
ScrollingDirection = Enum.ScrollingDirection.X,
|
||||
ElasticBehavior = Enum.ElasticBehavior.WhenScrollable,
|
||||
CanvasSize = UDim2.new(0, canvasSizeX, 1, 0),
|
||||
},
|
||||
}
|
||||
self.friendsIcon = friendsIcon
|
||||
else
|
||||
self.rbx = Create.new"Frame" {
|
||||
Name = "UserThumbnailBar",
|
||||
BackgroundTransparency = 0,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.WHITE,
|
||||
Size = UDim2.new(1, 0, 0, THUMBNAIL_PLUS_HEIGHT),
|
||||
LayoutOrder = layoutOrder,
|
||||
friendsIcon,
|
||||
}
|
||||
end
|
||||
|
||||
self.removed = Signal.new()
|
||||
self.thumbnails = {}
|
||||
for i = 1, self.maxSize do
|
||||
local thumb = UserThumbnailPlus.new(appState)
|
||||
table.insert(self.thumbnails, thumb)
|
||||
if LuaChatCreateChatUserScrolling then
|
||||
thumb.rbx.Position = UDim2.new(0, THUMBNAIL_PLUS_WIDTH * (i-1), 0, 0)
|
||||
thumb.rbx.Parent = self.rbx.UserScrollingBar
|
||||
else
|
||||
thumb.rbx.Position = UDim2.new(0, ICON_CELL_WIDTH + THUMBNAIL_PLUS_WIDTH * (i-1), 0, 0)
|
||||
thumb.rbx.Parent = self.rbx
|
||||
end
|
||||
table.insert(self.connections, thumb.removed:connect(function(id)
|
||||
self.removed:fire(id)
|
||||
end))
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserThumbnailBar:Update(participants)
|
||||
local users = self.appState.store:getState().Users
|
||||
local participantCount = #participants
|
||||
for index,thumbnail in ipairs(self.thumbnails) do
|
||||
if index <= participantCount then
|
||||
thumbnail:Update(users[participants[index]])
|
||||
else
|
||||
thumbnail:Update(nil)
|
||||
end
|
||||
end
|
||||
if LuaChatCreateChatUserScrolling then
|
||||
self.friendsIcon.FriendCount.Text = participantCount .. "/" .. self.maxSize
|
||||
else
|
||||
self.rbx.GroupIcon.FriendCount.Text = participantCount .. "/" .. self.maxSize
|
||||
end
|
||||
end
|
||||
|
||||
function UserThumbnailBar:Destruct()
|
||||
for _, connection in pairs(self.connections) do
|
||||
connection:disconnect()
|
||||
end
|
||||
for _,thumbnail in ipairs(self.thumbnails) do
|
||||
thumbnail:Destruct()
|
||||
end
|
||||
self.thumbnails = {}
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return UserThumbnailBar
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local Modules = CoreGui.RobloxGui.Modules
|
||||
local Common = Modules.Common
|
||||
local LuaChat = Modules.LuaChat
|
||||
|
||||
local Constants = require(LuaChat.Constants)
|
||||
local Conversation = require(LuaChat.Models.Conversation)
|
||||
local Create = require(LuaChat.Create)
|
||||
local Signal = require(Common.Signal)
|
||||
local Text = require(LuaChat.Text)
|
||||
|
||||
local Components = LuaChat.Components
|
||||
local TypingIndicator = require(Components.TypingIndicator)
|
||||
local UserThumbnail = require(Components.UserThumbnail)
|
||||
|
||||
local BUBBLE_PADDING = 10
|
||||
local DEFAULT_SPACER_BETWEEN_BUBBLES = 2
|
||||
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 UserTypingIndicator = {}
|
||||
|
||||
UserTypingIndicator.__index = UserTypingIndicator
|
||||
|
||||
function UserTypingIndicator.new(appState, conversation)
|
||||
local self = {
|
||||
lastTyping = 0
|
||||
}
|
||||
|
||||
self.Resized = Signal.new()
|
||||
self.conversation = conversation
|
||||
|
||||
self.indicator = TypingIndicator.new(appState)
|
||||
self.indicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
self.indicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
self.indicator.rbx.Size = UDim2.new(0, 50, 0, 12)
|
||||
|
||||
local localUserId = tostring(Players.LocalPlayer.UserId)
|
||||
local otherUserId
|
||||
for _, participant in ipairs(conversation.participants) do
|
||||
if participant ~= localUserId then
|
||||
otherUserId = participant
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
self.otherUserId = otherUserId
|
||||
|
||||
self.thumbnail = UserThumbnail.new(appState, otherUserId, true)
|
||||
self.thumbnail.rbx.Position = UDim2.new(0, 10, 0, 0)
|
||||
self.thumbnail.rbx.Overlay.ImageColor3 = Constants.Color.GRAY6
|
||||
|
||||
self.tail = Create.new "ImageLabel" {
|
||||
Name = "Tail",
|
||||
Size = UDim2.new(0, 6, 0, 6),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Image = RECEIVED_TAIL,
|
||||
}
|
||||
|
||||
self.bubble = Create.new "ImageLabel" {
|
||||
Name = "Bubble",
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 56, 0, 0),
|
||||
Size = UDim2.new(0, 70, 0, 38),
|
||||
ScaleType = Enum.ScaleType.Slice,
|
||||
SliceCenter = Rect.new(10, 10, 11, 11),
|
||||
Image = RECEIVED_BUBBLE_WITH_TAIL,
|
||||
LayoutOrder = 2,
|
||||
|
||||
Create.new "Frame" {
|
||||
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),
|
||||
|
||||
self.indicator.rbx
|
||||
},
|
||||
|
||||
self.tail,
|
||||
}
|
||||
|
||||
self.rbx = Create.new "Frame" {
|
||||
Name = "UserTypingIndicator",
|
||||
Size = UDim2.new(1, 0, 0, 0),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = Constants.Color.GRAY6,
|
||||
|
||||
self.thumbnail.rbx,
|
||||
self.bubble,
|
||||
}
|
||||
|
||||
setmetatable(self, UserTypingIndicator)
|
||||
|
||||
self:Hide()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function UserTypingIndicator:Update(conversation)
|
||||
if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then
|
||||
return
|
||||
end
|
||||
|
||||
if conversation.usersTyping == self.conversation.usersTyping then
|
||||
return
|
||||
end
|
||||
|
||||
self.conversation = conversation
|
||||
|
||||
local timeNow = tick()
|
||||
|
||||
self.lastTyping = timeNow
|
||||
|
||||
if conversation.usersTyping[self.otherUserId] then
|
||||
self:Show()
|
||||
else
|
||||
self:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function UserTypingIndicator:Show()
|
||||
self.rbx.Visible = true
|
||||
self.indicator.rbx.Visible = true
|
||||
-- In order to treat indicator as a single line message, generate text
|
||||
-- bubble height from default font size 18 to ensure typing indiactor
|
||||
-- height will scale with font scale.
|
||||
local temporaryTextHeight = Text.GetTextHeight("", Enum.Font.SourceSans, Constants.Font.FONT_SIZE_18,
|
||||
self.rbx.AbsoluteSize.X)
|
||||
self.rbx.Size = UDim2.new(1, 0, 0, temporaryTextHeight + BUBBLE_PADDING * 2 + DEFAULT_SPACER_BETWEEN_BUBBLES)
|
||||
self.Resized:fire()
|
||||
end
|
||||
|
||||
function UserTypingIndicator:Hide()
|
||||
self.rbx.Visible = false
|
||||
self.indicator.rbx.Visible = false
|
||||
self.rbx.Size = UDim2.new(1, 0, 0, 0)
|
||||
self.Resized:fire()
|
||||
end
|
||||
|
||||
function UserTypingIndicator:Destruct()
|
||||
self.rbx:Destroy()
|
||||
end
|
||||
|
||||
return UserTypingIndicator
|
||||
Reference in New Issue
Block a user