mirror of
https://github.com/MathMan05/Fermi.git
synced 2026-09-17 08:05:01 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
+97
-57
@@ -92,7 +92,6 @@ export class Discovery {
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.classList.add("flexttb", "guildy", "messagecontainer");
|
||||
content.textContent = I18n.guild.loadingDiscovery();
|
||||
|
||||
scrollWrap.append(content);
|
||||
|
||||
@@ -100,79 +99,120 @@ export class Discovery {
|
||||
guildsButton.textContent = I18n.guild.guilds();
|
||||
guildsButton.classList.add("discoverButton", "selected");
|
||||
channels.append(guildsButton);
|
||||
const guilds = document.createElement("div");
|
||||
guilds.classList.add("discovery-guild-content");
|
||||
|
||||
const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", {
|
||||
headers: this.headers,
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log([...json.guilds], json.guilds);
|
||||
|
||||
content.innerHTML = "";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = I18n.guild.disoveryTitle(json.guilds.length + "");
|
||||
content.appendChild(title);
|
||||
|
||||
const guilds = document.createElement("div");
|
||||
guilds.id = "discovery-guild-content";
|
||||
const buttonRow = document.createElement("div");
|
||||
buttonRow.classList.add("flexltr");
|
||||
|
||||
json.guilds.forEach((guild: guildjson["properties"]) => {
|
||||
const content = document.createElement("div");
|
||||
const render = async (offset = 0) => {
|
||||
guilds.textContent = I18n.guild.loadingDiscovery();
|
||||
const limit = 50;
|
||||
const res = await fetch(
|
||||
`${this.info.api}/discoverable-guilds?limit=${limit}&offset=${offset}`,
|
||||
{
|
||||
headers: this.headers,
|
||||
},
|
||||
);
|
||||
|
||||
this.context.bindContextmenu(content, guild.id, () => {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("flexltr");
|
||||
const json = (await res.json()) as {guilds: guildjson["properties"][]; total: number};
|
||||
console.log([...json.guilds], json.guilds);
|
||||
guilds.textContent = "";
|
||||
|
||||
title.textContent = I18n.guild.disoveryTitle(json.total + "");
|
||||
|
||||
content.appendChild(guilds);
|
||||
|
||||
json.guilds.forEach((guild) => {
|
||||
const content = document.createElement("div");
|
||||
|
||||
this.context.bindContextmenu(content, guild.id, () => {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("flexltr");
|
||||
const img = this.getIconURL(guild);
|
||||
img.classList.add("icon");
|
||||
img.crossOrigin = "anonymous";
|
||||
|
||||
img.alt = "";
|
||||
div.appendChild(img);
|
||||
|
||||
const name = document.createElement("h3");
|
||||
name.textContent = guild.name;
|
||||
div.appendChild(name);
|
||||
return div;
|
||||
});
|
||||
content.classList.add("discovery-guild");
|
||||
const banner = this.getBannerURL(guild);
|
||||
if (banner) {
|
||||
banner.classList.add("banner");
|
||||
banner.crossOrigin = "anonymous";
|
||||
banner.alt = "";
|
||||
content.appendChild(banner);
|
||||
}
|
||||
|
||||
const nameContainer = document.createElement("div");
|
||||
nameContainer.classList.add("flex");
|
||||
const img = this.getIconURL(guild);
|
||||
img.classList.add("icon");
|
||||
img.crossOrigin = "anonymous";
|
||||
|
||||
img.alt = "";
|
||||
div.appendChild(img);
|
||||
nameContainer.appendChild(img);
|
||||
|
||||
const name = document.createElement("h3");
|
||||
name.textContent = guild.name;
|
||||
div.appendChild(name);
|
||||
return div;
|
||||
nameContainer.appendChild(name);
|
||||
content.appendChild(nameContainer);
|
||||
const desc = document.createElement("p");
|
||||
desc.textContent = guild.description;
|
||||
content.appendChild(desc);
|
||||
|
||||
content.addEventListener("click", async () => {
|
||||
let guildObj = this.localuser.guildids.get(guild.id);
|
||||
if (guildObj) {
|
||||
guildObj.loadGuild();
|
||||
guildObj.loadChannel();
|
||||
return;
|
||||
}
|
||||
if (await this.confirmJoin(guild)) {
|
||||
await this.join(guild);
|
||||
}
|
||||
});
|
||||
guilds.appendChild(content);
|
||||
});
|
||||
content.classList.add("discovery-guild");
|
||||
const banner = this.getBannerURL(guild);
|
||||
if (banner) {
|
||||
banner.classList.add("banner");
|
||||
banner.crossOrigin = "anonymous";
|
||||
banner.alt = "";
|
||||
content.appendChild(banner);
|
||||
|
||||
let switching = false;
|
||||
|
||||
buttonRow.textContent = "";
|
||||
|
||||
if (offset !== 0) {
|
||||
const back = document.createElement("button");
|
||||
back.textContent = I18n.search.back();
|
||||
buttonRow.append(back);
|
||||
back.onclick = () => {
|
||||
if (switching) return;
|
||||
switching = true;
|
||||
render(offset - limit);
|
||||
};
|
||||
}
|
||||
//TODO once https://codeberg.org/MelodyChat/Harmony/pulls/77 is merged this should be reverted to a < only, the === case means there is no more, though right now server side logic is incorrect.
|
||||
if (offset + json.guilds.length <= json.total) {
|
||||
const next = document.createElement("button");
|
||||
next.textContent = I18n.search.next();
|
||||
buttonRow.append(next);
|
||||
next.onclick = () => {
|
||||
if (switching) return;
|
||||
switching = true;
|
||||
render(offset + limit);
|
||||
};
|
||||
}
|
||||
content.append(buttonRow);
|
||||
};
|
||||
|
||||
const nameContainer = document.createElement("div");
|
||||
nameContainer.classList.add("flex");
|
||||
const img = this.getIconURL(guild);
|
||||
img.classList.add("icon");
|
||||
img.crossOrigin = "anonymous";
|
||||
|
||||
img.alt = "";
|
||||
nameContainer.appendChild(img);
|
||||
|
||||
const name = document.createElement("h3");
|
||||
name.textContent = guild.name;
|
||||
nameContainer.appendChild(name);
|
||||
content.appendChild(nameContainer);
|
||||
const desc = document.createElement("p");
|
||||
desc.textContent = guild.description;
|
||||
content.appendChild(desc);
|
||||
|
||||
content.addEventListener("click", async () => {
|
||||
let guildObj = this.localuser.guildids.get(guild.id);
|
||||
if (guildObj) {
|
||||
guildObj.loadGuild();
|
||||
guildObj.loadChannel();
|
||||
return;
|
||||
}
|
||||
if (await this.confirmJoin(guild)) {
|
||||
await this.join(guild);
|
||||
}
|
||||
});
|
||||
guilds.appendChild(content);
|
||||
});
|
||||
content.appendChild(guilds);
|
||||
render();
|
||||
}
|
||||
getIconURL(guild: guildjson["properties"]) {
|
||||
return createImg(
|
||||
|
||||
@@ -122,6 +122,7 @@ export class Favorites {
|
||||
this.gifs = deapClone(store.current.gifs);
|
||||
this.emojiFrecency = deapClone(store.current.emojiFrecency);
|
||||
this.emojiReactionFrecency = deapClone(store.current.emojiReactionFrecency);
|
||||
|
||||
this.guildAndChannelFrecency = deapClone(store.current.guildAndChannelFrecency);
|
||||
}
|
||||
saveLocal() {
|
||||
@@ -181,6 +182,11 @@ export class Favorites {
|
||||
headers: this.headers,
|
||||
});
|
||||
const res: {settings: Partial<favandfreq>} = await (await sat).json();
|
||||
//TODO remove this eventually
|
||||
delete this.emojiReactionFrecency["undefined"];
|
||||
delete this.store.current.emojiReactionFrecency["undefined"];
|
||||
if (res.settings.emojiReactionFrecency)
|
||||
delete res.settings.emojiReactionFrecency.emojis["undefined"];
|
||||
this.saveDifs(res.settings, save);
|
||||
}
|
||||
async setup() {
|
||||
|
||||
@@ -1074,6 +1074,26 @@ class Guild extends SnowFlake {
|
||||
|
||||
if (this.member.hasPermission("BAN_MEMBERS")) {
|
||||
const banMenu = settings.addButton(I18n.guild.bans());
|
||||
banMenu.addButtonInput("", I18n.guild.banId(), () => {
|
||||
const opt = banMenu.addSubOptions(I18n.guild.banId(), {noSubmit: true});
|
||||
const reason = opt.addTextInput(I18n.member["reason:"](), () => {});
|
||||
opt.addTextInput(I18n.guild.idSel(), async (id) => {
|
||||
const headers = structuredClone(this.headers);
|
||||
(headers as any)["x-audit-log-reason"] = reason.value;
|
||||
const ret = await fetch(`${this.info.api}/guilds/${this.id}/bans/${id}`, {
|
||||
method: "PUT",
|
||||
headers,
|
||||
});
|
||||
if (!ret.ok) {
|
||||
new Dialog((await ret.json()).message).show();
|
||||
return;
|
||||
}
|
||||
banMenu.returnFromSub();
|
||||
});
|
||||
opt.addButtonInput("", I18n.submit(), () => {
|
||||
opt.submit();
|
||||
});
|
||||
});
|
||||
const makeBanMenu = () => {
|
||||
const banDiv = document.createElement("div");
|
||||
const bansp = ProgessiveDecodeJSON<banObj[]>(
|
||||
|
||||
@@ -939,6 +939,27 @@ type messageCreateJson = {
|
||||
s: number;
|
||||
t: "MESSAGE_CREATE";
|
||||
};
|
||||
type userNote = {
|
||||
op: 0;
|
||||
t: "USER_NOTE_UPDATE";
|
||||
d: {
|
||||
note: string;
|
||||
id: string;
|
||||
};
|
||||
s: number;
|
||||
};
|
||||
export type pollUpdateJson = {
|
||||
op: 0;
|
||||
d: {
|
||||
user_id: string;
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
guild_id?: string;
|
||||
answer_id: number;
|
||||
};
|
||||
s: number;
|
||||
t: "MESSAGE_POLL_VOTE_ADD" | "MESSAGE_POLL_VOTE_REMOVE";
|
||||
};
|
||||
export interface relationJson {
|
||||
id: string;
|
||||
type: 0 | 1 | 2 | 3 | 4;
|
||||
@@ -1030,6 +1051,7 @@ type wsjson =
|
||||
heartbeat_interval: number;
|
||||
};
|
||||
}
|
||||
| userNote
|
||||
| {
|
||||
op: 0;
|
||||
t: "MESSAGE_REACTION_ADD";
|
||||
|
||||
+158
-5
@@ -47,6 +47,7 @@ import {getDeveloperSettings, setDeveloperSettings} from "./utils/storage/devSet
|
||||
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";
|
||||
type traceObj = {
|
||||
micros: number;
|
||||
calls?: (string | traceObj)[];
|
||||
@@ -768,6 +769,11 @@ class Localuser {
|
||||
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;
|
||||
@@ -4359,7 +4365,7 @@ class Localuser {
|
||||
MDFindChannel(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
||||
const maybe: [number, Channel][] = [];
|
||||
if (this.lookingguild && this.lookingguild.id !== "@me") {
|
||||
for (const channel of this.lookingguild.channels) {
|
||||
for (const channel of this.lookingguild.channels.filter((_) => _.visible)) {
|
||||
const confidence = channel.similar(name);
|
||||
if (confidence > 0) {
|
||||
maybe.push([confidence, channel]);
|
||||
@@ -4539,11 +4545,23 @@ class Localuser {
|
||||
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 + "");
|
||||
fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), {
|
||||
headers: this.headers,
|
||||
})
|
||||
const guildSearch = this.lookingguild?.id !== "@me";
|
||||
fetch(
|
||||
this.info.api +
|
||||
`${guildSearch ? `/guilds/${this.lookingguild?.id}` : `/channels/${this.channelfocus?.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) {
|
||||
@@ -4571,6 +4589,141 @@ class Localuser {
|
||||
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.lookingguild;
|
||||
if (!g) return [];
|
||||
g.searchMembers(8, name);
|
||||
const members = [] as [User | Member, number][];
|
||||
if (g.id === "@me") {
|
||||
const dirrect = this.channelfocus 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.lookingguild;
|
||||
if (!g || g.id === "@me") {
|
||||
return this.userMap.get(id);
|
||||
} else {
|
||||
return [...g.members].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.lookingguild;
|
||||
if (!g || g.id === "@me") {
|
||||
return this.userMap.get(id);
|
||||
} else {
|
||||
return [...g.members].find(({id: d}) => d === id);
|
||||
}
|
||||
})
|
||||
.filter((_) => _ !== undefined)
|
||||
.map((_) => ({value: _.id, name: _.name})),
|
||||
}).onchange = (values: string[]) => {
|
||||
mentionIds = values;
|
||||
};
|
||||
if (this.lookingguild?.id !== "@me")
|
||||
opt.addAsyncMultiSelect(
|
||||
I18n.search.channels(),
|
||||
() => {},
|
||||
(name, ids) => {
|
||||
const g = this.lookingguild;
|
||||
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.lookingguild;
|
||||
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()];
|
||||
@@ -4593,7 +4746,7 @@ class Localuser {
|
||||
const spaceElm = document.createElement("div");
|
||||
spaceElm.classList.add("spaceElm");
|
||||
|
||||
sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, newB, old);
|
||||
sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, settingsB, newB, old);
|
||||
|
||||
sideDiv.append(sortBar);
|
||||
|
||||
|
||||
@@ -355,7 +355,11 @@ class Message extends SnowFlake {
|
||||
|
||||
let reactiontxt: string;
|
||||
if (emoji instanceof Emoji) {
|
||||
reactiontxt = `${emoji.name}:${emoji.id}`;
|
||||
if (emoji.id) {
|
||||
reactiontxt = `${emoji.name}:${emoji.id}`;
|
||||
} else {
|
||||
reactiontxt = encodeURIComponent(emoji.name);
|
||||
}
|
||||
} else {
|
||||
reactiontxt = encodeURIComponent(emoji);
|
||||
}
|
||||
|
||||
+172
-1
@@ -187,6 +187,21 @@ class TextInput implements OptionsElement<string> {
|
||||
}
|
||||
}
|
||||
class DateInput extends TextInput {
|
||||
dateValue: Date | null;
|
||||
constructor(
|
||||
label: string,
|
||||
onSubmit: (str: string) => void,
|
||||
owner: Options,
|
||||
{initText = "" as string | Date} = {},
|
||||
) {
|
||||
let initDate: DateInput["dateValue"] = null;
|
||||
if (initText instanceof Date) {
|
||||
initDate = initText;
|
||||
initText = "";
|
||||
}
|
||||
super(label, onSubmit, owner, {initText});
|
||||
this.dateValue = initDate;
|
||||
}
|
||||
generateHTML(): HTMLDivElement {
|
||||
const div = document.createElement("div");
|
||||
const span = document.createElement("span");
|
||||
@@ -195,11 +210,20 @@ class DateInput extends TextInput {
|
||||
const input = document.createElement("input");
|
||||
input.value = this.value;
|
||||
input.type = "date";
|
||||
if (this.dateValue) input.valueAsDate = this.dateValue;
|
||||
input.oninput = this.onChange.bind(this);
|
||||
this.input = new WeakRef(input);
|
||||
div.append(input);
|
||||
return div;
|
||||
}
|
||||
onChange() {
|
||||
const input = this.input.deref();
|
||||
if (input) {
|
||||
const value = input.valueAsDate;
|
||||
this.dateValue = value;
|
||||
}
|
||||
super.onChange();
|
||||
}
|
||||
}
|
||||
class SettingsMDText implements OptionsElement<void> {
|
||||
readonly onSubmit!: (str: string) => void;
|
||||
@@ -412,7 +436,134 @@ export class ColorInput implements OptionsElement<string> {
|
||||
this.onSubmit(this.colorContent);
|
||||
}
|
||||
}
|
||||
interface searchRes {
|
||||
value: string;
|
||||
name: string;
|
||||
}
|
||||
class AsyncMultiSelect implements OptionsElement<string[]> {
|
||||
readonly label: string;
|
||||
readonly owner: Options;
|
||||
readonly onSubmit: (str: string[]) => void;
|
||||
select!: WeakRef<HTMLSelectElement>;
|
||||
searchFunc: (term: string, cur: string[]) => Promise<searchRes[]> | searchRes[];
|
||||
value = [] as string[];
|
||||
nmap = [] as string[];
|
||||
constructor(
|
||||
label: string,
|
||||
onSubmit: (str: string[]) => void,
|
||||
selections: AsyncMultiSelect["searchFunc"],
|
||||
owner: Options,
|
||||
{defaultValues = []}: {defaultValues: searchRes[]} = {defaultValues: []},
|
||||
) {
|
||||
this.label = label;
|
||||
this.value = defaultValues.map((_) => _.value);
|
||||
this.nmap = defaultValues.map((_) => _.name);
|
||||
this.owner = owner;
|
||||
this.onSubmit = onSubmit;
|
||||
this.searchFunc = selections;
|
||||
}
|
||||
generateHTML() {
|
||||
const div = document.createElement("div");
|
||||
const span = document.createElement("span");
|
||||
span.textContent = this.label;
|
||||
div.append(span);
|
||||
const s = document.createElement("div");
|
||||
s.classList.add("amsCont");
|
||||
const genArea = () => {
|
||||
s.textContent = "";
|
||||
let i = 0;
|
||||
for (const name of this.nmap) {
|
||||
const si = i++;
|
||||
const elm = document.createElement("div");
|
||||
elm.classList.add("amsElm", "flexltr");
|
||||
|
||||
const x = document.createElement("span");
|
||||
x.classList.add("svgicon", "svg-plainx");
|
||||
x.onclick = () => {
|
||||
this.value.splice(si, 1);
|
||||
this.nmap.splice(si, 1);
|
||||
genArea();
|
||||
this.onchange(this.value);
|
||||
};
|
||||
const nspan = document.createElement("span");
|
||||
nspan.textContent = name;
|
||||
|
||||
elm.append(nspan, x);
|
||||
|
||||
s.append(elm);
|
||||
}
|
||||
const addbg = document.createElement("div");
|
||||
addbg.classList.add("addbg");
|
||||
const add = document.createElement("span");
|
||||
add.classList.add("svgicon", "svg-plus");
|
||||
addbg.append(add);
|
||||
s.append(addbg);
|
||||
add.onclick = (e) => {
|
||||
this.popUpSearchBox(e.x, e.y, () => {
|
||||
genArea();
|
||||
this.onchange(this.value);
|
||||
});
|
||||
};
|
||||
};
|
||||
genArea();
|
||||
div.append(s);
|
||||
return div;
|
||||
}
|
||||
async popUpSearchBox(x: number, y: number, done: () => void) {
|
||||
const searchBox = document.createElement("div");
|
||||
searchBox.style.top = y + "px";
|
||||
searchBox.style.left = x + "px";
|
||||
searchBox.classList.add("amsBox");
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
|
||||
const reses = document.createElement("div");
|
||||
reses.classList.add("flexttb", "reses");
|
||||
searchBox.append(input, reses);
|
||||
let opts = [] as searchRes[];
|
||||
const search = async () => {
|
||||
const res = await this.searchFunc(input.value || "", this.value);
|
||||
reses.textContent = "";
|
||||
opts = res;
|
||||
for (const opt of opts) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = opt.name;
|
||||
span.onmousedown = () => {
|
||||
removeAni(searchBox);
|
||||
this.value.push(opt.value);
|
||||
this.nmap.push(opt.name);
|
||||
done();
|
||||
};
|
||||
reses.append(span);
|
||||
}
|
||||
input.onblur = async () => {
|
||||
removeAni(searchBox);
|
||||
};
|
||||
};
|
||||
|
||||
input.onkeyup = (e) => {
|
||||
if (e.key === "Enter" && opts[0]) {
|
||||
removeAni(searchBox);
|
||||
this.value.push(opts[0].value);
|
||||
this.nmap.push(opts[0].name);
|
||||
done();
|
||||
} else {
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
search();
|
||||
document.body.append(searchBox);
|
||||
input.focus();
|
||||
}
|
||||
submit() {
|
||||
this.onSubmit(this.value);
|
||||
}
|
||||
onchange: (str: string[]) => void = (_) => {};
|
||||
watchForChange(func: (str: string[]) => void) {
|
||||
this.onchange = func;
|
||||
}
|
||||
}
|
||||
class SelectInput implements OptionsElement<number> {
|
||||
readonly label: string;
|
||||
readonly owner: Options;
|
||||
@@ -869,6 +1020,7 @@ class Dialog {
|
||||
this.float = new Float(name, {ltr, noSubmit});
|
||||
this.above = goAbove;
|
||||
}
|
||||
onhide = () => {};
|
||||
show(hideOnClick = true) {
|
||||
const background = document.createElement("div");
|
||||
background.classList.add("background");
|
||||
@@ -883,6 +1035,7 @@ class Dialog {
|
||||
background.onclick = (_) => {
|
||||
if (hideOnClick && _.target === background) {
|
||||
removeAni(background);
|
||||
this.onhide();
|
||||
}
|
||||
};
|
||||
background.tabIndex = 0;
|
||||
@@ -890,6 +1043,7 @@ class Dialog {
|
||||
background.onkeydown = (e) => {
|
||||
if (e.key === "Escape" && hideOnClick) {
|
||||
removeAni(background);
|
||||
this.onhide();
|
||||
}
|
||||
};
|
||||
return center;
|
||||
@@ -1177,6 +1331,19 @@ class Options implements OptionsElement<void> {
|
||||
this.generate(select);
|
||||
return select;
|
||||
}
|
||||
addAsyncMultiSelect(
|
||||
label: string,
|
||||
onSubmit: (str: string[]) => void,
|
||||
selections: AsyncMultiSelect["searchFunc"],
|
||||
{defaultValues = [] as searchRes[]} = {},
|
||||
) {
|
||||
const select = new AsyncMultiSelect(label, onSubmit, selections, this, {
|
||||
defaultValues,
|
||||
});
|
||||
this.options.push(select);
|
||||
this.generate(select);
|
||||
return select;
|
||||
}
|
||||
addImageInput(
|
||||
label: string,
|
||||
onSubmit: (files: FileList | null) => void,
|
||||
@@ -1193,7 +1360,11 @@ class Options implements OptionsElement<void> {
|
||||
this.generate(FI);
|
||||
return FI;
|
||||
}
|
||||
addDateInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) {
|
||||
addDateInput(
|
||||
label: string,
|
||||
onSubmit: (str: string | undefined) => void,
|
||||
{initText = "" as string | Date} = {},
|
||||
) {
|
||||
const textInput = new DateInput(label, onSubmit, this, {
|
||||
initText,
|
||||
});
|
||||
|
||||
@@ -13,5 +13,8 @@ abstract class SnowFlake {
|
||||
throw new Error(`The ID is corrupted, it's ${str} when it should be some number.`);
|
||||
}
|
||||
}
|
||||
static DateToID(date: Date) {
|
||||
return ((BigInt(+date) - 1420070400000n) << 22n).toString();
|
||||
}
|
||||
}
|
||||
export {SnowFlake};
|
||||
|
||||
+66
-1
@@ -78,6 +78,10 @@ body {
|
||||
div {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.pfp {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
.templateMiniBox {
|
||||
display: flex;
|
||||
@@ -183,6 +187,42 @@ body {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
.amsCont {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.addbg {
|
||||
background: #0000006e;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.svg-plus {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.amsElm {
|
||||
height: 18px;
|
||||
background: #00000061;
|
||||
border-radius: 4px;
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
.svg-plainx {
|
||||
margin-left: 10px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.sessionDiv {
|
||||
padding: 8px;
|
||||
background: #00000040;
|
||||
@@ -221,6 +261,15 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.reses {
|
||||
span {
|
||||
background: #0000007a;
|
||||
margin: 3px;
|
||||
padding: 3px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.actConnectionDiv {
|
||||
background: #0000005c;
|
||||
padding: 6px;
|
||||
@@ -377,6 +426,9 @@ body {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.userNote {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
#player {
|
||||
flex-grow: 0;
|
||||
|
||||
@@ -3851,7 +3903,7 @@ fieldset input[type="radio"] {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#discovery-guild-content {
|
||||
.discovery-guild-content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
@@ -5055,3 +5107,16 @@ img.error::after {
|
||||
max-height: 400px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.amsBox {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
padding: 5px;
|
||||
background: var(--primary-bg);
|
||||
box-shadow: 0px 0px 3px 1px black;
|
||||
border-radius: 6px;
|
||||
&.removeElm {
|
||||
animation-duration: 0.2s;
|
||||
animation-name: context-fade-out;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,28 +124,24 @@
|
||||
--primary-hover: color-mix(in srgb, #373737 68%, var(--accent-color));
|
||||
--primary-text: #ebebeb;
|
||||
--primary-text-soft: #ebebebb8;
|
||||
|
||||
--secondary-bg: color-mix(in srgb, #222222 72%, var(--accent-color));
|
||||
--secondary-hover: color-mix(in srgb, #222222 65%, var(--accent-color));
|
||||
--folder-bg: color-mix(in srgb, var(--primary-text-soft) 70%, var(--accent-color));
|
||||
|
||||
--servers-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color));
|
||||
--channels-bg: color-mix(in srgb, #292929 68%, var(--accent-color));
|
||||
--channel-selected: color-mix(in srgb, #555555 65%, var(--accent-color));
|
||||
--typebox-bg: color-mix(in srgb, #666666 60%, var(--accent-color));
|
||||
|
||||
--button-bg: color-mix(in srgb, #777777 56%, var(--accent-color));
|
||||
--button-hover: color-mix(in srgb, #585858 58%, var(--accent-color));
|
||||
|
||||
--spoiler: color-mix(in srgb, #101010 72%, var(--accent-color));
|
||||
--link: color-mix(in srgb, #99ccff 75%, var(--accent-color));
|
||||
|
||||
--black: color-mix(in srgb, #000000 90%, var(--accent-color));
|
||||
--icon: color-mix(in srgb, #ffffff, var(--accent-color));
|
||||
--dock-bg: color-mix(in srgb, #171717 68%, var(--accent-color));
|
||||
--spoiler-hover: color-mix(in srgb, #111111 80%, var(--accent-color));
|
||||
--card-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color));
|
||||
--spoiler-bg: #000000;
|
||||
--reaction-reacted-bg: color-mix(in srgb, var(--reaction-bg) 80%, white);
|
||||
}
|
||||
|
||||
/* Optional Variables */
|
||||
|
||||
+36
-1
@@ -1173,6 +1173,33 @@ class User extends SnowFlake {
|
||||
}
|
||||
}
|
||||
}
|
||||
note?: string;
|
||||
async saveNotes(note: string) {
|
||||
this.note = note;
|
||||
await fetch(`${this.info.api}/users/@me/notes/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({note}),
|
||||
});
|
||||
}
|
||||
async getNotes() {
|
||||
try {
|
||||
if (this.note === undefined) {
|
||||
this.note =
|
||||
(
|
||||
await (
|
||||
await fetch(`${this.info.api}/users/@me/notes/${this.id}`, {
|
||||
method: "GET",
|
||||
headers: this.headers,
|
||||
})
|
||||
).json()
|
||||
).note || "";
|
||||
}
|
||||
} catch {
|
||||
this.note = "";
|
||||
}
|
||||
return this.note as string;
|
||||
}
|
||||
async fullProfile(guild: Guild | null | Member = null) {
|
||||
console.log(guild);
|
||||
const membres = (async () => {
|
||||
@@ -1380,6 +1407,14 @@ class User extends SnowFlake {
|
||||
document.body.append(background);
|
||||
background.append(div);
|
||||
console.log(background);
|
||||
const notes = buttons.add(I18n.profile.notes());
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.classList.add("userNote");
|
||||
notes.addHTMLArea(textArea);
|
||||
this.getNotes().then((_) => (textArea.textContent = _));
|
||||
textArea.onblur = () => {
|
||||
this.saveNotes(textArea.value);
|
||||
};
|
||||
(async () => {
|
||||
const high = await this.highInfo();
|
||||
const mut = buttons.add(I18n.profile.mut());
|
||||
@@ -1587,7 +1622,7 @@ class User extends SnowFlake {
|
||||
friendSpan.classList.add("svg-hasfriend");
|
||||
}
|
||||
};
|
||||
if (this !== this.localuser.user && !this.bot) {
|
||||
if (this !== this.localuser.user && !this.bot && x !== -1) {
|
||||
friendDiv.append(friendSpan);
|
||||
div.append(friendDiv);
|
||||
updateIcon();
|
||||
|
||||
+12
-2
@@ -320,6 +320,8 @@
|
||||
"banner:": "Banner:",
|
||||
"bans": "Bans",
|
||||
"boostMessage?": "Send a message when someone boosts your guild!",
|
||||
"banId":"Ban by ID",
|
||||
"idSel":"ID:",
|
||||
"community": "Community",
|
||||
"confirmDelete": "Are you sure you want to delete $1?",
|
||||
"confirmLeave": "Are you sure you want to leave?",
|
||||
@@ -773,7 +775,8 @@
|
||||
"mut": "Mutual guilds",
|
||||
"mutFriends": "Mutual friends",
|
||||
"permInfo": "Permissions",
|
||||
"userInfo": "User info"
|
||||
"userInfo": "User info",
|
||||
"notes":"User notes"
|
||||
},
|
||||
"profileColor": "Profile color",
|
||||
"pronouns": "Pronouns:",
|
||||
@@ -825,7 +828,14 @@
|
||||
"nofind": "There seems to be no messages that match your search, maybe trying broadening your search to try and find what you want",
|
||||
"old": "Old",
|
||||
"page": "Page $1",
|
||||
"search": "Search"
|
||||
"search": "Search",
|
||||
"settings":"Search Settings",
|
||||
"before":"Before:",
|
||||
"after":"After:",
|
||||
"includensfw":"Include NSFW:",
|
||||
"authors":"Authors:",
|
||||
"mentions":"Mentions:",
|
||||
"channels":"Channels:"
|
||||
},
|
||||
"searchGifs": "Search $1",
|
||||
"settings": {
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
"guild_booster_lvl6": "부스트된 길드",
|
||||
"guild_booster_lvl7": "부스트된 길드",
|
||||
"guild_booster_lvl8": "부스트된 길드",
|
||||
"guild_booster_lvl9": "잠시동안 부스트된 길드"
|
||||
"guild_booster_lvl9": "잠시동안 부스트된 길드",
|
||||
"hypesquad": "Vibesquad [자리 표시자]"
|
||||
},
|
||||
"blankMessage": "빈 메시지",
|
||||
"blog": {
|
||||
@@ -145,6 +146,9 @@
|
||||
"title": "이모티콘",
|
||||
"upload": "이모티콘 업로드"
|
||||
},
|
||||
"folder": {
|
||||
"name": "폴더 이름:"
|
||||
},
|
||||
"friends": {
|
||||
"addfriend": "친구 추가",
|
||||
"addfriendpromt": "사용자 이름으로 친구 추가:",
|
||||
@@ -470,6 +474,7 @@
|
||||
}
|
||||
},
|
||||
"sticker": {
|
||||
"name": "이름:",
|
||||
"upload": "스티커 업로드"
|
||||
},
|
||||
"submit": "제출",
|
||||
@@ -496,6 +501,9 @@
|
||||
"unblock": "사용자 차단 해제",
|
||||
"viewProfile": "프로필 보기"
|
||||
},
|
||||
"webauth": {
|
||||
"keyname": "키 이름:"
|
||||
},
|
||||
"webhook": "웹훅",
|
||||
"webhooks": {
|
||||
"EnterWebhookName": "웹훅 이름 입력",
|
||||
|
||||
@@ -827,7 +827,14 @@
|
||||
"nofind": "Er zijn er geen berichten gevonden met uw zoekopdracht. Misschien vind u wat u zoekt als u de zoekopdracht verbreedt.",
|
||||
"old": "Oud",
|
||||
"page": "Pagina $1",
|
||||
"search": "Zoeken"
|
||||
"search": "Zoeken",
|
||||
"settings": "Zoekinstellingen",
|
||||
"before": "Vóór:",
|
||||
"after": "Na:",
|
||||
"includensfw": "NSFW opnemen:",
|
||||
"authors": "Auterurs:",
|
||||
"mentions": "Vermeldingen:",
|
||||
"channels": "Kanalen:"
|
||||
},
|
||||
"searchGifs": "Zoeken in $1",
|
||||
"settings": {
|
||||
|
||||
Reference in New Issue
Block a user