Merge remote-tracking branch 'upstream/main'

This commit is contained in:
ygg2
2026-05-22 08:02:13 -04:00
14 changed files with 615 additions and 75 deletions
+97 -57
View File
@@ -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(
+6
View File
@@ -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() {
+20
View File
@@ -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[]>(
+22
View File
@@ -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
View File
@@ -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);
+5 -1
View File
@@ -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
View File
@@ -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,
});
+3
View File
@@ -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
View File
@@ -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;
}
}
+1 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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": {
+9 -1
View File
@@ -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": "웹훅 이름 입력",
+8 -1
View File
@@ -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": {