ghost messages and some fixes

This commit is contained in:
MathMan05 2025-04-04 16:31:15 -05:00
parent 8e98535407
commit c479d9a683
11 changed files with 222 additions and 29 deletions

View file

@ -12,6 +12,7 @@ import {SnowFlake} from "./snowflake.js";
import {
channeljson,
embedjson,
filejson,
messageCreateJson,
messagejson,
readyjson,
@ -24,6 +25,7 @@ import {User} from "./user.js";
import {I18n} from "./i18n.js";
import {mobile} from "./utils/utils.js";
import {webhookMenu} from "./webhooks.js";
import {File} from "./file.js";
declare global {
interface NotificationOptions {
@ -926,6 +928,11 @@ class Channel extends SnowFlake {
messages.append(html);
}
async getHTML(addstate = true) {
const ghostMessages = document.getElementById("ghostMessages") as HTMLElement;
ghostMessages.innerHTML = "";
for (const thing of this.fakeMessages) {
ghostMessages.append(thing[1]);
}
if (addstate) {
history.pushState([this.guild_id, this.id], "", "/channels/" + this.guild_id + "/" + this.id);
}
@ -1438,6 +1445,91 @@ class Channel extends SnowFlake {
return "default";
}
}
fakeMessages: [Message, HTMLElement][] = [];
fakeMessageMap = new Map<string, [Message, HTMLElement]>();
destroyFakeMessage(id: string) {
const message = this.fakeMessageMap.get(id);
if (!message) return;
this.fakeMessages = this.fakeMessages.filter((_) => _[0] !== message[0]);
message[1].remove();
for (const {url} of message[0].attachments) {
URL.revokeObjectURL(url);
}
this.fakeMessageMap.delete(id);
}
makeFakeMessage(content: string, files: filejson[] = []) {
const m = new Message(
{
author: this.localuser.user.tojson(),
channel_id: this.id,
guild_id: this.guild.id,
id: "fake" + Math.random(),
content,
timestamp: new Date() + "",
edited_timestamp: null,
mentions: [],
mention_roles: [],
mention_everyone: false,
attachments: files,
tts: false,
embeds: [],
reactions: [],
nonce: Math.random() + "",
type: 0,
pinned: false,
},
this,
true,
);
const ghostMessages = document.getElementById("ghostMessages");
if (!ghostMessages) throw Error("oops");
const html = m.buildhtml(this.lastmessage, true);
html.classList.add("messagediv", "loadingMessage");
console.log(html);
ghostMessages.append(html);
this.fakeMessages.push([m, html]);
let loadingP = document.createElement("span");
const buttons = document.createElement("div");
buttons.classList.add("flexltr");
const retryB = document.createElement("button");
retryB.textContent = I18n.message.retry();
const dont = document.createElement("button");
dont.textContent = I18n.message.delete();
dont.onclick = (_) => html.remove();
dont.style.marginLeft = "4px";
buttons.append(retryB, dont);
return {
gotid: (id: string) => {
this.fakeMessageMap.set(id, [m, html]);
const m2 = this.messages.get(id);
if (m2 && m2.div) {
this.destroyFakeMessage(id);
}
},
progress: (total: number, sofar: number) => {
if (total < 20000 || sofar === total) {
loadingP.remove();
return;
}
html.append(loadingP);
loadingP.textContent = File.filesizehuman(sofar) + " / " + File.filesizehuman(total);
},
failed: (retry: () => void) => {
loadingP.remove();
html.append(buttons);
retryB.onclick = () => {
retry();
html.classList.remove("erroredMessage");
buttons.remove();
};
html.classList.add("erroredMessage");
},
};
}
async sendMessage(
content: string,
{
@ -1453,6 +1545,36 @@ class Channel extends SnowFlake {
message_id: replyingto.id,
};
}
let prom: Promise<void>;
let res: XMLHttpRequest;
let funcs: {
gotid: (id: string) => void;
progress: (total: number, sofar: number) => void;
failed: (restart: () => void) => void;
};
const progress = (e: ProgressEvent<EventTarget>) => {
funcs.progress(e.total, e.loaded);
};
const promiseHandler = (resolve: () => void) => {
res.onload = () => {
resolve();
console.log(res.response);
funcs.gotid(res.response.id);
};
};
const fail = () => {
funcs.failed(() => {
res.open("POST", this.info.api + "/channels/" + this.id + "/messages");
res.setRequestHeader("Authorization", this.headers.Authorization);
if (ctype) {
res.setRequestHeader("Content-type", ctype);
}
res.send(rbody);
});
};
let rbody: string | FormData;
let ctype: string | undefined;
if (attachments.length === 0) {
const body = {
content,
@ -1462,11 +1584,23 @@ class Channel extends SnowFlake {
if (replyjson) {
body.message_reference = replyjson;
}
return await fetch(this.info.api + "/channels/" + this.id + "/messages", {
res = new XMLHttpRequest();
res.responseType = "json";
res.upload.onprogress = progress;
res.onerror = fail;
prom = new Promise<void>(promiseHandler);
res.open("POST", this.info.api + "/channels/" + this.id + "/messages");
res.setRequestHeader("Content-type", (ctype = this.headers["Content-type"]));
res.setRequestHeader("Authorization", this.headers.Authorization);
funcs = this.makeFakeMessage(content);
res.send((rbody = JSON.stringify(body)));
/*
res = fetch(this.info.api + "/channels/" + this.id + "/messages", {
method: "POST",
headers: this.headers,
body: JSON.stringify(body),
});
*/
} else {
const formData = new FormData();
const body = {
@ -1481,12 +1615,36 @@ class Channel extends SnowFlake {
for (const i in attachments) {
formData.append("files[" + i + "]", attachments[i]);
}
return await fetch(this.info.api + "/channels/" + this.id + "/messages", {
res = new XMLHttpRequest();
res.responseType = "json";
res.upload.onprogress = progress;
res.onerror = fail;
prom = new Promise<void>(promiseHandler);
res.open("POST", this.info.api + "/channels/" + this.id + "/messages", true);
res.setRequestHeader("Authorization", this.headers.Authorization);
funcs = this.makeFakeMessage(
content,
attachments.map((_) => ({
id: "string",
filename: "",
content_type: _.type,
size: _.size,
url: URL.createObjectURL(_),
})),
);
res.send((rbody = formData));
/*
res = fetch(this.info.api + "/channels/" + this.id + "/messages", {
method: "POST",
body: formData,
headers: {Authorization: this.headers.Authorization},
});
*/
}
return prom;
}
unreads() {
if (!this.hasunreads) {
@ -1502,7 +1660,12 @@ class Channel extends SnowFlake {
}
}
}
messageCreate(messagep: messageCreateJson): void {
async messageCreate(messagep: messageCreateJson): Promise<void> {
if (this.localuser.channelfocus !== this) {
if (this.fakeMessageMap.has(this.id)) {
this.destroyFakeMessage(this.id);
}
}
if (!this.hasPermission("VIEW_CHANNEL")) {
return;
}
@ -1528,9 +1691,9 @@ class Channel extends SnowFlake {
this.guild.unreads();
if (this === this.localuser.channelfocus) {
if (!this.infinitefocus) {
this.tryfocusinfinate();
await this.tryfocusinfinate();
}
this.infinite.addedBottom();
await this.infinite.addedBottom();
}
if (messagez.author === this.localuser.user) {
return;

View file

@ -429,6 +429,11 @@ class Group extends Channel {
return div;
}
async getHTML(addstate = true) {
const ghostMessages = document.getElementById("ghostMessages") as HTMLElement;
ghostMessages.innerHTML = "";
for (const thing of this.fakeMessages) {
ghostMessages.append(thing[1]);
}
const id = ++Channel.genid;
if (this.localuser.channelfocus) {
this.localuser.channelfocus.infinite.delete();
@ -462,7 +467,15 @@ class Group extends Channel {
}
this.buildmessages();
}
messageCreate(messagep: {d: messagejson}) {
async messageCreate(messagep: {d: messagejson}) {
if (this.localuser.channelfocus !== this) {
if (this.fakeMessageMap.has(this.id)) {
this.destroyFakeMessage(this.id);
}
}
if (this.fakeMessageMap.has(messagep.d.id)) {
this.destroyFakeMessage(messagep.d.id);
}
this.mentions++;
const messagez = new Message(messagep.d, this);
@ -501,9 +514,9 @@ class Group extends Channel {
}
if (this === this.localuser.channelfocus) {
if (!this.infinitefocus) {
this.tryfocusinfinate();
await this.tryfocusinfinate();
}
this.infinite.addedBottom();
await this.infinite.addedBottom();
}
this.unreads();
if (messagez.author === this.localuser.user) {

View file

@ -88,6 +88,7 @@
<div style="position: relative">
<div id="searchOptions" class="flexttb searchOptions"></div>
</div>
<div id="ghostMessages"></div>
<div id="pasteimage" class="flexltr"></div>
<div id="replybox" class="hideReplyBox"></div>
<div id="typediv">

View file

@ -188,11 +188,11 @@ import {I18n} from "./i18n.js";
(document.getElementById("settings") as HTMLImageElement).onclick = userSettings;
const memberListToggle = document.getElementById("memberlisttoggle") as HTMLInputElement;
memberListToggle.checked = !localStorage.getItem("memberNotChecked");
memberListToggle.onclick = () => {
memberListToggle.onchange = () => {
if (!memberListToggle.checked) {
localStorage.setItem("memberNotChecked", "true");
} else {
localStorage.delete("memberNotChecked");
localStorage.removeItem("memberNotChecked");
}
};
if (mobile) {

View file

@ -130,7 +130,13 @@ class InfiniteScroller {
async addedBottom(): Promise<void> {
await this.updatestuff();
const func = this.snapBottom();
await this.watchForChange();
if (this.changePromise) {
while (this.changePromise) {
await new Promise((res) => setTimeout(res, 30));
}
} else {
await this.watchForChange();
}
func();
}
@ -245,6 +251,7 @@ class InfiniteScroller {
}
this.changePromise = new Promise<boolean>(async (res) => {
//debugger;
try {
if (!this.div) {
res(false);

View file

@ -334,7 +334,7 @@ type messagejson = {
member?: memberjson;
content: string;
timestamp: string;
edited_timestamp: string;
edited_timestamp: string | null;
tts: boolean;
mention_everyone: boolean;
mentions: []; //need examples to fix
@ -349,7 +349,7 @@ type messagejson = {
nonce: string;
pinned: boolean;
type: number;
webhook: webhookInfo;
webhook?: webhookInfo;
};
type filejson = {
id: string;

View file

@ -156,7 +156,6 @@ function makePlayBox(mor: string | media, player: MediaPlayer, ctime = 0) {
};
function followUpdates(cur: mediaEvents) {
if (audio && cur.type !== "playing") {
console.log(cur);
}
if (cur.type == "audio" && audio) {
if (cur.t == "start") {
@ -220,7 +219,6 @@ function makePlayBox(mor: string | media, player: MediaPlayer, ctime = 0) {
regenTime(+bar.value * 1000);
};
async function regenTime(curTime: number = 0) {
console.log(med.length);
const len = await med.length;
bar.disabled = false;
bar.max = "" + len / 1000;
@ -229,7 +227,6 @@ function makePlayBox(mor: string | media, player: MediaPlayer, ctime = 0) {
}
regenTime();
title.textContent = thing.title;
console.log(thing);
});
return div;
}
@ -274,7 +271,6 @@ class MediaPlayer {
if (!document.contains(elm)) {
clearInterval(int);
this.listeners = this.listeners.filter((_) => _[0] !== updates);
console.log("cleared data");
}
}, 1000);
}
@ -352,7 +348,6 @@ class MediaPlayer {
if (size !== 0) {
cbuff = (await read.read()).value;
index = 0;
console.log("got more buffer", index, arri, size);
}
}
return arr;
@ -372,7 +367,6 @@ class MediaPlayer {
const Identify = String.fromCharCode(await next(), await next(), await next());
const sizeArr = await get8BitArray(3);
const size = (sizeArr[0] << 16) + (sizeArr[1] << 8) + sizeArr[2];
console.log(sizeLeft, size, index);
if (Identify === String.fromCharCode(0, 0, 0)) {
break;
}
@ -389,7 +383,6 @@ class MediaPlayer {
} else {
mappy.set(Identify, await get8BitArray(size));
}
console.warn(sizeLeft);
}
const pic = mappy.get("PIC");
if (pic) {
@ -400,7 +393,6 @@ class MediaPlayer {
}
const description = new TextDecoder().decode(new Uint8Array(desc));
i++;
console.warn(pic, i);
const blob = new Blob([pic.slice(i, pic.length).buffer], {type: "image/jpeg"});
const urlmaker = window.URL || window.webkitURL;
const url = urlmaker.createObjectURL(blob);
@ -467,7 +459,6 @@ class MediaPlayer {
continue;
}
console.log(sizeLeft, size, index);
if (Identify === String.fromCharCode(0, 0, 0, 0)) {
break;
}
@ -484,7 +475,6 @@ class MediaPlayer {
} else {
mappy.set(Identify, await get8BitArray(size));
}
console.warn(sizeLeft);
}
const pic = mappy.get("APIC");
if (pic) {
@ -530,14 +520,12 @@ class MediaPlayer {
}
const TYER = mappy.get("TYER");
if (TYER) {
console.log(decodeText(TYER));
output.year = +decodeText(TYER);
}
const TLEN = mappy.get("TLEN");
if (TLEN) {
output.length = +decodeText(TLEN);
}
console.log(mappy);
}
} //TODO implement more metadata types
} catch (e) {
@ -550,12 +538,10 @@ class MediaPlayer {
const audio = document.createElement("audio");
audio.src = url;
audio.onloadeddata = (_) => {
console.log("Loaded!", audio.duration * 1000);
output.length = audio.duration * 1000;
res(audio.duration * 1000);
};
audio.load();
console.log(audio);
});
}
if (!output.title) {

View file

@ -310,6 +310,9 @@ class Message extends SnowFlake {
}
return build;
}
getUnixTime(): number {
return new Date(this.timestamp).getTime();
}
async edit(content: string) {
return await fetch(this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, {
method: "PATCH",
@ -920,6 +923,9 @@ class Message extends SnowFlake {
}
}
buildhtml(premessage?: Message | undefined, dupe = false): HTMLElement {
if (this.channel.fakeMessageMap.has(this.id)) {
this.channel.destroyFakeMessage(this.id);
}
if (dupe) {
return this.generateMessage(premessage, false, document.createElement("div")) as HTMLElement;
}

View file

@ -123,6 +123,9 @@ async function getfile(event: FetchEvent): Promise<Response> {
self.addEventListener("fetch", (e) => {
const event = e as FetchEvent;
if (event.request.method === "POST") {
return;
}
try {
event.respondWith(getfile(event));
} catch (e) {

View file

@ -1087,6 +1087,9 @@ span.instanceStatus {
overflow-y: auto;
flex-wrap: wrap;
}
#ghostMessages {
transform: translate(0px, -22px);
}
#pasteimage:empty {
height: 0;
padding: 0;
@ -1218,7 +1221,17 @@ span.instanceStatus {
.dot:nth-child(3) {
animation-delay: 0.66s;
}
.loadingMessage {
span {
opacity: 0.5;
}
}
.erroredMessage {
span {
opacity: 1;
color: var(--red);
}
}
/* Message */
.messagediv,
.titlespace {

View file

@ -393,7 +393,8 @@
"edit": "Edit message",
"edited": "(edited)",
"deleted": "Deleted message",
"attached": "Sent an attachment"
"attached": "Sent an attachment",
"retry": "Resend errored message"
},
"instanceStats": {
"name": "Instance stats: $1",