Remove router, fix media, fix thread
This commit is contained in:
parent
1066f0ccf1
commit
b909401063
15 changed files with 229 additions and 198 deletions
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
33
src/App.tsx
33
src/App.tsx
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
12
src/Auth.tsx
12
src/Auth.tsx
|
@ -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>
|
||||
</>)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
91
src/Main.tsx
91
src/Main.tsx
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,10 +3,6 @@
|
|||
background: var(--background-3);
|
||||
position: relative;
|
||||
|
||||
& > img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& > figcaption {
|
||||
opacity: 0;
|
||||
transition: all .2s;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
11
src/Room.tsx
11
src/Room.tsx
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
109
src/Thread.tsx
109
src/Thread.tsx
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue