From a7b260ccce97fdf6161dea993f67c38f312ddf37 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 12:06:25 -0600 Subject: [PATCH] new scroller --- src/webpage/channel.ts | 10 +- src/webpage/infiniteScroller.ts | 608 +++++++++++++------------------- src/webpage/message.ts | 10 +- 3 files changed, 260 insertions(+), 368 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 6ecd857..fdac34a 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -466,6 +466,7 @@ class Channel extends SnowFlake { return document.createElement("div"); }, async (id: string) => { + console.log(id); const message = this.messages.get(id); try { if (message) { @@ -2226,17 +2227,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 { diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 7c73cfc..9154e1a 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -3,38 +3,24 @@ class InfiniteScroller { 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,99 +32,60 @@ 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 createObserver(root: HTMLDivElement) { + const scroller = root.children[0]; + function sorted() { + return Array.from(scroller.children).filter((_) => visable.has(_)) as HTMLElement[]; } - this.resetVars(); + 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); + } + } + } + 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(); + + const middle = elms[(elms.length / 2) | 0]; + console.log(obv.target, middle, elms); + 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}, + ); + } + + async getDiv(initialId: string, flash = false): Promise { + const div = document.createElement("div"); + div.classList.add("scroller"); + this.div = div; + const scroll = document.createElement("div"); - scroll.classList.add("scroller"); - this.div = scroll; + div.append(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(); - }); + this.createObserver(div); - 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; + this.focus(initialId, flash, true); + return div; } - 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(); - } + async addedBottom(): Promise {} snapBottom(): () => void { const scrollBottom = this.scrollBottom; @@ -148,259 +95,210 @@ class InfiniteScroller { } }; } - 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; + this.removeElm(id); + } + + 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 = ""; + } + + private async removeElm(id: string) { + const back = this.backElm.get(id); + console.log(id, back, "back"); + if (back) this.forElm.delete(back); + + const forward = this.forElm.get(id); + console.log(id, forward, "for"); + if (forward) this.backElm.delete(forward); + + // This purposefully leaves all references pointing out alone so nothing breaks + 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; + } + 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; + while (top) { + count++; + if (count > 100) { + const list: string[] = []; + while (top) { + list.push(top); + console.log("top"); + top = this.forElm.get(top); + } + console.log(list, top); + list.forEach((_) => this.removeElm(_)); + break; + } + 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; + } + } + for (const elmProm of futElms) { + const elm = await elmProm; + scroll.prepend(elm); + } + } + private async fillInBottom() { + const scroll = this.scroller; + if (!scroll) return; + let bottom = this.curFocID; + const backElms: [string, Promise, string][] = []; + let count = 0; + let limit = 50; + while (bottom) { + count++; + if (count > 100) { + const list: string[] = []; + while (bottom) { + list.push(bottom); + console.log("bottom"); + bottom = this.backElm.get(bottom); + } + console.log(list, bottom); + list.forEach((_) => this.removeElm(_)); + break; + } + if (this.backElm.has(bottom) && this.curElms.has(bottom)) { + if (limit === 75) throw new 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); + + if (id) { + if (this.curElms.has(id)) debugger; + console.log(this.curElms.has(bottom)); + backElms.push([id, this.getFromID(id), bottom]); + } + bottom = id; + } + } + for (const [id, elmProm] of backElms) { + const elm = await elmProm; + if (!this.curElms.has(id)) console.error("bottom is missing"); + scroll.append(elm); + } + console.log(backElms); + } + + filling?: Promise; + private async fillIn(refill = false) { + if (this.filling && !refill) { + return this.filling; + } + await this.filling; + + const fill = new Promise(async (res) => { + await Promise.all([this.fillInTop(), this.fillInBottom()]); + res(); + if (this.filling === fill) { + this.filling = undefined; + } + this.checkIDs(); }); + this.filling = fill; + return fill; } - 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()]); + const scroller = this.scroller; + if (!scroller) return; + await this.clearElms(); - 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); - } + let div = this.curElms.get(id); + let had = true; + + if (!div) { + had = false; + const obj = await this.getFromID(id); + scroller.append(obj); + this.curFocID = id; + await this.fillIn(true); + div = obj; + } + if (had && !sec) { + div.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "center", }); - await this.changePromise; } else { - console.warn("elm not exist"); + div.scrollIntoView({ + block: "center", + }); + } + + if (flash) { } } - 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); - } - } + async delete(): Promise {} } export {InfiniteScroller}; diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 9a68a02..3bb85f7 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;