IdlePixel Group Chat

A private group chat panel

// ==UserScript==
// @name			IdlePixel Group Chat
// @namespace		luxferre.dev
// @version			2.1.2
// @description		A private group chat panel
// @author			Lux-Ferre
// @license			MIT
// @match			*://idle-pixel.com/login/play*
// @grant			none
// @require			https://greasyfork.org/scripts/441206-idlepixel/code/IdlePixel+.js?anticache=20220905
// @require			https://greasyfork.org/scripts/484046/code/IdlePixel%2B%20Custom%20Handling.js?anticache=20240721
// @require		 	https://greasyfork.org/scripts/491983-idlepixel-plugin-paneller/code/IdlePixel%2B%20Plugin%20Paneller.js?anticache=20240721
// ==/UserScript==
 
(function() {
	'use strict';

	class GroupChatPlugin extends IdlePixelPlusPlugin {
		constructor() {
			super("groupChat", {
				about: {
					name: GM_info.script.name,
					version: GM_info.script.version,
					author: GM_info.script.author,
					description: GM_info.script.description
				},
			});
			this.groups = {}
			this.activeGroup = undefined
			this.invitations = {}
			this.chats = {}
		}

		saveData(){
			const user = window["var_username"]
			const groupsJSON = JSON.stringify(this.groups)
			localStorage.setItem(`groupChatGroups${user}`, groupsJSON)

			const invitationsJSON = JSON.stringify(this.invitations)
			localStorage.setItem(`groupChatInvitations${user}`, invitationsJSON)
		}

		loadData(){
			const user = window["var_username"]
			const groupData = localStorage.getItem(`groupChatGroups${user}`)
			if (groupData){
				this.groups = JSON.parse(groupData)
			} else {
				this.groups = {}
			}

			const invitationData = localStorage.getItem(`groupChatInvitations${user}`)
			if (invitationData){
				this.invitations = JSON.parse(invitationData)
			} else {
				this.invitations = {}
			}
		}
 
		createPanel(){
			IdlePixelPlus.addPanel("groupchat", "Group Chat Panel", function() {
				return `
<div class="groupChatUIContainer w-100">
    <div id="groupChatInfoModule" class="row groupChatUIModule">
        <div class="col">
            <div class="row">
                <div class="col-4 text-end align-self-center groupChatInfoContainer">
                    <div class="row gx-0">
                        <div id="groupChatGroupNotification" class="col-1 text-center align-self-center groupChatGroupNotificationInactive" onclick="IdlePixelPlus.plugins.groupChat.showInvitationModal()"><span>!</span></div>
                        <div class="col-11 align-self-center"><select id="groupChatGroupSelector" class="w-100"></select></div>
                    </div>
                </div>
                <div class="col-8 d-flex groupChatInfoContainer">
                    <div id="groupChatMembersContainer" class="d-flex align-items-center"><span>Members:</span></div>
                </div>
            </div>
        </div>
    </div>
    <div id="groupChatChatModule" class="row groupChatUIModule">
        <div class="col">
            <div class="row">
                <div id="groupChatChatFormContainer" class="col">
                    <div id="groupChatChatBox" class="overflow-auto"></div>
                    <form onsubmit="event.preventDefault(); IdlePixelPlus.plugins.groupChat.sendGroupChatButton();">
                        <div class="row d-flex flex-fill">
                            <div class="col-11"><input id="groupChatChatIn" class="form-control w-100" type="text" /></div>
                            <div class="col-1"><input id="groupChatChatButton" class="w-100 h-100" type="submit" value="Send" /></div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <div id="groupChatSettingsModules" class="row groupChatUIModule">
        <div class="col">
            <div class="row">
                <div id="groupChatSettings" class="col d-flex justify-content-around align-self-center"><button id="groupChatCreateGroupButton" class="btn btn-info" type="button" onclick="IdlePixelPlus.plugins.groupChat.createGroupButton()">Create Group</button><button id="groupChatShareRaidButton" class="btn btn-info groupChatInGroupButton" type="button" onclick="IdlePixelPlus.plugins.groupChat.shareRaidButton()">Share Raid</button><button id="groupChatInviteButton" class="btn btn-info groupChatOwnerButton groupChatInGroupButton" type="button" onclick="IdlePixelPlus.plugins.groupChat.inviteButton()">Invite</button><button id="groupChatUninviteButton" class="btn btn-info groupChatOwnerButton groupChatInGroupButton" type="button" onclick="IdlePixelPlus.plugins.groupChat.uninviteButton()">Uninvite</button><button id="groupChatRemovePlayerButton" class="btn btn-info disabled groupChatOwnerButton groupChatInGroupButton" type="button" disabled>Remove</button></div>
            </div>
        </div>
    </div>
</div>
				`
			});
		}

		addStyles(){
			let backgroundColour
			let textColour

			if ("ui-tweaks" in IdlePixelPlus.plugins){
				backgroundColour = IdlePixelPlus.plugins["ui-tweaks"].config["color-chat-area"]
				textColour = IdlePixelPlus.plugins["ui-tweaks"].config["font-color-chat-area"]
			} else {
				backgroundColour = "white"
				textColour = "black"
			}

			$("head").append(`
				<style id="styles-groupchat">
					#groupChatInvitationsModalInner {
					  background-color: ${backgroundColour};
					  color: ${textColour};
					}

					.groupChatUIModule {
					  border: outset 2px;
					}

					.groupChatUIContainer {
					  width: 100%;
					  height: 100%;
					  padding: 5px;
					  margin: 0;
					}

					#groupChatChatBox {
					  width: 100%;
					  height: 70vh;
					  margin-top: 10px;
					}

					#groupChatChatBox {
					  border: inset 1px;
					}

					.groupChatGroupNotificationActive {
					  color: red;
					  border: outset;
					  border-color: red;
					}

					.groupChatGroupNotificationInactive {
					  color: gray;
					  border: outset;
					  border-color: gray;
					}

					.groupChatInfoContainer {
					  border: inset;
					}

					#groupChatSelectGroupDrop {
					  border: solid;
					  border-width: 1px;
					}

					.groupChatMember {
					  margin: 0px 3px;
					  border: 1px groove;
					}

					#groupChatGroupNotification {
					  cursor: pointer;
					}

					.groupChatInvitation {
					  background-color: RGBA(1, 150, 150, 0.5);
					  margin-bottom: 2px;
					}

					.groupChatInvCheck {
					  cursor: pointer;
					}

					.groupChatInvCross {
					  cursor: pointer;
					  margin-right: 5px;
					}

					.groupChatInvData {
					  margin-left: 10px;
					}

					#groupChatModalHeader {
					  padding: calc(var(--bs-modal-padding) - var(--bs-modal-header-gap) * .5);
					  background-color: var(--bs-modal-header-bg);
					  border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);
					  border-top-right-radius: var(--bs-modal-inner-border-radius);
					  border-top-left-radius: var(--bs-modal-inner-border-radius);
					}

					#groupChatInvitationsModal .modal-body {
					  overflow-y: auto;
					}
				</style>
			`)
		}
	
		onLogin() {
			Paneller.registerPanel("groupchat", "Group Chat")

			this.loadData()
			this.createPanel()
			this.addStyles()
			this.createNotification()
			this.updatePanelInfo()
			this.createInvModal()
			this.updateInvitationNotification()

			$('#groupChatGroupSelector').on('change', function() {
				IdlePixelPlus.plugins.groupChat.joinGroup(this.value)
			});

			$("#groupChatChatModule").hide()
			$(".groupChatInGroupButton").hide()
		}

		onPanelChanged(panelBefore, panelAfter){
			if (panelAfter==="groupchat"){
				$("#groupChatNotification").hide()
			}
		}
 
		onConfigsChanged() {
			this.updatePanelInfo();
		}

		createInvModal(){
			const modalString = `
				<div id="groupChatInvitationsModal" class="modal fade" role="dialog" tabindex="-1"">
				    <div class="modal-dialog modal-dialog-centered" role="document">
				        <div id="groupChatInvitationsModalInner" class="modal-content">
							<div id="groupChatModalHeader" class="modal-header text-center">
								<h3 class="modal-title w-100"><u>Pending Invitations</u></h3>
							</div>
				            <div class="modal-body">
				                <div id="groupChatInvitationsModalList" class="overflow-auto"></div>
				            </div>
				        </div>
				    </div>
				</div>
			`

			const modalElement = $.parseHTML(modalString)
			$(document.body).append(modalElement)
		}

		addInvitationsToModal(){
			const invitationsModalList = $("#groupChatInvitationsModalList")
			invitationsModalList.empty()

			for (const [groupName, inviter] of Object.entries(this.invitations)){
				const newItemString = `<div class="d-flex justify-content-between rounded-pill groupChatInvitation"><span class="groupChatInvData">${groupName} | ${inviter}</span><div><span data-groupName="${groupName}" class="groupChatInvCheck">✔️</span> | <span data-groupName="${groupName}" class="groupChatInvCross">❌</span></div></div>`
				const newItemElement = $.parseHTML(newItemString)
				invitationsModalList.append(newItemElement)
			}

			$(".groupChatInvCheck").attr("onclick", "IdlePixelPlus.plugins.groupChat.acceptInvitation(this.getAttribute('data-groupName'))")
			$(".groupChatInvCross").attr("onclick", "IdlePixelPlus.plugins.groupChat.rejectInvitation(this.getAttribute('data-groupName'))")
		}

		showInvitationModal(){
			document.body.scrollTop = document.documentElement.scrollTop = 0;

			this.addInvitationsToModal()

			$('#groupChatInvitationsModal').modal('show')
		}
 
		onCustomMessageReceived(player, content, callbackId) {
			const customData = Customs.parseCustom(player, content, callbackId)
			if(customData.plugin==="groupchat") {
				if(customData.command==="chat"){
					this.handleReceivedChat(customData)
				} else if (customData.command==="invite"){
					this.onInviteReceived(customData.player, customData.payload, customData.callbackId)
				} else if (customData.command==="accept"){
					this.onAcceptedInvite(customData.player, customData.payload)
				} else if (customData.command==="reject"){
					this.onRejectedInvite(customData.player, customData.payload)
				} else if (customData.command==="unaccept"){
					this.onUnacceptInvite(customData.player, customData.payload)
				} else if (customData.command==="join"){
					this.addGroup(customData.player, customData.payload)
				}
			}
		}

		handleReceivedChat(data){
			const player = data.player
			const splitData = data.payload.split(";")
			const groupName = splitData[0]
			const givenPassword = splitData[1]
			const message = splitData.slice(2).join(";")

			if(!(groupName in this.groups)){console.log(`Group ${groupName} doesn't exist.`); return}

			const correctPassword = this.groups[groupName].password
			if(givenPassword!==correctPassword){console.log(`Incorrct password given`); return}

			if (!(this.groups[groupName].members.includes(player))){
				this.addPlayerToGroup(player, groupName)
			}

			const newMessageString = `<div class=""><span class="color-green">${Chat._get_time()}</span><span><strong>${player}: </strong></span><span>${sanitize_input(message)}</span></div>`

			if(!(groupName in this.chats)){this.chats[groupName] = []}

			this.chats[groupName].push(newMessageString)

			if(this.activeGroup.name === groupName){
				this.addMessageToChat(newMessageString)
			}
		}
	
		updatePanelInfo(){
			this.updateGroupsList()
		}
	
		createNotification(){
			const notificationString = `
			<div id="groupChatNotification" class="notification hover" onclick="IdlePixelPlus.setPanel('groupchat')">
        		<img src="https://d1xsc8x7nc5q8t.cloudfront.net/images/meteor_radar_detector.png" class="w20" alt="">
        		<span class="font-small color-yellow">Group Chat</span>
    		</div>
			`

			const notificationElement = $.parseHTML(notificationString)
			const notificationBar = $("#notifications-area")

			notificationBar.append(notificationElement)
			$("#groupChatNotification").hide()
		}

		showNotification(){
			if(Globals.currentPanel === "panel-groupchat"){return;}
			
			$("#groupChatNotification").show()
		}

		sendGroupChat(chatMessage){
			if(!this.activeGroup){console.log("No active Group!"); return}
			const password = this.activeGroup.password
			const memberList = this.activeGroup.members
			const groupName = this.activeGroup.name

			memberList.forEach(member => {
				Customs.sendBasicCustom(member, "groupchat", "chat", `${groupName};${password};${chatMessage}`)
			})
		}

		sendGroupChatButton(){
			const chatIn = $("#groupChatChatIn")
			const chatMessage = chatIn.val()
			chatIn.val("")
			
			this.sendGroupChat(chatMessage)
		}

		addMessageToChat(messageText){
			const chatBox = $("#groupChatChatBox")

			const messageElement = $.parseHTML(messageText)
			chatBox.append(messageElement);

			chatBox.scrollTop(chatBox[0].scrollHeight);

			this.showNotification()
		}

		genPass(){
			return Math.random().toString(36).slice(2)
		}

		createGroup(groupName){
			if (groupName in this.groups){
				console.log("Group already exists")
				return
			}

			this.groups[groupName] = {
				name: groupName,
				members: [window["var_username"]],
				invited_members: [],
				password: this.genPass(),
				owner: window["var_username"]
			}

			this.updateGroupsList()
			this.saveData()
		}

		joinGroup(group_name){
			const loadedGroup = this.groups[group_name]
			this.activeGroup = {
				name: group_name,
				members: loadedGroup.members,
				password: loadedGroup.password,
				owner: loadedGroup.owner,
				online_members: new Set(),
			}

			this.updateMembersList()

			$("#groupChatChatModule").show()
			$(".groupChatInGroupButton").show()

			const ownerButtons = $(".groupChatOwnerButton")

			if (this.activeGroup.owner === window["var_username"]){
				ownerButtons.show()
			} else {
				ownerButtons.hide()
			}

			const chatBox = $("#groupChatChatBox")

			chatBox.empty()

			if(!(group_name in this.chats)){return}

			this.chats[group_name].forEach(chatMessage=>{
				this.addMessageToChat(chatMessage)
			})

			chatBox.scrollTop(chatBox[0].scrollHeight);
		}

		addPlayerToGroup(player, group){
			if (this.groups[group].members.includes(player)){return}
			this.groups[group].members.push(player)

			this.updateMembersList()
			this.saveData()
		}

		getGroups(){
			return Object.keys(this.groups)
		}

		updateGroupsList(){
			const groups = this.getGroups()
			const groupSelector = $("#groupChatGroupSelector")
			groupSelector.empty()

			groupSelector.append(`<option disabled selected value=""> -- select a group -- </option>`)

			groups.forEach(groupName=>{
				groupSelector.append(`<option value="${groupName}">${groupName}</option>`)
			})
		}

		updateMembersList(){
			const membersDisplay = $("#groupChatMembersContainer")
			membersDisplay.empty()
			membersDisplay.append(`<span>Members: </span>`)
			this.activeGroup.members.forEach(member=>{
				membersDisplay.append(`<span class="rounded-pill groupChatMember">&nbsp ${member} &nbsp</span>`)
			})
		}

		addPlayertoInvited(player, group){
			this.groups[group].invited_members.push(player)
			this.saveData()
		}

		sendInvitation(player){
			if(!this.activeGroup){return}
			if(this.activeGroup.owner !== window["var_username"]){
				console.log("Only the owner can invite players.")
				return
			}
			if(this.activeGroup.members.length >7){
				console.log("Group too big to invite more players!")
				return
			}

			IdlePixelPlus.sendCustomMessage(player, {
				content: `groupchat:invite:${this.activeGroup.name}`,
				onResponse: function(player, content, callbackId) {
					IdlePixelPlus.plugins.groupChat.addPlayertoInvited(player, content)
					console.log("Player invited!")
					return true;
					},
				onOffline: function(player, content) {
					console.log("Cannot invite offline player!")
					return true;
					},
				timeout: 2000 // callback expires after 2 seconds
			});

		}

		onInviteReceived(player, groupName, callback){
			this.invitations[groupName] = player
			this.updateInvitationNotification()
			IdlePixelPlus.sendCustomMessage(player, {
				content: `${groupName}`,
				callbackId: callback,
				onResponse: function(player, content, callbackId) {return true;},
				onOffline: function(player, content) {return true;},
				timeout: 2000 // callback expires after 2 seconds
			});
			this.saveData()
		}

		acceptInvitation(groupName){
			Customs.sendBasicCustom(this.invitations[groupName], "groupchat", "accept", groupName)
			delete this.invitations[groupName]
			this.addInvitationsToModal()
			this.updateInvitationNotification()
			this.saveData()
		}

		rejectInvitation(groupName){
			Customs.sendBasicCustom(this.invitations[groupName], "groupchat", "reject", `${groupName};Player rejected invitation.`)
			delete this.invitations[groupName]
			this.addInvitationsToModal()
			this.updateInvitationNotification()
			this.saveData()
		}

		onAcceptedInvite(player, groupName){
			if(!(groupName in this.groups)){return}
			if (this.groups[groupName].members.length >= 8){
				console.log("Group too big!")
				Customs.sendBasicCustom(player, "groupchat", "unaccept", "Group has too many members")
				return
			}

			if(!this.groups[groupName].invited_members.includes(player)){console.log("Univited player"); return;}

			this.groups[groupName].invited_members = this.groups[groupName].invited_members.filter(item => item !== player)
			this.addPlayerToGroup(player, groupName)

			let data_string = `${groupName};${this.groups[groupName]["password"]};`

			let membersString = ""
			this.groups[groupName].members.forEach(member=>{
				membersString+=`,${member}`
			})
			membersString = membersString.slice(1)
			data_string += membersString
			Customs.sendBasicCustom(player, "groupchat", "join", data_string)
			this.saveData()
		}

		onRejectedInvite(player, data){
			const splitData = data.split(";")
			const groupName = splitData[0]
			const reason = splitData.slice(1).join(";")
			this.groups[groupName].invited_members = this.groups[groupName].invited_members.filter(item => item !== player)
			console.log(`${player} could not be added to ${groupName} for reason: ${reason}`)
			this.saveData()
		}

		onUnacceptInvite(player, data){
			const splitData = data.split(";")
			const groupName = splitData[0]
			const reason = splitData.slice(1).join(";")
			console.log(`Could not be added to ${groupName} for reason: ${reason}`)
		}

		createGroupButton(){
			const newGroup = prompt("Group Name:")
			this.createGroup(newGroup)
		}

		shareRaidButton(){
			const raidCode = $("#raids-team-panel-uuid").text()
			if (raidCode===""){return}
			this.sendGroupChat(raidCode)
		}

		inviteButton(){
			const newPlayer = prompt("Player Name:")
			this.sendInvitation(newPlayer)
		}

		uninviteButton(){
			const player = prompt("Player Name:")
			const groupName = this.activeGroup.name

			this.groups[groupName].invited_members = this.groups[groupName].invited_members.filter(item => item !== player)
			this.saveData()
		}

		addGroup(player, data){
			const splitData = data.split(";")
			const groupName = splitData[0]
			const password = splitData[1]
			const membersList = splitData[2].split(",")

			if (groupName in this.groups){
				console.log("Group already exists")
				return
			}

			this.groups[groupName] = {
				name: groupName,
				members: membersList,
				invited_members: [],
				password: password,
				owner: player
			}

			this.updateGroupsList()
			this.saveData()
		}

		updateInvitationNotification(){
			const notificationButton = $("#groupChatGroupNotification")
			if (Object.keys(this.invitations).length){
				notificationButton.removeClass("groupChatGroupNotificationInactive")
				notificationButton.addClass("groupChatGroupNotificationActive")
			} else {
				notificationButton.removeClass("groupChatGroupNotificationActive")
				notificationButton.addClass("groupChatGroupNotificationInactive")
			}
		}
	}

	const plugin = new GroupChatPlugin();
	IdlePixelPlus.registerPlugin(plugin);
 
})();