FlatChat

Better chat for FlatMMO

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         FlatChat
// @namespace    com.dounford.flatmmo.flatChat
// @version      2.4.2
// @description  Better chat for FlatMMO
// @author       Dounford
// @license      MIT
// @match        *://flatmmo.com/play.php*
// @grant        none
// @require      https://update.greasyfork.org/scripts/544062/FlatMMOPlus.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/anchorme/2.1.2/anchorme.min.js
// ==/UserScript==

(function() {
	'use strict';

	//Which css variable corresponds to each color
	const messageColors = {
		white: "messagesColor",//local message
		grey: "messagesColor", //global message
		server: "serverMessages",
		pink: "milestoneMessages", //server messages
		red: "warningMessages", //errors (trade declines, no energy)
		lime: "restMessages", //rest message
		green: "lvlUpMessages", //level up
		cyan: "areaChangeMessages", //Leaving/Entering town
		pmReceived: "pmReceivedMessages", //private messages
		pmSent: "pmSentMessages",
		gold: "pingMessages",
	};
	const ding = new Audio("https://github.com/Dounford-Felipe/DHM-Idle-Again/raw/refs/heads/main/ding.wav");
	const IPSigils = new Set([
		'images/ui/basket_egg_sigil.png', 'images/ui/basket_sigil.png', 'images/ui/bat_sigil.png', 'images/ui/bell_sigil.png', 'images/ui/blue_party_hat_sigil.png', 'images/ui/broken_bell_sigil.png', 'images/ui/bronze_event_2_sigil.png', 'images/ui/bronze_event_sigil.png', 'images/ui/bunny_sigil.png', 'images/ui/candy_cane_sigil.png', 'images/ui/carrot_sigil.png', 'images/ui/cat_sigil.png', 'images/ui/chocolate_sigil.png', 'images/ui/dh1_max_sigil.png', 'images/ui/easter_egg_sigil.png', 'images/ui/event_2_sigil.png', 'images/ui/event_sigil.png', 'images/ui/fake_bell_sigil.png', 'images/ui/fancy_bell_sigil.png', 'images/ui/ghost_sigil.png', 'images/ui/gift_sigil.png', 'images/ui/gold_event_2_sigil.png', 'images/ui/gold_event_sigil.png', 'images/ui/green_party_hat_sigil.png', 'images/ui/hatching_chicken_sigil.png', 'images/ui/mad_bunny_sigil.png', 'images/ui/mummy_head_sigil.png', 'images/ui/mummy_sigil.png', 'images/ui/pink_party_hat_sigil.png', 'images/ui/pumpkin_sigil.png', 'images/ui/red_party_hat_sigil.png', 'images/ui/reindeer_sigil.png', 'images/ui/santa_hat_sigil.png', 'images/ui/silver_event_2_sigil.png', 'images/ui/silver_event_sigil.png', 'images/ui/skull_sigil.png', 'images/ui/snowflake_sigil.png', 'images/ui/snowman_sigil.png', 'images/ui/spider_sigil.png', 'images/ui/tree_sigil.png', 'images/ui/white_party_hat_sigil.png', 'images/ui/yellow_party_hat_sigil.png', 'images/ui/zombie_sigil.png'
	])
	const defaultThemes = {
		dark: {
            chatBackground: "#191b24",
            /*Top bar*/
            topBarBackground: "#131c37",
            tabsBackground: "#393a5b",
            activeTabBackground: "#4357af",
            hoverTabBackground: "#D3D3D3",
            tabsTextColor: "#e7e7e7",
            unreadMessagesBackground: "#000090",
            unreadMessagesTextColor: "#e7e7e7",
            /*Channels*/
            oddMessageBackground: "#191b24",
            evenMessageBackground: "#191b24",
            messageTimeColor: "#ffffff",
            messageSenderUsernameColor: "#ffffff",
            regularMessageColor: "#E1E1E1",
            serverMessageColor: "#6495ED",
            milestoneMessageColor: "#FF1493",
            warningMessageColor: "#FF0000",
            restMessageColor: "#00FF00",
            lvlUpMessageColor: "#008000",
            areaChangeMessageColor: "#00FFFF",
            pmReceivedMessageColor: "#ffdea1",
            pmSentMessageColor: "#78ffb5",
            pingBackground: "#3F51B5",
            pingTextColor: "#ffffff",
			tooltipBorder: "#808080",
			tooltipBackground: "#aaaaaa",
			tooltipTextColor: "#000000",
            /*Bottom bar*/
            chatBarBackground: "#131419",
            usernameBottomBar: "#C0C0C0",
            chatBarTextColor: "#ffffff",
            buttonsBackground: "#000090",
            buttonsTextColor: "#C0C0C0",
            /*Context Menu*/
            contextMenuBackground: "#191b24",
            contextMenuUsernameColor: "#C0C0C0",
            contextMenuButtonBackground: "#000090",
            contextMenuWarningButtonBackground: "#ff0000",
            contextMenuTextColor: "#ffffff",
            /*Misc*/
            hyperlinkTextColor: "#00FFFF",
            visitedHyperlinkTextColor: "#00FFFF",
        },
		light: {
			chatBackground: "#F5F7FA",
			/*Top bar*/
			topBarBackground: "#E3E8F0",
			tabsBackground: "#D1DAE6",
			activeTabBackground: "#4A90E2",
			hoverTabBackground: "#B8C7D9",
			tabsTextColor: "#2C3E50",
			unreadMessagesBackground: "#FF6B6B",
			unreadMessagesTextColor: "#FFFFFF",
			/*Channels*/
			oddMessageBackground: "#F5F7FA",
			evenMessageBackground: "#FFFFFF",
			messageTimeColor: "#7F8C8D",
			messageSenderUsernameColor: "#2C3E50",
			regularMessageColor: "#34495E",
			serverMessageColor: "#3498DB",
			milestoneMessageColor: "#9B59B6",
			warningMessageColor: "#E74C3C",
			restMessageColor: "#27AE60",
			lvlUpMessageColor: "#2ECC71",
			areaChangeMessageColor: "#1ABC9C",
			pmReceivedMessageColor: "#E67E22",
			pmSentMessageColor: "#D35400",
			pingBackground: "#F1C40F",
			pingTextColor: "#2C3E50",
			tooltipBorder: "#BDC3C7",
			tooltipBackground: "#ECF0F1",
			tooltipTextColor: "#2C3E50",
			/*Bottom bar*/
			chatBarBackground: "#E3E8F0",
			usernameBottomBar: "#7F8C8D",
			chatBarTextColor: "#2C3E50",
			buttonsBackground: "#4A90E2",
			buttonsTextColor: "#FFFFFF",
			/*Context Menu*/
			contextMenuBackground: "#FFFFFF",
			contextMenuUsernameColor: "#34495E",
			contextMenuButtonBackground: "#4A90E2",
			contextMenuWarningButtonBackground: "#E74C3C",
			contextMenuTextColor: "#2C3E50",
			/*Misc*/
			hyperlinkTextColor: "#2980B9",
			visitedHyperlinkTextColor: "#8E44AD",
		},
		mocha: {
			chatBackground: "#1e1e2e",
			/*Top bar*/
			topBarBackground: "#181825",
			tabsBackground: "#1e1e2e",
			activeTabBackground: "#45475a",
			hoverTabBackground: "#313244",
			tabsTextColor: "#cdd6f4",
			unreadMessagesBackground: "#313244",
			unreadMessagesTextColor: "#cdd6f4",
			/*Channels*/
			oddMessageBackground: "#1e1e2e",
			evenMessageBackground: "#1e1e2e",
			messageTimeColor: "#cdd6f4",
			messageSenderUsernameColor: "#cdd6f4",
			regularMessageColor: "#cdd6f4",
			serverMessageColor: "#b4befe",
			milestoneMessageColor: "#f5c2e7",
			warningMessageColor: "#f38ba8",
			restMessageColor: "#a6e3a1",
			lvlUpMessageColor: "#94e2d5",
			areaChangeMessageColor: "#89dceb",
			pmReceivedMessageColor: "#fab387",
			pmSentMessageColor: "#f9e2af",
			pingBackground: "#313244",
			pingTextColor: "#cdd6f4",
			tooltipBorder: "#11111b",
			tooltipBackground: "#313244",
			tooltipTextColor: "#cdd6f4",
			/*Bottom bar*/
			chatBarBackground: "#11111b",
			usernameBottomBar: "#cdd6f4",
			chatBarTextColor: "#cdd6f4",
			buttonsBackground: "#1e1e2e",
			buttonsTextColor: "#c0c0c0",
			/*Context Menu*/
			contextMenuBackground: "#191b24",
			contextMenuUsernameColor: "#cdd6f4",
			contextMenuButtonBackground: "#191b24",
			contextMenuWarningButtonBackground: "#f38ba8",
			contextMenuTextColor: "#cdd6f4",
			/*Misc*/
			hyperlinkTextColor: "#cba6f7",
			visitedHyperlinkTextColor: "#cba6f7"
		}
	}
	const themesText = {
		chatBackground: "Chat Background",
		/*Top bar*/
		topBarBackground: "Top Bar Background",
		tabsBackground: "Tabs Background",
		activeTabBackground: "Active Tab Background",
		hoverTabBackground: "Hover Tab Background",
		tabsTextColor: "Tabs Text",
		unreadMessagesBackground: "Unread Background",
		unreadMessagesTextColor: "Unread Text",
		/*Channels*/
		oddMessageBackground: "Odd Messages Background",
		evenMessageBackground: "Even Messages Background",
		messageTimeColor: "Message Time Color (#fff = unset)",
		messageSenderUsernameColor: "Sender Color (#fff = unset)",
		regularMessageColor: "Regular Message Color",
		serverMessageColor: "Server Message",
		milestoneMessageColor: "Milestone Message",
		warningMessageColor: "Error/Warning Message",
		restMessageColor: "Rest Messages",
		lvlUpMessageColor: "LVL UP Messages",
		areaChangeMessageColor: "Entering/Leaving Town",
		pmReceivedMessageColor: "Private Messages Received",
		pmSentMessageColor: "Private Messages Sent",
		pingBackground: "Ping Messages Background",
		pingTextColor: "Ping Messages Text",
		tooltipBorder: "!lvl Tooltip Border",
		tooltipBackground: "!lvl Tooltip Background",
		tooltipTextColor: "!lvl Tooltip Text",
		/*Bottom bar*/
		chatBarBackground: "Chat Bar Background",
		usernameBottomBar: "Username Color",
		chatBarTextColor: "Chat Text",
		buttonsBackground: "Buttons Background",
		buttonsTextColor: "Buttons Text",
		/*Context Menu*/
		contextMenuBackground: "Context Menu Background",
		contextMenuUsernameColor: "Context Menu Username",
		contextMenuButtonBackground: "Context Menu Button Background",
		contextMenuWarningButtonBackground: "Context Menu Warning Button Background",
		contextMenuTextColor: "Context Menu Text",
		/*Misc*/
		hyperlinkTextColor: "Hyperlink Color",
		visitedHyperlinkTextColor: "Visited Hyperlink Color",
	};
	const chatInset = {
		bottomRight: "auto 0 0 auto",
		bottomLeft: "auto auto 0 auto",
		topRight: "0 0 auto auto",
		topLeft: "0 auto auto"
	}

	const textToNotification = {
		nessieTime: {
			blocked: " seconds left.",
			name: "nessieTime",
			image: "https://flatmmo.com/images/npcs/lochness_monster_stand1.png",
			title: "TIMER",
			text: "0 seconds left",
			ticks: 3600,
			color: "white"
		},
		cannonFixed: {
			blocked: "has fixed the cannon",
			name: "cannonFix",
			image: "https://flatmmo.com/images/objects/cannon1_lower.png",
			title: "CANNON",
			text: "FIXED",
			ticks: 900,
			color: "white"
		},
		cannonBroken: {
			blocked: "The cannon broke",
			name: "cannonFix",
			image: "https://flatmmo.com/images/objects/broken_cannon1_lower.png",
			title: "CANNON",
			text: "BROKEN",
			ticks: 900,
			color: "white"
		},
		fireFish: {
			blocked: "fires fish",
			name: "fishFired",
			image: "https://flatmmo.com/images/items/yellow_fish.png",
			title: "FISH FIRED",
			text: "0/0",
			ticks: 900,
			color: "white"
		},
		bondfirePoints: {
			blocked: "1 bondfire",
			name: "bondfirePoint",
			image: "https://flatmmo.com/images/objects/bondfire1_lower.png",
			title: "BONDFIRE",
			text: "0 points",
			ticks: 900,
			color: "white"
		},
		prepareFire: {
			blocked: "prepare to light",
			name: "ignore",
			image: "https://flatmmo.com/images/items/none.png",
			title: "",
			text: "",
			ticks: 0,
			color: "white"
		},
		lightFire: {
			blocked: "successfully light",
			name: "lightFire",
			image: "https://flatmmo.com/images/objects/fire1_lower.png",
			title: "FIRE",
			text: "0 seconds",
			ticks: 600,
			color: "white"
		},
		feedingTheDead: {
			blocked: "Feeding the Dead",
			name: "feedingTheDead",
			image: "https://flatmmo.com/images/items/bones.png",
			title: "Bones",
			text: "Burried 0 bones",
			ticks: 600,
			color: "white"
		},
		lowEnergy: {
			blocked: "too tired",
			name: "lowEnergy",
			image: "https://flatmmo.com/images/ui/sleep.png",
			title: "TIRED",
			text: "0 energy",
			ticks: 600,
			color: "white"
		},
		dehydration: {
			blocked: "due to dehydration",
			name: "dehydration",
			image: "https://flatmmo.com/images/items/water_bucket.png",
			title: "DEHYDRATION",
			text: "-10 hp",
			ticks: 600,
			color: "white"
		},
		sipOfWater: {
			blocked: "sip of water",
			name: "sipOfWater",
			image: "https://flatmmo.com/images/items/water_bucket.png",
			title: "-1 BUCKET",
			text: "",
			ticks: 600,
			color: "white"
		},
		orbDig: {
			blocked: "start digging",
			name: "orbDig",
			image: "https://flatmmo.com/images/items/none.png",
			title: "",
			text: "",
			ticks: 0,
			color: "white"
		},
		noRunning: {
			blocked: "combat has interrupetd",
			name: "noRunning",
			image: "https://flatmmo.com/images/worship/run.png",
			title: "COMBAT",
			text: "No longer Running",
			ticks: 600,
			color: "white"
		},
		fastBite: {
			blocked: "between bites!",
			name: "fastBite",
			image: "https://flatmmo.com/images/items/raw_tuna.png",
			title: "FAST BITE",
			text: "Wait 3 seconds",
			ticks: 600,
			color: "white"
		},
		alreadyRunning: {
			blocked: "already running",
			name: "alreadyRunning",
			image: "https://flatmmo.com/images/worship/run.png",
			title: "",
			text: "",
			ticks: 0,
			color: "white"
		},
		lavaFishing: {
			blocked: "lava to catch",
			name: "lavaFishing",
			image: "https://flatmmo.com/images/worship/run.png",
			title: "",
			text: "",
			ticks: 0,
			color: "white"
		},
		dodge: {
            blocked: "No Dodges Available.",
            name: "dodge",
            image: "https://flatmmo.com/images/worship/run.png",
            title: "",
            text: "",
            ticks: 0,
            color: "white"
        },
		agility: {
            blocked: "You dodge the attack and gain",
            name: "agility",
            image: "https://flatmmo.com/images/worship/run.png",
            title: "",
            text: "",
            ticks: 0,
            color: "white"
        },
		superstorm: {
            blocked: "storm spirit point",
            name: "superstorm",
            image: "https://flatmmo.com/images/worship/run.png",
            title: "",
            text: "",
            ticks: 0,
            color: "white"
        },
	}

	class flatChatPlugin extends FlatMMOPlusPlugin {
		constructor() {
			super("flatChat", {
				about: {
					name: "FlatChat",
					version: "2.4.2",
					author: "Liam",
					description: "Improved chat for FlatMMO"
				},
				config: [
					{
						id: "ignorePings",
						label: "Silence Pings",
						type: "boolean",
						default: false
					},
					{
						id: "pingVolume",
						label: "Ping Volume",
						type: "range",
						min: 0,
						max: 100,
						step: 1,
						default: 100,
					},
					{
						id: "showTime",
						label: "Mssage Received Time",
						type: "boolean",
						default: true
					},
					{
						id: "showSpam",
						label: "Show Spam Messages",
						type: "boolean",
						default: false
					},
					{
						id: "hideUnwanted",
						label: "Mark Ignored Words as spoiler",
						type: "boolean",
						default: true
					},
					{
						id: "fontSize",
						label: "Message Font Size",
						type: "number",
						min: 0,
						max: 10,
						step: 0.1,
						default: 1.5
					},
					{
						id: "theme",
						label: "Theme",
						type: "select",
						options: [
							{value: "light", label: "Light"},
							{value: "dark", label: "Dark"},
							{value: "mocha", label: "Mocha by Mae"}
						],
						default: "dark"
					},
					{
						id: "chatWidth",
						label: "Chat Width",
						type: "range",
						min: 0,
						max: 100,
						step: 1,
						default: 50,
					},
					{
						id: "chatHeight",
						label: "Chat Height",
						type: "number",
						step: 5,
						default: 190
					},
					{
						id: "chatOpacity",
						label: "Chat Opacity (%)",
						type: "range",
						min: 10,
						max: 100,
						step: 5,
						default: 100,
					},
					{
						id: "maxMessages",
						label: "Max Messages Per Channel (0 = unlimited)",
						type: "number",
						max: 5000,
						step: 50,
						default: 0
					},
					{
						id: "chatPosition",
						label: "Chat Position",
						type: "select",
						options: [
							{value: "detached", label: "Own Window (Detached)"},
							{value: "bottomRight", label: "Bottom Right"},
							{value: "bottomLeft", label: "Bottom Left"},
							{value: "topRight", label: "Top Right"},
							{value: "topLeft", label: "Top Left"},
							{value: "outside", label: "Outside Canvas"},
							{value: "outsideSideTab", label: "Outside Canvas (Side Tabs)"},
						],
						default: "bottomRight"
					},
					{
						id: "profilePage",
						label: "Default Profile",
						type: "select",
						options: [
							{value: "ingame", label: "In-game Profile"},
							{value: "page", label: "Profile Page"},
							{value: "stats", label: "FlatMMO Stats"},
						],
						default: "ingame"
					},
					{
						id: "alwaysOnFocus",
						label: "Keep chat on focus all the time",
						type: "boolean",
						default: false
					},
					{
						id: "pingChat",
						label: "Copy all ping messages on the ping chat",
						type: "boolean",
						default: true
					},
					{
						id: "alwaysTabsPM",
						label: "Always create tabs for PMs",
						type: "boolean",
						default: false
					},
					{
						id: "hideCloseBtn",
						label: "Hide Close Button",
						type: "boolean",
						default: false
					},
					{
						id: "defaultPmChat",
						label: "Default PM Chat Tab",
						type: "select",
						options: [
							{value: "whisper", label: "Whisper"},
							{value: "local", label: "Local"},
							{value: "global", label: "Global"},
						],
						default: "whisper"
					},
					{
						id: "shortcuts",
						label: "Shortcuts List (Use the key between [])",
						type: "object",
						default: {gz: "gratz"},
						key: "Shortcut",
						value: "Message"
					},
					{
						id: "nicknames",
						label: "Nicknames List",
						type: "object",
						default: {dounbot2: "dounbot", felipewolf: "Liam"},
						key: "Username",
						value: "Nickname"
					},
					{
						id: "ignoredPlayers",
						label: "Players Ignored",
						type: "list",
						default: []
					},
					{
						id: "ignoredWords",
						label: "Blocked Words",
						type: "list",
						default: []
					},
					{
						id: "watchedPlayers",
						label: "Watched Players",
						type: "list",
						default: []
					},
					{
						id: "watchedWords",
						label: "Watched Words",
						type: "list",
						default: []
					},
					{
						id: "themeEditor",
						label: "THEME EDITOR",
						panel: "flatChat-ThemeEditor",
						type: "panel"
					}
				]
			});

			this.fcElement = null;
			this.detachedChat = null;

			this.defaultChannels = new Set(["channel_local","channel_global","channel_pings","channel_whisper"])
			this.channels = {};
			this.currentChannel = "channel_local";

			//This is for messages received before the chat loads
			this.messagesWaiting = [];

			//This is for the up and down arrows feature
			this.chatHistory = [];
			this.historyPosition = -1;

			this.ignoreClick = false;

			this.lastPM = ""; //Used for /r

			this.themes = {};

			this.contextActions = {};
		}

		onLogin() {
			this.removeOriginalChat();
			this.addStyle();
			this.addUI();
			this.addContextBtns();
			this.loadChannels();
			this.switchChannel("local", false);
			this.messagesWaiting.forEach((message)=>{
				if(message.username === "") {
					this.showServerMessage(message);
					return
				}
				this.showMessage(message);
			});
			this.defineCommands();
			ding.volume = this.config.pingVolume / 100;
			this.addThemeEditor();
			this.changeThemeEditor();
			FlatMMOPlus.showWarning("Welcome to flatmmo.com", "orange");
			FlatMMOPlus.showWarning(document.querySelectorAll("#chat span")[1].innerHTML, "white");
			FlatMMOPlus.showWarning(`<span><strong style="color:cyan">FYI: </strong> Use the /help command to see information on available chat commands.</span>`, "white");
			this.configurePosition();
		}

		onChat(data) {
			if (data.yell) {
				data.channel = "channel_global";
			} else {
				data.channel = "channel_local";
			}

			if (data.username === "" && data.color === "white") {
				data.color = "server"
			};

			if (data.color === "pink" && data.message.startsWith("[PM")) {
				let match = data.message.match(/\[PM (?:to|from) (.*?)\](.*)/);
				if(match) {
					if(data.message.startsWith("[PM to")) {
						data.color = "pmSent";
					} else {
						data.color = "pmReceived";
					}
					data.username = match[1].trim().replaceAll(" ", "_");
					data.message = match[2].trim();
					if(this.config.ignoredPlayers.includes(data.username)) {
						return
					}
					if(this.config["alwaysTabsPM"] && !this.channels["private_" + data.username]) {
						this.newChannel(data.username, true)
					}
					data.channel = this.channels["private_" + data.username] ? "private_" + data.username : "channel_" + this.config.defaultPmChat;
					this.lastPM = data.username;
				} else {
					console.warn("There was something wrong with this pm:", data.message)
				}
			};

			if(FlatMMOPlus.loggedIn) {
				if(data.username === "") {
					this.showServerMessage(data);
					return
				}
				this.showMessage(data);
			} else {
				this.messagesWaiting.push(data);
			}
		}

		onConfigsChanged() {
			this.changedConfigs.forEach(config => {
				switch (config) {
					case "pingVolume": {
						ding.volume = this.config.pingVolume / 100;
					} break;
					case "fontSize": {
						this.fcElement.querySelector("#flatChatChannels").style.setProperty("--fc-messageFontSize", this.config.fontSize + "rem");
					} break;
					case "theme": {
						this.fcElement.classList.forEach(className => {
							if(className.startsWith("flatChatTheme-")) {
								this.fcElement.classList.remove(className);
							}
						})
						this.fcElement.classList.add("flatChatTheme-" + this.config.theme);
					} break;
					case "pingChat": {
						if(this.config.pingChat) {
							this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']").classList.remove("displaynone")
						} else {
							this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']").classList.add("displaynone")
						}
					} break;
					case "defaultPmChat": {
						if(this.config.defaultPmChat === "whisper") {
							this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']").classList.remove("displaynone")
						} else {
							this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']").classList.add("displaynone")
						}
					} break;
					case "chatWidth": {
						this.fcElement.style.setProperty("--fc-chatWidth", this.config.chatWidth + "%");
					} break;
					case "chatHeight": {
						this.fcElement.style.setProperty("--fc-chatHeight", this.config.chatHeight + "px");
					} break;
					case "chatPosition": {
						this.configurePosition()
					} break;
					case "hideCloseBtn": {
						const closeBtn = this.fcElement.querySelector("#flatChatCloseBtn");
						closeBtn.style.display = this.config.hideCloseBtn ? "none" : "";
					} break;
					case "chatOpacity": {
						this.fcElement.style.setProperty("--fc-chatOpacity", this.config.chatOpacity / 100);
						if(this.config.chatOpacity < 100) {
							this.fcElement.classList.add("flatChatDimmed");
						} else {
							this.fcElement.classList.remove("flatChatDimmed");
						}
					} break;
				}
			})
		}

		saveConfig() {
			localStorage.setItem("flatmmoplus.flatChat.config", JSON.stringify(this.config));
		}

		removeOriginalChat() {
			add_to_chat = function(){};
			refresh_chat_div = function(){};
			paint_chat = function(){};
			document.getElementById("chat").style.display = "none";
			document.querySelector(".chat-input").style.display = "none"

			window.FlatMMOPlus.registerCustomChatCommand(["clear","clean"], (command, data='') => {
				this.fcElement.querySelector(`#flatChatChannels [data-channel=${FlatMMOPlus.plugins.flatChat.currentChannel}]`).innerHTML = "";
			}, `Clears all messages in chat.`);

			const oldLocalMute = add_local_mute;
			add_local_mute = (user) => {
				oldLocalMute(user);
				this.watchIgnorePlayersWords("ignoredPlayers", user);
			}

			FlatMMOPlus.__proto__.showWarning = function(message, color = "aquamarine") {
				const data = {
					username: "",
					tag: "none",
					sigil: "none",
					color: color,
					message: message,
					yell: false,
					channel: FlatMMOPlus.plugins.flatChat.currentChannel
				}
				FlatMMOPlus.plugins.flatChat.showMessage(data, true);
			}

			FlatMMOPlus.deleteHotkey("Enter")
		}

		addStyle() {
			document.querySelector(".td-ui").style.height = "auto"
			const style = document.createElement("style");
			style.innerHTML = `#game>table>tbody>tr:nth-child(2) td {
				position: relative;
			}
			#flatChat {
				position: absolute;
				width: var(--fc-chatWidth, 50%);
				background-color: var(--fc-chatBackground);
				border-radius: 0 0 3% 3%;
				user-select: text;
			}
			#flatChatExpandBtn{
				width: 2rem;
				cursor: pointer;
			}
			#flatChatTopBar {
				background-color: var(--fc-topBarBackground);
				display: flex;
				flex-direction: row;
				justify-content: space-between;
				color: var(--fc-tabsTextColor);
				fill: var(--fc-tabsTextColor);
			}
			#flatChatTabs {
				display: flex;
				flex-direction: row;
				font-size: var(--fc-tabFontSize, 1rem);
			}
			.flatChatTab {
				background-color: var(--fc-tabsBackground);
				position: relative;
				border-radius: 15% 15% 0 0;
				margin-right: 1px;
				padding: 0.375rem;
				&:hover {
					background-color: var(--fc-hoverTabBackground);
					cursor: pointer;
				}
			}
			.flatChatTabActive {
				background-color: var(--fc-activeTabBackground);
			}
			.flatChatUnread {
				position: absolute;
				right: 10%;
				top: 0;
				background-color: var(--fc-unreadMessagesBackground);
				color: var(--fc-unreadMessagesTextColor);
				padding: 0 0.3rem;
				border-radius: 0.5rem;
			}
			.flatChatTopBarCollapsed {
				transform: rotate(180deg);
			}
			#flatChatMainArea[closed] {
				display: none;
			}
			#flatChatChannels {
				height: var(--fc-chatHeight, 150px);
				font-size: var(--fc-messageFontSize, 1rem);
			}
			.flatChatChannel {
				width: 100%;
				height: 100%;
				overflow-y: auto;
				scrollbar-width: thin;
			}
			/*messages*/
			.flatChatChannel div {
				overflow-wrap: anywhere;
				color: var(--fc-regularMessageColor);

				span {
					margin-left: 5px;
				}

				a {
					text-decoration: none;
					color: var(--fc-hyperlinkTextColor) !important;

					&:visited {
						color: var(--fc-visitedHyperlinkTextColor) !important;
					}
				}
			}
			.flatChatChannel div:nth-child(even) {
				background-color: var(--fc-evenMessageBackground, var(--fc-chatBackground));
			}
			.flatChatChannel div:nth-child(odd) {
				background-color: var(--fc-oddMessageBackground, var(--fc-chatBackground));
			}
			.fc-timestamp {
				color: var(--fc-messageTimeColor);
			}
			.fc-playerName {
				color: var(--fc-messageSenderUsernameColor);
			}
			.fc-serverMessages {
				color: var(--fc-serverMessageColor) !important;
			}
			.fc-milestoneMessages {
				color: var(--fc-milestoneMessageColor) !important;
			}
			.fc-warningMessages {
				color: var(--fc-warningMessageColor) !important;
			}
			.fc-restMessages {
				color: var(--fc-restMessageColor) !important;
			}
			.fc-lvlUpMessages {
				color: var(--fc-lvlUpMessageColor) !important;
			}
			.fc-areaChangeMessages {
				color: var(--fc-areaChangeMessageColor) !important;
			}
			.fc-pmReceivedMessages {
				color: var(--fc-pmReceivedMessageColor) !important;
			}
			.fc-pmSentMessages {
				color: var(--fc-pmSentMessageColor) !important;
			}
			.fc-pingMessages {
				background-color: var(--fc-pingBackground) !important;
				color: var(--fc-pingTextColor) !important;
			}
			#flatChatBottomBar {
				display: flex;
			}
			#flatChatInputDiv {
				flex: auto;
				display: flex;
				align-items: center;
				border-radius: 0 0 3% 3%;
				background-color: var(--fc-chatBarBackground);
				color: var(--fc-chatBarTextColor);
			}
			#flatChatInput {
				flex: auto;
				border: 0;
				padding: 0;
				margin: 5px;
				font-size: 18px;
				background-color: transparent;
				color: var(--fc-chatBarTextColor);
				outline: transparent;
				&::placeholder {
					color: var(--fc-usernameBottomBar);
				}
				&::-webkit-search-cancel-button {
					display: none;
				}
			}
			.flatChatBtn {
				background-color: var(--fc-buttonsBackground) !important;
				color: var(--fc-buttonsTextColor) !important;
				border: 0 !important;
				padding: 5px;
				height: 100%;
				display: flex;
				align-self: center;
				align-items: center;
				margin-left: 3px;
			}
			.chatSigil {
				width: var(--fc-messageFontSize, 1rem);
				vertical-align: bottom;
				margin-right: 5px;
			}
			.chatName {
				-webkit-background-clip: text;
				background-clip: text;
				color: transparent;
			}
			.flatChatHidden {
				background-color: black;
				color: transparent;
				a {
					color: transparent;
				}
			}
			.flatChatSideTabs {
				display: flex;
				#flatChatTopBar {
					flex-direction: column;
				}
				#flatChatTabs {
					flex-direction: column;
				}
				#flatChatMainArea {
					width: var(--fc-chatWidth, 50%);
				}
			}
			.flatChatPopup {
				border-radius: 0;
				#flatChatChannels {
					height: calc(100vh - 86px) !important;
				}
			}
			.flatChatLevelTooltip {
				background-color: var(--fc-tooltipBorder, gray) !important;
				div {
					background-color: var(--fc-tooltipBackground, gray) !important;
					color: var(--fc-tooltipTextColor, black) !important;
				}
			}
			#flatChatContextMenu {
				display: flex;
				flex-direction: column;
				align-items: center;
				row-gap: 5px;
				position: absolute;
				padding: 5px;
				background-color: var(--fc-contextMenuBackground) !important;
				border-radius: 5%;
				bottom: 0;
				}
			#flatChatContextMenu button {
				width: 80%;
				color: var(--fc-contextMenuTextColor) !important;
				cursor: pointer;
				font-size: var(--fc-messageFontSize);
				&:hover {
					filter: brightness(0.8);
				}
			}
			#flatChatContextUsername {
				color: var(--fc-contextMenuUsernameColor);
				cursor: text;
				font-size: var(--fc-messageFontSize);
			}
			.flatChatContextBtn {
				background-color: var(--fc-contextMenuButtonBackground) !important;
			}
			.flatChatContextWarningBtn {
				background-color: var(--fc-contextMenuWarningButtonBackground) !important;
			}
			#flatChatCopyUsername {
				visibility: hidden;
				position: absolute;
				top: 0;
				padding: 3px;
				border-radius: 5px;
				background-color: var(--fc-contextMenuBackground);
				color: var(--fc-contextMenuTextColor);
				font-size: var(--fc-messageFontSize);
			}
			#flatChat.flatChatDimmed {
				opacity: var(--fc-chatOpacity, 1);
				transition: opacity 0.2s ease;
			}
			#flatChat.flatChatDimmed:hover,
			#flatChat.flatChatDimmed:focus-within {
				opacity: 1 !important;
			}
			#flatChatResizeHandle {
				height: 6px;
				cursor: ns-resize;
				display: flex;
				justify-content: center;
				align-items: center;
				opacity: 0;
				transition: opacity 0.2s ease;
				background-color: var(--fc-topBarBackground);
			}
			#flatChatResizeHandle:hover {
				opacity: 1;
			}
			#flatChatResizeHandle::after {
				content: "···";
				font-size: 10px;
				color: var(--fc-tabsTextColor, #aaa);
				letter-spacing: 2px;
			}`
			document.head.append(style);

			//Get saved themes
			if(localStorage.getItem("flatChat2-themes")) {
				this.themes = JSON.parse(localStorage.getItem("flatChat2-themes"));
			}

			//Default themes can't be removed
			for (let theme in defaultThemes) {
				if (!this.themes.hasOwnProperty(theme)) {
					this.themes[theme] = defaultThemes[theme];
				}
			}

			for (let theme in this.themes) {
				this.addTheme(theme)
				this.opts.config[6].options = this.opts.config[6].options.filter(t => t.value !== theme)
				this.opts.config[6].options.push({value: theme, label: this.toTitleCase(theme)})
			}
		}

		addUI() {
			this.fcElement = document.createElement("div");
			this.fcElement.innerHTML = `<div id="flatChatTopBar">
				<div id="flatChatTabs"></div>
				<div id="flatChatExpandBtn" style="fill:var(--fc-tabsTextColor, white)">
					<svg id="Layer_1" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="
						height: 30px;
						">
						<path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9  c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3  z"></path>
					</svg>
				</div>
			</div>
			<div id="flatChatMainArea">
				<div id="flatChatResizeHandle"></div>
				<div id="flatChatChannels" style="height: var(--fc-chatHeight, 190px);"></div>
				<div id="flatChatBottomBar">
					<button type="text" id="flatChatCloseBtn" class="flatChatBtn">
						<img src="https://cdn.idle-pixel.com/images/x.png">
					</button>
					<div id="flatChatInputDiv">
						<input type="search" id="flatChatInput" autocomplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" placeholder="">
						<button type="text" id="flatChatSendBtn" class="flatChatBtn">Send</button>
					</div>
					<button type="button" id="flatChatAutoScrollBtn" class="flatChatBtn">
						<img src="https://cdn.idle-pixel.com/images/x.png" id="flatChatAutoScrollfalse" alt="" class="displaynone">
						<img src="https://cdn.idle-pixel.com/images/check.png" id="flatChatAutoScrolltrue" alt="">
					</button>
				</div>
				<div id="flatChatContextMenu" style="visibility:hidden">
					<span id="flatChatContextUsername" data-user="" class="flatChatContextUsername"></span>
				</div>
				<div id="flatChatCopyUsername" style="visibility:hidden">USERNAME COPIED</div>`
			this.fcElement.id = "flatChat";
			this.fcElement.style.inset = "auto 0 0 auto";
			this.fcElement.style.setProperty("--fc-messageFontSize", this.config.fontSize + "rem")
			this.fcElement.style.setProperty("--fc-chatWidth", this.config.chatWidth + "%")
			this.fcElement.style.setProperty("--fc-chatHeight", this.config.chatHeight + "px")
			this.fcElement.style.setProperty("--fc-chatOpacity", this.config.chatOpacity / 100)
			let currentTheme = this.config.theme
			if(this.themes[currentTheme]) {
				this.fcElement.classList = "flatChat flatChatTheme-" + this.config.theme;
			} else {
				this.config.theme = "dark";
				this.fcElement.classList = "flatChat flatChatTheme-dark";
				this.saveConfig();
			}
			if(this.config.chatOpacity < 100) {
				this.fcElement.classList.add("flatChatDimmed");
			}

			document.getElementById("chat").insertAdjacentElement("beforebegin", this.fcElement);

			this.fcElement.querySelector("#flatChatExpandBtn").addEventListener("click", ()=>{
				this.fcElement.querySelector("#flatChatExpandBtn").classList.toggle("flatChatTopBarCollapsed");
				this.fcElement.querySelector("#flatChatMainArea").toggleAttribute("closed");
			})

			const input = this.fcElement.querySelector("#flatChatInput");
			FlatMMOPlus.handler.chatInput = input;
			input.placeholder = Globals.local_username;

			const closeBtn = this.fcElement.querySelector("#flatChatCloseBtn");
			closeBtn.style.display = this.config.hideCloseBtn ? "none" : "";
			closeBtn.onclick = () => this.closeChannel();

			const sendBtn = this.fcElement.querySelector("#flatChatSendBtn");
			sendBtn.onclick = () => this.sendMessage();

			this.fcElement.querySelector("#flatChatAutoScrollBtn").onclick = () => this.toggleAutoScroll();

			// Resize handle drag logic
			const resizeHandle = this.fcElement.querySelector("#flatChatResizeHandle");
			resizeHandle.addEventListener("mousedown", (e) => {
				e.preventDefault();
				const startY = e.clientY;
				const startHeight = this.fcElement.querySelector("#flatChatChannels").offsetHeight;
				const onMouseMove = (e) => {
					const delta = startY - e.clientY;
					const newHeight = Math.max(50, startHeight + delta);
					this.fcElement.style.setProperty("--fc-chatHeight", newHeight + "px");
				};
				const onMouseUp = () => {
					this.fcElement.removeEventListener("mousemove", onMouseMove);
					this.fcElement.removeEventListener("mouseup", onMouseUp);
					const finalHeight = this.fcElement.querySelector("#flatChatChannels").offsetHeight;
					this.config.chatHeight = finalHeight;
					this.saveConfig();
				};
				this.fcElement.addEventListener("mousemove", onMouseMove);
				this.fcElement.addEventListener("mouseup", onMouseUp);
			});

			const channelsDiv = this.fcElement.querySelector("#flatChatChannels");

			channelsDiv.onwheel = (event)=>{
				this.scrollChannel(event);
			}

			//Shows custom context menu on right click
			channelsDiv.addEventListener("contextmenu", (e) => {
				const sender = e.target.closest("[data-sender]");
				if (sender) {
					e.preventDefault()
					const username = sender.dataset.sender;

					const contestUser = this.fcElement.querySelector("#flatChatContextUsername");					
					contestUser.setAttribute("data-user", username);
					contestUser.innerText = username;
					
					const contextMenu = this.fcElement.querySelector("#flatChatContextMenu");
					contextMenu.style.visibility = "visible";
				}
			});

			this.fcElement.querySelector("#flatChatContextMenu").addEventListener("click", (e) => {this.contextMenu(e)})

			this.addDocumentEvents(document);

			this.fcElement.querySelector("#flatChatInput").addEventListener("blur", function(){
				setTimeout(function() {
					if(FlatMMOPlus.plugins.flatChat.config["alwaysOnFocus"] && document.activeElement.tagName !== "INPUT" && document.activeElement.contentEditable !== "true"){
						this.fcElement.querySelector("#flatChatInput").focus({
							preventScroll: true
						})
					}
				}, 1)
			})
		}

		addDocumentEvents(documentElement) {
			//Context menu should close if you click outside
			documentElement.addEventListener("click", (e) => {
				const contextMenu = this.fcElement.querySelector("#flatChatContextMenu");
				if (!contextMenu.contains(e.target)) {
					contextMenu.style.visibility = "hidden";
				}
			});

			documentElement.addEventListener("keydown", (e) => {
				//Switch back and forth between channels with tab and shift+tab
				if (e.key === "Tab" && e.target.closest('#flatChat')) {
					e.preventDefault();
					let sibling = null;
					if(e.shiftKey) {
						sibling = this.fcElement.querySelector(".flatChatTabActive").previousElementSibling || this.fcElement.querySelector("#flatChatTabs").lastElementChild;
					} else {
						sibling = this.fcElement.querySelector(".flatChatTabActive").nextElementSibling || this.fcElement.querySelector("#flatChatTabs").firstElementChild;
					}
					const channel = sibling.dataset.channel
					const match = channel.match(/(.*?)_(.*)/);
					if (match) {
						const type = match[1];
						const channel = match[2];
						this.switchChannel(channel, type === "private")
					}
					return;
				}
				const input = this.fcElement.querySelector("#flatChatInput");
				if(documentElement.activeElement === input) {
					if(e.key === "Enter") {
						if(has_modal_open()) return;
						this.sendMessage();
					}

					//Navigate between sent messages
					if (e.key === "ArrowUp" && input.selectionStart === 0) {
						if(this.historyPosition + 1 === this.chatHistory.length) {return}
						input.value = this.chatHistory[++this.historyPosition] || "";
						input.selectionStart = input.value.length
					} else if (e.key === "ArrowDown" && input.selectionStart === input.value.length) {
						if(this.historyPosition - 1 < -1) {return}
						input.value = this.chatHistory[--this.historyPosition] || "";
						input.selectionStart = input.value.length
					}
				} else if(e.key === "Enter") {
					if(has_modal_open()) return;
					input.focus({
						preventScroll: true
					})
				}
			}, true)


			documentElement.querySelector("#flatChatChannels").addEventListener("click", async (e) => {
				const sender = e.target.closest("[data-sender]");
				if (sender) {
					const username = sender.dataset.sender;
					//It will try to copy with navigator.clipboard, but it doesn't work if the window is not focused
					try {
						await documentElement.defaultView.navigator.clipboard.writeText(username.replaceAll("_", " "));
					} catch (error) {
						
					}

					const copyMessage = this.fcElement.querySelector("#flatChatCopyUsername");
					copyMessage.style.visibility = "visible"
					setTimeout(()=>{copyMessage.style.visibility = "hidden"}, 1000);
				}
			});
		}

		addContextBtns() {
			this.addContextButton("message", "Message", (username) => {
				const input = this.fcElement.querySelector("#flatChatInput");
				input.value = "/pm " + username + " ";
				input.focus();
			}, false);
			this.addContextButton("tabMessage", "Message (Tab)", (username) => {
				this.newChannel(username, true);
				this.switchChannel(username, true);
			}, false);
			this.addContextButton("profile", "Profile", (username) => {
				switch(this.config.profilePage) {
					case "ingame": {
						Globals.websocket.send("RIGHT_CLICKED_PLAYER=" + username.replaceAll("_", " "));
					} break;
					case "page": {
						window.open(`https://flatmmo.com/profile/?user=${username.replaceAll("_", " ")}`, '_blank');
					} break;
					case "stats": {
						window.open(`https://flatstats.ravenwoodsoftware.org/player/${username.replaceAll("_", " ")}`, '_blank');
					} break;
				}
			}, false);
			this.addContextButton("trade", "Trade", (username) => {
				Globals.websocket.send("SEND_TRADE_REQUEST=" + username.replaceAll("_", " "));
			}, false);
			this.addContextButton("stalk", "Stalk", (username) => {
				this.watchIgnorePlayersWords("watchedPlayers", username);
			}, true);
			this.addContextButton("ignore", "Ignore", (username) => {
				this.watchIgnorePlayersWords("ignoredPlayers", username);
			}, true);
		}

		configurePosition() {
			if(this.config.chatPosition === "detached") {
				this.detach();
				return;
			} else if(this.detachedChat) {
				this.reattach();
			}

			if(this.config.chatPosition === "outsideSideTab") {
				flatChat.classList.add("flatChatSideTabs");
				flatChat.style.position = "unset";
			} else {
				flatChat.classList.remove("flatChatSideTabs");
				if(this.config.chatPosition === "outside") {
					flatChat.style.position = "unset";
				} else {
					flatChat.style.position = "absolute";
					flatChat.style.inset = chatInset[this.config.chatPosition];
				}
			}
		}

		detach() {
			if(this.detachedChat && !this.detachedChat.closed) {
				this.detachedChat.focus();
				return;
			}
			this.detachedChat = window.open(
				"",
				"chatPopup",
				"width=600,height=800,resizable=yes"
			);

			this.fcElement.classList.add("flatChatPopup");
			this.detachedChat.document.head.insertAdjacentHTML("beforeend","<title>Flat Chat</title>")
			document.head.querySelectorAll("style").forEach(style => {
				this.detachedChat.document.head.appendChild(style.cloneNode(true));
			})
			document.head.querySelectorAll("link").forEach(link => {
				this.detachedChat.document.head.appendChild(link.cloneNode(true));
			})
			this.detachedChat.document.body.appendChild(this.fcElement);
			this.addDocumentEvents(this.detachedChat.document);

			this.detachedChat.onbeforeunload = () => {
				this.config.chatPosition = "bottomRight";
				this.saveConfig();
				this.reattach();
			}

			window.onbeforeunload = () => {
				if(this.detachedChat && !this.detachedChat.closed) {
					this.detachedChat.close();
					this.config.chatPosition = "detached";
					this.saveConfig();
				}
			}
		}

		reattach() {
			this.detachedChat?.close();
			this.detachedChat = null;
			document.getElementById("chat").insertAdjacentElement("beforebegin", this.fcElement)
			this.fcElement.classList.remove("flatChatPopup");
		}

		addTheme(theme) {
			if(!this.themes[theme]) {return};
			let themeSyle;
			if(document.querySelector("#fc-themeStyle-" + theme)) {
				themeSyle = document.querySelector("#fc-themeStyle-" + theme)
				themeSyle.innerHTML = "";
			} else {
				themeSyle = document.createElement("style");
				themeSyle.id = "fc-themeStyle-" + theme;
				document.head.appendChild(themeSyle);
			}
			themeSyle.innerHTML = `.flatChatTheme-${theme} {`
			for (let option in this.themes[theme]) {
				themeSyle.innerHTML += `\n\t--fc-${option}: ${this.themes[theme][option]};`
			}
			themeSyle.innerHTML += "\n}"
		}

		addThemeEditor() {
			FlatMMOPlus.addPanel("flatChat-ThemeEditor", "Theme Editor", ()=>{
				let content = `
				<style>
					#ui-panel-flatChat-ThemeEditor-content {
						display: grid;
						grid-template-columns: auto auto;
						font-size: larger;
						height: 550px;
						overflow-y: scroll;

						* {
							border-top: solid 1px black;
							margin-bottom: 5px;
							padding: 5px;
						}

						select {
							grid-column: 1 / span 2;
							text-align: center;
							font-size: large;
						}

						input[type="color"] {
							width: 50px;
							height: 50px;
						}
					}
				</style>
				<select id="flatChat-ThemeEditor-theme" onchange="FlatMMOPlus.plugins.flatChat.changeThemeEditor()">`
				FlatMMOPlus.plugins.flatChat.opts.config[6].options.forEach(theme=>{
					content += `<option value="${theme.value}">${theme.label}</option>`
				})
				content += "</select>"

				for (let option in themesText) {
					content += `<label for="fc-${option}-editor">${themesText[option]}</label>
					<input type="color" id="fc-${option}-editor">`
				}

				content += `<div style="display: grid;grid-template-columns: auto auto;grid-column: 1 / span 2;">
					<input type="text" id="fc-themeName-editor" placeholder="Theme Name" style="grid-column: 1 / span 2;">
					<button type="button" onclick="FlatMMOPlus.plugins.flatChat.saveTheme()">Save</button>
					<button type="button" onclick="FlatMMOPlus.plugins.flatChat.deleteTheme()">Delete Theme</button>

					<input type="text" id="fc-import-editor" placeholder="Import/Export" style="grid-column: 1 / span 2;">
					<button type="button" onclick="FlatMMOPlus.plugins.flatChat.importTheme()">Import</button>
					<button type="button" onclick="FlatMMOPlus.plugins.flatChat.exportTheme()">Export</button>
				</div>
				`
				return content;
			})

		}

		changeThemeEditor() {
			const theme = document.getElementById("flatChat-ThemeEditor-theme").value;
			document.getElementById("fc-themeName-editor").value = document.querySelector(`#flatChat-ThemeEditor-theme option[value=${theme}]`).innerText;
			for (let option in this.themes[theme]) {
				document.getElementById("fc-" + option + "-editor").value = this.themes[theme][option]
			}
		}

		saveTheme() {
			const theme = document.getElementById("fc-themeName-editor").value;
			if(!theme) {return};
			const themeName = this.toCamelCase(theme);

			//Make sure it doesn't duplicate
			if(this.themes[themeName]) {
				this.opts.config[6].options = this.opts.config[6].options.filter(t => t.value !== themeName)
				document.querySelector(`#flatChat-ThemeEditor-theme option[value=${themeName}]`).remove();
			} else {
				this.themes[themeName] = {};
			}

			for (let option in this.themes.dark) {
				const value = document.getElementById("fc-" + option + "-editor").value;
				this.themes[themeName][option] = value;
				if((option === "messageTimeColor" || option === "messageSenderUsernameColor") && value == "#ffffff") {
					this.themes[themeName][option] = "unset";
				}
			}


			this.opts.config[6].options.push({value: themeName, label: theme})

			document.getElementById("flatChat-ThemeEditor-theme").innerHTML += `<option value="${themeName}">${theme}</option>`
			document.getElementById("flatChat-ThemeEditor-theme").value = themeName;

			//Change to new theme
			this.config.theme =  themeName;
			const flatChat = document.getElementById("flatChat");
			flatChat.classList = "flatChat flatChatTheme-" + this.config.theme;
			this.saveConfig();

			localStorage.setItem("flatChat2-themes", JSON.stringify(this.themes));

			this.addTheme(themeName);
		}

		deleteTheme() {
			const theme = document.getElementById("flatChat-ThemeEditor-theme").value;
			console.log(theme)

			//Return if it doesn't exist
			if(!this.themes[theme]) {return};

			//Default themes can't be removed, they will be go back to default instead
			if(theme in defaultThemes)  {
				this.themes[theme] = structuredClone(defaultThemes[theme]);
				this.changeThemeEditor();
				this.saveTheme();
				return;
			};

			//remove from themes
			delete this.themes[theme];

			//remove from fm+ config
			this.opts.config[6].options = this.opts.config[6].options.filter(t => t.value !== theme);

			//Remove the option on theme editor
			document.querySelector(`#flatChat-ThemeEditor-theme option[value=${theme}]`).remove();

			//save themes on localstorage
			localStorage.setItem("flatChat2-themes", JSON.stringify(this.themes));

			//If there is a theme style (it should exist) remove it
			if (document.getElementById("fc-themeStyle-" + theme)) {
				document.getElementById("fc-themeStyle-" + theme).remove();
			}

			this.config.theme =  "dark";
			const flatChat = document.getElementById("flatChat");
			flatChat.classList = "flatChat flatChatTheme-dark";
		}

		importTheme() {
			const themeString = document.getElementById("fc-import-editor").value;
			try {
				const themeObj = JSON.parse(themeString);
				if(!themeObj.name) {return};
				document.getElementById("fc-themeName-editor").value = this.toTitleCase(themeObj.name);
				for (let option in themeObj.theme) {
					document.getElementById("fc-" + option + "-editor").value = themeObj.theme[option]
				}
				this.saveTheme()
			} catch (error) {
				console.error("What you are trying to import is not a valid theme");
			}
		}

		exportTheme() {
			const theme = document.getElementById("flatChat-ThemeEditor-theme").value;
			const themeString = JSON.stringify({name: theme, "theme": this.themes[theme]});
			document.getElementById("fc-import-editor").value = themeString;
		}

		defineCommands() {
			window.FlatMMOPlus.registerCustomChatCommand(["players","who"], (command, data='') => {
				if (this.currentChannel === "channel_global") {
					Globals.websocket.send('CHAT=/players');
				} else if (this.currentChannel === "channel_local") {
					FlatMMOPlus.showWarning(Object.keys(players).join(", "), "white");
				} else if (this.currentChannel.startsWith("private_")) {
					FlatMMOPlus.showWarning(`${this.currentChannel.slice(8)} & ${Globals.local_username}`, "white");
				}
			}, `Show all players in room or global.`);

			window.FlatMMOPlus.registerCustomChatCommand("detach", (command, data='') => {
				if(this.config.chatPosition === "detached") {
					this.config.chatPosition = "bottomRight";
					this.saveConfig();
					this.reattach();
				} else {
					this.config.chatPosition = "detached";
					this.saveConfig();
					this.detach();
				}
			}, "Detach/Reattach the chat from the window");

			//Pm will only open a tab if a message is not specified
			window.FlatMMOPlus.registerCustomChatCommand("pm", (command, data='') => {
				if (data === "") {
					FlatMMOPlus.showWarning("You need to specify the username", "red");
					return;
				}
				const space = data.indexOf(" ");
				if (space <= 0) {
					this.newChannel(data, true);
					this.switchChannel(data, true);
				} else {
					const receiver = data.substring(0, space);
					const message = data.substring(space + 1);

					Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
				}
			}, `Send a private message to someone.<br><strong>Usage:</strong> /pm [username] [message]`);

			window.FlatMMOPlus.registerCustomChatCommand("r", (command, data='') => {
				if (this.lastPM === "") {
					return
				}
				if (data === "") {
					this.newChannel(this.lastPM, true);
					this.switchChannel(this.lastPM, true);
					return;
				}
				const receiver = this.lastPM;
				const message = data;
				Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
			}, `Auto respond the last pm.`);

			//pm* will always open a new tab
			window.FlatMMOPlus.registerCustomChatCommand("pm*", (command, data='') => {
				if (data === "") {
					FlatMMOPlus.showWarning("You need to specify the username", "red");
					return;
				}
				const space = data.indexOf(" ");
				if (space <= 0) {
					this.newChannel(data, true);
					this.switchChannel(data, true);
				} else {
					const receiver = data.substring(0, space);
					const message = data.substring(space + 1);
					this.newChannel(receiver, true);
					this.switchChannel(receiver, true);
					Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
				}
			}, `Opens a private channel in a new tab.<br><b>Usage:</b>/pm* [username] <message (optional)>`);

			window.FlatMMOPlus.registerCustomChatCommand("page", (command, data='') => {
				if (data === "") {
					window.open(`https://flatstats.ravenwoodsoftware.org/player/${Globals.local_username}`,"_blank")
					return;
				}
				window.open(`https://flatstats.ravenwoodsoftware.org/player/${data}`,"_blank");
			}, `Opens the Player Profile Page in a new tab.<br><b>Usage:</b>/page [username]`);

			window.FlatMMOPlus.registerCustomChatCommand("profile", (command, data='') => {
				if (data === "") {
					FlatMMOPlus.showWarning("You need to specify the username", "red");
					return;
				}
				Globals.websocket.send("RIGHT_CLICKED_PLAYER=" + data.replaceAll("_", " "));
			}, `Opens the player profile.<br><b>Usage:</b>/profile [username]`);

			window.FlatMMOPlus.registerCustomChatCommand("leave", (command, data='') => {
				if (data === "") {
					this.closeChannel();
					return;
				};
				if(data in this.channels) {
					this.closeChannel(data);
				};
			}, `Closes a chat tab.<br><b>Usage:</b>/leave <channel (optional)>`);
		}

		newChannel(channel, isPrivate) {
			const channelName = (isPrivate ? "private_" : "channel_") + channel;
			if(this.channels[channelName]) {return};
			this.channels[channelName] = {
				name: channel,
				isPrivate,
				autoScroll: true,
				unreadMessages: 0,
				inputText: "",
				lastMessage: "",
				lastSender: "",
			}
			const tabBtn = document.createElement("div");
			tabBtn.className = "flatChatTab";
			tabBtn.setAttribute("data-channel", channelName);
			tabBtn.innerHTML = `<span class="flatChatTabName">${channel.replaceAll("_", " ")}</span>
				<div class="flatChatUnread"></div>`
			this.fcElement.querySelector("#flatChatTabs").appendChild(tabBtn);

			tabBtn.onclick = () => {
				this.switchChannel(channel, isPrivate);
			}

			this.fcElement.querySelector("#flatChatChannels").insertAdjacentHTML("beforeend",`<div class="flatChatChannel" data-channel="${channelName}" style="display:none"></div>`);
			if(isPrivate) {
				const privateChannel = this.fcElement.querySelector(`#flatChatChannels [data-channel=${channelName}]`);
				privateChannel.insertAdjacentHTML("beforeend",`
				<div class="fc-lvlUpMessages"><strong>${this.getDateStr()}</strong><span>New chat with ${channel}</span></div>`)
				this.fcElement.querySelectorAll(`[data-channel=${this.config.defaultPmChat}] .fc-pmReceivedMessages,.fc-pmSentMessages`).forEach(el => {
				const match = el.innerText.match(/(?:>|<) (?:\[[0-9][0-9]:[0-9][0-9]])?(.*?):/)
					if(match && match[1] === channel) {
						privateChannel.appendChild(el);
					}
				})
			}
			this.saveChannels();
		}

		closeChannel(channel) {
			const oldChannel = channel || this.currentChannel;
			if(this.defaultChannels.has(oldChannel)) {
				return;
			}

			this.switchChannel("local", false);

			delete this.channels[oldChannel];
			this.fcElement.querySelector(`#flatChatTabs [data-channel=${oldChannel}]`).remove();
			this.fcElement.querySelector(`#flatChatChannels [data-channel=${oldChannel}]`).remove();

			this.saveChannels();
		}

		saveChannels() {
			const channels = Object.keys(this.channels);
			localStorage.setItem("flatChat-channels",JSON.stringify(channels))
		}

		loadChannels() {
			const channels = JSON.parse(localStorage.getItem("flatChat-channels") || '["channel_local","channel_global","channel_pings","channel_whisper"]');

			//It should not be possible to remove default channels, but just in case
			this.defaultChannels.forEach(channel => {
				if(!channels.includes(channel)) {
					channels.push(channel)
				}
			})
			channels.forEach(channel => {
				const match = channel.match(/(.*?)_(.*)/);
				if (match) {
					const type = match[1];
					const name = match[2];
					this.newChannel(name, type === "private");
				}
			})

			if(this.config.pingChat) {
				this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']")?.classList.remove("displaynone")
			} else {
				this.fcElement.querySelector(".flatChatTab[data-channel='channel_pings']")?.classList.add("displaynone")
			}
			if(this.config.defaultPmChat === "whisper") {
				this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']")?.classList.remove("displaynone")
			} else {
				this.fcElement.querySelector(".flatChatTab[data-channel='channel_whisper']")?.classList.add("displaynone")
			}

			this.fcElement.querySelector(`.flatChatChannel[data-channel=channel_pings]`).addEventListener("click", async (e) => {
				const message = e.target.closest("[data-mid]");
				if (message) {
					const id = message.dataset.mid;
					const channelName = message.dataset.channelname;
					const isPrivate = channelName.slice(0,7) === "private";
					const channel = channelName.slice(8);

					this.switchChannel(channel, isPrivate)
					this.fcElement.querySelector(`[data-omid="${id}"]`)?.scrollIntoView();
				}
			});
		}

		switchChannel(channel, isPrivate) {
			const input = this.fcElement.querySelector("#flatChatInput");
			//remove old
			this.fcElement.querySelector(`.flatChatTab[data-channel=${this.currentChannel}]`).classList.remove("flatChatTabActive");
			this.fcElement.querySelector(`.flatChatChannel[data-channel=${this.currentChannel}]`).style.display = "none";
			this.channels[this.currentChannel].inputText = input.value;

			//update current chat
			const newChannel = (isPrivate ? "private_" : "channel_") + channel
			//Makes sure the channel exists
			if (!this.channels[newChannel]) {
				newChannel = "channel_local"
			};
			this.currentChannel = newChannel;

			//Removes unreadMessages number
			this.channels[this.currentChannel].unreadMessages = 0;
			const unreadDiv = this.fcElement.querySelector(`.flatChatTab[data-channel=${this.currentChannel}] .flatChatUnread`);
			unreadDiv.style.visibility = "hidden";

			//Change auto scroll icon
			const autoScroll = this.channels[this.currentChannel].autoScroll;
			this.fcElement.querySelector("#flatChatAutoScroll" + autoScroll).className = "";
			this.fcElement.querySelector("#flatChatAutoScroll" + !autoScroll).className = "displaynone";

			//shows the new chat
			this.fcElement.querySelector(`.flatChatTab[data-channel=${this.currentChannel}]`).classList.add("flatChatTabActive");
			const messageArea = this.fcElement.querySelector(`.flatChatChannel[data-channel=${this.currentChannel}]`);
			messageArea.style.display = "block";
			input.value = this.channels[this.currentChannel].inputText;

			//Auto scrolls if needed
			if (this.channels[this.currentChannel].autoScroll) {
				messageArea.scrollTop = messageArea.scrollHeight;
			}
		}

		scrollChannel(e) {
			//Zoom in/out chat messages
			if (e.shiftKey) {
				let size = this.config.fontSize || 1
				if (e.deltaY < 0) {
					size = parseFloat((size + 0.1).toFixed(1));
				} else {
					size = parseFloat((size - 0.1).toFixed(1));
				}
				this.fcElement.style.setProperty("--fc-messageFontSize", size + "rem")
				this.config.fontSize = size;
				this.saveConfig();
				return;
			}
		}

		toggleAutoScroll() {
			this.channels[this.currentChannel].autoScroll = !this.channels[this.currentChannel].autoScroll;
			this.fcElement.querySelector("#flatChatAutoScrolltrue").classList.toggle("displaynone");
			this.fcElement.querySelector("#flatChatAutoScrollfalse").classList.toggle("displaynone");
		}

		watchIgnorePlayersWords(type, word) {
			//type can be watchedWords, ignoredWords, watchedPlayers, ignoredPlayers
			//Warning first because of the ignoredWord list
			FlatMMOPlus.showWarning(word + " added to " + type + " list");
			this.config[type].push(word)
			this.saveConfig();
		}

		addContextButton(action, text, func, isWarningBtn) {
			this.contextActions[action] = func;
			document.getElementById("flatChatContextMenu").insertAdjacentHTML("beforeend", `<button data-action="${action}" class="${isWarningBtn ? "flatChatContextWarningBtn" : "flatChatContextBtn"}">${text}</button>`)
		}

		contextMenu(e) {
			const data = e.target.closest("[data-action]");
			if (data) {
				const username = this.fcElement.querySelector("#flatChatContextUsername").dataset.user;
				const action = data.dataset.action;
				this.contextActions[action](username);
				const contextMenu = this.fcElement.querySelector("#flatChatContextMenu");
				contextMenu.style.visibility = "hidden";
			}
		}

		updateUnread(channel) {
			if(channel !== this.currentChannel) {
				const unreadMessages = ++this.channels[channel].unreadMessages;
				const unreadDiv = this.fcElement.querySelector(`.flatChatTab[data-channel=${channel}] .flatChatUnread`);
				unreadDiv.innerText = unreadMessages
				unreadDiv.style.visibility = "visible";
			}
		}

		//Server messages don't require some of the checks, most is the same as user messages, but I think it is better to have them as different functions now
		showServerMessage(data, html = false) {
			// data = {
			//     username: "",
			//     tag: "none",
			//     sigil: "none",
			//     color: "white",
			//     message: "oi gente",
			//     yell: false,
			//     channel: "channel_local"
			//     channel: "private_dounford"
			//     usernameColor: "red"
			// }

			//If for some reason the channel does not exist
			if(!data.channel in this.channels) {
				data.channel = "channel_local"
			}

			let message = data.message;

			for (const not in textToNotification) {
				if(message.includes(textToNotification[not].blocked)) {
					this.updateNotification(message, not);
					this.showNotification(not);
					return;
				}
			}

			let messageContainer = document.createElement('div');

			if (messageColors[data.color]) {
				messageContainer.className += " fc-" + messageColors[data.color]
			} else {
				//In case a color that doesn't has a variable yet is used
				messageContainer.style.color = data.color || "white";
			}

			//Message reiceived time [12:43]
			if (this.config.showTime) {
				const timeStrong = document.createElement("strong");
				timeStrong.className = "fc-timestamp"
				timeStrong.innerHTML = this.getDateStr();
				messageContainer.appendChild(timeStrong);
			}

			//For now it will never use html, but maybe Smitty changes something in the future
			const messageSpan = document.createElement('span');
			if (html) {
				messageSpan.innerHTML = message;
			} else {
				messageSpan.innerText = message;
			}

			//I find it hard to believe that the server will ever send links, but just in case...
			messageSpan.innerHTML = anchorme({
				input: messageSpan.innerHTML,
				options: {
					attributes: {
						target: "_blank",
						class: "detected"
					}
				},
			});

			//If the message contains any ignored word it will ignore the message or mark as spoiler
			if(this.config.ignoredWords.some(word => message.includes(word))) {
				if(this.config["hideUnwanted"]) {
					messageSpan.style.cursor = "pointer";
					messageSpan.classList.add("flatChatHidden");
					messageSpan.onclick = ()=>{
						messageSpan.classList.toggle("flatChatHidden")
					}
				} else {
					return;
				}
			}

			messageContainer.appendChild(messageSpan);

			const messageArea = this.fcElement.querySelector(`#flatChatChannels [data-channel=${data.channel}]`);
			messageArea.appendChild(messageContainer);

			//Trim old messages if over the limit
			if(this.config.maxMessages > 0) {
					console.log(this.config.maxMessages)

				while(messageArea.children.length > this.config.maxMessages) {
					messageArea.removeChild(messageArea.firstElementChild);
				}
			}

			//Update the unread messages number if needed
			this.updateUnread(data.channel);

			if (this.channels[data.channel].autoScroll) {
				messageArea.scrollTop = messageArea.scrollHeight;
			}
		}

		showMessage(data, html = false) {
			// data = {
			//     username: "dounford",
			//     tag: "none",
			//     sigil: "none",
			//     color: "white",
			//     message: "oi gente",
			//     yell: false,
			//     channel: "channel_local"
			//     channel: "private_dounford"
			//     usernameColor: "red"
			// }

			//If for some reason the channel does not exist
			if(!data.channel in this.channels) {
				data.channel = "channel_local"
			}

			let message = data.message;

			//This should prevent some spam to show
			const lastSender = this.channels[data.channel]?.lastSender || "";
			const lastMessage = this.channels[data.channel]?.lastMessage || "";
			if (lastSender === data.username && lastMessage === data.message && !this.getConfig("showSpam")) {
				return;
			}

			//If the message sender is blocked the message will be ignored
			if(this.config.ignoredPlayers.includes(data.username)) {
				return;
			}

			let messageContainer = document.createElement('div');

			//Ping if any watched word is present or if the message contains the username
			//Doesn't ping if it is yours message
			const isMention = message.includes("@" + Globals.local_username);
			const hasWatchedWord = this.config.watchedWords.some(word => message.includes(word));
			let isPing = false;
			if (data.username !== Globals.local_username && (isMention || hasWatchedWord)) {
				isPing = this.config.pingChat;
				messageContainer.className = "fc-pingMessages";
				//Ignore ping is just about the sound
				if(!this.config.ignorePings) {
					ding.play();
				}
			}

			if (data.color && data.color !== "white" && data.color !== "grey") {
				if (messageColors[data.color]) {
					messageContainer.className += " fc-" + messageColors[data.color]
				} else {
					//In case a color that doesn't has a variable yet is used
					messageContainer.style.color = data.color;
				}
			}

			//Message reiceived time [12:43]
			if (this.config.showTime) {
				const timeStrong = document.createElement("strong");
				timeStrong.className = "fc-timestamp"
				timeStrong.innerHTML = this.getDateStr();
				messageContainer.appendChild(timeStrong);
			}

			if (data.tag && data.tag !== "none") {
				let tag = document.createElement("span");

				tag.innerText = data.tag === "investor-gold" ? "GOLD INVESTOR" : data.tag === "investor-plus" ? "INVESTOR" : data.tag === "moderator" ? "MOD" : data.tag.toUpperCase();

				tag.classList.add("chat-tag-" + data.tag);
				messageContainer.appendChild(tag);
			}

			if (data.sigil && data.sigil !== "none") {
				const sigilImg = new Image();
				sigilImg.className = "chatSigil"

				if (IPSigils.has(data.sigil)) {
					//I'm using IP sigils for now, FMMO sigils have terrible resolution
					sigilImg.src = "https://cdn.idle-pixel.com/images/" + data.sigil.slice(10);
				} else {
					sigilImg.src = "https://flatmmo.com/" + data.sigil;
				}

				messageContainer.appendChild(sigilImg);
			}

			let isPm = data.channel.startsWith("private_");

			if (data.username) {
				const senderStrong = document.createElement("strong");
				let username = data.username.replaceAll("_", " ");
				if(isPm && data.color === "pmSent") {
					username = Globals.local_username;
				}
				username = this.config.nicknames[username] || username;
				senderStrong.innerText = username + ":";
				senderStrong.className = "fc-playerName";
				senderStrong.setAttribute("data-sender", data.username.replaceAll(" ", "_"));
				messageContainer.appendChild(senderStrong);

				if(this.config.watchedPlayers.includes(data.username) && !isPm) {
					messageContainer.className = "fc-pingMessages";
				}
			} else {
				for (const not in textToNotification) {
					if(message.includes(textToNotification[not].blocked)) {
						this.updateNotification(message, not);
						this.showNotification(not);
						return;
					}
				}
			}

			const messageSpan = document.createElement('span');
			const match = message.match(/My (.*?) level is: (.*?) \((.*?) xp\)/);
			if(match) {
				const [_, skill, level, xp] = match;
				this.createLevelTooltip(messageSpan, this.toTitleCase(skill), level, xp);
			} else if (html) {
				messageSpan.innerHTML = message;
			} else {
				messageSpan.innerText = message;
			}

			messageSpan.innerHTML = anchorme({
				input: messageSpan.innerHTML,
				options: {
					attributes: {
						target: "_blank",
						class: "detected"
					}
				},
			});

			//If the message contains any ignored word it will ignore the message or mark as spoiler
			if(this.config.ignoredWords.some(word => message.includes(word))) {
				if(this.config["hideUnwanted"]) {
					messageSpan.style.cursor = "pointer";
					messageSpan.classList.add("flatChatHidden");
					messageSpan.onclick = ()=>{
						messageSpan.classList.toggle("flatChatHidden")
					}
				} else {
					return;
				}
			}
			messageContainer.appendChild(messageSpan);

			const messageArea = this.fcElement.querySelector(`#flatChatChannels [data-channel=${data.channel}]`);
			messageArea.appendChild(messageContainer);
			//Clones the message on the ping channel
			if(isPing) {
				const pingElement = messageContainer.cloneNode(true);
				pingElement.className = ""; //Every message is a ping, there is no reason to highlight them

				const mid = Date.now();
				messageContainer.setAttribute("data-omid", mid);
				pingElement.setAttribute("data-mid", mid);
				pingElement.setAttribute("data-channelname", data.channel);

				const pingsArea = this.fcElement.querySelector(`.flatChatChannel[data-channel=channel_pings]`);
				pingsArea.appendChild(pingElement);
				if(this.config.maxMessages > 0) {
					console.log(this.config.maxMessages)
					while(pingsArea.children.length > this.config.maxMessages) {
						pingsArea.removeChild(pingsArea.firstElementChild);
					}
				}

				//Update the unread messages number if needed
				if(this.currentChannel !== "channel_pings") {
					this.updateUnread("channel_pings");
				}
			}

			//Trim old messages if over the limit
			if(this.config.maxMessages > 0) {
				while(messageArea.children.length > this.config.maxMessages) {
					messageArea.removeChild(messageArea.firstElementChild);
				}
			}

			if(data.channel !== this.currentChannel) {
				//Update the unread messages number if needed
				this.updateUnread(data.channel);
			}

			if (this.channels[data.channel].autoScroll) {
				messageArea.scrollTop = messageArea.scrollHeight;
			}

			this.channels[data.channel].lastSender = data.username;
			this.channels[data.channel].lastMessage = data.message;
		}

		createLevelTooltip(element, skill, level, xp) {
			const ixp = parseInt(xp.replaceAll(",",""));
			level = parseInt(level);
			let nextLvl = "";
			if(skill !== "Global" && level !== 100) {
				const value = format_number(FlatMMOPlus.level[level + 1] - ixp);
				nextLvl = `<div>XP to Level Up: ${value}</div>`
			}
			element.innerHTML = `<span class="flatChatLevelSpan">Lvl ${level} ${skill}</span>
			<div class="tooltiptext flatChatLevelTooltip">
				<div>${skill}</div>
				<div>Level: ${level}</div>
				<div>XP: ${xp}</div>
				${nextLvl}
			</div>`;
			element.classList.add("tooltip");
		}

		updateNotification(message, blocked) {
			switch (blocked) {
				case "nessieTime": {
					textToNotification[blocked].text = message.slice(0,-1);
				} break;
				case "fireFish": {
					const match = message.match(/\((.*)\)/);
					if (match) {
						textToNotification[blocked].text = match[1];
					} else {
						textToNotification[blocked].text = "0/0";
					}
				} break;
				case "feedingTheDead": {
					const match = message.match(/\((.*)\)/)
					if (match) {
						textToNotification[blocked].text = match[1];
					} else {
						textToNotification[blocked].text = "+0 xp";
					}
				} break;
				case "bondfirePoints": {
					const match = message.match(/\((.*) /);
					if (match) {
						textToNotification[blocked].text = match[1] + " points";
					} else {
						textToNotification[blocked].text = "0 points";
					}
				} break;
				case "lightFire": {
					const match = message.match(/last (.*) /)
					if (match) {
						textToNotification[blocked].text = match[1] + " seconds";
					} else {
						textToNotification[blocked].text = "0 seconds";
					}
				} break;

			}
		}

		showNotification(blocked) {
			const not = textToNotification[blocked];
			FlatMMOPlus.addNotification(not.name, not.image, not.title, not.text, not.ticks, not.color);
		}

		

		sendMessage() {
			const input = this.fcElement.querySelector("#flatChatInput");
			let message = input.value.trim();
			if (!message) {return};

			//[here] can't be modified, it will always use the current_map id
			message = message.replace("[here]", current_map);
			//This replaces all [shortcuts]
			Object.keys(this.config.shortcuts).forEach(shortcut => message = message.replaceAll(`[${shortcut}]`, this.config.shortcuts[shortcut]));

			input.value = "";
			this.channels[this.currentChannel].inputText = "";

			if(message !== this.chatHistory[0]) {
				this.chatHistory.unshift(message);
			}
			this.historyPosition = -1;


			if(message.startsWith("/")) {
				const space = message.indexOf(" ");
				let command;
				let data;
				if (space <= 0) {
					command = message.substring(1);
					data = "";
				} else {
					command = message.substring(1, space);
					data = message.substring(space + 1);
				}

				if (window.FlatMMOPlus.handleCustomChatCommand(command, data)) {
					return;
				} else {
					Globals.websocket.send('CHAT=' + message);
					return;
				}
			}

			const maxlength = this.currentChannel.startsWith("channel") ? 95 : 85;
			const messageArray = this.divideStringByLength(message, maxlength);
			const isPM = message.startsWith("/pm");
			for (let i = 0; i < messageArray.length; i++) {
				if(this.currentChannel === "channel_whisper" || (isPM && i > 0)) {
					FlatMMOPlus.customChatCommands.r("r", messageArray[i])
				} else if(this.currentChannel === "channel_local") {
					Globals.websocket.send('CHAT=' + messageArray[i]);
				} else if(this.currentChannel === "channel_global") {
					Globals.websocket.send('CHAT=/yell ' + messageArray[i]);
				} else if (this.currentChannel.startsWith("private_")) {
					const username = this.currentChannel.slice(8);
					Globals.websocket.send(`CHAT=/pm ${username} ${messageArray[i]}`);
				}
			}
		}

		//Utilities functions
		getDateStr(timestamp) {
			const date = timestamp ? new Date(timestamp) : new Date();
			const hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
			const min = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
			const dataStr = '[' + hour + ':' + min + ']'
			return dataStr;
		}

		toCamelCase(str) {
			if (!str || typeof str !== 'string') {
				return '';
			}

			const words = str.split(/[\s_-]+/);

			const camelCaseWords = words.map((word, index) => {
				if (index === 0) {
					return word.toLowerCase();
				} else {
					return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
				}
			});

			return camelCaseWords.join('');
		}

		toTitleCase(str) {
			const result = str.replace(/([A-Z])/g, " $1");
			return result.charAt(0).toUpperCase() + result.slice(1)
		}

		divideStringByLength(str, l, delimiterChat = " "){
			const strs = [];
			while(str.length > l){
				let pos = str.substring(0, l).lastIndexOf(delimiterChat);
				pos = pos <= 0 ? l : pos;
				strs.push(str.substring(0, pos));
				let i = str.indexOf(delimiterChat, pos)+1;
				if(i < pos || i > pos+l)
					i = pos;
				str = str.substring(i);
			}
			strs.push(str);
			return strs;
		}
	}

	const plugin = new flatChatPlugin();
	FlatMMOPlus.registerPlugin(plugin);

})();