first thread commits

This commit is contained in:
MathMan05 committed 2026-02-03 23:55:17 -06:00
1 parent 2c05b6bbd0
commit 0737279146
8 files changed
+348 -43

No files matched your search

+74 -13
View File
@@ -16,6 +16,7 @@ import {
mute_config,
readyjson,
startTypingjson,
threadMember,
} from "./jsontypes.js";
import {MarkDown} from "./markdown.js";
import {Member} from "./member.js";
@@ -120,6 +121,35 @@ class Channel extends SnowFlake {
},
);
this.contextmenu.addSeperator();
this.contextmenu.addButton(
() => I18n.threads.leave(),
function () {
fetch(this.info.api + "/channels/" + this.id + "/thread-members/@me", {
method: "DELETE",
headers: this.headers,
});
},
{
visible: function () {
return !!this.member;
},
},
);
this.contextmenu.addButton(
() => I18n.threads.join(),
function () {
fetch(this.info.api + "/channels/" + this.id + "/thread-members/@me", {
method: "POST",
headers: this.headers,
});
},
{
visible: function () {
return !this.member;
},
},
);
this.contextmenu.addSeperator();
//TODO notifcations icon
this.contextmenu.addButton(
() => I18n.guild.notifications(),
@@ -501,6 +531,7 @@ class Channel extends SnowFlake {
);
}
last_pin_timestamp?: string;
member?: threadMember;
constructor(json: channeljson | -1, owner: Guild, id: string = json === -1 ? "" : json.id) {
super(id);
this.idToNext = owner.localuser.idToNext;
@@ -513,6 +544,7 @@ class Channel extends SnowFlake {
if (json === -1) {
return;
}
this.member = json.member;
this.rate_limit_per_user = json.rate_limit_per_user || 0;
this.editing;
this.type = json.type;
@@ -527,7 +559,7 @@ class Channel extends SnowFlake {
this.guild_id = json.guild_id;
this.permission_overwrites = new Map();
this.permission_overwritesar = [];
for (const thing of json.permission_overwrites) {
for (const thing of json.permission_overwrites || []) {
if (!this.permission_overwrites.has(thing.id)) {
//either a bug in the server requires this, or the API is cursed
this.permission_overwrites.set(thing.id, new Permissions(thing.allow, thing.deny));
@@ -560,6 +592,11 @@ class Channel extends SnowFlake {
}
this.setUpInfiniteScroller();
this.perminfo ??= {};
const read = this.localuser.unknownRead.get(this.id);
if (read) {
this.readStateInfo(read);
this.localuser.unknownRead.delete(this.id);
}
}
get perminfo() {
return this.guild.perminfo.channels[this.id];
@@ -714,7 +751,7 @@ class Channel extends SnowFlake {
return a.position - b.position;
});
}
resolveparent(_guild: Guild) {
resolveparent(_guild: Guild = this.owner) {
const parentid = this.parent_id;
if (!parentid) return false;
this.parent = this.localuser.channelids.get(parentid);
@@ -754,7 +791,7 @@ class Channel extends SnowFlake {
return [build, position] as const;
}
static dragged: [Channel, HTMLDivElement] | [] = [];
html: WeakRef<HTMLElement> | undefined;
html: WeakRef<HTMLDivElement> | undefined;
get visible() {
return this.hasPermission("VIEW_CHANNEL");
}
@@ -808,8 +845,12 @@ class Channel extends SnowFlake {
}
return icon;
}
isThread() {
return this.type === 10 || this.type === 11 || this.type === 12;
}
createguildHTML(admin = false): HTMLDivElement {
const div = document.createElement("div");
const div = this.html?.deref() || document.createElement("div");
div.innerHTML = "";
if (this.muted) {
div.classList.add("muted");
@@ -820,6 +861,7 @@ class Channel extends SnowFlake {
Math.min((this.mute_config?.end_time as number) - Date.now(), 2147483647),
);
}
this.html = new WeakRef(div);
if (!this.visible) {
let quit = true;
@@ -832,7 +874,7 @@ class Channel extends SnowFlake {
return div;
}
}
div.draggable = admin;
div.draggable = admin && !this.isThread();
div.addEventListener("dragstart", (e) => {
Channel.dragged = [this, div];
e.stopImmediatePropagation();
@@ -840,6 +882,14 @@ class Channel extends SnowFlake {
div.addEventListener("dragend", () => {
Channel.dragged = [];
});
const childrendiv = document.createElement("div");
childrendiv.classList.add("channels");
for (const channel of this.children.filter(
(_) => !_.isThread() || _.member || this.localuser.channelfocus === _,
)) {
childrendiv.appendChild(channel.createguildHTML(admin));
}
if (this.type === 4) {
this.sortchildren();
const caps = document.createElement("div");
@@ -855,7 +905,7 @@ class Channel extends SnowFlake {
this.nameSpan = new WeakRef(myhtml);
decdiv.appendChild(myhtml);
caps.appendChild(decdiv);
const childrendiv = document.createElement("div");
if (admin) {
const addchannel = document.createElement("span");
addchannel.classList.add("addchannel", "svgicon", "svg-plus");
@@ -872,16 +922,12 @@ class Channel extends SnowFlake {
Channel.contextmenu.bindContextmenu(decdiv, this, undefined);
for (const channel of this.children) {
childrendiv.appendChild(channel.createguildHTML(admin));
}
childrendiv.classList.add("channels");
setTimeout((_: any) => {
if (!this.perminfo.collapsed) {
childrendiv.style.height = childrendiv.scrollHeight + "px";
}
}, 100);
div.appendChild(childrendiv);
if (this.perminfo.collapsed) {
decoration.classList.add("hiddencat");
childrendiv.style.height = "0px";
@@ -909,10 +955,11 @@ class Channel extends SnowFlake {
handleColapse();
};
} else {
childrendiv.classList.add("threads");
div.classList.add("channel");
this.unreads();
Channel.contextmenu.bindContextmenu(div, this, undefined);
if (admin) {
if (admin && !this.isThread()) {
this.coatDropDiv(div);
}
const button = document.createElement("button");
@@ -938,6 +985,7 @@ class Channel extends SnowFlake {
this.updateVoiceUsers();
}
}
div.appendChild(childrendiv);
return div;
}
async moveForDrag(x: number) {
@@ -1822,8 +1870,15 @@ class Channel extends SnowFlake {
this.myhtml.classList.add("viewChannel");
}
const id = ++Channel.genid;
if (this.localuser.channelfocus && this.localuser.channelfocus !== this) {
this.localuser.channelfocus.infinite.delete();
if (this.localuser.channelfocus.isThread() && !this.localuser.channelfocus.member) {
const prev = this.localuser.channelfocus;
this.localuser.channelfocus = this;
prev.parent?.createguildHTML();
}
} else if (this.localuser.channelfocus === this && !aroundMessage) {
if (this.lastmessageid)
this.infinite.focus(aroundMessage || this.lastmessageid, !!aroundMessage, true);
@@ -1833,6 +1888,13 @@ class Channel extends SnowFlake {
this.guild.perminfo.prevchannel = this.id;
this.localuser.channelfocus = this;
if (this.isThread() && !this.member) {
this.parent?.createguildHTML();
if (this.myhtml) {
this.myhtml.classList.add("viewChannel");
}
}
if (
this.nsfw && //@ts-ignore another hack
(!this.perminfo.nsfwOk || !this.localuser.user.nsfw_allowed)
@@ -2320,7 +2382,6 @@ class Channel extends SnowFlake {
this.parent_id = undefined;
}
this.children = [];
this.guild_id = json.guild_id;
const oldover = this.permission_overwrites;
this.permission_overwrites = new Map();
+34 -10
View File
@@ -1373,6 +1373,7 @@ class Guild extends SnowFlake {
this.channels.push(temp);
this.localuser.channelids.set(temp.id, temp);
}
this.headchannels = [];
for (const thing of this.channels) {
const parent = thing.resolveparent(this);
@@ -1380,6 +1381,11 @@ class Guild extends SnowFlake {
this.headchannels.push(thing);
}
}
for (const thread of json.threads) {
const temp = new Channel(thread, this);
this.localuser.channelids.set(temp.id, temp);
temp.resolveparent(this);
}
this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel);
this.stickers = json.stickers.map((_) => new Sticker(_, this)) || [];
}
@@ -1675,6 +1681,21 @@ class Guild extends SnowFlake {
noti.classList.add("notiunread");
}
}
async goToThread(threadId: string) {
if (!this.localuser.channelids.has(threadId)) {
const channelJson = (await (
await fetch(this.info.api + "/channels/" + threadId, {
headers: this.headers,
})
).json()) as channeljson;
const channel = new Channel(channelJson, this);
this.localuser.channelids.set(channel.id, channel);
channel.resolveparent(this);
const par = this.localuser.channelids.get(channel.parent_id as string);
par?.createguildHTML();
}
this.localuser.goToChannel(threadId);
}
getHTML() {
const sideContainDiv = document.getElementById("sideContainDiv");
if (sideContainDiv) {
@@ -1737,6 +1758,9 @@ class Guild extends SnowFlake {
if (channel) {
await channel.getHTML(addstate, undefined, message);
return;
} else {
await this.goToThread(ID);
return;
}
}
if (this.prevchannel && ID !== null && this.prevchannel.visible) {
@@ -1809,22 +1833,22 @@ class Guild extends SnowFlake {
updateChannel(json: channeljson) {
const channel = this.localuser.channelids.get(json.id);
if (channel) {
const parent = channel.parent;
channel.updateChannel(json);
this.headchannels = [];
for (const thing of this.channels) {
thing.children = [];
}
this.headchannels = [];
for (const thing of this.channels) {
const parent = thing.resolveparent(this);
if (!parent) {
this.headchannels.push(thing);
this.channels = this.channels.sort((a, b) => a.position - b.position);
if (parent !== channel.parent) {
if (parent) parent.children.filter((_) => _ !== channel);
channel.resolveparent();
} else if (parent && !json.parent_id) {
this.headchannels.push(channel);
for (const thing of this.channels) {
thing.children = thing.children.filter((_) => _ !== channel);
}
}
for (const channel of this.channels) {
channel.children = channel.children.sort((a, b) => a.position - b.position);
}
this.channels = this.channels.sort((a, b) => a.position - b.position);
this.printServers();
}
}
+2 -2
View File
@@ -84,9 +84,9 @@ if (window.location.pathname.startsWith("/channels")) {
}
regSwap(thisUser);
thisUser.initwebsocket().then(() => {
thisUser.initwebsocket().then(async () => {
thisUser.loaduser();
thisUser.init();
await thisUser.init();
const loading = document.getElementById("loading") as HTMLDivElement;
loading.classList.add("doneloading");
loading.classList.remove("loading");
+35 -8
View File
@@ -48,13 +48,7 @@ interface readyjson {
guilds: guildjson[];
relationships: relationJson[];
read_state: {
entries: {
id: string;
channel_id: string;
last_message_id: string;
last_pin_timestamp: string;
mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware
}[];
entries: readStateEntry[];
partial: boolean;
version: number;
};
@@ -91,6 +85,13 @@ interface readyjson {
};
};
}
export interface readStateEntry {
id: string;
channel_id: string;
last_message_id: string;
last_pin_timestamp: string;
mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware
}
export interface sessionJson {
active: boolean;
activities: []; //will need to find example of this
@@ -455,6 +456,7 @@ type emojipjson = emojijson & {
type guildjson = {
application_command_counts: {[key: string]: number};
channels: channeljson[];
threads: channeljson[];
data_mode: string;
emojis: emojipjson[];
guild_scheduled_events: [];
@@ -503,7 +505,6 @@ type guildjson = {
roles: rolesjson[];
stage_instances: [];
stickers: stickerJson[];
threads: [];
version: string;
guild_hashes: {};
joined_at: string;
@@ -530,7 +531,20 @@ type startTypingjson = {
member?: memberjson;
};
};
export interface threadMember {
id: string;
user_id: string;
join_timestamp: string;
flags: number;
muted: boolean;
mute_config?: {
end_time?: string;
selected_time_window?: number;
};
member?: memberjson;
}
type channeljson = {
member: threadMember;
id: string;
rate_limit_per_user?: number;
owner_id?: string;
@@ -683,6 +697,7 @@ type messagejson = {
emoji: emojijson; //very likely needs expanding
me: boolean;
}[];
thread?: channeljson;
interaction?: {
id: string;
type: 2 | 3;
@@ -842,6 +857,18 @@ type wsjson =
s: number;
t: "MESSAGE_DELETE";
}
| {
op: 0;
t: "THREAD_MEMBERS_UPDATE";
d: {
guild_id: string;
id: string;
member_count: number;
added_members?: [threadMember];
removed_member_ids?: string[];
};
s: 3;
}
| {
op: 0;
d: {
+34 -6
View File
@@ -14,6 +14,7 @@ import {
messageCreateJson,
messagejson,
presencejson,
readStateEntry,
readyjson,
startTypingjson,
wsjson,
@@ -122,10 +123,10 @@ class Localuser {
sessionStorage.setItem("currentuser", specialUser.uid);
localStorage.setItem("userinfos", JSON.stringify(Localuser.users));
thisUser.initwebsocket().then(() => {
thisUser.initwebsocket().then(async () => {
const loaddesc = document.getElementById("load-desc") as HTMLElement;
thisUser.loaduser();
thisUser.init();
await thisUser.init();
loading.classList.add("doneloading");
loaddesc.textContent = I18n.loaded();
loading.classList.remove("loading");
@@ -331,6 +332,7 @@ class Localuser {
}
}
guildFolders: guildFolder[] = [];
unknownRead = new Map<string, readStateEntry>();
async gottenReady(ready: readyjson): Promise<void> {
await I18n.done;
this.queryBlog();
@@ -406,6 +408,7 @@ class Localuser {
for (const thing of ready.d.read_state.entries) {
const channel = this.channelids.get(thing.channel_id);
if (!channel) {
this.unknownRead.set(thing.channel_id, thing);
continue;
}
channel.readStateInfo(thing);
@@ -654,9 +657,9 @@ class Localuser {
() => {
if (this.swapped) return;
loaddesc.textContent = I18n.retrying();
this.initwebsocket().then(() => {
this.initwebsocket().then(async () => {
this.loaduser();
this.init();
await this.init();
const loading = document.getElementById("loading") as HTMLElement;
loading.classList.add("doneloading");
loading.classList.remove("loading");
@@ -695,6 +698,31 @@ class Localuser {
}
if (temp.op == 0) {
switch (temp.t) {
case "THREAD_MEMBERS_UPDATE": {
const channel = this.channelids.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":
@@ -1507,7 +1535,7 @@ class Localuser {
this.loadGuild(json.guild_id, true);
}
}
init(): void {
async init() {
const location = window.location.href.split("/");
this.buildservers();
if (location[3] === "channels") {
@@ -1515,7 +1543,7 @@ class Localuser {
if (!guild) {
return;
}
guild.loadChannel(location[5], true, location[6]);
await guild.loadChannel(location[5], true, location[6]);
this.channelfocus = this.channelids.get(location[5]);
}
}
+105 -2
View File
@@ -8,7 +8,14 @@ import {Localuser} from "./localuser.js";
import {Role} from "./role.js";
import {File} from "./file.js";
import {SnowFlake} from "./snowflake.js";
import {emojijson, interactionEvents, memberjson, messagejson, userjson} from "./jsontypes.js";
import {
channeljson,
emojijson,
interactionEvents,
memberjson,
messagejson,
userjson,
} from "./jsontypes.js";
import {Emoji} from "./emoji.js";
import {mobile} from "./utils/utils.js";
import {I18n} from "./i18n.js";
@@ -118,6 +125,33 @@ class Message extends SnowFlake {
},
},
);
Message.contextmenu.addButton(
() => I18n.message.createThread(),
function () {
const dio = new Dialog(I18n.message.threadOptions());
const opt = dio.options.addForm(
"",
(body) => {
const channelJson = body as channeljson;
this.guild.goToThread(channelJson.id);
dio.hide();
},
{
fetchURL:
this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id + "/threads",
headers: this.headers,
},
);
opt.addTextInput(I18n.threads.name(), "name");
dio.show();
},
{
visible: function () {
return this.channel.hasPermission("CREATE_PUBLIC_THREADS");
},
},
);
Message.contextmenu.addButton(
() => I18n.message.reactions(),
function (this: Message) {
@@ -335,6 +369,7 @@ class Message extends SnowFlake {
}
components?: Components;
edited_timestamp: string | null = null;
thread?: Channel;
giveData(messagejson: messagejson) {
const func = this.channel.infinite.snapBottom();
for (const thing of Object.keys(messagejson)) {
@@ -370,6 +405,14 @@ class Message extends SnowFlake {
} else if (thing === "components" && messagejson.components) {
this.components = new Components(messagejson.components, this);
continue;
} else if (thing === "thread" && messagejson.thread) {
let thread = this.localuser.channelids.get(messagejson.thread.id);
if (!thread) {
thread = new Channel(messagejson.thread, this.guild);
thread.resolveparent();
this.localuser.channelids.set(thread.id, thread);
this.thread = thread;
}
}
(this as any)[thing] = (messagejson as any)[thing];
}
@@ -606,6 +649,15 @@ class Message extends SnowFlake {
if (div === this.div) {
this.div.classList.add("messagediv");
}
if (this.type === 21) {
this.channel.parent?.getmessage(this.message_reference?.message_id as string).then((_) => {
const gen = _?.generateMessage(undefined, undefined, div);
if (!gen) {
//idk
}
});
return div;
}
const editmode = this.channel.editing === this;
if (!premessage && !dupe) {
@@ -722,7 +774,7 @@ class Message extends SnowFlake {
}
}
}
if (this.message_reference && this.type !== 6) {
if (this.message_reference && this.type !== 6 && this.type !== 18) {
const replyline = document.createElement("div");
const minipfp = document.createElement("img");
@@ -1031,6 +1083,38 @@ class Message extends SnowFlake {
afterText.classList.add("pinText");
text.append(afterText);
const time = document.createElement("span");
time.textContent = " " + formatTime(new Date(this.timestamp));
time.classList.add("timestamp");
text.append(time);
div.classList.add("topMessage");
} else if (this.type === 18) {
const text = document.createElement("div");
build.appendChild(text);
const m = I18n.message.pin("|||", "???").split("|||");
if (m.length === 2) text.append(m.shift() as string);
const username = document.createElement("span");
username.textContent = this.author.name;
//this.author.profileclick(username);
this.author.bind(username, this.guild);
text.appendChild(username);
username.classList.add("username");
const midText = m[0].split("???");
const afterText = document.createElement("span");
afterText.textContent = midText[0];
text.append(afterText);
const threadName = document.createElement("span");
threadName.textContent = this.content.rawString;
threadName.classList.add("pinText");
text.append(threadName);
threadName.onclick = () => {
this.guild.goToThread(this.message_reference?.channel_id as string);
};
const time = document.createElement("span");
time.textContent = " " + formatTime(new Date(this.timestamp));
time.classList.add("timestamp");
@@ -1111,6 +1195,25 @@ class Message extends SnowFlake {
}
this.bindButtonEvent();
if (!dupe && this.thread) {
const thread = this.thread;
const threadBox = document.createElement("div");
threadBox.classList.add("flexttb", "threadBox");
const topRow = document.createElement("div");
topRow.classList.add("flexltr");
const title = document.createElement("span");
title.textContent = thread.name;
topRow.onclick = () => {
this.localuser.goToChannel(thread.id);
};
const messages = document.createElement("span");
messages.classList.add("clickable");
messages.textContent = I18n.message.messages("10");
topRow.append(title, messages);
threadBox.append(topRow);
div.append(threadBox);
}
return div;
}
bindButtonEvent() {
+55 -2
View File
@@ -1706,6 +1706,33 @@ span.instanceStatus {
color: var(--primary-text-soft);
overflow-y: auto;
}
.threads {
button {
position: relative;
}
button::before {
content: "";
position: absolute;
top: 0px;
left: 0px;
height: calc(50% - 1px);
width: 3px;
border-bottom: 2px solid var(--reply-line);
border-left: 2px solid var(--reply-line);
border-bottom-left-radius: 8px;
}
button::after {
content: "";
position: absolute;
bottom: 0px;
left: 0px;
height: calc(50% - 1px);
width: 3px;
border-top: 2px solid var(--reply-line);
border-left: 2px solid var(--reply-line);
border-top-left-radius: 8px;
}
}
.channels {
overflow-y: hidden;
transition: height 0.2s ease-in-out;
@@ -1744,8 +1771,8 @@ span.instanceStatus {
.channels .channelbutton {
margin-left: 8px;
}
.viewChannel .channelbutton,
.viewChannel .channelbutton:hover {
.viewChannel > .channelbutton,
.viewChannel > .channelbutton:hover {
background: var(--channel-selected);
font-weight: bold;
color: var(--primary-text-prominent);
@@ -4636,3 +4663,29 @@ img.error::after {
object-fit: cover;
opacity: 0.8;
}
.threadBox {
padding: 6px;
background: #00000063;
border-radius: 4px;
margin: 6px;
min-width: 300px;
width: fit-content;
margin-left: 50px;
margin-top: 10px;
position: relative;
::before {
content: "";
position: absolute;
top: -8px;
left: -32px;
height: calc(100% - 8px);
width: 26px;
border-bottom: 2px solid var(--reply-line);
border-left: 2px solid var(--reply-line);
border-bottom-left-radius: 8px;
}
.clickable {
color: var(--link);
margin-left: auto;
}
}
+9
View File
@@ -137,6 +137,11 @@
"unmute": "Unmute Channel",
"voice": "Voice"
},
"threads":{
"leave":"Leave Thread",
"join":"Join Thread",
"name":"Name:"
},
"channelLink": "# $1",
"clientDesc": "Client version: $1\n\n[Join the official Fermi guild]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Help translate Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Help create Fermi](https://github.com/MathMan05/Fermi)\n\n[Help maintain the server Fermi relies on](https://github.com/spacebarchat/server)\n\n[Read the blog](https://blog.fermi.chat/)\n\nCalculated rights: $3",
"commands": {
@@ -555,6 +560,7 @@
"timeoutTime": "Timeout Time:"
},
"message": {
"messages":"$1 {{PLURAL:$1|Message|Messages}} >",
"andMore": "$1, and more!",
"attached": "Sent an attachment",
"delete": "Delete message",
@@ -564,8 +570,11 @@
"edited": "(edited)",
"fullMessage": "Full message:",
"pin": "$1 has pinned a message",
"pin": "$1 has created a thread: $2",
"reactedWith": "Reacted with $1",
"reactionAdd": "Add reaction",
"createThread":"Create Thread",
"threadOptions":"Thread options",
"reactions": "View Reactions",
"reactionsTitle": "Reactions",
"retry": "Resend errored message",