new scroller

This commit is contained in:
MathMan05
2026-01-26 12:06:25 -06:00
parent 7f2a3f6ae1
commit a7b260ccce
3 changed files with 260 additions and 368 deletions
+6 -4
View File
@@ -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 {
+253 -355
View File
@@ -3,38 +3,24 @@ class InfiniteScroller {
readonly getHTMLFromID: (ID: string) => Promise<HTMLElement>;
readonly destroyFromID: (ID: string) => Promise<boolean>;
readonly reachesBottom: () => void;
private readonly minDist = 2000;
private readonly fillDist = 3000;
private readonly maxDist = 6000;
HTMLElements: [HTMLElement, string][] = [];
div: HTMLDivElement | null = null;
timeout: ReturnType<typeof setTimeout> | null = null;
beenloaded = false;
scrollBottom = 0;
scrollTop = 0;
needsupdate = true;
averageheight = 60;
watchtime = false;
changePromise: Promise<boolean> | 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<string, HTMLElement>();
private backElm = new Map<string, string | undefined>();
private forElm = new Map<string, string | undefined>();
private weakElmId = new WeakMap<HTMLElement, string>();
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<HTMLDivElement> {
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<Element>();
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<HTMLDivElement> {
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<void> {
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<void> {
if (!this.div) return;
const html = await this.getHTMLFromID(id);
this.div.appendChild(html);
this.HTMLElements.push([html, id]);
}
async addedBottom(): Promise<void> {
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<void> {}
snapBottom(): () => void {
const scrollBottom = this.scrollBottom;
@@ -148,259 +95,210 @@ class InfiniteScroller {
}
};
}
whenFrag: (() => number)[] = [];
private async watchForTop(already = false, fragment = new DocumentFragment()): Promise<boolean> {
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<HTMLElement>[] = [];
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<HTMLElement>, 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<void>;
private async fillIn(refill = false) {
if (this.filling && !refill) {
return this.filling;
}
await this.filling;
const fill = new Promise<void>(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<boolean> {
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<boolean> {
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<boolean>(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<void> {
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<boolean>(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<void>((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<void> {
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<void> {}
}
export {InfiniteScroller};
+1 -9
View File
@@ -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;