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