From a7b260ccce97fdf6161dea993f67c38f312ddf37 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 12:06:25 -0600 Subject: [PATCH 01/18] new scroller --- src/webpage/channel.ts | 10 +- src/webpage/infiniteScroller.ts | 564 +++++++++++++------------------- src/webpage/message.ts | 10 +- 3 files changed, 238 insertions(+), 346 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 6ecd857d..fdac34ab 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 7c73cfcc..9154e1aa 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 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; - } + 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(); - checkscroll(): void { - if (this.beenloaded && this.div && !document.body.contains(this.div)) { - console.warn("not in document"); - this.div = null; - } + 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 updatestuff(): Promise { - this.timeout = null; - if (!this.div) return; + async getDiv(initialId: string, flash = false): Promise { + const div = document.createElement("div"); + div.classList.add("scroller"); + this.div = div; - 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; + const scroll = document.createElement("div"); + div.append(scroll); - if (this.scrollBottom < 5 && !(await this.watchForChange())) { - this.reachesBottom(); - } - if (!this.scrollTop) { - await this.watchForChange(); - } - this.needsupdate = false; - } + this.createObserver(div); - async firstElement(id: string): Promise { - if (!this.div) return; - const html = await this.getHTMLFromID(id); - this.div.appendChild(html); - this.HTMLElements.push([html, id]); + this.focus(initialId, flash, true); + return div; } - 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); + deleteId(id: string) { + this.removeElm(id); + } - 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); + console.log(id, back, "back"); + if (back) this.forElm.delete(back); - this.div.prepend(fragment); - this.whenFrag.forEach((_) => (height += _())); - this.div.scrollTop += height; + const forward = this.forElm.get(id); + console.log(id, forward, "for"); + if (forward) this.backElm.delete(forward); - this.whenFrag = []; - } + // 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; } - deleteId(id: string) { - this.HTMLElements = this.HTMLElements.filter(([elm, elmid]) => { - if (id === elmid) { - elm.remove(); - return false; + 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))); } - - 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; + 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 (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; } } + 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); - 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) { + if (this.curElms.has(id)) debugger; + console.log(this.curElms.has(bottom)); + backElms.push([id, this.getFromID(id), bottom]); } + bottom = id; } } - if (this.changePromise) { - this.watchtime = true; - return await this.changePromise; - } else { - this.watchtime = false; + 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); + } - 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); + 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(); }); - - 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]; - } - } - 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", + }); } - } - 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); + if (flash) { } } + + async delete(): Promise {} } export {InfiniteScroller}; diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 9a68a028..3bb85f73 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; From a52c9fb05fcef5c0086021bb0bba5bd5ccd43f66 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 12:16:06 -0600 Subject: [PATCH 02/18] remove danglers --- src/webpage/infiniteScroller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 9154e1aa..f74d3dfe 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -114,10 +114,12 @@ class InfiniteScroller { const back = this.backElm.get(id); console.log(id, back, "back"); if (back) this.forElm.delete(back); + this.backElm.delete(id); const forward = this.forElm.get(id); console.log(id, forward, "for"); if (forward) this.backElm.delete(forward); + this.forElm.delete(id); // This purposefully leaves all references pointing out alone so nothing breaks const elm = this.curElms.get(id); From 7a2022aca636a0bbd7778c6b7f911e7ce03f6438 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 12:16:38 -0600 Subject: [PATCH 03/18] make it async --- src/webpage/infiniteScroller.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index f74d3dfe..6f3babbd 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -96,8 +96,8 @@ class InfiniteScroller { }; } - deleteId(id: string) { - this.removeElm(id); + async deleteId(id: string) { + await this.removeElm(id); } private async clearElms() { @@ -121,7 +121,6 @@ class InfiniteScroller { if (forward) this.backElm.delete(forward); this.forElm.delete(id); - // 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); From d0ecdbeae43b5f6a6cbc90405776072c3d53fe43 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 12:25:40 -0600 Subject: [PATCH 04/18] get rid of debugging elements --- src/webpage/channel.ts | 1 - src/webpage/infiniteScroller.ts | 17 +++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index fdac34ab..0918bf35 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -466,7 +466,6 @@ class Channel extends SnowFlake { return document.createElement("div"); }, async (id: string) => { - console.log(id); const message = this.messages.get(id); try { if (message) { diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 6f3babbd..749d99fb 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -58,7 +58,6 @@ class InfiniteScroller { 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; @@ -112,12 +111,10 @@ class InfiniteScroller { private async removeElm(id: string) { const back = this.backElm.get(id); - console.log(id, back, "back"); if (back) this.forElm.delete(back); this.backElm.delete(id); const forward = this.forElm.get(id); - console.log(id, forward, "for"); if (forward) this.backElm.delete(forward); this.forElm.delete(id); @@ -175,10 +172,8 @@ class InfiniteScroller { 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; } @@ -206,7 +201,7 @@ class InfiniteScroller { const scroll = this.scroller; if (!scroll) return; let bottom = this.curFocID; - const backElms: [string, Promise, string][] = []; + const backElms: Promise[] = []; let count = 0; let limit = 50; while (bottom) { @@ -215,10 +210,8 @@ class InfiniteScroller { 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; } @@ -233,19 +226,15 @@ class InfiniteScroller { 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]); + backElms.push(this.getFromID(id)); } bottom = id; } } - for (const [id, elmProm] of backElms) { + for (const 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; From da964384e73fbce146abe74d118ea854dd55fddd Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 13:41:27 -0600 Subject: [PATCH 05/18] add to bottom --- src/webpage/infiniteScroller.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 749d99fb..3042f73c 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -84,7 +84,14 @@ class InfiniteScroller { return div; } - async addedBottom(): Promise {} + async addedBottom(): Promise { + 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); + this.fillIn(); + } snapBottom(): () => void { const scrollBottom = this.scrollBottom; @@ -237,7 +244,7 @@ class InfiniteScroller { } } - filling?: Promise; + private filling?: Promise; private async fillIn(refill = false) { if (this.filling && !refill) { return this.filling; From 4f89c144863c3f9236a1dac66ae87457ca8ca6a0 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 13:44:00 -0600 Subject: [PATCH 06/18] proper delete --- src/webpage/infiniteScroller.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 3042f73c..5871f68a 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -103,7 +103,11 @@ class InfiniteScroller { } 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); } private async clearElms() { From 3793aa09034d0432619903163e221a7ffb671e6a Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 14:45:40 -0600 Subject: [PATCH 07/18] more changes --- src/webpage/channel.ts | 58 ++++++++++++++++++-------------- src/webpage/infiniteScroller.ts | 59 +++++++++++++++++++++++++++------ src/webpage/localuser.ts | 4 +-- src/webpage/message.ts | 2 +- 4 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 0918bf35..b5d80864 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -1239,35 +1239,45 @@ 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; + } + m1 = this.idToNext.get(m1); + } + m1 = m.id; + for (let i = 0; i <= 10; i++) { + if (!m1) { + waits.push(this.grabAfter(id)); + break; } + m1 = this.idToPrev.get(m1); } - console.log(await this.getmessage(id)); + await Promise.all(waits); + } + async focus(id: string) { + 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); } diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 5871f68a..91a88445 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -68,6 +68,16 @@ class InfiniteScroller { }, {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(); + } + }); } async getDiv(initialId: string, flash = false): Promise { @@ -83,23 +93,40 @@ class InfiniteScroller { 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; + } + } 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); - this.fillIn(); + await this.fillIn(); + snap(); } snapBottom(): () => void { - const scrollBottom = this.scrollBottom; - return () => { - if (this.div && scrollBottom < 4) { - this.div.scrollTop = this.div.scrollHeight; - } - }; + 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 { + return nothing; + } } async deleteId(id: string) { @@ -261,28 +288,29 @@ class InfiniteScroller { if (this.filling === fill) { this.filling = undefined; } - this.checkIDs(); }); this.filling = fill; return fill; } async focus(id: string, flash = true, sec = false): Promise { + // debugger; const scroller = this.scroller; if (!scroller) return; - await this.clearElms(); 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); - this.curFocID = id; - await this.fillIn(true); div = obj; } + await this.fillIn(true); if (had && !sec) { div.scrollIntoView({ behavior: "smooth", @@ -290,12 +318,21 @@ class InfiniteScroller { block: "center", }); } else { + 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"); } } diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index c24766ab..ba419030 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -4314,9 +4314,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 3bb85f73..fbfb06ea 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -773,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); } From 718102c66806d1cd2e1f4146519418fcb8161a9f Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 14:52:55 -0600 Subject: [PATCH 08/18] more fixes --- src/webpage/channel.ts | 4 ++-- src/webpage/infiniteScroller.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index b5d80864..b3153669 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -1249,7 +1249,7 @@ class Channel extends SnowFlake { waits.push(this.grabBefore(id)); break; } - m1 = this.idToNext.get(m1); + if (this.idToNext.has(m1) && !(m1 = this.idToNext.get(m1))) break; } m1 = m.id; for (let i = 0; i <= 10; i++) { @@ -1257,7 +1257,7 @@ class Channel extends SnowFlake { waits.push(this.grabAfter(id)); break; } - m1 = this.idToPrev.get(m1); + if (this.idToPrev.has(m1) && !(m1 = this.idToPrev.get(m1))) break; } await Promise.all(waits); } diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 91a88445..a26bdfea 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -336,7 +336,12 @@ class InfiniteScroller { } } - async delete(): Promise {} + async delete(): Promise { + if (this.div) { + this.div.remove(); + } + this.clearElms(); + } } export {InfiniteScroller}; From fb4140a145773643637e16152f0c1ead4791740a Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 16:00:36 -0600 Subject: [PATCH 09/18] webkit + frag --- src/webpage/infiniteScroller.ts | 46 +++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index a26bdfea..c6348f6a 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -1,3 +1,31 @@ +function fragAppend(div: HTMLElement, pre = false) { + let qued = false; + function appendFrag() { + const elms = Array.from(frag.children) as HTMLElement[]; + div[pre ? "prepend" : "append"](frag); + if (pre && !supports) { + const h = elms.map((_) => _.getBoundingClientRect().height).reduce((a, b) => a + b, 0); + const p = div.parentNode; + if (p instanceof HTMLElement) { + p.scrollTop += h; + console.warn(h); + } + } + } + const frag = document.createDocumentFragment(); + return (elm: HTMLElement) => { + frag[pre ? "prepend" : "append"](elm); + if (!qued) { + queueMicrotask(() => { + appendFrag(); + qued = false; + }); + qued = true; + } + }; +} + +const supports = CSS.supports("overflow-anchor", "auto"); class InfiniteScroller { readonly getIDFromOffset: (ID: string, offset: number) => Promise; readonly getHTMLFromID: (ID: string) => Promise; @@ -34,6 +62,7 @@ class InfiniteScroller { } observer: IntersectionObserver = new IntersectionObserver(console.log); + private heightMap = new WeakMap(); private createObserver(root: HTMLDivElement) { const scroller = root.children[0]; function sorted() { @@ -49,6 +78,8 @@ class InfiniteScroller { } else { visable.delete(obv.target); } + + this.heightMap.set(obv.target, obv.boundingClientRect.height); } } for (const obv of obvs) { @@ -204,6 +235,7 @@ class InfiniteScroller { const futElms: Promise[] = []; let count = 0; let limit = 50; + while (top) { count++; if (count > 100) { @@ -212,6 +244,12 @@ class InfiniteScroller { list.push(top); top = this.forElm.get(top); } + 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; } @@ -230,9 +268,11 @@ class InfiniteScroller { top = id; } } + + const app = fragAppend(scroll, true); for (const elmProm of futElms) { const elm = await elmProm; - scroll.prepend(elm); + app(elm); } } private async fillInBottom() { @@ -269,9 +309,11 @@ class InfiniteScroller { bottom = id; } } + + const app = fragAppend(scroll); for (const elmProm of backElms) { const elm = await elmProm; - scroll.append(elm); + app(elm); } } From 01048c1d2c6a69f9de541c391895b2d4ba71a03f Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 16:15:45 -0600 Subject: [PATCH 10/18] lag reduction --- src/webpage/channel.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index b3153669..6f9793cf 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -2052,17 +2052,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; @@ -2132,16 +2130,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 await (const response of messages) { let messager: Message; if (this.messages.has(response.id)) { messager = this.messages.get(response.id) as Message; From 9ed2dbdfd52b3ed16675367da9ebb7359fd12e0a Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 17:59:31 -0600 Subject: [PATCH 11/18] some cleanup --- src/webpage/channel.ts | 5 ++--- src/webpage/infiniteScroller.ts | 1 + src/webpage/localuser.ts | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 6f9793cf..fe581916 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"; @@ -2140,7 +2139,7 @@ class Channel extends SnowFlake { ).json()) as messagejson[]; let previd = id; let i = 0; - for await (const response of messages) { + for (const response of messages) { let messager: Message; if (this.messages.has(response.id)) { messager = this.messages.get(response.id) as Message; @@ -2159,7 +2158,7 @@ class Channel extends SnowFlake { previd = messager.id; - if (messages.done && i < 99) { + if (i < 99) { this.topid = previd; } diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index c6348f6a..ab4abec0 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -202,6 +202,7 @@ class InfiniteScroller { this.observer.observe(elm); return elm; } + //@ts-ignore-error private checkIDs() { const scroll = this.scroller; if (!scroll) return; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index ba419030..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"; From f2a1bed372c9a5023a8a59fc96994a6291a242ba Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 26 Jan 2026 17:59:49 -0600 Subject: [PATCH 12/18] include the old source for reference --- src/webpage/infiniteScrollerOld.ts | 406 +++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 src/webpage/infiniteScrollerOld.ts 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}; From ce134d331a6881d9b2fdf3f04baa4b7623fd5953 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 27 Jan 2026 10:03:39 -0600 Subject: [PATCH 13/18] check for support oops --- src/webpage/infiniteScroller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index ab4abec0..acadabd7 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -250,7 +250,7 @@ class InfiniteScroller { .map((_) => this.heightMap.get(_ as HTMLElement)) .filter((_) => _ !== undefined) .reduce((a, b) => a + b, 0); - this.div!.scrollTop -= heights; + if (!supports) this.div!.scrollTop -= heights; list.forEach((_) => this.removeElm(_)); break; } From c281173f43a4bcbb2d79580ad8e4fd2c7cfa22a1 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 27 Jan 2026 11:06:28 -0600 Subject: [PATCH 14/18] properly use the fragement --- src/webpage/infiniteScroller.ts | 61 +++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index acadabd7..2bff867a 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -1,8 +1,23 @@ 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) { const h = elms.map((_) => _.getBoundingClientRect().height).reduce((a, b) => a + b, 0); const p = div.parentNode; @@ -13,16 +28,20 @@ function fragAppend(div: HTMLElement, pre = false) { } } const frag = document.createDocumentFragment(); - return (elm: HTMLElement) => { - frag[pre ? "prepend" : "append"](elm); - if (!qued) { - queueMicrotask(() => { - appendFrag(); - qued = false; - }); - qued = true; - } - }; + 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"); @@ -245,12 +264,14 @@ class InfiniteScroller { list.push(top); top = this.forElm.get(top); } - const heights = list - .map((_) => this.curElms.get(_)) - .map((_) => this.heightMap.get(_ as HTMLElement)) - .filter((_) => _ !== undefined) - .reduce((a, b) => a + b, 0); - if (!supports) this.div!.scrollTop -= heights; + 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; } @@ -271,10 +292,12 @@ class InfiniteScroller { } const app = fragAppend(scroll, true); + const proms: Promise[] = []; for (const elmProm of futElms) { const elm = await elmProm; - app(elm); + proms.push(app(elm)); } + await Promise.all(proms); } private async fillInBottom() { const scroll = this.scroller; @@ -312,10 +335,12 @@ class InfiniteScroller { } const app = fragAppend(scroll); + const proms: Promise[] = []; for (const elmProm of backElms) { const elm = await elmProm; - app(elm); + proms.push(app(elm)); } + await Promise.all(proms); } private filling?: Promise; From 478e20d3e83b9fef42a02cbeb65787b10fdc6194 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 27 Jan 2026 11:13:06 -0600 Subject: [PATCH 15/18] another bug squashed --- src/webpage/infiniteScroller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 2bff867a..01c4c729 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -255,6 +255,7 @@ class InfiniteScroller { const futElms: Promise[] = []; let count = 0; let limit = 50; + const r = (Math.random() * 1000) ^ 0; while (top) { count++; @@ -348,14 +349,16 @@ class InfiniteScroller { 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()]); - res(); if (this.filling === fill) { this.filling = undefined; } + res(); }); this.filling = fill; return fill; From 646213552464f1706b9748a6ed76e2704d25c5b0 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 27 Jan 2026 11:19:38 -0600 Subject: [PATCH 16/18] maybe make webkit work betterer? --- src/webpage/infiniteScroller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 01c4c729..4b31ed44 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -19,7 +19,14 @@ function fragAppend(div: HTMLElement, pre = false) { par.scrollTop -= 3; } if (pre && !supports) { - const h = elms.map((_) => _.getBoundingClientRect().height).reduce((a, b) => a + b, 0); + 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; From 05c0a09005cc80360a7a1986924659cfad19d56e Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 27 Jan 2026 11:20:07 -0600 Subject: [PATCH 17/18] remove debug line --- src/webpage/infiniteScroller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 4b31ed44..3cd8994a 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -30,7 +30,6 @@ function fragAppend(div: HTMLElement, pre = false) { const p = div.parentNode; if (p instanceof HTMLElement) { p.scrollTop += h; - console.warn(h); } } } @@ -262,7 +261,6 @@ class InfiniteScroller { const futElms: Promise[] = []; let count = 0; let limit = 50; - const r = (Math.random() * 1000) ^ 0; while (top) { count++; From 358ce693c55cf2c2b51adcf321352215f7dfc5bd Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 27 Jan 2026 13:54:17 -0600 Subject: [PATCH 18/18] more bug fixing --- src/webpage/channel.ts | 25 +++++++++++++------------ src/webpage/infiniteScroller.ts | 6 +++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index fe581916..59ef8082 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -1260,7 +1260,7 @@ class Channel extends SnowFlake { } await Promise.all(waits); } - async focus(id: string) { + async focus(id: string, flash = true) { const prom = this.getMessages(id); if (await Promise.race([prom, new Promise((res) => setTimeout(() => res(true), 300))])) { @@ -1278,7 +1278,7 @@ class Channel extends SnowFlake { try { 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; @@ -2849,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")) { @@ -2887,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; @@ -2902,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 3cd8994a..230784c0 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -186,8 +186,8 @@ class InfiniteScroller { } 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; + 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); @@ -324,7 +324,7 @@ class InfiniteScroller { break; } if (this.backElm.has(bottom) && this.curElms.has(bottom)) { - if (limit === 75) throw new Error("patchy?"); + if (limit === 75) console.error("patchy?"); bottom = this.backElm.get(bottom); } else if (count > limit) { break;