--// Global Chat GUI (2x smaller, heartbeat online tracking)
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local UserInputService = game:GetService("UserInputService")
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
local API_BASE = "https://mii-response-server--721843.replit.app/api"
local JOIN_URL = API_BASE .. "/chat/join"
local LEAVE_URL = API_BASE .. "/chat/leave"
local SEND_URL = API_BASE .. "/chat/send"
local MESSAGES_URL = API_BASE .. "/chat/messages"
local ONLINE_URL = API_BASE .. "/chat/online"
local HEARTBEAT_URL = API_BASE .. "/chat/heartbeat"
local POLL_INTERVAL = 2
local ONLINE_INTERVAL = 5
local HEARTBEAT_INTERVAL = 10
local MAX_MESSAGE_LEN = 300
local function jsonDecodeSafe(str)
local ok, r = pcall(function() return HttpService:JSONDecode(str) end)
return ok and r or nil
end
local function jsonEncodeSafe(tbl)
local ok, r = pcall(function() return HttpService:JSONEncode(tbl) end)
return ok and r or nil
end
local function httpRequest(method, url, bodyTable)
local body = bodyTable and jsonEncodeSafe(bodyTable) or nil
if typeof(request) == "function" then
local ok, res = pcall(function()
return request({
Url = url, Method = method,
Headers = body and { ["Content-Type"] = "application/json" } or {},
Body = body,
})
end)
if ok and res then
local success = res.Success or (res.StatusCode and res.StatusCode >= 200 and res.StatusCode < 300)
return success, res.Body or ""
end
end
if method == "GET" then
local ok, res = pcall(function() return game:HttpGet(url) end)
return ok, res
elseif method == "POST" then
local ok, res = pcall(function() return game:HttpPost(url, body, "application/json") end)
return ok, res
end
return false, "Unsupported method"
end
local function escapeRichText(text)
text = tostring(text or "")
text = text:gsub("&","&"):gsub("<","<"):gsub(">",">")
return text
end
local function trim(text)
return (tostring(text or ""):gsub("^%s*(.-)%s*$", "%1"))
end
local function formatTime(ts)
local ok, r = pcall(function()
return os.date("!%H:%M", math.floor(tonumber(ts) or os.time()))
end)
return ok and r or "--:--"
end
-- GUI
local gui = Instance.new("ScreenGui")
gui.Name = "GlobalChatGui"
gui.ResetOnSpawn = false
gui.IgnoreGuiInset = true
gui.Parent = PlayerGui
local main = Instance.new("Frame")
main.AnchorPoint = Vector2.new(0.5, 0.5)
main.Position = UDim2.new(0.5, 0, 0.5, 0)
main.Size = UDim2.new(0.45, 0, 0.29, 0)
main.BackgroundColor3 = Color3.fromRGB(20, 20, 24)
main.BorderSizePixel = 0
main.Parent = gui
local c = Instance.new("UICorner"); c.CornerRadius = UDim.new(0, 8); c.Parent = main
local s = Instance.new("UIStroke"); s.Thickness = 1; s.Transparency = 0.5
s.Color = Color3.fromRGB(255,255,255); s.Parent = main
local lim = Instance.new("UISizeConstraint")
lim.MinSize = Vector2.new(140, 180); lim.MaxSize = Vector2.new(210, 280); lim.Parent = main
local topBar = Instance.new("Frame")
topBar.Size = UDim2.new(1, 0, 0, 20)
topBar.BackgroundColor3 = Color3.fromRGB(28, 28, 34)
topBar.BorderSizePixel = 0
topBar.Parent = main
local tc = Instance.new("UICorner"); tc.CornerRadius = UDim.new(0, 8); tc.Parent = topBar
local tm = Instance.new("Frame")
tm.Size = UDim2.new(1, 0, 0, 8); tm.Position = UDim2.new(0, 0, 1, -8)
tm.BackgroundColor3 = Color3.fromRGB(28,28,34); tm.BorderSizePixel = 0; tm.Parent = topBar
local titleLbl = Instance.new("TextLabel")
titleLbl.BackgroundTransparency = 1
titleLbl.Size = UDim2.new(1, -60, 1, 0); titleLbl.Position = UDim2.new(0, 7, 0, 0)
titleLbl.Font = Enum.Font.GothamBold; titleLbl.Text = "Global Chat"
titleLbl.TextColor3 = Color3.fromRGB(255,255,255); titleLbl.TextSize = 11
titleLbl.TextXAlignment = Enum.TextXAlignment.Left; titleLbl.Parent = topBar
local onlineLabel = Instance.new("TextLabel")
onlineLabel.BackgroundTransparency = 1
onlineLabel.Size = UDim2.new(0, 55, 1, 0); onlineLabel.Position = UDim2.new(1, -58, 0, 0)
onlineLabel.Font = Enum.Font.Gotham; onlineLabel.Text = "Online: 0"
onlineLabel.TextColor3 = Color3.fromRGB(200,200,200); onlineLabel.TextSize = 10
onlineLabel.TextXAlignment = Enum.TextXAlignment.Right; onlineLabel.Parent = topBar
local statusLabel = Instance.new("TextLabel")
statusLabel.BackgroundTransparency = 1
statusLabel.Size = UDim2.new(1, -14, 0, 12); statusLabel.Position = UDim2.new(0, 7, 0, 23)
statusLabel.Font = Enum.Font.Gotham; statusLabel.Text = "Connecting..."
statusLabel.TextColor3 = Color3.fromRGB(180,180,180); statusLabel.TextSize = 10
statusLabel.TextXAlignment = Enum.TextXAlignment.Left; statusLabel.Parent = main
local messagesFrame = Instance.new("ScrollingFrame")
messagesFrame.Size = UDim2.new(1, -14, 1, -66)
messagesFrame.Position = UDim2.new(0, 7, 0, 38)
messagesFrame.BackgroundColor3 = Color3.fromRGB(16,16,20)
messagesFrame.BorderSizePixel = 0; messagesFrame.ScrollBarThickness = 3
messagesFrame.CanvasSize = UDim2.new(0,0,0,0)
messagesFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
messagesFrame.ScrollingDirection = Enum.ScrollingDirection.Y
messagesFrame.Parent = main
local mc = Instance.new("UICorner"); mc.CornerRadius = UDim.new(0, 7); mc.Parent = messagesFrame
local mp = Instance.new("UIPadding")
mp.PaddingTop = UDim.new(0,6); mp.PaddingBottom = UDim.new(0,6)
mp.PaddingLeft = UDim.new(0,6); mp.PaddingRight = UDim.new(0,6); mp.Parent = messagesFrame
local layout = Instance.new("UIListLayout")
layout.Padding = UDim.new(0, 4); layout.SortOrder = Enum.SortOrder.LayoutOrder
layout.Parent = messagesFrame
local inputFrame = Instance.new("Frame")
inputFrame.Size = UDim2.new(1, -14, 0, 22); inputFrame.Position = UDim2.new(0, 7, 1, -28)
inputFrame.BackgroundTransparency = 1; inputFrame.Parent = main
local textBox = Instance.new("TextBox")
textBox.Size = UDim2.new(1, -52, 1, 0)
textBox.BackgroundColor3 = Color3.fromRGB(28,28,34); textBox.BorderSizePixel = 0
textBox.ClearTextOnFocus = false; textBox.PlaceholderText = "Type a message..."
textBox.Text = ""; textBox.Font = Enum.Font.Gotham; textBox.TextSize = 11
textBox.TextColor3 = Color3.fromRGB(255,255,255)
textBox.PlaceholderColor3 = Color3.fromRGB(150,150,150)
textBox.TextXAlignment = Enum.TextXAlignment.Left; textBox.Parent = inputFrame
local tbc = Instance.new("UICorner"); tbc.CornerRadius = UDim.new(0,6); tbc.Parent = textBox
local tbp = Instance.new("UIPadding")
tbp.PaddingLeft = UDim.new(0,6); tbp.PaddingRight = UDim.new(0,6); tbp.Parent = textBox
local sendButton = Instance.new("TextButton")
sendButton.Size = UDim2.new(0, 46, 1, 0); sendButton.Position = UDim2.new(1, -46, 0, 0)
sendButton.BackgroundColor3 = Color3.fromRGB(70,110,255); sendButton.BorderSizePixel = 0
sendButton.Text = "Send"; sendButton.Font = Enum.Font.GothamBold; sendButton.TextSize = 11
sendButton.TextColor3 = Color3.fromRGB(255,255,255); sendButton.Parent = inputFrame
local sc = Instance.new("UICorner"); sc.CornerRadius = UDim.new(0,6); sc.Parent = sendButton
local messageCount = 0
local function scrollToBottom()
task.defer(function()
messagesFrame.CanvasPosition = Vector2.new(0, math.huge)
end)
end
local function addMessage(entry)
if not entry then return end
messageCount += 1
local username = escapeRichText(entry.username or "Unknown")
local message = escapeRichText(entry.message or "")
local timeStr = formatTime(entry.timestamp)
local row = Instance.new("Frame")
row.BackgroundTransparency = 1
row.Size = UDim2.new(1, 0, 0, 0)
row.AutomaticSize = Enum.AutomaticSize.Y
row.LayoutOrder = messageCount
row.Parent = messagesFrame
local label = Instance.new("TextLabel")
label.BackgroundTransparency = 1
label.Size = UDim2.new(1, 0, 0, 0)
label.AutomaticSize = Enum.AutomaticSize.Y
label.Font = Enum.Font.Gotham; label.TextSize = 11
label.TextWrapped = true; label.RichText = true
label.TextXAlignment = Enum.TextXAlignment.Left
label.TextYAlignment = Enum.TextYAlignment.Top
label.TextColor3 = Color3.fromRGB(240,240,240)
label.Text = string.format(
"[%s] %s: %s",
timeStr, username, message
)
label.Parent = row
scrollToBottom()
end
local function setStatus(text, color)
statusLabel.Text = text
statusLabel.TextColor3 = color or Color3.fromRGB(180,180,180)
end
-- Drag
local dragging, dragInput, dragStart, startPos = false, nil, nil, nil
topBar.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true; dragStart = input.Position; startPos = main.Position
input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then dragging = false end
end)
end
end)
topBar.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch then dragInput = input end
end)
UserInputService.InputChanged:Connect(function(input)
if dragging and input == dragInput then
local delta = input.Position - dragStart
main.Position = UDim2.new(
startPos.X.Scale, startPos.X.Offset + delta.X,
startPos.Y.Scale, startPos.Y.Offset + delta.Y
)
end
end)
local lastId = 0
local sending = false
local alive = true
local function getMessages(afterId, limit)
local url = string.format("%s?after=%d&limit=%d", MESSAGES_URL, afterId, limit)
local ok, res = httpRequest("GET", url)
if not ok then return nil end
return jsonDecodeSafe(res)
end
local function refreshOnline()
local ok, res = httpRequest("GET", ONLINE_URL)
if not ok then return end
local data = jsonDecodeSafe(res)
if data and data.count ~= nil then
onlineLabel.Text = "Online: " .. tostring(data.count)
end
end
local function sendChatMessage()
if sending then return end
local text = trim(textBox.Text)
if text == "" then return end
if #text > MAX_MESSAGE_LEN then text = text:sub(1, MAX_MESSAGE_LEN) end
sending = true; sendButton.Text = "..."; sendButton.Active = false
local ok = httpRequest("POST", SEND_URL, {
username = LocalPlayer.Name,
userId = tostring(LocalPlayer.UserId),
message = text,
})
if ok then textBox.Text = ""; setStatus("Sent", Color3.fromRGB(120,255,160))
else setStatus("Send failed", Color3.fromRGB(255,120,120)) end
sendButton.Text = "Send"; sendButton.Active = true; sending = false
end
sendButton.MouseButton1Click:Connect(sendChatMessage)
textBox.FocusLost:Connect(function(enter) if enter then sendChatMessage() end end)
-- Initial load + join
task.spawn(function()
httpRequest("POST", JOIN_URL, {
username = LocalPlayer.Name,
userId = tostring(LocalPlayer.UserId),
})
refreshOnline()
local data = getMessages(0, 50)
if data and data.messages then
table.sort(data.messages, function(a,b) return (tonumber(a.id) or 0) < (tonumber(b.id) or 0) end)
for _, msg in ipairs(data.messages) do
addMessage(msg)
lastId = math.max(lastId, tonumber(msg.id) or 0)
end
scrollToBottom()
end
setStatus("Connected", Color3.fromRGB(120,255,160))
end)
-- Poll messages
task.spawn(function()
while alive do
task.wait(POLL_INTERVAL)
local data = getMessages(lastId, 50)
if data and data.messages then
for _, msg in ipairs(data.messages) do
local id = tonumber(msg.id) or 0
if id > lastId then addMessage(msg); lastId = id end
end
end
end
end)
-- Refresh online count
task.spawn(function()
while alive do
task.wait(ONLINE_INTERVAL)
refreshOnline()
end
end)
-- Heartbeat — keeps this player in the online list
task.spawn(function()
while alive do
task.wait(HEARTBEAT_INTERVAL)
httpRequest("POST", HEARTBEAT_URL, { userId = tostring(LocalPlayer.UserId) })
end
end)
-- Leave on close
pcall(function()
game:BindToClose(function()
alive = false
httpRequest("POST", LEAVE_URL, {
username = LocalPlayer.Name,
userId = tostring(LocalPlayer.UserId),
})
end)
end)