FlatChat+

Better chat for FlatMMO

// ==UserScript==
// @name         FlatChat+
// @namespace    com.dounford.flatmmo.flatChat
// @version      0.2.0
// @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: "lvlMilestoneMessages", //server messages
		red: "errorMessages", //errors (trade declines, no energy)
		lime: "restMessages", //rest message
		green: "lvlUpMessages", //level up
		cyan: "areaChangeMessages", //Leaving/Entering town
		private: "privateMessages", //private messages
		ownPrivate: "ownPrivateMessages",
		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: {
			bgColor: "#323437",
			pickerLocal: "#C0C0C0",
			pickerGlobal: "#C0C0C0",
			pickerRoom: "#C0C0C0",
			pickerPrivate: "#C0C0C0",
			inputName: "#FA8072",
			inputColor: "#252729",
			inputText: "#C0C0C0",
			messagesColor: "#e1e1e1",
			serverMessages: "#6495ED",
			lvlMilestoneMessages: "#FF1493",
			errorMessages: "#FF0000",
			restMessages: "#00FF00",
			lvlUpMessages: "#00ad00",
			areaChangeMessages: "#00FFFF",
			privateMessages: "#FFA500",
			ownPrivateMessages: "#e88f4f",
			pingMessages: "#5F9EA0",
			contextBackground: "#323437",
			contextSection: "#252729",
			contextText: "#C0C0C0",
			linkColor: "#00FFFF",
		},
		light: {
			bgColor: "#ffffff",
			pickerLocal: "#000000",
			pickerGlobal: "#000000",
			pickerRoom: "#000000",
			pickerPrivate: "#000000",
			inputName: "#FA8072",
			inputColor: "#D3D3D3",
			inputText: "#000000",
			messagesColor: "#000000",
			serverMessages: "#6495ED",
			lvlMilestoneMessages: "#FF1493",
			errorMessages: "#FF0000",
			restMessages: "#00FF00",
			lvlUpMessages: "#008000",
			areaChangeMessages: "#00FFFF",
			privateMessages: "#FFA500",
			ownPrivateMessages: "#e88f4f",
			pingMessages: "#5F9EA0",
			contextBackground: "#323437",
			contextSection: "#252729",
			contextText: "#C0C0C0",
			linkColor: "#00FFFF",
		}
	}
 
	class flatChatPlugin extends FlatMMOPlusPlugin {
		constructor() {
			super("flatChat", {
				about: {
					name: GM_info.script.name,
					version: GM_info.script.version,
					author: GM_info.script.author,
					description: GM_info.script.description
				},
				config: [
					{
						id: "ignorePings",
						label: "Ignore all chat pings",
						type: "boolean",
						default: false
					},
					{
						id: "pingVolume",
						label: "Ping Volume",
						type: "range",
						min: 0,
						max: 100,
						step: 1,
						default: 100,
					},
					{
						id: "lessEnergyWarning",
						label: "Reduce the amount of low energy warnings",
						type: "boolean",
						default: false
					},
					{
						id: "showTime",
						label: "Display message received time",
						type: "boolean",
						default: true
					},
					{
						id: "showSpam",
						label: "Show duplicate messages from the same user (it may be spam)",
						type: "boolean",
						default: false
					},
					{
						id: "fontSize",
						label: "Message font size",
						type: "number",
						min: 0,
						max: 10,
						step: 0.1,
						default: 1
					},
					{
						id: "theme",
						label: "Theme",
						type: "select",
						options: [
							{value: "light", label: "Light"},
							{value: "dark", label: "Dark"},
						],
						default: "dark"
					},
					{
						id: "ignoredPlayers",
						label: "Players ignored by you (use , to separate)",
						type: "string",
						default: ""
					},
					{
						id: "ignoredWords",
						label: "Words blocked by you, messages containing these will not show (use , to separate)",
						type: "string",
						default: ""
					},
					{
						id: "watchedPlayers",
						label: "Players watched, every message from them will be highlighted (use , to separate)",
						type: "string",
						default: ""
					},
					{
						id: "watchedWords",
						label: "Words watched, you will receive a ping every time a message containing them is sent (use , to separate)",
						type: "string",
						default: ""
					},
					{
					    id: "themeEditor",
					    label: "THEME EDITOR",
					    panel: "flatChat-ThemeEditor",
					    type: "panel"
					}
				]
			});
			this.settings = {
				ignoredPlayers: new Set(),
				ignoredWords: new Set(),
				watchedPlayers: new Set(),
				watchedWords: new Set(),
			}

			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 is for when the user is offline and you sent a pm, instead of telling you that they are offline every message it tells you once each 5 minutes
			this.offlineUsers = {}

			this.lastWarning = Date.now() - 60000;

			this.themes = {
				dark: {
					bgColor: "#323437",
					pickerLocal: "#C0C0C0",
					pickerGlobal: "#C0C0C0",
					pickerRoom: "#C0C0C0",
					pickerPrivate: "#C0C0C0",
					inputName: "#FA8072",
					inputColor: "#252729",
					inputText: "#C0C0C0",
					messagesColor: "#e1e1e1",
					serverMessages: "#6495ED",
					lvlMilestoneMessages: "#FF1493",
					errorMessages: "#FF0000",
					restMessages: "#00FF00",
					lvlUpMessages: "#00ad00",
					areaChangeMessages: "#00FFFF",
					privateMessages: "#FFA500",
					ownPrivateMessages: "#e88f4f",
					pingMessages: "#5F9EA0",
					contextBackground: "#323437",
					contextSection: "#252729",
					contextText: "#C0C0C0",
					linkColor: "#00FFFF",
				},
				light: {
					bgColor: "#ffffff",
					pickerLocal: "#000000",
					pickerGlobal: "#000000",
					pickerRoom: "#000000",
					pickerPrivate: "#000000",
					inputName: "#FA8072",
					inputColor: "#D3D3D3",
					inputText: "#000000",
					messagesColor: "#000000",
					serverMessages: "#6495ED",
					lvlMilestoneMessages: "#FF1493",
					errorMessages: "#FF0000",
					restMessages: "#00FF00",
					lvlUpMessages: "#008000",
					areaChangeMessages: "#00FFFF",
					privateMessages: "#FFA500",
					ownPrivateMessages: "#e88f4f",
					pingMessages: "#5F9EA0",
					contextBackground: "#323437",
					contextSection: "#252729",
					contextText: "#C0C0C0",
					linkColor: "#00FFFF",
				}
			}
		}
 
		onLogin() {
			this.removeOriginalChat();
			this.addStyle();
			this.addUI();
			this.loadChannels();
			this.switchChannel("local", false);
			this.messagesWaiting.forEach((message)=>this.showMessage(message));
			this.defineCommands();
			ding.volume = this.config.pingVolume / 100;
			this.addThemeEditor();
			this.changeThemeEditor();
			this.showWarning("Welcome to flatmmo.com", "orange");
			this.showWarning(document.querySelectorAll("#chat span")[1].innerHTML, "white");
			this.showWarning(`<span><strong style="color:cyan">FYI: </strong> Use the /help command to see information on available chat commands.</span>`, "white");
		}
 
		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 = "ownPrivate";
					} else {
						data.color = "private";
					}
					data.username = match[1].trim().replaceAll(" ", "_");
    				data.message = match[2].trim();
					data.channel = this.channels["private_" + data.username] ? "private_" + data.username : "channel_global";
				} else {
					console.warn("There was something wrong with this pm:", data.message)
				}
			};

			if(FlatMMOPlus.loggedIn) {
				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": {
						document.getElementById("flatChat-channels").style.setProperty("--fc-size", this.config.fontSize + "rem");
					} break;
					case "theme": {
						const flatChat = document.getElementById("flatChat");
						flatChat.classList = "flatChat flatChat-" + this.config.theme;
					} break;
					case "ignoredPlayers":
					case "ignoredWords":
					case "watchedPlayers":
					case "watchedWords": {
						this.watchIgnorePlayersWords(config, this.config[config], true);
					} 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";

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

		addStyle() {
			const style = document.createElement("style");
			style.innerHTML = `
			/*Chat box*/
			.flatChat {
				max-width: 1880px;
				background: var(--fc-bgColor);
				border: solid black 2px;
				border-radius: 5px;
				text-align: justify;
				outline: none;
			}
			.flatChat * {
    			outline: none;
			}
			.flatChat-mainArea {
				display: flex;
				height: 300px
			}
			/*channel list*/
			#flatChat-channelPicker {
				width: 10%;
				padding: 5px;
				flex: none;
				overflow-x: hidden;
				scrollbar-width: thin;
				display: block;
				transition: all 1s ease-in-out;
				transition-behavior: allow-discrete;

				@starting-style {
					width: 0px;
				}

				button {
					display: flex;
					border: 0;
					background-color: transparent;
					font-weight: bold;
					white-space: nowrap;
					&:hover {
						transform: scale(1.05);
					}
				}

				/*Current room btn*/
				[data-channel="channel_local"] {
					color: var(--fc-pickerLocal) !important;
				}
				
				/*yell chat btn*/
				[data-channel="channel_global"] {
					color: var(--fc-pickerGlobal) !important;
				}

				/*custom room btn*/
				.flatChat-channelPicker-room {
					color: var(--fc-pickerRoom);
				}

				/*direct message btn*/
				.flatChat-channelPicker-private {
					color: var(--fc-pickerPrivate);
				}
			}
			#flatChat-channelPicker[closed] {
				width: 0;
				display: none;
			}

			/*messages area*/
			#flatChat-channels {
				width: -webkit-fill-available;
				height: 300px;
				font-size: var(--fc-size);
				>div {
					height: 100%;
					overflow-y: auto;
					padding: 5px;

					div {
						overflow-wrap: anywhere;
						color: var(--fc-messagesColor);
					}

					img {
						width: var(--fc-size);
						vertical-align: bottom;
					}

					a {
						text-decoration: none;
						color: var(--fc-linkColor);

						&:visited {
							color: var(--fc-linkColor);
						}
					}
				}
			}
			.fc-serverMessages {
				color: var(--fc-serverMessages) !important;
			}
			.fc-lvlMilestoneMessages {
				color: var(--fc-lvlMilestoneMessages) !important;
			}
			.fc-errorMessages {
				color: var(--fc-errorMessages) !important;
			}
			.fc-restMessages {
				color: var(--fc-restMessages) !important;
			}
			.fc-lvlUpMessages {
				color: var(--fc-lvlUpMessages) !important;
			}
			.fc-areaChangeMessages {
				color: var(--fc-areaChangeMessages) !important;
			}
			.fc-privateMessages {
				color: var(--fc-privateMessages) !important;
			}
			.fc-ownPrivateMessages {
				color: var(--fc-ownPrivateMessages) !important;
			}
			.fc-pingMessages {
				background-color: var(--fc-pingMessages) !important;
			}
			/*player name*/
			.flatChat-player {
				cursor: pointer;
			}
			/*bottom bar*/
			#flatChat-BottomBar{
				display: flex;
				align-items: center;
				margin-top: 5px;
				padding: 2px;

				button {
					border: solid black 1px;
					border-radius: 5px;
					padding: 0;
					background-color: transparent;
					cursor: pointer;

					&:hover{
						background-color: rgba(0,0,0,0.1);
					}
					
					img {
						width: 40px;
						vertical-align: middle;
						padding: 1px;
					}
				}
			}
			/*message input*/
			#flatChat-inputDiv {
				display: flex;
				align-items: center;
				width: 100%;
				margin: 0 5px;
				background-color: var(--fc-inputColor);
				border-radius: 5px;
				padding: 2px;
				label {
					user-select: none;
					cursor: text;
					color: var(--fc-inputName);
					font-size: larger;
				}
				input {
					flex: auto;
					border: 0;
					padding-top: 0;
					background-color: transparent;
					color: var(--fc-inputText);
					&:focus {
						outline: transparent;
					}
				}
			}
			/*bottom bar btns*/
			.flatChat-buttons{
				flex: none;
				text-align: center;
			}
			/*Context menu*/
			#flatChat-contextMenu {
				visibility: hidden;
				position: absolute;
				list-style: none;
				padding: 0;
				background-color: var(--fc-contextBackground);
				color: var(--fc-contextText);
				cursor: pointer;
				font-size: clamp(12px, 1.2vw, 24px);
				top:0;

				li {
					padding: 5px;
				}
			}

			.flatChat-contextSection {
				background-color: var(--fc-contextSection);
				cursor: default;
			}

			#flatChat-copyUsername {
				position: absolute;
				background-color: var(--fc-inputColor);
				color: var(--fc-inputText);
				padding: 3px;
				border-radius: 5px;
				visibility: hidden;
				font-size: clamp(12px, 1.2vw, 24px); 
				top:0;
			}`
			document.head.append(style);

			if(localStorage.getItem("flatChat-themes")) {
				this.themes = JSON.parse(localStorage.getItem("flatChat-themes"));
			}
			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() {
			const chatDiv = document.createElement("div");
			chatDiv.innerHTML = `
				<div class="flatChat-mainArea">
					<div id="flatChat-channelPicker"></div>
					<div id="flatChat-channels" style="--fc-size:${this.config.fontSize}rem;"></div>
				</div>
				<div id="flatChat-BottomBar">
					<div class="flatChat-buttons">
						<button type="button" id="flatChat-togglePicker">
							<img src="https://cdn.idle-pixel.com/images/collection_small_circle_icon.png" alt="">
						</button>
						<button type="button" id="flatChat-closeChat">
							<img src="https://cdn.idle-pixel.com/images/x.png" alt="">
						</button>
					</div>
					<div id="flatChat-inputDiv">
						<label for="flatChat-input"></label>
						<input type="text" id="flatChat-input" autocomplete="off" maxlength="${LOCAL_CHAT_MAX_LENGTH || 100}">
					</div>
					<div class="flatChat-buttons">
						<button type="button" id="flatChat-autoScroll">
							<img src="https://cdn.idle-pixel.com/images/x.png" id="fc-autoScrollfalse" alt="" class="displaynone">
							<img src="https://cdn.idle-pixel.com/images/check.png" id="fc-autoScrolltrue" alt="">
						</button>
						<button type="button" id="flatChat-scrollToBottom">
							<img src="https://flatmmo.com/images/icons/damage.png" alt="" style="transform: rotate(180deg);">
						</button>
						<button type="button" id="flatChat-srollDown">
							<img src="https://flatmmo.com/images/icons/arrow_damage.png" alt="" style="transform: rotate(180deg);">
						</button>
						<button type="button" id="flatChat-srollUp">
							<img src="https://flatmmo.com/images/icons/arrow_damage.png" alt="">
						</button>
					</div>
				</div>
				<ul id="flatChat-contextMenu">
					<li id="flatChat-contextUser" class="flatChat-contextSection" data-user=""></li>
					<li data-action="message">Message</li>
					<li data-action="tabMessage">Message (tab)</li>
					<li data-action="profile">Profile</li>
					<li data-action="trade">Trade</li>
					<li class="flatChat-contextSection">MODERATION</li>
					<li data-action="stalk">Stalk</li>
					<li data-action="ignore">Ignore</li>
				</ul>
				<div id="flatChat-copyUsername">USERNAME COPIED</div>`
			chatDiv.id = "flatChat";
			chatDiv.tabIndex = 0;
			let currentTheme = this.config.theme
			if(this.themes[currentTheme]) {
				chatDiv.classList = "flatChat flatChat-" + this.config.theme;
			} else {
				this.config.theme = "dark";
				chatDiv.classList = "flatChat flatChat-dark";
				this.saveConfig();
			}

			document.querySelector("body center").insertAdjacentElement("beforeend",chatDiv);

			document.querySelector("#flatChat-inputDiv label").innerText = `[${Globals.local_username}]:`;

			document.querySelector("#flatChat-channelPicker").onclick = (e) => {
				const channelName = e.target.closest("[data-channel]");
				if (channelName) {
					const match = channelName.dataset.channel.match(/(.*?)_(.*)/);
					if (match) {
						const type = match[1];
						const channel = match[2];
						this.switchChannel(channel, type === "private")
					}
				}
			}

			document.getElementById("flatChat-togglePicker").addEventListener("click", ()=>{
				const picker = document.getElementById("flatChat-channelPicker");
				picker.toggleAttribute("closed");
			})

			document.getElementById("flatChat-closeChat").addEventListener("click",()=>{this.closeChannel()})
			document.getElementById("flatChat-autoScroll").addEventListener("click",()=>{this.toggleAutoScroll()})
			document.getElementById("flatChat-scrollToBottom").addEventListener("click",()=>{this.scrollButtons("bottom")})
			document.getElementById("flatChat-srollDown").addEventListener("click",()=>{this.scrollButtons("down")})
			document.getElementById("flatChat-srollUp").addEventListener("click",()=>{this.scrollButtons("up")})

			document.getElementById("flatChat-channels").onwheel = (event)=>{this.scrollChannel(event)}

			document.querySelector("#flatChat-channels").addEventListener("click", async (e) => {
				const sender = e.target.closest("[data-sender]");
				if (sender) {
					const username = sender.dataset.sender;
					await navigator.clipboard.writeText(username);

					const copyMessage = document.getElementById("flatChat-copyUsername");
					copyMessage.style.top = e.clientY + "px"
					copyMessage.style.left = e.clientX + "px"
					copyMessage.style.visibility = "visible"
					setTimeout(()=>{copyMessage.style.visibility = "hidden"}, 1000);
				}
			});

			//Shows custom context menu on right click
			document.querySelector("#flatChat-channels").addEventListener("contextmenu", (e) => {
				const sender = e.target.closest("[data-sender]");
				if (sender) {
					e.preventDefault()
					const contextMenu = document.getElementById("flatChat-contextMenu");
					const username = sender.dataset.sender;
					document.getElementById("flatChat-contextUser").setAttribute("data-user", username);
					document.getElementById("flatChat-contextUser").innerText = username;

					const menuRect = contextMenu.getBoundingClientRect();
					const menuWidth = menuRect.width;
					const menuHeight = menuRect.height;

					const mouseX = e.pageX;
					const mouseY = e.pageY;

					if (mouseY + menuHeight > window.innerHeight) {
						contextMenu.style.top = (mouseY - menuHeight) + "px";
					} else {
						contextMenu.style.top = mouseY + "px";
					}

					if (mouseX + menuWidth > window.innerWidth) {
						contextMenu.style.left = (mouseX - menuWidth) + "px";
					} else {
						contextMenu.style.left = mouseX + "px";
					}

					contextMenu.style.visibility = "visible";
				}
			});

			document.getElementById("flatChat-contextMenu").addEventListener("click", (e) => {this.contextMenu(e)})

			//Context menu should close if you click outside
			document.addEventListener("click", function (e) {
				const contextMenu = document.getElementById("flatChat-contextMenu");
				if (!contextMenu.contains(e.target)) {
					contextMenu.style.visibility = "hidden";
				}
			});

			document.addEventListener("keydown", (e) => {
				if (e.key === "Tab" && e.target.closest('#flatChat')) {
					e.preventDefault();
					if(document.querySelector(`#flatChat-channelPicker [data-channel=${this.currentChannel}]`).nextElementSibling) {
						const channel = document.querySelector(`#flatChat-channelPicker [data-channel=${this.currentChannel}]`).nextElementSibling.dataset.channel
						const match = channel.match(/(.*?)_(.*)/);
						if (match) {
							const type = match[1];
							const channel = match[2];
							this.switchChannel(channel, type === "private")
						}
						return;
					} else {
						this.switchChannel("local", false)
						return;
					}
				}
				if(document.activeElement === document.getElementById("flatChat-input")) {
					if(e.key === "Enter") {
						if(has_modal_open()) return;
						this.sendMessage();
					}

					const input = document.getElementById("flatChat-input");

					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
					}
				}
			}, true)
		}

		addTheme(theme) {
			if(!this.themes[theme]) {return};
			let themeSyle;
			if(document.getElementById("fc-themeStyle-" + theme)) {
				themeSyle = document.getElementById("fc-themeStyle-" + theme)
				themeSyle.innerHTML = "";
			} else {
				themeSyle = document.createElement("style");
				themeSyle.id = "fc-themeStyle-" + theme;
				document.head.appendChild(themeSyle);
			}
			themeSyle.innerHTML = `.flatChat-${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;

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

						select {
							grid-column: 1 / span 2;
							text-align: center;
							font-size: large;
						}
					}
				</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>
				<label for="fc-bgColor-editor">Chat Background</label>
				<input type="color" id="fc-bgColor-editor">

				<label for="fc-pickerLocal-editor">Picker Local Channel</label>
				<input type="color" id="fc-pickerLocal-editor">
				
				<label for="fc-pickerGlobal-editor">Picker Global Channel</label>
				<input type="color" id="fc-pickerGlobal-editor">
				
				<label for="fc-pickerRoom-editor">Picker Room Channel (not available)</label>
				<input type="color" id="fc-pickerRoom-editor">
				
				<label for="fc-pickerPrivate-editor">Picker Private Channel</label>
				<input type="color" id="fc-pickerPrivate-editor">
				
				<label for="fc-inputName-editor">Username on Chat Bar</label>
				<input type="color" id="fc-inputName-editor">
				
				<label for="fc-inputColor-editor">Background of Chat Bar</label>
				<input type="color" id="fc-inputColor-editor">
				
				<label for="fc-inputText-editor">Chat Bar Text Color</label>
				<input type="color" id="fc-inputText-editor">
				
				<label for="fc-messagesColor-editor">Regular Message Color</label>
				<input type="color" id="fc-messagesColor-editor">
				
				<label for="fc-serverMessages-editor">Server Messages</label>
				<input type="color" id="fc-serverMessages-editor">
				
				<label for="fc-lvlMilestoneMessages-editor">Lvl Up Milestone (each 10 lvls)</label>
				<input type="color" id="fc-lvlMilestoneMessages-editor">
				
				<label for="fc-errorMessages-editor">Error/Warning Messages</label>
				<input type="color" id="fc-errorMessages-editor">
				
				<label for="fc-restMessages-editor">Rest Messages</label>
				<input type="color" id="fc-restMessages-editor">
				
				<label for="fc-lvlUpMessages-editor">Lvl Up Messages</label>
				<input type="color" id="fc-lvlUpMessages-editor">
				
				<label for="fc-areaChangeMessages-editor">Entering/Leaving Town</label>
				<input type="color" id="fc-areaChangeMessages-editor">
				
				<label for="fc-privateMessages-editor">Private Messages Received</label>
				<input type="color" id="fc-privateMessages-editor">
				
				<label for="fc-ownPrivateMessages-editor">Private Messages Sent</label>
				<input type="color" id="fc-ownPrivateMessages-editor">
				
				<label for="fc-pingMessages-editor">Ping/Watched Messages</label>
				<input type="color" id="fc-pingMessages-editor">
				
				<label for="fc-contextBackground-editor">Context Menu Background Color</label>
				<input type="color" id="fc-contextBackground-editor">
				
				<label for="fc-contextSection-editor">Context Menu Section Background (MODERATION)</label>
				<input type="color" id="fc-contextSection-editor">
				
				<label for="fc-contextText-editor">Context Menu Text Color</label>
				<input type="color" id="fc-contextText-editor">
				
				<label for="fc-linkColor-editor">Hyperlink Text Color</label>
				<input type="color" id="fc-linkColor-editor">

				<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) {
				this.themes[themeName][option] = document.getElementById("fc-" + option + "-editor").value
			}

			
			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 flatChat-" + this.config.theme;
			this.saveConfig();
			
			localStorage.setItem("flatChat-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 === "light" || theme === "dark")  {
				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("flatChat-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 flatChat-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") {
					this.showWarning(Object.keys(players).join(", "), "white");
				} else if (this.currentChannel.startsWith("private_")) {
					this.showWarning(`${this.currentChannel.slice(8)} & ${Globals.local_username}`, "white");
				}
			}, `Show all players in room or global.`);

			//Pm will only open a tab if a message is not specified
			window.FlatMMOPlus.registerCustomChatCommand("pm", (command, data='') => {
				if (data === "") {
					this.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);
					if (this.channels["private_" + receiver]) {
						this.switchChannel(receiver, true);
					} else {
						this.switchChannel("global", false);
					}
					Globals.websocket.send(`CHAT=/pm ${receiver} ${message}`);
				}
			}, `Send a private message to someone.`);

			//pm* will always open a new tab
			window.FlatMMOPlus.registerCustomChatCommand("pm*", (command, data='') => {
				if (data === "") {
					this.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("profile", (command, data='') => {
				if (data === "") {
					this.showWarning("You need to specify the username", "red");
					return;
				}
				Globals.websocket.send("RIGHT_CLICKED_PLAYER=" + data);
			}, `Opens the player profile.<br><b>Usage:</b>/profile [username]`);

			window.FlatMMOPlus.registerCustomChatCommand("trade", (command, data='') => {
				if (data === "") {
					this.showWarning("You need to specify the username", "red");
					return;
				}
				Globals.websocket.send("SEND_TRADE_REQUEST=" + data);
			}, `Send a trade request if the player is in the same map.<br><b>Usage:</b>/trade [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)>`);

			window.FlatMMOPlus.registerCustomChatCommand("ignore", (command, data='') => {
				if (data === "") {
					this.showWarning("You need to specify the username", "red");
					return;
				}
				this.watchIgnorePlayersWords("ignoredPlayers", data)
				this.showWarning("Player added to ignored list");
			}, `Ignores all messages from user.<br><b>Usage:</b>/ignore [username] (use _ for names with space)`);

			window.FlatMMOPlus.registerCustomChatCommand("watch", (command, data='') => {
				if (data === "") {
					this.showWarning("You need to specify the username", "red");
					return;
				}
				this.watchIgnorePlayersWords("watchedPlayers",data);
				this.showWarning("Player added to watched list");
			}, `Highlights all messages from user.<br><b>Usage:</b>/watch [username] (use _ for names with space)`);

			window.FlatMMOPlus.registerCustomChatCommand("ignoreword", (command, data='') => {
				if (data === "") {
					this.showWarning("You need to specify at least one word", "red");
					return;
				}
				this.watchIgnorePlayersWords("ignoredWords",data);
				this.showWarning("Word added to ignored list");
			}, `Ignores all messages that contains this word.<br><b>Usage:</b>/ignoreword [word] (use _ for words with space)`);

			window.FlatMMOPlus.registerCustomChatCommand("watchword", (command, data='') => {
				if (data === "") {
					this.showWarning("You need to specify at least one word", "red");
					return;
				}
				this.watchIgnorePlayersWords("watchedWords",data);
				this.showWarning("Word added to watched list");
			}, `Ping you every time this word is sent.<br><b>Usage:</b>/watchword [word] (use _ for words with space)`);

		}

		newChannel(channel, isPrivate) {
			console.log(channel, isPrivate)
			const channelName = (isPrivate ? "private_" : "channel_") + channel;
			if(this.channels[channelName]) {return};
			this.channels[channelName] = {
				name: channel,
				isPrivate: isPrivate,
				autoScroll: true,
				unreadMessages: 0,
				inputText: "",
				lastMessage: "",
				lastSender: "",
			}
			document.getElementById("flatChat-channelPicker").insertAdjacentHTML("beforeend",`<button data-channel="${channelName}" class="flatChat-channelPicker-${isPrivate ? "private" : "room"}">
				<span id="unreadMessages-${channelName}" style="display: none;"></span><span id="activeChat-${channelName}" style="display:none">></span><span>${isPrivate ? "@" : "#"}${channel.replace("_"," ")}</span>
			</button>`)
			document.getElementById("flatChat-channels").insertAdjacentHTML("beforeend",`<div data-channel="${channelName}" style="display:none"></div>`);
			if(isPrivate) {
				document.querySelector(`#flatChat-channels [data-channel=${channelName}]`).insertAdjacentHTML("beforeend",`
				<div style="color: var(--fc-lvlUpMessages);"><strong>${this.getDateStr()}</strong><span>New chat with ${channel}</span></div>`)
			}
			this.saveChannels();
		}

		closeChannel(channel) {
			const oldChannel = channel || this.currentChannel;
			if (oldChannel === "channel_local" || oldChannel === "channel_global") {
				return;
			}
			this.switchChannel("local", false);

			delete this.channels[oldChannel];
			document.querySelector(`#flatChat-channelPicker [data-channel=${oldChannel}]`).remove();
			document.querySelector(`#flatChat-channels [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"]');
			channels.forEach(channel => {
				const match = channel.match(/(.*?)_(.*)/);
				if (match) {
					const type = match[1];
					const name = match[2];
					this.newChannel(name, type === "private");
				}
			})
		}

		switchChannel(channel, isPrivate) {
			const input = document.getElementById("flatChat-input");
			//remove old
			document.getElementById("activeChat-" + this.currentChannel).style.display = "none";
			document.querySelector(`#flatChat-channels [data-channel=${this.currentChannel}]`).style.display = "none";
			this.channels[this.currentChannel].inputText = input.value;

			//show  new
			const newChannel = (isPrivate ? "private_" : "channel_") + channel
			//Makes sure the channel exists
			if (!newChannel in this.channels) {
				newChannel = "channel_local"
			};
			this.currentChannel = newChannel;

			//Removes unreadMessages number
			this.channels[this.currentChannel].unreadMessages = 0;
			const unreadSpan = document.getElementById("unreadMessages-" + this.currentChannel);
			unreadSpan.style.display = "none";

			//Change auto scroll icon
			const autoScroll = this.channels[this.currentChannel].autoScroll;
			document.getElementById("fc-autoScroll" + autoScroll).className = "";
			document.getElementById("fc-autoScroll" + !autoScroll).className = "displaynone";

			//shows the new chat
			document.getElementById("activeChat-" + this.currentChannel).style.display = "block";
			document.querySelector(`#flatChat-channels [data-channel=${this.currentChannel}]`).style.display = "block";
			input.value = this.channels[this.currentChannel].inputText;

			//Auto scrolls if needed
			if (autoScroll) {
				const messageArea = document.querySelector(`#flatChat-channels [data-channel=${this.currentChannel}]`);
				messageArea.scrollTop = messageArea.scrollHeight;
			}
		}

		toggleAutoScroll() {
			this.channels[this.currentChannel].autoScroll = !this.channels[this.currentChannel].autoScroll;
			document.getElementById("fc-autoScrolltrue").classList.toggle("displaynone");
			document.getElementById("fc-autoScrollfalse").classList.toggle("displaynone");
		}

		scrollButtons(btn) {
			const messageArea = document.querySelector(`#flatChat-channels [data-channel=${this.currentChannel}]`);
			if (btn === "up") {
				messageArea.scrollTop -= 100;
			} else if (btn === "down") {
				messageArea.scrollTop += 100
			} else { //btn === bottom
				messageArea.scrollTop = messageArea.scrollHeight;
			}
		}

		scrollChannel(e) {
			//Zoom in/out chat messages
			if (e.shiftKey) {
				let channels = document.getElementById("flatChat-channels");
				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));
				}
				channels.style.setProperty("--fc-size", size + "rem")
				this.config.fontSize = size;
				this.saveConfig();
				return;
			}
		}

		watchIgnorePlayersWords(type, words, replace) {
			//type can be watchedWords, ignoredWords, watchedPlayers, ignoredPlayers
			if(!replace) {
				words += "," + this.config[type];
			};
			words = words.split(",") //split by ,
			.flatMap((word)=>word.split(" ")) //split by spaces
			.map(word=>word.replaceAll("_"," ")) //replace underscore to space
			.filter(word=>word); //remove empty
			this.settings[type] = new Set([...words]);//This is the variable I use to check
			this.config[type] = words.join(",").replaceAll(" ", "_");//Config is saved as string
			this.saveConfig();
		}

		contextMenu(e) {
			const data = e.target.closest("[data-action]");
			if (data) {
				const username = document.getElementById("flatChat-contextUser").dataset.user;
				const action = data.dataset.action;
				const input = document.getElementById("flatChat-input");
				switch (action) {
					case "message": {
						input.value = "/pm " + username + " ";
						input.focus();
					} break;
					case "tabMessage": {
						this.newChannel(username, true);
						this.switchChannel(username, true);
					} break;
					case "profile": {
						Globals.websocket.send("RIGHT_CLICKED_PLAYER=" + username);
					} break;
					case "trade": {
						Globals.websocket.send("SEND_TRADE_REQUEST=" + username);
					} break;
					case "watch": {
						this.watchIgnorePlayersWords("watchedPlayers", username);
					} break;
					case "ignore": {
						this.watchIgnorePlayersWords("ignoredPlayers", username);
					} break;
				}

				const contextMenu = document.getElementById("flatChat-contextMenu");
				contextMenu.style.visibility = "hidden";
			}
		}

		showMessage(data, html = false) {
			// data = {
			//     username: "dounford",
			//     tag: "none",
			//     sigil: "none",
			//     color: "white",
			//     message: "oi gente",
			//     yell: false,
			//     channel: "channel_local"
			//     channel: "private_dounford"
			// }
			if(!data.channel in this.channels) {
				data.channel = "channel_local"
			}
			let message = data.message;

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

			//If the message contains any ignored word it will ignore the message
			if(this.settings.ignoredWords.some(word => message.includes(word))) {
				return;
			}
			//If the message sender is blocked the message will be ignored
			if(this.settings.ignoredPlayers.has(data.username)) {
				return;
			}

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

			//Ping if any watched word is present or if the message contains the username
			if (!this.getConfig("ignorePings") && (message.includes("@" + Globals.local_username) || this.settings.watchedWords.some(word => message.includes(word)))) {
				messageContainer.className = "fc-pingMessages";
				ding.play();
			}

			if (data.color && data.color !== "white" && data.color !== "grey") {
				if (messageColors[data.color]) {
					messageContainer.className += " fc-" + messageColors[data.color]
					if (data.color === "ownPrivate") {
						const ownPrivateSpan = document.createElement("span");
						ownPrivateSpan.innerText = "< "
						messageContainer.appendChild(ownPrivateSpan);
					} else if (data.color === "private") {
						const privateSpan = document.createElement("span");
						privateSpan.innerText = "> "
						messageContainer.appendChild(privateSpan);
					}
				} else {
					//In case a color that doesn't has a variable yet is used
					messageContainer.style.color = data.color;
				}
			}

			//Some of the private messages sent when someone is offline are received (max 10 per player)
			if (data.offlineMessage) {
				const timeStrong = document.createElement("strong");
				timeStrong.innerHTML = this.getDateStr(data.offlineMessage);
				messageContainer.appendChild(timeStrong);
			//only show current time if the setting is true, false by default
			} else if (this.config.showTime) {
				const timeStrong = document.createElement("strong");
				timeStrong.innerHTML = this.getDateStr();
				messageContainer.appendChild(timeStrong);
			}

			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);
			}

			if (data.tag && data.tag !== "none") {
				let tag = document.createElement("span");
			
				tag.innerText = data.tag === "investor-plus" ? "INVESTOR" : data.tag.toUpperCase();
				tag.classList.add("chat-tag-" + data.tag);
			}

			if (data.username) {
				const senderStrong = document.createElement("strong");
				senderStrong.innerText = data.username.replaceAll("_", " ") + ":";
				senderStrong.className = "flatChat-player";
				senderStrong.setAttribute("data-sender", data.username.replaceAll(" ", "_"));
				messageContainer.appendChild(senderStrong);

				if(this.settings.watchedPlayers.has(data.username)) {
					messageContainer.className = "fc-pingMessages";
				}
			} else if(this.getConfig("lessEnergyWarning") && message.startsWith("You are too tired to gain xp")) {//TBD
				const now = Date.now()
				if (this.lastWarning > now - 300000) {
					return
				} else {
					this.lastWarning = now;
				}
			}

			const messageSpan = document.createElement('span');
			if (html) {
				messageSpan.innerHTML = message;
			} else {
				messageSpan.innerText = " " + message;
			}

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

			const messageArea = document.querySelector(`#flatChat-channels [data-channel=${data.channel}]`);
			messageArea.appendChild(messageContainer);

			if(data.channel !== this.currentChannel) {
				const unreadMessages = ++this.channels[data.channel].unreadMessages;
				const unreadSpan = document.getElementById("unreadMessages-" + data.channel);
				unreadSpan.innerText = `[${unreadMessages}]`
				unreadSpan.style.display = "block";
			}

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

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

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

		showOwnMessage(message, channel, color = "white") {
			//I'm getting the sigil in a hacky way, I don't know how to get the tag, so unless Smitty adds a variable for it I can't do much
			const data = {
				username: Globals.local_username,
				tag: "none",
				color,
				message,
				channel
			}
			data.sigil = document.querySelector("#equipment-slot-sigil img").src.slice(33,-4) == "none" ? "none" : "images/ui" + document.querySelector("#equipment-slot-sigil img").src.slice(32);
			this.showMessage(data, false);
		}

		sendMessage() {
			let message = document.getElementById("flatChat-input").value.slice(-LOCAL_CHAT_MAX_LENGTH).trim();
			if (!message) {return};

			document.getElementById("flatChat-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;
				}
			}

			if(this.currentChannel === "channel_local") {
				Globals.websocket.send('CHAT=' + message);
			} else if(this.currentChannel === "channel_global") {
				Globals.websocket.send('CHAT=/yell ' + message);
			} else if (this.currentChannel.startsWith("private_")) {
				const username = this.currentChannel.slice(8);
				Globals.websocket.send(`CHAT=/pm ${username} ${message}`);
			}
		}

		//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)
		}
	}
 
	const plugin = new flatChatPlugin();
	FlatMMOPlus.registerPlugin(plugin);

})();