diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 75e5bb0..5c83c02 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -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(); + 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; + let res: XMLHttpRequest; + let funcs: { + gotid: (id: string) => void; + progress: (total: number, sofar: number) => void; + failed: (restart: () => void) => void; + }; + const progress = (e: ProgressEvent) => { + 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(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(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 { + 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; diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index a54ff10..d12f5bc 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -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) { diff --git a/src/webpage/index.html b/src/webpage/index.html index 6465f49..51cc205 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -88,6 +88,7 @@
+
diff --git a/src/webpage/index.ts b/src/webpage/index.ts index 4511516..e6458e7 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -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) { diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 47e55dd..eb7e561 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -130,7 +130,13 @@ class InfiniteScroller { async addedBottom(): Promise { 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(async (res) => { + //debugger; try { if (!this.div) { res(false); diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index e737a6e..ea4b8ae 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -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; diff --git a/src/webpage/media.ts b/src/webpage/media.ts index 8909667..c82e935 100644 --- a/src/webpage/media.ts +++ b/src/webpage/media.ts @@ -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) { diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 212c6ec..e68ec43 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -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; } diff --git a/src/webpage/service.ts b/src/webpage/service.ts index e04c3e6..d505e9f 100644 --- a/src/webpage/service.ts +++ b/src/webpage/service.ts @@ -123,6 +123,9 @@ async function getfile(event: FetchEvent): Promise { self.addEventListener("fetch", (e) => { const event = e as FetchEvent; + if (event.request.method === "POST") { + return; + } try { event.respondWith(getfile(event)); } catch (e) { diff --git a/src/webpage/style.css b/src/webpage/style.css index aa5053d..375d92a 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -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 { diff --git a/translations/en.json b/translations/en.json index 78a685e..a93165b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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",