fix scrolling
This commit is contained in:
parent
b909401063
commit
13f1a4cdec
3 changed files with 92 additions and 62 deletions
|
@ -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" });
|
||||
|
|
|
@ -60,6 +60,35 @@ function sanitizeInline(html: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
function escape(str: string) {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
119
src/Thread.tsx
119
src/Thread.tsx
|
@ -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 })}>></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 })}>></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>
|
||||
|
|
Loading…
Reference in a new issue