improving context menus

This commit is contained in:
MathMan05 2025-01-16 22:15:11 -06:00
parent ec08cdfde0
commit 3d06440053
10 changed files with 404 additions and 251 deletions

View file

@ -73,56 +73,73 @@ class Channel extends SnowFlake {
this.mute_config = settings.mute_config;
}
static setupcontextmenu() {
this.contextmenu.addbutton(
() => I18n.getTranslation("channel.copyId"),
function (this: Channel) {
navigator.clipboard.writeText(this.id);
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("channel.markRead"),
function (this: Channel) {
this.readbottom();
},
);
this.contextmenu.addbutton(
() => I18n.getTranslation("channel.settings"),
//TODO invite icon
this.contextmenu.addButton(
() => I18n.getTranslation("channel.makeInvite"),
function (this: Channel) {
this.generateSettings();
this.createInvite();
},
null,
function () {
return this.hasPermission("MANAGE_CHANNELS");
{
visable: function () {
return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4;
},
color: "blue",
},
);
this.contextmenu.addbutton(
() => I18n.getTranslation("channel.delete"),
function (this: Channel) {
this.deleteChannel();
},
null,
function () {
return this.isAdmin();
},
);
this.contextmenu.addbutton(
this.contextmenu.addSeperator();
//TODO notifcations icon
this.contextmenu.addButton(
() => I18n.getTranslation("guild.notifications"),
function () {
this.setnotifcation();
},
);
this.contextmenu.addbutton(
() => I18n.getTranslation("channel.makeInvite"),
this.contextmenu.addButton(
() => I18n.getTranslation("channel.settings"),
function (this: Channel) {
this.createInvite();
this.generateSettings();
},
null,
function () {
return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4;
{
visable: function () {
return this.hasPermission("MANAGE_CHANNELS");
},
icon: {
css: "svg-settings",
},
},
);
this.contextmenu.addButton(
() => I18n.getTranslation("channel.delete"),
function (this: Channel) {
this.deleteChannel();
},
{
visable: function () {
//TODO there is no way that this is correct
return this.isAdmin();
},
icon: {
css: "svg-delete",
},
color: "red",
},
);
this.contextmenu.addSeperator();
//TODO copy ID icon
this.contextmenu.addButton(
() => I18n.getTranslation("channel.copyId"),
function (this: Channel) {
navigator.clipboard.writeText(this.id);
},
);
}

View file

@ -1,15 +1,114 @@
import {iOS} from "./utils/utils.js";
import {mobile} from "./utils/utils.js";
type iconJson =
| {
src: string;
}
| {
css: string;
}
| {
html: HTMLElement;
};
interface menuPart<x, y> {
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void;
}
class ContextButton<x, y> implements menuPart<x, y> {
private text: string | (() => string);
private onClick: (this: x, arg: y, e: MouseEvent) => void;
private icon?: iconJson;
private visable?: (this: x, arg: y) => boolean;
private enabled?: (this: x, arg: y) => boolean;
//TODO there *will* be more colors
private color?: "red" | "blue";
constructor(
text: ContextButton<x, y>["text"],
onClick: ContextButton<x, y>["onClick"],
addProps: {
icon?: iconJson;
visable?: (this: x, arg: y) => boolean;
enabled?: (this: x, arg: y) => boolean;
color?: "red" | "blue";
} = {},
) {
this.text = text;
this.onClick = onClick;
this.icon = addProps.icon;
this.visable = addProps.visable;
this.enabled = addProps.enabled;
this.color = addProps.color;
}
isVisable(obj1: x, obj2: y): boolean {
if (!this.visable) return true;
return this.visable.call(obj1, obj2);
}
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement) {
if (!this.isVisable(obj1, obj2)) {
return;
}
const intext = document.createElement("button");
intext.classList.add("contextbutton");
intext.append(this.textContent);
intext.disabled = !!this.enabled && !this.enabled.call(obj1, obj2);
if (this.icon) {
if ("src" in this.icon) {
const icon = document.createElement("img");
icon.classList.add("svgicon");
icon.src = this.icon.src;
intext.append(icon);
} else if ("css" in this.icon) {
const icon = document.createElement("span");
icon.classList.add(this.icon.css, "svgicon");
switch (this.color) {
case "red":
icon.style.background = "var(--red)";
break;
case "blue":
icon.style.background = "var(--blue)";
break;
}
intext.append(icon);
} else {
intext.append(this.icon.html);
}
}
switch (this.color) {
case "red":
intext.style.color = "var(--red)";
break;
case "blue":
intext.style.color = "var(--blue)";
break;
}
intext.onclick = (e) => {
menu.remove();
this.onClick.call(obj1, obj2, e);
};
menu.append(intext);
}
get textContent() {
if (this.text instanceof Function) {
return this.text();
}
return this.text;
}
}
class Seperator<x, y> implements menuPart<x, y> {
makeContextHTML(_obj1: x, _obj2: y, menu: HTMLDivElement): void {
menu.append(document.createElement("hr"));
}
}
class Contextmenu<x, y> {
static currentmenu: HTMLElement | "";
name: string;
buttons: [
string | (() => string),
(this: x, arg: y, e: MouseEvent) => void,
string | null,
(this: x, arg: y) => boolean,
(this: x, arg: y) => boolean,
string,
][];
buttons: menuPart<x, y>[];
div!: HTMLDivElement;
static setup() {
Contextmenu.currentmenu = "";
@ -27,56 +126,33 @@ class Contextmenu<x, y> {
this.name = name;
this.buttons = [];
}
addbutton(
text: string | (() => string),
onclick: (this: x, arg: y, e: MouseEvent) => void,
img: null | string = null,
shown: (this: x, arg: y) => boolean = (_) => true,
enabled: (this: x, arg: y) => boolean = (_) => true,
addButton(
text: ContextButton<x, y>["text"],
onClick: ContextButton<x, y>["onClick"],
addProps: {
icon?: iconJson;
visable?: (this: x, arg: y) => boolean;
enabled?: (this: x, arg: y) => boolean;
color?: "red" | "blue";
} = {},
) {
this.buttons.push([text, onclick, img, shown, enabled, "button"]);
return {};
this.buttons.push(new ContextButton(text, onClick, addProps));
}
addsubmenu(
text: string | (() => string),
onclick: (this: x, arg: y, e: MouseEvent) => void,
img = null,
shown: (this: x, arg: y) => boolean = (_) => true,
enabled: (this: x, arg: y) => boolean = (_) => true,
) {
this.buttons.push([text, onclick, img, shown, enabled, "submenu"]);
return {};
addSeperator() {
this.buttons.push(new Seperator());
}
private makemenu(x: number, y: number, addinfo: x, other: y) {
const div = document.createElement("div");
div.classList.add("contextmenu", "flexttb");
let visibleButtons = 0;
for (const thing of this.buttons) {
if (!thing[3].call(addinfo, other)) continue;
visibleButtons++;
const intext = document.createElement("button");
intext.disabled = !thing[4].call(addinfo, other);
intext.classList.add("contextbutton");
if (thing[0] instanceof Function) {
intext.textContent = thing[0]();
} else {
intext.textContent = thing[0];
}
console.log(thing);
if (thing[5] === "button" || thing[5] === "submenu") {
intext.onclick = (e) => {
div.remove();
thing[1].call(addinfo, other, e);
};
}
div.appendChild(intext);
for (const button of this.buttons) {
button.makeContextHTML(addinfo, other, div);
}
if (visibleButtons == 0) return;
//NOTE I don't know if this'll ever actually happen in reality
if (div.childNodes.length === 0) return;
if (Contextmenu.currentmenu != "") {
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
div.style.top = y + "px";
@ -100,7 +176,8 @@ class Contextmenu<x, y> {
this.makemenu(event.clientX, event.clientY, addinfo, other);
};
obj.addEventListener("contextmenu", func);
if (iOS) {
//NOTE not sure if this code is correct, seems fine at least for now
if (mobile) {
let hold: NodeJS.Timeout | undefined;
let x!: number;
let y!: number;

View file

@ -342,28 +342,28 @@ class Group extends Channel {
user: User;
static contextmenu = new Contextmenu<Group, undefined>("channel menu");
static setupcontextmenu() {
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("DMs.copyId"),
function (this: Group) {
navigator.clipboard.writeText(this.id);
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("DMs.markRead"),
function (this: Group) {
this.readbottom();
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("DMs.close"),
function (this: Group) {
this.deleteChannel();
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.copyId"),
function () {
navigator.clipboard.writeText(this.user.id);

View file

@ -39,89 +39,96 @@ class Guild extends SnowFlake {
members = new Set<Member>();
static contextmenu = new Contextmenu<Guild, undefined>("guild menu");
static setupcontextmenu() {
Guild.contextmenu.addbutton(
() => I18n.getTranslation("guild.copyId"),
function (this: Guild) {
navigator.clipboard.writeText(this.id);
},
);
Guild.contextmenu.addbutton(
() => I18n.getTranslation("guild.markRead"),
function (this: Guild) {
this.markAsRead();
},
);
Guild.contextmenu.addbutton(
() => I18n.getTranslation("guild.notifications"),
function (this: Guild) {
this.setnotifcation();
},
);
this.contextmenu.addbutton(
() => I18n.getTranslation("user.editServerProfile"),
function () {
this.member.showEditProfile();
},
);
Guild.contextmenu.addbutton(
() => I18n.getTranslation("guild.leave"),
function (this: Guild) {
this.confirmleave();
},
null,
function (_) {
return this.properties.owner_id !== this.member.user.id;
},
);
Guild.contextmenu.addbutton(
() => I18n.getTranslation("guild.delete"),
function (this: Guild) {
this.confirmDelete();
},
null,
function (_) {
return this.properties.owner_id === this.member.user.id;
},
);
Guild.contextmenu.addbutton(
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.makeInvite"),
function (this: Guild) {
const d = new Dialog("");
this.makeInviteMenu(d.options);
d.show();
},
null,
(_) => true,
function () {
return this.member.hasPermission("CREATE_INSTANT_INVITE");
{
enabled: function () {
return this.member.hasPermission("CREATE_INSTANT_INVITE");
},
color: "blue",
},
);
Guild.contextmenu.addbutton(
Guild.contextmenu.addSeperator();
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.markRead"),
function (this: Guild) {
this.markAsRead();
},
);
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.notifications"),
function (this: Guild) {
this.setnotifcation();
},
);
Guild.contextmenu.addSeperator();
this.contextmenu.addButton(
() => I18n.getTranslation("user.editServerProfile"),
function () {
this.member.showEditProfile();
},
);
Guild.contextmenu.addSeperator();
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.leave"),
function (this: Guild) {
this.confirmleave();
},
{
visable: function (_) {
return this.properties.owner_id !== this.member.user.id;
},
color: "red",
},
);
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.delete"),
function (this: Guild) {
this.confirmDelete();
},
{
visable: function (_) {
return this.properties.owner_id === this.member.user.id;
},
color: "red",
icon: {
css: "svg-delete",
},
},
);
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.settings"),
function (this: Guild) {
this.generateSettings();
},
null,
function () {
return this.member.hasPermission("MANAGE_GUILD");
{
visable: function () {
return this.member.hasPermission("MANAGE_GUILD");
},
icon: {
css: "svg-settings",
},
},
);
/* -----things left for later-----
guild.contextmenu.addbutton("Leave Guild",function(){
console.log(this)
this.deleteChannel();
},null,_=>{return thisuser.isAdmin()})
guild.contextmenu.addbutton("Mute Guild",function(){
editchannelf(this);
},null,_=>{return thisuser.isAdmin()})
*/
Guild.contextmenu.addSeperator();
Guild.contextmenu.addButton(
() => I18n.getTranslation("guild.copyId"),
function (this: Guild) {
navigator.clipboard.writeText(this.id);
},
);
//TODO mute guild button
}
generateSettings() {
const settings = new Settings(I18n.getTranslation("guild.settingsFor", this.properties.name));

View file

@ -127,26 +127,24 @@ import {I18n} from "./i18n.js";
}
const menu = new Contextmenu<void, void>("create rightclick");
menu.addbutton(
menu.addButton(
I18n.getTranslation("channel.createChannel"),
() => {
if (thisUser.lookingguild) {
thisUser.lookingguild.createchannels();
}
},
null,
() => thisUser.isAdmin(),
{visable: () => thisUser.isAdmin()},
);
menu.addbutton(
menu.addButton(
I18n.getTranslation("channel.createCatagory"),
() => {
if (thisUser.lookingguild) {
thisUser.lookingguild.createcategory();
}
},
null,
() => thisUser.isAdmin(),
{visable: () => thisUser.isAdmin()},
);
menu.bindContextmenu(document.getElementById("channels") as HTMLDivElement);

View file

@ -57,50 +57,81 @@ class Message extends SnowFlake {
Message.setupcmenu();
}
static setupcmenu() {
Message.contextmenu.addbutton(
() => I18n.getTranslation("copyrawtext"),
function (this: Message) {
navigator.clipboard.writeText(this.content.rawString);
},
);
Message.contextmenu.addbutton(
Message.contextmenu.addButton(
() => I18n.getTranslation("reply"),
function (this: Message) {
this.channel.setReplying(this);
},
);
Message.contextmenu.addbutton(
() => I18n.getTranslation("copymessageid"),
function (this: Message) {
navigator.clipboard.writeText(this.id);
{
icon: {
css: "svg-reply",
},
},
);
Message.contextmenu.addsubmenu(
Message.contextmenu.addButton(
() => I18n.getTranslation("message.edit"),
function (this: Message) {
this.setEdit();
},
{
visable: function () {
return this.author.id === this.localuser.user.id;
},
icon: {
css: "svg-edit",
},
},
);
Message.contextmenu.addButton(
() => I18n.getTranslation("message.reactionAdd"),
function (this: Message, _, e: MouseEvent) {
Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => {
this.reactionToggle(_);
});
},
{
icon: {
css: "svg-emoji",
},
},
);
Message.contextmenu.addbutton(
() => I18n.getTranslation("message.edit"),
Message.contextmenu.addSeperator();
Message.contextmenu.addButton(
() => I18n.getTranslation("copyrawtext"),
function (this: Message) {
this.setEdit();
navigator.clipboard.writeText(this.content.rawString);
},
null,
function () {
return this.author.id === this.localuser.user.id;
{
icon: {
css: "svg-copy",
},
},
);
Message.contextmenu.addbutton(
Message.contextmenu.addButton(
() => I18n.getTranslation("copymessageid"),
function (this: Message) {
navigator.clipboard.writeText(this.id);
},
);
Message.contextmenu.addSeperator();
Message.contextmenu.addButton(
() => I18n.getTranslation("message.delete"),
function (this: Message) {
this.confirmDelete();
},
null,
function () {
return this.canDelete();
{
visable: function () {
return this.canDelete();
},
icon: {
css: "svg-delete",
},
color: "red",
},
);
}

View file

@ -236,7 +236,7 @@ class RoleList extends Buttons {
static guildrolemenu = this.GuildRoleMenu();
private static ChannelRoleMenu() {
const menu = new Contextmenu<RoleList, Role>("role settings");
menu.addbutton(
menu.addButton(
() => I18n.getTranslation("role.remove"),
function (role) {
if (!this.channel) return;
@ -246,13 +246,12 @@ class RoleList extends Buttons {
headers: this.headers,
});
},
null,
);
return menu;
}
private static GuildRoleMenu() {
const menu = new Contextmenu<RoleList, Role>("role settings");
menu.addbutton(
menu.addButton(
() => I18n.getTranslation("role.delete"),
function (role) {
if (!confirm(I18n.getTranslation("role.confirmDelete"))) return;
@ -262,7 +261,6 @@ class RoleList extends Buttons {
headers: this.headers,
});
},
null,
);
return menu;
}

View file

@ -318,6 +318,7 @@ textarea {
width: 100%;
background: var(--primary-text-soft);
mask-repeat: no-repeat;
aspect-ratio: 1/1;
}
.selectarrow {
position: absolute;
@ -1534,8 +1535,13 @@ img.bigembedimg {
background: var(--secondary-bg);
border-radius: 4px;
box-shadow: 0 0 8px var(--shadow);
hr{
width:90%;
height: 1px;
}
}
.contextbutton {
position: relative;
width: 180px;
padding: 8px;
background: transparent;
@ -1544,6 +1550,15 @@ img.bigembedimg {
color: var(--secondary-text);
border: none;
transition: none;
display: flex;
flex-direction: row;
.svgicon {
height: 0.2in;
width: 0.2in;
position: absolute;
right: 6px;
top: 6px;
}
}
.contextbutton:enabled:hover {
background: var(--secondary-hover);

View file

@ -4,6 +4,7 @@
--red: #ff5555;
--yellow: #ffc159;
--green: #1c907b;
--blue: #779bff;
}
/* Themes. See themes.txt */

View file

@ -164,111 +164,118 @@ class User extends SnowFlake {
this.relationshipType = type;
}
static setUpContextMenu(): void {
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.copyId"),
function (this: User) {
navigator.clipboard.writeText(this.id);
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.message"),
function (this: User) {
this.opendm();
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.block"),
function (this: User) {
this.block();
},
null,
function () {
return this.relationshipType !== 2 && this.id !== this.localuser.user.id;
{
visable: function () {
return this.relationshipType !== 2 && this.id !== this.localuser.user.id;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.unblock"),
function (this: User) {
this.unblock();
},
null,
function () {
return this.relationshipType === 2 && this.id !== this.localuser.user.id;
{
visable: function () {
return this.relationshipType === 2 && this.id !== this.localuser.user.id;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.friendReq"),
function (this: User) {
this.changeRelationship(1);
},
null,
function () {
return (
(this.relationshipType === 0 || this.relationshipType === 3) &&
this.id !== this.localuser.user.id
);
{
visable: function () {
return (
(this.relationshipType === 0 || this.relationshipType === 3) &&
this.id !== this.localuser.user.id
);
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("friends.removeFriend"),
function (this: User) {
this.changeRelationship(0);
},
null,
function () {
return this.relationshipType === 1 && this.id !== this.localuser.user.id;
{
visable: function () {
return this.relationshipType === 1 && this.id !== this.localuser.user.id;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.kick"),
function (this: User, member: Member | undefined) {
member?.kick();
},
null,
function (member) {
if (!member) return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return us.hasPermission("KICK_MEMBERS") && this.id !== this.localuser.user.id;
{
visable: function (member) {
if (!member) return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return us.hasPermission("KICK_MEMBERS") && this.id !== this.localuser.user.id;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.editServerProfile"),
function (this: User, member: Member | undefined) {
if (!member) return;
member.showEditProfile();
},
null,
function (member) {
return member?.id === this.localuser.user.id;
{
visable: function (member) {
return member?.id === this.localuser.user.id;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.ban"),
function (this: User, member: Member | undefined) {
member?.ban();
},
null,
function (member) {
if (!member) return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return us.hasPermission("BAN_MEMBERS") && this.id !== this.localuser.user.id;
{
visable: function (member) {
if (!member) return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return us.hasPermission("BAN_MEMBERS") && this.id !== this.localuser.user.id;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.addRole"),
async function (this: User, member: Member | undefined, e) {
if (member) {
@ -286,15 +293,16 @@ class User extends SnowFlake {
member.addRole(result);
}
},
null,
(member) => {
if (!member) return false;
const us = member.guild.member;
console.log(us.hasPermission("MANAGE_ROLES"));
return us.hasPermission("MANAGE_ROLES") || false;
{
visable: (member) => {
if (!member) return false;
const us = member.guild.member;
console.log(us.hasPermission("MANAGE_ROLES"));
return us.hasPermission("MANAGE_ROLES") || false;
},
},
);
this.contextmenu.addbutton(
this.contextmenu.addButton(
() => I18n.getTranslation("user.removeRole"),
async function (this: User, member: Member | undefined, e) {
if (member) {
@ -312,12 +320,13 @@ class User extends SnowFlake {
member.removeRole(result);
}
},
null,
(member) => {
if (!member) return false;
const us = member.guild.member;
console.log(us.hasPermission("MANAGE_ROLES"));
return us.hasPermission("MANAGE_ROLES") || false;
{
visable: (member) => {
if (!member) return false;
const us = member.guild.member;
console.log(us.hasPermission("MANAGE_ROLES"));
return us.hasPermission("MANAGE_ROLES") || false;
},
},
);
}