Finished up with profile tooltips and context-menus.

This commit is contained in:
rainbow napkin 2025-01-08 04:08:27 -05:00
parent 9a8def18d7
commit b56c9a3365
10 changed files with 370 additions and 335 deletions

View file

@ -35,6 +35,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
</body> </body>
<footer> <footer>
<%- include('partial/scripts', {user}); %> <%- include('partial/scripts', {user}); %>
<script src="/js/adminUtils.js"></script>
<script src="/js/popup/banPopup.js"></script>
<script src="/js/adminPanel.js"></script> <script src="/js/adminPanel.js"></script>
</footer> </footer>
</html> </html>

View file

@ -33,6 +33,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<footer> <footer>
<%- include('partial/scripts', {user}); %> <%- include('partial/scripts', {user}); %>
<script src="/socket.io/socket.io.min.js"></script> <script src="/socket.io/socket.io.min.js"></script>
<script src="/js/adminUtils.js"></script>
<script src="/js/popup/banPopup.js"></script>
<script src="/js/channel/commandPreprocessor.js"></script> <script src="/js/channel/commandPreprocessor.js"></script>
<script src="/js/channel/chatPostprocessor.js"></script> <script src="/js/channel/chatPostprocessor.js"></script>
<script src="/js/channel/chat.js"></script> <script src="/js/channel/chat.js"></script>

View file

@ -36,6 +36,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<button href="javascript:" class="danger-button" id="chan-delete">Delete Channel</button> <button href="javascript:" class="danger-button" id="chan-delete">Delete Channel</button>
<footer> <footer>
<%- include('partial/scripts', {user}); %> <%- include('partial/scripts', {user}); %>
<script src="/js/popup/banPopup.js"></script>
<script src="/js/channelSettings.js"></script> <script src="/js/channelSettings.js"></script>
</footer> </footer>
</body> </body>

View file

@ -194,6 +194,7 @@ span.user-entry{
margin: 0 1em 0 0.1em; margin: 0 1em 0 0.1em;
font-size: 1em; font-size: 1em;
width: fit-content; width: fit-content;
user-select: none;
} }
.whisper{ .whisper{

View file

@ -181,5 +181,5 @@ p.tooltip, h3.tooltip{
} }
.context-menu button{ .context-menu button{
margin: 0.05em 0; margin: 2px 0;
} }

View file

@ -14,284 +14,6 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
class canopyAdminUtils{
constructor(){
}
//Statics
static banUserPopup = class{
constructor(target, cb){
this.target = target;
this.cb = cb;
this.popup = new canopyUXUtils.popup("userBan", true, this.asyncConstruction.bind(this));
}
asyncConstruction(){
this.title = document.querySelector(".popup-title");
//Setup title text real quick-like :P
this.title.innerHTML = `Ban ${this.target}`;
this.ipBan = document.querySelector("#ban-popup-ip");
this.permBan = document.querySelector("#ban-popup-perm");
this.expiration = document.querySelector("#ban-popup-expiration");
this.expirationPrefix = document.querySelector("#ban-popup-expiration-prefix");
this.banButton = document.querySelector("#ban-popup-ban-button");
this.cancelButton = document.querySelector("#ban-popup-cancel-button");
this.setupInput();
}
setupInput(){
this.permBan.addEventListener("change", this.permaBanLabel.bind(this));
this.cancelButton.addEventListener("click", this.popup.closePopup.bind(this.popup));
this.banButton.addEventListener("click",this.ban.bind(this));
}
permaBanLabel(event){
if(event.target.checked){
this.expirationPrefix.innerHTML = "Account Deletion In: "
this.expiration.value = 30;
}else{
this.expirationPrefix.innerHTML = "Ban Expires In: "
this.expiration.value = 14;
}
}
async ban(event){
//Close out the popup
this.popup.closePopup();
//Submit the user ban based on input
const data = await adminUtil.banUser(this.target, this.permBan.checked, this.ipBan.checked, this.expiration.value);
//For some reason comparing this against undefined or null wasnt working in and of itself...
if(data != null){
//Why add an extra get request when we already have the data? :P
await this.cb(data);
}
}
}
//Methods
async setPermission(permMap){
var response = await fetch(`/api/admin/permissions`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({permissionsMap: Object.fromEntries(permMap)})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async setChannelOverride(permMap){
var response = await fetch(`/api/admin/permissions`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({channelPermissionsMap: Object.fromEntries(permMap)})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async setUserRank(user, rank){
var response = await fetch(`/api/admin/changeRank`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user, rank})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getBans(){
var response = await fetch(`/api/admin/ban`,{
method: "GET"
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async banUser(user, permanent, ipBan, expirationDays){
var response = await fetch(`/api/admin/ban`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user, permanent, ipBan, expirationDays})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async unbanUser(user){
var response = await fetch(`/api/admin/ban`,{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getTokeCommands(){
var response = await fetch(`/api/admin/tokeCommands`,{
method: "GET"
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async addTokeCommand(command){
var response = await fetch(`/api/admin/tokeCommands`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({command})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async deleteTokeCommand(command){
var response = await fetch(`/api/admin/tokeCommands`,{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({command})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getEmotes(){
var response = await fetch(`/api/admin/emote`,{
method: "GET"
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async addEmote(name, link){
var response = await fetch(`/api/admin/emote`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({name, link})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async deleteEmote(name){
var response = await fetch(`/api/admin/emote`,{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({name})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async genPasswordResetLink(user){
var response = await fetch(`/api/admin/genPasswordReset`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
}
class adminUserList{ class adminUserList{
constructor(){ constructor(){
this.userNames = document.querySelectorAll(".admin-user-list-name"); this.userNames = document.querySelectorAll(".admin-user-list-name");
@ -361,7 +83,7 @@ class adminUserList{
banPopup(event){ banPopup(event){
const user = event.target.id.replace("admin-user-list-ban-icon-",""); const user = event.target.id.replace("admin-user-list-ban-icon-","");
new canopyAdminUtils.banUserPopup(user, userBanList.renderBanList.bind(userBanList)); new banUserPopup(user, userBanList.renderBanList.bind(userBanList));
} }
updateSelect(update, select){ updateSelect(update, select){
@ -785,7 +507,6 @@ class adminEmoteList{
} }
} }
const adminUtil = new canopyAdminUtils();
const userList = new adminUserList(); const userList = new adminUserList();
const permissionList = new adminPermissionList(); const permissionList = new adminPermissionList();
const userBanList = new adminUserBanList(); const userBanList = new adminUserBanList();

241
www/js/adminUtils.js Normal file
View file

@ -0,0 +1,241 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 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 <https://www.gnu.org/licenses/>.*/
class canopyAdminUtils{
constructor(){
}
//Methods
async setPermission(permMap){
var response = await fetch(`/api/admin/permissions`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({permissionsMap: Object.fromEntries(permMap)})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async setChannelOverride(permMap){
var response = await fetch(`/api/admin/permissions`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({channelPermissionsMap: Object.fromEntries(permMap)})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async setUserRank(user, rank){
var response = await fetch(`/api/admin/changeRank`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user, rank})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getBans(){
var response = await fetch(`/api/admin/ban`,{
method: "GET"
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async banUser(user, permanent, ipBan, expirationDays){
var response = await fetch(`/api/admin/ban`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user, permanent, ipBan, expirationDays})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async unbanUser(user){
var response = await fetch(`/api/admin/ban`,{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getTokeCommands(){
var response = await fetch(`/api/admin/tokeCommands`,{
method: "GET"
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async addTokeCommand(command){
var response = await fetch(`/api/admin/tokeCommands`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({command})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async deleteTokeCommand(command){
var response = await fetch(`/api/admin/tokeCommands`,{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({command})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async getEmotes(){
var response = await fetch(`/api/admin/emote`,{
method: "GET"
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async addEmote(name, link){
var response = await fetch(`/api/admin/emote`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({name, link})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async deleteEmote(name){
var response = await fetch(`/api/admin/emote`,{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({name})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
async genPasswordResetLink(user){
var response = await fetch(`/api/admin/genPasswordReset`,{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user})
});
if(response.status == 200){
return await response.json();
}else{
utils.ux.displayResponseError(await response.json());
}
}
}
const adminUtil = new canopyAdminUtils();

View file

@ -135,6 +135,20 @@ class userList{
["Toke With", ()=>{client.chatBox.tokeWith(user.user)}], ["Toke With", ()=>{client.chatBox.tokeWith(user.user)}],
]); ]);
if(user.user != "Tokebot"){
if(client.user.permMap.chan.get("kickUser")){
menuMap.set("Kick", ()=>{client.chatBox.commandPreprocessor.preprocess(`!kick ${user.user}`)});
}
if(client.user.permMap.chan.get("banUser")){
menuMap.set("Channel Ban", ()=>{new chanBanUserPopup(client.channelName, user.user);});
}
if(client.user.permMap.site.get("banUser")){
menuMap.set("Site Ban", ()=>{new banUserPopup(user.user);});
}
}
//Display the menu //Display the menu
utils.ux.displayContextMenu(event, user.user, menuMap); utils.ux.displayContextMenu(event, user.user, menuMap);
} }

View file

@ -170,7 +170,7 @@ class banList{
} }
async ban(event){ async ban(event){
new banUserPopup(this.channel, this.banPrompt.value, this.updateList.bind(this)); new chanBanUserPopup(this.channel, this.banPrompt.value, this.updateList.bind(this));
} }
async unban(event){ async unban(event){
@ -220,59 +220,6 @@ class banList{
} }
} }
class banUserPopup{
constructor(channel, target, cb){
this.channel = channel;
this.target = target;
this.cb = cb;
this.popup = new canopyUXUtils.popup("channelUserBan", true, this.asyncConstruction.bind(this));
}
asyncConstruction(){
this.title = document.querySelector(".popup-title");
//Setup title text real quick-like :P
this.title.innerHTML = `Ban ${this.target}`;
this.permBan = document.querySelector("#ban-popup-perm");
this.expiration = document.querySelector("#ban-popup-expiration");
this.expirationPrefix = document.querySelector("#ban-popup-expiration-prefix");
this.banAlts = document.querySelector("#ban-popup-alts");
this.banButton = document.querySelector("#ban-popup-ban-button");
this.cancelButton = document.querySelector("#ban-popup-cancel-button");
this.setupInput();
}
setupInput(){
this.permBan.addEventListener("change", this.permaBanLabel.bind(this));
this.cancelButton.addEventListener("click", this.popup.closePopup.bind(this.popup));
this.banButton.addEventListener("click",this.ban.bind(this));
}
permaBanLabel(event){
if(event.target.checked){
this.expiration.disabled = true;
}else{
this.expiration.disabled = false;
}
}
async ban(event){
//Get expiration days
const expirationDays = this.permBan.checked ? -1 : this.expiration.value;
//Send ban request off to server and retrieve new ban list
const data = await utils.ajax.chanBan(this.channel, this.target, expirationDays, this.banAlts.checked);
//Close the popup
this.popup.closePopup();
//If we have data and a callback, run the callback with our data
if(data != null && this.cb != null){
await this.cb(data);
}
}
}
class prefrenceList{ class prefrenceList{
constructor(channel){ constructor(channel){
this.channel = channel; this.channel = channel;

106
www/js/popup/banPopup.js Normal file
View file

@ -0,0 +1,106 @@
class banUserPopup{
constructor(target, cb){
this.target = target;
this.cb = cb;
this.popup = new canopyUXUtils.popup("userBan", true, this.asyncConstruction.bind(this));
}
asyncConstruction(){
this.title = document.querySelector(".popup-title");
//Setup title text real quick-like :P
this.title.innerHTML = `Ban ${this.target}`;
this.ipBan = document.querySelector("#ban-popup-ip");
this.permBan = document.querySelector("#ban-popup-perm");
this.expiration = document.querySelector("#ban-popup-expiration");
this.expirationPrefix = document.querySelector("#ban-popup-expiration-prefix");
this.banButton = document.querySelector("#ban-popup-ban-button");
this.cancelButton = document.querySelector("#ban-popup-cancel-button");
this.setupInput();
}
setupInput(){
this.permBan.addEventListener("change", this.permaBanLabel.bind(this));
this.cancelButton.addEventListener("click", this.popup.closePopup.bind(this.popup));
this.banButton.addEventListener("click",this.ban.bind(this));
}
permaBanLabel(event){
if(event.target.checked){
this.expirationPrefix.innerHTML = "Account Deletion In: "
this.expiration.value = 30;
}else{
this.expirationPrefix.innerHTML = "Ban Expires In: "
this.expiration.value = 14;
}
}
async ban(event){
//Close out the popup
this.popup.closePopup();
//Submit the user ban based on input
const data = await adminUtil.banUser(this.target, this.permBan.checked, this.ipBan.checked, this.expiration.value);
//For some reason comparing this against undefined or null wasnt working in and of itself...
if(data != null && this.cb != null){
//Why add an extra get request when we already have the data? :P
await this.cb(data);
}
}
}
class chanBanUserPopup{
constructor(channel, target, cb){
this.channel = channel;
this.target = target;
this.cb = cb;
this.popup = new canopyUXUtils.popup("channelUserBan", true, this.asyncConstruction.bind(this));
}
asyncConstruction(){
this.title = document.querySelector(".popup-title");
//Setup title text real quick-like :P
this.title.innerHTML = `Ban ${this.target}`;
this.permBan = document.querySelector("#ban-popup-perm");
this.expiration = document.querySelector("#ban-popup-expiration");
this.expirationPrefix = document.querySelector("#ban-popup-expiration-prefix");
this.banAlts = document.querySelector("#ban-popup-alts");
this.banButton = document.querySelector("#ban-popup-ban-button");
this.cancelButton = document.querySelector("#ban-popup-cancel-button");
this.setupInput();
}
setupInput(){
this.permBan.addEventListener("change", this.permaBanLabel.bind(this));
this.cancelButton.addEventListener("click", this.popup.closePopup.bind(this.popup));
this.banButton.addEventListener("click",this.ban.bind(this));
}
permaBanLabel(event){
if(event.target.checked){
this.expiration.disabled = true;
}else{
this.expiration.disabled = false;
}
}
async ban(event){
//Get expiration days
const expirationDays = this.permBan.checked ? -1 : this.expiration.value;
//Send ban request off to server and retrieve new ban list
const data = await utils.ajax.chanBan(this.channel, this.target, expirationDays, this.banAlts.checked);
//Close the popup
this.popup.closePopup();
//If we have data and a callback, run the callback with our data
if(data != null && this.cb != null){
await this.cb(data);
}
}
}