/*Canopy - The next generation of stoner streaming software Copyright (C) 2024-2026 Rainbownapkin and the TTN Community This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ class profileUpdatePrompt{ constructor(field){ this.field = field; this.contentNode = document.querySelector(`#profile-${field}-content`); this.promptNode = document.querySelector(`#profile-${field}-prompt`); if(this.promptNode != null){ this.setupInput(); } } setupInput(){ this.contentNode.addEventListener('click', this.popPrompt.bind(this)); this.promptNode.addEventListener('keydown', this.closePrompt.bind(this)); } popPrompt(){ this.promptNode.style.display = 'inline'; this.promptNode.focus(); } closePrompt(event){ //Check if we're finished const fin = event.key == "Escape" || (event.key == "Enter" && !event.shiftKey); //IF we are if(fin){ //Display the prompt this.promptNode.style.display = 'none'; } //Return whether or not we're done return fin; } } class profileUpdateTextPrompt extends profileUpdatePrompt{ constructor(field, fillPrompt = false){ //call derived constructor super(field); //Set fill prompt this.fillPrompt = fillPrompt; } popPrompt(){ //Call derived method super.popPrompt(); //For each line break this.contentNode.querySelectorAll('br').forEach((br)=>{ //Replace the linenreaks with newlines to get the text area to play nice br.outerHTML = "\n" }); //If we're filling the prompt if(this.fillPrompt){ //Fill the prompt content and placeholder this.promptNode.value = this.contentNode.textContent; this.promptNode.placeholder = this.field; //otherwise }else{ //Just fill the placeholder this.promptNode.placeholder = this.contentNode.textContent; } //Hide content this.contentNode.style.display = 'none'; } async closePrompt(event){ //Call derived constructor and check if we're finished, assuming we're finished... if(super.closePrompt(event)){ //Display this.contentNode.style.display = 'inline-block'; //If we're not cancelling if(event.key == "Enter" && !event.shiftKey){ //Create empty update object let updateObj = {}; //Fill field updateObj[this.field] = this.promptNode.value; //Send er' off const update = await utils.ajax.updateProfile(updateObj); //Fill content from update, make sure to add line breaks for the bio this.contentNode.textContent = utils.unescapeEntities(update[this.field].replaceAll('\n','
')); } } } } class profileUpdateImagePrompt extends profileUpdatePrompt{ constructor(){ //call derived constructor super('img'); } popPrompt(){ //Call derived method super.popPrompt(); this.promptNode.placeholder = this.contentNode.src; } async closePrompt(event){ //Call derived constructor super.closePrompt(event); //If we're not cancelling if(event.key == "Enter"){ //Create empty update object let updateObj = {}; //Fill field updateObj[this.field] = this.promptNode.value; //Send er' off const update = await utils.ajax.updateProfile(updateObj); //Fill content from update this.contentNode.src = update[this.field]; } } } class tokeList{ constructor(){ this.tokeList = document.querySelector('#profile-tokes'); this.tokeListLabel = document.querySelector('.profile-toke-count'); this.tokeListToggleIcon = document.querySelector('#toggle-toke-list'); this.setupInput(); } setupInput(){ this.tokeListLabel.addEventListener('click', this.toggleTokeList.bind(this)); } toggleTokeList(){ if(this.tokeList.style.visibility == "visible"){ this.tokeList.style.visibility = "collapse"; this.tokeListToggleIcon.classList.replace("bi-caret-down-fill","bi-caret-left-fill"); }else{ this.tokeList.style.visibility = "visible"; this.tokeListToggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill"); } } } class accountSettingsButton{ constructor(field, popupClass){ this.deleteLink = document.querySelector(`#account-settings-${field}-button`); this.popupClass = popupClass; if(this.deleteLink != null){ this.setupInput(); } } setupInput(){ this.deleteLink.addEventListener("click",this.showPopup.bind(this)); } async showPopup(event){ this.popup = new this.popupClass(); } } class changeEmailPopup{ constructor(){ this.popup = new canopyUXUtils.popup("changeEmail", true, this.asyncConstructor.bind(this)); } asyncConstructor(){ this.emailPrompt = document.querySelector('#email-change-popup-email'); this.passPrompt = document.querySelector('#email-change-popup-password'); this.setupInput(); } setupInput(){ this.emailPrompt.addEventListener('keydown', this.emailRequest.bind(this)); this.passPrompt.addEventListener('keydown', this.emailRequest.bind(this)); } async emailRequest(event){ if(event.key == "Enter"){ await utils.ajax.requestEmailChange(this.emailPrompt.value, this.passPrompt.value); } } } class changePasswordPopup{ constructor(){ this.popup = new canopyUXUtils.popup("changePassword", true, this.asyncConstructor.bind(this)); } asyncConstructor(){ this.oldPassPrompt = document.querySelector('#password-change-popup-old-password'); this.newPassPrompt = document.querySelector('#password-change-popup-new-password'); this.confirmPassPrompt = document.querySelector('#password-change-popup-confirm-new-password'); this.setupInput(); } setupInput(){ this.oldPassPrompt.addEventListener('keydown', this.emailRequest.bind(this)); this.newPassPrompt.addEventListener('keydown', this.emailRequest.bind(this)); this.confirmPassPrompt.addEventListener('keydown', this.emailRequest.bind(this)); } async emailRequest(event){ if(event.key == "Enter"){ const updateObj = {}; updateObj.passChange = { oldPass: this.oldPassPrompt.value, newPass: this.newPassPrompt.value, confirmPass: this.confirmPassPrompt.value }; const response = await utils.ajax.updateProfile(updateObj); if(response != null){ //Return user homepage after good pass change, as we've probably been logged out by the server for security. window.location.pathname = '/'; } } } } class ignoredUsersPopup{ constructor(){ this.popup = new canopyUXUtils.popup("ignoredUsers", true, this.asyncConstructor.bind(this), document, false); } asyncConstructor(){ this.ignoredUsersListDiv = document.querySelector('#ignored-users-popup-list-div') this.ignorePrompt = document.querySelector('#ignored-users-popup-prompt'); this.ignoreButton = document.querySelector('#ignored-users-popup-button'); //Refresh ignored list this.refreshList(); this.setupInput(); } //grabs and re-renders list, good for asynchronous fire-and-forget usage async refreshList(){ this.renderUsers(await utils.ajax.getIgnoredUsers()); } renderUsers(users){ //Clear out list div this.ignoredUsersListDiv.innerHTML = ''; //run through our ignored users for(const user of users){ //Create a div about it const userDiv = document.createElement('div'); userDiv.classList.add('ignored-users-popup-ignored-user'); //Create userlabel const userLabel = document.createElement('p'); userLabel.classList.add('ignored-users-popup-ignored-user-label'); userLabel.textContent = user.user; //Create unignore icon const unignoreIcon = document.createElement('i'); unignoreIcon.classList.add('bi-emoji-smile-fill', 'ignored-users-popup-unignore-user-icon'); unignoreIcon.title = `Unignore ${user.user}`; //Make the unignore button actually fuckin' do something for once in its no-good, lazy, yellowbellied, chicken-fucking life unignoreIcon.addEventListener('click', (event)=>{this.unignore(user.user)}); //ASSEMBLE userDiv.append(userLabel, unignoreIcon); //Render out to popup this.ignoredUsersListDiv.appendChild(userDiv); } } setupInput(){ this.ignorePrompt.addEventListener('keydown', this.ignore.bind(this)); this.ignoreButton.addEventListener('click', this.ignore.bind(this)); } async ignore(event){ //If we clicked or hit enter if(event.key == null || event.key == "Enter"){ //Tell server who you wanna ignore and get the new list back const ignoredList = await utils.ajax.ignoreUser(this.ignorePrompt.value); //If we received a good list if(ignoredList != null){ //Render out received ignored lsit this.renderUsers(ignoredList); } //Clear prompt this.ignorePrompt.value = ""; } } async unignore(user){ //Tell server who you wanna unignore and get the new list back const ignoredList = await utils.ajax.unignoreUser(user); //If we received a good list if(ignoredList != null){ //Render out received ignored lsit this.renderUsers(ignoredList); } } } class deleteAccountPopup{ constructor(){ this.popup = new canopyUXUtils.popup("nukeUser", true, this.asyncConstructor.bind(this)); } asyncConstructor(){ this.passwordPrompt = document.querySelector("#delete-account-popup-password"); this.setupInput(); } setupInput(){ this.passwordPrompt.addEventListener("keydown", this.nukeAccount.bind(this)); } async nukeAccount(event){ if(event.key == "Enter"){ await utils.ajax.deleteAccount(event.target.value); } } } //Object Instantiation const imgPrompt = new profileUpdateImagePrompt(); const pronounsPrompt = new profileUpdateTextPrompt('pronouns'); const signaturePrompt = new profileUpdateTextPrompt('signature'); const bioPrompt = new profileUpdateTextPrompt('bio', true); const profileTokeList = new tokeList(); const accountEmailChange = new accountSettingsButton('update-email', changeEmailPopup); const accountPasswordChange = new accountSettingsButton('change-password', changePasswordPopup); const accountIgnoredUsers = new accountSettingsButton('change-password', changePasswordPopup); const accountDeleteButton = new accountSettingsButton('ignored-users', ignoredUsersPopup);