Remove router, fix media, fix thread

This commit is contained in:
tezlm 2024-01-17 11:33:21 -08:00
parent 1066f0ccf1
commit b909401063
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
15 changed files with 229 additions and 198 deletions

View file

@ -14,7 +14,6 @@
"@popperjs/core": "^2.11.8",
"@solid-primitives/event-bus": "^1.0.8",
"@solid-primitives/scheduled": "^1.4.1",
"@solidjs/router": "^0.10.2",
"@tanstack/solid-virtual": "^3.0.1",
"fuzzysort": "^2.0.4",
"i18next": "^23.7.9",

View file

@ -20,9 +20,6 @@ dependencies:
'@solid-primitives/scheduled':
specifier: ^1.4.1
version: 1.4.1(solid-js@1.8.7)
'@solidjs/router':
specifier: ^0.10.2
version: 0.10.2(solid-js@1.8.7)
'@tanstack/solid-virtual':
specifier: ^3.0.1
version: 3.0.1(solid-js@1.8.7)
@ -809,14 +806,6 @@ packages:
solid-js: 1.8.7
dev: false
/@solidjs/router@0.10.2(solid-js@1.8.7):
resolution: {integrity: sha512-Yp4G9/+oNRTcTQ2snoxsVzo7HZqSm4L5GdaZDIfg24N7wMoRfanQ8aG5DcwEyA5YqFfOF8mjMW3iOCTaafmqdw==}
peerDependencies:
solid-js: ^1.8.6
dependencies:
solid-js: 1.8.7
dev: false
/@tanstack/solid-virtual@3.0.1(solid-js@1.8.7):
resolution: {integrity: sha512-DxP3GUBEDUNdCH50Q2RgRkaol3bAGpkMcJAdUIPWywEL37TkH/MC748nees0EXRylrC7RMP0zVNN3Z94WFBULA==}
peerDependencies:

View file

@ -147,13 +147,13 @@ h1 {
display: grid;
place-items: center;
animation: backdrop .1s forwards;
// animation: backdrop .1s forwards;
background: #4444;
backdrop-filter: blur(5px);
& > .dialog {
outline: none !important;
animation: dialog 150ms cubic-bezier(.13,.37,.49,1) forwards;
// animation: dialog 150ms cubic-bezier(.13,.37,.49,1) forwards;
}
& > .dialog:not(.raw) {

View file

@ -1,8 +1,7 @@
import { lazy, onCleanup } from "solid-js";
import { Match, Switch, lazy, onCleanup } from "solid-js";
import "./App.scss";
import { Client } from "sdk";
import { Contextualizer } from "./Context";
import { HashRouter, Route, Navigate } from "@solidjs/router";
import { Contextualizer, useGlobals } from "./Context";
const Auth = lazy(() => import("./Auth"));
const Main = lazy(() => import("./Main"));
@ -22,25 +21,29 @@ function Wrapper() {
const isLoggedOut = () => !client || client.state.state === "logout";
return (
<HashRouter root={(props) => <Contextualizer client={client}>{props.children}</Contextualizer>}>
<Route path="/" component={() => <Navigate href={isLoggedOut() ? "/login" : "/home"} />} />
<Route path="/login" component={Auth} />
<Route path="/register" component={Auth} />
<Route path="/home" component={Main} />
<Route path="/inbox" component={Main} />
<Route path="/settings" component={UserSettings} />
<Route path="/rooms/:roomId" component={Main} />
<Route path="/rooms/:roomId/settings" component={RoomSettings} />
<Route path="/*all" component={NotFound} />
</HashRouter>
<Contextualizer client={client}>
<Wrap />
</Contextualizer>
);
}
function Wrap() {
const [globals] = useGlobals();
return (
<Switch>
<Match when={globals.scene.type === "not-found"} children={<NotFound />} />
<Match when={globals.scene.type === "auth"} children={<Auth />} />
<Match when={globals.scene.type === "config-room"} children={<RoomSettings room={(globals.scene as any).room} />} />
<Match when={true} children={<Main />} />
</Switch>
)
}
function NotFound() {
return (
<div class="fatal-error">
<b>oh no</b>
<p>This page doesn't exist! Would you like to return to <a href="/">home?</a></p>
<p>This page doesn't exist! Would you like to return to <a target="_self" href="#">home?</a></p>
</div>
)
}

View file

@ -1,4 +1,3 @@
import { useLocation, A, useNavigate } from "@solidjs/router";
import "./Auth.scss";
import { createForm, required } from "@modular-forms/solid";
import { UserId } from "sdk/dist/src/api";
@ -6,10 +5,9 @@ import { Client, Setup } from "sdk";
import { useGlobals } from "./Context";
export default function Auth() {
const location = useLocation();
const navigate = useNavigate();
const [globals] = useGlobals();
if (globals.client && globals.client.state.state !== "logout") navigate("/home");
const location = { pathname: "/login" };
// const [globals] = useGlobals();
// if (globals.client && globals.client.state.state !== "logout") navigate("/home");
return (
<>
<div class="auth">
@ -65,7 +63,7 @@ function Login() {
<button type="submit" disabled={form.submitting}>{form.submitting ? "logging in..." : "login"}</button>
</Form>
<div style="flex:1"></div>
<p> or <A target="_self" href="/register">register</A></p>
<p> or <a target="_self" href="/register">register</a></p>
</>)
}
@ -74,6 +72,6 @@ function Register() {
<h1>register</h1>
<p>todo</p>
<div style="flex:1"></div>
<p> or <A target="_self" href="/login">login</A></p>
<p> or <a target="_self" href="/login">login</a></p>
</>)
}

View file

@ -4,7 +4,6 @@ import { Client, ClientState, Event, Room, Thread } from "sdk";
import { EventId, RoomId, UserId } from "sdk/dist/src/api";
import { Store, createStore } from "solid-js/store";
import { createEmitter } from "@solid-primitives/event-bus";
import { useNavigate } from "@solidjs/router";
import { FileI } from "./Media";
interface Settings {
@ -43,9 +42,14 @@ interface Globals {
// mutation" than other settings, and may change constantly
type Scene =
{ type: "auth", sub: "register" | "login" } |
{ type: "main", room?: Room, thread?: Thread, isThreadFullscreen: boolean } |
{ type: "config-room", room: Room, sub: "general" | "permissions" | "members" } |
{ type: "config-user", sub: "account" | "appearance" }
{ type: "loading-room", roomId: RoomId } |
{ type: "room", room: Room } |
{ type: "thread", thread: Thread } |
{ type: "inbox", space?: Thread } |
{ type: "home" } |
{ type: "config-room", room: Room, sub: "general" | "permissions" | "security" | "integrations" | "members" | "rooms" } |
{ type: "config-user", sub: "account" | "appearance" } |
{ type: "not-found" }
type Modal =
{ type: "user", user: UserId } |
@ -61,6 +65,7 @@ type Action =
{ type: "dialog.open", dialog: Dialog } |
{ type: "dialog.close" } |
{ type: "input.reply", thread: Thread, event: Event | null } |
{ type: "scene.set", scene: Scene } |
{ type: "contextmenu.set", menu: ContextMenu | null }
const GlobalsContext = createContext<[Store<Globals>, (change: Action) => void]>();
@ -74,8 +79,7 @@ type Events = {
export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
const emitter = createEmitter<ExtractEvents<Events>>();
const scene: Scene = props.client ? { type: "main", isThreadFullscreen: false } : { type: "auth", sub: "login" };
const scene: Scene = props.client ? { type: "home" } : { type: "auth", sub: "login" };
const [globals, update] = createStore({
settings: {
@ -110,7 +114,6 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
}
});
const navigate = useNavigate();
function redux(action: Action) {
console.log("dispatch action", action);
switch (action.type) {
@ -120,7 +123,7 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
case "login":
update("client", action.client);
localStorage.setItem("auth", JSON.stringify(action.client.config));
navigate("/home");
redux({ type: "scene.set", scene: { type: "home" }});
Object.assign(globalThis, { client: action.client });
globals.client.on("state", handleState);
globals.client.start();
@ -144,8 +147,15 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
}
break;
}
case "scene.set": {
update("scene", action.scene);
const hash = "#" + sceneToHash(action.scene);
if (hash !== location.hash && hash !== "#/todo") location.hash = hash;
break;
}
case "contextmenu.set": {
update("contextMenu", action.menu);
break;
}
}
}
@ -155,16 +165,58 @@ export function Contextualizer(props: ParentProps<{ client: Client | null }>) {
if (state.state === "logout") {
localStorage.removeItem("auth");
globals.client.off("state", handleState);
navigate("/login");
redux({ type: "scene.set", scene: { type: "auth", sub: "login" }});
}
}
function sceneToHash(s: Scene) {
switch (s.type) {
case "auth": return `/${s.sub}`
case "loading-room": return `/rooms/${s.roomId}`;
case "room": return `/rooms/${s.room.id}`;
case "thread": return `/todo`;
case "inbox": return `/inbox`;
case "home": return `/home`;
case "config-room": return `/todo`;
case "config-user": return `/todo`;
case "not-found": return location.hash.slice(1);
}
}
function handleHash() {
console.log("hash change", location.hash, location.hash.split("/"));
if (!globals.client) {
update("scene", { type: "auth", sub: "login" });
return;
}
switch (location.hash.split("/")[1]) {
case undefined:
case "home": return update("scene", { type: "home" });
case "inbox": return update("scene", { type: "inbox" });
case "rooms": {
const roomId = location.hash.split("/")[2];
const room = globals.client.rooms.get(roomId);
if (room) {
return update("scene", { type: "room", room });
} else {
return update("scene", { type: "loading-room", roomId });
}
}
default: return update("scene", { type: "not-found" });
}
}
globals.client?.on("state", handleState);
globals.client?.start();
Object.assign(globalThis, { client: globals.client });
Object.assign(globalThis, { client: globals.client, redux });
window.addEventListener("hashchange", handleHash);
handleHash();
onCleanup(() => {
globals.client?.off("state", handleState);
globals.client?.stop();
window.removeEventListener("hashchange", handleHash);
});
return (

View file

@ -6,42 +6,32 @@ import { Portal } from "solid-js/web";
import { Dialog, Dropdown, Text, Time, Tooltip } from "./Atoms";
import { useGlobals } from "./Context";
import { ThreadView } from "./Thread";
import { useParams, A, useNavigate, Navigate, useLocation } from "@solidjs/router";
import * as menu from "./Menu";
import { useFloating } from "solid-floating-ui";
import { ClientRectObject, ReferenceElement, autoPlacement, autoUpdate, shift } from "@floating-ui/dom";
import { ROOM_TYPE_DEFAULT, ROOM_TYPE_SPACE } from "./consts";
import { File, FileI } from "./Media";
type View =
{ type: "loading" } |
{ type: "room", room: Room } |
{ type: "home" } |
{ type: "inbox" };
export default function App(): JSX.Element {
const params = useParams();
const [globals, change] = useGlobals();
if (!globals.client || globals.client.state.state === "logout") {
return <Navigate href="/login"></Navigate>;
}
const [globals, action] = useGlobals();
// if (!globals.client || globals.client.state.state === "logout") {
// return <Navigate href="/login"></Navigate>;
// }
const [rooms, setRooms] = createSignal(globals.client.lists.get("rooms") ?? { count: 0, rooms: [] as Array<Room> });
const [view, setView] = createSignal<View>({ type: "loading" });
const loc = useLocation();
createEffect(() => {
if (loc.pathname === "/home") {
setView({ type: "home" });
} else if (loc.pathname === "/inbox") {
setView({ type: "inbox" });
} else if (globals.client.rooms.has(params.roomId)) {
setView({
type: "room",
room: globals.client.rooms.get(params.roomId)!,
})
}
});
// const loc = useLocation();
// createEffect(() => {
// if (loc.pathname === "/home") {
// setView({ type: "home" });
// } else if (loc.pathname === "/inbox") {
// setView({ type: "inbox" });
// } else if (globals.client.rooms.has(params.roomId)) {
// setView({
// type: "room",
// room: globals.client.rooms.get(params.roomId)!,
// })
// }
// });
if (!globals.client.lists.has("rooms")) {
globals.client.lists.subscribe("rooms", {
@ -68,14 +58,19 @@ export default function App(): JSX.Element {
if (name === "rooms") setRooms({ ...list });
// if (name === "requests") console.log("update requests", list);
const newRoom = globals.client.rooms.get(params.roomId);
if ((view() as any)?.room !== newRoom) {
setView({
if (globals.scene.type === "loading-room") {
const newRoom = globals.client.rooms.get(globals.scene.roomId);
if (newRoom) {
action({
type: "scene.set",
scene: {
type: "room",
room: newRoom!,
room: newRoom,
}
});
}
}
}
function buildTree(rooms: Array<Room>): Map<RoomId | null, Array<Room>> {
const seen = new Set(rooms);
@ -104,12 +99,12 @@ export default function App(): JSX.Element {
const willHandleRoomContextMenu = (room: Room) => (e: MouseEvent) => {
e.preventDefault();
change({ type: "contextmenu.set", menu: { type: "room", room, x: e.clientX, y: e.clientY } });
action({ type: "contextmenu.set", menu: { type: "room", room, x: e.clientX, y: e.clientY } });
}
// TODO: put on main div
function hideContextMenu() {
change({ type: "contextmenu.set", menu: null });
action({ type: "contextmenu.set", menu: null });
}
window.addEventListener("mousedown", hideContextMenu);
@ -144,12 +139,11 @@ export default function App(): JSX.Element {
<header id="header">
header: settings, pinned, search, info, help
</header>
<main id="main" class={view().type === "home" ? "home" : "room"}>
<main id="main" class={globals.scene.type === "home" ? "home" : "room"}>
<Switch>
<Match when={view().type === "loading"}>please wait...</Match>
<Match when={view().type === "home"}><Home client={globals.client} /></Match>
<Match when={view().type === "inbox"}><Inbox /></Match>
<Match when={view().type === "room"}><RoomView room={(view() as any).room} /></Match>
<Match when={globals.scene.type === "home"}><Home client={globals.client} /></Match>
<Match when={globals.scene.type === "inbox"}><Inbox /></Match>
<Match when={globals.scene.type === "room"}><RoomView room={(globals.scene as any).room} /></Match>
</Switch>
</main>
<nav id="nav-spaces" aria-label="Space List">
@ -169,15 +163,15 @@ export default function App(): JSX.Element {
<nav id="nav-rooms" aria-label="Room List">
<ul>
<li>
<A target="_self" href="/home">home</A>
<a target="_self" href="#/home">home</a>
</li>
<li>
<A target="_self" href="/inbox">inbox</A>
<a target="_self" href="#/inbox">inbox</a>
</li>
<For each={tree().get(space()?.id ?? null)}>
{room => room.getState("m.room.create")!.content.type !== ROOM_TYPE_SPACE && (
<li oncontextmenu={willHandleRoomContextMenu(room)}>
<A target="_self" href={`/rooms/${room.id}`}>{room.getState("m.room.name")?.content.name || "unnamed"}</A>
<a target="_self" href={`#/rooms/${room.id}`}>{room.getState("m.room.name")?.content.name || "unnamed"}</a>
</li>
)}
</For>
@ -194,8 +188,8 @@ export default function App(): JSX.Element {
</Match>
<Match when={!globals.sidebar && false}>
<div style="padding: 4px">
<h1>{(view() as any).room?.getState("m.room.name")?.content.name || "no name"}</h1>
<p>{(view() as any).room?.getState("m.room.topic")?.content.topic || "no topic"}</p>
<h1>{(globals.scene as any).room?.getState("m.room.name")?.content.name || "no name"}</h1>
<p>{(globals.scene as any).room?.getState("m.room.topic")?.content.topic || "no topic"}</p>
<hr />
<p>some useful info/stats</p>
<hr />
@ -206,7 +200,7 @@ export default function App(): JSX.Element {
</div>
</Show>
<footer id="status">
<button onClick={() => change({ type: "logout" })}>logout</button>
<button onClick={() => action({ type: "logout" })}>logout</button>
</footer>
<Portal>
<For each={globals.dialogs}>
@ -231,11 +225,14 @@ export default function App(): JSX.Element {
}
function MediaDialog(props: { file: FileI }) {
const [globals, _actions] = useGlobals();
const httpUrl = () => props.file.url.replace(/^mxc:\/\//, `${globals.client.config.baseUrl}/_matrix/media/v3/download/`);
return (
<Dialog raw>
<div style="max-height: 80%; max-width: 80%">
<File file={props.file} />
</div>
<figure class="image">
<img src={httpUrl()} style="max-height: 80vh; max-width: 80vw" />
<figcaption>{props.file.info?.name}</figcaption>
</figure>
</Dialog>
)
}

View file

@ -3,10 +3,6 @@
background: var(--background-3);
position: relative;
& > img {
height: 100%;
}
& > figcaption {
opacity: 0;
transition: all .2s;

View file

@ -9,7 +9,7 @@ export function File(props: VoidProps<{
bounding?: { height?: number, width?: number },
onClick?: (e: MouseEvent) => void,
}>) {
const [globals, action] = useGlobals();
const [globals, _action] = useGlobals();
const major = () => props.file.info?.mimetype?.split("/")[0] || "application";
const httpUrl = () => props.file.url.replace(/^mxc:\/\//, `${globals.client.config.baseUrl}/_matrix/media/v3/download/`);
return (
@ -21,7 +21,7 @@ export function File(props: VoidProps<{
<Audio file={props.file} src={httpUrl()} />
</Match>
<Match when={major() === "video"}>
<Video file={props.file} src={httpUrl()} />
<Video file={props.file} src={httpUrl()} bounding={props.bounding} />
</Match>
<Match when={major() === "text" || props.file.info?.mimetype === "application/json"}>
<div>
@ -189,8 +189,8 @@ function formatTime(time: number): string {
}
function getDimensions(info: { width?: number, height?: number }, bounding?: { width?: number, height?: number }) {
let height = Math.max(info.height || bounding?.height || 300, 100);
let width = Math.max(info.width || bounding?.width || 300, 100);
let height = Math.max(info.height || bounding?.height || 300, 10);
let width = Math.max(info.width || bounding?.width || 300, 10);
if (bounding?.height) {
const newHeight = Math.min(height, bounding.height);
@ -199,7 +199,7 @@ function getDimensions(info: { width?: number, height?: number }, bounding?: { w
}
if (bounding?.width) {
const newWidth = Math.min(width, 300);
const newWidth = Math.min(width, bounding.width);
height *= newWidth / width;
width = newWidth;
}

View file

@ -86,8 +86,9 @@ export function SpaceMenu(props: VoidProps<{ room: Room }>) {
// the context menu for rooms
export function RoomMenu(props: VoidProps<{ room: Room }>) {
const [_globals, action] = useGlobals();
const copyId = () => navigator.clipboard.writeText(props.room.id);
const edit = () => location.hash = `#/rooms/${props.room.id}/settings`;
const edit = () => action({ type: "scene.set", scene: { type: "config-room", room: props.room, sub: "general" }});
const leave = () => {
if (confirm("really leave?")) props.room.leave();
};

View file

@ -82,14 +82,14 @@ export function TextBlock(props: VoidProps<{ text: any, formatting?: boolean, fa
}
const text = props.text.find((i: any) => !i.type || i.type === "text/plain")?.body;
if (text) return escape(text);
if (text) return `<p>${escape(text)}</p>`;
return "";
}
// for portability, it might be better to add a custom renderer instead of using html
return (
<Show when={hasBody()} fallback={props.fallback}>
<div innerHTML={getBody()}></div>
<div style="display: contents" innerHTML={getBody()}></div>
</Show>
);
}

View file

@ -5,7 +5,6 @@ import { Dropdown, Text, Time } from "./Atoms";
import { ThreadTimeline } from "sdk/dist/src/timeline";
import { shouldSplit } from "./util";
import { Message, TextBlock } from "./Message";
import { A, useNavigate } from "@solidjs/router";
import { SetStoreFunction, createStore } from "solid-js/store";
import "./Room.scss";
import { EVENT_TYPE_THREAD_CHAT } from "./consts";
@ -71,6 +70,7 @@ function RoomActions(props: VoidProps<{ room: Room }>) {
}
function RoomHeader(props: VoidProps<{ room: Room }>) {
const [_globals, action] = useGlobals();
const name = () => props.room.getState("m.room.name")?.content.name || "unnamed room";
const topic = () => props.room.getState("m.room.topic")?.content.topic || "";
@ -79,14 +79,17 @@ function RoomHeader(props: VoidProps<{ room: Room }>) {
if (uid) props.room.client.net.roomInvite(props.room.id, uid);
}
const navigate = useNavigate();
function openSettings() {
action({ type: "scene.set", scene: { type: "config-room", room: props.room, sub: "general" } });
}
return (
<div class="timeline-create">
<h1><Text name={name()}>room.welcome</Text></h1>
<p><Text topic={topic()}>room.topic</Text></p>
<p style="color:var(--foreground-2)">Debug: room_id=<code style="user-select:all">{props.room.id}</code></p>
<div class="actions">
<button onClick={() => navigate(`/rooms/${props.room.id}/settings`)}>edit room</button>
<button onClick={openSettings}>edit room</button>
<button onClick={invite}>invite people</button>
</div>
</div>
@ -346,7 +349,7 @@ export function Inbox() {
<TextBlock text={thread.baseEvent.content.title} formatting={false} fallback={<Text>thread.unnamed</Text>} />
</td>
<td>
<A href={`/rooms/${thread.room.id}`} onClick={e => e.stopPropagation()} target="_self">{thread.baseEvent.room.getState("m.room.name")?.content.name || thread.baseEvent.room.id}</A>
<a href={`#/rooms/${thread.room.id}`} onClick={e => e.stopPropagation()} target="_self">{thread.baseEvent.room.getState("m.room.name")?.content.name || thread.baseEvent.room.id}</a>
</td>
</tr>}
</For>

View file

@ -1,6 +1,5 @@
import { For, Match, Switch, VoidProps, createEffect, createSignal, lazy, onCleanup } from "solid-js";
import { Dropdown } from "./Atoms";
import { A, useParams } from "@solidjs/router";
import "./RoomSettings.scss";
import { createForm } from "@modular-forms/solid";
import { useGlobals } from "./Context";
@ -9,64 +8,51 @@ import { ROOM_TYPE_SPACE } from "./consts";
import { createStore, unwrap } from "solid-js/store";
const Editor = lazy(() => import("./Editor"));
export default function RoomSettings() {
const tabs = ["general", "permissions", "security", "integrations", "members", "rooms"];
const [selected, setSelected] = createSignal("general");
const params = useParams();
const [globals] = useGlobals();
const [room, setRoom] = createSignal(globals.client.rooms.get(params.roomId));
const resetRoom = () => {
setRoom(globals.client.rooms.get(params.roomId));
};
globals.client.on("roomInit", resetRoom);
onCleanup(() => globals.client.off("roomInit", resetRoom));
export default function RoomSettings(props: { room: Room }) {
const tabs = ["general", "permissions", "security", "integrations", "members", ...props.room.getState("m.room.create")?.content.type === ROOM_TYPE_SPACE ? ["rooms"] : []];
const [globals, action] = useGlobals();
if (globals.scene.type !== "config-room") throw new Error("unreachable");
return (
<div class="room-settings">
<nav>
<ul>
<li><A target="_self" href={`/rooms/${params.roomId}`}>back</A></li>
{tabs.map(i => <li onClick={() => setSelected(i)}>{i}</li>)}
<li><a target="_self" href={`#/rooms/${props.room.id}`} onClick={() => action({ type: "scene.set", scene: { type: "room", room: props.room }})}>back</a></li>
{tabs.map(i => <li onClick={() => action({ type: "scene.set", scene: { type: "config-room", room: props.room, sub: i as any }})}>{i}</li>)}
</ul>
</nav>
<main>
<Switch>
<Match when={selected() === "general" && room()}>
<General room={room()!} />
<Match when={globals.scene.sub === "general"}>
<General room={props.room} />
</Match>
<Match when={selected() === "permissions"}>
<Match when={globals.scene.sub === "permissions"}>
<Permissions />
</Match>
<Match when={selected() === "security" && room()}>
<Security room={room()!} />
<Match when={globals.scene.sub === "security"}>
<Security room={props.room} />
</Match>
<Match when={selected() === "integrations"}>
<Match when={globals.scene.sub === "integrations"}>
<h1>integrations</h1>
<h2>bridges</h2>
<h2>bots</h2>
</Match>
<Match when={selected() === "members" && room()}>
<Members room={room()!} />
<Match when={globals.scene.sub === "members"}>
<Members room={props.room} />
</Match>
<Match when={selected() === "rooms" && room()?.getState("m.room.create")?.content.type === ROOM_TYPE_SPACE}>
<Match when={globals.scene.sub === "rooms"}>
<h1>room list</h1>
<button onClick={() => {
const id = window.prompt("room id");
if (!id) return;
room()?.sendState("m.space.child", id, { via: ["localhost"] });
props.room.sendState("m.space.child", id, { via: ["localhost"] });
}}>add room</button>
<ul>
<For each={room()!.getAllState("m.space.child")}>
<For each={props.room.getAllState("m.space.child")}>
{(ev) => <li>{ev.stateKey}: {JSON.stringify(ev.content)}</li>}
</For>
</ul>
</Match>
<Match when={selected() === "rooms" && room()?.getState("m.room.create")?.content.type !== ROOM_TYPE_SPACE}>
<h1>room list</h1>
<p>this only should exist in spaces</p>
</Match>
</Switch>
</main>
</div>

View file

@ -75,6 +75,11 @@
& > .header {
display: contents;
& > .spacer {
min-height: 100px;
flex: 1;
}
& > header {
display: contents;
@ -93,6 +98,9 @@
transition: all .2s;
flex: 1;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.stuck {

View file

@ -13,7 +13,6 @@ import "./Thread.scss";
import { createProgress, delay, shouldSplit } from "./util";
import { createStore, reconcile } from "solid-js/store";
import { classList } from "solid-js/web";
import { action } from "@solidjs/router";
import { useFloating } from "solid-floating-ui";
const Editor = lazy(() => import("./Editor"));
@ -49,15 +48,15 @@ function createTimeline(timelineSet: Accessor<ThreadTimelineSet>) {
function updateItems() {
const { start, end } = info()!;
const items: Array<TimelineItem> = [];
if (!isAtBeginning()) {
items.push({ type: "spacer", key: "space-begin" });
}
items.push({
type: "info",
key: "info",
key: "info" + isAtBeginning(),
header: isAtBeginning(),
class: "header",
});
if (!isAtBeginning()) {
items.push({ type: "spacer", key: "space-begin" });
}
const events = timeline().getEvents();
const lastAck = timelineSet()?.thread.unreads?.last_ack;
for (let i = start; i < end; i++) {
@ -143,7 +142,7 @@ function createTimeline(timelineSet: Accessor<ThreadTimelineSet>) {
// async function append(_event: Event) {
async function append() {
console.log("append", { status: status(), auto: isAutoscrolling() });
console.log("append", { status: status(), auto: isAutoscrolling(), timeline: timeline() });
if (status() !== "ready") return;
if (!isAutoscrolling()) return;
@ -237,52 +236,15 @@ function createList<T extends { class?: string }>(options: {
});
function setRefs() {
// console.log("set refs", {
// topPos: options.topPos?.() ?? 0,
// bottomPos: options.bottomPos?.() ?? options.items().length - 1,
// });
const children = [...wrapperEl()?.children ?? []] as Array<HTMLElement>;
console.log(wrapperEl(), children);
setTopEl(children[options.topPos?.() ?? 0]);
setBottomEl(children[options.bottomPos?.() ?? options.items().length - 1]);
}
createEffect(on(options.items, () => {
console.log("updating!", { topRef, bottomRef });
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();
}));
createEffect(on(topEl, (topEl) => {
if (!topEl) return;
if (topRef) observer.unobserve(topRef);
topOffset = topEl.offsetTop;
topHeight = topEl.offsetHeight;
topRef = topEl;
observer.observe(topEl);
}));
createEffect(on(bottomEl, (bottomEl) => {
if (!bottomEl) return;
if (bottomRef) observer.unobserve(bottomRef);
bottomOffset = bottomEl.offsetTop;
bottomHeight = bottomEl.offsetHeight;
bottomRef = bottomEl;
observer.observe(bottomEl);
}));
onCleanup(() => {
observer.disconnect();
});
@ -301,12 +263,46 @@ function createList<T extends { class?: string }>(options: {
});
},
List(props: { children: (item: T, idx: Accessor<number>) => JSX.Element }) {
onMount(() => {
console.log("mounted");
console.log(wrapperEl());
console.log(wrapperEl()?.children);
console.log(options.items());
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();
});
}));
createEffect(on(topEl, (topEl) => {
if (!topEl) return;
if (topRef) observer.unobserve(topRef);
topOffset = topEl.offsetTop;
topHeight = topEl.offsetHeight;
topRef = topEl;
observer.observe(topEl);
}));
createEffect(on(bottomEl, (bottomEl) => {
if (!bottomEl) return;
if (bottomRef) observer.unobserve(bottomRef);
bottomOffset = bottomEl.offsetTop;
bottomHeight = bottomEl.offsetHeight;
bottomRef = bottomEl;
observer.observe(bottomEl);
}));
return (
<ul class="scroll" ref={setWrapperEl}>
@ -460,7 +456,6 @@ export function ThreadViewChat(props: VoidProps<{ thread: Thread }>) {
function getItem(item: TimelineItem) {
switch (item.type) {
case "message": {
// return <Message thread={thread()} event={item.event} title={item.separate} />
return (
<div class="toolbar-wrap">
<div class="toolbar">
@ -645,11 +640,15 @@ function ThreadInfo(props: VoidProps<{ thread: Thread, showHeader: boolean }>) {
});
});
createEffect(() => {
console.log({ head: props.showHeader })
});
return (
<>
<div style="min-height: 100px"></div>
<div class="spacer"></div>
<header>
<div ref={stickyEl!} class="sticky" classList={{ stuck: stuck() }}>
<div ref={stickyEl!} class="sticky" classList={{ stuck: stuck() || !props.showHeader }}>
<h1>
<TextBlock text={event().content.title} fallback={<Text>thread.untitled</Text>} />
</h1>