fix scrolling

This commit is contained in:
tezlm 2024-01-19 16:03:24 -08:00
parent b909401063
commit 13f1a4cdec
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
3 changed files with 92 additions and 62 deletions

View file

@ -115,7 +115,7 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
});
function redux(action: Action) {
console.log("dispatch action", action);
console.log("context::dispatch", action);
switch (action.type) {
case "sidebar.focus":
update("sidebar", action.item);
@ -161,7 +161,7 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
}
function handleState(state: ClientState) {
console.log("client state:", state);
console.log("context::state", state);
if (state.state === "logout") {
localStorage.removeItem("auth");
globals.client.off("state", handleState);
@ -184,7 +184,7 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
}
function handleHash() {
console.log("hash change", location.hash, location.hash.split("/"));
console.log("context::hash", location.hash, location.hash.split("/"));
if (!globals.client) {
update("scene", { type: "auth", sub: "login" });

View file

@ -60,6 +60,35 @@ function sanitizeInline(html: string): string {
});
}
function escape(str: string) {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
export function renderText(props: { text: any, formatting?: boolean, fallback?: string }) {
function hasBody() {
return props.text?.some?.((i: any) => !i.type || ["text/plain", "text/html"].includes(i.type)) || typeof props.text === "string";
}
function getBody() {
if (typeof props.text === "string") return escape(props.text);
if (props.formatting !== false) {
const html = props.text.find((i: any) => i.type === "text/html")?.body;
if (html) return sanitize(html);
}
const text = props.text.find((i: any) => !i.type || i.type === "text/plain")?.body;
if (text) return `<p>${escape(text)}</p>`;
return "";
}
return hasBody() ? getBody() : props.fallback;
}
export function TextBlock(props: VoidProps<{ text: any, formatting?: boolean, fallback?: JSX.Element }>) {
function escape(str: string) {
return str

View file

@ -12,7 +12,7 @@ import { useGlobals } from "./Context";
import "./Thread.scss";
import { createProgress, delay, shouldSplit } from "./util";
import { createStore, reconcile } from "solid-js/store";
import { classList } from "solid-js/web";
import { Dynamic, classList } from "solid-js/web";
import { useFloating } from "solid-floating-ui";
const Editor = lazy(() => import("./Editor"));
@ -78,7 +78,9 @@ function createTimeline(timelineSet: Accessor<ThreadTimelineSet>) {
} else {
items.push({ type: "spacer", key: "space-end" });
}
console.time("perf::updateItems");
setItems((old) => [...reconcile(items, { key: "key" })(old)]);
console.timeEnd("perf::updateItems");
}
async function init() {
@ -105,6 +107,7 @@ function createTimeline(timelineSet: Accessor<ThreadTimelineSet>) {
async function backwards() {
if (status() !== "ready") return;
if (isAtBeginning()) return;
console.log("timeline::backwards");
setStatus("loading");
const currentInfo = info()!;
@ -122,6 +125,7 @@ function createTimeline(timelineSet: Accessor<ThreadTimelineSet>) {
async function forwards() {
if (status() !== "ready") return;
if (isAtEnd()) return;
console.log("timeline::forwards");
setStatus("loading");
const currentInfo = info()!;
@ -166,7 +170,6 @@ function createTimeline(timelineSet: Accessor<ThreadTimelineSet>) {
oldTimeline?.off("timelineAppend", append);
oldTimeline?.off("timelineUpdate", append);
oldTimeline = timeline();
console.log("isLive", timeline() === timelineSet().live);
});
onCleanup(() => {
@ -202,44 +205,42 @@ function createList<T extends { class?: string }>(options: {
const [wrapperEl, setWrapperEl] = createSignal<HTMLElement>();
const [topEl, setTopEl] = createSignal<HTMLElement>();
const [bottomEl, setBottomEl] = createSignal<HTMLElement>();
let topOffset: number | undefined;
let topHeight: number | undefined;
let topRef: HTMLElement | undefined;
let bottomOffset: number | undefined;
let bottomHeight: number | undefined;
let bottomRef: HTMLElement | undefined;
let anchorRef: Element;
let anchorRect: DOMRect;
let shouldAutoscroll = false;
const margin = 0;
const observer = new IntersectionObserver((entries) => {
const el = entries[0];
console.log("list::intersection", entries);
if (el.target === topEl()) {
if (el.isIntersecting) {
console.log("scroll up");
console.log("list::up");
anchorRef = el.target;
anchorRect = el.boundingClientRect;
options.onPaginate?.("backwards");
}
} else if (entries[0].target === bottomEl()) {
if (el.isIntersecting) {
console.log("scroll down");
console.log("list::down");
shouldAutoscroll = options.autoscroll?.() || false;
console.log({ shouldAutoscroll })
anchorRef = el.target;
anchorRect = el.boundingClientRect;
options.onPaginate?.("forwards");
} else {
shouldAutoscroll = false;
console.log({ shouldAutoscroll })
}
} else {
console.warn("unknown intersection entry");
console.warn("list::unknownIntersectionEntry");
}
}, {
rootMargin: `${margin}px 0px ${margin}px 0px`,
});
function setRefs() {
// console.log("set refs", {
// topPos: options.topPos?.() ?? 0,
// bottomPos: options.bottomPos?.() ?? options.items().length - 1,
// });
const children = [...wrapperEl()?.children ?? []] as Array<HTMLElement>;
setTopEl(children[options.topPos?.() ?? 0]);
setBottomEl(children[options.bottomPos?.() ?? options.items().length - 1]);
@ -263,34 +264,32 @@ function createList<T extends { class?: string }>(options: {
});
},
List(props: { children: (item: T, idx: Accessor<number>) => JSX.Element }) {
function reanchor() {
const wrap = wrapperEl();
console.log("list::reanchor", wrap, anchorRef);
if (!wrap || !anchorRef) return setRefs();
if (shouldAutoscroll) {
console.log("list::autoscroll");
wrap.scrollBy({ top: 999999, behavior: "instant" });
} else {
// FIXME: tons of reflow and jank
console.time("perf::forceReflow");
const currentRect = anchorRef.getBoundingClientRect();
console.timeEnd("perf::forceReflow");
const diff = (currentRect.y - anchorRect.y) + (currentRect.height - anchorRect.height);
wrapperEl()?.scrollBy(0, diff);
}
setRefs();
}
createEffect(on(options.items, () => {
queueMicrotask(() => {
if (!topRef || !bottomRef) {
setRefs();
return;
}
if (shouldAutoscroll) {
console.log("will scroll now!");
wrapperEl()!.scrollBy({ top: 999999, behavior: "instant" });
} else if (wrapperEl()?.contains(topRef)) {
const newOffsetTop = topRef.offsetTop;
const newOffsetHeight = topRef.offsetHeight;
wrapperEl()?.scrollBy(0, (newOffsetTop - topOffset!) - (topHeight! - newOffsetHeight));
} else if (wrapperEl()?.contains(bottomRef)) {
const newOffsetBottom = bottomRef.offsetTop;
const newOffsetHeight = bottomRef.offsetHeight;
console.log({ bottomRef, bottomOffset, newOffsetBottom, bottomHeight, newOffsetHeight, diff: (newOffsetBottom - bottomOffset!) - (bottomHeight! - newOffsetHeight), scrollTop: wrapperEl()?.scrollTop });
wrapperEl()?.scrollBy(0, (newOffsetBottom - bottomOffset!) - (bottomHeight! - newOffsetHeight));
}
setRefs();
});
queueMicrotask(reanchor);
// requestAnimationFrame(reanchor);
}));
createEffect(on(topEl, (topEl) => {
if (!topEl) return;
if (topRef) observer.unobserve(topRef);
topOffset = topEl.offsetTop;
topHeight = topEl.offsetHeight;
topRef = topEl;
observer.observe(topEl);
}));
@ -298,8 +297,6 @@ function createList<T extends { class?: string }>(options: {
createEffect(on(bottomEl, (bottomEl) => {
if (!bottomEl) return;
if (bottomRef) observer.unobserve(bottomRef);
bottomOffset = bottomEl.offsetTop;
bottomHeight = bottomEl.offsetHeight;
bottomRef = bottomEl;
observer.observe(bottomEl);
}));
@ -446,37 +443,45 @@ export function ThreadViewChat(props: VoidProps<{ thread: Thread }>) {
const thread = () => props.thread;
createEffect(on(thread, () => {
console.log("thread updated!");
console.log("thread::update");
tl.setIsAutoscrolling(true);
queueMicrotask(() => {
list.scrollBy(99999);
});
}));
function getItem(item: TimelineItem) {
switch (item.type) {
case "message": {
return (
function Item(props: { item: any }) {
return (
<Switch>
<Match when={props.item.type === "message"}>
<div class="toolbar-wrap">
<div class="toolbar">
<button onClick={() => alert("todo")}>+</button>
<button onClick={() => action({ type: "input.reply", thread: thread(), event: item.event })}>&gt;</button>
<button onClick={(e) => action({ type: "contextmenu.set", menu: { type: "message", x: e.clientX, y: e.clientY, thread: thread(), event: item.event } })}>:</button>
<button onClick={() => action({ type: "input.reply", thread: thread(), event: props.item.event })}>&gt;</button>
<button onClick={(e) => action({ type: "contextmenu.set", menu: { type: "message", x: e.clientX, y: e.clientY, thread: thread(), event: props.item.event } })}>:</button>
</div>
<Message thread={thread()} event={item.event} title={item.separate} />
<Message thread={thread()} event={props.item.event} title={props.item.separate} />
</div>
);
}
case "info": return <ThreadInfo thread={thread()} showHeader={item.header} />;
case "spacer": return <div style={{ "min-height": `${SCROLL_MARGIN}px` }}></div>;
case "spacer-mini": return <div style={{ "min-height": `80px` }}></div>;
case "unread-marker": return <div class="unread">new messages</div>;
}
</Match>
<Match when={props.item.type === "info"}>
<ThreadInfo thread={thread()} showHeader={props.item.header} />
</Match>
<Match when={props.item.type === "spacer"}>
<div style={{ "min-height": `${SCROLL_MARGIN}px` }}></div>
</Match>
<Match when={props.item.type === "spacer-mini"}>
<div style={{ "min-height": `80px` }}></div>
</Match>
<Match when={props.item.type === "unread-marker"}>
<div class="unread">new messages</div>
</Match>
</Switch>
);
}
return (
<div class="thread-view" onKeyDown={binds}>
<list.List>{getItem}</list.List>
<list.List>{(item) => <Item item={item} />}</list.List>
<Input thread={props.thread} />
</div>
);
@ -640,10 +645,6 @@ function ThreadInfo(props: VoidProps<{ thread: Thread, showHeader: boolean }>) {
});
});
createEffect(() => {
console.log({ head: props.showHeader })
});
return (
<>
<div class="spacer"></div>