varrious fixes and improvements

This commit is contained in:
MathMan05
2025-10-23 20:39:56 -05:00
parent b6ec780462
commit 0e84c6cda1
10 changed files with 656 additions and 52 deletions
+53 -7
View File
@@ -32,6 +32,7 @@ import {CustomHTMLDivElement} from "./index.js";
import {Direct} from "./direct.js";
import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js";
import {NotificationHandler} from "./notificationHandler.js";
import {Command} from "./interactions/commands.js";
class Channel extends SnowFlake {
editing!: Message | null;
@@ -1578,6 +1579,38 @@ class Channel extends SnowFlake {
this.textSave = MarkDown.gatherBoxText(typebox);
typebox.textContent = "";
}
curCommand?: Command;
curWatch = () => {};
async submitCommand() {
if (!this.curCommand) return;
if (await this.curCommand.submit(this)) {
this.curCommand = undefined;
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
typebox.markdown.boxEnabled = true;
typebox.innerHTML = "";
typebox.markdown.boxupdate();
typebox.removeEventListener("keyup", this.curWatch);
}
}
startCommand(command: Command) {
this.curCommand = command;
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
typebox.markdown.boxEnabled = false;
const func = () => {
const node = window.getSelection()?.focusNode;
if (this.localuser.channelfocus === this) {
const out = command.collect(typebox, this, node || undefined);
if (!out) {
typebox.markdown.boxEnabled = true;
typebox.markdown.boxupdate();
typebox.removeEventListener("keyup", func);
}
}
};
this.curWatch = func;
typebox.addEventListener("keyup", func);
command.render(typebox, this);
}
async getHTML(addstate = true, getMessages: boolean | void = undefined, aroundMessage?: string) {
if (this.owner instanceof Direct) {
this.owner.freindDiv?.classList.remove("viewChannel");
@@ -1586,11 +1619,17 @@ class Channel extends SnowFlake {
this.localuser.channelfocus.collectBox();
}
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
typebox.markdown.boxEnabled = !this.curCommand;
if (this.curCommand) {
this.curCommand.render(typebox, this);
}
typebox.style.setProperty("--channel-text", JSON.stringify(I18n.channel.typebox(this.name)));
const md = typebox.markdown;
md.owner = this;
typebox.textContent = this.textSave;
md.boxupdate(Infinity);
if (!this.curCommand) {
const md = typebox.markdown;
md.owner = this;
typebox.textContent = this.textSave;
md.boxupdate(Infinity);
}
this.localuser.fileExtange(this.files, this.htmls);
if (getMessages === undefined) {
@@ -1843,7 +1882,7 @@ class Channel extends SnowFlake {
headers: this.headers,
});
const response = await j.json();
const response = (await j.json()) as messagejson[];
if (response.length !== 100) {
this.allthewayup = true;
}
@@ -1859,6 +1898,10 @@ class Channel extends SnowFlake {
}
prev = message;
}
if (!response.length) {
this.lastmessageid = undefined;
this.lastreadmessageid = undefined;
}
}
delChannel(json: channeljson) {
const build: Channel[] = [];
@@ -1872,13 +1915,16 @@ class Channel extends SnowFlake {
afterProm?: Promise<void>;
afterProms = new Map<string, () => void>();
async grabAfter(id: string) {
if (this.idToNext.has(id)) {
return;
}
if (id === this.lastmessage?.id) {
return;
}
if (this.afterProm) return this.afterProm;
if (this.afterProm) return new Promise<void>((res) => this.afterProms.set(id, res));
let tempy: string | undefined = id;
while (tempy && tempy.includes("fake")) {
tempy = this.idToNext.get(tempy);
tempy = this.idToPrev.get(tempy);
}
if (!tempy) return;
id = tempy;
+50 -5
View File
@@ -18,6 +18,8 @@ import {
templateSkim,
mute_config,
GuildOverrides,
commandJson,
applicationJson,
} from "./jsontypes.js";
import {User} from "./user.js";
import {I18n} from "./i18n.js";
@@ -27,6 +29,7 @@ import {createImg} from "./utils/utils.js";
import {Sticker} from "./sticker.js";
import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js";
import {MarkDown} from "./markdown.js";
import {Command} from "./interactions/commands.js";
export async function makeInviteMenu(inviteMenu: Options, guild: Guild, url: string) {
const invDiv = document.createElement("div");
const bansp = ProgessiveDecodeJSON<invitejson[]>(url, {
@@ -1289,7 +1292,7 @@ class Guild extends SnowFlake {
}
this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel);
this.stickers = json.stickers.map((_) => new Sticker(_, this)) || [];
//this.getCommands();
this.getCommands();
}
get perminfo() {
return this.localuser.perminfo.guilds[this.id];
@@ -1849,14 +1852,56 @@ class Guild extends SnowFlake {
}),
});
}
commands?: Command[];
commandProm?: Promise<unknown>;
apps?: applicationJson[];
async getApps() {
if (this.commandProm) {
await this.commandProm;
}
if (this.apps) {
return this.commands;
} else {
const prom = this.getCommandsFetch();
this.commandProm = prom;
const {apps, commands} = await prom;
this.commands = commands;
this.apps = apps;
return apps;
}
}
async getCommands() {
const json = await (
if (this.commandProm) {
await this.commandProm;
}
if (this.commands) {
return this.commands;
} else {
const prom = this.getCommandsFetch();
this.commandProm = prom;
const {apps, commands} = await prom;
this.commands = commands;
this.apps = apps;
return commands;
}
}
async getCommandsFetch() {
const json = (await (
await fetch(this.info.api + `/guilds/${this.id}/application-command-index`, {
headers: this.headers,
})
).json();
if (this.id === "1006649183970562092") console.warn(json.application_commands);
).json()) as {application_commands: commandJson[]; applications: applicationJson[]};
//TODO remove this fix once the server fixes this
json.applications.forEach((_) => {
if (_.icon && _.icon.startsWith("data")) {
_.icon = null;
}
});
return {
apps: json.applications,
commands: json.application_commands.map((_) => new Command(_, this.localuser)),
};
}
}
Guild.setupcontextmenu();
+4
View File
@@ -154,6 +154,10 @@ async function handleEnter(event: KeyboardEvent): Promise<void> {
channel.typingstart();
if (event.key === "Enter" && !event.shiftKey) {
if (channel.curCommand) {
channel.submitCommand();
return;
}
event.preventDefault();
replyingTo = thisUser.channelfocus ? thisUser.channelfocus.replyingto : null;
if (replyingTo?.div) {
+401
View File
@@ -0,0 +1,401 @@
import {Channel} from "../channel.js";
import {Guild} from "../guild.js";
import {I18n} from "../i18n.js";
import {commandJson, commandOptionJson} from "../jsontypes.js";
import {Localuser} from "../localuser.js";
import {SnowFlake} from "../snowflake.js";
function focusInput(html: HTMLElement) {
const input = html.getElementsByTagName("input")[0];
if (input) input.focus();
}
function focusElm(node: HTMLElement | Text, before = true) {
const selection = window.getSelection();
if (!selection) return;
var range = document.createRange();
if (before) {
range.setStartBefore(node);
} else {
range.setStartAfter(node);
}
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
export class Command extends SnowFlake {
owner: Localuser | Guild;
type: 1 | 2 | 3 | 4;
applicationId: string;
name: string;
nameLocalizations: Record<string, string>;
descriptionLocalizations: Record<string, string>;
description: string;
defaultMemberPerms: BigInt;
permissions: {
user: boolean;
roles: Record<string, boolean>;
channels: Record<string, boolean>;
};
nsfw: boolean;
gpr: number;
version: string;
handler: 1 | 2 | 3;
options: Option[];
readonly rawJson: Readonly<commandJson>;
get localuser() {
if (this.owner instanceof Localuser) {
return this.owner;
} else {
return this.owner.owner;
}
}
constructor(command: commandJson, owner: Localuser | Guild) {
super(command.id);
this.rawJson = Object.freeze(structuredClone(command));
this.owner = owner;
this.type = command.type;
this.applicationId = command.application_id;
this.name = command.name;
this.nameLocalizations = command.name_localizations || {};
this.description = command.description;
this.descriptionLocalizations = command.description_localizations || {};
this.defaultMemberPerms = BigInt(command.default_member_permissions || "0");
this.permissions = {
user: command.permissions?.user || true,
roles: command.permissions?.roles || {},
channels: command.permissions?.channels || {},
};
command.options ||= [];
this.options = command.options.map((_) => Option.toOption(_, this));
this.nsfw = command.nsfw;
this.gpr = command.global_popularity_rank || 0;
this.version = command.version;
this.handler = command.handler || 1;
}
get localizedName() {
return this.nameLocalizations[I18n.lang] || this.name;
}
get localizedDescription() {
return this.descriptionLocalizations[I18n.lang] || this.description;
}
similar(search: string) {
if (search.length === 0) {
return 0.1;
}
const similar = (str: string) => {
if (str.includes(search)) {
return search.length / str.length;
} else if (str.toLowerCase().includes(search.toLowerCase())) {
return str.length / str.length / 1.4;
} else {
return 0;
}
};
return Math.max(
similar(this.name),
similar(this.description),
similar(this.localizedDescription),
similar(this.localizedName),
);
}
state = new WeakMap<
Channel,
(
| {
option: Option;
state: string;
}
| string
)[]
>();
collect(html: HTMLElement, channel: Channel, node?: Node): boolean {
const states = this.state.get(channel);
const build: (
| {
option: Option;
state: string;
}
| string
)[] = [];
if (!states) return false;
let gotname = false;
for (const elm of Array.from(html.childNodes)) {
if (elm instanceof HTMLElement) {
if (elm.classList.contains("commandFront")) {
gotname = true;
continue;
}
const name = elm.getAttribute("commandName");
const state = states.find((_) => _ instanceof Object && _.option.match(name || ""));
if (state) {
build.push(state);
} else {
const option = this.options.find((_) => _.match(name || ""));
if (option) {
build.push({option, state: ""});
}
}
} else if (elm instanceof Text) {
build.push(elm.textContent || "");
}
}
if (node instanceof Text) {
this.searchAtr(node, channel, html);
}
if (gotname) {
this.state.set(channel, build);
} else {
this.state.delete(channel);
}
return gotname;
}
searchAtr(textNode: Text, channel: Channel, Divhtml: HTMLElement) {
const text = (textNode.textContent || "").trim();
const states = this.state.get(channel);
if (!states) {
this.localuser.MDSearchOptions(
[],
"",
document.getElementById("searchOptions") as HTMLDivElement,
);
return;
}
const opts = this.options
.filter((obj) => !states.find((_) => _ instanceof Object && _.option === obj))
.map((opt) => [opt, opt.similar(text)] as const)
.filter((_) => _[1])
.sort((a, b) => a[1] - b[1])
.slice(0, 6)
.map((_) => _[0]);
this.localuser.MDSearchOptions(
opts.map((opt) => {
return [
opt.localizedName,
"",
void 0,
() => {
const html = opt.toHTML("", channel);
textNode.after(html);
textNode.remove();
this.collect(Divhtml, channel);
console.log(this.state.get(channel));
focusInput(html);
return true;
},
];
}),
"",
document.getElementById("searchOptions") as HTMLDivElement,
);
}
render(html: HTMLElement, channel: Channel) {
html.innerHTML = "";
let state = this.state.get(channel);
if (!state) {
const req = this.options.filter((_) => _.required);
state = req.map((option) => ({option, state: ""}));
this.state.set(channel, state);
}
const command = document.createElement("span");
command.classList.add("commandFront");
command.textContent = `/${this.localizedName}`;
command.contentEditable = "false";
html.append(command);
let lastElm: HTMLElement | undefined = undefined;
for (const thing of state) {
if (typeof thing === "string") {
html.append(thing);
continue;
}
const {option, state} = thing;
const opt = option.toHTML(state, channel);
lastElm = opt;
html.append(opt);
}
if (lastElm) {
focusInput(lastElm);
} else {
const node = new Text();
node.textContent = "";
html.append(node);
focusElm(node, false);
}
}
stateChange(option: Option, channel: Channel, state: string) {
const states = this.state.get(channel);
if (!states) return;
const stateObj = states.find((_) => _ instanceof Object && _.option === option);
if (stateObj && stateObj instanceof Object) {
stateObj.state = state;
}
}
get info() {
return this.owner.info;
}
get headers() {
return this.owner.headers;
}
async submit(channel: Channel) {
const nonce = Math.floor(Math.random() * 10 ** 9) + "";
const states = this.state.get(channel);
if (!states) {
return true;
}
const opts = states.filter((_) => typeof _ !== "string");
await fetch(this.info.api + "/interactions", {
method: "POST",
headers: this.headers,
body: JSON.stringify({
type: 2,
nonce: nonce,
guild_id: channel.owner.id,
channel_id: channel.id,
application_id: this.applicationId,
session_id: this.localuser.session_id,
data: {
application_command: this.rawJson,
attachments: [],
id: this.id,
name: this.name,
options: opts.map(({option, state}) => {
return option.toJson(state);
}),
type: 1,
version: this.version,
},
}),
});
this.state.delete(channel);
return true;
}
}
abstract class Option {
type: number;
required: boolean;
private name: string;
private description: string;
private nameLocalizations: Record<string, string>;
private descriptionLocalizations: Record<string, string>;
constructor(optionjson: commandOptionJson) {
this.required = optionjson.required || false;
this.name = optionjson.name;
this.nameLocalizations = optionjson.name_localizations || {};
this.description = optionjson.description;
this.descriptionLocalizations = optionjson.description_localizations || {};
this.type = optionjson.type;
}
match(str: string) {
return str === this.name;
}
get localizedName() {
return this.nameLocalizations[I18n.lang] || this.name;
}
get localizedDescription() {
return this.descriptionLocalizations[I18n.lang] || this.description;
}
static toOption(optionjson: commandOptionJson, owner: Command): Option {
switch (optionjson.type) {
case 3:
return new StringOption(optionjson, owner);
default:
return new ErrorOption(optionjson);
}
}
abstract toHTML(state: string, channel: Channel): HTMLElement;
imprintName(html: HTMLElement) {
html.setAttribute("commandName", this.name);
}
similar(search: string) {
if (search.length === 0) {
return 0.1;
}
const similar = (str: string) => {
if (str.includes(search)) {
return search.length / str.length;
} else if (str.toLowerCase().includes(search.toLowerCase())) {
return str.length / str.length / 1.4;
} else {
return 0;
}
};
return Math.max(
similar(this.name),
similar(this.description),
similar(this.localizedDescription),
similar(this.localizedName),
);
}
toJson(state: string) {
return {
value: state,
type: this.type,
name: this.name,
};
}
}
class ErrorOption extends Option {
constructor(optionjson: commandOptionJson) {
super(optionjson);
this.required = false;
}
toHTML(): HTMLElement {
const span = document.createElement("span");
this.imprintName(span);
span.textContent = "Fermi doesn't impl this yet";
return span;
}
}
class StringOption extends Option {
minLeng: number;
maxLeng: number;
choices: commandOptionJson["choices"];
autocomplete: boolean;
owner: Command;
constructor(optionjson: commandOptionJson, owner: Command) {
super(optionjson);
this.owner = owner;
this.minLeng = optionjson.min_length || 0;
this.maxLeng = optionjson.min_length || 6000;
this.choices = optionjson.choices;
this.autocomplete = optionjson.autocomplete || false;
}
toHTML(state: string, channel: Channel): HTMLElement {
const div = document.createElement("div");
div.contentEditable = "false";
div.classList.add("flexltr", "commandinput");
this.imprintName(div);
const label = document.createElement("span");
label.textContent = this.localizedName + ":";
const input = document.createElement("input");
input.type = "text";
input.value = state;
input.onkeydown = (e) => {
if (input.selectionStart === 0 && e.key === "Backspace") {
const before = !!div.nextSibling;
const sib = div.nextSibling || div.previousSibling;
div.remove();
focusElm(sib as HTMLElement, before);
e.preventDefault();
e.stopImmediatePropagation();
}
};
input.onkeyup = (e) => {
if (input.selectionStart === input.value.length && e.key === "ArrowRight") {
focusElm(div, false);
}
this.owner.stateChange(this, channel, input.value);
};
div.append(label, input);
return div;
}
}
@@ -1,9 +1,9 @@
import {Channel} from "./channel.js";
import {I18n} from "./i18n.js";
import {actionRow, button, component, select} from "./jsontypes.js";
import {MarkDown} from "./markdown";
import {Message} from "./message.js";
import {FancySelect} from "./utils/fancySelect.js";
import {Channel} from "../channel.js";
import {I18n} from "../i18n.js";
import {actionRow, button, component, select} from "../jsontypes.js";
import {MarkDown} from "../markdown";
import {Message} from "../message.js";
import {FancySelect} from "../utils/fancySelect.js";
abstract class compObj {
abstract owner: Components;
abstract getHTML(): HTMLElement;
+58
View File
@@ -206,6 +206,64 @@ export interface favandfreq {
};
};
}
export interface applicationJson {
description: string;
flags: number;
icon: null | string;
id: string;
name: string;
}
export interface commandOptionJson {
type: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
name: string;
name_localized?: string;
name_localizations?: null | Record<string, string>;
description: string;
description_localizations?: null | Record<string, string>;
description_localized?: string;
choices?:
| {
name: string;
name_localizations?: null | Record<string, string>;
name_localized?: string | null;
value: string | number;
}[]
| null;
options?: commandJson[];
channel_types?: number[];
min_value?: number;
max_value?: number;
min_length?: number;
max_length?: number;
autocomplete?: boolean;
required?: boolean;
}
export interface commandJson {
id: string;
type: 1 | 2 | 3 | 4;
application_id: string;
guild_id?: null | string;
name: string;
name_localized?: string;
name_localizations?: null | {[key: string]: string};
description: string;
description_localizations?: null | {[key: string]: string};
options?: commandOptionJson[];
default_member_permissions?: null | string;
dm_permission: boolean;
permissions?: null | {
user?: boolean;
roles?: {[key: string]: boolean};
channels?: {[key: string]: boolean};
};
nsfw: boolean;
integration_types?: null | number[];
global_popularity_rank: number;
contexts?: null | number[];
version: string;
handler: 0 | 1 | 2 | 3; //0 really shouldn't be here, but it's a bug and should be treated like 1.
}
interface readySuplemental {
op: 0;
t: "READY_SUPPLEMENTAL";
+59 -32
View File
@@ -3389,50 +3389,53 @@ class Localuser {
MDSearchOptions(
options: (
| [string, string, void | HTMLElement]
| [string, string, void | HTMLElement, () => void]
| [string, string, void | HTMLElement, () => void | boolean]
)[],
original: string,
div: HTMLDivElement,
typebox: MarkDown,
typebox?: MarkDown,
) {
if (!div) return;
div.innerHTML = "";
let i = 0;
const htmloptions: HTMLSpanElement[] = [];
for (const thing of options) {
for (const [name, replace, elm, func] of options) {
if (i == 8) {
break;
}
i++;
const span = document.createElement("span");
htmloptions.push(span);
if (thing[2]) {
span.append(thing[2]);
if (elm) {
span.append(elm);
}
span.append(thing[0]);
span.append(name);
span.onclick = (e) => {
if (e) {
const selection = window.getSelection() as Selection;
const box = typebox.box.deref();
if (!box) return;
if (selection) {
const pos = getTextNodeAtPosition(
box,
original.length -
(original.match(this.autofillregex) as RegExpMatchArray)[0].length +
thing[1].length,
);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
if (replace) {
const selection = window.getSelection() as Selection;
const box = typebox?.box.deref();
if (!box) return;
if (selection) {
const pos = getTextNodeAtPosition(
box,
original.length -
(original.match(this.autofillregex) as RegExpMatchArray)[0].length +
replace.length,
);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
}
box.focus();
}
e.preventDefault();
box.focus();
}
this.MDReplace(thing[1], original, typebox);
thing[3]?.();
if (!func?.() && typebox) {
this.MDReplace(replace, original, typebox);
}
div.innerHTML = "";
remove();
};
@@ -3508,15 +3511,6 @@ class Localuser {
typebox,
);
}
async getUser(id: string) {
if (this.userMap.has(id)) {
return this.userMap.get(id) as User;
}
return new User(
await (await fetch(this.info.api + "/users/" + id, {headers: this.headers})).json(),
this,
);
}
MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
let members: [Member | Role | User | "@everyone", number][] = [];
if (this.lookingguild && name !== "everyone") {
@@ -3634,6 +3628,34 @@ class Localuser {
});
this.MDSearchOptions(map, orginal, box, typebox);
}
async findCommands(search: string, box: HTMLDivElement, md: MarkDown) {
const guild = this.lookingguild;
if (!guild) return;
const commands = await guild.getCommands();
const sorted = commands
.map((_) => [_, _.similar(search)] as const)
.filter((_) => _[1] !== 0)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
this.MDSearchOptions(
sorted.map(([elm]) => {
return [
`/${elm.localizedName}`,
"",
undefined,
() => {
this.channelfocus?.startCommand(elm);
return true;
},
] as const;
}),
"",
box,
md,
);
console.log(sorted, search);
}
search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) {
if (!pre) {
const match = str.match(this.autofillregex);
@@ -3660,6 +3682,11 @@ class Localuser {
}
return;
}
const command = str.match(/^\/((\s*[\w\d]+)*)$/);
if (command) {
const search = command[1];
this.findCommands(search, box, md);
}
}
box.innerHTML = "";
}
+2
View File
@@ -973,7 +973,9 @@ class MarkDown {
) {
this.customBox = [stringToHTML, HTMLToString];
}
boxEnabled = true;
boxupdate(offset = 0, allowLazy = true, computedLength: void | number = undefined) {
if (!this.boxEnabled) return;
const box = this.box.deref();
if (!box) return;
let restore: undefined | (() => void);
+1 -2
View File
@@ -15,8 +15,7 @@ import {I18n} from "./i18n.js";
import {Hover} from "./hover.js";
import {Dialog} from "./settings.js";
import {Sticker} from "./sticker.js";
import {Components} from "./compontents.js";
import {Components} from "./interactions/compontents.js";
class Message extends SnowFlake {
static contextmenu = new Contextmenu<Message, void>("message menu");
stickers!: Sticker[];
+22
View File
@@ -163,6 +163,22 @@ body {
justify-content: space-between;
padding: 4px 14px;
}
.commandinput {
display: inline-flex !important;
width: fit-content;
margin-left: 6px;
align-items: center;
background: var(--primary-bg);
padding-left: 4px;
border-radius: 4px;
input {
padding: 5px !important;
margin: 0px !important;
margin-left: 5px !important;
height: 100%;
field-sizing: content;
}
}
.flexltr {
min-height: 0;
display: flex;
@@ -1955,6 +1971,12 @@ span.instanceStatus {
#realbox {
padding: 0 16px 28px 16px;
}
.commandFront {
user-select: none !important;
padding: 4px;
background: var(--secondary-bg);
border-radius: 4px;
}
#typebox {
margin: -10px 0px;
flex-grow: 1;