--[[
	// FileName: Chat.lua
	// Written by: SolarCrane
	// Description: Code for lua side chat on ROBLOX.
]]

--[[ CONSTANTS ]]

-- NOTE: IF YOU WANT TO USE THIS CHAT SCRIPT IN YOUR OWN GAME:
-- 1) COPY THE CONTENTS OF THIS FILE INTO A MODULE
-- 2) CREATE A LOCALSCRIPT AND PARENT IT TO StarterGui
-- 3) IN THE LOCALSCRIPT require() THE CHAT MODULE YOU MADE IN STEP 1
-- 4) CONFIGURE YOUR PLACE ON THE WEBSITE TO USE BUBBLE-CHAT
-- 5) SET THE FOLLOWING TWO VARIABLES TO TRUE
local FORCE_CHAT_GUI = false
local NON_CORESCRIPT_MODE = false
-- 6) (OPTIONAL) PUT THE FOLLOWING LINE IN A SERVER SCRIPT TO MAKE CHAT PERSIST THROUGH RESPAWNING
--  game:GetService('StarterGui').ResetPlayerGuiOnSpawn = false
---------------------------------

local MESSAGES_FADE_OUT_TIME = 30
local MAX_UDIM_SIZE = 2^15 - 1

local PHONE_SCREEN_WIDTH = 640
local TABLET_SCREEN_WIDTH = 1024

local FLOOD_CHECK_MESSAGE_COUNT = 7
local FLOOD_CHECK_MESSAGE_INTERVAL = 15 -- This is in seconds

local VR_CHAT_CLICK_DEBOUNCE = 0.25

local SCROLLBAR_THICKNESS = 7
local CHAT_COLORS =
{
	Color3.new(253/255, 41/255, 67/255), -- BrickColor.new("Bright red").Color,
	Color3.new(1/255, 162/255, 255/255), -- BrickColor.new("Bright blue").Color,
	Color3.new(2/255, 184/255, 87/255), -- BrickColor.new("Earth green").Color,
	BrickColor.new("Bright violet").Color,
	BrickColor.new("Bright orange").Color,
	BrickColor.new("Bright yellow").Color,
	BrickColor.new("Light reddish violet").Color,
	BrickColor.new("Brick yellow").Color,
}

local thisModuleName = "Chat"

local emptySelectionImage = Instance.new("ImageLabel")
emptySelectionImage.ImageTransparency = 1
emptySelectionImage.BackgroundTransparency = 1

--[[ END OF CONSTANTS ]]

--[[ SERVICES ]]
local RunService = game:GetService('RunService')
local CoreGuiService = game:GetService('CoreGui')
local PlayersService = game:GetService('Players')
local DebrisService = game:GetService('Debris')
local GuiService = game:GetService('GuiService')
local InputService = game:GetService('UserInputService')
local StarterGui = game:GetService('StarterGui')
local ContextActionService = game:GetService('ContextActionService')
--[[ END OF SERVICES ]]

--[[ Fast Flags ]]--
local playerDropDownEnabledSuccess, playerDropDownEnabledFlagValue = pcall(function() return settings():GetFFlag("PlayerDropDownEnabled") end)
local IsPlayerDropDownEnabled = playerDropDownEnabledSuccess and playerDropDownEnabledFlagValue

local getMoveChatSuccess, moveChatActiveValue = pcall(function() return settings():GetFFlag("SetCoreMoveChat") end)
local allowMoveChat = getMoveChatSuccess and moveChatActiveValue

local getDisableChatBarSuccess, disableChatBarValue = pcall(function() return settings():GetFFlag("SetCoreDisableChatBar") end)
local allowDisableChatBar = getDisableChatBarSuccess and disableChatBarValue

--[[ SCRIPT VARIABLES ]]
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local VRHub = require(RobloxGui.Modules.VR.VRHub)
local PlayerPermissionsModule = require(RobloxGui.Modules.PlayerPermissionsModule)

-- I am not fond of waiting at the top of the script here...
while PlayersService.LocalPlayer == nil do PlayersService.ChildAdded:wait() end
local Player = PlayersService.LocalPlayer
-- GuiRoot will act as the top-node for parenting GUIs
local GuiRoot = Instance.new('Frame')
GuiRoot.Name = 'GuiRoot';
GuiRoot.Size = UDim2.new(1,0,1,0);
GuiRoot.BackgroundTransparency = 1;



local chatRepositioned = false
local chatBarDisabled = false

local lastSelectedPlayer = nil
local lastSelectedButton = nil

local playerDropDownModule = nil
local playerDropDown = nil
local blockingUtility = nil

local topbarEnabled = true



if not NON_CORESCRIPT_MODE and not InputService.VREnabled then
	playerDropDownModule = require(RobloxGui.Modules:WaitForChild("PlayerDropDown"))
	playerDropDown = playerDropDownModule:CreatePlayerDropDown()
	blockingUtility = playerDropDownModule:CreateBlockingUtility()
end

--[[ END OF SCRIPT VARIABLES ]]

local function GetLuaChatFilteringFlag()
	local flagSuccess, flagValue = pcall(function() return settings():GetFFlag("LuaChatFiltering") end)
	return flagSuccess and flagValue == true
end

local Util = {}
do
	-- Check if we are running on a touch device
	function Util.IsTouchDevice()
		local touchEnabled = false
		pcall(function() touchEnabled = InputService.TouchEnabled end)
		return touchEnabled
	end

	function Util.IsSmallScreenSize()
		return GuiRoot.AbsoluteSize.X <= PHONE_SCREEN_WIDTH
	end

	function Util.Create(instanceType)
		return function(data)
			local obj = Instance.new(instanceType)
			for k, v in pairs(data) do
				if type(k) == 'number' then
					v.Parent = obj
				else
					obj[k] = v
				end
			end
			return obj
		end
	end

	function Util.Clamp(low, high, input)
		return math.max(low, math.min(high, input))
	end

	function Util.Linear(t, b, c, d)
		if t >= d then return b + c end

		return c*t/d + b
	end

	function Util.EaseOutQuad(t, b, c, d)
		if t >= d then return b + c end

		t = t/d;
		return -c * t*(t-2) + b
	end

	function Util.EaseInOutQuad(t, b, c, d)
		if t >= d then return b + c end

		t = t / (d/2);
		if (t < 1) then return c/2*t*t + b end;
		t = t - 1;
		return -c/2 * (t*(t-2) - 1) + b;
	end

	function Util.PropertyTweener(instance, prop, start, final, duration, easingFunc, cbFunc)
		local this = {}
		this.StartTime = tick()
		this.EndTime = this.StartTime + duration
		this.Cancelled = false

		local finished = false
		local percentComplete = 0
		spawn(function()
			local now = tick()
			while now < this.EndTime and instance do
				if this.Cancelled then
					return
				end
				instance[prop] = easingFunc(now - this.StartTime, start, final - start, duration)
				percentComplete = Util.Clamp(0, 1, (now - this.StartTime) / duration)
				RunService.RenderStepped:wait()
				now = tick()
			end
			if this.Cancelled == false and instance then
				instance[prop] = final
				finished = true
				percentComplete = 1
				if cbFunc then
					cbFunc()
				end
			end
		end)

		function this:GetPercentComplete()
			return percentComplete
		end

		function this:IsFinished()
			return finished
		end

		function this:Cancel()
			this.Cancelled = true
		end

		return this
	end

	function Util.Signal()
		local sig = {}

		local mSignaler = Instance.new('BindableEvent')

		local mArgData = nil
		local mArgDataCount = nil

		function sig:fire(...)
			mArgData = {...}
			mArgDataCount = select('#', ...)
			mSignaler:Fire()
		end

		function sig:connect(f)
			if not f then error("connect(nil)", 2) end
			return mSignaler.Event:connect(function()
				f(unpack(mArgData, 1, mArgDataCount))
			end)
		end

		function sig:wait()
			mSignaler.Event:wait()
			assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.")
			return unpack(mArgData, 1, mArgDataCount)
		end

		return sig
	end

	function Util.DisconnectEvent(conn)
		if conn then
			conn:disconnect()
		end
		return nil
	end

	function Util.SetGUIInsetBounds(x, y)
		local success, _ = pcall(function() GuiService:SetGlobalGuiInset(0, x, 0, y) end)
		if not success then
			pcall(function() GuiService:SetGlobalSizeOffsetPixel(-x, -y) end) -- Legacy GUI-offset function
		end
	end

	local baseUrl = game:GetService("ContentProvider").BaseUrl:lower()
	baseUrl = string.gsub(baseUrl,"/m.","/www.") --mobile site does not work for this stuff!
	function Util.GetSecureApiBaseUrl()
		local secureApiUrl = baseUrl
		secureApiUrl = string.gsub(secureApiUrl,"http://","https://")
		secureApiUrl = string.gsub(secureApiUrl,"www","api")
		return secureApiUrl
	end

	function Util.GetPlayerByName(playerName)
		-- O(n), may be faster if I store a reverse hash from the players list; can't trust FindFirstChild in PlayersService because anything can be parented to there.
		local lowerName = string.lower(playerName)
		for _, player in pairs(PlayersService:GetPlayers()) do
			if string.lower(player.Name) == lowerName then
				return player
			end
		end
		return nil -- Found no player
	end

	local function MakeIsInGroup(groupId, requiredRank)
		assert(type(requiredRank) == "nil" or type(requiredRank) == "number", "requiredRank must be a number or nil")
		
		local inGroupCache = {}
		return function(player)
			if player and player.userId then
				local userId = player.userId

				if inGroupCache[userId] == nil then
					local inGroup = false
					pcall(function() -- Many things can error is the IsInGroup check
						if requiredRank then
							inGroup = player:GetRankInGroup(groupId) > requiredRank
						else
							inGroup = player:IsInGroup(groupId)
						end
					end)
					inGroupCache[userId] = inGroup
				end

				return inGroupCache[userId]
			end

			return false
		end
	end
	Util.IsPlayerAdminAsync = MakeIsInGroup(1200769)
	Util.IsPlayerInternAsync = MakeIsInGroup(2868472, 100)

	local function GetNameValue(pName)
		local value = 0
		for index = 1, #pName do
			local cValue = string.byte(string.sub(pName, index, index))
			local reverseIndex = #pName - index + 1
			if #pName%2 == 1 then
				reverseIndex = reverseIndex - 1
			end
			if reverseIndex%4 >= 2 then
				cValue = -cValue
			end
			value = value + cValue
		end
		return value
	end

	function Util.ComputeChatColor(pName)
		return CHAT_COLORS[(GetNameValue(pName) % #CHAT_COLORS) + 1]
	end

	-- This is a memo-izing function
	local testLabel = Instance.new('TextLabel')
	testLabel.TextWrapped = true;
	testLabel.Position = UDim2.new(1,0,1,0)
	testLabel.Parent = GuiRoot -- Note: We have to parent it to check TextBounds
	-- The TextSizeCache table looks like this Text->Font->sizeBounds->FontSize
	local TextSizeCache = {}
	function Util.GetStringTextBounds(text, font, fontSize, sizeBounds)
		-- If no sizeBounds are specified use some huge number
		sizeBounds = sizeBounds or false
		if not TextSizeCache[text] then
			TextSizeCache[text] = {}
		end
		if not TextSizeCache[text][font] then
			TextSizeCache[text][font] = {}
		end
		if not TextSizeCache[text][font][sizeBounds] then
			TextSizeCache[text][font][sizeBounds] = {}
		end
		if not TextSizeCache[text][font][sizeBounds][fontSize] then
			testLabel.Text = text
			testLabel.Font = font
			testLabel.FontSize = fontSize
			if sizeBounds then
				testLabel.TextWrapped = true;
				testLabel.Size = sizeBounds
			else
				testLabel.TextWrapped = false;
			end
			TextSizeCache[text][font][sizeBounds][fontSize] = testLabel.TextBounds
		end
		return TextSizeCache[text][font][sizeBounds][fontSize]
	end

	local PRINTABLE_CHARS = '[^' .. string.char(32) .. '-' ..  string.char(126) .. ']'
	local WHITESPACE_CHARS = '(' .. string.rep('%s', 7) .. ')%s+'
	function Util.FilterUnprintableCharacters(str)
		if not GetLuaChatFilteringFlag() then
			return str
		end

		local result = str:gsub(PRINTABLE_CHARS, '');
		result = str:gsub(WHITESPACE_CHARS, '%1');
		return result
	end
end

local SelectChatModeEvent = Util.Signal()
local SelectPlayerEvent = Util.Signal()

local function CreateChatMessage()
	local this = {}
	this.FadeRoutines = {}

	function this:GetMessageFontSize(settings)
		return Util.IsSmallScreenSize() and settings.SmallScreenFontSize or settings.FontSize
	end

	function this:OnResize()
		-- Nothing!
	end

	function this:FadeIn()
		local gui = this:GetGui()
		if gui then
			gui.Visible = true
		end
	end

	function this:FadeOut()
		local gui = this:GetGui()
		if gui then
			gui.Visible = false
		end
	end

	function this:GetGui()
		return this.Container
	end

	function this:Destroy()
		if this.Container ~= nil then
			this.Container:Destroy()
			this.Container = nil
		end
		if this.FadeRoutines then
			for _, routine in pairs(this.FadeRoutines) do
				routine:Cancel()
			end
			this.FadeRoutines = {}
		end
	end

	return this
end

local function CreateSystemChatMessage(settings, chattedMessage)
	local this = CreateChatMessage()

	this.Settings = settings
	this.rawChatString = chattedMessage

	function this:OnResize(containerSize)
		if this.Container and this.ChatMessage then

			if InputService.VREnabled then
				this.ChatMessage.Position = UDim2.new(0, 4, 0, 0)
				this.ChatMessage.Size = UDim2.new(1, 0, 1, 0)
			end

			this.Container.Size = UDim2.new(1,0,0,1000)
			local textHeight = this.ChatMessage.TextBounds.Y
		
			local newContainerHeight = textHeight + 5
			this.Container.Size = UDim2.new(1,0,0,newContainerHeight)
			return newContainerHeight
		end
	end

	function this:FadeIn()
		local gui = this:GetGui()
		if gui then
			gui.Visible = true
			for _, routine in pairs(this.FadeRoutines) do
				routine:Cancel()
			end
			this.FadeRoutines = {}
			local tweenableObjects = {
				this.ChatMessage;
			}
			for _, object in pairs(tweenableObjects) do
				object.TextTransparency = 0;
				object.TextStrokeTransparency = this.Settings.TextStrokeTransparency;
			end

			if this.MessageBackgroundImage then
				this.MessageBackgroundImage.Visible = InputService.VREnabled
			end
		end
	end

	function this:FadeOut(instant)
		local gui = this:GetGui()
		if gui then
			if instant then
				gui.Visible = false
			else
				local tweenableObjects = {
					this.ChatMessage;
				}
				for _, object in pairs(tweenableObjects) do
					table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextTransparency', object.TextTransparency, 1, 1, Util.Linear))
					table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextStrokeTransparency', object.TextStrokeTransparency, 1, 0.85, Util.Linear))
				end
			end
			if this.MessageBackgroundImage then
				this.MessageBackgroundImage.Visible = false
			end
		end
	end

	local function CreateMessageGuiElement()
		local fontSize = this:GetMessageFontSize(this.Settings)

		local systemMessageDisplayText = this.rawChatString or ""
		local systemMessageSize = Util.GetStringTextBounds(systemMessageDisplayText, this.Settings.Font, fontSize, UDim2.new(0, 400, 0, 1000))

		local container = Util.Create'Frame'
		{
			Name = 'MessageContainer';
			Position = UDim2.new(0, 0, 0, 0);
			ZIndex = 1;
			BackgroundColor3 = Color3.new(0, 0, 0);
			BackgroundTransparency = 1;
		};
			this.MessageBackgroundImage = Util.Create'ImageLabel'
			{
				Name = 'TextEntryBackground';
				Size = UDim2.new(1,0,1,-2);
				Position = UDim2.new(0,0,0,1);
				Image = 'rbxasset://textures/ui/Chat/VRChatBackground.png';
				ScaleType = Enum.ScaleType.Slice;
				SliceCenter = Rect.new(8,8,56,56);
				BackgroundTransparency = 1;
				ImageTransparency = 0.3;
				BorderSizePixel = 0;
				ZIndex = 1;
				Visible = InputService.VREnabled;
				Parent = container;
			}

			local chatMessage = Util.Create'TextLabel'
			{
				Name = 'SystemChatMessage';
				Position = UDim2.new(0, 0, 0, 0);
				Size = UDim2.new(1, 0, 1, 0);
				Text = systemMessageDisplayText;
				ZIndex = 1;
				BackgroundColor3 = Color3.new(0, 0, 0);
				BackgroundTransparency = 1;
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				TextWrapped = true;
				TextColor3 = this.Settings.DefaultMessageTextColor;
				FontSize = fontSize;
				Font = this.Settings.Font;
				TextStrokeColor3 = this.Settings.TextStrokeColor;
				TextStrokeTransparency = this.Settings.TextStrokeTransparency;
				Parent = container;
			};
			if InputService.VREnabled then
				chatMessage.Position = UDim2.new(0, 4, 0, 0)
				chatMessage.Size = UDim2.new(1, 0, 1, 0)
			end

		container.Size = UDim2.new(1, 0, 0, systemMessageSize.Y + 1);
		this.Container = container
		this.ChatMessage = chatMessage
	end

	CreateMessageGuiElement()

	return this
end

--[[ Popup Handling ]]--
function createPopupFrame(selectedPlayer, selectedButton)
	if selectedPlayer and selectedPlayer.Parent == PlayersService then
		if lastSelectedButton ~= selectedButton then
			if lastSelectedButton ~= nil then
				lastSelectedButton.BackgroundTransparency = 1
				lastSelectedButton = nil
			end
			lastSelectedButton = selectedButton
			lastSelectedPlayer = selectedPlayer
			selectedButton.BackgroundTransparency = 0.5

			if IsPlayerDropDownEnabled then
				playerDropDown.HidePopupImmediately = true
				local PopupFrame = playerDropDown:CreatePopup(selectedPlayer)
				PopupFrame.Position = UDim2.new(0, selectedButton.AbsolutePosition.X + selectedButton.AbsoluteSize.X + 2, 0, selectedButton.AbsolutePosition.Y)
				PopupFrame.Size = UDim2.new(0, 150, PopupFrame.Size.Y.Scale, PopupFrame.Size.Y.Offset)
				PopupFrame.ZIndex = 5
				PopupFrame.Parent = GuiRoot
			end

			for _, button in pairs(PopupFrame:GetChildren()) do
				button.BackgroundTransparency = 0
				button.ZIndex = 6
			end
		else
			if IsPlayerDropDownEnabled then
				playerDropDown:Hide()
			end
			lastSelectedPlayer = nil
		end
	end
end

function popupHidden()
	if lastSelectedButton then
		lastSelectedPlayer = nil
		lastSelectedButton.BackgroundTransparency = 1
		lastSelectedButton = nil
	end
end

if IsPlayerDropDownEnabled and playerDropDown then
	playerDropDown.HiddenSignal:connect(popupHidden)
end

InputService.InputBegan:connect(function(inputObject, isProcessed)
	if isProcessed then return end
	local inputType = inputObject.UserInputType
	if (inputType == Enum.UserInputType.Touch and  inputObject.UserInputState == Enum.UserInputState.Begin) or
		inputType == Enum.UserInputType.MouseButton1 then
		if lastSelectedButton and IsPlayerDropDownEnabled then
			playerDropDown:Hide()
		end
	end
end)

--[[ End of popup handling ]]--

local function CreatePlayerChatMessage(settings, playerChatType, sendingPlayer, chattedMessage, receivingPlayer)
	local this = CreateChatMessage()

	this.Settings = settings
	this.PlayerChatType = playerChatType
	this.SendingPlayer = sendingPlayer
	this.RawMessageContent = chattedMessage
	this.ReceivingPlayer = receivingPlayer
	this.ReceivedTime = tick()

	this.Neutral = this.SendingPlayer and this.SendingPlayer.Neutral or true
	this.TeamColor = this.SendingPlayer and this.SendingPlayer.TeamColor or BrickColor.new("White")

	function this:OnResize(containerSize)
		if this.Container and this.ChatMessage then
			this.Container.Size = UDim2.new(1,0,0,1000)
			local textHeight = this.ChatMessage.TextBounds.Y
			local newContainerHeight = textHeight + 5
			this.Container.Size = UDim2.new(1,0,0,newContainerHeight)
			return newContainerHeight
		end
	end

	function this:FormatMessage()
		local result = ""
		if this.RawMessageContent then
			local message = this.RawMessageContent
			result = message
		end
		return result
	end

	function this:FormatChatType()
		if this.PlayerChatType then
			if this.PlayerChatType == Enum.PlayerChatType.All then
				--return "[All]"
			elseif this.PlayerChatType == Enum.PlayerChatType.Team then
				return "[Team]"
			elseif this.PlayerChatType == Enum.PlayerChatType.Whisper then
				-- nothing!
			end
		end
	end

	function this:FormatPlayerNameText()
		local playerName = ""
		-- If we are sending a whisper to someone, then we should show their name
		if this.PlayerChatType == Enum.PlayerChatType.Whisper and this.SendingPlayer and this.SendingPlayer == Player then
			playerName = (this.ReceivingPlayer and this.ReceivingPlayer.Name or "")
		else
			playerName = (this.SendingPlayer and this.SendingPlayer.Name or "")
		end
		return "[" ..  playerName .. "]:"
	end

	function this:FadeIn()
		local gui = this:GetGui()
		if gui then
			gui.Visible = true
			for _, routine in pairs(this.FadeRoutines) do
				routine:Cancel()
			end
			this.FadeRoutines = {}
			local tweenableObjects = {
					this.WhisperToText;
					this.WhisperFromText;
					this.ChatModeButton;
					this.UserNameButton;
					this.ChatMessage;
				}
			for _, object in pairs(tweenableObjects) do
				object.TextTransparency = 0;
				object.TextStrokeTransparency = this.Settings.TextStrokeTransparency;
				object.Active = true
			end
			if this.UserNameDot then
				this.UserNameDot.ImageTransparency = 0
			end

			if this.MessageBackgroundImage then
				this.MessageBackgroundImage.Visible = InputService.VREnabled
			end
		end
	end

	function this:FadeOut(instant)
		local gui = this:GetGui()
		if gui then
			if instant then
				gui.Visible = false
			else
				local tweenableObjects = {
					this.WhisperToText;
					this.WhisperFromText;
					this.ChatModeButton;
					this.UserNameButton;
					this.ChatMessage;
				}
				for _, object in pairs(tweenableObjects) do
					table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextTransparency', object.TextTransparency, 1, 1, Util.Linear))
					table.insert(this.FadeRoutines, Util.PropertyTweener(object, 'TextStrokeTransparency', object.TextStrokeTransparency, 1, 0.85, Util.Linear))
					object.Active = false
				end
				if this.UserNameDot then
					table.insert(this.FadeRoutines, Util.PropertyTweener(this.UserNameDot, 'ImageTransparency', this.UserNameDot.ImageTransparency, 1, 1, Util.Linear))
				end
			end
			if this.MessageBackgroundImage then
				this.MessageBackgroundImage.Visible = false
			end
		end
	end

	function this:Destroy()
		if this.Container ~= nil then
			this.Container:Destroy()
			this.Container = nil
		end
		this.ClickedOnModeConn = Util.DisconnectEvent(this.ClickedOnModeConn)
		this.ClickedOnPlayerConn = Util.DisconnectEvent(this.ClickedOnPlayerConn)
		this.RightClickedOnPlayerConn = Util.DisconnectEvent(this.RightClickedOnPlayerConn)
	end

	local function CreateMessageGuiElement()
		local fontSize = this:GetMessageFontSize(this.Settings)

		local toMesasgeDisplayText = "To "
		local toMessageSize = Util.GetStringTextBounds(toMesasgeDisplayText, this.Settings.Font, fontSize)
		local fromMesasgeDisplayText = "From "
		local fromMessageSize = Util.GetStringTextBounds(fromMesasgeDisplayText, this.Settings.Font, fontSize)
		local chatTypeDisplayText = this:FormatChatType()
		local chatTypeSize = chatTypeDisplayText and Util.GetStringTextBounds(chatTypeDisplayText, this.Settings.Font, fontSize) or Vector2.new(0,0)
		local playerNameDisplayText = this:FormatPlayerNameText()
		local playerNameSize = Util.GetStringTextBounds(playerNameDisplayText, this.Settings.Font, fontSize)

		local singleSpaceSize = Util.GetStringTextBounds(" ", this.Settings.Font, fontSize)
		local numNeededSpaces = math.ceil(playerNameSize.X / singleSpaceSize.X) + 1
		local chatMessageDisplayText = string.rep(" ", numNeededSpaces) .. this:FormatMessage()
		local chatMessageSize = Util.GetStringTextBounds(chatMessageDisplayText, this.Settings.Font, fontSize, UDim2.new(0, 400 - 5 - playerNameSize.X, 0, 1000))


		local playerColor = this.Settings.DefaultMessageTextColor
		if this.SendingPlayer then
			if this.PlayerChatType == Enum.PlayerChatType.Whisper then
				if this.SendingPlayer == Player and this.ReceivingPlayer then
					playerColor = Util.ComputeChatColor(this.ReceivingPlayer.Name)
				else
					playerColor = Util.ComputeChatColor(this.SendingPlayer.Name)
				end
			else
				if this.SendingPlayer.Neutral then
					playerColor = Util.ComputeChatColor(this.SendingPlayer.Name)
				else
					playerColor = this.SendingPlayer.TeamColor.Color
				end
			end
		end

		local container = Util.Create'Frame'
		{
			Name = 'MessageContainer';
			Position = UDim2.new(0, 0, 0, 0);
			ZIndex = 1;
			BackgroundColor3 = Color3.new(0, 0, 0);
			BackgroundTransparency = 1;
		};
			this.MessageBackgroundImage = Util.Create'ImageLabel'
			{
				Name = 'TextEntryBackground';
				Size = UDim2.new(1,0,1,-2);
				Position = UDim2.new(0,0,0,1);
				Image = 'rbxasset://textures/ui/Chat/VRChatBackground.png';
				ScaleType = Enum.ScaleType.Slice;
				SliceCenter = Rect.new(8,8,56,56);
				BackgroundTransparency = 1;
				ImageTransparency = 0.3;
				BorderSizePixel = 0;
				ZIndex = 1;
				Visible = InputService.VREnabled;
				Parent = container;
			}

			local xOffset = InputService.VREnabled and 4 or 0

			if this.SendingPlayer and this.SendingPlayer == Player and this.PlayerChatType == Enum.PlayerChatType.Whisper then
				local whisperToText = Util.Create'TextLabel'
				{
					Name = 'WhisperTo';
					Position = UDim2.new(0, 0, 0, 0);
					Size = UDim2.new(0, toMessageSize.X, 0, toMessageSize.Y);
					Text = toMesasgeDisplayText;
					ZIndex = 1;
					BackgroundColor3 = Color3.new(0, 0, 0);
					BackgroundTransparency = 1;
					TextXAlignment = Enum.TextXAlignment.Left;
					TextYAlignment = Enum.TextYAlignment.Top;
					TextWrapped = true;
					TextColor3 = this.Settings.DefaultMessageTextColor;
					FontSize = fontSize;
					Font = this.Settings.Font;
					TextStrokeColor3 = this.Settings.TextStrokeColor;
					TextStrokeTransparency = this.Settings.TextStrokeTransparency;
					Parent = container;
				};
				xOffset = xOffset + toMessageSize.X
				this.WhisperToText = whisperToText
			elseif this.SendingPlayer and this.SendingPlayer ~= Player and this.PlayerChatType == Enum.PlayerChatType.Whisper then
				local whisperFromText = Util.Create'TextLabel'
				{
					Name = 'WhisperFromText';
					Position = UDim2.new(0, 0, 0, 0);
					Size = UDim2.new(0, fromMessageSize.X, 0, fromMessageSize.Y);
					Text = fromMesasgeDisplayText;
					ZIndex = 1;
					BackgroundColor3 = Color3.new(0, 0, 0);
					BackgroundTransparency = 1;
					TextXAlignment = Enum.TextXAlignment.Left;
					TextYAlignment = Enum.TextYAlignment.Top;
					TextWrapped = true;
					TextColor3 = this.Settings.DefaultMessageTextColor;
					FontSize = fontSize;
					Font = this.Settings.Font;
					TextStrokeColor3 = this.Settings.TextStrokeColor;
					TextStrokeTransparency = this.Settings.TextStrokeTransparency;
					Parent = container;
				};
				xOffset = xOffset + fromMessageSize.X
				this.WhisperFromText = whisperFromText
			end
		if chatTypeDisplayText then
			local chatModeButton = Util.Create(Util.IsTouchDevice() and 'TextLabel' or 'TextButton')
			{
				Name = 'ChatMode';
				BackgroundTransparency = 1;
				ZIndex = 2;
				Text = chatTypeDisplayText;
				TextColor3 = this.Settings.DefaultMessageTextColor;
				Position = UDim2.new(0, xOffset, 0, 0);
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				FontSize = fontSize;
				Font = this.Settings.Font;
				Size = UDim2.new(0, chatTypeSize.X, 0, chatTypeSize.Y);
				TextStrokeColor3 = this.Settings.TextStrokeColor;
				TextStrokeTransparency = this.Settings.TextStrokeTransparency;
				Parent = container
			}
			if chatModeButton:IsA('TextButton') then
				this.ClickedOnModeConn = chatModeButton.MouseButton1Click:connect(function()
					SelectChatModeEvent:fire(this.PlayerChatType)
				end)
			end
			if this.PlayerChatType == Enum.PlayerChatType.Team then
				chatModeButton.TextColor3 = playerColor
			end
			xOffset = xOffset + chatTypeSize.X + 1
			this.ChatModeButton = chatModeButton
		end
			local userNameButton = Util.Create(Util.IsTouchDevice() and 'TextLabel' or 'TextButton')
			{
				Name = 'PlayerName';
				BackgroundTransparency = 1;
				BackgroundColor3 = Color3.new(0, 1, 1);
				BorderSizePixel = 0;
				ZIndex = 2;
				Text = playerNameDisplayText;
				TextColor3 = playerColor;
				Position = UDim2.new(0, xOffset, 0, 0);
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				FontSize = fontSize;
				Font = this.Settings.Font;
				Size = UDim2.new(0, playerNameSize.X, 0, playerNameSize.Y);
				TextStrokeColor3 = this.Settings.TextStrokeColor;
				TextStrokeTransparency = this.Settings.TextStrokeTransparency;
				Parent = container
			}
			if userNameButton:IsA('TextButton') then
				this.ClickedOnPlayerConn = userNameButton.MouseButton1Click:connect(function()
					local gui = this:GetGui()
					if gui and gui.Visible then
						if this.PlayerChatType == Enum.PlayerChatType.Whisper and this.SendingPlayer == Player and this.ReceivingPlayer then
							SelectPlayerEvent:fire(this.ReceivingPlayer)
						else
							SelectPlayerEvent:fire(this.SendingPlayer)
						end
					end
				end)
				this.RightClickedOnPlayerConn = userNameButton.MouseButton2Click:connect(function()
					local gui = this:GetGui()
					if gui and gui.Visible then
						if IsPlayerDropDownEnabled and playerDropDown then
							if this.SendingPlayer and this.SendingPlayer ~= Player then
								createPopupFrame(this.SendingPlayer, userNameButton)
							end
						end
					end
				end)
			end

			local chatMessage = Util.Create'TextLabel'
			{
				Name = 'ChatMessage';
				Position = UDim2.new(0, xOffset, 0, 0);
				Size = UDim2.new(1, -xOffset, 1, 0);
				Text = chatMessageDisplayText;
				ZIndex = 1;
				BackgroundColor3 = Color3.new(0, 0, 0);
				BackgroundTransparency = 1;
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				TextWrapped = true;
				TextColor3 = this.Settings.DefaultMessageTextColor;
				FontSize = fontSize;
				Font = this.Settings.Font;
				TextStrokeColor3 = this.Settings.TextStrokeColor;
				TextStrokeTransparency = this.Settings.TextStrokeTransparency;
				Parent = container;
			};
			if InputService.VREnabled then
				chatMessage.Size = chatMessage.Size - UDim2.new(0,4,0,0)
			end
			-- Check if they got moderated and put up a real message instead of Label
			if chatMessage.Text == 'Label' and chatMessageDisplayText ~= 'Label' then
				chatMessage.Text = string.rep(" ", numNeededSpaces) .. '[Content Deleted]'
			end
			if this.SendingPlayer then
				if PlayerPermissionsModule.IsPlayerAdminAsync(this.SendingPlayer) then
					chatMessage.TextColor3 = this.Settings.AdminTextColor
				elseif PlayerPermissionsModule.IsPlayerInternAsync(this.SendingPlayer) then
					chatMessage.TextColor3 = this.Settings.InternTextColor
				end
			end
			chatMessage.Size = chatMessage.Size + UDim2.new(0, 0, 0, chatMessage.TextBounds.Y);

		container.Size = UDim2.new(1, 0, 0, math.max(chatMessageSize.Y + 1, userNameButton.Size.Y.Offset + 1));
		this.Container = container
		this.ChatMessage = chatMessage
		this.UserNameButton = userNameButton
	end

	CreateMessageGuiElement()

	return this
end

local function CreateChatBarWidget(settings)
	local this = {}

	-- MessageModes: {All, Team, Whisper}
	this.MessageMode = 'All'
	this.TargetWhisperPlayer = nil
	this.Settings = settings

	this.WidgetVisible = false
	this.FadedIn = true

	this.ChatBarGainedFocusEvent = Util.Signal()
	this.ChatBarLostFocusEvent = Util.Signal()
	this.ChatCommandEvent = Util.Signal() -- Signal Signatue: success, actionType, [captures]
	this.ChatErrorEvent = Util.Signal() -- Signal Signatue: success, actionType, [captures]
	this.ChatBarFloodEvent = Util.Signal()

	this.unfocusedAt = 0

	local chatCoreGuiEnabled = true

	-- This function while lets string.find work case-insensitively without clobbering the case of the captures
	local function nocase(s)
      s = string.gsub(s, "%a", function (c)
            return string.format("[%s%s]", string.lower(c),
                                           string.upper(c))
          end)
      return s
    end

	this.ChatMatchingRegex =
	{
		[function(chatBarText) return string.find(chatBarText, nocase("^/w ") .. "(%w+_?%w+)") end] = "Whisper";
		[function(chatBarText) return string.find(chatBarText, nocase("^/whisper ") .. "(%w+_?%w+)") end] = "Whisper";

		[function(chatBarText) return string.find(chatBarText, "^%%") end] = "Team";
		[function(chatBarText) return string.find(chatBarText, "^%(TEAM%)") end] = "Team";
		[function(chatBarText) return string.find(chatBarText, nocase("^/t")) end] = "Team";
		[function(chatBarText) return string.find(chatBarText, nocase("^/team")) end] = "Team";

		[function(chatBarText) return string.find(chatBarText, nocase("^/a")) end] = "All";
		[function(chatBarText) return string.find(chatBarText, nocase("^/all")) end] = "All";
		[function(chatBarText) return string.find(chatBarText, nocase("^/s")) end] = "All";
		[function(chatBarText) return string.find(chatBarText, nocase("^/say")) end] = "All";

		[function(chatBarText) return string.find(chatBarText, nocase("^/e")) end] = "Emote";
		[function(chatBarText) return string.find(chatBarText, nocase("^/emote")) end] = "Emote";

		[function(chatBarText) return string.find(chatBarText, "^/%?") end] = "Help";
		[function(chatBarText) return string.find(chatBarText, nocase("^/help")) end] = "Help";

		[function(chatBarText) return string.find(chatBarText, nocase("^/block ") .. "(%w+_?%w+)") end] = "Block";

		[function(chatBarText) return string.find(chatBarText, nocase("^/unblock ") .. "(%w+_?%w+)") end] = "Unblock";

		[function(chatBarText) return string.find(chatBarText, nocase("^/mute ") .. "(%w+_?%w+)") end] = "Mute";

		[function(chatBarText) return string.find(chatBarText, nocase("^/unmute ") .. "(%w+_?%w+)") end] = "Unmute";
	}

	local ChatModesDict =
	{
		['Whisper'] = 'Whisper';
		['Team'] = 'Team';
		['All'] = 'All';
		[Enum.PlayerChatType.Whisper] = 'Whisper';
		[Enum.PlayerChatType.Team] = 'Team';
		[Enum.PlayerChatType.All] = 'All';
	}

	local function TearDownEvents()
		this.ClickToChatButtonConn = Util.DisconnectEvent(this.ClickToChatButtonConn)
		this.ChatBarFocusLostConn = Util.DisconnectEvent(this.ChatBarFocusLostConn)
		this.ChatBarLostFocusConn = Util.DisconnectEvent(this.ChatBarLostFocusConn)
		this.SelectChatModeConn = Util.DisconnectEvent(this.SelectChatModeConn)
		this.SelectPlayerConn = Util.DisconnectEvent(this.SelectPlayerConn)
		this.FocusChatBarInputBeganConn = Util.DisconnectEvent(this.FocusChatBarInputBeganConn)
		this.InputBeganConn = Util.DisconnectEvent(this.InputBeganConn)
		this.ChatBarChangedConn = Util.DisconnectEvent(this.ChatBarChangedConn)
	end

	local function HookUpEvents()
		TearDownEvents() -- Cleanup old events

		if this.ClickToChatButton then this.ClickToChatButtonConn = this.ClickToChatButton.MouseButton1Click:connect(function() this:FocusChatBar() end) end

		if this.ChatBar then
			-- Use a count to check for double backspace out of a chatmode
			local count = 0
			if not Util.IsTouchDevice() then
				this.FocusChatBarInputBeganConn = Util.DisconnectEvent(this.FocusChatBarInputBeganConn)
				this.FocusChatBarInputBeganConn = InputService.InputBegan:connect(function(inputObj)
					if inputObj.KeyCode == Enum.KeyCode.Backspace and this:GetChatBarText() == "" then
						if count == 0 then
							count = count + 1
						else
							this:SetMessageMode('All')
						end
					else
						count = 0
					end
				end)
			end

			this.ChatBarFocusLostConn = this.ChatBar.FocusLost:connect(function(...)
				count = 0
				this.unfocusedAt = tick()
				this.ChatBarLostFocusEvent:fire(...)
			end)
			this.ChatBarChangedConn = this.ChatBar.Changed:connect(function(prop)
				if prop == "Text" then
					this:OnChatBarTextChanged()
				elseif prop == 'TextFits' or prop == 'TextBounds' or prop == 'Visible' then
					this:OnChatBarBoundsChanged()
				end
			end)
		end

		if this.ChatBarLostFocusEvent then this.ChatBarLostFocusConn = this.ChatBarLostFocusEvent:connect(function(...) this:OnChatBarFocusLost(...) end) end

		this.SelectChatModeConn = SelectChatModeEvent:connect(function(chatType)
			this:SetMessageMode(chatType)
			this:FocusChatBar()
		end)

		this.SelectPlayerConn = SelectPlayerEvent:connect(function(chatPlayer)
			this.TargetWhisperPlayer = chatPlayer
			this:SetMessageMode("Whisper")
			this:FocusChatBar()
		end)

		this.InputBeganConn = InputService.InputBegan:connect(function(inputObject)
			if inputObject.KeyCode == Enum.KeyCode.Escape then
				-- Clear text when they press escape
				this:SetChatBarText("")
			end
		end)
	end

	function this:CalculateVisibility()
		if this.ChatBarContainer then
			local enabled = self.WidgetVisible and chatCoreGuiEnabled and not NON_CORESCRIPT_MODE
			if enabled then
				HookUpEvents()
			else
				TearDownEvents()
			end
			this.ChatBarContainer.Visible = enabled and self.FadedIn and (not chatBarDisabled)
		end
	end

	function this:ToggleVisibility(visible)
		if visible ~= self.WidgetVisible then
			self.WidgetVisible = visible
			self:CalculateVisibility()
		end
		if NON_CORESCRIPT_MODE or chatBarDisabled then
			this.ChatBarContainer.Visible = false
		end
	end

	function this:FadeIn()
		self.FadedIn = true
		self:CalculateVisibility()
	end

	function this:FadeOut()
		self.FadedIn = false
		self:CalculateVisibility()
	end

	function this:CoreGuiChanged(coreGuiType, enabled)
		if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then
			chatCoreGuiEnabled = enabled
			self:CalculateVisibility()
		end
	end

	function this:IsAChatMode(mode)
		return ChatModesDict[mode] ~= nil
	end

	function this:ProcessChatBarModes(requireWhitespaceAfterChatMode)
		local matchedAChatCommand = false
		if this.ChatBar then
			local chatBarText = this:SanitizeInput(this:GetChatBarText())
			for regexFunc, actionType in pairs(this.ChatMatchingRegex) do
				local start, finish, capture = regexFunc(chatBarText)
				if start and finish then
					-- The following line is for whether or not to try setting the chatmode as-you-type
					-- versus when you press enter.
					local whitespaceAfterSlashCommand = string.find(string.sub(chatBarText, finish+1, finish+1), "%s")
					if (not requireWhitespaceAfterChatMode and finish == #chatBarText) or whitespaceAfterSlashCommand then
						if this:IsAChatMode(actionType) then
							if actionType == "Whisper" then
								local targetPlayer = capture and Util.GetPlayerByName(capture)
								if targetPlayer then --and targetPlayer ~= Player then
									this.TargetWhisperPlayer = targetPlayer
									-- start from two over to eat the space or tab character after the slash command
									this:SetChatBarText(string.sub(chatBarText, finish + 2))
									this:SetMessageMode(actionType)
									this.ChatCommandEvent:fire(true, actionType, capture)
								else
									-- This is an indirect way of detecting if they used enter to close submit this chat
									if not requireWhitespaceAfterChatMode then
										this:SetChatBarText("")
										this.ChatCommandEvent:fire(false, actionType, capture)
									end
								end
							else
								-- start from two over to eat the space or tab character after the slash command
								this:SetChatBarText(string.sub(chatBarText, finish + 2))
								this:SetMessageMode(actionType)
								this.ChatCommandEvent:fire(true, actionType, capture)
							end
						elseif actionType == "Emote" then
							-- You can only emote to everyone.
							this:SetMessageMode('All')
						elseif not requireWhitespaceAfterChatMode then -- Some non-chat related command
							if actionType == "Help" then
								this:SetChatBarText("") -- Clear the chat so we don't send /? to everyone
							end
							this.ChatCommandEvent:fire(true, actionType, capture)
						end
						-- should we break here since we already matched a slash command or keep going?
						matchedAChatCommand = true
					end
				end
			end
		end
		return matchedAChatCommand
	end

	local previousText = ""
	function this:OnChatBarTextChanged()
		if not Util.IsTouchDevice() then
			this:ProcessChatBarModes(true)
			local originalText = this:GetChatBarText()
			local newText = Util.FilterUnprintableCharacters(originalText)
			if newText ~= originalText then
				previousText = newText
			end

			local fixedText = newText
			if #newText > this.Settings.MaxCharactersInMessage or originalText ~= newText then
				-- This is a hack to deal with the bug that holding down a key for repeated input doesn't trigger the textChanged event
				if #newText == #previousText + 1 then
					fixedText = string.sub(previousText, 1, this.Settings.MaxCharactersInMessage)
				else
					fixedText = string.sub(newText, 1, this.Settings.MaxCharactersInMessage)
				end
			end
			this:SetChatBarText(fixedText)
			previousText = fixedText
		end
	end

	function this:OnChatBarBoundsChanged()
		if this.ChatBarContainer and this.ChatBar then
			local currSize = this.ChatBarContainer.Size
			if this.ChatBar.Visible and not this.ChatBar.TextFits then
				local textBounds = Util.GetStringTextBounds(this.ChatBar.Text, this.ChatBar.Font, this.ChatBar.FontSize, UDim2.new(0, this.ChatBar.AbsoluteSize.X, 0, 1000))
				if textBounds.Y <= 36 then
					this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 58)
				else --if currSize.Y.Offset <= 54 then
					this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 76)
				end
			elseif this.ChatBar.Visible == false or this.ChatBar.TextBounds.Y <= 18 then
				if currSize.Y.Offset ~= 40 then
					this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 40)
				end
			elseif this.ChatBar.TextBounds.Y <= 36 then
				this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 58)
			end
		end
	end

	function this:GetChatBarText()
		return this.ChatBar and this.ChatBar.Text or ""
	end

	function this:SetChatBarText(newText)
		if this.ChatBar and newText ~= this.ChatBar.Text then
			this.ChatBar.Text = newText
		end
	end

	function this:GetMessageMode()
		return this.MessageMode
	end

	function this:SetMessageMode(newMessageMode)
		newMessageMode = ChatModesDict[newMessageMode]

		local chatRecipientText = "[" .. (this.TargetWhisperPlayer and this.TargetWhisperPlayer.Name or "") .. "]"
		if this.MessageMode ~= newMessageMode or (newMessageMode == 'Whisper' and this.ChatModeText and chatRecipientText ~= this.ChatModeText.Text) then
			if this.ChatModeText then
				this.MessageMode = newMessageMode
				if newMessageMode == 'Whisper' then
					local chatRecipientTextBounds = Util.GetStringTextBounds(chatRecipientText, this.ChatModeText.Font, this.ChatModeText.FontSize)

					this.ChatModeText.TextColor3 = this.Settings.WhisperTextColor
					this.ChatModeText.Text = chatRecipientText
					this.ChatModeText.Size = UDim2.new(0, chatRecipientTextBounds.X, 1, 0)
				elseif newMessageMode == 'Team' then
					local chatTeamText = '[Team]'
					local chatTeamTextBounds = Util.GetStringTextBounds(chatTeamText, this.ChatModeText.Font, this.ChatModeText.FontSize)

					this.ChatModeText.TextColor3 = this.Settings.TeamTextColor
					this.ChatModeText.Text = "[Team]"
					this.ChatModeText.Size = UDim2.new(0, chatTeamTextBounds.X, 1, 0)
				else
					this.ChatModeText.Text = ""
					this.ChatModeText.Size = UDim2.new(0, 0, 1, 0)
				end
				if this.ChatBar then
					local offset = this.ChatModeText.Size.X.Offset + this.ChatModeText.Position.X.Offset
					this.ChatBar.Size = UDim2.new(1, -14 - offset, 1, 0)
					this.ChatBar.Position = UDim2.new(0, 7 + offset, 0, 0)
				end
			end
		end
	end

	function this:FocusChatBar()
		if this.ChatBar and not chatBarDisabled then
			this.ChatBar.Visible = true
			this.ChatBar:CaptureFocus()
			if self.ClickToChatButton then
				self.ClickToChatButton.Visible = false
			end
			if this.ChatModeText then
				this.ChatModeText.Visible = true
			end
			if Util.IsTouchDevice() or InputService.VREnabled then
				this:SetMessageMode('All') -- Don't remember message mode on mobile devices or VR
			end
			-- Update chatbar properties when chatbar is focused
			this:OnChatBarBoundsChanged()
			if this.ChatBarContainer then
				if self.ChatBarInnerBackground then
					self.ChatBarInnerBackground.BackgroundTransparency = 0
				end
			end
			this.ChatBarGainedFocusEvent:fire()
		end
	end

	function this:RemoveFocus()
		if self:IsFocused() then
			self.ChatBar:ReleaseFocus()
		end
	end

	function this:IsFocused()
		return self.ChatBar and self.ChatBar == InputService:GetFocusedTextBox()
	end

	function this:WasFocused()
		return (tick() - this.unfocusedAt) < VR_CHAT_CLICK_DEBOUNCE
	end

	function this:SanitizeInput(input)
		local sanitizedInput = input
		-- Chomp the whitespace at the front and end of the string
		-- TODO: maybe only chop off the front space if there are more than a few?
		local _, _, capture = string.find(sanitizedInput, "^%s*(.*)%s*$")
		sanitizedInput = capture or ""

		return sanitizedInput
	end


	local sentMessageTimeQueue = {}
	function this:FloodCheck()
		if not GetLuaChatFilteringFlag() then
			return false
		end

		while sentMessageTimeQueue[1] and tick() - sentMessageTimeQueue[1] > FLOOD_CHECK_MESSAGE_INTERVAL do
			table.remove(sentMessageTimeQueue, 1)
		end
		if #sentMessageTimeQueue > FLOOD_CHECK_MESSAGE_COUNT then
			return true
		end
		return false
	end

	function this:OnChatBarFocusLost(enterPressed)
		if self.ChatBar then
			self.ChatBar.Visible = false
			-- TODO: remove this when API for VR to set enterPressed is released
			if enterPressed or InputService.VREnabled then
				local didMatchSlashCommand = self:ProcessChatBarModes(false)
				local cText = self:SanitizeInput(self:GetChatBarText())
				if cText ~= "" then
					if self:FloodCheck() then -- and not didMatchSlashCommand then
						self.ChatBarFloodEvent:fire()
					else
						-- For now we will let any slash command go through, NOTE: these will show up in bubble-chat
						--if not didMatchSlashCommand and string.sub(cText,1,1) == "/" then
						--	self.ChatCommandEvent:fire(false, "Unknown", cText)
						--else
						local currentMessageMode = self:GetMessageMode()
						-- {All, Team, Whisper}
						if currentMessageMode == 'Team' then
							if Player and Player.Neutral == true then
								self.ChatErrorEvent:fire("You're not on a team.")
							else
								pcall(function() PlayersService:TeamChat(cText) end)
							end
						elseif currentMessageMode == 'Whisper' then
							if self.TargetWhisperPlayer then
								if self.TargetWhisperPlayer == Player then
									self.ChatErrorEvent:fire("You cannot send a whisper to yourself.")
								else
									pcall(function() PlayersService:WhisperChat(cText, self.TargetWhisperPlayer) end)
								end
							else
								self.ChatErrorEvent:fire("Invalid whisper target.")
							end
						elseif currentMessageMode == 'All' then
							pcall(function() PlayersService:Chat(cText) end)
						else
							spawn(function() error("ChatScript: Unknown Message Mode of " .. tostring(currentMessageMode)) end)
						end
						table.insert(sentMessageTimeQueue, tick())
						--end
						self:SetChatBarText("")
					end
				end
			end
		end
		if self.ClickToChatButton then
			self.ClickToChatButton.Visible = true
			-- Fade-back in the text so it doesn't abruptly appear
			-- Normally I would like to cancel the old tween but it is so short that it doesn't matter
			self.ClickToChatButton.TextTransparency = 1
			Util.PropertyTweener(self.ClickToChatButton, 'TextTransparency', 1, 0, 0.25, Util.Linear)
		end
		if self.ChatModeText then
			self.ChatModeText.Visible = false
		end
		if this.ChatBarContainer then
			local currSize = this.ChatBarContainer.Size
			this.ChatBarContainer.Size = UDim2.new(currSize.X.Scale, currSize.X.Offset, currSize.Y.Scale, 32)
			if self.ChatBarInnerBackground then
				self.ChatBarInnerBackground.BackgroundTransparency = 0.5
			end
		end
		this.ChatBarChangedConn = Util.DisconnectEvent(this.ChatBarChangedConn)
		this.FocusChatBarInputBeganConn = Util.DisconnectEvent(this.FocusChatBarInputBeganConn)
	end

	local function CreateChatBar()
		local chatBarContainer = Util.Create'Frame'
		{
			Name = 'ChatBarContainer';
			Position = UDim2.new(0, 0, 1, 0);
			Size = UDim2.new(1, 0, 0, 20);
			ZIndex = 1;
			BackgroundColor3 = Color3.new(0, 0, 0);
			BackgroundTransparency = 0.25;
			BorderSizePixel = 0;
		};
		chatBarContainer.BackgroundColor3 = Color3.new(31/255, 31/255, 31/255);
		chatBarContainer.BackgroundTransparency = 0.5;
		local chatBarInnerBackground = Util.Create'Frame'
		{
			Name = 'InnerBackground';
			Position = UDim2.new(0, 7, 0, 5);
			Size = UDim2.new(1, -14, 1, -10);
			ZIndex = 1;
			BackgroundColor3 = Color3.new(209/255, 216/255, 221/255);
			BackgroundTransparency = 0.5;
			BorderSizePixel = 0;
		};
			local clickToChatButton = Util.Create'TextButton'
			{
				Name = 'ClickToChat';
				Position = UDim2.new(0,9,0,0);
				Size = UDim2.new(1, -9, 1, 0);
				BackgroundTransparency = 1;
				AutoButtonColor = false;
				ZIndex = 3;
				Text = 'To chat click here or press "/" key';
				TextColor3 = this.Settings.GlobalTextColor;
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				Font = Enum.Font.SourceSansBold;
				FontSize = Enum.FontSize.Size18;
				Parent = chatBarContainer;
			}
			clickToChatButton.TextWrapped = true;
			clickToChatButton.Position = UDim2.new(0, 7, 0, 0);
			clickToChatButton.Size = UDim2.new(1, -14, 1, 0);
			clickToChatButton.TextYAlignment = Enum.TextYAlignment.Center;
			if Util.IsTouchDevice() then
				clickToChatButton.Text = "Tap here to chat"
			end

			local chatBar = Util.Create'TextBox'
			{
				Name = 'ChatBar';
				Position = UDim2.new(0, 9, 0, 0);
				Size = UDim2.new(1, -9, 1, 0);
				Text = "";
				ZIndex = 1;
				BackgroundColor3 = Color3.new(0, 0, 0);
				Active = false;
				BackgroundTransparency = 1;
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				TextColor3 = this.Settings.GlobalTextColor;
				Font = Enum.Font.SourceSansBold;
				FontSize = Enum.FontSize.Size18;
				ClearTextOnFocus = false;
				Visible = not Util.IsTouchDevice();
				Parent = chatBarContainer;
				SelectionImageObject = emptySelectionImage;
			}
			chatBar.TextWrapped = true;
			chatBar.Position = UDim2.new(0, 7, 0, 0);
			chatBar.Size = UDim2.new(1, -14, 1, 0);
			chatBar.TextYAlignment = Enum.TextYAlignment.Center;
			chatBar.Visible = false;

			local chatModeText = Util.Create'TextButton'
			{
				Name = 'ChatModeText';
				Position = UDim2.new(0, 9, 0, 0);
				Size = UDim2.new(1, -9, 1, 0);
				AutoButtonColor = false;
				BackgroundTransparency = 1;
				ZIndex = 2;
				Text = '';
				TextColor3 = this.Settings.WhisperTextColor;
				TextXAlignment = Enum.TextXAlignment.Left;
				TextYAlignment = Enum.TextYAlignment.Top;
				Font = Enum.Font.SourceSansBold;
				FontSize = Enum.FontSize.Size18;
				Parent = chatBarContainer;
			}
			chatModeText.Position = UDim2.new(0, 7, 0, 0);
			chatModeText.Size = UDim2.new(1, -14, 1, 0);
			chatModeText.TextYAlignment = Enum.TextYAlignment.Center;
		-- Create grey background for text
		chatBarInnerBackground.Parent = chatBarContainer;
		clickToChatButton.Parent = chatBarInnerBackground;
		chatBar.Parent = chatBarInnerBackground;
		chatModeText.Parent = chatBarInnerBackground;

		this.ChatBarContainer = chatBarContainer
		this.ChatBarInnerBackground = chatBarInnerBackground
		this.ClickToChatButton = clickToChatButton
		this.ChatBar = chatBar
		this.ChatModeText = chatModeText
		this.ChatBarContainer.Parent = GuiRoot

		local function RobloxClientScreenSizeChanged(newSize)
			if chatBarContainer then
				local chatbarVisible = this.ChatBar and this.ChatBar.Visible
				local bubbleChatIsOn = not PlayersService.ClassicChat and PlayersService.BubbleChat
				-- Phone
				if newSize.X <= PHONE_SCREEN_WIDTH then
					chatBarContainer.Size = UDim2.new(0.5, 0,0, chatbarVisible and 40 or 32)
					if bubbleChatIsOn then
						chatBarContainer.Position = UDim2.new(0, 0, 0, 2)
					else
						chatBarContainer.Position = UDim2.new(0, 0, 0.5, 2)
					end
				-- Tablet
				elseif newSize.X <= TABLET_SCREEN_WIDTH then
					chatBarContainer.Size = UDim2.new(0.4, 0,0, chatbarVisible and 40 or 32)
					if bubbleChatIsOn then
						chatBarContainer.Position = UDim2.new(0, 0, 0, 2)
					else
						chatBarContainer.Position = UDim2.new(0, 0, 0.3, 2)
					end
				-- Desktop
				else
					chatBarContainer.Size = UDim2.new(0.3, 0,0, chatbarVisible and 40 or 32)
					if bubbleChatIsOn then
						chatBarContainer.Position = UDim2.new(0, 0, 0, 2)
					else
						chatBarContainer.Position = UDim2.new(0,0,0.25, 2)
					end
				end

				if Util.IsTouchDevice() or InputService.VREnabled then
					-- Hide the chatbar on mobile and in VR so they can't see it.
					chatBarContainer.Position = UDim2.new(0,0,1,20);
				end
			end
		end

		GuiRoot.Changed:connect(function(prop) if prop == "AbsoluteSize" and not chatRepositioned then RobloxClientScreenSizeChanged(GuiRoot.AbsoluteSize) end end)
		RobloxClientScreenSizeChanged(GuiRoot.AbsoluteSize)
	end


	CreateChatBar()
	return this
end

local function CreateChatWindowWidget(settings)
	local this = {}
	this.Settings = settings
	this.Chats = {}
	this.BackgroundVisible = false
	this.ChatsVisible = false
	this.WidgetVisible = false
	this.NewUnreadMessage = false
	this.MessageCount = 0

	this.MessageCountChanged = Util.Signal()
	this.FadeInSignal = Util.Signal()
	this.FadeOutSignal = Util.Signal()

	this.ChatWindowPagingConn = nil

	local lastMoveTime = tick()
	local lastEnterTime = tick()
	local lastLeaveTime = tick()

	local lastFadeOutTime = 0
	local lastFadeInTime = 0
	local lastChatActivity = 0

	local FadeLock = false

	local chatCoreGuiEnabled = true

	local function PointInChatWindow(pt)
		local point0 = this.ChatContainer.AbsolutePosition
		local point1 = point0 + this.ChatContainer.AbsoluteSize
		-- HACK, this is so the "ChatWindow" includes the chatbar box, TODO: refactor the fadeing code to include the chatbar
		point1 = point1 + Vector2.new(0, 34)
		return point0.X <= pt.X and point1.X >= pt.X and
		       point0.Y <= pt.Y and point1.Y >= pt.Y
	end

	function this:IsHovering()
		if this.ChatContainer and this.LastMousePosition and self:CalculateVisibility() then
			return PointInChatWindow(this.LastMousePosition)
		end
		return false
	end

	function this:SetFadeLock(lock)
		FadeLock = lock
	end

	function this:GetFadeLock()
		return FadeLock
	end

	function this:SetCanvasPosition(newCanvasPosition)
		if this.ScrollingFrame then
			local maxSize = Vector2.new(math.max(0, this.ScrollingFrame.CanvasSize.X.Offset - this.ScrollingFrame.AbsoluteWindowSize.X),
			                            math.max(0, this.ScrollingFrame.CanvasSize.Y.Offset - this.ScrollingFrame.AbsoluteWindowSize.Y))
			this.ScrollingFrame.CanvasPosition = Vector2.new(Util.Clamp(0, maxSize.X, newCanvasPosition.X),
			                                                 Util.Clamp(0, maxSize.Y, newCanvasPosition.Y))
		end
	end

	function this:ScrollToBottom()
		if this.ScrollingFrame then
			this:SetCanvasPosition(Vector2.new(this.ScrollingFrame.CanvasPosition.X, this.ScrollingFrame.CanvasSize.Y.Offset))
		end
	end

	function this:FadeIn(duration, lockFade)
		if not FadeLock then
			duration = duration or 0.75
			local backgroundTransparency = InputService.VREnabled and 1 or 0.5
			-- fade in
			if this.BackgroundTweener then
				this.BackgroundTweener:Cancel()
			end
			lastFadeInTime = tick()
			lastChatActivity = tick()
			this.ScrollingFrame.ScrollingEnabled = true
			this.ScrollingFrame.ScrollBarThickness = SCROLLBAR_THICKNESS
			this.BackgroundTweener = Util.PropertyTweener(this.ChatContainer, 'BackgroundTransparency', this.ChatContainer.BackgroundTransparency, backgroundTransparency, duration, Util.Linear)
			this.BackgroundVisible = true
			this:FadeInChats()

			this.ChatWindowPagingConn = Util.DisconnectEvent(this.ChatWindowPagingConn)
			this.ChatWindowPagingConn = InputService.InputBegan:connect(function(inputObject)
				local key = inputObject.KeyCode
				if key == Enum.KeyCode.PageUp then
					this:SetCanvasPosition(this.ScrollingFrame.CanvasPosition - Vector2.new(0, this.ScrollingFrame.AbsoluteWindowSize.Y))
				elseif key == Enum.KeyCode.PageDown then
					this:SetCanvasPosition(this.ScrollingFrame.CanvasPosition + Vector2.new(0, this.ScrollingFrame.AbsoluteWindowSize.Y))
				elseif key == Enum.KeyCode.Home then
					this:SetCanvasPosition(Vector2.new(0, 0))
				elseif key == Enum.KeyCode.End then
					this:ScrollToBottom()
				end
			end)
			if this.FadeInSignal then
				this.FadeInSignal:fire()
			end
		end
	end

	function this:FadeOut(duration, unlockFade)
		if not FadeLock then
			duration = duration or 0.75
			-- fade out
			if this.BackgroundTweener then
				this.BackgroundTweener:Cancel()
			end
			lastFadeOutTime = tick()
			lastChatActivity = tick()
			this.ScrollingFrame.ScrollingEnabled = false
			this.ScrollingFrame.ScrollBarThickness = 0
			this.BackgroundTweener = Util.PropertyTweener(this.ChatContainer, 'BackgroundTransparency', this.ChatContainer.BackgroundTransparency, 1, duration, Util.Linear)
			this.BackgroundVisible = false

			this.ChatWindowPagingConn = Util.DisconnectEvent(this.ChatWindowPagingConn)
			if this.FadeOutSignal then
				this.FadeOutSignal:fire()
			end
		end
	end

	function this:FadeInChats()
		if this.ChatsVisible == true then return end
		this.ChatsVisible = true
		for index, message in pairs(this.Chats) do
			message:FadeIn()
		end
	end

	function this:FadeOutChats()
		if InputService.VREnabled then return end
		if this.ChatsVisible == false then return end
		this.ChatsVisible = false
		if IsPlayerDropDownEnabled and playerDropDown then
			playerDropDown:Hide()
		end
		for index, message in pairs(this.Chats) do
			local messageGui = message:GetGui()
			local instant = false
			if messageGui and this.ScrollingFrame then
				-- If the chat is not in the visible frame then don't waste cpu cycles fading it out
				if messageGui.AbsolutePosition.Y > (this.ScrollingFrame.AbsolutePosition + this.ScrollingFrame.AbsoluteWindowSize).Y or
						messageGui.AbsolutePosition.Y + messageGui.AbsoluteSize.Y < this.ScrollingFrame.AbsolutePosition.Y then
					instant = true
				end
			end
			message:FadeOut(instant)
		end
	end

	local ResizeCount = 0
	function this:OnResize()
		ResizeCount = ResizeCount + 1
		local currentResizeCount = ResizeCount
		local isScrolledDown = this:IsScrolledDown()
		-- Unfortunately there is a race condition so we need this wait here.
		wait()
		if this.ScrollingFrame then
			if currentResizeCount ~= ResizeCount then return end
			local scrollingFrameAbsoluteSize = this.ScrollingFrame.AbsoluteWindowSize
			if scrollingFrameAbsoluteSize ~= nil and scrollingFrameAbsoluteSize.X > 0 and scrollingFrameAbsoluteSize.Y > 0 then
				local ySize = 0

				if this.ScrollingFrame then
					for _, message in pairs(this.Chats) do
						local newHeight = message:OnResize(scrollingFrameAbsoluteSize)
						if newHeight then
							local chatMessageElement = message:GetGui()
							if chatMessageElement then
								local chatMessageElementYSize = chatMessageElement.Size.Y.Offset
								chatMessageElement.Position = UDim2.new(0, 0, 0, ySize)
								ySize = ySize + chatMessageElementYSize
							end
						end
					end
				end
				if this.MessageContainer and this.ScrollingFrame then
					this.MessageContainer.Size = UDim2.new(
							this.MessageContainer.Size.X.Scale,
							this.MessageContainer.Size.X.Offset,
							0,
							ySize)
					this.MessageContainer.Position = UDim2.new(0, 0, 1, -this.MessageContainer.Size.Y.Offset)
					this.ScrollingFrame.CanvasSize = UDim2.new(this.ScrollingFrame.CanvasSize.X.Scale, this.ScrollingFrame.CanvasSize.X.Offset, this.ScrollingFrame.CanvasSize.Y.Scale, ySize)
				end
			end
			this:ScrollToBottom()
		end
	end

	function this:FilterMessage(playerChatType, sendingPlayer, chattedMessage, receivingPlayer)
		if chattedMessage and string.sub(chattedMessage, 1, 1) ~= '/' then
			return true
		end
		return false
	end

	function this:PushMessageIntoQueue(chatMessage, silently)
		table.insert(this.Chats, chatMessage)

		local isScrolledDown = this:IsScrolledDown()

		local chatMessageElement = chatMessage:GetGui()

		chatMessageElement.Parent = this.MessageContainer
		local chatMessageHeight = chatMessage:OnResize() or 10
		local ySize = this.MessageContainer.Size.Y.Offset
		local chatMessageElementYSize = UDim2.new(0, 0, 0, chatMessageHeight)

		if not silently then
			this.MessageCount = this.MessageCount + 1
		end

		chatMessageElement.Position = chatMessageElement.Position + UDim2.new(0, 0, 0, ySize)
		this.MessageContainer.Size = this.MessageContainer.Size + chatMessageElementYSize
		this.ScrollingFrame.CanvasSize = this.ScrollingFrame.CanvasSize + chatMessageElementYSize

		if this.Settings.MaxWindowChatMessages < #this.Chats then
			this:RemoveOldestMessage()
		end
		if isScrolledDown then
			this:ScrollToBottom()
		elseif not silently then
			-- Raise unread message alert!
			this.NewUnreadMessage = true
		end

		if silently then
			if this.ChatsVisible == false then
				chatMessage:FadeOut(true)
			end
		else
			this:FadeInChats()
			lastChatActivity = tick()
			this.MessageCountChanged:fire(this.MessageCount)
		end

		-- NOTE: Sort of hacky, but if we are approaching the max 16 bit size
		-- we need to rebase y back to 0 which can be done with the resize function
		if ySize > (MAX_UDIM_SIZE / 2) then
			self:OnResize()
		end
	end

	function this:AddSystemChatMessage(chattedMessage, silently)
		local chatMessage = CreateSystemChatMessage(this.Settings, chattedMessage)
		this:PushMessageIntoQueue(chatMessage, silently)
	end

	local function checkEnum(enumItems, value)
		for _, enum in pairs(enumItems) do
			if enum.Value == value then
				return enum
			end
		end
		return nil
	end

	-- We only need to copy the top level for the settings table
	local function shallowCopy(tableToCopy)
		local newTable = {}
		for key, value in pairs(tableToCopy) do
			newTable[key] = value
		end
		return newTable
	end

	function this:AddDeveloperSystemChatMessage(informationTable)
		local settings = shallowCopy(this.Settings)

		if informationTable["Text"] and type(informationTable["Text"]) == "string" then
			if informationTable["Color"] and pcall(function() Color3.new(informationTable["Color"].r, informationTable["Color"].g, informationTable["Color"].b) end) then
				settings.DefaultMessageTextColor = informationTable["Color"]
			end
			if informationTable["Font"] then
				local success, value = pcall(function() return checkEnum(Enum.Font:GetEnumItems(), informationTable["Font"].Value) end)
				if success and value ~= nil then
					settings.Font = value
				end
			end
			if informationTable["FontSize"] then
				local success, value = pcall(function() return checkEnum(Enum.FontSize:GetEnumItems(), informationTable["FontSize"].Value) end)
				if success and value ~= nil then
					settings.FontSize = value
				end
			end
			local chatMessage = CreateSystemChatMessage(settings, informationTable["Text"])
			this:PushMessageIntoQueue(chatMessage, false)
		end
	end

	function this:AddChatMessage(playerChatType, sendingPlayer, chattedMessage, receivingPlayer, silently)
		local fixedChattedMessage = Util.FilterUnprintableCharacters(chattedMessage)
		if this:FilterMessage(playerChatType, sendingPlayer, fixedChattedMessage, receivingPlayer) then
			local chatMessage = CreatePlayerChatMessage(this.Settings, playerChatType, sendingPlayer, fixedChattedMessage, receivingPlayer)
			this:PushMessageIntoQueue(chatMessage, silently)
		end
	end

	function this:RemoveOldestMessage()
		local oldestChat = this.Chats[1]
		if oldestChat then
			return this:RemoveChatMessage(oldestChat)
		end
	end

	function this:RemoveChatMessage(chatMessage)
		if chatMessage then
			for index, message in pairs(this.Chats) do
				if chatMessage == message then
					local guiObj = chatMessage:GetGui()
					if guiObj then
						local ySize = guiObj.Size.Y.Offset
						this.ScrollingFrame.CanvasSize = this.ScrollingFrame.CanvasSize - UDim2.new(0,0,0,ySize)
						-- Clamp the canvasposition
						this:SetCanvasPosition(this.ScrollingFrame.CanvasPosition)
						guiObj.Parent = nil
					end
					message:Destroy()
					return table.remove(this.Chats, index)
				end
			end
		end
	end

	function this:IsScrolledDown()
		if this.ScrollingFrame then
			local yCanvasSize = this.ScrollingFrame.CanvasSize.Y.Offset
			local yContainerSize = this.ScrollingFrame.AbsoluteWindowSize.Y
			local yScrolledPosition = this.ScrollingFrame.CanvasPosition.Y
			-- Check if the messages are at the bottom
			return yCanvasSize < yContainerSize or
			       yCanvasSize - yScrolledPosition <= yContainerSize + 5 -- a little wiggle room
		end
		return false
	end

	function this:GetMessageCount()
		return this.MessageCount
	end

	function this:CalculateVisibility()
		return this.WidgetVisible and ((chatCoreGuiEnabled and PlayersService.ClassicChat) or NON_CORESCRIPT_MODE)
	end

	function this:ToggleVisibility(visible)
		if visible ~= self.WidgetVisible then
			self.WidgetVisible = visible
			if this.ChatContainer then
				this.ChatContainer.Visible = self:CalculateVisibility()
			end
		end
		if NON_CORESCRIPT_MODE then
			this.ChatContainer.Visible = true
		end
	end

	function this:CoreGuiChanged(coreGuiType, enabled)
		if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then
			chatCoreGuiEnabled = enabled
			if this.ChatContainer then
				this.ChatContainer.Visible = self:CalculateVisibility()
			end
		end
	end

	local function CreateChatWindow()
		local container = Util.Create'TextButton'
		{
			Name = 'ChatWindowContainer';
			Size = UDim2.new(0.3, 0, 0.25, 0);
			Position = UDim2.new(0, 8, 0, 37);
			ZIndex = 1;
			BackgroundColor3 = Color3.new(0, 0, 0);
			BackgroundTransparency = 1;
			BorderSizePixel = 0;
			Text = "";
			SelectionImageObject = emptySelectionImage;
		};
		container.Position = UDim2.new(0,0,0,37);
		container.BackgroundColor3 = Color3.new(31/255, 31/255, 31/255);
			local scrollingFrame = Util.Create'ScrollingFrame'
			{
				Name = 'ChatWindow';
				Size = UDim2.new(1, -4 - 10, 1, -20);
				CanvasSize = UDim2.new(1, -4 - 10, 0, 0);
				Position = UDim2.new(0, 10, 0, 10);
				ZIndex = 1;
				BackgroundColor3 = Color3.new(0, 0, 0);
				BackgroundTransparency = 1;
				BottomImage = "rbxasset://textures/ui/scroll-bottom.png";
				MidImage = "rbxasset://textures/ui/scroll-middle.png";
				TopImage = "rbxasset://textures/ui/scroll-top.png";
				ScrollBarThickness = 0;
				BorderSizePixel = 0;
				ScrollingEnabled = false;
				Parent = container;
			};
				local messageContainer = Util.Create'Frame'
				{
					Name = 'MessageContainer';
					Size = UDim2.new(1, -SCROLLBAR_THICKNESS - 1, 0, 0);
					Position = UDim2.new(0, 0, 1, 0);
					ZIndex = 1;
					BackgroundColor3 = Color3.new(0, 0, 0);
					BackgroundTransparency = 1;
					Parent = scrollingFrame
				};

		-- This is some trickery we are doing to make the first chat messages appear at the bottom and go towards the top.
		local function OnChatWindowResize(prop)
			if prop == 'AbsoluteSize' then
				messageContainer.Position = UDim2.new(0, 0, 1, -messageContainer.Size.Y.Offset)
			end
			if prop == 'CanvasPosition' then
				if this.ScrollingFrame then
					if this:IsScrolledDown() then
						this.NewUnreadMessage = false
					end
				end
			end
		end
		container.Changed:connect(function(prop) if prop == 'AbsoluteSize' then this:OnResize() end end)

		local function RobloxClientScreenSizeChanged(newSize)
			if container then
				container.Position = UDim2.new(0,0,0,2);
				if InputService.VREnabled then
					container.Size = UDim2.new(1,0,1,0)
				-- Phone
				elseif newSize.X <= 640 then
					container.Size = UDim2.new(0.5,0,0.5,0) - container.Position
				-- Tablet
				elseif newSize.X <= 1024 then
					container.Size = UDim2.new(0.4,0,0.3,0) - container.Position
				-- Desktop
				else
					container.Size = UDim2.new(0.3,0,0.25,0) - container.Position
				end
			end
		end

		GuiRoot.Changed:connect(function(prop) if prop == "AbsoluteSize" and not chatRepositioned then RobloxClientScreenSizeChanged(GuiRoot.AbsoluteSize) end end)
		RobloxClientScreenSizeChanged(GuiRoot.AbsoluteSize)

		messageContainer.Changed:connect(OnChatWindowResize)
		scrollingFrame.Changed:connect(OnChatWindowResize)

		this.ChatContainer = container
		this.ScrollingFrame = scrollingFrame
		this.MessageContainer = messageContainer
		this.ChatContainer.Parent = GuiRoot


		-- It is important to set this to true in NON_CORESCRIPT_MODE because normally the topbar sets
		-- the chat window to visible
		if NON_CORESCRIPT_MODE then
			this:ToggleVisibility(true)
		end

		--- BACKGROUND FADING CODE ---
			-- This is so we don't accidentally fade out when we are scrolling and mess with the scrollbar.
			local dontFadeOutOnMouseLeave = false

			if Util:IsTouchDevice() then
				local touchCount = 0
				this.InputBeganConn = InputService.InputBegan:connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.Begin then
						if PointInChatWindow(Vector2.new(inputObject.Position.X, inputObject.Position.Y)) then
							touchCount = touchCount + 1
							dontFadeOutOnMouseLeave = true
						end
					end
				end)

				this.InputEndedConn = InputService.InputEnded:connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.End then
						local endedCount = touchCount
						wait(2)
						if touchCount == endedCount then
							dontFadeOutOnMouseLeave = false
						end
					end
				end)

				spawn(function()
					local now = tick()
					while true do
						wait()
						now = tick()
						if this.BackgroundVisible then
							if not dontFadeOutOnMouseLeave then
								this:FadeOut(0.25)
							end
						-- If background is not visible/in-focus
						elseif this.ChatsVisible and now > lastChatActivity + MESSAGES_FADE_OUT_TIME then
							this:FadeOutChats()
						end
					end
				end)
			else
				this.LastMousePosition = Vector2.new()

				this.MouseEnterFrameConn = this.ChatContainer.MouseEnter:connect(function()
					lastEnterTime = tick()
					if this.BackgroundTweener and not this.BackgroundTweener:IsFinished() and not this.BackgroundVisible then
						this:FadeIn()
					end
				end)

				this.MouseMoveConn = InputService.InputChanged:connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.MouseMovement then
						lastMoveTime = tick()
						this.LastMousePosition = Vector2.new(inputObject.Position.X, inputObject.Position.Y)
						if this.BackgroundTweener and this.BackgroundTweener:GetPercentComplete() < 0.5 and this.BackgroundVisible then
							if not dontFadeOutOnMouseLeave then
								this:FadeOut()
							end
						end
					end
				end)

				local clickCount = 0
				this.InputBeganConn = InputService.InputBegan:connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and inputObject.UserInputState == Enum.UserInputState.Begin then
						if PointInChatWindow(Vector2.new(inputObject.Position.X, inputObject.Position.Y)) then
							clickCount = clickCount + 1
							dontFadeOutOnMouseLeave = true
						end
					end
				end)

				this.InputEndedConn = InputService.InputEnded:connect(function(inputObject)
					if inputObject.UserInputType == Enum.UserInputType.MouseButton1 and inputObject.UserInputState == Enum.UserInputState.End then
						local nowCount = clickCount
						wait(1.3)
						if nowCount == clickCount then
							dontFadeOutOnMouseLeave = false
						end
					end
				end)

				this.MouseLeaveFrameConn = this.ChatContainer.MouseLeave:connect(function()
					lastLeaveTime = tick()
					if this.BackgroundTweener and not this.BackgroundTweener:IsFinished() and this.BackgroundVisible then
						if not dontFadeOutOnMouseLeave then
							this:FadeOut()
						end
					end
				end)

				spawn(function()
					while true do
						wait()
						local now = tick()
						if this:IsHovering() then
							if now - lastMoveTime > 1.3 and not this.BackgroundVisible then
								this:FadeIn()
							end
						else -- not this:IsHovering()
							if this.BackgroundVisible then
								if not dontFadeOutOnMouseLeave then
									this:FadeOut(0.25)
								end
							-- If background is not visible/in-focus
							elseif this.ChatsVisible and now > lastChatActivity + MESSAGES_FADE_OUT_TIME then
								this:FadeOutChats()
							end
						end
					end
				end)
			end
		--- END OF BACKGROUND FADING CODE ---
	end

	CreateChatWindow()

	return this
end


local function CreateChat()
	local this = {}

	this.Settings =
	{
		GlobalTextColor = Color3.new(112/255, 110/255, 106/255);
		WhisperTextColor = Color3.new(77/255, 139/255, 255/255);
		TeamTextColor = Color3.new(230/255, 207/255, 0);
		DefaultMessageTextColor = Color3.new(255/255, 255/255, 243/255);
		AdminTextColor = Color3.new(1, 215/255, 0);
		InternTextColor = Color3.new(175/255, 221/255, 1);
		TextStrokeTransparency = 0.75;
		TextStrokeColor = Color3.new(34/255,34/255,34/255);
		Font = Enum.Font.SourceSansBold;
		SmallScreenFontSize = Enum.FontSize.Size14;
		FontSize = Enum.FontSize.Size18;
		MaxWindowChatMessages = 50;
		MaxCharactersInMessage = 140;
	}

	this.CurrentWindowMessageCountChanged = nil
	this.VisibilityStateChanged = Util.Signal()
	this.ChatBarFocusChanged = Util.Signal()
	this.Visible = false

	function this:CoreGuiChanged(coreGuiType, enabled)
		enabled = enabled and (topbarEnabled or InputService.VREnabled)
		if coreGuiType == Enum.CoreGuiType.Chat or coreGuiType == Enum.CoreGuiType.All then
			if enabled then
				pcall(function()
					self.SpecialKeyPressedConn = Util.DisconnectEvent(self.SpecialKeyPressedConn)
					GuiService:AddSpecialKey(Enum.SpecialKey.ChatHotkey)
					self.SpecialKeyPressedConn = GuiService.SpecialKeyPressed:connect(function(key)
						if key == Enum.SpecialKey.ChatHotkey then
							if self.Visible == false then
								self:ToggleVisibility()
							end
							if self.ChatBarWidget then
								self.ChatBarWidget:FocusChatBar()
							end
						end
					end)
				end)
			else
				pcall(function() GuiService:RemoveSpecialKey(Enum.SpecialKey.ChatHotkey) end)
				self.SpecialKeyPressedConn = Util.DisconnectEvent(self.SpecialKeyPressedConn)
			end
			if this.MobileChatButton then
				if enabled == true then
					this.MobileChatButton.Parent = GuiRoot
					-- we need to set it to be visible in-case we missed a lost focus event while chat was turned off.
					this.MobileChatButton.Visible = true
				else
					this.MobileChatButton.Parent = nil
				end
			end
		end
		if this.ChatWindowWidget then
			this.ChatWindowWidget:CoreGuiChanged(coreGuiType, enabled)
		end
		if this.ChatBarWidget then
			this.ChatBarWidget:CoreGuiChanged(coreGuiType, enabled)
		end
	end

	-- This event has 4 callback arguments
	-- Enum.PlayerChatType.{All|Team|Whisper}, chatPlayer, message, targetPlayer
	function this:OnPlayerChatted(playerChatType, sendingPlayer, chattedMessage, receivingPlayer)
		if this.ChatWindowWidget then
			-- Don't add messages from blocked players, don't show message if is a debug command
			local isDebugCommand = false
			pcall(function()
				if not NON_CORESCRIPT_MODE and sendingPlayer == PlayersService.LocalPlayer then
					isDebugCommand = game:GetService("GuiService"):ShowStatsBasedOnInputString(chattedMessage)
					
					-- allows dev console to be opened on mobile
					-- NOTE: Removed ToggleDevConsole bindable event, so engine no longer handles this
					if string.lower(chattedMessage) == "/console" then
						local devConsoleModule = require(RobloxGui.Modules.DeveloperConsoleModule)
						if devConsoleModule then
							local devConsoleVisible = devConsoleModule:GetVisibility()
							devConsoleModule:SetVisibility(not devConsoleVisible)
						end
					end
				end
			end)
			if not (this:IsPlayerBlocked(sendingPlayer) or this:IsPlayerMuted(sendingPlayer) or isDebugCommand) then
				this.ChatWindowWidget:AddChatMessage(playerChatType, sendingPlayer, chattedMessage, receivingPlayer)
			end
		end
	end

	function this:OnPlayerAdded(newPlayer)
		if newPlayer then
			assert(coroutine.resume(coroutine.create(function() PlayerPermissionsModule.IsPlayerAdminAsync(newPlayer) end)))
		end
		if NON_CORESCRIPT_MODE then
			newPlayer.Chatted:connect(function(msg, recipient)
				this:OnPlayerChatted(Enum.PlayerChatType.All, newPlayer, msg, recipient)
			end)
		else
			this.PlayerChattedConn = Util.DisconnectEvent(this.PlayerChattedConn)
			this.PlayerChattedConn = PlayersService.PlayerChatted:connect(function(...)
				this:OnPlayerChatted(...)
			end)
		end
	end

	function this:IsPlayerBlocked(player)
		if blockingUtility then
			return player and blockingUtility:IsPlayerBlockedByUserId(player.userId)
		else
			return false
		end
	end

	function this:BlockPlayerAsync(playerToBlock)
		if playerToBlock and Player ~= playerToBlock then
			local blockUserId = playerToBlock.userId
			local playerToBlockName = playerToBlock.Name
			if blockUserId > 0 then
				if not this:IsPlayerBlocked(playerToBlock) then
					if blockingUtility then
						blockingUtility:BlockPlayerAsync(playerToBlock)
						this.ChatWindowWidget:AddSystemChatMessage(playerToBlockName .. " is now blocked.")
					end
				else
					this.ChatWindowWidget:AddSystemChatMessage(playerToBlockName .. " is already blocked.")
				end
			else
				this.ChatWindowWidget:AddSystemChatMessage("You cannot block guests.")
			end
		else
			this.ChatWindowWidget:AddSystemChatMessage("You cannot block yourself.")
		end
	end

	function this:UnblockPlayerAsync(playerToUnblock)
		if playerToUnblock then
			local unblockUserId = playerToUnblock.userId
			local playerToUnblockName = playerToUnblock.Name

			if this:IsPlayerBlocked(playerToUnblock) then
				if blockingUtility then
					this.ChatWindowWidget:AddSystemChatMessage(playerToUnblockName .. " is no longer blocked.")
					blockingUtility:UnblockPlayerAsync(playerToUnblock)
				end
			else
				this.ChatWindowWidget:AddSystemChatMessage(playerToUnblockName .. " is not blocked.")
			end
		end
	end

	function this:IsPlayerMuted(player)
		if blockingUtility then
			return player and blockingUtility:IsPlayerMutedByUserId(player.userId)
		else
			return false
		end
	end

	function this:MutePlayer(playerToMute)
		if playerToMute and playerToMute ~= Player then
			if playerToMute.UserId > 0 then
				if not this:IsPlayerMuted(playerToMute) then
					if blockingUtility then
						blockingUtility:MutePlayer(playerToMute)
						this.ChatWindowWidget:AddSystemChatMessage(playerToMute.Name .. " is now muted.")
					end
				else
					this.ChatWindowWidget:AddSystemChatMessage(playerToMute.Name .. " is already muted.")
				end
			else
				this.ChatWindowWidget:AddSystemChatMessage("You cannot mute guests.")
			end
		else
			this.ChatWindowWidget:AddSystemChatMessage("You cannot mute yourself.")
		end
	end

	function this:UnmutePlayer(playerToUnmute)
		if playerToUnmute then
			if this:IsPlayerMuted(playerToUnmute) then
				if blockingUtility then
					blockingUtility:UnmutePlayer(playerToUnmute)
					this.ChatWindowWidget:AddSystemChatMessage(playerToUnmute.Name .. " is no longer muted.")
				end
			else
				this.ChatWindowWidget:AddSystemChatMessage(playerToUnmute.Name .. " is not muted.")
			end
		end
	end

	function this:CreateTouchDeviceChatButton()
		return Util.Create'ImageButton'
		{
			Name = 'TouchDeviceChatButton';
			Size = UDim2.new(0, 128, 0, 32);
			Position = UDim2.new(0, 88, 0, 0);
			BackgroundTransparency = 1.0;
			Image = 'https://www.roblox.com/asset/?id=97078724';
		};
	end

	function this:PrintWelcome()
		if this.ChatWindowWidget then
			if Util.IsTouchDevice() then
				this.ChatWindowWidget:AddSystemChatMessage("Please press the '...' icon to chat", true)
			end
			this.ChatWindowWidget:AddSystemChatMessage("hey hello welcome to this Estonix server", true)
		end
	end

	local doOnceVRWelcome = false
	function this:PrintVRWelcome()
		if this.ChatWindowWidget and not doOnceVRWelcome then
			if InputService.VREnabled then
				this.ChatWindowWidget:AddSystemChatMessage("Press here to chat", true)
				doOnceVRWelcome = true
			end
		end
	end

	function this:PrintHelp()
		if this.ChatWindowWidget then
			this.ChatWindowWidget:AddSystemChatMessage("Help Menu")
			this.ChatWindowWidget:AddSystemChatMessage("Chat Commands:")
			this.ChatWindowWidget:AddSystemChatMessage("/w [PlayerName] or /whisper [PlayerName] - Whisper Chat")
			this.ChatWindowWidget:AddSystemChatMessage("/t or /team - Team Chat")
			this.ChatWindowWidget:AddSystemChatMessage("/a or /all - All Chat")

			this.ChatWindowWidget:AddSystemChatMessage("/block [PlayerName] - Block communications from Target Player")
			this.ChatWindowWidget:AddSystemChatMessage("/unblock [PlayerName] - Restore communications with Target Player")
			this.ChatWindowWidget:AddSystemChatMessage("/mute [PlayerName] - Mute in-game communications from Target Player")
			this.ChatWindowWidget:AddSystemChatMessage("/unmute [PlayerName] - Restore in-game communications with Target Player")
		end
	end

	local focusCount = 0
	function this:CreateGUI()
		if	FORCE_CHAT_GUI or Player.ChatMode == Enum.ChatMode.TextAndMenu and
			game:GetService("UserInputService"):GetPlatform() ~= Enum.Platform.XBoxOne
		then
			if NON_CORESCRIPT_MODE then
				local chatGui = Instance.new("ScreenGui")
				chatGui.Name = "RobloxGui"
				chatGui.Parent = Player:WaitForChild('PlayerGui')
				GuiRoot.Parent = chatGui
			end

			-- NOTE: eventually we will make multiple chat window frames
			this.ChatWindowWidget = CreateChatWindowWidget(this.Settings)
			this.ChatBarWidget = CreateChatBarWidget(this.Settings)
			this.CurrentWindowMessageCountChanged = this.ChatWindowWidget.MessageCountChanged

			this.ChatWindowWidget.FadeInSignal:connect(function()
				this.ChatBarWidget:FadeIn()
			end)
			this.ChatWindowWidget.FadeOutSignal:connect(function()
				this.ChatBarWidget:FadeOut()
			end)

			this.ChatWindowWidget:FadeOut(0)
			this.ChatBarWidget.ChatBarGainedFocusEvent:connect(function()
				focusCount = focusCount + 1
				this.ChatWindowWidget:FadeIn(0.25)
				this.ChatWindowWidget:SetFadeLock(true)
				this.ChatBarFocusChanged:fire(true)
			end)
			this.ChatBarWidget.ChatBarLostFocusEvent:connect(function()
				local focusNow = focusCount
				if Util:IsTouchDevice() then
					delay(2, function()
						if focusNow == focusCount then
							this.ChatWindowWidget:SetFadeLock(false)
						end
					end)
				else
					this.ChatWindowWidget:SetFadeLock(false)
				end
				this.ChatBarFocusChanged:fire(false)
			end)
			this.ChatBarWidget.ChatBarFloodEvent:connect(function()
				if this.ChatWindowWidget then
					this.ChatWindowWidget:AddSystemChatMessage("Wait before sending another message.")
				end
			end)

			this.ChatBarWidget.ChatErrorEvent:connect(function(msg)
				if msg then
					this.ChatWindowWidget:AddSystemChatMessage(msg)
				end
			end)

			this.ChatBarWidget.ChatCommandEvent:connect(function(success, actionType, capture)
				if actionType == "Help" then
					this:PrintHelp()
				elseif actionType == "Block" then
					local blockPlayerName = capture and tostring(capture) or ""
					local playerToBlock = Util.GetPlayerByName(blockPlayerName)
					if playerToBlock then
						spawn(function() this:BlockPlayerAsync(playerToBlock) end)
					else
						this.ChatWindowWidget:AddSystemChatMessage("Cannot block " .. blockPlayerName .. " because they are not in the game.")
					end
				elseif actionType == "Unblock" then
					local unblockPlayerName = capture and tostring(capture) or ""
					local playerToBlock = Util.GetPlayerByName(unblockPlayerName)
					if playerToBlock then
						spawn(function() this:UnblockPlayerAsync(playerToBlock) end)
					else
						this.ChatWindowWidget:AddSystemChatMessage("Cannot unblock " .. unblockPlayerName .. " because they are not in the game.")
					end
				elseif actionType == "Mute" then
					local mutePlayerName = capture and tostring(capture) or ""
					local playerToMute = Util.GetPlayerByName(mutePlayerName)
					if playerToMute then
						this:MutePlayer(playerToMute)
					else
						this.ChatWindowWidget:AddSystemChatMessage("Cannot mute " .. mutePlayerName .. " because they are not in the game.")
					end
				elseif actionType == "Unmute" then
					local unmutePlayerName = capture and tostring(capture) or ""
					local playerToUnmute = Util.GetPlayerByName(unmutePlayerName)
					if playerToUnmute then
						this:UnmutePlayer(playerToUnmute)
					else
						this.ChatWindowWidget:AddSystemChatMessage("Cannot unmute " .. unmutePlayerName .. " because they are not in the game.")
					end
				elseif actionType == "Whisper" then
					if success == false then
						local playerName = capture and tostring(capture) or "Unknown"
						this.ChatWindowWidget:AddSystemChatMessage("Unable to Send a Whisper to Player: " .. playerName)
					end
				elseif actionType == "Unknown" then
					if success == false then
						local commandText = capture and tostring(capture) or "Unknown"
						this.ChatWindowWidget:AddSystemChatMessage("Invalid Slash Command: " .. commandText)
					end
				end
			end)

			if not NON_CORESCRIPT_MODE then
				local function onVREnabled()
					if InputService.VREnabled then
						self.Settings.TextStrokeTransparency = 1
						self:PrintVRWelcome()
						local Panel3D = require(RobloxGui.Modules.VR.Panel3D)

						local panel = Panel3D.Get(thisModuleName)
						panel:LinkTo("Keyboard")
						panel:SetType(Panel3D.Type.Fixed)
						panel:ResizePixels(300, 125)
						GuiRoot.Parent = panel:GetGUI()

						if this.ChatWindowWidget and this.ChatWindowWidget.ChatContainer then
							this.ChatWindowWidget.ChatContainer.MouseButton1Click:connect(function()
								if this.ChatBarWidget then
									if this.ChatBarWidget:WasFocused() then
										this.ChatBarWidget:RemoveFocus()
									else
										self:FocusChatBar()
									end
								end
							end)
						end

						function panel:CalculateTransparency()
							return 0
						end

						VRHub.ModuleOpened.Event:connect(function(moduleName)
							local module = VRHub:GetModule(moduleName)
							if moduleName ~= thisModuleName and module.VRIsExclusive then
								this:SetVisible(false)
							end
						end)
					else
						self.Settings.TextStrokeTransparency = 0.75
						GuiRoot.Parent = RobloxGui
					end
				end
				onVREnabled()
				InputService.Changed:connect(function(prop)
					if prop == 'VREnabled' then
						onVREnabled()
					end
				end)
			end
		end
	end

	local toggleCount = 0
	local function SetVisbility(newVisibility)
		this.Visible = newVisibility
		if this.ChatWindowWidget then
			this.ChatWindowWidget:ToggleVisibility(this.Visible)
			if this.Visible then
				toggleCount = toggleCount + 1
				local thisToggle = toggleCount
				local thisFocusCount = focusCount
				this.ChatWindowWidget:FadeIn()
				this.ChatWindowWidget:SetFadeLock(true)
				delay(5, function()
					if thisToggle == toggleCount and thisFocusCount == focusCount then
						this.ChatWindowWidget:SetFadeLock(false)
					end
				end)
			end
		end
		if this.ChatBarWidget then
			this.ChatBarWidget:ToggleVisibility(this.Visible)
			if this.Visible then
				this.ChatBarWidget:FadeIn()
			end
			if InputService.VREnabled and not this.Visible then
				this.ChatBarWidget:RemoveFocus()
			end
		end
		if IsPlayerDropDownEnabled and playerDropDown then
			if not this.Visible then
				playerDropDown:Hide()
			end
		end
		if InputService.VREnabled then
			local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
			
			local panel = Panel3D.Get(thisModuleName)
			if this.Visible then
				local topbarPanel = Panel3D.Get("Topbar3D")
				panel.localCF = topbarPanel.localCF * CFrame.Angles(math.rad(-5), 0, 0) * CFrame.new(0, 4, 0) * CFrame.Angles(math.rad(-15), 0, 0)
				panel:SetVisible(true)
				panel:ForceShowUntilLookedAt()

				VRHub:FireModuleOpened(thisModuleName)
			else
				panel:SetVisible(this.Visible)

				VRHub:FireModuleClosed(thisModuleName)
			end			
		end
		this.VisibilityStateChanged:fire(this.Visible)
	end

	function this:ToggleVisibility()
		SetVisbility(not self.Visible)
	end

	function this:SetVisible(visible)
		SetVisbility(visible)
	end

	function this:FocusChatBar()
		if self.ChatBarWidget and this.Visible then
			self.ChatBarWidget:FocusChatBar()
		end
	end

	function this:IsFocused(useWasFocused)
		if not self.ChatBarWidget then return false end
		return self.ChatBarWidget:IsFocused() or (useWasFocused and self.ChatBarWidget:WasFocused())
	end

	function this:GetCurrentWindowMessageCount()
		if this.ChatWindowWidget then
			return this.ChatWindowWidget:GetMessageCount()
		end
		return 0
	end

	function this:TopbarEnabledChanged(enabled)
		topbarEnabled = enabled
		-- Update coregui to reflect new topbar status
		self:CoreGuiChanged(Enum.CoreGuiType.Chat, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat))
	end

	function this:Initialize()
		--[[ Developer Customization API ]]--
		if not NON_CORESCRIPT_MODE then
			game:WaitForChild("StarterGui"):RegisterSetCore("ChatMakeSystemMessage", 	function(informationTable)
																							if this.ChatWindowWidget then
																								this.ChatWindowWidget:AddDeveloperSystemChatMessage(informationTable)
																							end
																						end)
			local function isUDim2Value(value)
				local success, value = pcall(function() return UDim2.new(value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset) end)
				return success and value or nil
			end

			local function isBubbleChatOn()
				return not PlayersService.ClassicChat and PlayersService.BubbleChat
			end

			if allowMoveChat then
				game.StarterGui:RegisterSetCore("ChatWindowPosition", 	function(value)
																			if this.ChatWindowWidget and this.ChatBarWidget then
																				value = isUDim2Value(value)
																				if value ~= nil and not isBubbleChatOn() then
																					chatRepositioned = true -- Prevent chat from moving back to the original position on screen resolution change
																					this.ChatWindowWidget.ChatContainer.Position = value
																					this.ChatBarWidget.ChatBarContainer.Position = value + UDim2.new(0, 0, this.ChatWindowWidget.ChatContainer.Size.Y.Scale, this.ChatWindowWidget.ChatContainer.Size.Y.Offset + 2)
																				end
																			end
																		end)

				game.StarterGui:RegisterSetCore("ChatWindowSize", 	function(value)
 																		if this.ChatWindowWidget and this.ChatBarWidget then
 																			value = isUDim2Value(value)
																			if value ~= nil and not isBubbleChatOn() then
																				chatRepositioned = true
																				this.ChatWindowWidget.ChatContainer.Size = value
																				this.ChatBarWidget.ChatBarContainer.Size = UDim2.new(this.ChatWindowWidget.ChatContainer.Size.X.Scale, this.ChatWindowWidget.ChatContainer.Size.X.Offset, this.ChatBarWidget.ChatBarContainer.Size.Y.Scale, this.ChatBarWidget.ChatBarContainer.Size.Y.Offset)
																				this.ChatBarWidget.ChatBarContainer.Position = this.ChatWindowWidget.ChatContainer.Position + UDim2.new(0, 0, this.ChatWindowWidget.ChatContainer.Size.Y.Scale, this.ChatWindowWidget.ChatContainer.Size.Y.Offset + 2)
 																			end
 																		end
 																	end)

			else
				game.StarterGui:RegisterSetCore("ChatWindowPosition", 	function() end)
				game.StarterGui:RegisterSetCore("ChatWindowSize",		function() end)
			end

 			game.StarterGui:RegisterGetCore("ChatWindowPosition", 	function()
 																		if this.ChatWindowWidget then
 																			return this.ChatWindowWidget.ChatContainer.Position
 																		else
 																			return nil
 																		end
																	end)

 			game.StarterGui:RegisterGetCore("ChatWindowSize", 	function()
																	if this.ChatWindowWidget then
																		return this.ChatWindowWidget.ChatContainer.Size
																	else
																		return nil
																	end
																end)

			if allowDisableChatBar then
				game.StarterGui:RegisterSetCore("ChatBarDisabled", 	function(value)
																		if this.ChatBarWidget then
																			if type(value) == "boolean" then
																				chatBarDisabled = value
																				if value == true then
																					this.ChatBarWidget:ToggleVisibility(false)
																				end
																			end
																		end
																	end)
			else
				game.StarterGui:RegisterSetCore("ChatBarDisabled", 	function() end)
			end

 			game.StarterGui:RegisterGetCore("ChatBarDisabled", function() return chatBarDisabled end)
		end

		this:OnPlayerAdded(Player)
		-- Upsettingly, it seems everytime a player is added, you have to redo the connection
		-- NOTE: PlayerAdded only fires on the server, hence ChildAdded is used here
		PlayersService.ChildAdded:connect(function(child)
			if child:IsA('Player') then
				this:OnPlayerAdded(child)
			end
		end)
		this:CreateGUI()


		this:CoreGuiChanged(Enum.CoreGuiType.Chat, StarterGui:GetCoreGuiEnabled(Enum.CoreGuiType.Chat))
		this.CoreGuiChangedConn = Util.DisconnectEvent(this.CoreGuiChangedConn)
		pcall(function()
			this.CoreGuiChangedConn = StarterGui.CoreGuiChangedSignal:connect(
				function(coreGuiType,enabled)
					this:CoreGuiChanged(coreGuiType, enabled)
				end)
		end)

		if not NON_CORESCRIPT_MODE then
			this:PrintWelcome()
		end

		--SetVisbility(true)
	end

	return this
end

local moduleApiTable = {}
-- Main Entry Point
do
	moduleApiTable.ModuleName = thisModuleName
	moduleApiTable.KeepVRTopbarOpen = true 
	moduleApiTable.VRIsExclusive = true
	moduleApiTable.VRClosesNonExclusive = false
	VRHub:RegisterModule(moduleApiTable)

	VRHub.ModuleOpened.Event:connect(function(moduleName)
		if moduleName ~= thisModuleName then
			local module = VRHub:GetModule(moduleName)
			if module.VRIsExclusive then
				moduleApiTable:SetVisible(false)
			end
		end
	end)

	local ChatInstance = CreateChat()
	ChatInstance:Initialize()

	function moduleApiTable:ToggleVisibility()
		ChatInstance:ToggleVisibility()
	end

	function moduleApiTable:SetVisible(visible)
		ChatInstance:SetVisible(visible)
	end

	function moduleApiTable:FocusChatBar()
		ChatInstance:FocusChatBar()
	end

	function moduleApiTable:GetVisibility()
		return ChatInstance.Visible
	end

	function moduleApiTable:GetMessageCount()
		return ChatInstance:GetCurrentWindowMessageCount()
	end

	function moduleApiTable:TopbarEnabledChanged(...)
		return ChatInstance:TopbarEnabledChanged(...)
	end

	function moduleApiTable:IsFocused(useWasFocused)
		return ChatInstance:IsFocused(useWasFocused)
	end

	moduleApiTable.ChatBarFocusChanged = ChatInstance.ChatBarFocusChanged
	moduleApiTable.VisibilityStateChanged = ChatInstance.VisibilityStateChanged
	moduleApiTable.MessagesChanged = ChatInstance.CurrentWindowMessageCountChanged

end

return moduleApiTable

