mirror of
https://github.com/MathMan05/Fermi.git
synced 2026-10-15 15:30:06 +00:00
5574 lines
160 KiB
TypeScript
5574 lines
160 KiB
TypeScript
import {Guild} from "./guild.js";
|
|
import {Channel} from "./channel.js";
|
|
import {Direct, Group} from "./direct.js";
|
|
import {User} from "./user.js";
|
|
import {createImg, getapiurls, getBulkUsers, getInstances, installPGet, SW} from "./utils/utils.js";
|
|
import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js";
|
|
import {
|
|
channeljson,
|
|
expSessionJson,
|
|
guildFolder,
|
|
mainuserjson,
|
|
memberjson,
|
|
memberlistupdatejson,
|
|
messageCreateJson,
|
|
messagejson,
|
|
ConnectionJson,
|
|
presencejson,
|
|
readStateEntry,
|
|
readyjson,
|
|
startTypingjson,
|
|
wsjson,
|
|
pollUpdateJson,
|
|
} from "./jsontypes.js";
|
|
import {Member} from "./member.js";
|
|
import {buttonColor, Dialog, Form, FormError, Options, Settings} from "./settings.js";
|
|
import {getTextNodeAtPosition, MarkDown, saveCaretPosition} from "./markdown.js";
|
|
import {Bot} from "./bot.js";
|
|
import {Role} from "./role.js";
|
|
import {VoiceFactory, voiceStatusStr} from "./voice.js";
|
|
import {I18n, langmap} from "./i18n.js";
|
|
import {Emoji} from "./emoji.js";
|
|
import {Play} from "./audio/play.js";
|
|
import {Message} from "./message.js";
|
|
import {badgeArr} from "./Dbadges.js";
|
|
import {Rights} from "./rights.js";
|
|
import {Contextmenu} from "./contextmenu.js";
|
|
import {Sticker} from "./sticker.js";
|
|
import {Hover} from "./hover.js";
|
|
import {AccountSwitcher} from "./utils/switcher.js";
|
|
import {Favorites} from "./favorites.js";
|
|
import {
|
|
AnimateTristateValues,
|
|
getPreferences,
|
|
setPreferences,
|
|
ThemeOption,
|
|
} from "./utils/storage/userPreferences";
|
|
import {getDeveloperSettings, setDeveloperSettings} from "./utils/storage/devSettings";
|
|
import {getLocalSettings, ServiceWorkerModeValues} from "./utils/storage/localSettings.js";
|
|
import {PromiseLock} from "./utils/promiseLock.js";
|
|
import {CDNParams} from "./utils/cdnParams.js";
|
|
import {SnowFlake} from "./snowflake.js";
|
|
import {trimTrailingSlashes} from "./utils/netUtils.js";
|
|
import {Versions} from "./versions.js";
|
|
import {Shortcut} from "./shortcuts/shortcut.js";
|
|
import {getShortcuts, setShortcuts} from "./utils/storage/shortcuts.js";
|
|
type traceObj = {
|
|
micros: number;
|
|
calls?: (string | traceObj)[];
|
|
};
|
|
type trace = [string, traceObj];
|
|
const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]);
|
|
interface CustomHTMLDivElement extends HTMLDivElement {
|
|
markdown: MarkDown;
|
|
}
|
|
interface MDSearchOption {
|
|
name: string;
|
|
replace: string;
|
|
icon?: HTMLElement;
|
|
otherLogic?: () => boolean;
|
|
}
|
|
|
|
MarkDown.emoji = Emoji;
|
|
class Localuser {
|
|
readonly badges = new Map<
|
|
string,
|
|
{id: string; description: string; icon: string; link?: string; translate?: boolean}
|
|
>(badgeArr);
|
|
lastSequence: number | null = null;
|
|
get token() {
|
|
return this.headers.Authorization;
|
|
}
|
|
userinfo!: Specialuser;
|
|
initialized!: boolean;
|
|
get info() {
|
|
return this.userinfo.serverurls;
|
|
}
|
|
set info(e) {
|
|
this.userinfo.serverurls = e;
|
|
}
|
|
headers!: {"Content-type": string; Authorization: string};
|
|
user!: User;
|
|
readonly guilds = new Map<string, Guild>();
|
|
readonly channels: Map<string, Channel> = new Map();
|
|
readonly userMap: Map<string, User> = new Map();
|
|
readonly messages = new Map<string, Message>();
|
|
readonly idToPrev = new Map<string, string | undefined>();
|
|
readonly idToNext = new Map<string, string | undefined>();
|
|
static readonly globalShortcuts = new Shortcut();
|
|
get status() {
|
|
return this.user.status;
|
|
}
|
|
set status(status: string) {
|
|
this.user.setstatus(status);
|
|
}
|
|
focusChannel?: Channel;
|
|
focusGuild?: Guild;
|
|
private ws?: WebSocket;
|
|
private connectionSucceed = 0;
|
|
private errorBackoff = 0;
|
|
voiceFactory?: VoiceFactory;
|
|
play?: Play;
|
|
instancePing = {
|
|
name: "Unknown",
|
|
};
|
|
mfa_enabled!: boolean;
|
|
get perminfo() {
|
|
return this.userinfo.localuserStore;
|
|
}
|
|
set perminfo(e) {
|
|
this.userinfo.localuserStore = e;
|
|
}
|
|
static users = getBulkUsers();
|
|
static async showAccountSwitcher(thisUser?: Localuser) {
|
|
const specialUser = await new AccountSwitcher().show();
|
|
|
|
const onswap = thisUser?.onswap;
|
|
thisUser?.unload();
|
|
if (thisUser) thisUser.swapped = true;
|
|
const loading = document.getElementById("loading") as HTMLDivElement;
|
|
loading.classList.remove("doneloading");
|
|
loading.classList.add("loading");
|
|
|
|
thisUser = new Localuser(specialUser);
|
|
Localuser.users.currentuser = specialUser.uid;
|
|
sessionStorage.setItem("currentuser", specialUser.uid);
|
|
localStorage.setItem("userinfos", JSON.stringify(Localuser.users));
|
|
|
|
thisUser.initwebsocket().then(async () => {
|
|
const loaddesc = document.getElementById("load-desc") as HTMLElement;
|
|
thisUser.loaduser();
|
|
await thisUser.init();
|
|
loading.classList.add("doneloading");
|
|
loaddesc.textContent = I18n.loaded();
|
|
loading.classList.remove("loading");
|
|
console.log("done loading");
|
|
});
|
|
|
|
onswap?.(thisUser);
|
|
}
|
|
static userMenu = this.generateUserMenu();
|
|
private readonly userResMap = new Map<string, Promise<User>>();
|
|
async getUser(id: string) {
|
|
let user = this.userMap.get(id);
|
|
if (user) return user;
|
|
const cache = this.userResMap.get(id);
|
|
if (cache) return cache;
|
|
const prom = User.resolve(id, this);
|
|
this.userResMap.set(id, prom);
|
|
await prom;
|
|
this.userResMap.delete(id);
|
|
return prom;
|
|
}
|
|
static generateUserMenu() {
|
|
const menu = new Contextmenu<Localuser, void>("");
|
|
menu.addButton(
|
|
() => I18n.localuser.addStatus(),
|
|
function () {
|
|
const d = new Dialog(I18n.localuser.status());
|
|
const opt = d.float.options.addForm(
|
|
"",
|
|
() => {
|
|
const status = cust.value;
|
|
sessionStorage.setItem("cstatus", JSON.stringify({text: status}));
|
|
//this.user.setstatus(status);
|
|
d.hide();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/settings",
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
opt.addText(I18n.localuser.customStatusWarn());
|
|
opt.addPreprocessor((obj) => {
|
|
if ("custom_status" in obj) {
|
|
obj.custom_status = {text: obj.custom_status};
|
|
}
|
|
});
|
|
const cust = opt.addTextInput(I18n.localuser.status(), "custom_status", {});
|
|
d.show();
|
|
},
|
|
);
|
|
menu.addButton(
|
|
() => I18n.localuser.status(),
|
|
function () {
|
|
const d = new Dialog(I18n.localuser.status());
|
|
const opt = d.float.options;
|
|
const selection = ["online", "invisible", "dnd", "idle"] as const;
|
|
const smap = selection.map((_) => I18n.user[_]());
|
|
let index = selection.indexOf(this.status as "online" | "invisible" | "dnd" | "idle");
|
|
if (index === -1) {
|
|
index = 0;
|
|
}
|
|
opt
|
|
.addSelect("", () => {}, smap, {
|
|
defaultIndex: index,
|
|
})
|
|
.watchForChange(async (i) => {
|
|
const status = selection[i];
|
|
await fetch(this.info.api + "/users/@me/settings", {
|
|
body: JSON.stringify({
|
|
status,
|
|
}),
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
});
|
|
sessionStorage.setItem("status", status);
|
|
this.user.setstatus(status);
|
|
});
|
|
d.show();
|
|
},
|
|
);
|
|
menu.addButton(
|
|
() => I18n.switchAccounts(),
|
|
function () {
|
|
Localuser.showAccountSwitcher(this);
|
|
},
|
|
);
|
|
return menu;
|
|
}
|
|
async searchMembers(limit: number, query: string, guild: Guild): Promise<Member[]> {
|
|
if (guild.id !== "@me") {
|
|
return new Promise<Member[]>((res) => {
|
|
const nonce = Math.floor(Math.random() * 10 ** 8) + "";
|
|
this.ws!.send(
|
|
JSON.stringify({
|
|
op: 8,
|
|
d: {
|
|
guild_id: [guild.id],
|
|
query,
|
|
limit,
|
|
presences: true,
|
|
nonce,
|
|
},
|
|
}),
|
|
);
|
|
this.searchMap.set(nonce, async (e) => {
|
|
console.log(e);
|
|
if (e.members && e.members[0]) {
|
|
if (e.members[0].user) {
|
|
res(
|
|
(await Promise.all(e.members.map(async (_) => await Member.new(_, guild)))).filter(
|
|
(_) => _ !== undefined,
|
|
),
|
|
);
|
|
} else {
|
|
const prom1: Promise<User>[] = [];
|
|
for (const thing of e.members) {
|
|
prom1.push(this.getUser(thing.id));
|
|
}
|
|
await Promise.all(prom1);
|
|
res(
|
|
(await Promise.all(e.members.map(async (_) => await Member.new(_, guild)))).filter(
|
|
(_) => _ !== undefined,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return [];
|
|
});
|
|
});
|
|
}
|
|
return [];
|
|
}
|
|
onswap?: (l: Localuser) => void;
|
|
constructor(userinfo: Specialuser) {
|
|
Play.playURL("/audio/sounds.jasf").then((_) => {
|
|
this.play = _;
|
|
});
|
|
|
|
this.userinfo = userinfo;
|
|
this.perminfo.guilds ??= {};
|
|
this.perminfo.user ??= {};
|
|
this.perminfo.user.decorations ??= true;
|
|
this.initialized = false;
|
|
SW.postMessage({
|
|
code: "canRefresh",
|
|
host: new URL(this.info.cdn).host,
|
|
});
|
|
SW.captureEvent("refreshURL", async (e) => {
|
|
SW.postMessage({
|
|
code: "refreshedUrl",
|
|
url: await this.refreshURL(e.url),
|
|
oldurl: e.url,
|
|
});
|
|
});
|
|
this.headers = {
|
|
"Content-type": "application/json; charset=UTF-8",
|
|
Authorization: this.userinfo.token,
|
|
};
|
|
this.favorites = new Favorites(this);
|
|
const rights = this.perminfo.user.rights || "875069521787904";
|
|
this.rights = new Rights(rights);
|
|
|
|
if (this.perminfo.user.disableColors === undefined) this.perminfo.user.disableColors = true;
|
|
this.updateTranslations();
|
|
Versions.makeVersion(this.info.api, "start");
|
|
}
|
|
favorites!: Favorites;
|
|
readysup = false;
|
|
get voiceAllowed() {
|
|
return this.readysup;
|
|
}
|
|
mute = true;
|
|
deaf = false;
|
|
updateOtherMic = () => {};
|
|
updateMic(updateVoice: boolean = true) {
|
|
this.updateOtherMic();
|
|
const mic = document.getElementById("mic") as HTMLElement;
|
|
mic.classList.remove("svg-mic", "svg-micmute");
|
|
if (this.voiceFactory && updateVoice) this.voiceFactory.mute = this.mute;
|
|
if (this.mute) {
|
|
mic.classList.add("svg-micmute");
|
|
} else {
|
|
mic.classList.add("svg-mic");
|
|
}
|
|
}
|
|
trace: {trace: trace; time: Date}[] = [];
|
|
handleTrace(str: string[]) {
|
|
const json = str.map((_) => JSON.parse(_)) as trace[];
|
|
console.log(json);
|
|
this.trace.push(
|
|
...json.map((trace) => {
|
|
return {trace, time: new Date()};
|
|
}),
|
|
);
|
|
}
|
|
async queryBlog() {
|
|
this.perminfo.localuser ??= {};
|
|
const prefs = getPreferences();
|
|
const bstate = prefs.showBlogUpdates;
|
|
if (bstate === undefined) {
|
|
const pop = new Dialog("");
|
|
pop.options.addText(I18n.blog.wantUpdates());
|
|
const opts = pop.options.addOptions("", {ltr: true});
|
|
opts.addButtonInput("", I18n.yes(), async () => {
|
|
prefs.showBlogUpdates = true;
|
|
await setPreferences(prefs);
|
|
this.queryBlog();
|
|
pop.hide();
|
|
});
|
|
opts.addButtonInput("", I18n.no(), async () => {
|
|
prefs.showBlogUpdates = false;
|
|
await setPreferences(prefs);
|
|
this.queryBlog();
|
|
pop.hide();
|
|
});
|
|
pop.show();
|
|
} else if (bstate) {
|
|
const post = (await this.getPosts()).items[0];
|
|
if (this.perminfo.localuser.mostRecent !== post.url) {
|
|
this.perminfo.localuser.mostRecent = post.url;
|
|
const pop = new Dialog(post.title);
|
|
//TODO implement images for the rendering of this
|
|
pop.options.addText(post.content_html);
|
|
pop.options.addButtonInput("", I18n.blog.gotoPost(), () => {
|
|
window.open(post.url);
|
|
pop.hide();
|
|
});
|
|
pop.show();
|
|
}
|
|
}
|
|
}
|
|
guildFolders: guildFolder[] = [];
|
|
readonly unknownRead = new Map<string, readStateEntry>();
|
|
async gottenReady(ready: readyjson): Promise<void> {
|
|
this.getGifProvidors();
|
|
await I18n.done;
|
|
this.errorBackoff = 0;
|
|
this.queryBlog();
|
|
this.guildFolders = ready.d.user_settings.guild_folders;
|
|
document.body.style.setProperty("--view-rest", I18n.message.viewrest());
|
|
this.initialized = true;
|
|
this.guilds.clear();
|
|
this.channels.clear();
|
|
this.userResMap.clear();
|
|
this.inrelation.clear();
|
|
this.userMap.clear();
|
|
this.user = new User(ready.d.user, this);
|
|
this.user.setstatus(sessionStorage.getItem("status") || "online");
|
|
this.resume_gateway_url = ready.d.resume_gateway_url;
|
|
this.session_id = ready.d.session_id;
|
|
|
|
this.mdBox();
|
|
|
|
this.voiceFactory = new VoiceFactory(
|
|
{id: this.user.id},
|
|
(g) => {
|
|
if (this.ws) {
|
|
this.ws.send(JSON.stringify(g));
|
|
}
|
|
},
|
|
this.info.api.startsWith("https://"),
|
|
);
|
|
this.handleVoice();
|
|
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
|
|
this.userinfo.username = this.user.username;
|
|
this.userinfo.id = this.user.id;
|
|
this.userinfo.pfpsrc = this.user.getpfpsrc();
|
|
|
|
if (ready.d.auth_token) {
|
|
this.userinfo.token = ready.d.auth_token;
|
|
this.userinfo.json.token = ready.d.auth_token;
|
|
this.headers.Authorization = ready.d.auth_token;
|
|
this.userinfo.updateLocal();
|
|
}
|
|
|
|
this.status = ready.d.user_settings.status;
|
|
this.focusChannel = undefined;
|
|
this.focusGuild = undefined;
|
|
const members: {[key: string]: memberjson} = {};
|
|
if (ready.d.merged_members) {
|
|
for (const thing of ready.d.merged_members) {
|
|
members[thing[0].guild_id] = thing[0];
|
|
}
|
|
}
|
|
this.updateMic();
|
|
const mic = document.getElementById("mic") as HTMLElement;
|
|
mic.onclick = () => {
|
|
this.mute = !this.mute;
|
|
this.updateMic();
|
|
};
|
|
for (const thing of ready.d.guilds) {
|
|
const temp = new Guild(thing, this, members[thing.id]);
|
|
this.guilds.set(temp.id, temp);
|
|
}
|
|
{
|
|
const temp = new Direct(ready.d.private_channels, this);
|
|
this.guilds.set(temp.id, temp);
|
|
}
|
|
if (ready.d.user_guild_settings) {
|
|
console.log(ready.d.user_guild_settings.entries);
|
|
|
|
for (const thing of ready.d.user_guild_settings.entries) {
|
|
(this.guilds.get(thing.guild_id) as Guild).notisetting(thing);
|
|
}
|
|
}
|
|
if (ready.d.read_state) {
|
|
for (const thing of ready.d.read_state.entries) {
|
|
const channel = this.channels.get(thing.channel_id);
|
|
if (!channel) {
|
|
this.unknownRead.set(thing.channel_id, thing);
|
|
continue;
|
|
}
|
|
channel.readStateInfo(thing);
|
|
}
|
|
}
|
|
for (const relationship of ready.d.relationships) {
|
|
const user = new User(relationship.user, this);
|
|
user.handleRelationship(relationship);
|
|
}
|
|
|
|
this.pingEndpoint();
|
|
const prefs = getPreferences();
|
|
const ml = document.getElementById("memberlisttoggle")!;
|
|
if (prefs.checkMemberList) {
|
|
ml.classList = "";
|
|
} else {
|
|
ml.classList = "svgicon svg-friends";
|
|
}
|
|
}
|
|
readonly inrelation = new Set<User>();
|
|
outoffocus(): void {
|
|
const servers = document.getElementById("guildRail") as HTMLDivElement;
|
|
servers.innerHTML = "";
|
|
const channels = document.getElementById("channels") as HTMLDivElement;
|
|
channels.innerHTML = "";
|
|
if (this.focusChannel) {
|
|
this.focusChannel.infinite.delete();
|
|
}
|
|
this.focusGuild = undefined;
|
|
this.focusChannel = undefined;
|
|
}
|
|
giveMessage(m: messagejson) {
|
|
const c = this.channels.get(m.channel_id);
|
|
if (!c) return;
|
|
new Message(m, c);
|
|
}
|
|
unload(): void {
|
|
this.initialized = false;
|
|
this.outoffocus();
|
|
this.guilds.clear();
|
|
this.channels.clear();
|
|
this.userResMap.clear();
|
|
if (this.ws) {
|
|
this.ws.close(4040);
|
|
}
|
|
}
|
|
swapped = false;
|
|
resume_gateway_url?: string;
|
|
session_id?: string;
|
|
async initwebsocket(resume = false): Promise<void> {
|
|
let returny: () => void;
|
|
if (!this.resume_gateway_url || !this.session_id) {
|
|
resume = false;
|
|
}
|
|
if (!resume) {
|
|
this.messages.clear();
|
|
this.idToPrev.clear();
|
|
this.idToNext.clear();
|
|
}
|
|
const doComp = DecompressionStream && !getDeveloperSettings().gatewayCompression;
|
|
const ws = new WebSocket(
|
|
(resume ? this.resume_gateway_url : this.info.gateway.toString()) +
|
|
"?encoding=json&v=9" +
|
|
(doComp ? "&compress=zlib-stream" : ""),
|
|
);
|
|
this.ws = ws;
|
|
let ds: DecompressionStream;
|
|
let w: WritableStreamDefaultWriter;
|
|
let arr: Uint8Array;
|
|
|
|
if (DecompressionStream) {
|
|
ds = new DecompressionStream("deflate");
|
|
w = ds.writable.getWriter();
|
|
|
|
arr = new Uint8Array();
|
|
}
|
|
const promise = new Promise<void>((res) => {
|
|
returny = res;
|
|
ws.addEventListener("open", (_event) => {
|
|
console.log("WebSocket connected");
|
|
if (resume) {
|
|
ws.send(
|
|
JSON.stringify({
|
|
op: 6,
|
|
d: {
|
|
token: this.token,
|
|
session_id: this.session_id,
|
|
seq: this.lastSequence,
|
|
},
|
|
}),
|
|
);
|
|
this.resume_gateway_url = undefined;
|
|
this.session_id = undefined;
|
|
} else {
|
|
ws.send(
|
|
JSON.stringify({
|
|
op: 2,
|
|
d: {
|
|
token: this.token,
|
|
capabilities: 16381,
|
|
properties: {
|
|
browser: "Fermi",
|
|
client_build_number: 0, //might update this eventually lol
|
|
release_channel: "Custom",
|
|
browser_user_agent: navigator.userAgent,
|
|
},
|
|
compress: Boolean(DecompressionStream),
|
|
presence: {
|
|
status: sessionStorage.getItem("status") || "online",
|
|
since: null, //new Date().getTime()
|
|
activities: [],
|
|
afk: false,
|
|
}, //TODO think this through, it's just a stupid large number to fix op 8 requests
|
|
large_threshold: 100000000,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
if (DecompressionStream) {
|
|
(async () => {
|
|
let build = "";
|
|
for await (const data of ds.readable.pipeThrough(new TextDecoderStream())) {
|
|
build += data;
|
|
try {
|
|
const temp = JSON.parse(build);
|
|
build = "";
|
|
await this.handleEvent(temp);
|
|
|
|
if (temp.op === 0 && temp.t === "READY") {
|
|
console.log("in here?");
|
|
returny();
|
|
}
|
|
} catch (e) {
|
|
if (!(e instanceof SyntaxError)) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
});
|
|
|
|
let order = new Promise<void>((res) => res());
|
|
|
|
ws.addEventListener("message", async (event) => {
|
|
const temp2 = order;
|
|
order = new Promise<void>(async (res) => {
|
|
await temp2;
|
|
let temp: {op: number; t: string};
|
|
try {
|
|
if (event.data instanceof Blob) {
|
|
const buff = await event.data.arrayBuffer();
|
|
const array = new Uint8Array(buff);
|
|
|
|
const temparr = new Uint8Array(array.length + arr.length);
|
|
temparr.set(arr, 0);
|
|
temparr.set(array, arr.length);
|
|
arr = temparr;
|
|
|
|
const len = array.length;
|
|
if (
|
|
!(
|
|
array[len - 1] === 255 &&
|
|
array[len - 2] === 255 &&
|
|
array[len - 3] === 0 &&
|
|
array[len - 4] === 0
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
w.write(arr.buffer);
|
|
arr = new Uint8Array();
|
|
return; //had to move the while loop due to me being dumb
|
|
} else {
|
|
temp = JSON.parse(event.data);
|
|
}
|
|
|
|
await this.handleEvent(temp as readyjson);
|
|
if (temp.op === 0 && temp.t === "READY") {
|
|
returny();
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
res();
|
|
}
|
|
});
|
|
});
|
|
|
|
ws.addEventListener("close", async (event) => {
|
|
this.ws = undefined;
|
|
this.voiceFactory?.close();
|
|
console.log("WebSocket closed with code " + event.code);
|
|
if (
|
|
(event.code > 1000 && event.code < 1016 && this.errorBackoff === 0) ||
|
|
(wsCodesRetry.has(event.code) && this.errorBackoff === 0)
|
|
) {
|
|
this.errorBackoff++;
|
|
this.initwebsocket(true).then(() => {
|
|
this.loaduser();
|
|
});
|
|
return;
|
|
}
|
|
this.unload();
|
|
(document.getElementById("loading") as HTMLElement).classList.remove("doneloading");
|
|
(document.getElementById("loading") as HTMLElement).classList.add("loading");
|
|
this.fetchingmembers.clear();
|
|
this.memberNonceMap.clear();
|
|
this.memberNonceBuild.clear();
|
|
const loaddesc = document.getElementById("load-desc") as HTMLElement;
|
|
if (
|
|
(event.code > 1000 && event.code < 1016) ||
|
|
wsCodesRetry.has(event.code) ||
|
|
event.code == 4041
|
|
) {
|
|
if (this.connectionSucceed !== 0 && Date.now() > this.connectionSucceed + 20000) {
|
|
this.errorBackoff = 0;
|
|
} else this.errorBackoff++;
|
|
this.connectionSucceed = 0;
|
|
|
|
loaddesc.innerHTML = "";
|
|
loaddesc.append(
|
|
new MarkDown(
|
|
I18n.errorReconnect(Math.round(0.2 + this.errorBackoff * 2.8) + ""),
|
|
).makeHTML(),
|
|
);
|
|
switch (
|
|
this.errorBackoff //try to recover from bad domain
|
|
) {
|
|
case 3:
|
|
const newurls = await getapiurls(this.info.wellknown);
|
|
if (newurls) {
|
|
this.info = newurls;
|
|
this.userinfo.json.serverurls = this.info;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 4: {
|
|
const newurls = await getapiurls(new URL(this.info.wellknown).origin);
|
|
if (newurls) {
|
|
this.info = newurls;
|
|
this.userinfo.json.serverurls = this.info;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case 5: {
|
|
const breakappart = new URL(this.info.wellknown).host.split(".");
|
|
const url = "https://" + breakappart.at(-2) + "." + breakappart.at(-1);
|
|
const newurls = await getapiurls(url);
|
|
if (newurls) {
|
|
this.info = newurls;
|
|
this.userinfo.json.serverurls = this.info;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
setTimeout(
|
|
() => {
|
|
if (this.swapped) return;
|
|
loaddesc.textContent = I18n.retrying();
|
|
this.initwebsocket().then(async () => {
|
|
console.log("FINE ME");
|
|
this.loaduser();
|
|
await this.init();
|
|
const loading = document.getElementById("loading") as HTMLElement;
|
|
loading.classList.add("doneloading");
|
|
loading.classList.remove("loading");
|
|
loaddesc.textContent = I18n.loaded();
|
|
console.log("done loading");
|
|
});
|
|
},
|
|
200 + this.errorBackoff * 2800,
|
|
);
|
|
} else loaddesc.textContent = I18n.unableToConnect();
|
|
});
|
|
console.log("here?");
|
|
await promise;
|
|
console.warn("huh");
|
|
}
|
|
readonly interNonceMap = new Map<string, Message>();
|
|
registerInterNonce(nonce: string, thing: Message) {
|
|
this.interNonceMap.set(nonce, thing);
|
|
}
|
|
relationshipsUpdate = () => {};
|
|
rights: Rights;
|
|
updateRights(rights: string | number) {
|
|
if (this.rights.isSameAs(rights)) return;
|
|
this.rights.update(rights);
|
|
this.perminfo.user.rights = rights;
|
|
}
|
|
traceSub() {
|
|
SW.captureEvent("trace", (e) => {
|
|
this.handleTrace(e.trace);
|
|
});
|
|
}
|
|
readonly relChangeUpdateMap = new Map<string, (() => void)[]>();
|
|
async relationChange(id: string): Promise<void> {
|
|
const arr = this.relChangeUpdateMap.get(id) || [];
|
|
const {promise, resolve} = Promise.withResolvers<void>();
|
|
arr.push(resolve);
|
|
this.relChangeUpdateMap.set(id, arr);
|
|
return promise;
|
|
}
|
|
conectionChange = () => {};
|
|
async handleEvent(temp: wsjson) {
|
|
try {
|
|
window.checker?.checkEvent(temp);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
if (temp.d._trace) this.handleTrace(temp.d._trace);
|
|
if (getDeveloperSettings().gatewayLogging) console.debug(temp);
|
|
if (temp.s) this.lastSequence = temp.s;
|
|
if (temp.op === 9 && this.ws) {
|
|
this.errorBackoff = 0;
|
|
this.ws.close(4041);
|
|
}
|
|
if (temp.op == 0) {
|
|
switch (temp.t) {
|
|
case "THREAD_MEMBERS_UPDATE": {
|
|
const channel = this.channels.get(temp.d.id);
|
|
if (!channel) return;
|
|
if (temp.d.added_members) {
|
|
for (const memb of temp.d.added_members) {
|
|
if (memb.user_id === this.user.id) {
|
|
channel.member = memb;
|
|
channel.parent?.createguildHTML();
|
|
} else {
|
|
//TODO store these somewhere
|
|
}
|
|
}
|
|
}
|
|
if (temp.d.removed_member_ids) {
|
|
for (const id of temp.d.removed_member_ids) {
|
|
if (id === this.user.id) {
|
|
channel.member = undefined;
|
|
channel.parent?.createguildHTML();
|
|
} else {
|
|
//TODO unstore these somewhere
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "INTERACTION_FAILURE":
|
|
case "INTERACTION_CREATE":
|
|
case "INTERACTION_SUCCESS":
|
|
const m = this.interNonceMap.get(temp.d.nonce);
|
|
if (m) {
|
|
//Punt the events off to the message class
|
|
m.interactionEvents(temp);
|
|
}
|
|
break;
|
|
case "MESSAGE_CREATE":
|
|
if (this.initialized) {
|
|
this.messageCreate(temp);
|
|
}
|
|
break;
|
|
case "USER_NOTE_UPDATE": {
|
|
const u = this.userMap.get(temp.d.id);
|
|
if (u) u.note = temp.d.note;
|
|
break;
|
|
}
|
|
case "USER_CONNECTIONS_UPDATE": {
|
|
this.conectionChange();
|
|
break;
|
|
}
|
|
case "MESSAGE_POLL_VOTE_ADD":
|
|
case "MESSAGE_POLL_VOTE_REMOVE": {
|
|
const m = this.messages.get(temp.d.message_id);
|
|
m?.pollUpdate(temp);
|
|
const f = this.pollUpdateSubMap.get(temp.d.message_id);
|
|
f?.(temp);
|
|
break;
|
|
}
|
|
case "MESSAGE_DELETE": {
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.id);
|
|
if (!message) break;
|
|
message.deleteEvent();
|
|
break;
|
|
}
|
|
case "READY":
|
|
await this.gottenReady(temp as readyjson);
|
|
break;
|
|
case "MESSAGE_UPDATE": {
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.id);
|
|
if (!message) break;
|
|
message.giveData(temp.d);
|
|
break;
|
|
}
|
|
case "TYPING_START":
|
|
if (this.initialized) {
|
|
this.typingStart(temp);
|
|
}
|
|
break;
|
|
case "USER_UPDATE":
|
|
if (this.initialized) {
|
|
const users = this.userMap.get(temp.d.id);
|
|
if (users) {
|
|
users.userupdate(temp.d);
|
|
}
|
|
}
|
|
break;
|
|
case "CHANNEL_PINS_UPDATE":
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
delete channel.pinnedMessages;
|
|
channel.lastpin = new Date() + "";
|
|
const pinnedM = document.getElementById("pinnedMDiv");
|
|
if (pinnedM) {
|
|
pinnedM.classList.add("unreadPin");
|
|
}
|
|
break;
|
|
case "CHANNEL_UPDATE":
|
|
if (this.initialized) {
|
|
this.updateChannel(temp.d);
|
|
}
|
|
break;
|
|
case "CHANNEL_CREATE":
|
|
case "THREAD_CREATE":
|
|
if (this.initialized) {
|
|
this.createChannel(temp.d);
|
|
}
|
|
break;
|
|
case "CHANNEL_DELETE":
|
|
if (this.initialized) {
|
|
this.delChannel(temp.d);
|
|
}
|
|
break;
|
|
case "GUILD_DELETE": {
|
|
const guildy = this.guilds.get(temp.d.id);
|
|
if (guildy) {
|
|
this.guilds.delete(temp.d.id);
|
|
guildy.html.remove();
|
|
if (guildy === this.focusGuild) {
|
|
this.guilds.get("@me")?.loadGuild();
|
|
this.guilds.get("@me")?.loadChannel();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "GUILD_UPDATE": {
|
|
const guildy = this.guilds.get(temp.d.id);
|
|
if (guildy) {
|
|
guildy.update(temp.d);
|
|
}
|
|
break;
|
|
}
|
|
case "GUILD_CREATE":
|
|
(async () => {
|
|
const m = temp.d.members.find(({id}) => id === this.user.id);
|
|
const guildy = new Guild(temp.d, this, m ?? this.user);
|
|
this.guilds.set(guildy.id, guildy);
|
|
for (const m of temp.d.members) {
|
|
Member.newUnsafe(m, guildy);
|
|
}
|
|
const divy = this.makeGuildIcon(guildy);
|
|
guildy.HTMLicon = divy;
|
|
(document.getElementById("guildRail") as HTMLDivElement).insertBefore(
|
|
divy,
|
|
document.getElementById("bottomseparator"),
|
|
);
|
|
guildy.messageNotifications = guildy.properties.default_message_notifications;
|
|
guildy.showWelcome();
|
|
})();
|
|
break;
|
|
case "MESSAGE_REACTION_ADD":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
let thing: Member | {id: string};
|
|
if (temp.d.member) {
|
|
thing = (await Member.new(temp.d.member, guild)) as Member;
|
|
} else {
|
|
thing = {id: temp.d.user_id};
|
|
}
|
|
message.reactionAdd(temp.d.emoji, thing);
|
|
}
|
|
break;
|
|
case "MESSAGE_REACTION_REMOVE":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
|
|
message.reactionRemove(temp.d.emoji, temp.d.user_id);
|
|
}
|
|
break;
|
|
case "MESSAGE_REACTION_REMOVE_ALL":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
message.reactionRemoveAll();
|
|
}
|
|
break;
|
|
case "MESSAGE_REACTION_REMOVE_EMOJI":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
message.reactionRemoveEmoji(temp.d.emoji);
|
|
}
|
|
break;
|
|
case "GUILD_MEMBERS_CHUNK":
|
|
this.gotChunk(temp.d);
|
|
break;
|
|
case "GUILD_MEMBER_LIST_UPDATE": {
|
|
this.memberListUpdate(temp);
|
|
break;
|
|
}
|
|
case "READY_SUPPLEMENTAL":
|
|
{
|
|
temp.d.guilds.forEach((_) =>
|
|
_.voice_states.forEach((status) => {
|
|
if (this.voiceFactory && status.channel_id) {
|
|
this.voiceFactory.voiceStateUpdate(status);
|
|
console.log(status);
|
|
}
|
|
}),
|
|
);
|
|
this.readysup = temp.d.guilds.length !== 0;
|
|
}
|
|
break;
|
|
case "VOICE_STATE_UPDATE":
|
|
if (this.user.id === temp.d.user_id) {
|
|
this.mute = temp.d.self_mute;
|
|
this.updateMic(false);
|
|
}
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.voiceStateUpdate(temp.d);
|
|
}
|
|
|
|
break;
|
|
case "STREAM_SERVER_UPDATE": {
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.streamServerUpdate(temp);
|
|
}
|
|
break;
|
|
}
|
|
case "STREAM_CREATE": {
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.streamCreate(temp);
|
|
}
|
|
break;
|
|
}
|
|
case "VOICE_SERVER_UPDATE":
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.voiceServerUpdate(temp);
|
|
}
|
|
break;
|
|
case "GUILD_ROLE_CREATE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.newRole(temp.d.role);
|
|
break;
|
|
}
|
|
case "GUILD_ROLE_UPDATE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.updateRole(temp.d.role);
|
|
break;
|
|
}
|
|
case "GUILD_ROLE_DELETE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.deleteRole(temp.d.role_id);
|
|
break;
|
|
}
|
|
case "GUILD_MEMBER_UPDATE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.memberupdate(temp.d);
|
|
break;
|
|
}
|
|
case "RELATIONSHIP_UPDATE":
|
|
case "RELATIONSHIP_ADD": {
|
|
(async () => {
|
|
const user = temp.d.user ? new User(temp.d.user, this) : await this.getUser(temp.d.id);
|
|
user.handleRelationship(temp.d);
|
|
this.relationshipsUpdate();
|
|
const me = this.guilds.get("@me");
|
|
if (!me) return;
|
|
me.unreads();
|
|
const arr = this.relChangeUpdateMap.get(user.id);
|
|
if (arr) {
|
|
arr.forEach((_) => _());
|
|
this.relChangeUpdateMap.delete(user.id);
|
|
}
|
|
})();
|
|
break;
|
|
}
|
|
case "RELATIONSHIP_REMOVE": {
|
|
const user = this.userMap.get(temp.d.id);
|
|
if (!user) return;
|
|
user.removeRelation();
|
|
this.relationshipsUpdate();
|
|
const arr = this.relChangeUpdateMap.get(user.id);
|
|
if (arr) {
|
|
arr.forEach((_) => _());
|
|
this.relChangeUpdateMap.delete(user.id);
|
|
}
|
|
break;
|
|
}
|
|
case "PRESENCE_UPDATE": {
|
|
if (temp.d.user) {
|
|
const user = new User(temp.d.user, this);
|
|
this.presences.set(temp.d.user.id, temp.d);
|
|
user.setstatus(temp.d.status);
|
|
if (user === this.user) this.loaduser();
|
|
}
|
|
break;
|
|
}
|
|
case "GUILD_MEMBER_ADD": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
Member.new(temp.d, guild);
|
|
break;
|
|
}
|
|
case "GUILD_MEMBER_REMOVE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
const user = new User(temp.d.user, this);
|
|
const member = user.members.get(guild);
|
|
if (!(member instanceof Member)) break;
|
|
member.remove();
|
|
break;
|
|
}
|
|
case "GUILD_EMOJIS_UPDATE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.emojis = temp.d.emojis;
|
|
guild.onEmojiUpdate(guild.emojis);
|
|
break;
|
|
}
|
|
case "GUILD_STICKERS_UPDATE": {
|
|
const guild = this.guilds.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.stickers.clear();
|
|
temp.d.stickers.forEach((sticker) =>
|
|
guild.stickers.set(sticker.id, new Sticker(sticker, guild)),
|
|
);
|
|
|
|
guild.onStickerUpdate(guild.stickers);
|
|
break;
|
|
}
|
|
case "CHANNEL_RECIPIENT_REMOVE": {
|
|
const guild = this.guilds.get("@me") as Direct;
|
|
const channel = guild.channels.find(({id}) => id == temp.d.channel_id) as Group;
|
|
if (!channel) break;
|
|
channel.removeRec(new User(temp.d.user, this));
|
|
break;
|
|
}
|
|
case "CHANNEL_RECIPIENT_ADD": {
|
|
const guild = this.guilds.get("@me") as Direct;
|
|
const channel = guild.channels.find(({id}) => id == temp.d.channel_id) as Group;
|
|
if (!channel) break;
|
|
channel.addRec(new User(temp.d.user, this));
|
|
break;
|
|
}
|
|
case "MESSAGE_ACK": {
|
|
const channel = this.channels.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
channel.lastreadmessageid = temp.d.message_id;
|
|
channel.mentions = 0;
|
|
channel.unreads();
|
|
channel.guild.unreads();
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
//@ts-expect-error
|
|
console.warn("Unhandled case " + temp.t, temp);
|
|
}
|
|
}
|
|
this.generateFavicon();
|
|
} else if (temp.op === 10) {
|
|
if (!this.ws) return;
|
|
console.log("heartbeat down");
|
|
this.heartbeat_interval = temp.d.heartbeat_interval;
|
|
this.ws.send(JSON.stringify({op: 1, d: this.lastSequence}));
|
|
} else if (temp.op === 11) {
|
|
setTimeout((_: any) => {
|
|
if (!this.ws) return;
|
|
const reasons = this.generateReasons();
|
|
|
|
if (this.connectionSucceed === 0) this.connectionSucceed = Date.now();
|
|
this.ws.send(
|
|
JSON.stringify({
|
|
op: 40,
|
|
d: {seq: this.lastSequence, qos: {ver: 27, active: !!reasons.length, reasons}},
|
|
}),
|
|
);
|
|
}, this.heartbeat_interval);
|
|
} else {
|
|
console.log("Unhandled case " + temp.d, temp);
|
|
}
|
|
}
|
|
generateReasons() {
|
|
const reasons: string[] = [];
|
|
if (!document.hidden) reasons.push("foregrounded");
|
|
return reasons;
|
|
}
|
|
setUserAudio(id: string, vol: number) {
|
|
if (!this.perminfo.userAudio) this.perminfo.userAudio = {};
|
|
this.perminfo.userAudio[id] = vol;
|
|
const v = this.voiceFactory?.currentVoice;
|
|
if (v) {
|
|
const u = v.uVolMap.get(id);
|
|
if (u) u.volume = vol / 100;
|
|
}
|
|
}
|
|
getUserAudio(id: string) {
|
|
if (!this.perminfo.userAudio) this.perminfo.userAudio = {};
|
|
|
|
return (this.perminfo.userAudio[id] as number) ?? 100;
|
|
}
|
|
get currentVoice() {
|
|
return this.voiceFactory?.currentVoice;
|
|
}
|
|
async getAudioDeviceList() {
|
|
return (await navigator.mediaDevices.enumerateDevices()).filter(
|
|
(dev) => dev.kind === "audioinput",
|
|
);
|
|
}
|
|
async setNewDefualtDevice(id: string) {
|
|
const devices = await this.getAudioDeviceList();
|
|
const d = devices.find((_) => _.deviceId === id);
|
|
if (!d) return;
|
|
const device = await navigator.mediaDevices.getUserMedia({
|
|
audio: {
|
|
deviceId: {exact: d.deviceId},
|
|
},
|
|
});
|
|
if (!device) return;
|
|
await this.voiceFactory?.currentVoice?.giveMicTrack(device);
|
|
this.perminfo.localuser.defaultAudio = id;
|
|
}
|
|
getDefaultAudio() {
|
|
return this.perminfo.localuser.defaultAudio || "default";
|
|
}
|
|
async joinVoice(channel: Channel) {
|
|
if (!this.voiceFactory) return;
|
|
if (!this.ws) return;
|
|
const v = this.voiceFactory.joinVoice(channel.id, channel.guild.id, this.mute);
|
|
if (this.perminfo.localuser.defaultAudio) {
|
|
const devices = await this.getAudioDeviceList();
|
|
const d = devices.find((_) => _.deviceId === this.perminfo.localuser.defaultAudio);
|
|
if (d) {
|
|
navigator.mediaDevices
|
|
.getUserMedia({
|
|
audio: {
|
|
deviceId: {exact: d.deviceId},
|
|
},
|
|
})
|
|
.then((_) => v.giveMicTrack(_));
|
|
return;
|
|
}
|
|
}
|
|
const d = await navigator.mediaDevices.getUserMedia({video: false, audio: true});
|
|
this.perminfo.localuser.defaultAudio = d.getAudioTracks()[0]?.id;
|
|
v.giveMicTrack(d);
|
|
return undefined;
|
|
}
|
|
regenVoiceIcons = () => {};
|
|
changeVCStatus(status: voiceStatusStr, channel: Channel) {
|
|
const statuselm = document.getElementById("VoiceStatus");
|
|
const VoiceGuild = document.getElementById("VoiceGuild");
|
|
const VoiceButtons = document.getElementById("VoiceButtons");
|
|
if (!statuselm || !VoiceGuild || !VoiceButtons) throw new Error("Missing status element");
|
|
|
|
statuselm.textContent = I18n.Voice.status[status]();
|
|
const guildName = document.createElement("span");
|
|
guildName.textContent = channel.guild.properties.name;
|
|
const channelName = document.createElement("span");
|
|
channelName.textContent = channel.name;
|
|
VoiceGuild.innerHTML = ``;
|
|
VoiceGuild.append(guildName, " / ", channelName);
|
|
VoiceGuild.onclick = () => {
|
|
this.goToChannel(channel.id);
|
|
};
|
|
|
|
VoiceButtons.innerHTML = "";
|
|
|
|
const leave = document.createElement("div");
|
|
const leaveIcon = document.createElement("span");
|
|
leaveIcon.classList.add("svg-hangup");
|
|
leave.append(leaveIcon);
|
|
|
|
leave.onclick = () => {
|
|
channel.voice?.leave();
|
|
};
|
|
|
|
const screenShare = document.createElement("div");
|
|
const screenShareIcon = document.createElement("span");
|
|
const updateStreamIcon = () => {
|
|
screenShareIcon.classList.remove("svg-stopstream", "svg-stream");
|
|
if (channel.voice?.isLive()) {
|
|
screenShareIcon.classList.add("svg-stopstream");
|
|
} else {
|
|
screenShareIcon.classList.add("svg-stream");
|
|
}
|
|
};
|
|
updateStreamIcon();
|
|
|
|
screenShare.append(screenShareIcon);
|
|
|
|
screenShare.onclick = async () => {
|
|
if (channel.voice?.isLive()) {
|
|
channel.voice.stopStream();
|
|
} else {
|
|
const stream = await navigator.mediaDevices.getDisplayMedia();
|
|
await channel.voice?.createLive(stream);
|
|
}
|
|
updateStreamIcon();
|
|
};
|
|
|
|
const video = document.createElement("div");
|
|
const videoIcon = document.createElement("span");
|
|
const updateVideoIconIcon = () => {
|
|
videoIcon.classList.remove("svg-novideo", "svg-video");
|
|
if (this.voiceFactory?.video) {
|
|
videoIcon.classList.add("svg-video");
|
|
} else {
|
|
videoIcon.classList.add("svg-novideo");
|
|
}
|
|
};
|
|
updateVideoIconIcon();
|
|
|
|
video.append(videoIcon);
|
|
|
|
video.onclick = async () => {
|
|
if (this.voiceFactory?.video) {
|
|
channel.voice?.stopVideo();
|
|
} else {
|
|
const cam = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
advanced: [
|
|
{
|
|
aspectRatio: 1.75,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
if (!cam) return;
|
|
channel.voice?.startVideo(cam);
|
|
}
|
|
updateVideoIconIcon();
|
|
};
|
|
|
|
this.regenVoiceIcons = () => {
|
|
updateStreamIcon();
|
|
updateVideoIconIcon();
|
|
};
|
|
VoiceButtons.append(leave, video, screenShare);
|
|
|
|
const clear = () => {
|
|
statuselm.textContent = "";
|
|
VoiceGuild.textContent = "";
|
|
VoiceButtons.innerHTML = "";
|
|
};
|
|
const conSet = new Set(["notconnected"]);
|
|
if (conSet.has(status)) {
|
|
setTimeout(() => {
|
|
if (statuselm.textContent === I18n.Voice.status[status]()) {
|
|
clear();
|
|
}
|
|
}, 2000);
|
|
} else if (status === "left") {
|
|
clear();
|
|
}
|
|
}
|
|
handleVoice() {
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.onJoin = (voice) => {
|
|
voice.onSatusChange = (status) => {
|
|
let channel: Channel | undefined = this.channels.values().find((_) => _.voice === voice);
|
|
if (channel) this.changeVCStatus(status, channel);
|
|
else console.error("Uh, no channel found?");
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
heartbeat_interval: number = 0;
|
|
updateChannel(json: channeljson): void {
|
|
const guild = this.guilds.get(json.guild_id || "@me");
|
|
if (guild) {
|
|
guild.updateChannel(json);
|
|
if (json.guild_id === this.focusGuild?.id) {
|
|
this.loadGuild(json.guild_id);
|
|
}
|
|
}
|
|
}
|
|
async createChannel(json: channeljson): Promise<undefined | Channel> {
|
|
const c = this.channels.get(json.id);
|
|
if (c) {
|
|
c.updateChannel(json);
|
|
return c;
|
|
}
|
|
json.guild_id ??= "@me";
|
|
const guild = this.guilds.get(json.guild_id);
|
|
if (!guild) return;
|
|
if (guild.channels.find((_) => _.id === json.id)) return;
|
|
const channel = guild.createChannelpac(json);
|
|
if (json.guild_id === this.focusGuild?.id) {
|
|
this.loadGuild(json.guild_id, true);
|
|
}
|
|
if (channel.id === this.gotoid) {
|
|
guild.loadGuild();
|
|
if (channel?.type !== 4) {
|
|
await guild.loadChannel(channel.id);
|
|
}
|
|
this.gotoRes();
|
|
this.gotoRes = () => {};
|
|
this.gotoid = undefined;
|
|
}
|
|
return channel; // Add this line to return the 'channel' variable
|
|
}
|
|
listque = false;
|
|
memberListQue() {
|
|
if (this.listque) {
|
|
return;
|
|
}
|
|
this.listque = true;
|
|
setTimeout(async () => {
|
|
await this.memberListUpdate();
|
|
this.listque = false;
|
|
}, 100);
|
|
}
|
|
async memberListUpdate(list: memberlistupdatejson | void) {
|
|
if (this.searching) return;
|
|
const guild = this.focusGuild;
|
|
if (!guild) return;
|
|
|
|
const channel = this.focusChannel;
|
|
if (!channel) return;
|
|
if (channel.voice && this.voiceAllowed) {
|
|
const div = document.getElementById("sideDiv") as HTMLDivElement;
|
|
div.textContent = "";
|
|
return;
|
|
}
|
|
if (guild.id === "@me" && (channel as Group).type === 1) {
|
|
const div = document.getElementById("sideDiv") as HTMLDivElement;
|
|
div.textContent = "";
|
|
return;
|
|
}
|
|
|
|
if (list) {
|
|
if (list.d.guild_id !== guild.id) {
|
|
return;
|
|
}
|
|
const counts = new Map<string, number>();
|
|
await Promise.all(
|
|
list.d.ops[0].items.map(async (member) => {
|
|
if ("member" in member) {
|
|
if (this.userMap.get(member.member.id)?.members.has(guild)) return;
|
|
if (this.focusGuild?.id && this.focusGuild?.id !== "@me")
|
|
await this.getMember(member.member.user!.id, this.focusGuild.id);
|
|
} else {
|
|
counts.set(member.group.id, member.group.count);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
const elms: Map<Role | "offline" | "online", (Member | User)[]> = new Map([]);
|
|
for (const role of guild.roles) {
|
|
if (role.hoist) {
|
|
elms.set(role, []);
|
|
}
|
|
}
|
|
elms.set("online", []);
|
|
elms.set("offline", []);
|
|
let members = new Set<User | Member>(guild.members.values());
|
|
if (channel instanceof Group) {
|
|
members = new Set(channel.users);
|
|
members.add(this.user);
|
|
}
|
|
members.forEach((member) => {
|
|
if (member instanceof User) {
|
|
return;
|
|
}
|
|
if (!channel.hasPermission("VIEW_CHANNEL", member)) {
|
|
members.delete(member);
|
|
return;
|
|
}
|
|
});
|
|
|
|
for (const [role, list] of elms) {
|
|
members.forEach((member) => {
|
|
const user = member instanceof Member ? member.user : member;
|
|
if (role === "offline") {
|
|
if (user.getStatus() === "offline" || user.getStatus() === "invisible") {
|
|
list.push(member);
|
|
members.delete(member);
|
|
}
|
|
return;
|
|
}
|
|
if (user.getStatus() === "offline" || user.getStatus() === "invisible") {
|
|
return;
|
|
}
|
|
if (member instanceof Member) {
|
|
if (role !== "online" && member.hasRole(role.id)) {
|
|
list.push(member);
|
|
members.delete(member);
|
|
}
|
|
}
|
|
});
|
|
if (!list.length) continue;
|
|
list.sort((a, b) => {
|
|
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
|
});
|
|
}
|
|
const online = [...members];
|
|
online.sort((a, b) => {
|
|
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
|
});
|
|
elms.set("online", online);
|
|
this.generateListHTML(elms, channel);
|
|
}
|
|
instanceString(): string {
|
|
const insts = getInstances();
|
|
if (insts) {
|
|
for (const inst of insts) {
|
|
if (trimTrailingSlashes(inst.url ?? "") === trimTrailingSlashes(this.info.wellknown)) {
|
|
return inst.name;
|
|
}
|
|
}
|
|
}
|
|
console.log("failed with", insts, this.info.wellknown);
|
|
return this.info.wellknown;
|
|
}
|
|
readonly roleListMap = new WeakMap<
|
|
HTMLDivElement,
|
|
{
|
|
role: Role | "offline" | "online";
|
|
memberListMap: Map<HTMLElement, User>;
|
|
}
|
|
>();
|
|
listGuild?: Guild;
|
|
generateListHTML(elms: Map<Role | "offline" | "online", (Member | User)[]>, channel: Channel) {
|
|
const div = document.getElementById("sideDiv") as HTMLDivElement;
|
|
let roleMap = new Map<
|
|
Role | "offline" | "online",
|
|
{elm: HTMLDivElement; memberListMap: Map<HTMLElement, User>}
|
|
>();
|
|
if (channel.guild !== this.listGuild) {
|
|
this.listGuild = channel.guild;
|
|
div.innerHTML = "";
|
|
}
|
|
Array.from(div.children)
|
|
.map((_) => [this.roleListMap.get(_ as HTMLDivElement), _ as HTMLDivElement] as const)
|
|
.forEach(([role, elm]) => {
|
|
if (role && elms.get(role.role)?.length) {
|
|
if (document.contains(elm))
|
|
roleMap.set(role.role, {elm, memberListMap: role.memberListMap});
|
|
} else if (elm) {
|
|
elm.remove();
|
|
}
|
|
});
|
|
div.classList.remove("searchDiv");
|
|
div.classList.remove("hideSearchDiv");
|
|
let lastDiv: HTMLDivElement | void = undefined;
|
|
for (const [role, list] of elms) {
|
|
if (!list.length) continue;
|
|
|
|
let category: HTMLDivElement;
|
|
let memberMap: Map<HTMLElement, User>;
|
|
const getF = roleMap.get(role);
|
|
roleMap.delete(role);
|
|
if (getF) {
|
|
category = getF.elm;
|
|
memberMap = getF.memberListMap;
|
|
if (lastDiv) {
|
|
const nextElm = lastDiv.nextElementSibling as HTMLElement | null;
|
|
if (nextElm !== category) {
|
|
lastDiv.after(category);
|
|
}
|
|
} else {
|
|
const first = div.firstElementChild;
|
|
if (first !== category) {
|
|
div.prepend(category);
|
|
}
|
|
}
|
|
} else {
|
|
category = document.createElement("div");
|
|
category.classList.add("memberList");
|
|
let title = document.createElement("h3");
|
|
if (role === "offline") {
|
|
title.textContent = I18n.user.offline();
|
|
category.classList.add("offline");
|
|
} else if (role === "online") {
|
|
title.textContent = I18n.user.online();
|
|
} else {
|
|
title.textContent = role.name;
|
|
}
|
|
category.append(title);
|
|
const membershtml = document.createElement("div");
|
|
membershtml.classList.add("flexttb");
|
|
category.append(membershtml);
|
|
memberMap = new Map();
|
|
if (lastDiv) {
|
|
lastDiv.after(category);
|
|
} else {
|
|
div.prepend(category);
|
|
}
|
|
this.roleListMap.set(category, {
|
|
role,
|
|
memberListMap: memberMap,
|
|
});
|
|
}
|
|
lastDiv = category;
|
|
const membershtml = category.getElementsByTagName("div")[0];
|
|
const cur = new Set(
|
|
list.map((member) => {
|
|
return member instanceof Member ? member.user : member;
|
|
}),
|
|
);
|
|
const userToHTMLMap = new Map<User, HTMLElement>();
|
|
Array.from(membershtml.children)
|
|
.map((_) => [_ as HTMLElement, memberMap.get(_ as HTMLElement)] as const)
|
|
.forEach(([elm, memb]) => {
|
|
if (!memb || !cur.has(memb)) {
|
|
memberMap.delete(elm);
|
|
elm.remove();
|
|
} else {
|
|
userToHTMLMap.set(memb, elm);
|
|
}
|
|
});
|
|
const makeMemberDiv = (user: User, member: Member | User) => {
|
|
user.localstatusUpdate = () => {
|
|
this.memberListQue();
|
|
};
|
|
const memberdiv = document.createElement("div");
|
|
memberMap.set(memberdiv, user);
|
|
const pfp = user.buildstatuspfp(channel);
|
|
const username = document.createElement("span");
|
|
username.classList.add("ellipsis");
|
|
username.textContent = member.name;
|
|
member.subName(username);
|
|
if (user.bot) {
|
|
const bot = document.createElement("span");
|
|
bot.classList.add("bot");
|
|
bot.textContent = I18n.bot();
|
|
username.appendChild(bot);
|
|
}
|
|
|
|
memberdiv.append(pfp, username);
|
|
if (channel instanceof Group) {
|
|
Group.groupMenu.bindContextmenu(memberdiv, channel, user);
|
|
if (channel.owner_id === user.id) {
|
|
const crown = document.createElement("span");
|
|
crown.classList.add("svg-crown");
|
|
memberdiv.append(crown);
|
|
}
|
|
}
|
|
member.bind(username);
|
|
user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false);
|
|
|
|
memberdiv.classList.add("flexltr", "liststyle", "memberListStyle");
|
|
return memberdiv;
|
|
};
|
|
|
|
let lastElm: void | HTMLElement = void 0;
|
|
for (const member of list) {
|
|
const user = member instanceof Member ? member.user : member;
|
|
let elm = userToHTMLMap.get(user);
|
|
if (!elm) {
|
|
elm = makeMemberDiv(user, member);
|
|
if (!lastElm) {
|
|
membershtml.append(elm);
|
|
} else {
|
|
lastElm.after(elm);
|
|
}
|
|
} else if (lastElm) {
|
|
//@ts-expect-error TS Bug, let me know when it's fixed :3
|
|
// https://github.com/microsoft/TypeScript/issues/62872
|
|
const nextElm = lastElm.nextElementSibling;
|
|
if (nextElm !== elm) {
|
|
lastElm.after(elm);
|
|
}
|
|
} else {
|
|
const first = membershtml.firstElementChild;
|
|
if (first !== elm) {
|
|
membershtml.prepend(elm);
|
|
}
|
|
}
|
|
|
|
lastElm = elm;
|
|
}
|
|
}
|
|
}
|
|
emojiPicker(x: number, y: number, guildEmojis = true) {
|
|
return Emoji.emojiPicker(x, y, guildEmojis ? this : undefined);
|
|
}
|
|
async getSidePannel() {
|
|
if (this.ws && this.focusChannel) {
|
|
console.log(this.focusChannel.guild.id);
|
|
this.memberListQue();
|
|
if (this.focusChannel.guild.id === "@me") {
|
|
return;
|
|
}
|
|
if (!this.focusChannel.visible) return;
|
|
this.ws.send(
|
|
JSON.stringify({
|
|
d: {
|
|
channels: {[this.focusChannel.id]: [[0, 99]]},
|
|
guild_id: this.focusChannel.guild.id,
|
|
},
|
|
op: 14,
|
|
}),
|
|
);
|
|
} else {
|
|
console.log("false? :3");
|
|
}
|
|
}
|
|
async goToState(state: [string, string | undefined, string | undefined]) {
|
|
const [guildid, channelid, messageid] = state;
|
|
if (!channelid) {
|
|
if (guildid === "@me") {
|
|
const dir = this.guilds.get("@me") as Direct;
|
|
dir.loadChannel(null, false);
|
|
}
|
|
return;
|
|
}
|
|
this.goToChannel(channelid, false, messageid);
|
|
}
|
|
|
|
gotoid: string | undefined;
|
|
gotoRes = () => {};
|
|
async goToChannel(channelid: string, addstate = true, messageid: undefined | string = undefined) {
|
|
const channel = this.channels.get(channelid);
|
|
if (channel?.type === 4) return;
|
|
if (channel) {
|
|
const guild = channel.guild;
|
|
guild.loadGuild();
|
|
await guild.loadChannel(channelid, addstate, messageid);
|
|
} else {
|
|
this.gotoid = channelid;
|
|
return new Promise<void>((res) => (this.gotoRes = res));
|
|
}
|
|
}
|
|
delChannel(json: channeljson): void {
|
|
let guild_id = json.guild_id;
|
|
guild_id ??= "@me";
|
|
const guild = this.guilds.get(guild_id);
|
|
if (guild) {
|
|
guild.delChannel(json);
|
|
}
|
|
|
|
if (json.guild_id === this.focusGuild?.id) {
|
|
this.loadGuild(json.guild_id, true);
|
|
}
|
|
}
|
|
async init() {
|
|
const location = window.location.href.split("/");
|
|
this.buildservers();
|
|
this.generateFavicon();
|
|
if (location[3] === "channels") {
|
|
const guild = this.loadGuild(location[4]);
|
|
if (!guild) {
|
|
return;
|
|
}
|
|
await guild.loadChannel(location[5], true, location[6]);
|
|
this.focusChannel = this.channels.get(location[5]);
|
|
}
|
|
}
|
|
loaduser(): void {
|
|
(document.getElementById("username") as HTMLSpanElement).textContent = this.user.username;
|
|
(document.getElementById("userpfp") as HTMLImageElement).src = this.user.getpfpsrc();
|
|
(document.getElementById("status") as HTMLSpanElement).textContent = this.status;
|
|
}
|
|
isAdmin(): boolean {
|
|
if (this.focusGuild) {
|
|
return this.focusGuild.member.isAdmin();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
loadGuild(id: string, forceReload = false): Guild | undefined {
|
|
this.searching = false;
|
|
let guild = this.guilds.get(id);
|
|
if (!guild) {
|
|
guild = this.guilds.get("@me");
|
|
}
|
|
console.log(forceReload);
|
|
if (!forceReload && this.focusGuild === guild) {
|
|
return guild;
|
|
}
|
|
if (this.focusChannel && this.focusGuild !== guild) {
|
|
this.focusChannel.infinite.delete();
|
|
this.focusChannel = undefined;
|
|
}
|
|
if (this.focusGuild) {
|
|
this.focusGuild.html.classList.remove("serveropen");
|
|
}
|
|
|
|
if (!guild) return;
|
|
if (guild.html) {
|
|
guild.html.classList.add("serveropen");
|
|
}
|
|
this.focusGuild = guild;
|
|
(document.getElementById("serverName") as HTMLElement).textContent = guild.properties.name;
|
|
const banner = document.getElementById("servertd");
|
|
console.log(guild.banner, banner);
|
|
if (banner) {
|
|
if (guild.banner) {
|
|
//https://cdn.discordapp.com/banners/677271830838640680/fab8570de5bb51365ba8f36d7d3627ae.webp?size=240
|
|
banner.style.setProperty(
|
|
"background-image",
|
|
`linear-gradient(rgba(80, 80, 80, 1) 0%, rgba(80, 80, 80, 0) 40%), url(${this.info.cdn}/banners/${guild.id}/${guild.banner + new CDNParams({expectedSize: 128})})`,
|
|
);
|
|
banner.classList.add("Banner");
|
|
//background-image:
|
|
} else {
|
|
banner.style.removeProperty("background-image");
|
|
banner.classList.remove("Banner");
|
|
}
|
|
if (guild.id !== "@me") {
|
|
banner.style.setProperty("cursor", `pointer`);
|
|
banner.onclick = (e) => {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
const box = banner.getBoundingClientRect();
|
|
Guild.contextmenu.makemenu(box.left + 16, box.bottom + 5, guild, undefined);
|
|
};
|
|
} else {
|
|
banner.style.removeProperty("cursor");
|
|
banner.onclick = () => {};
|
|
}
|
|
}
|
|
//console.log(this.guildids,id)
|
|
const channels = document.getElementById("channels") as HTMLDivElement;
|
|
channels.innerHTML = "";
|
|
const html = guild.getHTML();
|
|
channels.appendChild(html);
|
|
return guild;
|
|
}
|
|
readonly dragMap = new WeakMap<
|
|
HTMLElement,
|
|
| Guild
|
|
| {
|
|
guilds: Guild[];
|
|
color?: number | null;
|
|
name: string;
|
|
id: number;
|
|
}
|
|
>();
|
|
dragged?: HTMLElement;
|
|
makeGuildDragable(
|
|
elm: HTMLElement,
|
|
thing:
|
|
| Guild
|
|
| {
|
|
guilds: Guild[];
|
|
color?: number | null;
|
|
name: string;
|
|
id: number;
|
|
},
|
|
) {
|
|
this.dragMap.set(elm, thing);
|
|
elm.addEventListener("dragstart", (e) => {
|
|
this.dragged = elm;
|
|
e.stopImmediatePropagation();
|
|
});
|
|
elm.addEventListener("dragend", () => {
|
|
delete this.dragged;
|
|
});
|
|
|
|
elm.style.position = "relative";
|
|
elm.draggable = true;
|
|
elm.addEventListener("dragenter", (event) => {
|
|
console.log("enter");
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
});
|
|
const guildWithin = (guild?: Guild) => {
|
|
return !this.guildOrder.find((_) => _ === guild);
|
|
};
|
|
elm.addEventListener("dragover", (event) => {
|
|
event.stopImmediatePropagation();
|
|
const thingy = this.dragMap.get(this.dragged as HTMLElement);
|
|
if (!thingy) return;
|
|
if (this.dragged === elm) return;
|
|
if (!(thingy instanceof Guild) && guildWithin(thing as Guild)) return;
|
|
const height = elm.getBoundingClientRect().height;
|
|
if (event.offsetY < 0.3 * 48) {
|
|
elm.classList.add("dragTopView");
|
|
elm.classList.remove("dragFolderView");
|
|
elm.classList.remove("dragBottomView");
|
|
} else if (height - event.offsetY < 0.3 * 48) {
|
|
elm.classList.remove("dragTopView");
|
|
elm.classList.remove("dragFolderView");
|
|
elm.classList.add("dragBottomView");
|
|
} else {
|
|
elm.classList.remove("dragTopView");
|
|
elm.classList.remove("dragBottomView");
|
|
if (thingy instanceof Guild && (!(thing instanceof Guild) || !guildWithin(thing))) {
|
|
elm.classList.add("dragFolderView");
|
|
}
|
|
}
|
|
event.preventDefault();
|
|
});
|
|
elm.addEventListener("dragleave", () => {
|
|
elm.classList.remove("dragFolderView");
|
|
elm.classList.remove("dragTopView");
|
|
elm.classList.remove("dragBottomView");
|
|
});
|
|
elm.addEventListener("drop", async (event) => {
|
|
event.stopImmediatePropagation();
|
|
if (!this.dragged) return;
|
|
const drag = this.dragged;
|
|
const thingy = this.dragMap.get(this.dragged);
|
|
if (!thingy) return;
|
|
elm.classList.remove("dragFolderView");
|
|
elm.classList.remove("dragTopView");
|
|
elm.classList.remove("dragBottomView");
|
|
|
|
if (!(thingy instanceof Guild) && guildWithin(thing as Guild)) return;
|
|
if (this.dragged === elm) return;
|
|
|
|
const height = elm.getBoundingClientRect().height;
|
|
let arr = this.guildOrder;
|
|
console.warn(arr === this.guildOrder);
|
|
let found = arr.find((elm) => {
|
|
if (elm === thing) {
|
|
return true;
|
|
}
|
|
if (!(elm instanceof Guild) && elm.guilds.find((_) => _ === thing)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
if (found && "guilds" in found && found !== thing) arr = found.guilds;
|
|
console.warn(arr === this.guildOrder);
|
|
const removeThingy = (thing = thingy) => {
|
|
const index = this.guildOrder.indexOf(thing);
|
|
if (-1 !== index) this.guildOrder.splice(index, 1);
|
|
this.guildOrder.forEach((_) => {
|
|
if (!(_ instanceof Guild)) {
|
|
const index = _.guilds.indexOf(thing as Guild);
|
|
if (-1 !== index) _.guilds.splice(index, 1);
|
|
}
|
|
});
|
|
};
|
|
console.log(arr, found);
|
|
if (event.offsetY < 0.3 * 48) {
|
|
removeThingy();
|
|
elm.before(this.dragged);
|
|
const index = arr.indexOf(thing);
|
|
arr.splice(index, 0, thingy);
|
|
} else if (height - event.offsetY < 0.3 * 48) {
|
|
removeThingy();
|
|
elm.after(this.dragged);
|
|
arr.splice(arr.indexOf(thing) + 1, 0, thingy);
|
|
} else if (thingy instanceof Guild && (!(thing instanceof Guild) || !guildWithin(thing))) {
|
|
if (thing instanceof Guild) {
|
|
await new Promise<void>((res) => {
|
|
const dia = new Dialog(I18n.folder.create());
|
|
const opt = dia.options;
|
|
const color = opt.addColorInput(I18n.folder.color(), () => {});
|
|
const name = opt.addTextInput(I18n.folder.name(), () => {});
|
|
opt.addButtonInput("", I18n.submit(), () => {
|
|
removeThingy();
|
|
|
|
let id = 1;
|
|
while (this.guildOrder.find((_) => _.id === id)) {
|
|
id++;
|
|
}
|
|
const folder = {
|
|
color: +("0x" + (color.value || "#0").split("#")[1]),
|
|
name: name.value,
|
|
guilds: [thing, thingy],
|
|
id,
|
|
};
|
|
|
|
this.guildOrder.splice(this.guildOrder.indexOf(thing), 1, folder);
|
|
const hold = document.createElement("hr");
|
|
elm.after(hold);
|
|
hold.after(
|
|
this.makeFolder(
|
|
folder,
|
|
new Map([
|
|
[thing, elm],
|
|
[thingy, drag],
|
|
]),
|
|
),
|
|
);
|
|
hold.remove();
|
|
|
|
dia.hide();
|
|
res();
|
|
});
|
|
dia.show();
|
|
});
|
|
} else {
|
|
removeThingy();
|
|
thing.guilds.push(thingy);
|
|
elm.append(drag);
|
|
}
|
|
}
|
|
console.log(this.guildOrder);
|
|
this.guildOrder = this.guildOrder.filter((folder) => {
|
|
if (folder instanceof Guild) return true;
|
|
if (folder.guilds.length === 0) {
|
|
const servers = document.getElementById("guildRail");
|
|
if (servers)
|
|
Array.from(servers.children).forEach((_) => {
|
|
const html = _ as HTMLElement;
|
|
if (this.dragMap.get(html) === folder) html.remove();
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
await this.saveGuildOrder();
|
|
});
|
|
}
|
|
async saveGuildOrder() {
|
|
const guild_folders: guildFolder[] = this.guildOrder.map((elm) => {
|
|
if (elm instanceof Guild) {
|
|
return {
|
|
id: null,
|
|
name: null,
|
|
guild_ids: [elm.id],
|
|
color: null,
|
|
};
|
|
} else {
|
|
return {
|
|
id: elm.id,
|
|
name: elm.name,
|
|
guild_ids: elm.guilds.map((guild) => guild.id),
|
|
color: elm.color,
|
|
};
|
|
}
|
|
});
|
|
|
|
await fetch(this.info.api + "/users/@me/settings", {
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
body: JSON.stringify({guild_folders}),
|
|
});
|
|
}
|
|
makeGuildIcon(guild: Guild) {
|
|
const divy = guild.generateGuildIcon();
|
|
guild.HTMLicon = divy;
|
|
this.makeGuildDragable(divy, guild);
|
|
return divy;
|
|
}
|
|
makeFolder(
|
|
folder: {color?: number | null; id: number; name: string; guilds: Guild[]},
|
|
icons = new Map<Guild, HTMLElement | undefined>(),
|
|
) {
|
|
const folderDiv = document.createElement("div");
|
|
folderDiv.classList.add("folder-div");
|
|
const iconDiv = document.createElement("div");
|
|
iconDiv.classList.add("folder-icon-div");
|
|
const icon = document.createElement("span");
|
|
icon.classList.add("svg-folder");
|
|
|
|
const menu = new Contextmenu<void, void>("");
|
|
menu.addButton(I18n.folder.edit(), () => {
|
|
const dio = new Dialog(I18n.folder.edit());
|
|
const opt = dio.options;
|
|
const name = opt.addTextInput(I18n.folder.name(), () => {}, {
|
|
initText: folder.name,
|
|
});
|
|
const color = opt.addColorInput(I18n.folder.color(), () => {}, {
|
|
initColor: "#" + (folder.color || 0).toString(16),
|
|
});
|
|
opt.addButtonInput("", I18n.submit(), async () => {
|
|
folder.name = name.value;
|
|
folder.color = +("0x" + (color.value || "#0").split("#")[1]);
|
|
icon.style.setProperty("--folder-color", "#" + folder.color.toString(16).padStart(6, "0"));
|
|
if (!folder.color && folder.color !== 0) icon.style.removeProperty("--folder-color");
|
|
await this.saveGuildOrder();
|
|
dio.hide();
|
|
});
|
|
dio.show();
|
|
});
|
|
|
|
menu.bindContextmenu(iconDiv);
|
|
if (folder.color !== null && folder.color !== undefined) {
|
|
icon.style.setProperty("--folder-color", "#" + folder.color.toString(16).padStart(6, "0"));
|
|
if (!folder.color && folder.color !== 0) icon.style.removeProperty("--folder-color");
|
|
}
|
|
iconDiv.append(icon);
|
|
const divy = document.createElement("div");
|
|
divy.append(
|
|
...folder.guilds.map((guild) => {
|
|
const icon = icons.get(guild);
|
|
if (icon) return icon;
|
|
return this.makeGuildIcon(guild);
|
|
}),
|
|
);
|
|
divy.classList.add("guilds-div-folder");
|
|
folderDiv.append(iconDiv, divy);
|
|
let height = -1;
|
|
const toggle = async (fast = false) => {
|
|
if (height === -1) {
|
|
divy.style.overflow = "clip";
|
|
height = divy.getBoundingClientRect().height;
|
|
divy.style.height = height + "px";
|
|
await new Promise((res) => requestAnimationFrame(res));
|
|
divy.style.height = "0px";
|
|
this.perminfo.folderStates[folder.id].state = true;
|
|
} else {
|
|
divy.style.height = height + "px";
|
|
if (!fast) await new Promise((res) => setTimeout(res, 200));
|
|
divy.style.height = "unset";
|
|
height = -1;
|
|
divy.style.overflow = "unset";
|
|
this.perminfo.folderStates[folder.id].state = false;
|
|
}
|
|
};
|
|
iconDiv.onclick = () => toggle();
|
|
this.perminfo.folderStates ??= {};
|
|
this.perminfo.folderStates[folder.id] ??= {};
|
|
if (this.perminfo.folderStates[folder.id].state === true) {
|
|
toggle(true);
|
|
}
|
|
this.makeGuildDragable(folderDiv, folder);
|
|
return folderDiv;
|
|
}
|
|
guildOrder: (
|
|
| Guild
|
|
| {
|
|
guilds: Guild[];
|
|
color?: number | null;
|
|
name: string;
|
|
id: number;
|
|
}
|
|
)[] = [];
|
|
buildservers(): void {
|
|
const serverlist = document.getElementById("guildRail") as HTMLDivElement; //
|
|
const outdiv = document.createElement("div");
|
|
const home: any = document.createElement("span");
|
|
const div = document.createElement("div");
|
|
div.classList.add("home", "servericon");
|
|
|
|
home.classList.add("svgicon", "svg-home");
|
|
(this.guilds.get("@me") as Guild).html = outdiv;
|
|
const unread = document.createElement("div");
|
|
unread.classList.add("unread");
|
|
outdiv.append(unread);
|
|
outdiv.append(div);
|
|
div.appendChild(home);
|
|
|
|
outdiv.classList.add("servernoti");
|
|
serverlist.append(outdiv);
|
|
home.onclick = () => {
|
|
const guild = this.guilds.get("@me");
|
|
if (!guild) return;
|
|
guild.loadGuild();
|
|
guild.loadChannel();
|
|
};
|
|
const sentdms = document.createElement("div");
|
|
sentdms.classList.add("sentdms");
|
|
serverlist.append(sentdms);
|
|
sentdms.id = "sentdms";
|
|
|
|
const br = document.createElement("hr");
|
|
br.classList.add("lightbr");
|
|
serverlist.appendChild(br);
|
|
const guilds = new Set(this.guilds.values());
|
|
const dirrect = this.guilds.get("@me") as Direct;
|
|
|
|
guilds.delete(dirrect);
|
|
const folders = this.guildFolders
|
|
.map((folder) => {
|
|
return {
|
|
guilds: folder.guild_ids
|
|
.map((id) => {
|
|
const guild = this.guilds.get(id);
|
|
if (!guild) {
|
|
console.error(`guild ${id} does not exist`);
|
|
return;
|
|
}
|
|
if (!guilds.has(guild)) {
|
|
console.error(`guild ${id} is already in a folder`);
|
|
return;
|
|
}
|
|
guilds.delete(guild);
|
|
return guild;
|
|
})
|
|
.filter((_) => _ !== undefined),
|
|
color: folder.color,
|
|
name: folder.name || "",
|
|
id: folder.id || 0,
|
|
};
|
|
})
|
|
.filter((_) => {
|
|
if (_.guilds.length === 0) {
|
|
console.error("empty folder depected");
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.map((folder) => {
|
|
if (!folder.id && folder.guilds.length === 1) {
|
|
return folder.guilds[0];
|
|
}
|
|
return folder;
|
|
});
|
|
const guildOrder = [...guilds, ...folders];
|
|
this.guildOrder = guildOrder;
|
|
for (const thing of guildOrder) {
|
|
if (thing instanceof Guild) {
|
|
serverlist.append(this.makeGuildIcon(thing));
|
|
} else {
|
|
const folderDiv = this.makeFolder(thing);
|
|
serverlist.append(folderDiv);
|
|
}
|
|
}
|
|
|
|
{
|
|
const br = document.createElement("hr");
|
|
br.classList.add("lightbr");
|
|
serverlist.appendChild(br);
|
|
br.id = "bottomseparator";
|
|
|
|
const div = document.createElement("div");
|
|
const plus = document.createElement("span");
|
|
plus.classList.add("svgicon", "svg-plus");
|
|
div.classList.add("home", "servericon");
|
|
div.appendChild(plus);
|
|
serverlist.appendChild(div);
|
|
div.onclick = (_) => {
|
|
this.createGuild();
|
|
};
|
|
const guilddsdiv = document.createElement("div");
|
|
const guildDiscoveryContainer = document.createElement("span");
|
|
guildDiscoveryContainer.classList.add("svgicon", "svg-explore");
|
|
guilddsdiv.classList.add("home", "servericon");
|
|
guilddsdiv.appendChild(guildDiscoveryContainer);
|
|
serverlist.appendChild(guilddsdiv);
|
|
guildDiscoveryContainer.addEventListener("click", () => {
|
|
this.guildDiscovery();
|
|
});
|
|
}
|
|
this.unreads();
|
|
dirrect.unreaddms();
|
|
}
|
|
passTemplateID(id: string) {
|
|
this.createGuild(id);
|
|
}
|
|
createGuild(templateID?: string) {
|
|
const full = new Dialog("");
|
|
const buttons = full.options.addButtons("", {top: true});
|
|
const viacode = buttons.add(I18n.invite.joinUsing());
|
|
{
|
|
const form = viacode.addForm("", async (e: any) => {
|
|
let parsed = "";
|
|
if (e.code.includes("/")) {
|
|
parsed = e.code.split("/")[e.code.split("/").length - 1];
|
|
} else {
|
|
parsed = e.code;
|
|
}
|
|
const json = await (
|
|
await fetch(this.info.api + "/invites/" + parsed, {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
})
|
|
).json();
|
|
if (json.message) {
|
|
throw new FormError(text, json.message);
|
|
}
|
|
full.hide();
|
|
});
|
|
const text = form.addTextInput(I18n.invite.inviteLinkCode(), "code");
|
|
}
|
|
const guildcreate = buttons.add(I18n.guild.create());
|
|
{
|
|
const form = guildcreate.addForm("", (fields: any) => {
|
|
this.makeGuild(fields).then((_) => {
|
|
if (_.message) {
|
|
loading.hide();
|
|
full.show();
|
|
alert(_.errors.name._errors[0].message);
|
|
} else {
|
|
loading.hide();
|
|
full.hide();
|
|
}
|
|
});
|
|
});
|
|
form.addImageInput(I18n.guild["icon:"](), "icon", {
|
|
clear: true,
|
|
});
|
|
form.addTextInput(I18n.guild["name:"](), "name", {required: true});
|
|
const loading = new Dialog("");
|
|
loading.float.options.addTitle(I18n.guild.creating());
|
|
form.onFormError = () => {
|
|
loading.hide();
|
|
full.show();
|
|
};
|
|
form.addPreprocessor(() => {
|
|
loading.show();
|
|
full.hide();
|
|
});
|
|
}
|
|
const guildcreateFromTemplate = buttons.add(I18n.guild.createFromTemplate());
|
|
{
|
|
const form = guildcreateFromTemplate.addForm(
|
|
"",
|
|
(_: any) => {
|
|
if (_.message) {
|
|
loading.hide();
|
|
full.show();
|
|
alert(_.message);
|
|
const htmlarea = buttons.htmlarea.deref();
|
|
if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea);
|
|
} else {
|
|
loading.hide();
|
|
full.hide();
|
|
}
|
|
},
|
|
{
|
|
method: "POST",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
const template = form.addTextInput(I18n.guild.template(), "template", {
|
|
initText: templateID || "",
|
|
});
|
|
form.addImageInput(I18n.guild["icon:"](), "icon", {files: "one", clear: true});
|
|
form.addTextInput(I18n.guild["name:"](), "name", {required: true});
|
|
|
|
const loading = new Dialog("");
|
|
loading.float.options.addTitle(I18n.guild.creating());
|
|
form.onFormError = () => {
|
|
loading.hide();
|
|
full.show();
|
|
};
|
|
form.addPreprocessor((e) => {
|
|
loading.show();
|
|
full.hide();
|
|
if ("template" in e) delete e.template;
|
|
let code: string;
|
|
if (URL.canParse(template.value) && new URL(template.value).protocol.startsWith("http")) {
|
|
const url = new URL(template.value);
|
|
code = url.pathname.split("/").at(-1) as string;
|
|
if (url.host === "discord.com" || url.host === "discord.new") {
|
|
code = "discord:" + code;
|
|
}
|
|
} else {
|
|
code = template.value;
|
|
}
|
|
form.fetchURL = this.info.api + "/guilds/templates/" + code;
|
|
});
|
|
}
|
|
full.show();
|
|
if (templateID) {
|
|
const htmlarea = buttons.htmlarea.deref();
|
|
if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea);
|
|
}
|
|
}
|
|
async makeGuild(fields: {name: string; icon: string | null}) {
|
|
return await (
|
|
await fetch(this.info.api + "/guilds", {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
body: JSON.stringify(fields),
|
|
})
|
|
).json();
|
|
}
|
|
async guildDiscovery() {
|
|
this.guilds.get("@me")?.loadChannel("discover");
|
|
}
|
|
messageCreate(messagep: messageCreateJson): void {
|
|
messagep.d.guild_id ??= "@me";
|
|
const channel = this.channels.get(messagep.d.channel_id);
|
|
if (channel) {
|
|
channel.messageCreate(messagep);
|
|
this.unreads();
|
|
}
|
|
}
|
|
unreads(): void {
|
|
for (const thing of this.guilds.values()) {
|
|
if (thing.id === "@me") {
|
|
thing.unreads();
|
|
continue;
|
|
}
|
|
const html = thing.html;
|
|
thing.unreads(html);
|
|
}
|
|
}
|
|
private static favC = document.createElement("canvas");
|
|
private static favCTX = this.favC.getContext("2d") as CanvasRenderingContext2D;
|
|
private static favImg = this.getFaviconImg();
|
|
static getFaviconImg() {
|
|
const img = document.createElement("img");
|
|
img.src = "/logo.webp";
|
|
return img;
|
|
}
|
|
private last = "-1";
|
|
private generateFavicon() {
|
|
const make = () => {
|
|
const favicon = document.getElementById("favicon") as HTMLLinkElement;
|
|
|
|
let text = this.totalMentions() + "";
|
|
if (this.last === text) return;
|
|
this.last = text;
|
|
if (text === "0") {
|
|
favicon.href = "/favicon.ico";
|
|
return;
|
|
}
|
|
if (+text > 99) text = "+99";
|
|
|
|
const c = Localuser.favC;
|
|
c.width = 256;
|
|
c.height = 256;
|
|
const ctx = Localuser.favCTX;
|
|
ctx.drawImage(Localuser.favImg, 0, 0, c.width, c.height);
|
|
ctx.fillStyle = "#F00";
|
|
const pos = 0.675;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(c.width * pos, c.height * pos, c.width * (1 - pos), 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = "#FFF";
|
|
|
|
ctx.font = `bolder ${text.length === 1 ? 150 : 100}px sans-serif`;
|
|
|
|
const messure = ctx.measureText(text);
|
|
const height = messure.fontBoundingBoxAscent + messure.fontBoundingBoxDescent;
|
|
ctx.fillText(text, c.width * pos - messure.width / 2, c.height * pos + height / 2 - 25);
|
|
|
|
favicon.href = c.toDataURL("image/x-icon");
|
|
};
|
|
if (Localuser.favImg.complete) {
|
|
make();
|
|
}
|
|
Localuser.favImg.onload = () => {
|
|
make();
|
|
};
|
|
}
|
|
totalMentions() {
|
|
let sum = 0;
|
|
for (const guild of this.guilds.values()) {
|
|
sum += guild.mentions;
|
|
}
|
|
for (const channel of (this.guilds.get("@me") as Direct).channels) {
|
|
sum += channel.mentions;
|
|
}
|
|
return sum;
|
|
}
|
|
async typingStart(typing: startTypingjson): Promise<void> {
|
|
const channel = this.channels.get(typing.d.channel_id);
|
|
if (!channel) return;
|
|
channel.typingStart(typing);
|
|
}
|
|
updatepfp(file: Blob | null): void {
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file);
|
|
reader.onload = () => {
|
|
fetch(this.info.api + "/users/@me", {
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
body: JSON.stringify({
|
|
avatar: reader.result,
|
|
}),
|
|
});
|
|
};
|
|
} else {
|
|
fetch(this.info.api + "/users/@me", {
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
body: JSON.stringify({
|
|
avatar: null,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
updatebanner(file: Blob | null): void {
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file);
|
|
reader.onload = () => {
|
|
fetch(this.info.api + "/users/@me", {
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
body: JSON.stringify({
|
|
banner: reader.result,
|
|
}),
|
|
});
|
|
};
|
|
} else {
|
|
fetch(this.info.api + "/users/@me", {
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
body: JSON.stringify({
|
|
banner: null,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
updateProfile(json: {bio?: string; pronouns?: string; accent_color?: number}) {
|
|
fetch(this.info.api + "/users/@me/profile", {
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
body: JSON.stringify(json),
|
|
});
|
|
}
|
|
async getPosts() {
|
|
const text = await (await fetch("https://blog.fermi.chat/feed_rss_created.xml")).text();
|
|
const xml = new DOMParser().parseFromString(text, "text/xml");
|
|
const posts = Array.from(xml.getElementsByTagName("channel")[0].getElementsByTagName("item"));
|
|
return {
|
|
items: posts.map((post) => {
|
|
return {
|
|
url: post.getElementsByTagName("link")[0].textContent,
|
|
title: post.getElementsByTagName("title")[0].textContent,
|
|
content_html: post.getElementsByTagName("description")[0].textContent,
|
|
image: post.getElementsByTagName("image")[0]?.textContent ?? null,
|
|
};
|
|
}),
|
|
} satisfies {
|
|
items: {
|
|
url: string;
|
|
title: string;
|
|
content_html: string;
|
|
image: null | string;
|
|
}[];
|
|
};
|
|
}
|
|
async getConnections() {
|
|
const json = await fetch(this.info.api + "/connections", {
|
|
headers: this.headers,
|
|
}).then((r) => r.json() as Promise<{[key: string]: {enabled: boolean; icon_url?: string}}>);
|
|
json["domain"] = {enabled: true, icon_url: "/icons/domain.svg"};
|
|
return json;
|
|
}
|
|
gifProvideors: {name: string; api_name: string}[] = [];
|
|
selectedGifProfidor?: {name: string; api_name: string};
|
|
async getGifProvidors() {
|
|
const res = (await (
|
|
await fetch(this.info.api + "/gifs", {
|
|
headers: this.headers,
|
|
})
|
|
).json()) as {name: string; api_name: string}[];
|
|
if (res[0] && !("name" in res[0])) {
|
|
if (res.length === 3) {
|
|
this.gifProvideors = [
|
|
{name: "Giphy", api_name: "giphy"},
|
|
{name: "Klipy", api_name: "klipy"},
|
|
];
|
|
}
|
|
} else {
|
|
this.gifProvideors = res.filter((_) => _.name !== "tenor");
|
|
}
|
|
this.figureDefaultProvidor();
|
|
}
|
|
async figureDefaultProvidor() {
|
|
const prefs = getPreferences();
|
|
this.selectedGifProfidor =
|
|
this.gifProvideors.find((_) => _.api_name == prefs.gifProvidor) || this.gifProvideors[0];
|
|
}
|
|
domainVerification() {
|
|
const d = new Dialog(I18n.domain.title(), {noSubmit: true});
|
|
const text = d.options.addTextInput(I18n.domain.domain(), () => {});
|
|
d.options.addButtonInput("", I18n.submit(), async () => {
|
|
const dom = text.value;
|
|
d.options.removeAll();
|
|
console.log(dom);
|
|
const res = await fetch(this.info.api + "/users/@me/connections/domain/" + dom, {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
});
|
|
if (res.ok) {
|
|
d.hide();
|
|
} else {
|
|
const json = (await res.json()) as {code?: number; message: string; proof: string};
|
|
if (json.code === 50187) {
|
|
const err = d.options.addErrorBox();
|
|
d.options.addMDText(new MarkDown(I18n.domain.dnsinst(dom, json.proof)));
|
|
|
|
d.options.addButtonInput("", I18n.submit(), async () => {
|
|
const dom = text.value;
|
|
const res = await fetch(this.info.api + "/users/@me/connections/domain/" + dom, {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
});
|
|
if (res.ok) {
|
|
d.hide();
|
|
} else {
|
|
err.showError((await res.json()).message);
|
|
}
|
|
});
|
|
} else {
|
|
d.options.addText(json.message);
|
|
}
|
|
}
|
|
});
|
|
d.show();
|
|
}
|
|
static initShortcuts() {
|
|
const cur = getShortcuts();
|
|
for (const [name, combo] of cur) {
|
|
this.globalShortcuts.registerKeycombo(combo, name);
|
|
}
|
|
this.globalShortcuts.registerShortcut("gifSearch", () => {
|
|
document.getElementById("gifTB")?.click();
|
|
});
|
|
}
|
|
async showusersettings() {
|
|
const prefs = getPreferences();
|
|
const localSettings = getLocalSettings();
|
|
const settings = new Settings(I18n.localuser.settings());
|
|
{
|
|
const donate = settings.addButton(I18n.donate.donate(), {initable: false});
|
|
donate.addMDText(new MarkDown(I18n.donate.mdText(window.location.origin + "/donate")));
|
|
donate.addText(I18n.donate.desc());
|
|
}
|
|
settings.addButton(I18n.localuser.general(), {
|
|
head: true,
|
|
});
|
|
{
|
|
const userOptions = settings.addButton(I18n.localuser.userSettings(), {
|
|
ltr: true,
|
|
contained: true,
|
|
});
|
|
const hypotheticalProfile = document.createElement("div");
|
|
let file: undefined | File | null;
|
|
let newpronouns: string | undefined;
|
|
let newbio: string | undefined;
|
|
const hypouser = this.user.clone();
|
|
let color: string;
|
|
async function regen() {
|
|
hypotheticalProfile.textContent = "";
|
|
const hypoprofile = await hypouser.buildprofile(-1, -1);
|
|
|
|
hypotheticalProfile.appendChild(hypoprofile);
|
|
}
|
|
regen();
|
|
const settingsLeft = userOptions.addOptions("");
|
|
const settingsRight = userOptions.addOptions("");
|
|
settingsRight.addHTMLArea(hypotheticalProfile);
|
|
|
|
const finput = settingsLeft.addImageInput(
|
|
I18n.uploadPfp(),
|
|
(_) => {
|
|
if (file !== undefined) {
|
|
this.updatepfp(file);
|
|
}
|
|
},
|
|
{clear: true, initImg: this.user.getpfpsrc()},
|
|
);
|
|
finput.img.classList.add("pfp");
|
|
finput.watchForChange((_) => {
|
|
if (!_) {
|
|
file = null;
|
|
hypouser.avatar = null;
|
|
hypouser.hypotheticalpfp = true;
|
|
regen();
|
|
return;
|
|
}
|
|
if (_.length) {
|
|
file = _[0];
|
|
const blob = URL.createObjectURL(file);
|
|
hypouser.avatar = blob;
|
|
hypouser.hypotheticalpfp = true;
|
|
regen();
|
|
}
|
|
});
|
|
let bfile: undefined | File | null;
|
|
const binput = settingsLeft.addImageInput(
|
|
I18n.uploadBanner(),
|
|
(_) => {
|
|
if (bfile !== undefined) {
|
|
this.updatebanner(bfile);
|
|
}
|
|
},
|
|
{
|
|
clear: true,
|
|
width: 96 * 3,
|
|
initImg: this.user.banner ? this.user.getBannerUrl() : "",
|
|
objectFit: "cover",
|
|
},
|
|
);
|
|
binput.watchForChange((_) => {
|
|
if (!_) {
|
|
bfile = null;
|
|
hypouser.banner = undefined;
|
|
hypouser.hypotheticalbanner = true;
|
|
regen();
|
|
return;
|
|
}
|
|
if (_.length) {
|
|
bfile = _[0];
|
|
const blob = URL.createObjectURL(bfile);
|
|
hypouser.banner = blob;
|
|
hypouser.hypotheticalbanner = true;
|
|
regen();
|
|
}
|
|
});
|
|
let changed = false;
|
|
const pronounbox = settingsLeft.addTextInput(
|
|
I18n.pronouns(),
|
|
(_) => {
|
|
if (newpronouns !== undefined || newbio !== undefined || changed !== undefined) {
|
|
this.updateProfile({
|
|
pronouns: newpronouns,
|
|
bio: newbio,
|
|
accent_color: Number.parseInt("0x" + color.substring(1), 16),
|
|
});
|
|
}
|
|
},
|
|
{initText: this.user.pronouns},
|
|
);
|
|
pronounbox.watchForChange((_) => {
|
|
hypouser.pronouns = _;
|
|
newpronouns = _;
|
|
regen();
|
|
});
|
|
const bioBox = settingsLeft.addMDInput(I18n.bio(), (_) => {}, {
|
|
initText: this.user.bio.rawString,
|
|
});
|
|
bioBox.watchForChange((_) => {
|
|
newbio = _;
|
|
hypouser.bio = new MarkDown(_, this);
|
|
regen();
|
|
});
|
|
|
|
if (this.user.accent_color) {
|
|
color = "#" + this.user.accent_color.toString(16);
|
|
} else {
|
|
color = "transparent";
|
|
}
|
|
const colorPicker = settingsLeft.addColorInput(I18n.profileColor(), (_) => {}, {
|
|
initColor: color,
|
|
});
|
|
|
|
colorPicker.watchForChange((_) => {
|
|
console.log();
|
|
color = _;
|
|
hypouser.accent_color = Number.parseInt("0x" + _.substring(1), 16);
|
|
changed = true;
|
|
regen();
|
|
});
|
|
settingsLeft.addButtonInput("", I18n.localuser.connections(), () => {
|
|
const connections = userOptions.addSubOptions(I18n.localuser.connections());
|
|
const connectionContainer = document.createElement("div");
|
|
const actConDivCont = document.createElement("div");
|
|
|
|
connectionContainer.classList.add("connection-container");
|
|
this.conectionChange = () => {
|
|
if (document.contains(settings.html)) {
|
|
remake();
|
|
} else {
|
|
this.conectionChange = () => {};
|
|
}
|
|
};
|
|
const remake = () => {
|
|
connectionContainer.innerHTML = "";
|
|
actConDivCont.innerHTML = "";
|
|
const cons = fetch(this.info.api + "/users/@me/connections", {
|
|
headers: this.headers,
|
|
});
|
|
|
|
this.getConnections().then(async (json) => {
|
|
const actCons = (await (await cons).json()) as ConnectionJson[];
|
|
const actConMap = new Map<string, ConnectionJson>(
|
|
actCons.map((_) => [_.type, _] as const),
|
|
);
|
|
const serverConnections = Object.keys(json).sort((key) => (json[key].enabled ? -1 : 1));
|
|
|
|
serverConnections
|
|
.filter((_) => !actConMap.has(_))
|
|
.forEach((key) => {
|
|
if (key === "domain") return;
|
|
const connection = json[key];
|
|
|
|
const container = document.createElement("div");
|
|
if (connection.icon_url) {
|
|
const span = document.createElement("span");
|
|
span.classList.add("conImg", "svgicon");
|
|
span.style.setProperty("mask", `url("${connection.icon_url}")`);
|
|
//span.alt = key;
|
|
container.append(span);
|
|
} else {
|
|
container.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
|
}
|
|
|
|
if (connection.enabled) {
|
|
container.addEventListener("click", async () => {
|
|
const connectionRes = await fetch(
|
|
this.info.api + "/connections/" + key + "/authorize",
|
|
{
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
const connectionJSON = await connectionRes.json();
|
|
window.open(connectionJSON.url, "_blank", "noopener noreferrer");
|
|
});
|
|
} else {
|
|
container.classList.add("disabled");
|
|
}
|
|
|
|
connectionContainer.appendChild(container);
|
|
});
|
|
|
|
const container = document.createElement("div");
|
|
|
|
const span = document.createElement("span");
|
|
span.classList.add("conImg", "svgicon");
|
|
span.style.setProperty("mask", `url("/icons/domain.svg")`);
|
|
container.append(span);
|
|
|
|
container.addEventListener("click", async () => {
|
|
this.domainVerification();
|
|
});
|
|
|
|
connectionContainer.appendChild(container);
|
|
|
|
serverConnections
|
|
.filter((_) => actConMap.has(_))
|
|
.forEach((_) => {
|
|
const con = actConMap.get(_);
|
|
if (!con) return;
|
|
const connectionObj = json[_];
|
|
|
|
const actConDiv = document.createElement("div");
|
|
actConDiv.classList.add("flexttb", "actConnectionDiv");
|
|
const topRow = document.createElement("div");
|
|
actConDiv.append(topRow);
|
|
topRow.classList.add("flexltr");
|
|
if (connectionObj.icon_url) {
|
|
const span = document.createElement("span");
|
|
span.classList.add("conImg", "svgicon");
|
|
span.style.setProperty("mask", `url("${connectionObj.icon_url}")`);
|
|
//span.alt = key;
|
|
topRow.append(span);
|
|
}
|
|
|
|
const nameDiv = document.createElement("div");
|
|
nameDiv.classList.add("flexttb");
|
|
|
|
const name = document.createElement("span");
|
|
name.textContent = con.name;
|
|
|
|
const serviceName = document.createElement("span");
|
|
serviceName.textContent = _;
|
|
|
|
nameDiv.append(name, serviceName);
|
|
|
|
topRow.append(nameDiv);
|
|
|
|
const input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.checked = !!con.visibility;
|
|
input.onchange = () => {
|
|
fetch(this.info.api + "/users/@me/connections/" + con.type + "/" + con.id, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({
|
|
visibility: input.checked,
|
|
}),
|
|
headers: this.headers,
|
|
});
|
|
};
|
|
|
|
const dispRow = document.createElement("div");
|
|
dispRow.classList.add("flexltr");
|
|
actConDiv.append(dispRow);
|
|
|
|
const dispText = document.createElement("span");
|
|
dispText.textContent = I18n.connections.display();
|
|
dispRow.append(dispText, input);
|
|
|
|
const remove = document.createElement("button");
|
|
remove.textContent = I18n.connections.delete();
|
|
actConDiv.append(remove);
|
|
remove.onclick = () => {
|
|
const d = new Dialog(I18n.connections.sure());
|
|
d.options.addText(I18n.connections.sureDesc());
|
|
const row = d.options.addOptions("", {ltr: true});
|
|
row.addButtonInput("", I18n.yes(), async () => {
|
|
await fetch(
|
|
this.info.api + "/users/@me/connections/" + con.type + "/" + con.id,
|
|
{
|
|
method: "DELETE",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
d.hide();
|
|
});
|
|
row.addButtonInput("", I18n.no(), () => {
|
|
d.hide();
|
|
});
|
|
d.show();
|
|
};
|
|
|
|
actConDivCont.append(actConDiv);
|
|
});
|
|
});
|
|
};
|
|
remake();
|
|
connections.addHTMLArea(connectionContainer);
|
|
connections.addHR();
|
|
connections.addHTMLArea(actConDivCont);
|
|
});
|
|
}
|
|
{
|
|
const security = settings.addButton(I18n.localuser.accountSettings(), {contained: true});
|
|
|
|
security.addSubButtonInput(I18n.localuser.changeDiscriminator(), (sub) => {
|
|
const form = sub.addForm(
|
|
"",
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser.newDiscriminator(), "discriminator");
|
|
});
|
|
security.addSubButtonInput(I18n.localuser.changeEmail(), (sub) => {
|
|
const form = sub.addForm(
|
|
"",
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser["password:"](), "password", {
|
|
password: true,
|
|
});
|
|
if (this.mfa_enabled) {
|
|
form.addTextInput(I18n.localuser["2faCode:"](), "code");
|
|
}
|
|
form.addTextInput(I18n.localuser["newEmail:"](), "email");
|
|
});
|
|
security.addSubButtonInput(I18n.localuser.changeUsername(), (sub) => {
|
|
const form = sub.addForm(
|
|
"",
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser["password:"](), "password", {
|
|
password: true,
|
|
});
|
|
if (this.mfa_enabled) {
|
|
form.addTextInput(I18n.localuser["2faCode:"](), "code");
|
|
}
|
|
form.addTextInput(I18n.localuser.newUsername(), "username");
|
|
});
|
|
security.addSubButtonInput(I18n.localuser.changePassword(), (sub) => {
|
|
const form = sub.addForm(
|
|
"",
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser["oldPassword:"](), "password", {
|
|
password: true,
|
|
});
|
|
if (this.mfa_enabled) {
|
|
form.addTextInput(I18n.localuser["2faCode:"](), "code");
|
|
}
|
|
let in1 = "";
|
|
let in2 = "";
|
|
form
|
|
.addTextInput(I18n.localuser["newPassword:"](), "", {password: true})
|
|
.watchForChange((text) => {
|
|
in1 = text;
|
|
});
|
|
const copy = form.addTextInput("New password again:", "", {password: true});
|
|
copy.watchForChange((text) => {
|
|
in2 = text;
|
|
});
|
|
form.setValue("new_password", () => {
|
|
if (in1 === in2) {
|
|
return in1;
|
|
} else {
|
|
throw new FormError(copy, I18n.localuser.PasswordsNoMatch());
|
|
}
|
|
});
|
|
});
|
|
|
|
{
|
|
security.addButtonInput("", I18n.logout.logout(), async () => {
|
|
if (await this.userinfo.logout()) window.location.href = "/";
|
|
});
|
|
}
|
|
}
|
|
{
|
|
const lang = settings.addButton(I18n.localuser.language(), {contained: true});
|
|
const div = document.createElement("div");
|
|
div.classList.add("flexttb");
|
|
const langBuMap = new Map<string, HTMLButtonElement>();
|
|
let l = [...langmap];
|
|
const langF = l.find((_) => _[0] === I18n.lang + ".json");
|
|
l = l.filter((_) => _ !== langF);
|
|
if (langF) l.unshift(langF);
|
|
div.append(
|
|
...l.map(([key, name]) => {
|
|
const button = document.createElement("button");
|
|
button.classList.add("langButton");
|
|
button.textContent = name;
|
|
langBuMap.set(key, button);
|
|
if (key === I18n.lang + ".json") {
|
|
button.classList.add("selected");
|
|
}
|
|
button.onclick = () => {
|
|
const b = langBuMap.get(I18n.lang + ".json");
|
|
if (b) b.classList.remove("selected");
|
|
button.classList.add("selected");
|
|
I18n.setLanguage(key.replace(".json", ""));
|
|
this.updateTranslations();
|
|
};
|
|
console.log(I18n.lang, key);
|
|
return button;
|
|
}),
|
|
);
|
|
lang.addHTMLArea(div);
|
|
}
|
|
|
|
{
|
|
const prefs = getPreferences();
|
|
const tas = settings.addButton(I18n.localuser.themesAndSounds(), {contained: true});
|
|
{
|
|
const themes = ["Dark", "WHITE", "Light", "Dark-Accent"];
|
|
tas.addSelect(
|
|
I18n.localuser["theme:"](),
|
|
async (_) => {
|
|
prefs.theme = themes[_] as ThemeOption;
|
|
|
|
await setTheme(prefs.theme);
|
|
},
|
|
themes,
|
|
{
|
|
defaultIndex: themes.indexOf(prefs.theme),
|
|
},
|
|
);
|
|
}
|
|
{
|
|
const initArea = (index: number) => {
|
|
if (index === sounds.length - 1) {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "audio/*";
|
|
input.addEventListener("change", () => {
|
|
if (input.files?.length === 1) {
|
|
const file = input.files[0];
|
|
|
|
let reader = new FileReader();
|
|
reader.onload = () => {
|
|
let dataUrl = reader.result;
|
|
if (typeof dataUrl !== "string") return;
|
|
this.perminfo.sound = {};
|
|
try {
|
|
this.perminfo.sound.cSound = dataUrl;
|
|
console.log(this.perminfo.sound.cSound);
|
|
this.playSound("custom");
|
|
} catch (_) {
|
|
alert(I18n.localuser.soundTooLarge());
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
area.append(input);
|
|
} else {
|
|
area.innerHTML = "";
|
|
}
|
|
};
|
|
const sounds = [...(this.play?.tracks || []), I18n.localuser.customSound()];
|
|
const initIndex = sounds.indexOf(this.getNotificationSound());
|
|
const select = tas.addSelect(
|
|
I18n.localuser.notisound(),
|
|
(index) => {
|
|
this.setNotificationSound(sounds[index]);
|
|
},
|
|
sounds,
|
|
{defaultIndex: initIndex},
|
|
);
|
|
select.watchForChange((index) => {
|
|
initArea(index);
|
|
this.playSound(sounds[index]);
|
|
});
|
|
const input = document.createElement("input");
|
|
input.type = "range";
|
|
input.value = this.getNotiVolume() + "";
|
|
input.min = "0";
|
|
input.max = "100";
|
|
input.onchange = () => {
|
|
this.setNotificationVolume(+input.value);
|
|
this.playSound(sounds[select.index]);
|
|
};
|
|
|
|
const area = document.createElement("div");
|
|
initArea(initIndex);
|
|
tas.addHTMLArea(area);
|
|
tas.addText(I18n.notiVolume());
|
|
tas.addHTMLArea(input);
|
|
}
|
|
|
|
{
|
|
tas.addColorInput(
|
|
I18n.localuser.accentColor(),
|
|
async (_) => {
|
|
prefs.accentColor = _;
|
|
await setPreferences(prefs);
|
|
|
|
document.documentElement.style.setProperty("--accent-color", prefs.accentColor);
|
|
},
|
|
{initColor: prefs.accentColor},
|
|
);
|
|
}
|
|
{
|
|
const options = [[null, I18n.noEmojiFont()], ...Localuser.fonts] as const;
|
|
const cur = prefs.emojiFont;
|
|
let index = options.findIndex((_) => _[1] == cur);
|
|
if (index === -1) index = 0;
|
|
tas.addSelect(
|
|
I18n.emojiSelect(),
|
|
async (index) => {
|
|
if (options[index][0]) {
|
|
prefs.emojiFont = options[index][1];
|
|
} else {
|
|
prefs.emojiFont = undefined;
|
|
}
|
|
|
|
await setPreferences(prefs);
|
|
Localuser.loadFont();
|
|
},
|
|
options.map((font) => font[1]),
|
|
{
|
|
defaultIndex: index,
|
|
},
|
|
);
|
|
}
|
|
{
|
|
const ind = this.gifProvideors.findIndex((_) => this.selectedGifProfidor === _);
|
|
console.log(prefs.gifProvidor);
|
|
tas.addSelect(
|
|
I18n.gifProvidor(),
|
|
async (i) => {
|
|
prefs.gifProvidor = this.gifProvideors[i].api_name;
|
|
console.log(prefs.gifProvidor);
|
|
|
|
await setPreferences(prefs);
|
|
Localuser.loadFont();
|
|
this.figureDefaultProvidor();
|
|
},
|
|
this.gifProvideors.map((_) => _.name),
|
|
{defaultIndex: ind === -1 ? 0 : ind},
|
|
);
|
|
}
|
|
{
|
|
tas.addCheckboxInput(
|
|
"Show today at:",
|
|
(b) => {
|
|
prefs.showToday = b;
|
|
setPreferences(prefs);
|
|
},
|
|
{
|
|
initState: prefs.showToday,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
{
|
|
const blog = settings.addButton(I18n.blog.blog(), {contained: true});
|
|
blog.addCheckboxInput(
|
|
I18n.blog.blogUpdates(),
|
|
async (check) => {
|
|
prefs.showBlogUpdates = check;
|
|
await setPreferences(prefs);
|
|
},
|
|
{initState: prefs.showBlogUpdates},
|
|
);
|
|
(async () => {
|
|
const posts = await this.getPosts();
|
|
for (const post of posts.items) {
|
|
const div = document.createElement("div");
|
|
div.classList.add("flexltr", "blogDiv");
|
|
if (post.image) {
|
|
//TODO handle this case, no blog posts currently do this
|
|
}
|
|
const titleStuff = document.createElement("div");
|
|
titleStuff.classList.add("flexttb");
|
|
|
|
const h2 = document.createElement("h2");
|
|
h2.textContent = post.title;
|
|
|
|
const p = document.createElement("p");
|
|
p.textContent = post.content_html;
|
|
titleStuff.append(h2, p);
|
|
div.append(titleStuff);
|
|
blog.addHTMLArea(div);
|
|
MarkDown.safeLink(div, post.url);
|
|
}
|
|
})();
|
|
}
|
|
{
|
|
const shortcuts = settings.addButton(I18n.keyboard.shortcuts(), {contained: true});
|
|
const cur = getShortcuts();
|
|
for (const [name] of cur) {
|
|
shortcuts.addText(I18n.keyboard.descs[name]());
|
|
shortcuts.addHTMLArea(
|
|
Localuser.globalShortcuts.shortCutbutton(
|
|
name,
|
|
(short) => {
|
|
cur[name] = short;
|
|
setShortcuts(cur);
|
|
console.log(cur);
|
|
},
|
|
true,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const installP = installPGet();
|
|
if (installP) {
|
|
const c = settings.addButton(I18n.localuser.install(), {contained: true});
|
|
c.addText(I18n.localuser.installDesc());
|
|
c.addButtonInput("", I18n.localuser.installJank(), async () => {
|
|
//@ts-expect-error have to do this :3
|
|
await installP.prompt();
|
|
});
|
|
}
|
|
|
|
settings.addButton(I18n.accessibility.name(), {head: true});
|
|
|
|
{
|
|
const visuals = settings.addButton(I18n.accessibility.visuals(), {contained: true});
|
|
visuals.addCheckboxInput(
|
|
I18n.accessibility.roleColors(),
|
|
(t) => {
|
|
console.log(t);
|
|
this.perminfo.user.disableColors = !t;
|
|
},
|
|
{initState: !this.perminfo.user.disableColors},
|
|
);
|
|
visuals.addCheckboxInput(
|
|
I18n.accessibility.gradientColors(),
|
|
(t) => {
|
|
console.log(t);
|
|
this.perminfo.user.gradientColors = t;
|
|
},
|
|
{initState: this.perminfo.user.gradientColors},
|
|
);
|
|
visuals.addCheckboxInput(
|
|
I18n.channel.allowIcons(),
|
|
(t) => {
|
|
console.log(t);
|
|
this.perminfo.user.disableIcons = !t;
|
|
},
|
|
{initState: !this.perminfo.user.disableIcons},
|
|
);
|
|
|
|
visuals.addCheckboxInput(
|
|
I18n.accessibility.decorations(),
|
|
(t) => {
|
|
this.perminfo.user.decorations = t;
|
|
},
|
|
{initState: this.perminfo.user.decorations},
|
|
);
|
|
{
|
|
const cur = prefs.renderJoinAvatars;
|
|
visuals.addCheckboxInput(
|
|
I18n.renderJoinAvatars(),
|
|
async (v) => {
|
|
prefs.renderJoinAvatars = v;
|
|
await setPreferences(prefs);
|
|
},
|
|
{initState: cur},
|
|
);
|
|
}
|
|
visuals.addCheckboxInput(
|
|
I18n.checkBoxMemberList(),
|
|
(c) => {
|
|
prefs.checkMemberList = c;
|
|
const ml = document.getElementById("memberlisttoggle")!;
|
|
if (c) {
|
|
ml.classList = "";
|
|
} else {
|
|
ml.classList = "svgicon svg-friends";
|
|
}
|
|
setPreferences(prefs);
|
|
},
|
|
{
|
|
initState: !!prefs.checkMemberList,
|
|
},
|
|
);
|
|
}
|
|
{
|
|
const animations = settings.addButton(I18n.accessibility.animations(), {contained: true});
|
|
animations.addSelect(
|
|
I18n.accessibility.playGif(),
|
|
async (i) => {
|
|
prefs.animateGifs = AnimateTristateValues[i];
|
|
await setPreferences(prefs);
|
|
},
|
|
AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()),
|
|
{defaultIndex: AnimateTristateValues.indexOf(prefs.animateGifs)},
|
|
);
|
|
animations.addSelect(
|
|
I18n.accessibility.playIcon(),
|
|
async (i) => {
|
|
prefs.animateIcons = AnimateTristateValues[i];
|
|
await setPreferences(prefs);
|
|
},
|
|
AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()),
|
|
{defaultIndex: AnimateTristateValues.indexOf(prefs.animateIcons)},
|
|
);
|
|
}
|
|
settings.addButton(I18n.localuser.security(), {head: true});
|
|
{
|
|
const twofa = settings.addButton(I18n.localuser["2fa"](), {contained: true});
|
|
const gen2FA = () => {
|
|
twofa.removeAll();
|
|
if (this.mfa_enabled) {
|
|
twofa.addSubButtonInput(I18n.localuser["2faDisable"](), (sub) => {
|
|
const form = sub.addForm(
|
|
"",
|
|
(_: any) => {
|
|
if (_.message) {
|
|
switch (_.code) {
|
|
case 60008:
|
|
form.error("code", I18n.localuser.badCode());
|
|
break;
|
|
}
|
|
} else {
|
|
this.mfa_enabled = false;
|
|
twofa.returnFromSub();
|
|
gen2FA();
|
|
}
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/mfa/totp/disable",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true});
|
|
});
|
|
} else {
|
|
twofa.addSubButtonInput(I18n.localuser["2faEnable"](), async (sub) => {
|
|
let secret = "";
|
|
for (let i = 0; i < 18; i++) {
|
|
secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random() * 32)];
|
|
}
|
|
const form = sub.addForm(
|
|
"",
|
|
(_: any) => {
|
|
if (_.message) {
|
|
switch (_.code) {
|
|
case 60008:
|
|
form.error("code", I18n.localuser.badCode());
|
|
break;
|
|
case 400:
|
|
form.error("password", I18n.localuser.badPassword());
|
|
break;
|
|
}
|
|
} else {
|
|
gen2FA();
|
|
this.mfa_enabled = true;
|
|
twofa.returnFromSub();
|
|
}
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/mfa/totp/enable",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
form.addTitle(I18n.localuser.setUp2faInstruction());
|
|
form.addText(I18n.localuser["2faCodeGive"](secret));
|
|
form.addTextInput(I18n.localuser["password:"](), "password", {
|
|
required: true,
|
|
password: true,
|
|
});
|
|
form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true});
|
|
form.setValue("secret", secret);
|
|
});
|
|
}
|
|
{
|
|
twofa.addSubButtonInput(I18n.webauth.manage(), (keyMenu) => {
|
|
const addKey = (key: {name: string; id: string}) => {
|
|
keyMenu.addButtonInput("", key.name, () => {
|
|
const opt = keyMenu.addSubOptions(key.name);
|
|
const button = opt.addButtonInput("", I18n.delete(), async () => {
|
|
await fetch(this.info.api + "/users/@me/mfa/webauthn/credentials/" + key.id, {
|
|
headers: this.headers,
|
|
method: "DELETE",
|
|
});
|
|
keyMenu.returnFromSub();
|
|
keyMenu.deleteElm(button);
|
|
});
|
|
});
|
|
};
|
|
keyMenu.addButtonInput("", I18n.webauth.addKey(), () => {
|
|
const form = keyMenu.addSubForm(
|
|
I18n.webauth.addKey(),
|
|
async (obj) => {
|
|
const body = obj as {ticket: string; challenge: string};
|
|
const challenge = JSON.parse(body.challenge)
|
|
.publicKey as PublicKeyCredentialCreationOptionsJSON;
|
|
console.log(challenge.challenge);
|
|
challenge.challenge = challenge.challenge
|
|
.split("=")[0]
|
|
.replaceAll("+", "-")
|
|
.replaceAll("/", "_");
|
|
console.log(challenge.challenge);
|
|
const options = PublicKeyCredential.parseCreationOptionsFromJSON(challenge);
|
|
const credential = (await navigator.credentials.create({
|
|
publicKey: options,
|
|
})) as unknown as {
|
|
rawId: ArrayBuffer;
|
|
response: {
|
|
attestationObject: ArrayBuffer;
|
|
clientDataJSON: ArrayBuffer;
|
|
};
|
|
};
|
|
if (!credential) return;
|
|
function toBase64(buf: ArrayBuffer) {
|
|
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
}
|
|
const res = {
|
|
rawId: toBase64(credential.rawId),
|
|
response: {
|
|
clientDataJSON: toBase64(credential.response.clientDataJSON),
|
|
attestationObject: toBase64(credential.response.attestationObject),
|
|
},
|
|
};
|
|
const key = await (
|
|
await fetch(this.info.api + "/users/@me/mfa/webauthn/credentials", {
|
|
headers: this.headers,
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
ticket: body.ticket,
|
|
credential: JSON.stringify(res),
|
|
name: name.value,
|
|
}),
|
|
})
|
|
).json();
|
|
addKey(key);
|
|
keyMenu.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/mfa/webauthn/credentials",
|
|
method: "POST",
|
|
headers: this.headers,
|
|
tfaCheck: false,
|
|
},
|
|
);
|
|
form.addTextInput(I18n.htmlPages.pwField(), "password", {
|
|
password: true,
|
|
});
|
|
const name = form.options.addTextInput(I18n.webauth.keyname(), () => {}, {
|
|
initText: "Key",
|
|
});
|
|
});
|
|
fetch(this.info.api + "/users/@me/mfa/webauthn/credentials", {
|
|
headers: this.headers,
|
|
})
|
|
.then((_) => _.json())
|
|
.then((keys: {id: string; name: string}[]) => {
|
|
for (const key of keys) {
|
|
addKey(key);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|
|
gen2FA();
|
|
}
|
|
{
|
|
const manageSessions = settings.addButton(I18n.deviceManage.title(), {contained: true});
|
|
(async () => {
|
|
const json = (await (
|
|
await fetch(this.info.api + "/auth/sessions?extended=true", {headers: this.headers})
|
|
).json()) as {user_sessions: expSessionJson[]};
|
|
for (const session of json.user_sessions.sort(
|
|
(a, b) => +new Date(a.last_seen) - +new Date(b.last_seen),
|
|
)) {
|
|
const div = document.createElement("div");
|
|
div.classList.add("flexltr", "sessionDiv");
|
|
|
|
const info = document.createElement("div");
|
|
info.classList.add("flexttb");
|
|
div.append(info);
|
|
|
|
let line2 = "";
|
|
const last = session.last_seen_location_info;
|
|
if (last) {
|
|
line2 += last.country_name;
|
|
if (last.region) line2 += ", " + last.region;
|
|
if (last.city) line2 += ", " + last.city;
|
|
}
|
|
if (line2) {
|
|
line2 += " • ";
|
|
}
|
|
const format = new Intl.RelativeTimeFormat(I18n.lang, {style: "short"});
|
|
const time = (Date.now() - +new Date(session.last_seen)) / 1000;
|
|
if (time < 60) {
|
|
line2 += format.format(-Math.floor(time), "seconds");
|
|
} else if (time < 60 * 60) {
|
|
line2 += format.format(-Math.floor(time / 60), "minutes");
|
|
} else if (time < 60 * 60 * 24) {
|
|
line2 += format.format(-Math.floor(time / 60 / 60), "hours");
|
|
} else if (time < 60 * 60 * 24 * 7) {
|
|
line2 += format.format(-Math.floor(time / 60 / 60 / 24), "days");
|
|
} else if (time < 60 * 60 * 24 * 365) {
|
|
line2 += format.format(-Math.floor(time / 60 / 60 / 24 / 7), "weeks");
|
|
} else {
|
|
line2 += format.format(-Math.floor(time / 60 / 60 / 24 / 365), "years");
|
|
}
|
|
const loc = document.createElement("span");
|
|
loc.textContent = line2;
|
|
info.append(loc);
|
|
const r = manageSessions.addHTMLArea(div);
|
|
div.onclick = () => {
|
|
const sub = manageSessions.addSubOptions(I18n.deviceManage.manageDev());
|
|
sub.addText(I18n.deviceManage.ip(session.last_seen_ip));
|
|
sub.addText(I18n.deviceManage.last(session.approx_last_used_time));
|
|
if (last) {
|
|
sub.addText(I18n.deviceManage.estimateWarn());
|
|
sub.addText(I18n.deviceManage.continent(last.continent_name));
|
|
sub.addText(I18n.deviceManage.country(last.country_name));
|
|
if (last.region) sub.addText(I18n.deviceManage.region(last.region));
|
|
if (last.city) sub.addText(I18n.deviceManage.city(last.city));
|
|
if (last.postal) sub.addText(I18n.deviceManage.postal(last.postal));
|
|
sub.addText(I18n.deviceManage.longitude(last.longitude + ""));
|
|
sub.addText(I18n.deviceManage.latitude(last.latitude + ""));
|
|
}
|
|
if (session.id !== this.session_id) {
|
|
sub.addButtonInput("", I18n.deviceManage.logout(), () => {
|
|
div.remove();
|
|
r.html = document.createElement("div");
|
|
manageSessions.returnFromSub();
|
|
fetch(this.info.api + "/auth/sessions/logout", {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
body: JSON.stringify({
|
|
session_id_hashes: [session.id_hash],
|
|
}),
|
|
});
|
|
});
|
|
} else sub.addText(I18n.deviceManage.curSes());
|
|
};
|
|
}
|
|
})();
|
|
}
|
|
{
|
|
const trusted = settings.addButton(I18n.localuser.trusted(), {contained: true});
|
|
trusted.addMDText(new MarkDown(I18n.localuser.trustedDesc()));
|
|
for (const thing of MarkDown.trustedDomains) {
|
|
const div = document.createElement("div");
|
|
div.classList.add("flexltr", "trustedDomain");
|
|
|
|
const name = document.createElement("span");
|
|
name.textContent = thing;
|
|
|
|
const remove = document.createElement("button");
|
|
remove.textContent = I18n.remove();
|
|
remove.onclick = () => {
|
|
MarkDown.saveTrusted();
|
|
MarkDown.trustedDomains.delete(thing);
|
|
MarkDown.saveTrusted(true);
|
|
div.remove();
|
|
};
|
|
|
|
div.append(name, remove);
|
|
trusted.addHTMLArea(div);
|
|
}
|
|
}
|
|
settings.addButton(I18n.localuser.advanced(), {head: true});
|
|
|
|
{
|
|
const devPortal = settings.addButton(I18n.localuser.devPortal(), {contained: true});
|
|
|
|
fetch(this.info.api + "/teams", {
|
|
headers: this.headers,
|
|
}).then(async (teamsRes) => {
|
|
const teams = await teamsRes.json();
|
|
|
|
const button = devPortal.addSubButtonInput(I18n.localuser.createApp(), (sub) => {
|
|
const form = sub.addForm(
|
|
"",
|
|
(json: any) => {
|
|
if (json.message) form.error("name", json.message);
|
|
else {
|
|
devPortal.returnFromSub();
|
|
this.manageApplication(json.id, devPortal, () => {
|
|
form.options.deleteElm(button);
|
|
});
|
|
}
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/applications",
|
|
headers: this.headers,
|
|
method: "POST",
|
|
},
|
|
);
|
|
|
|
form.addTextInput("Name:", "name", {required: true});
|
|
form.addSelect(
|
|
I18n.localuser["team:"](),
|
|
"team_id",
|
|
["Personal", ...teams.map((team: {name: string}) => team.name)],
|
|
{defaultIndex: 0},
|
|
);
|
|
});
|
|
|
|
const appListContainer = document.createElement("div");
|
|
appListContainer.id = "app-list-container";
|
|
fetch(this.info.api + "/applications", {
|
|
headers: this.headers,
|
|
})
|
|
.then((r) => r.json())
|
|
.then((json) => {
|
|
json.forEach(
|
|
(application: {
|
|
cover_image: any;
|
|
icon: any;
|
|
id: string | undefined;
|
|
name: string | number;
|
|
bot: any;
|
|
}) => {
|
|
const container = document.createElement("div");
|
|
|
|
if (application.cover_image || application.icon) {
|
|
const cover = createImg(
|
|
this.info.cdn +
|
|
"/app-icons/" +
|
|
application.id +
|
|
"/" +
|
|
(application.cover_image || application.icon) +
|
|
".png" +
|
|
new CDNParams({expectedSize: 256}),
|
|
);
|
|
cover.alt = "";
|
|
cover.loading = "lazy";
|
|
container.appendChild(cover);
|
|
}
|
|
|
|
const name = document.createElement("h2");
|
|
name.textContent = application.name + (application.bot ? " (Bot)" : "");
|
|
container.appendChild(name);
|
|
|
|
container.addEventListener("click", async () => {
|
|
this.manageApplication(application.id, devPortal, () => {
|
|
appListContainer.remove();
|
|
});
|
|
});
|
|
appListContainer.appendChild(container);
|
|
},
|
|
);
|
|
});
|
|
devPortal.addHTMLArea(appListContainer);
|
|
});
|
|
}
|
|
|
|
if (
|
|
this.rights.hasPermission("OPERATOR") ||
|
|
this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")
|
|
) {
|
|
const manageInstance = settings.addButton(I18n.localuser.manageInstance(), {contained: true});
|
|
if (this.rights.hasPermission("OPERATOR")) {
|
|
manageInstance.addButtonInput("", I18n.manageInstance.stop(), () => {
|
|
const menu = new Dialog("");
|
|
const options = menu.float.options;
|
|
options.addTitle(I18n.manageInstance.AreYouSureStop());
|
|
const yesno = options.addOptions("", {ltr: true});
|
|
yesno.addButtonInput("", I18n.yes(), () => {
|
|
fetch(this.info.api + "/stop", {headers: this.headers, method: "POST"});
|
|
menu.hide();
|
|
});
|
|
yesno.addButtonInput("", I18n.no(), () => {
|
|
menu.hide();
|
|
});
|
|
menu.show();
|
|
});
|
|
}
|
|
if (this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")) {
|
|
manageInstance.addSubButtonInput(
|
|
I18n.manageInstance.createTokens(),
|
|
(tokens) => {
|
|
const count = tokens.addTextInput(I18n.manageInstance.count(), () => {}, {
|
|
initText: "1",
|
|
});
|
|
const length = tokens.addTextInput(I18n.manageInstance.length(), () => {}, {
|
|
initText: "32",
|
|
});
|
|
const format = tokens.addSelect(
|
|
I18n.manageInstance.format(),
|
|
() => {},
|
|
[
|
|
I18n.manageInstance.TokenFormats.JSON(),
|
|
I18n.manageInstance.TokenFormats.plain(),
|
|
I18n.manageInstance.TokenFormats.URLs(),
|
|
],
|
|
{
|
|
defaultIndex: 2,
|
|
},
|
|
);
|
|
format.watchForChange((e) => {
|
|
if (e !== 2) {
|
|
urlOption.removeAll();
|
|
} else {
|
|
makeURLMenu();
|
|
}
|
|
});
|
|
const urlOption = tokens.addOptions("");
|
|
const urlOptionsJSON = {
|
|
url: window.location.origin,
|
|
type: "Fermi",
|
|
};
|
|
function makeURLMenu() {
|
|
urlOption
|
|
.addTextInput(I18n.manageInstance.clientURL(), () => {}, {
|
|
initText: urlOptionsJSON.url,
|
|
})
|
|
.watchForChange((str) => {
|
|
urlOptionsJSON.url = str;
|
|
});
|
|
urlOption
|
|
.addSelect(
|
|
I18n.manageInstance.regType(),
|
|
() => {},
|
|
["Fermi", I18n.manageInstance.genericType()],
|
|
{
|
|
defaultIndex: ["Fermi", "generic"].indexOf(urlOptionsJSON.type),
|
|
},
|
|
)
|
|
.watchForChange((i) => {
|
|
urlOptionsJSON.type = ["Fermi", "generic"][i];
|
|
});
|
|
}
|
|
makeURLMenu();
|
|
tokens.addButtonInput("", I18n.manageInstance.create(), async () => {
|
|
const params = new URLSearchParams();
|
|
params.set("count", count.value);
|
|
params.set("length", length.value);
|
|
const json = (await (
|
|
await fetch(
|
|
this.info.api + "/auth/generate-registration-tokens?" + params.toString(),
|
|
{
|
|
headers: this.headers,
|
|
},
|
|
)
|
|
).json()) as {tokens: string[]};
|
|
if (format.index === 0) {
|
|
pre.textContent = JSON.stringify(json.tokens);
|
|
} else if (format.index === 1) {
|
|
pre.textContent = json.tokens.join("\n");
|
|
} else if (format.index === 2) {
|
|
if (urlOptionsJSON.type === "Fermi") {
|
|
const options = new URLSearchParams();
|
|
options.set("instance", this.instanceString());
|
|
pre.textContent = json.tokens
|
|
.map((token) => {
|
|
options.set("token", token);
|
|
return `${urlOptionsJSON.url}/register?` + options.toString();
|
|
})
|
|
.join("\n");
|
|
} else {
|
|
const options = new URLSearchParams();
|
|
pre.textContent = json.tokens
|
|
.map((token) => {
|
|
options.set("token", token);
|
|
return `${urlOptionsJSON.url}/register?` + options.toString();
|
|
})
|
|
.join("\n");
|
|
}
|
|
}
|
|
});
|
|
tokens.addButtonInput("", I18n.manageInstance.copy(), async () => {
|
|
try {
|
|
if (pre.textContent) {
|
|
await navigator.clipboard.writeText(pre.textContent);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
});
|
|
const pre = document.createElement("pre");
|
|
tokens.addHTMLArea(pre);
|
|
},
|
|
{
|
|
noSubmit: true,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
(async () => {
|
|
const jankInfo = settings.addButton(I18n.jankInfo(), {contained: true});
|
|
const img = document.createElement("img");
|
|
img.src = "/logo.svg";
|
|
jankInfo.addHTMLArea(img);
|
|
img.width = 128;
|
|
img.height = 128;
|
|
const ver = await (await fetch("/getupdates")).text();
|
|
jankInfo.addMDText(
|
|
new MarkDown(
|
|
I18n.clientDesc(ver, window.location.origin, this.rights.allow + ""),
|
|
undefined,
|
|
),
|
|
);
|
|
})();
|
|
|
|
{
|
|
const devSettings = settings.addButton(I18n.devSettings.name(), {
|
|
noSubmit: true,
|
|
contained: true,
|
|
});
|
|
devSettings.addText(I18n.devSettings.description());
|
|
devSettings.addHR();
|
|
const box1 = devSettings.addCheckboxInput(I18n.devSettings.logGateway(), () => {}, {
|
|
initState: getDeveloperSettings().gatewayLogging,
|
|
});
|
|
box1.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.gatewayLogging = e;
|
|
setDeveloperSettings(settings);
|
|
};
|
|
|
|
const box2 = devSettings.addCheckboxInput(I18n.devSettings.badUser(), () => {}, {
|
|
initState: getDeveloperSettings().logBannedFields,
|
|
});
|
|
box2.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.logBannedFields = e;
|
|
setDeveloperSettings(settings);
|
|
};
|
|
|
|
const box3 = devSettings.addCheckboxInput(I18n.devSettings.traces(), () => {}, {
|
|
initState: getDeveloperSettings().showTraces,
|
|
});
|
|
box3.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.showTraces = e;
|
|
setDeveloperSettings(settings);
|
|
};
|
|
|
|
const box4 = devSettings.addCheckboxInput(I18n.devSettings.cache(), () => {}, {
|
|
initState: getDeveloperSettings().cacheSourceMaps,
|
|
});
|
|
box4.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.cacheSourceMaps = e;
|
|
setDeveloperSettings(settings);
|
|
SW.postMessage({code: "isDev", dev: e});
|
|
};
|
|
devSettings.addText(I18n.devSettings.cacheDesc());
|
|
|
|
const box5 = devSettings.addCheckboxInput(I18n.devSettings.captureTrace(), () => {}, {
|
|
initState: getDeveloperSettings().interceptApiTraces,
|
|
});
|
|
box5.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.interceptApiTraces = e;
|
|
setDeveloperSettings(settings);
|
|
SW.traceInit();
|
|
};
|
|
|
|
const box6 = devSettings.addCheckboxInput(I18n.devSettings.gatewayComp(), () => {}, {
|
|
initState: getDeveloperSettings().gatewayCompression,
|
|
});
|
|
box6.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.gatewayCompression = e;
|
|
setDeveloperSettings(settings);
|
|
SW.traceInit();
|
|
};
|
|
|
|
const box7 = devSettings.addCheckboxInput(I18n.devSettings.reportSystem(), () => {}, {
|
|
initState: getDeveloperSettings().reportSystem,
|
|
});
|
|
box7.onchange = (e) => {
|
|
const settings = getDeveloperSettings();
|
|
settings.reportSystem = e;
|
|
setDeveloperSettings(settings);
|
|
SW.traceInit();
|
|
};
|
|
|
|
devSettings.addButtonInput("", I18n.devSettings.clearWellKnowns(), async () => {
|
|
const currentUserInfos = JSON.parse(localStorage.getItem("userinfos")!);
|
|
await Promise.all(
|
|
Object.keys(currentUserInfos.users).map(async (user) => {
|
|
const key =
|
|
currentUserInfos.users[user].serverurls.value ??
|
|
currentUserInfos.users[user].serverurls.wellknown ??
|
|
currentUserInfos.users[user].serverurls.api;
|
|
currentUserInfos.users[user].serverurls = await getapiurls(key);
|
|
console.log(key, currentUserInfos.users[user].serverurls);
|
|
localStorage.setItem("userinfos", JSON.stringify(currentUserInfos));
|
|
}),
|
|
);
|
|
|
|
localStorage.removeItem("instanceinfo");
|
|
await SW.postMessage({
|
|
code: "clearCdnCache",
|
|
});
|
|
|
|
// @ts-ignore - chromium is smelly for not supporting the `forceGet` option (aka skip cache)
|
|
window.location.reload(true);
|
|
});
|
|
}
|
|
if (this.trace.length && getDeveloperSettings().showTraces) {
|
|
const traces = settings.addButton(I18n.localuser.trace(), {
|
|
noSubmit: true,
|
|
contained: true,
|
|
});
|
|
const traceArr = this.trace;
|
|
|
|
const sel = traces.addSelect(
|
|
"",
|
|
() => {},
|
|
this.trace.map((_) =>
|
|
I18n.trace.traces(
|
|
_.trace[0],
|
|
_.trace[1].micros / 1000 + "",
|
|
_.time.getHours() + ":" + _.time.getMinutes(),
|
|
),
|
|
),
|
|
);
|
|
function generateTraceHTML(trace: trace, indent: number): HTMLElement {
|
|
const div = document.createElement("div");
|
|
div.classList.add("traceDiv", "flexttb");
|
|
|
|
const head = document.createElement("div");
|
|
div.append(head);
|
|
|
|
const title = document.createElement("h3");
|
|
title.textContent = I18n.trace.totalTime(trace[1].micros / 1000 + "", trace[0]);
|
|
const indents = document.createElement("span");
|
|
indents.classList.add("visually-hidden");
|
|
indents.textContent = " ".repeat(indent);
|
|
title.prepend(indents);
|
|
head.append(title);
|
|
|
|
if (!trace[1].calls) return div;
|
|
|
|
let objs: {name: string; val: traceObj}[] = [];
|
|
{
|
|
const names = trace[1].calls.filter((_) => typeof _ === "string");
|
|
const vals = trace[1].calls.filter((_) => _ instanceof Object);
|
|
let i = 0;
|
|
for (const name of names) {
|
|
const val = vals[i];
|
|
objs.push({name, val});
|
|
i++;
|
|
}
|
|
}
|
|
|
|
const bars = document.createElement("div");
|
|
bars.classList.add("flexltr", "traceBars");
|
|
|
|
const colors = ["red", "orange", "yellow", "lime", "blue", "indigo", "violet"];
|
|
let i = 0;
|
|
for (const thing of objs) {
|
|
const bar = document.createElement("div");
|
|
bar.style.setProperty(
|
|
"flex-grow",
|
|
Math.ceil((thing.val.micros / trace[1].micros) * 1000) + "",
|
|
);
|
|
bar.style.setProperty("background", colors[i % colors.length]);
|
|
bars.append(bar);
|
|
new Hover(I18n.trace.totalTime(thing.val.micros / 1000 + "", thing.name)).addEvent(bar);
|
|
i++;
|
|
}
|
|
const body = document.createElement("div");
|
|
div.append(body);
|
|
head.append(bars);
|
|
let dropped = false;
|
|
head.onclick = () => {
|
|
if (!trace[1].calls) return;
|
|
if (dropped) {
|
|
dropped = false;
|
|
body.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
let i = 0;
|
|
for (const obj of objs) {
|
|
body.append(generateTraceHTML([obj.name, obj.val], indent + 1));
|
|
i++;
|
|
}
|
|
dropped = true;
|
|
};
|
|
|
|
div.classList.add("dropDownTrace");
|
|
head.classList.add("traceHead");
|
|
return div;
|
|
}
|
|
const blank = document.createElement("div");
|
|
traces.addHTMLArea(blank);
|
|
const updateInfo = () => {
|
|
const trace = traceArr[sel.index];
|
|
blank.innerHTML = "";
|
|
blank.append(generateTraceHTML(trace.trace, 0));
|
|
};
|
|
sel.onchange = () => {
|
|
updateInfo();
|
|
};
|
|
updateInfo();
|
|
}
|
|
{
|
|
const update = settings.addButton(I18n.localuser.updateSettings(), {contained: true});
|
|
let index = ServiceWorkerModeValues.indexOf(localSettings.serviceWorkerMode);
|
|
if (index === -1) {
|
|
index = 2;
|
|
}
|
|
const sw = update.addSelect(
|
|
I18n.settings.updates.serviceWorkerMode.title(),
|
|
() => {},
|
|
ServiceWorkerModeValues.map((e) => I18n.settings.updates.serviceWorkerMode[e]()),
|
|
{
|
|
defaultIndex: index,
|
|
},
|
|
);
|
|
sw.onchange = (e) => {
|
|
SW.setMode(ServiceWorkerModeValues[e]);
|
|
};
|
|
update.addButtonInput("", I18n.localuser.CheckUpdate(), async () => {
|
|
const update = await SW.checkUpdates();
|
|
const text = update ? I18n.localuser.updatesYay() : I18n.localuser.noUpdates();
|
|
const d = new Dialog("");
|
|
d.options.addTitle(text);
|
|
if (update) {
|
|
d.options.addButtonInput("", I18n.localuser.refreshPage(), () => {
|
|
window.location.reload();
|
|
});
|
|
}
|
|
d.show();
|
|
});
|
|
update.addButtonInput("", I18n.localuser.clearCache(), () => {
|
|
SW.forceClear();
|
|
});
|
|
}
|
|
{
|
|
const instanceInfo = settings.addButton(I18n.instanceInfo.name(), {contained: true});
|
|
fetch(this.info.api + "/policies/instance", {
|
|
headers: this.headers,
|
|
})
|
|
.then((_) => _.json())
|
|
.then((body) => {
|
|
const json = body as {
|
|
instanceName: string;
|
|
instanceDescription: string | null;
|
|
frontPage: string | null;
|
|
tosPage: string | null;
|
|
correspondenceEmail: string | null;
|
|
correspondenceUserID: string | null;
|
|
image: string | null;
|
|
instanceId: string;
|
|
autoCreateBotUsers: false;
|
|
publicUrl: string | null;
|
|
};
|
|
instanceInfo.addTitle(json.instanceName);
|
|
if (json.correspondenceEmail) {
|
|
const a = document.createElement("a");
|
|
a.target = "_blank";
|
|
a.rel = "noreferrer";
|
|
a.href = "mailto:" + json.correspondenceEmail;
|
|
a.textContent = I18n.instanceInfo.contact();
|
|
instanceInfo.addHTMLArea(a);
|
|
}
|
|
if (json.tosPage)
|
|
instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.tosPage(json.tosPage)));
|
|
if (json.publicUrl)
|
|
instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.publicUrl(json.publicUrl)));
|
|
if (json.frontPage)
|
|
instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.frontPage(json.frontPage)));
|
|
instanceInfo.addButtonInput("", I18n.instInfo(), () => {
|
|
this.instanceStats();
|
|
});
|
|
});
|
|
}
|
|
settings.addButton(I18n.localuser.danger(), {head: true});
|
|
{
|
|
const deleteAccount = settings
|
|
.addButton(I18n.localuser.deleteAccount(), {contained: true, color: buttonColor.RED})
|
|
.addForm(
|
|
"",
|
|
(e) => {
|
|
if ("message" in e) {
|
|
if (typeof e.message === "string") {
|
|
throw new FormError(password, e.message);
|
|
}
|
|
} else {
|
|
this.userinfo.remove();
|
|
window.location.href = "/";
|
|
}
|
|
},
|
|
{
|
|
headers: this.headers,
|
|
method: "POST",
|
|
fetchURL: this.info.api + "/users/@me/delete/",
|
|
traditionalSubmit: false,
|
|
submitText: I18n.localuser.deleteAccountButton(),
|
|
},
|
|
);
|
|
const shrek = deleteAccount.addTextInput(
|
|
I18n.localuser.areYouSureDelete(I18n.localuser.sillyDeleteConfirmPhrase()),
|
|
"shrek",
|
|
);
|
|
const password = deleteAccount.addTextInput(I18n.localuser["password:"](), "password", {
|
|
password: true,
|
|
});
|
|
deleteAccount.addPreprocessor((obj) => {
|
|
if ("shrek" in obj) {
|
|
if (obj.shrek !== I18n.localuser.sillyDeleteConfirmPhrase()) {
|
|
throw new FormError(shrek, I18n.localuser.mustTypePhrase());
|
|
}
|
|
delete obj.shrek;
|
|
} else {
|
|
throw new FormError(shrek, I18n.localuser.mustTypePhrase());
|
|
}
|
|
});
|
|
}
|
|
|
|
settings.show();
|
|
}
|
|
readonly botTokens: Map<string, string> = new Map();
|
|
async manageApplication(appId = "", container: Options, deleteButton: () => void) {
|
|
if (this.perminfo.applications) {
|
|
for (const item of Object.keys(this.perminfo.applications)) {
|
|
this.botTokens.set(item, this.perminfo.applications[item]);
|
|
}
|
|
}
|
|
const res = await fetch(this.info.api + "/applications/" + appId, {
|
|
headers: this.headers,
|
|
});
|
|
const json = await res.json();
|
|
console.error(json);
|
|
const form = container.addSubForm(json.name, () => {}, {
|
|
fetchURL: this.info.api + "/applications/" + appId,
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
traditionalSubmit: true,
|
|
});
|
|
form.addTextInput(I18n.localuser.appName(), "name", {initText: json.name});
|
|
form.addMDInput(I18n.localuser.description(), "description", {
|
|
initText: json.description,
|
|
});
|
|
form.addImageInput("Icon:", "icon", {
|
|
clear: true,
|
|
initImg: json.icon
|
|
? this.info.cdn +
|
|
"/app-icons/" +
|
|
appId +
|
|
"/" +
|
|
json.icon +
|
|
new CDNParams({expectedSize: 96})
|
|
: "",
|
|
});
|
|
form.addTextInput(I18n.localuser.privacyPolcyURL(), "privacy_policy_url", {
|
|
initText: json.privacy_policy_url,
|
|
});
|
|
form.addText(I18n.localuser.appID(appId));
|
|
form.addSubButtonInput(
|
|
I18n.localuser.showSecret(),
|
|
(opt) => {
|
|
opt.addText(I18n.localuser.clientSecret(json.verify_key));
|
|
},
|
|
{
|
|
intText: I18n.localuser.secret(),
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser.TOSURL(), "terms_of_service_url", {
|
|
initText: json.terms_of_service_url,
|
|
});
|
|
form.addCheckboxInput(I18n.localuser.publicAvaliable(), "bot_public", {
|
|
initState: json.bot_public,
|
|
});
|
|
form.addCheckboxInput(I18n.localuser.requireCode(), "bot_require_code_grant", {
|
|
initState: json.bot_require_code_grant,
|
|
});
|
|
form.addButtonInput("", I18n.localuser[json.bot ? "manageBot" : "addBot"](), async () => {
|
|
if (!json.bot) {
|
|
if (!confirm(I18n.localuser.confirmAddBot())) {
|
|
return;
|
|
}
|
|
const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot", {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
});
|
|
const updateJSON = await updateRes.json();
|
|
this.botTokens.set(appId, updateJSON.token);
|
|
}
|
|
this.manageBot(appId, form);
|
|
});
|
|
form.addButtonInput("", I18n.applications.delete(), () => {
|
|
const sub = form.addSubForm(
|
|
I18n.applications.delete(),
|
|
() => {
|
|
deleteButton();
|
|
container.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/applications/" + appId + "/delete",
|
|
method: "POST",
|
|
headers: this.headers,
|
|
submitText: I18n.delete(),
|
|
},
|
|
);
|
|
sub.addText(I18n.applications.sure(json.name));
|
|
});
|
|
}
|
|
async manageBot(appId = "", container: Form) {
|
|
const res = await fetch(this.info.api + "/applications/" + appId, {
|
|
headers: this.headers,
|
|
});
|
|
const json = await res.json();
|
|
if (!json.bot) {
|
|
return alert(I18n.localuser.confuseNoBot());
|
|
}
|
|
const bot: User = new User(json.bot, this);
|
|
const form = container.addSubForm(
|
|
I18n.localuser.editingBot(bot.username),
|
|
(out) => {
|
|
console.log(out);
|
|
},
|
|
{
|
|
method: "PATCH",
|
|
fetchURL: this.info.api + "/applications/" + appId + "/bot",
|
|
headers: this.headers,
|
|
traditionalSubmit: true,
|
|
},
|
|
);
|
|
form.addTextInput(I18n.localuser.botUsername(), "username", {
|
|
initText: bot.username,
|
|
});
|
|
form.addImageInput(I18n.localuser.botAvatar(), "avatar", {
|
|
initImg: bot.getpfpsrc(),
|
|
clear: true,
|
|
});
|
|
form.addButtonInput("", I18n.localuser.resetToken(), async () => {
|
|
if (!confirm(I18n.localuser.confirmReset())) {
|
|
return;
|
|
}
|
|
const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot/reset", {
|
|
method: "POST",
|
|
headers: this.headers,
|
|
});
|
|
const updateJSON = await updateRes.json();
|
|
text.setText(I18n.localuser.tokenDisplay(updateJSON.token));
|
|
this.botTokens.set(appId, updateJSON.token);
|
|
if (this.perminfo.applications[appId]) {
|
|
this.perminfo.applications[appId] = updateJSON.token;
|
|
}
|
|
});
|
|
const text = form.addText(
|
|
I18n.localuser.tokenDisplay(
|
|
this.botTokens.has(appId) ? (this.botTokens.get(appId) as string) : "*****************",
|
|
),
|
|
);
|
|
const check = form.addOptions("", {noSubmit: true});
|
|
if (!this.perminfo.applications) {
|
|
this.perminfo.applications = {};
|
|
}
|
|
const checkbox = check.addCheckboxInput(I18n.localuser.saveToken(), () => {}, {
|
|
initState: !!this.perminfo.applications[appId],
|
|
});
|
|
checkbox.watchForChange((_) => {
|
|
if (_) {
|
|
if (this.botTokens.has(appId)) {
|
|
this.perminfo.applications[appId] = this.botTokens.get(appId);
|
|
} else {
|
|
alert(I18n.localuser.noToken());
|
|
checkbox.setState(false);
|
|
}
|
|
} else {
|
|
delete this.perminfo.applications[appId];
|
|
}
|
|
});
|
|
form.addButtonInput("", I18n.localuser.advancedBot(), () => {
|
|
const token = this.botTokens.get(appId);
|
|
if (token) {
|
|
//TODO check if this is actually valid or not
|
|
const botc = new Bot(bot as unknown as mainuserjson, token, this);
|
|
botc.settings();
|
|
}
|
|
});
|
|
form.addButtonInput("", I18n.localuser.botInviteCreate(), () => {
|
|
Bot.InviteMaker(appId, form, this.info);
|
|
});
|
|
}
|
|
updateSend() {
|
|
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
|
|
if (
|
|
(typebox.markdown.rawString && typebox.markdown.rawString !== "\n") ||
|
|
document.getElementById("pasteimage")?.children.length
|
|
) {
|
|
typebox.parentElement!.classList.remove("noConent");
|
|
} else {
|
|
typebox.parentElement!.classList.add("noConent");
|
|
}
|
|
}
|
|
//TODO make this an option
|
|
readonly autofillregex = Object.freeze(/(^|\s|\n)[@#:]([a-zA-Z0-9]*)$/i);
|
|
mdBox() {
|
|
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
|
|
const typeMd = typebox.markdown;
|
|
typeMd.owner = this;
|
|
typeMd.onUpdate = (str, pre) => {
|
|
this.search(document.getElementById("searchOptions") as HTMLDivElement, typeMd, str, pre);
|
|
this.updateSend();
|
|
};
|
|
}
|
|
async pinnedClick(rect: DOMRect) {
|
|
if (!this.focusChannel) return;
|
|
await this.focusChannel.pinnedClick(rect);
|
|
}
|
|
async makeStickerBox(rect: DOMRect) {
|
|
const sticker = await Sticker.stickerPicker(
|
|
-0 + rect.right - window.innerWidth,
|
|
-20 + rect.top - window.innerHeight,
|
|
this,
|
|
);
|
|
this.favorites.addStickerFreq(sticker.id);
|
|
console.log(sticker);
|
|
if (this.focusChannel) {
|
|
this.focusChannel.sendMessage("", {
|
|
embeds: [],
|
|
attachments: [],
|
|
sticker_ids: [sticker.id],
|
|
replyingto: this.focusChannel.replyingto,
|
|
});
|
|
this.focusChannel.replyingto = null;
|
|
}
|
|
}
|
|
|
|
async makeGifBox(rect: DOMRect) {
|
|
interface fullgif {
|
|
id: string;
|
|
title: string;
|
|
url: string;
|
|
src: string;
|
|
gif_src: string;
|
|
width: number;
|
|
height: number;
|
|
preview: string;
|
|
}
|
|
const menu = document.createElement("div");
|
|
menu.classList.add("flexttb", "gifmenu");
|
|
menu.style.bottom = window.innerHeight - rect.top + 15 + "px";
|
|
menu.style.right = window.innerWidth - rect.right + "px";
|
|
document.body.append(menu);
|
|
Contextmenu.keepOnScreen(menu);
|
|
Contextmenu.declareMenu(menu);
|
|
const trending = (await (
|
|
await fetch(
|
|
this.info.api +
|
|
"/gifs/trending?" +
|
|
new URLSearchParams([
|
|
["locale", I18n.lang],
|
|
["provider", this.selectedGifProfidor?.api_name!],
|
|
]),
|
|
{headers: this.headers},
|
|
)
|
|
).json()) as {
|
|
categories: {
|
|
name: string;
|
|
src: string;
|
|
}[];
|
|
gifs: [fullgif];
|
|
};
|
|
|
|
await fetch(
|
|
this.info.api +
|
|
"/gifs/trending-gifs?" +
|
|
new URLSearchParams([
|
|
["locale", I18n.lang],
|
|
["provider", this.selectedGifProfidor?.api_name!],
|
|
]),
|
|
{headers: this.headers},
|
|
);
|
|
const gifbox = document.createElement("div");
|
|
gifbox.classList.add("gifbox");
|
|
const search = document.createElement("input");
|
|
let gifs = gifbox;
|
|
const placeGifs = (
|
|
gifs: HTMLDivElement,
|
|
gifReturns: {src: string; width: number; height: number; title?: string}[],
|
|
) => {
|
|
const width = menu.getBoundingClientRect().width;
|
|
let left = 0;
|
|
let right = width < 370 ? Infinity : 0;
|
|
console.warn(right, width);
|
|
for (const gif of gifReturns) {
|
|
const div = document.createElement("div");
|
|
div.classList.add("gifBox");
|
|
const img = createImg(gif.src);
|
|
this.refreshIfNeeded(gif.src).then((url) => {
|
|
if (url === gif.src) return;
|
|
img.setSrcs(url);
|
|
});
|
|
if (gif.title) img.alt = gif.title;
|
|
const scale = gif.width / 196;
|
|
|
|
img.width = gif.width / scale;
|
|
img.height = gif.height / scale;
|
|
div.append(img);
|
|
|
|
if (left <= right) {
|
|
div.style.top = left + "px";
|
|
left += Math.ceil(img.height) + 10;
|
|
div.style.left = "5px";
|
|
} else {
|
|
div.style.top = right + "px";
|
|
right += Math.ceil(img.height) + 10;
|
|
div.style.left = "210px";
|
|
}
|
|
|
|
gifs.append(div);
|
|
|
|
div.onclick = () => {
|
|
if (this.focusChannel) {
|
|
this.focusChannel.sendMessage(gif.src, {
|
|
embeds: [],
|
|
attachments: [],
|
|
sticker_ids: [],
|
|
replyingto: this.focusChannel.replyingto,
|
|
});
|
|
menu.remove();
|
|
this.focusChannel.replyingto = null;
|
|
}
|
|
};
|
|
}
|
|
gifs.style.height = (right == Infinity ? left : Math.max(left, right)) + "px";
|
|
};
|
|
const searchBox = async () => {
|
|
gifs.remove();
|
|
if (search.value === "") {
|
|
menu.append(gifbox);
|
|
gifs = gifbox;
|
|
return;
|
|
}
|
|
gifs = document.createElement("div");
|
|
gifs.classList.add("gifbox");
|
|
menu.append(gifs);
|
|
const sValue = search.value;
|
|
const gifReturns = (await (
|
|
await fetch(
|
|
this.info.api +
|
|
"/gifs/search?" +
|
|
new URLSearchParams([
|
|
["locale", I18n.lang],
|
|
["q", sValue],
|
|
["limit", "500"],
|
|
["provider", this.selectedGifProfidor?.api_name!],
|
|
]),
|
|
{headers: this.headers},
|
|
)
|
|
).json()) as fullgif[];
|
|
if (sValue !== search.value) {
|
|
return;
|
|
}
|
|
placeGifs(
|
|
gifs,
|
|
gifReturns.map((gif) => {
|
|
return {src: gif.gif_src, width: gif.width, height: gif.height, title: gif.title};
|
|
}),
|
|
);
|
|
};
|
|
let last = "";
|
|
search.onkeyup = () => {
|
|
if (last === search.value) {
|
|
return;
|
|
}
|
|
last = search.value;
|
|
searchBox();
|
|
};
|
|
search.classList.add("searchGifBar");
|
|
//TODO fix this once we swap over
|
|
search.placeholder = I18n.searchGifs(this.selectedGifProfidor?.name!);
|
|
const favs = this.favorites.favoriteGifs();
|
|
if (favs.length) {
|
|
favs.forEach(async (_) => (_.src = await this.refreshIfNeeded(_.src)));
|
|
|
|
const div = document.createElement("div");
|
|
div.classList.add("gifPreviewBox");
|
|
const img = document.createElement("img");
|
|
img.src = favs[0].src;
|
|
img.src = await this.refreshIfNeeded(img.src);
|
|
const title = document.createElement("span");
|
|
title.textContent = I18n.favoriteGifs();
|
|
div.append(img, title);
|
|
gifbox.append(div);
|
|
div.onclick = (e) => {
|
|
e.stopImmediatePropagation();
|
|
search.remove();
|
|
gifs.remove();
|
|
gifs = document.createElement("div");
|
|
gifs.classList.add("gifbox");
|
|
|
|
const div = document.createElement("div");
|
|
div.classList.add("flexltr", "title");
|
|
|
|
const back = document.createElement("span");
|
|
back.classList.add("svg-leftArrow");
|
|
back.onclick = (e) => {
|
|
e.stopImmediatePropagation();
|
|
div.remove();
|
|
gifs.remove();
|
|
gifs = gifbox;
|
|
menu.append(search, gifbox);
|
|
};
|
|
|
|
const title = document.createElement("h3");
|
|
title.textContent = I18n.favoriteGifs();
|
|
div.append(back, title);
|
|
|
|
menu.append(div, gifs);
|
|
placeGifs(gifs, favs);
|
|
};
|
|
}
|
|
for (const category of trending.categories) {
|
|
const div = document.createElement("div");
|
|
div.classList.add("gifPreviewBox");
|
|
const img = document.createElement("img");
|
|
img.src = category.src;
|
|
const title = document.createElement("span");
|
|
title.textContent = category.name;
|
|
div.append(img, title);
|
|
gifbox.append(div);
|
|
div.onclick = (e) => {
|
|
e.stopImmediatePropagation();
|
|
search.value = category.name;
|
|
searchBox();
|
|
};
|
|
}
|
|
menu.append(search, gifbox);
|
|
search.focus();
|
|
}
|
|
async TBEmojiMenu(rect: DOMRect) {
|
|
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
|
|
const p = saveCaretPosition(typebox);
|
|
if (!p) return;
|
|
const original = MarkDown.getText();
|
|
|
|
const emoji = await Emoji.emojiPicker(
|
|
-0 + rect.right - window.innerWidth,
|
|
-20 + rect.top - window.innerHeight,
|
|
this,
|
|
);
|
|
this.favorites.addEmoji(emoji.id || (emoji.emoji as string));
|
|
p();
|
|
const md = typebox.markdown;
|
|
this.MDReplace(
|
|
emoji.id
|
|
? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`
|
|
: (emoji.emoji as string),
|
|
original,
|
|
md,
|
|
null,
|
|
);
|
|
}
|
|
MDReplace(
|
|
replacewith: string,
|
|
original: string,
|
|
typebox: MarkDown,
|
|
start: RegExp | null = this.autofillregex,
|
|
) {
|
|
let raw = typebox.rawString;
|
|
let empty = raw.length === 0;
|
|
raw = original !== "" ? raw.split(original)[1] : raw;
|
|
if (raw === undefined && !empty) return;
|
|
if (empty) {
|
|
raw = "";
|
|
}
|
|
raw = (start ? original.replace(start, "") : original) + " " + replacewith + raw;
|
|
|
|
typebox.txt = raw.split("");
|
|
const match = start ? original.match(start) : true;
|
|
if (match) {
|
|
typebox.boxupdate(
|
|
replacewith.length - (match === true ? 0 : match[0].length) + 1,
|
|
false,
|
|
original.length,
|
|
);
|
|
}
|
|
}
|
|
fileExtange!: (
|
|
files: Blob[],
|
|
html: WeakMap<Blob, HTMLElement>,
|
|
) => [Blob[], WeakMap<Blob, HTMLElement>];
|
|
MDSearchOptions(
|
|
options: MDSearchOption[],
|
|
original: string,
|
|
div: HTMLDivElement = document.getElementById("searchOptions") as HTMLDivElement,
|
|
typebox?: MarkDown,
|
|
) {
|
|
if (!div) return;
|
|
div.innerHTML = "";
|
|
let i = 0;
|
|
const htmloptions: HTMLSpanElement[] = [];
|
|
|
|
for (const {name, replace, icon, otherLogic} of options) {
|
|
if (i == 8) {
|
|
break;
|
|
}
|
|
i++;
|
|
const span = document.createElement("span");
|
|
htmloptions.push(span);
|
|
if (icon) {
|
|
span.append(icon);
|
|
}
|
|
|
|
span.append(name);
|
|
span.onclick = (e) => {
|
|
if (e) {
|
|
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();
|
|
}
|
|
if (!otherLogic?.() && typebox) {
|
|
this.MDReplace(replace, original, typebox);
|
|
}
|
|
div.innerHTML = "";
|
|
remove();
|
|
};
|
|
div.prepend(span);
|
|
}
|
|
const remove = () => {
|
|
if (div && div.innerHTML === "") {
|
|
this.keyup = () => false;
|
|
this.keydown = () => {};
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
if (htmloptions[0]) {
|
|
let curindex = 0;
|
|
let cur = htmloptions[0];
|
|
cur.classList.add("selected");
|
|
const cancel = new Set(["ArrowUp", "ArrowDown", "Enter", "Tab"]);
|
|
this.keyup = (event) => {
|
|
if (remove()) return false;
|
|
|
|
if (cancel.has(event.key)) {
|
|
switch (event.key) {
|
|
case "ArrowUp":
|
|
if (htmloptions[curindex + 1]) {
|
|
cur.classList.remove("selected");
|
|
curindex++;
|
|
cur = htmloptions[curindex];
|
|
cur.classList.add("selected");
|
|
}
|
|
break;
|
|
case "ArrowDown":
|
|
if (htmloptions[curindex - 1]) {
|
|
cur.classList.remove("selected");
|
|
curindex--;
|
|
cur = htmloptions[curindex];
|
|
cur.classList.add("selected");
|
|
}
|
|
break;
|
|
case "Enter":
|
|
case "Tab":
|
|
cur.click();
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
this.keydown = (event) => {
|
|
if (remove()) return;
|
|
if (cancel.has(event.key)) {
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
} else {
|
|
remove();
|
|
}
|
|
}
|
|
MDFindChannel(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
const maybe: [number, Channel][] = [];
|
|
if (this.focusGuild && this.focusGuild.id !== "@me") {
|
|
for (const channel of this.focusGuild.channels.filter((_) => _.visible)) {
|
|
const confidence = channel.similar(name);
|
|
if (confidence > 0) {
|
|
maybe.push([confidence, channel]);
|
|
}
|
|
}
|
|
}
|
|
maybe.sort((a, b) => b[0] - a[0]);
|
|
this.MDSearchOptions(
|
|
maybe.map((a) => {
|
|
return {name: "# " + a[1].name, replace: `<#${a[1].id}> `};
|
|
}),
|
|
original,
|
|
box,
|
|
typebox,
|
|
);
|
|
}
|
|
MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
let members: [Member | Role | User | "@everyone" | "@here", number][] = [];
|
|
if (this.focusGuild && name !== "everyone" && name !== "here") {
|
|
if (this.focusGuild.id === "@me") {
|
|
const dirrect = this.focusChannel as Group;
|
|
|
|
for (const user of dirrect.users) {
|
|
const rank = user.compare(name);
|
|
if (rank > 0) {
|
|
members.push([user, rank]);
|
|
}
|
|
}
|
|
} else {
|
|
for (const [_, member] of this.focusGuild.members) {
|
|
if (!this.focusChannel?.hasPermission("VIEW_CHANNEL", member)) continue;
|
|
const rank = member.compare(name);
|
|
if (rank > 0) {
|
|
members.push([member, rank]);
|
|
}
|
|
}
|
|
for (const role of this.focusGuild.roles.filter((_) => _.id !== this.focusGuild?.id)) {
|
|
const rank = role.compare(name);
|
|
if (rank > 0) {
|
|
members.push([role, rank]);
|
|
}
|
|
}
|
|
}
|
|
function similar(str2: string | null | undefined) {
|
|
if (!str2) return 0;
|
|
const strl = Math.max(name.length, 1);
|
|
if (str2.includes(name)) {
|
|
return strl / str2.length;
|
|
} else if (str2.toLowerCase().includes(name.toLowerCase())) {
|
|
return strl / str2.length / 1.2;
|
|
}
|
|
return 0;
|
|
}
|
|
const everyoneScore = similar("everyone");
|
|
if (everyoneScore) members.push(["@everyone", everyoneScore]);
|
|
const hereScore = similar("here");
|
|
if (hereScore) members.push(["@here", hereScore]);
|
|
}
|
|
members.sort((a, b) => b[1] - a[1]);
|
|
this.MDSearchOptions(
|
|
members.map(([member]) => {
|
|
const icon =
|
|
member instanceof Member
|
|
? member.user.buildpfp(member)
|
|
: member instanceof User
|
|
? member.buildpfp()
|
|
: undefined;
|
|
icon?.classList.add("pfpSearch");
|
|
return {
|
|
name: typeof member === "string" ? member : "@" + member.name,
|
|
replace:
|
|
member instanceof Role
|
|
? `<@&${member.id}> `
|
|
: typeof member === "string"
|
|
? member + " "
|
|
: `<@${member.id}> `,
|
|
icon: icon,
|
|
};
|
|
}),
|
|
original,
|
|
box,
|
|
typebox,
|
|
);
|
|
}
|
|
private MDGenID = 0;
|
|
MDFindMention(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
if (this.ws && this.focusGuild) {
|
|
const id = ++this.MDGenID;
|
|
this.MDFineMentionGen(name, original, box, typebox);
|
|
if (this.focusGuild.member_count <= this.focusGuild.members.size) return;
|
|
if (this.focusGuild.id !== "@me") {
|
|
this.focusGuild.searchMembers(8, name).then(async () => {
|
|
if (!typebox.rawString.startsWith(original)) return;
|
|
if (this.MDGenID === id) this.MDFineMentionGen(name, original, box, typebox);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
findEmoji(search: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
const emj = Emoji.searchEmoji(search, this, 10);
|
|
const map = emj.map(([emoji]): MDSearchOption => {
|
|
return {
|
|
name: emoji.name,
|
|
replace: emoji.id
|
|
? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`
|
|
: (emoji.emoji as string),
|
|
icon: emoji.getHTML(),
|
|
otherLogic: () => {
|
|
this.favorites.addEmoji(emoji.id || (emoji.emoji as string));
|
|
return false;
|
|
},
|
|
};
|
|
});
|
|
this.MDSearchOptions(map, original, box, typebox);
|
|
}
|
|
async findCommands(search: string, box: HTMLDivElement, md: MarkDown) {
|
|
const guild = this.focusGuild;
|
|
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 {
|
|
name: `/${elm.localizedName}`,
|
|
replace: "",
|
|
otherLogic: () => {
|
|
this.focusChannel?.startCommand(elm);
|
|
return true;
|
|
},
|
|
};
|
|
}),
|
|
"",
|
|
box,
|
|
md,
|
|
);
|
|
console.log(sorted, search);
|
|
}
|
|
search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) {
|
|
if (!pre) {
|
|
const match = str.match(this.autofillregex);
|
|
|
|
if (match) {
|
|
const trim = match[0].trim();
|
|
const [type, search] = [trim[0], trim.split(/@|#|:/)[1]];
|
|
switch (type) {
|
|
case "#":
|
|
this.MDFindChannel(search, str, box, md);
|
|
break;
|
|
case "@":
|
|
this.MDFindMention(search, str, box, md);
|
|
break;
|
|
case ":":
|
|
if (search.length >= 2) {
|
|
this.findEmoji(search, str, box, md);
|
|
} else {
|
|
this.MDSearchOptions([], "", box, md);
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
const command = str.match(/^\/((\s*[\w\d]+)*)$/);
|
|
if (command) {
|
|
const search = command[1];
|
|
this.findCommands(search, box, md);
|
|
}
|
|
}
|
|
box.innerHTML = "";
|
|
}
|
|
searching = false;
|
|
updateTranslations() {
|
|
const searchBox = document.getElementById("searchBox") as HTMLDivElement;
|
|
searchBox.style.setProperty("--hint-text", JSON.stringify(I18n.search.search()));
|
|
}
|
|
makePoll() {
|
|
const d = new Dialog(I18n.makePoll());
|
|
const opt = d.options;
|
|
const q = opt.addTextInput(I18n.poll.question(), () => {});
|
|
opt.addText(I18n.poll.answers());
|
|
const ansField = document.createElement("div");
|
|
ansField.classList.add("flexttb", "pollAnsM");
|
|
const answers = ["", ""] as string[];
|
|
const genAnswerField = () => {
|
|
ansField.textContent = "";
|
|
for (let i = 0; i < answers.length; i++) {
|
|
const si = i;
|
|
const div = document.createElement("div");
|
|
div.classList.add("flexltr");
|
|
const input = document.createElement("input");
|
|
input.type = "text";
|
|
input.value = answers[i];
|
|
input.onchange = () => {
|
|
answers[si] = input.value;
|
|
};
|
|
|
|
const del = document.createElement("span");
|
|
del.classList.add("svg-delete", "svgicon");
|
|
div.append(input, del);
|
|
del.onclick = () => {
|
|
answers.splice(si, 1);
|
|
genAnswerField();
|
|
};
|
|
ansField.append(div);
|
|
}
|
|
};
|
|
genAnswerField();
|
|
opt.addHTMLArea(ansField);
|
|
opt.addButtonInput("", I18n.poll.newAnswer(), () => {
|
|
answers.push("");
|
|
genAnswerField();
|
|
});
|
|
const hours = [1, 4, 8, 24, 72, 168, 336] as const;
|
|
const h = opt.addSelect(
|
|
I18n.poll.duration(),
|
|
() => {},
|
|
//@ts-ignore-error this is fine :P
|
|
hours.map((_) => I18n.poll.durCount[_ + ""]()),
|
|
{
|
|
defaultIndex: 3,
|
|
},
|
|
);
|
|
const c = opt.addCheckboxInput(I18n.poll.mult(), () => {});
|
|
opt.addButtonInput("", I18n.submit(), () => {
|
|
const chan = this.focusChannel;
|
|
if (!chan) return;
|
|
chan.sendMessage("", {
|
|
poll: {
|
|
question: {
|
|
text: q.value,
|
|
},
|
|
answers: answers.map((_) => ({poll_media: {text: _}})),
|
|
duration: hours[h.index] as number,
|
|
allow_multiselect: c.value,
|
|
},
|
|
});
|
|
d.hide();
|
|
});
|
|
d.show();
|
|
}
|
|
curSearch?: Symbol;
|
|
pollUpdateSubMap = new Map<string, (u: pollUpdateJson) => void>();
|
|
subToPollUpdate(m: string, func: ((u: pollUpdateJson) => void) | null) {
|
|
if (func) this.pollUpdateSubMap.set(m, func);
|
|
else this.pollUpdateSubMap.delete(m);
|
|
}
|
|
mSearch(query: string) {
|
|
const searchy = Symbol("search");
|
|
this.curSearch = searchy;
|
|
const p = new URLSearchParams("?");
|
|
this.searching = true;
|
|
p.set("content", query.trim());
|
|
p.set("sort_by", "timestamp");
|
|
p.set("sort_order", "desc");
|
|
let maxpage: undefined | number = undefined;
|
|
const sideDiv = document.getElementById("sideDiv");
|
|
const sideContainDiv = document.getElementById("sideContainDiv");
|
|
if (!sideDiv || !sideContainDiv) return;
|
|
let authorIds = [] as string[];
|
|
let mentionIds = [] as string[];
|
|
let channels = [] as Channel[];
|
|
const genPage = (page: number) => {
|
|
p.set("offset", page * 50 + "");
|
|
const guildSearch = this.focusGuild?.id !== "@me";
|
|
fetch(
|
|
this.info.api +
|
|
`${guildSearch ? `/guilds/${this.focusGuild?.id}` : `/channels/${this.focusChannel?.id}`}/messages/search/?` +
|
|
p.toString() +
|
|
(authorIds.length ? authorIds.map((_) => `&author_id=${_}`).join("") : "") +
|
|
(mentionIds.length ? mentionIds.map((_) => `&mentions=${_}`).join("") : "") +
|
|
(channels.length ? channels.map((_) => `&channel_id=${_.id}`).join("") : ""),
|
|
{
|
|
headers: this.headers,
|
|
},
|
|
)
|
|
.then((_) => _.json())
|
|
.then((json: {messages: [messagejson][]; total_results: number}) => {
|
|
if (this.curSearch !== searchy) {
|
|
return;
|
|
}
|
|
//FIXME total_results shall be ignored as it's known to be bad, spacebar bug.
|
|
const messages = json.messages
|
|
.map(([m]) => {
|
|
const c = this.channels.get(m.channel_id);
|
|
if (!c) return;
|
|
if (c.messages.get(m.id)) {
|
|
return c.messages.get(m.id);
|
|
}
|
|
return new Message(m, c, true);
|
|
})
|
|
.filter((_) => _ !== undefined);
|
|
sideDiv.innerHTML = "";
|
|
if (messages.length == 0 && page !== 0) {
|
|
maxpage = page - 1;
|
|
genPage(page - 1);
|
|
return;
|
|
} else if (messages.length !== 50) {
|
|
maxpage = page;
|
|
}
|
|
const sortBar = document.createElement("div");
|
|
sortBar.classList.add("flexltr", "sortBar");
|
|
|
|
const settingsB = document.createElement("button");
|
|
settingsB.textContent = I18n.search.settings();
|
|
settingsB.onclick = () => {
|
|
const d = new Dialog(I18n.search.settings());
|
|
const opt = d.options;
|
|
const b = p.get("max_id");
|
|
const before = opt.addDateInput(I18n.search.before(), () => {}, {
|
|
initText: b ? new Date(SnowFlake.stringToUnixTime(b)) : undefined,
|
|
});
|
|
before.onchange = (_) => {
|
|
if (before.dateValue) p.set("max_id", SnowFlake.DateToID(before.dateValue));
|
|
else p.delete("max_id");
|
|
console.log([...p.entries()], before.dateValue);
|
|
};
|
|
|
|
const a = p.get("min_id");
|
|
const after = opt.addDateInput(I18n.search.after(), () => {}, {
|
|
initText: a ? new Date(SnowFlake.stringToUnixTime(a)) : undefined,
|
|
});
|
|
after.onchange = (_) => {
|
|
if (after.dateValue) p.set("min_id", SnowFlake.DateToID(after.dateValue));
|
|
else p.delete("min_id");
|
|
console.log([...p.entries()], after.dateValue);
|
|
};
|
|
opt.addCheckboxInput(I18n.search.includensfw(), () => {}, {
|
|
initState: p.get("include_nsfw") !== "false",
|
|
}).onchange = (s) => {
|
|
if (s) p.delete("include_nsfw");
|
|
else p.set("include_nsfw", "false");
|
|
};
|
|
const userSearch = async (name: string, ids: string[]) => {
|
|
const g = this.focusGuild;
|
|
if (!g) return [];
|
|
g.searchMembers(8, name);
|
|
const members = [] as [User | Member, number][];
|
|
if (g.id === "@me") {
|
|
const dirrect = this.focusChannel as Group;
|
|
|
|
for (const user of dirrect.users) {
|
|
const rank = user.compare(name);
|
|
if (rank > 0) {
|
|
members.push([user, rank]);
|
|
}
|
|
}
|
|
} else {
|
|
for (const [_, member] of g.members) {
|
|
const rank = member.compare(name);
|
|
if (rank > 0) {
|
|
members.push([member, rank]);
|
|
}
|
|
}
|
|
}
|
|
const idSet = new Set(ids);
|
|
members.sort((a, b) => b[1] - a[1]);
|
|
|
|
return members
|
|
.filter((_) => !idSet.has(_[0].id))
|
|
.slice(0, 5)
|
|
.map(([_]) => {
|
|
return {value: _.id, name: _.name};
|
|
});
|
|
};
|
|
opt.addAsyncMultiSelect(I18n.search.authors(), () => {}, userSearch, {
|
|
defaultValues: authorIds
|
|
.map((id) => {
|
|
const g = this.focusGuild;
|
|
if (!g || g.id === "@me") {
|
|
return this.userMap.get(id);
|
|
} else {
|
|
return [...g.members.values()].find(({id: d}) => d === id);
|
|
}
|
|
})
|
|
.filter((_) => _ !== undefined)
|
|
.map((_) => ({value: _.id, name: _.name})),
|
|
}).onchange = (values: string[]) => {
|
|
authorIds = values;
|
|
};
|
|
|
|
opt.addAsyncMultiSelect(I18n.search.mentions(), () => {}, userSearch, {
|
|
defaultValues: mentionIds
|
|
.map((id) => {
|
|
const g = this.focusGuild;
|
|
if (!g || g.id === "@me") {
|
|
return this.userMap.get(id);
|
|
} else {
|
|
return [...g.members.values()].find(({id: d}) => d === id);
|
|
}
|
|
})
|
|
.filter((_) => _ !== undefined)
|
|
.map((_) => ({value: _.id, name: _.name})),
|
|
}).onchange = (values: string[]) => {
|
|
mentionIds = values;
|
|
};
|
|
if (this.focusGuild?.id !== "@me")
|
|
opt.addAsyncMultiSelect(
|
|
I18n.search.channels(),
|
|
() => {},
|
|
(name, ids) => {
|
|
const g = this.focusGuild;
|
|
if (!g) return [];
|
|
const c = g.channels.filter((_) => _.visible);
|
|
|
|
const maybe: [number, Channel][] = [];
|
|
|
|
for (const channel of c) {
|
|
const confidence = channel.similar(name);
|
|
if (confidence > 0) {
|
|
maybe.push([confidence, channel]);
|
|
}
|
|
}
|
|
|
|
maybe.sort((a, b) => b[0] - a[0]);
|
|
const idSet = new Set(ids);
|
|
|
|
return maybe
|
|
.filter((_) => !idSet.has(_[1].id))
|
|
.slice(0, 5)
|
|
.map(([_r, _]) => {
|
|
return {value: _.id, name: _.name};
|
|
});
|
|
},
|
|
{
|
|
defaultValues: channels.map((_) => ({name: _.name, value: _.id})),
|
|
},
|
|
).onchange = (values: string[]) => {
|
|
const g = this.focusGuild;
|
|
if (!g) return;
|
|
channels = values.map((id) => g.getChannel(id)).filter((_) => _ !== undefined);
|
|
};
|
|
d.onhide = () => {
|
|
genPage(0);
|
|
};
|
|
d.show();
|
|
};
|
|
|
|
const newB = document.createElement("button");
|
|
const old = document.createElement("button");
|
|
[newB.textContent, old.textContent] = [I18n.search.new(), I18n.search.old()];
|
|
old.onclick = () => {
|
|
p.set("sort_order", "asc");
|
|
deleteMessages();
|
|
genPage(0);
|
|
};
|
|
newB.onclick = () => {
|
|
p.set("sort_order", "desc");
|
|
deleteMessages();
|
|
genPage(0);
|
|
};
|
|
if (p.get("sort_order") === "asc") {
|
|
old.classList.add("selectedB");
|
|
} else {
|
|
newB.classList.add("selectedB");
|
|
}
|
|
|
|
const spaceElm = document.createElement("div");
|
|
spaceElm.classList.add("spaceElm");
|
|
|
|
sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, settingsB, newB, old);
|
|
|
|
sideDiv.append(sortBar);
|
|
|
|
sideContainDiv.classList.add("searchDiv");
|
|
let channel: Channel | undefined = undefined;
|
|
function deleteMessages() {
|
|
for (const elm of htmls) elm.remove();
|
|
}
|
|
const htmls: HTMLElement[] = [];
|
|
sideContainDiv.classList.remove("hideSearchDiv");
|
|
for (const message of messages) {
|
|
if (channel !== message.channel) {
|
|
channel = message.channel;
|
|
const h3 = document.createElement("h3");
|
|
h3.textContent = channel.name;
|
|
h3.classList.add("channelSTitle");
|
|
sideDiv.append(h3);
|
|
htmls.push(h3);
|
|
}
|
|
const html = message.buildhtml(undefined, true);
|
|
if (message.div) console.error(message.div);
|
|
html.addEventListener("click", async () => {
|
|
try {
|
|
sideContainDiv.classList.add("hideSearchDiv");
|
|
await message.channel.focus(message.id);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
});
|
|
sideDiv.append(html);
|
|
htmls.push(html);
|
|
}
|
|
if (messages.length === 0) {
|
|
const noMs = document.createElement("h3");
|
|
noMs.textContent = I18n.search.nofind();
|
|
sideDiv.append(noMs);
|
|
}
|
|
const bottombuttons = document.createElement("div");
|
|
bottombuttons.classList.add("flexltr", "searchNavButtons");
|
|
const next = document.createElement("button");
|
|
if (page == maxpage) next.disabled = true;
|
|
next.onclick = () => {
|
|
deleteMessages();
|
|
genPage(page + 1);
|
|
};
|
|
const prev = document.createElement("button");
|
|
prev.onclick = () => {
|
|
deleteMessages();
|
|
genPage(page - 1);
|
|
};
|
|
if (page == 0) prev.disabled = true;
|
|
[next.textContent, prev.textContent] = [I18n.search.next(), I18n.search.back()];
|
|
bottombuttons.append(prev, next);
|
|
sideDiv.append(bottombuttons);
|
|
sideDiv.scrollTo({top: 0, behavior: "instant"});
|
|
});
|
|
};
|
|
if (query === "") {
|
|
sideContainDiv.classList.remove("searchDiv");
|
|
sideContainDiv.classList.remove("hideSearchDiv");
|
|
sideDiv.innerHTML = "";
|
|
this.searching = false;
|
|
this.getSidePannel();
|
|
return;
|
|
}
|
|
genPage(0);
|
|
}
|
|
|
|
keydown: (event: KeyboardEvent) => unknown = () => {};
|
|
keyup: (event: KeyboardEvent) => boolean = () => false;
|
|
handleKeyUp(event: KeyboardEvent): boolean {
|
|
if (this.keyup(event)) {
|
|
return true;
|
|
}
|
|
if (event.key === "Escape") {
|
|
if (event.ctrlKey) {
|
|
this.focusGuild?.markAsRead();
|
|
} else {
|
|
this.focusChannel?.readbottom();
|
|
this.focusChannel?.goToBottom();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
//---------- resolving members code -----------
|
|
readonly waitingmembers = new Map<
|
|
string,
|
|
Map<string, (returns: memberjson | undefined) => void>
|
|
>();
|
|
readonly presences: Map<string, presencejson> = new Map();
|
|
static font?: FontFace;
|
|
static async loadFont() {
|
|
const prefs = getPreferences();
|
|
const fontName = prefs.emojiFont;
|
|
|
|
if (this.font) {
|
|
//TODO see when/if this can be removed
|
|
//@ts-ignore this is stupid. it's been here since 2020
|
|
document.fonts.delete(this.font);
|
|
}
|
|
|
|
const realname = this.fonts.find((_) => _[1] === fontName)?.[0];
|
|
if (realname) {
|
|
const font = new FontFace("emojiFont", `url("/emoji/${realname}")`);
|
|
await font.load();
|
|
console.error("Loaded font:", fontName, "/", realname);
|
|
//TODO see when/if this can be removed
|
|
//@ts-ignore this is stupid. it's been here since 2020
|
|
document.fonts.add(font);
|
|
console.log(font);
|
|
this.font = font;
|
|
}
|
|
}
|
|
static get fonts() {
|
|
const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
|
|
return [
|
|
[`NotoColorEmoji-Regular.${isFirefox ? "woff2" : "ttf"}`, "Noto Color Emoji"],
|
|
[`OpenMoji-color-glyf_colr_0.woff2`, "OpenMoji"],
|
|
[`Twemoji-16.0.1.${isFirefox ? "woff2" : "ttf"}`, "Twemoji"],
|
|
[`BlobmojiCompat.${isFirefox ? "woff2" : "ttf"}`, "Blobmoji"],
|
|
] as const;
|
|
}
|
|
readonly getMemberMap = new Map<string, Promise<Member | undefined>>();
|
|
async getMember(id: string, guildid: string): Promise<Member | undefined> {
|
|
const user = this.userMap.get(id);
|
|
const guild = this.guilds.get(guildid) as Guild;
|
|
if (user) {
|
|
const memb = user.members.get(guild);
|
|
if (memb) return memb;
|
|
}
|
|
const uid = id + "-" + guildid;
|
|
const prom = this.getMemberMap.get(uid);
|
|
if (prom) return prom;
|
|
const prom2 = new Promise<Member | undefined>(async (res) => {
|
|
const json = await this.resolvemember(id, guildid);
|
|
if (!json) {
|
|
res(undefined);
|
|
return;
|
|
}
|
|
res(Member.new(json, guild));
|
|
});
|
|
this.getMemberMap.set(uid, prom2);
|
|
return prom2;
|
|
}
|
|
memberLock = new PromiseLock();
|
|
async resolvemember(id: string, guildid: string): Promise<memberjson | undefined> {
|
|
if (guildid === "@me") {
|
|
return undefined;
|
|
}
|
|
const guild = this.guilds.get(guildid);
|
|
//TODO well, maybe think this over, I set the member count and whatnot, maybe for "large enough" guilds I return a member that's filled out blankly unless its needed to be fully resolved, added stuff to identify to make it work
|
|
const borked = false;
|
|
if (!guild || (borked && guild.member_count > 250)) {
|
|
const unlock = await this.memberLock.acquireLock();
|
|
try {
|
|
const req = await fetch(this.info.api + "/guilds/" + guildid + "/members/" + id, {
|
|
headers: this.headers,
|
|
});
|
|
if (req.status !== 200) {
|
|
return undefined;
|
|
}
|
|
return await req.json();
|
|
} catch {
|
|
return undefined;
|
|
} finally {
|
|
unlock();
|
|
}
|
|
}
|
|
let guildmap = this.waitingmembers.get(guildid);
|
|
if (!guildmap) {
|
|
guildmap = new Map();
|
|
this.waitingmembers.set(guildid, guildmap);
|
|
}
|
|
const promise: Promise<memberjson | undefined> = new Promise((res) => {
|
|
guildmap.set(id, res);
|
|
this.getmembers();
|
|
});
|
|
return await promise;
|
|
}
|
|
private readonly fetchingmembers = new Map<string, boolean>();
|
|
private readonly memberNonceMap = new Map<string, (r: [memberjson[], string[]]) => void>();
|
|
private readonly memberNonceBuild = new Map<string, [memberjson[], string[], number[]]>();
|
|
private readonly searchMap = new Map<
|
|
string,
|
|
(arg: {
|
|
chunk_index: number;
|
|
chunk_count: number;
|
|
nonce: string;
|
|
not_found?: string[];
|
|
members?: memberjson[];
|
|
presences: presencejson[];
|
|
}) => unknown
|
|
>();
|
|
async gotChunk(chunk: {
|
|
chunk_index: number;
|
|
chunk_count: number;
|
|
nonce: string;
|
|
not_found?: string[];
|
|
members?: memberjson[];
|
|
presences: presencejson[];
|
|
}) {
|
|
for (const thing of chunk.presences) {
|
|
if (thing.user) {
|
|
this.presences.set(thing.user.id, thing);
|
|
}
|
|
}
|
|
if (this.searchMap.has(chunk.nonce)) {
|
|
const func = this.searchMap.get(chunk.nonce);
|
|
this.searchMap.delete(chunk.nonce);
|
|
if (func) {
|
|
func(chunk);
|
|
return;
|
|
}
|
|
}
|
|
chunk.members ??= [];
|
|
const arr = this.memberNonceBuild.get(chunk.nonce);
|
|
if (!arr) return;
|
|
arr[0] = arr[0].concat(chunk.members);
|
|
if (chunk.not_found) {
|
|
arr[1] = chunk.not_found;
|
|
}
|
|
arr[2].push(chunk.chunk_index);
|
|
if (arr[2].length === chunk.chunk_count) {
|
|
this.memberNonceBuild.delete(chunk.nonce);
|
|
const func = this.memberNonceMap.get(chunk.nonce);
|
|
if (!func) return;
|
|
func([arr[0], arr[1]]);
|
|
this.memberNonceMap.delete(chunk.nonce);
|
|
}
|
|
}
|
|
async getmembers() {
|
|
const promise = new Promise((res) => {
|
|
setTimeout(res, 10);
|
|
});
|
|
await promise; //allow for more to be sent at once :P
|
|
if (this.ws) {
|
|
this.waitingmembers.forEach(async (value, guildid) => {
|
|
const keys = value.keys();
|
|
if (this.fetchingmembers.has(guildid)) {
|
|
return;
|
|
}
|
|
const build: string[] = [];
|
|
for (const key of keys) {
|
|
build.push(key);
|
|
if (build.length === 100) {
|
|
break;
|
|
}
|
|
}
|
|
if (!build.length) {
|
|
this.waitingmembers.delete(guildid);
|
|
return;
|
|
}
|
|
const promise: Promise<[memberjson[], string[]]> = new Promise((res) => {
|
|
const nonce = "" + Math.floor(Math.random() * 100000000000);
|
|
this.memberNonceMap.set(nonce, res);
|
|
this.memberNonceBuild.set(nonce, [[], [], []]);
|
|
if (!this.ws) return;
|
|
this.ws.send(
|
|
JSON.stringify({
|
|
op: 8,
|
|
d: {
|
|
user_ids: build,
|
|
guild_id: guildid,
|
|
limit: 100,
|
|
nonce,
|
|
presences: true,
|
|
},
|
|
}),
|
|
);
|
|
this.fetchingmembers.set(guildid, true);
|
|
});
|
|
const prom = await promise;
|
|
const data = prom[0];
|
|
for (const thing of data) {
|
|
if (value.has(thing.id)) {
|
|
const func = value.get(thing.id);
|
|
if (!func) {
|
|
value.delete(thing.id);
|
|
continue;
|
|
}
|
|
func(thing);
|
|
value.delete(thing.id);
|
|
}
|
|
}
|
|
for (const thing of prom[1]) {
|
|
if (value.has(thing)) {
|
|
const func = value.get(thing);
|
|
if (!func) {
|
|
value.delete(thing);
|
|
continue;
|
|
}
|
|
func(undefined);
|
|
value.delete(thing);
|
|
}
|
|
}
|
|
this.fetchingmembers.delete(guildid);
|
|
this.getmembers();
|
|
});
|
|
}
|
|
}
|
|
async pingEndpoint() {
|
|
const userInfo = getBulkInfo();
|
|
if (!userInfo.instances) userInfo.instances = {};
|
|
const wellknown = this.info.wellknown;
|
|
if (!userInfo.instances[wellknown]) {
|
|
const pingRes = await fetch(this.info.api + "/ping");
|
|
const pingJSON = await pingRes.json();
|
|
userInfo.instances[wellknown] = pingJSON;
|
|
localStorage.setItem("userinfos", JSON.stringify(userInfo));
|
|
}
|
|
this.instancePing = userInfo.instances[wellknown].instance;
|
|
|
|
this.pageTitle("Loading...");
|
|
}
|
|
pageTitle(channelName = "", guildName = "") {
|
|
(document.getElementById("channelname") as HTMLSpanElement).textContent = channelName;
|
|
(document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent =
|
|
channelName +
|
|
(guildName ? " | " + guildName : "") +
|
|
" | " +
|
|
this.instancePing.name +
|
|
" | Fermi";
|
|
}
|
|
async instanceStats() {
|
|
const dialog = new Dialog("");
|
|
dialog.options.addTitle(I18n.instanceStats.name(this.instancePing.name));
|
|
dialog.show();
|
|
const res = await fetch(this.info.api + "/policies/stats", {
|
|
headers: this.headers,
|
|
});
|
|
const json = await res.json();
|
|
dialog.options.addText(I18n.instanceStats.users(json.counts.user));
|
|
dialog.options.addText(I18n.instanceStats.servers(json.counts.guild));
|
|
dialog.options.addText(I18n.instanceStats.messages(json.counts.message));
|
|
dialog.options.addText(I18n.instanceStats.members(json.counts.members));
|
|
}
|
|
|
|
async refreshIfNeeded(url: string) {
|
|
const urlObj = new URL(url);
|
|
if (urlObj.host === new URL(this.info.cdn).host) {
|
|
if (urlObj.searchParams.get("ex")) {
|
|
if (Number.parseInt(urlObj.searchParams.get("ex") || "", 16) >= Date.now() - 5000) {
|
|
return url;
|
|
}
|
|
}
|
|
const newUrl = this.refreshURL(url);
|
|
newUrl.then((_) => (url = _));
|
|
return newUrl;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
private refreshTimeOut?: NodeJS.Timeout;
|
|
private urlsToRefresh: [string, (arg: string) => void][] = [];
|
|
refreshURL(url: string): Promise<string> {
|
|
if (!this.refreshTimeOut) {
|
|
this.refreshTimeOut = setTimeout(async () => {
|
|
const refreshes = this.urlsToRefresh;
|
|
this.urlsToRefresh = [];
|
|
delete this.refreshTimeOut;
|
|
const res = await fetch(this.info.api + "/attachments/refresh-urls", {
|
|
method: "POST",
|
|
body: JSON.stringify({attachment_urls: refreshes.map((_) => _[0])}),
|
|
headers: this.headers,
|
|
});
|
|
const body: {
|
|
refreshed_urls: string[];
|
|
} = await res.json();
|
|
let i = 0;
|
|
for (const url of body.refreshed_urls) {
|
|
refreshes[i][1](url);
|
|
i++;
|
|
}
|
|
}, 100);
|
|
}
|
|
return new Promise((res) => {
|
|
this.urlsToRefresh.push([url, res]);
|
|
});
|
|
}
|
|
getNotiVolume(): number {
|
|
const userinfos = getBulkInfo();
|
|
return userinfos.preferences.volume ?? 20;
|
|
}
|
|
setNotificationVolume(volume: number) {
|
|
const userinfos = getBulkInfo();
|
|
userinfos.preferences.volume = volume;
|
|
localStorage.setItem("userinfos", JSON.stringify(userinfos));
|
|
}
|
|
setNotificationSound(sound: string) {
|
|
const userinfos = getBulkInfo();
|
|
userinfos.preferences.notisound = sound;
|
|
localStorage.setItem("userinfos", JSON.stringify(userinfos));
|
|
}
|
|
playSound(name = this.getNotificationSound()) {
|
|
const volume = this.getNotiVolume();
|
|
if (this.play) {
|
|
const voice = this.play.tracks.includes(name);
|
|
if (voice) {
|
|
this.play.play(name, volume);
|
|
} else if (this.perminfo.sound && this.perminfo.sound.cSound) {
|
|
const audio = document.createElement("audio");
|
|
audio.volume = volume / 100;
|
|
audio.src = this.perminfo.sound.cSound;
|
|
audio.play().catch();
|
|
}
|
|
} else {
|
|
console.error("play object is missing");
|
|
}
|
|
}
|
|
getNotificationSound() {
|
|
const userinfos = getBulkInfo();
|
|
return userinfos.preferences.notisound;
|
|
}
|
|
}
|
|
Localuser.globalShortcuts.listen(document.body);
|
|
Localuser.initShortcuts();
|
|
export {Localuser};
|