diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 6ecd857d..59ef8082 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -28,7 +28,6 @@ import {File} from "./file.js"; import {Sticker} from "./sticker.js"; import {CustomHTMLDivElement} from "./index.js"; import {Direct} from "./direct.js"; -import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js"; import {NotificationHandler} from "./notificationHandler.js"; import {Command} from "./interactions/commands.js"; @@ -1239,37 +1238,47 @@ class Channel extends SnowFlake { return new Message(json[0], this); } } - async focus(id: string) { - const m = this.messages.get(id); - if (m && m.div) { - if (document.contains(m.div)) { - m.div.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - m.div.classList.remove("jumped"); - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - m.div.classList.add("jumped"); - return; + async getMessages(id: string) { + const m = await this.getmessage(id); + if (!m) return; + const waits: Promise[] = []; + let m1: string | undefined = m.id; + for (let i = 0; i <= 10; i++) { + if (!m1) { + waits.push(this.grabBefore(id)); + break; + } + if (this.idToNext.has(m1) && !(m1 = this.idToNext.get(m1))) break; + } + m1 = m.id; + for (let i = 0; i <= 10; i++) { + if (!m1) { + waits.push(this.grabAfter(id)); + break; } + if (this.idToPrev.has(m1) && !(m1 = this.idToPrev.get(m1))) break; } - console.log(await this.getmessage(id)); + await Promise.all(waits); + } + async focus(id: string, flash = true) { + const prom = this.getMessages(id); - if (this.localuser.channelfocus === this) { - this.localuser.channelfocus?.infinite.delete(); - this.localuser.channelfocus = undefined; + if (await Promise.race([prom, new Promise((res) => setTimeout(() => res(true), 300))])) { + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + Channel.regenLoadingMessages(); + loading.classList.add("loading"); + await prom; + loading.classList.remove("loading"); } - await this.getHTML(true); - console.warn(id); + + if (this.localuser.channelfocus !== this) { + await this.getHTML(true); + } + try { - await this.buildmessages(id); + await this.infinite.focus(id); } catch {} - this.infinite.focus(id, true, true); + this.infinite.focus(id, flash, true); } editLast() { let message: Message | undefined = this.lastmessage; @@ -2042,17 +2051,15 @@ class Channel extends SnowFlake { if (!tempy) return; id = tempy; this.afterProm = new Promise(async (res) => { - const messages = await ProgessiveDecodeJSON( - this.info.api + "/channels/" + this.id + "/messages?limit=100&after=" + id, - { + const messages = (await ( + await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100&after=" + id, { headers: this.headers, - }, - ); + }) + ).json()) as messagejson[]; let i = 0; let previd: string = id; - for await (const obj of messages) { + for (const response of messages) { let messager: Message; - const response = await obj.getWhole(); let willbreak = false; if (this.messages.has(response.id)) { messager = this.messages.get(response.id) as Message; @@ -2122,16 +2129,17 @@ class Channel extends SnowFlake { return; } id = tempy; - const messages = await ProgessiveDecodeJSON( - this.info.api + "/channels/" + this.id + "/messages?before=" + id + "&limit=100", - { - headers: this.headers, - }, - ); + const messages = (await ( + await fetch( + this.info.api + "/channels/" + this.id + "/messages?before=" + id + "&limit=100", + { + headers: this.headers, + }, + ) + ).json()) as messagejson[]; let previd = id; let i = 0; - for await (const messageProm of messages) { - const response = await messageProm.getWhole(); + for (const response of messages) { let messager: Message; if (this.messages.has(response.id)) { messager = this.messages.get(response.id) as Message; @@ -2150,7 +2158,7 @@ class Channel extends SnowFlake { previd = messager.id; - if (messages.done && i < 99) { + if (i < 99) { this.topid = previd; } @@ -2226,17 +2234,18 @@ class Channel extends SnowFlake { elm.remove(); console.warn("rouge element detected and removed"); } - messages.append(await this.infinite.getDiv(id)); - - this.infinite.updatestuff(); + messages.append(await this.infinite.getDiv(id, falsh)); + /* await this.infinite.watchForChange().then(async (_) => { //await new Promise(resolve => setTimeout(resolve, 0)); await this.infinite.focus(id, falsh); //if someone could figure out how to make this work correctly without this, that's be great :P - loading.classList.remove("loading"); + this.infinite.focus(id, falsh, true); }); + */ + loading.classList.remove("loading"); //this.infinite.focus(id.id,false); } private goBackIds(id: string, back: number, returnifnotexistant = true): string | undefined { @@ -2840,8 +2849,7 @@ class Channel extends SnowFlake { } } async goToBottom() { - if (this.localuser.channelfocus !== this) await this.tryfocusinfinate(); - if (this.lastmessageid) this.infinite.focus(this.lastmessageid, false, true); + if (this.lastmessageid) this.focus(this.lastmessageid, false); } async messageCreate(messagep: messageCreateJson): Promise { if (!this.hasPermission("VIEW_CHANNEL")) { @@ -2878,6 +2886,15 @@ class Channel extends SnowFlake { } this.setLastMessageId(messagez.id); + this.unreads(); + this.guild.unreads(); + if (this === this.localuser.channelfocus) { + if (!this.infinitefocus) { + await this.tryfocusinfinate(); + } + await this.infinite.addedBottom(); + } + if (messagez.author === this.localuser.user) { const next = this.messages.get(this.idToNext.get(this.lastreadmessageid as string) as string); this.lastSentMessage = messagez; @@ -2893,14 +2910,7 @@ class Channel extends SnowFlake { await this.goToBottom(); } } - this.unreads(); - this.guild.unreads(); - if (this === this.localuser.channelfocus) { - if (!this.infinitefocus) { - await this.tryfocusinfinate(); - } - await this.infinite.addedBottom(); - } + if (messagez.author === this.localuser.user) { return; } diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 7c73cfcc..230784c0 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -1,40 +1,79 @@ +function fragAppend(div: HTMLElement, pre = false) { + let qued = false; + const par = div.parentElement as Element; + if (!par) throw new Error("parrent is missing"); + function appendFrag() { + const elms = Array.from(frag.children) as HTMLElement[]; + let didForce = false; + if (pre) { + if (supports) { + if (par.scrollTop === 0) { + par.scrollTop += 3; + didForce = true; + } + } + } + div[pre ? "prepend" : "append"](frag); + + if (didForce) { + par.scrollTop -= 3; + } + if (pre && !supports) { + let top = -Infinity; + let bottom = Infinity; + elms.forEach((_) => { + const rec = _.getBoundingClientRect(); + top = Math.max(top, rec.top); + bottom = Math.min(bottom, rec.bottom); + }); + const h = top - bottom; + const p = div.parentNode; + if (p instanceof HTMLElement) { + p.scrollTop += h; + } + } + } + const frag = document.createDocumentFragment(); + return (elm: HTMLElement) => + new Promise((res) => { + frag[pre ? "prepend" : "append"](elm); + if (!qued) { + setTimeout(() => { + appendFrag(); + qued = false; + res(); + }); + qued = true; + } else { + res(); + } + }); +} + +const supports = CSS.supports("overflow-anchor", "auto"); class InfiniteScroller { readonly getIDFromOffset: (ID: string, offset: number) => Promise; readonly getHTMLFromID: (ID: string) => Promise; readonly destroyFromID: (ID: string) => Promise; readonly reachesBottom: () => void; - private readonly minDist = 2000; - private readonly fillDist = 3000; - private readonly maxDist = 6000; - HTMLElements: [HTMLElement, string][] = []; - div: HTMLDivElement | null = null; - timeout: ReturnType | null = null; - beenloaded = false; - scrollBottom = 0; - scrollTop = 0; - needsupdate = true; - averageheight = 60; - watchtime = false; - changePromise: Promise | undefined; - scollDiv!: {scrollTop: number; scrollHeight: number; clientHeight: number}; - - resetVars() { - this.scrollTop = 0; - this.scrollBottom = 0; - this.averageheight = 60; - this.watchtime = false; - this.needsupdate = true; - this.beenloaded = false; - this.changePromise = undefined; - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - for (const thing of this.HTMLElements) { - this.destroyFromID(thing[1]); - } - this.HTMLElements = []; + + private weakDiv = new WeakRef(document.createElement("div")); + private curFocID?: string; + private curElms = new Map(); + private backElm = new Map(); + private forElm = new Map(); + private weakElmId = new WeakMap(); + + get div() { + return this.weakDiv.deref(); + } + set div(div: HTMLDivElement | undefined) { + this.weakDiv = new WeakRef(div || document.createElement("div")); } + get scroller() { + return this.div?.children[0] as HTMLDivElement | undefined; + } + constructor( getIDFromOffset: InfiniteScroller["getIDFromOffset"], getHTMLFromID: InfiniteScroller["getHTMLFromID"], @@ -46,360 +85,338 @@ class InfiniteScroller { this.destroyFromID = destroyFromID; this.reachesBottom = reachesBottom; } + observer: IntersectionObserver = new IntersectionObserver(console.log); - async getDiv(initialId: string): Promise { - if (this.div) { - return this.div; + private heightMap = new WeakMap(); + private createObserver(root: HTMLDivElement) { + const scroller = root.children[0]; + function sorted() { + return Array.from(scroller.children).filter((_) => visable.has(_)) as HTMLElement[]; } - this.resetVars(); - const scroll = document.createElement("div"); - scroll.classList.add("scroller"); - this.div = scroll; + const visable = new Set(); + this.observer = new IntersectionObserver( + (obvs) => { + for (const obv of obvs) { + if (obv.target instanceof HTMLElement) { + if (obv.isIntersecting) { + visable.add(obv.target); + } else { + visable.delete(obv.target); + } - this.div.addEventListener("scroll", () => { - this.checkscroll(); - if (this.scrollBottom < 5) { - this.scrollBottom = 5; - } - if (this.timeout === null) { - this.timeout = setTimeout(this.updatestuff.bind(this), 300); - } - this.watchForChange(); - }); + this.heightMap.set(obv.target, obv.boundingClientRect.height); + } + } + for (const obv of obvs) { + if (obv.target instanceof HTMLElement) { + const id = this.weakElmId.get(obv.target); + if (id && !obv.isIntersecting && id === this.curFocID) { + const elms = sorted(); - let oldheight = 0; - new ResizeObserver(() => { - this.checkscroll(); - const func = this.snapBottom(); - this.updatestuff(); - const change = oldheight - scroll.offsetHeight; - if (change > 0 && this.div) { - this.div.scrollTop += change; + const middle = elms[(elms.length / 2) | 0]; + const id = this.weakElmId.get(middle); + if (!id) continue; + this.curFocID = id; + this.fillIn(true); + } else if (!id) console.log("uh..."); + } + } + }, + {root, threshold: 0.1}, + ); + root.addEventListener("scroll", () => { + if (this.scrollBottom < 5) { + const scroll = this.scroller; + if (!scroll) return; + const last = this.weakElmId.get(Array.from(scroll.children).at(-1) as HTMLElement); + if (!last) return; + if (this.backElm.get(last) || !this.backElm.has(last)) return; + this.reachesBottom(); } - oldheight = scroll.offsetHeight; - this.watchForChange(); - func(); - }).observe(scroll); - - new ResizeObserver(() => this.watchForChange()).observe(scroll); - - await this.firstElement(initialId); - this.updatestuff(); - await this.watchForChange().then(() => { - this.updatestuff(); - this.beenloaded = true; }); - - return scroll; } - checkscroll(): void { - if (this.beenloaded && this.div && !document.body.contains(this.div)) { - console.warn("not in document"); - this.div = null; - } - } + async getDiv(initialId: string, flash = false): Promise { + const div = document.createElement("div"); + div.classList.add("scroller"); + this.div = div; - async updatestuff(): Promise { - this.timeout = null; - if (!this.div) return; + const scroll = document.createElement("div"); + div.append(scroll); - this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; - this.averageheight = this.div.scrollHeight / this.HTMLElements.length; - if (this.averageheight < 10) { - this.averageheight = 60; - } - this.scrollTop = this.div.scrollTop; + this.createObserver(div); - if (this.scrollBottom < 5 && !(await this.watchForChange())) { - this.reachesBottom(); - } - if (!this.scrollTop) { - await this.watchForChange(); + this.focus(initialId, flash, true); + return div; + } + private get scrollBottom() { + if (this.div) { + return this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; + } else { + return 0; } - this.needsupdate = false; } - async firstElement(id: string): Promise { - if (!this.div) return; - const html = await this.getHTMLFromID(id); - this.div.appendChild(html); - this.HTMLElements.push([html, id]); + async addedBottom(): Promise { + const snap = this.snapBottom(); + const scroll = this.scroller; + if (!scroll) return; + const last = this.weakElmId.get(Array.from(scroll.children).at(-1) as HTMLElement); + if (!last) return; + this.backElm.delete(last); + await this.fillIn(); + snap(); } - async addedBottom(): Promise { - await this.updatestuff(); - const func = this.snapBottom(); - if (this.changePromise) { - while (this.changePromise) { - await new Promise((res) => setTimeout(res, 30)); - } + snapBottom(): () => void { + const nothing = () => {}; + const scroll = this.scroller; + if (!scroll) return nothing; + const last = this.weakElmId.get(Array.from(scroll.children).at(-1) as HTMLElement); + if (!last) return nothing; + if (this.backElm.get(last) || !this.backElm.has(last)) return nothing; + if (this.div) { + const trigger = this.scrollBottom < 4; + return () => { + if (this.div && trigger) this.div.scrollTop = this.div.scrollHeight; + }; } else { - await this.watchForChange(); + return nothing; } - func(); } - snapBottom(): () => void { - const scrollBottom = this.scrollBottom; - return () => { - if (this.div && scrollBottom < 4) { - this.div.scrollTop = this.div.scrollHeight; - } - }; + async deleteId(id: string) { + const prev = this.backElm.get(id) || (this.backElm.has(id) ? null : undefined); + const next = this.forElm.get(id) || (this.forElm.has(id) ? null : undefined); + await this.removeElm(id); + if (prev && next !== null) this.forElm.set(prev, next); + if (next && prev !== null) this.backElm.set(next, prev); } - whenFrag: (() => number)[] = []; - private async watchForTop(already = false, fragment = new DocumentFragment()): Promise { - const supports = CSS.supports("overflow-anchor", "auto"); - if (!this.div) return false; - const div = this.div; - try { - let again = false; - if (this.scrollTop < (already ? this.fillDist : this.minDist)) { - let nextid: string | undefined; - const firstelm = this.HTMLElements.at(0); - if (firstelm) { - const previd = firstelm[1]; - nextid = await this.getIDFromOffset(previd, 1); - } - - if (nextid) { - const html = await this.getHTMLFromID(nextid); - if (!html) { - this.destroyFromID(nextid); - return false; - } - if (!supports) { - this.whenFrag.push(() => { - const box = html.getBoundingClientRect(); - return box.height; - }); - } - again = true; - fragment.prepend(html); - this.HTMLElements.unshift([html, nextid]); - this.scrollTop += this.averageheight; - } - } - if (this.scrollTop > this.maxDist && this.remove) { - const html = this.HTMLElements.shift(); - if (html) { - let dec = 0; - if (!supports) { - const box = html[0].getBoundingClientRect(); - dec = box.height; - } - again = true; - await this.destroyFromID(html[1]); + private async clearElms() { + await Promise.all(this.curElms.keys().map((id) => this.destroyFromID(id))); + this.curElms.clear(); + this.backElm.clear(); + this.forElm.clear(); + const scroller = this.scroller; + if (!scroller) return; + scroller.innerHTML = ""; + } - div.scrollTop -= dec; - this.scrollTop -= this.averageheight; - } - } - if (again) { - await this.watchForTop(true, fragment); - } - return again; - } finally { - if (!already) { - if (this.div.scrollTop === 0) { - this.scrollTop = 1; - this.div.scrollTop = 10; - } - let height = 0; + private async removeElm(id: string) { + const back = this.backElm.get(id); + if (back) this.forElm.delete(back); + this.backElm.delete(id); - this.div.prepend(fragment); - this.whenFrag.forEach((_) => (height += _())); - this.div.scrollTop += height; + const forward = this.forElm.get(id); + if (forward) this.backElm.delete(forward); + this.forElm.delete(id); - this.whenFrag = []; - } + const elm = this.curElms.get(id); + this.curElms.delete(id); + await this.destroyFromID(id); + elm?.remove(); + } + private async getFromID(id: string) { + if (this.curElms.has(id)) { + return this.curElms.get(id) as HTMLElement; } + const elm = await this.getHTMLFromID(id); + this.curElms.set(id, elm); + this.weakElmId.set(elm, id); + this.observer.observe(elm); + return elm; } - deleteId(id: string) { - this.HTMLElements = this.HTMLElements.filter(([elm, elmid]) => { - if (id === elmid) { - elm.remove(); - return false; + //@ts-ignore-error + private checkIDs() { + const scroll = this.scroller; + if (!scroll) return; + const kids = Array.from(scroll.children) + .map((_) => this.weakElmId.get(_ as HTMLElement)) + .filter((_) => _ !== undefined); + let last = null; + for (const kid of kids) { + if (last === null) { + last = kid; } else { - return true; + if (this.backElm.get(last) !== kid) + console.log("back is wrong", kid, this.backElm.get(kid)); + if (this.forElm.get(kid) !== last) + console.log("for is wrong", last, this.backElm.get(last)); + last = kid; } - }); + } + const e = new Set(this.curElms.keys()); + if (e.symmetricDifference(new Set(kids)).size) + console.log("cur elms is wrong", e.symmetricDifference(new Set(kids))); } + private addLink(prev: string | undefined, next: string | undefined) { + if (prev) this.forElm.set(prev, next); + if (next) this.backElm.set(next, prev); + } + private async fillInTop() { + const scroll = this.scroller; + if (!scroll) return; + let top = this.curFocID; + const futElms: Promise[] = []; + let count = 0; + let limit = 50; - async watchForBottom(already = false, fragment = new DocumentFragment()): Promise { - let func: Function | undefined; - if (!already) func = this.snapBottom(); - if (!this.div) return false; - try { - let again = false; - const scrollBottom = this.scrollBottom; - if (scrollBottom < (already ? this.fillDist : this.minDist)) { - let nextid: string | undefined; - const lastelm = this.HTMLElements.at(-1); - if (lastelm) { - const previd = lastelm[1]; - nextid = await this.getIDFromOffset(previd, -1); - } - if (nextid) { - again = true; - const html = await this.getHTMLFromID(nextid); - fragment.appendChild(html); - this.HTMLElements.push([html, nextid]); - this.scrollBottom += this.averageheight; + while (top) { + count++; + if (count > 100) { + const list: string[] = []; + while (top) { + list.push(top); + top = this.forElm.get(top); } - } - if (scrollBottom > this.maxDist && this.remove) { - const html = this.HTMLElements.pop(); - if (html) { - await this.destroyFromID(html[1]); - this.scrollBottom -= this.averageheight; - again = true; + if (!supports) { + const heights = list + .map((_) => this.curElms.get(_)) + .map((_) => this.heightMap.get(_ as HTMLElement)) + .filter((_) => _ !== undefined) + .reduce((a, b) => a + b, 0); + this.div!.scrollTop -= heights; } + list.forEach((_) => this.removeElm(_)); + break; } - if (again) { - await this.watchForBottom(true, fragment); - } - return again; - } finally { - if (!already) { - this.div.append(fragment); - if (func) { - func(); + if (this.forElm.has(top) && this.curElms.has(top)) { + top = this.forElm.get(top); + } else if (count > limit) { + break; + } else { + limit = 75; + const id = await this.getIDFromOffset(top, 1); + this.addLink(top, id); + + if (id) { + futElms.push(this.getFromID(id)); } + top = id; } } + + const app = fragAppend(scroll, true); + const proms: Promise[] = []; + for (const elmProm of futElms) { + const elm = await elmProm; + proms.push(app(elm)); + } + await Promise.all(proms); } + private async fillInBottom() { + const scroll = this.scroller; + if (!scroll) return; + let bottom = this.curFocID; + const backElms: Promise[] = []; + let count = 0; + let limit = 50; + while (bottom) { + count++; + if (count > 100) { + const list: string[] = []; + while (bottom) { + list.push(bottom); + bottom = this.backElm.get(bottom); + } + list.forEach((_) => this.removeElm(_)); + break; + } + if (this.backElm.has(bottom) && this.curElms.has(bottom)) { + if (limit === 75) console.error("patchy?"); + bottom = this.backElm.get(bottom); + } else if (count > limit) { + break; + } else { + limit = 75; + const id = await this.getIDFromOffset(bottom, -1); + this.addLink(id, bottom); - async watchForChange(stop = false): Promise { - if (!this.remove) return false; - if (stop == true) { - let prom = this.changePromise; - while (this.changePromise) { - prom = this.changePromise; - await this.changePromise; - if (prom === this.changePromise) { - this.changePromise = undefined; - break; + if (id) { + backElms.push(this.getFromID(id)); } + bottom = id; } } - if (this.changePromise) { - this.watchtime = true; - return await this.changePromise; - } else { - this.watchtime = false; + + const app = fragAppend(scroll); + const proms: Promise[] = []; + for (const elmProm of backElms) { + const elm = await elmProm; + proms.push(app(elm)); } + await Promise.all(proms); + } - this.changePromise = new Promise(async (res) => { - try { - if (!this.div) { - res(false); - } - const out = (await Promise.allSettled([this.watchForTop(), this.watchForBottom()])) as { - value: boolean; - }[]; - const changed = out[0].value || out[1].value; - if (this.timeout === null && changed) { - this.timeout = setTimeout(this.updatestuff.bind(this), 300); - } - res(Boolean(changed)); - } catch (e) { - console.error(e); - res(false); - } finally { - if (stop === true) { - this.changePromise = undefined; - return; - } - setTimeout(() => { - this.changePromise = undefined; - if (this.watchtime) { - this.watchForChange(); - } - }, 300); + private filling?: Promise; + private async fillIn(refill = false) { + if (this.filling && !refill) { + return this.filling; + } + + await this.filling; + if (this.filling) return; + + const fill = new Promise(async (res) => { + await Promise.all([this.fillInTop(), this.fillInBottom()]); + if (this.filling === fill) { + this.filling = undefined; } + res(); }); - - return await this.changePromise; + this.filling = fill; + return fill; } - remove = true; + async focus(id: string, flash = true, sec = false): Promise { - let element: HTMLElement | undefined; - for (const thing of this.HTMLElements) { - if (thing[1] === id) { - element = thing[0]; - } + // debugger; + const scroller = this.scroller; + if (!scroller) return; + + let div = this.curElms.get(id); + if (div && !document.contains(div)) div = undefined; + let had = true; + this.curFocID = id; + + if (!div) { + await this.clearElms(); + had = false; + const obj = await this.getFromID(id); + scroller.append(obj); + div = obj; } - if (sec && element && document.contains(element)) { - if (flash) { - element.scrollIntoView({ - behavior: "smooth", - inline: "center", - block: "center", - }); - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - element.classList.remove("jumped"); - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - element.classList.add("jumped"); - } else { - element.scrollIntoView({ - block: "center", - }); - } - } else if (!sec) { - this.resetVars(); - //TODO may be a redundant loop, not 100% sure :P - for (const thing of this.HTMLElements) { - await this.destroyFromID(thing[1]); - } - this.HTMLElements = []; - await this.firstElement(id); - this.changePromise = new Promise(async (resolve) => { - try { - await this.updatestuff(); - this.remove = false; - await Promise.all([this.watchForBottom(), this.watchForTop()]); - - await new Promise((res) => queueMicrotask(res)); - await this.focus(id, !element && flash, true); - this.remove = true; - //TODO figure out why this fixes it and fix it for real :P - await new Promise(requestAnimationFrame); - await new Promise(requestAnimationFrame); - this.changePromise = undefined; - } finally { - resolve(true); - } + await this.fillIn(true); + if (had && !sec) { + div.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "center", }); - await this.changePromise; } else { - console.warn("elm not exist"); + console.log(had, sec); + div.scrollIntoView({ + block: "center", + }); + } + + if (flash) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + div.classList.remove("jumped"); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + div.classList.add("jumped"); } } async delete(): Promise { if (this.div) { this.div.remove(); - this.div = null; - } - this.resetVars(); - try { - for (const thing of this.HTMLElements) { - await this.destroyFromID(thing[1]); - } - } catch (e) { - console.error(e); - } - this.HTMLElements = []; - if (this.timeout) { - clearTimeout(this.timeout); } + this.clearElms(); } } diff --git a/src/webpage/infiniteScrollerOld.ts b/src/webpage/infiniteScrollerOld.ts new file mode 100644 index 00000000..7c73cfcc --- /dev/null +++ b/src/webpage/infiniteScrollerOld.ts @@ -0,0 +1,406 @@ +class InfiniteScroller { + readonly getIDFromOffset: (ID: string, offset: number) => Promise; + readonly getHTMLFromID: (ID: string) => Promise; + readonly destroyFromID: (ID: string) => Promise; + readonly reachesBottom: () => void; + private readonly minDist = 2000; + private readonly fillDist = 3000; + private readonly maxDist = 6000; + HTMLElements: [HTMLElement, string][] = []; + div: HTMLDivElement | null = null; + timeout: ReturnType | null = null; + beenloaded = false; + scrollBottom = 0; + scrollTop = 0; + needsupdate = true; + averageheight = 60; + watchtime = false; + changePromise: Promise | undefined; + scollDiv!: {scrollTop: number; scrollHeight: number; clientHeight: number}; + + resetVars() { + this.scrollTop = 0; + this.scrollBottom = 0; + this.averageheight = 60; + this.watchtime = false; + this.needsupdate = true; + this.beenloaded = false; + this.changePromise = undefined; + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + for (const thing of this.HTMLElements) { + this.destroyFromID(thing[1]); + } + this.HTMLElements = []; + } + constructor( + getIDFromOffset: InfiniteScroller["getIDFromOffset"], + getHTMLFromID: InfiniteScroller["getHTMLFromID"], + destroyFromID: InfiniteScroller["destroyFromID"], + reachesBottom: InfiniteScroller["reachesBottom"] = () => {}, + ) { + this.getIDFromOffset = getIDFromOffset; + this.getHTMLFromID = getHTMLFromID; + this.destroyFromID = destroyFromID; + this.reachesBottom = reachesBottom; + } + + async getDiv(initialId: string): Promise { + if (this.div) { + return this.div; + } + this.resetVars(); + const scroll = document.createElement("div"); + scroll.classList.add("scroller"); + this.div = scroll; + + this.div.addEventListener("scroll", () => { + this.checkscroll(); + if (this.scrollBottom < 5) { + this.scrollBottom = 5; + } + if (this.timeout === null) { + this.timeout = setTimeout(this.updatestuff.bind(this), 300); + } + this.watchForChange(); + }); + + let oldheight = 0; + new ResizeObserver(() => { + this.checkscroll(); + const func = this.snapBottom(); + this.updatestuff(); + const change = oldheight - scroll.offsetHeight; + if (change > 0 && this.div) { + this.div.scrollTop += change; + } + oldheight = scroll.offsetHeight; + this.watchForChange(); + func(); + }).observe(scroll); + + new ResizeObserver(() => this.watchForChange()).observe(scroll); + + await this.firstElement(initialId); + this.updatestuff(); + await this.watchForChange().then(() => { + this.updatestuff(); + this.beenloaded = true; + }); + + return scroll; + } + + checkscroll(): void { + if (this.beenloaded && this.div && !document.body.contains(this.div)) { + console.warn("not in document"); + this.div = null; + } + } + + async updatestuff(): Promise { + this.timeout = null; + if (!this.div) return; + + this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; + this.averageheight = this.div.scrollHeight / this.HTMLElements.length; + if (this.averageheight < 10) { + this.averageheight = 60; + } + this.scrollTop = this.div.scrollTop; + + if (this.scrollBottom < 5 && !(await this.watchForChange())) { + this.reachesBottom(); + } + if (!this.scrollTop) { + await this.watchForChange(); + } + this.needsupdate = false; + } + + async firstElement(id: string): Promise { + if (!this.div) return; + const html = await this.getHTMLFromID(id); + this.div.appendChild(html); + this.HTMLElements.push([html, id]); + } + + async addedBottom(): Promise { + await this.updatestuff(); + const func = this.snapBottom(); + if (this.changePromise) { + while (this.changePromise) { + await new Promise((res) => setTimeout(res, 30)); + } + } else { + await this.watchForChange(); + } + func(); + } + + snapBottom(): () => void { + const scrollBottom = this.scrollBottom; + return () => { + if (this.div && scrollBottom < 4) { + this.div.scrollTop = this.div.scrollHeight; + } + }; + } + whenFrag: (() => number)[] = []; + private async watchForTop(already = false, fragment = new DocumentFragment()): Promise { + const supports = CSS.supports("overflow-anchor", "auto"); + if (!this.div) return false; + const div = this.div; + try { + let again = false; + if (this.scrollTop < (already ? this.fillDist : this.minDist)) { + let nextid: string | undefined; + const firstelm = this.HTMLElements.at(0); + if (firstelm) { + const previd = firstelm[1]; + nextid = await this.getIDFromOffset(previd, 1); + } + + if (nextid) { + const html = await this.getHTMLFromID(nextid); + + if (!html) { + this.destroyFromID(nextid); + return false; + } + if (!supports) { + this.whenFrag.push(() => { + const box = html.getBoundingClientRect(); + return box.height; + }); + } + again = true; + fragment.prepend(html); + this.HTMLElements.unshift([html, nextid]); + this.scrollTop += this.averageheight; + } + } + if (this.scrollTop > this.maxDist && this.remove) { + const html = this.HTMLElements.shift(); + if (html) { + let dec = 0; + if (!supports) { + const box = html[0].getBoundingClientRect(); + dec = box.height; + } + again = true; + await this.destroyFromID(html[1]); + + div.scrollTop -= dec; + this.scrollTop -= this.averageheight; + } + } + if (again) { + await this.watchForTop(true, fragment); + } + return again; + } finally { + if (!already) { + if (this.div.scrollTop === 0) { + this.scrollTop = 1; + this.div.scrollTop = 10; + } + let height = 0; + + this.div.prepend(fragment); + this.whenFrag.forEach((_) => (height += _())); + this.div.scrollTop += height; + + this.whenFrag = []; + } + } + } + deleteId(id: string) { + this.HTMLElements = this.HTMLElements.filter(([elm, elmid]) => { + if (id === elmid) { + elm.remove(); + return false; + } else { + return true; + } + }); + } + + async watchForBottom(already = false, fragment = new DocumentFragment()): Promise { + let func: Function | undefined; + if (!already) func = this.snapBottom(); + if (!this.div) return false; + try { + let again = false; + const scrollBottom = this.scrollBottom; + if (scrollBottom < (already ? this.fillDist : this.minDist)) { + let nextid: string | undefined; + const lastelm = this.HTMLElements.at(-1); + if (lastelm) { + const previd = lastelm[1]; + nextid = await this.getIDFromOffset(previd, -1); + } + if (nextid) { + again = true; + const html = await this.getHTMLFromID(nextid); + fragment.appendChild(html); + this.HTMLElements.push([html, nextid]); + this.scrollBottom += this.averageheight; + } + } + if (scrollBottom > this.maxDist && this.remove) { + const html = this.HTMLElements.pop(); + if (html) { + await this.destroyFromID(html[1]); + this.scrollBottom -= this.averageheight; + again = true; + } + } + if (again) { + await this.watchForBottom(true, fragment); + } + return again; + } finally { + if (!already) { + this.div.append(fragment); + if (func) { + func(); + } + } + } + } + + async watchForChange(stop = false): Promise { + if (!this.remove) return false; + if (stop == true) { + let prom = this.changePromise; + while (this.changePromise) { + prom = this.changePromise; + await this.changePromise; + if (prom === this.changePromise) { + this.changePromise = undefined; + break; + } + } + } + if (this.changePromise) { + this.watchtime = true; + return await this.changePromise; + } else { + this.watchtime = false; + } + + this.changePromise = new Promise(async (res) => { + try { + if (!this.div) { + res(false); + } + const out = (await Promise.allSettled([this.watchForTop(), this.watchForBottom()])) as { + value: boolean; + }[]; + const changed = out[0].value || out[1].value; + if (this.timeout === null && changed) { + this.timeout = setTimeout(this.updatestuff.bind(this), 300); + } + res(Boolean(changed)); + } catch (e) { + console.error(e); + res(false); + } finally { + if (stop === true) { + this.changePromise = undefined; + return; + } + setTimeout(() => { + this.changePromise = undefined; + if (this.watchtime) { + this.watchForChange(); + } + }, 300); + } + }); + + return await this.changePromise; + } + remove = true; + async focus(id: string, flash = true, sec = false): Promise { + let element: HTMLElement | undefined; + for (const thing of this.HTMLElements) { + if (thing[1] === id) { + element = thing[0]; + } + } + if (sec && element && document.contains(element)) { + if (flash) { + element.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "center", + }); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + element.classList.remove("jumped"); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + element.classList.add("jumped"); + } else { + element.scrollIntoView({ + block: "center", + }); + } + } else if (!sec) { + this.resetVars(); + //TODO may be a redundant loop, not 100% sure :P + for (const thing of this.HTMLElements) { + await this.destroyFromID(thing[1]); + } + this.HTMLElements = []; + await this.firstElement(id); + this.changePromise = new Promise(async (resolve) => { + try { + await this.updatestuff(); + this.remove = false; + await Promise.all([this.watchForBottom(), this.watchForTop()]); + + await new Promise((res) => queueMicrotask(res)); + await this.focus(id, !element && flash, true); + this.remove = true; + //TODO figure out why this fixes it and fix it for real :P + await new Promise(requestAnimationFrame); + await new Promise(requestAnimationFrame); + this.changePromise = undefined; + } finally { + resolve(true); + } + }); + await this.changePromise; + } else { + console.warn("elm not exist"); + } + } + + async delete(): Promise { + if (this.div) { + this.div.remove(); + this.div = null; + } + this.resetVars(); + try { + for (const thing of this.HTMLElements) { + await this.destroyFromID(thing[1]); + } + } catch (e) { + console.error(e); + } + this.HTMLElements = []; + if (this.timeout) { + clearTimeout(this.timeout); + } + } +} + +export {InfiniteScroller}; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index c24766ab..ea841964 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -15,7 +15,6 @@ import { messagejson, presencejson, readyjson, - sessionJson, startTypingjson, wsjson, } from "./jsontypes.js"; @@ -4314,9 +4313,7 @@ class Localuser { html.addEventListener("click", async () => { try { sideContainDiv.classList.add("hideSearchDiv"); - (await message.channel.getmessage(message.id))?.deleteDiv(); - - await message.channel.getHTML(true, true, message.id); + await message.channel.focus(message.id); } catch (e) { console.error(e); } diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 9a68a028..fbfb06ea 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -676,15 +676,11 @@ class Message extends SnowFlake { div.append(span); span.classList.add("blocked"); span.onclick = (_) => { - const scroll = this.channel.infinite.scrollTop; let next: Message | undefined = this; while (next?.author === this.author) { next.generateMessage(); next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); } - if (this.channel.infinite.scollDiv && scroll) { - this.channel.infinite.scollDiv.scrollTop = scroll; - } }; } } else { @@ -706,7 +702,6 @@ class Message extends SnowFlake { span.textContent = I18n.showBlockedMessages(count + ""); build.append(span); span.onclick = (_) => { - const scroll = this.channel.infinite.scrollTop; const func = this.channel.infinite.snapBottom(); let next: Message | undefined = this; while (next?.author === this.author) { @@ -714,10 +709,7 @@ class Message extends SnowFlake { next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); console.log("loopy"); } - if (this.channel.infinite.scollDiv && scroll) { - func(); - this.channel.infinite.scollDiv.scrollTop = scroll; - } + func(); }; div.appendChild(build); return div; @@ -781,7 +773,7 @@ class Message extends SnowFlake { reply.onclick = (_) => { if (!this.message_reference) return; // TODO: FIX this - this.channel.infinite.focus(this.message_reference.message_id); + this.channel.focus(this.message_reference.message_id); }; div.appendChild(replyline); }