/*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);