Hook up the ui to sdk

This commit is contained in:
tezlm 2023-12-10 04:41:54 -08:00
parent d2bee0b6b4
commit 7b334b3bc6
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
12 changed files with 375 additions and 560 deletions

View file

@ -9,7 +9,8 @@
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"sdk": "git+https://git.celery.eu.org/jackwagon/sdk-ts",
"i18next": "^23.7.8",
"sdk": "link:../sdk-ts",
"solid-js": "^1.8.7"
},
"devDependencies": {

View file

@ -8,9 +8,12 @@ dependencies:
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
i18next:
specifier: ^23.7.8
version: 23.7.8
sdk:
specifier: git+https://git.celery.eu.org/jackwagon/sdk-ts
version: git.celery.eu.org/jackwagon/sdk-ts/d300dda34ee37deb2e5ec91fa0d831ace9e92546
specifier: link:../sdk-ts
version: link:../sdk-ts
solid-js:
specifier: ^1.8.7
version: 1.8.7
@ -323,6 +326,13 @@ packages:
'@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.23.5)
dev: true
/@babel/runtime@7.23.5:
resolution: {integrity: sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: false
/@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
@ -779,7 +789,7 @@ packages:
hasBin: true
dependencies:
caniuse-lite: 1.0.30001566
electron-to-chromium: 1.4.608
electron-to-chromium: 1.4.609
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: true
@ -841,8 +851,8 @@ packages:
ms: 2.1.2
dev: true
/electron-to-chromium@1.4.608:
resolution: {integrity: sha512-J2f/3iIIm3Mo0npneITZ2UPe4B1bg8fTNrFjD8715F/k1BvbviRuqYGkET1PgprrczXYTHFvotbBOmUp6KE0uA==}
/electron-to-chromium@1.4.609:
resolution: {integrity: sha512-ihiCP7PJmjoGNuLpl7TjNA8pCQWu09vGyjlPYw1Rqww4gvNuCcmvl+44G+2QyJ6S2K4o+wbTS++Xz0YN8Q9ERw==}
dev: true
/esbuild@0.19.8:
@ -885,11 +895,6 @@ packages:
engines: {node: '>=0.8.0'}
dev: true
/events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
dev: false
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
@ -931,6 +936,12 @@ packages:
resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
dev: true
/i18next@23.7.8:
resolution: {integrity: sha512-yCe9964O+1abdIG01AOzk6P9mQi0HVJV1B57whYJQu6TjmrB9JHHDYonDI8amGt6M6b9bP3x3R0Zh7ROmvX7JQ==}
dependencies:
'@babel/runtime': 7.23.5
dev: false
/immutable@4.3.4:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
dev: true
@ -1003,12 +1014,6 @@ packages:
hasBin: true
dev: true
/nanoid@5.0.4:
resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: true
@ -1043,6 +1048,10 @@ packages:
picomatch: 2.3.1
dev: true
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false
/rollup@4.7.0:
resolution: {integrity: sha512-7Kw0dUP4BWH78zaZCqF1rPyQ8D5DSU6URG45v1dqS/faNsx9WXyess00uTOZxKr7oR/4TOjO1CPudT8L1UsEgw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -1214,12 +1223,3 @@ packages:
/yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true
git.celery.eu.org/jackwagon/sdk-ts/d300dda34ee37deb2e5ec91fa0d831ace9e92546:
resolution: {commit: d300dda34ee37deb2e5ec91fa0d831ace9e92546, repo: https://git.celery.eu.org/jackwagon/sdk-ts, type: git}
name: sdk-ts
version: 0.1.0
dependencies:
events: 3.3.0
nanoid: 5.0.4
dev: false

View file

@ -131,6 +131,8 @@
// }
& > header {
display: flex;
flex-direction: column;
padding: 4px 8px;
cursor: pointer;
background: var(--background-3);
@ -161,21 +163,20 @@
}
& > .bottom {
display: flex;
align-self: start;
margin-top: 8px;
color: var(--foreground-2);
cursor: pointer;
& > .info {
flex: 1;
}
& > .date {
color: var(--foreground-2);
&:hover {
color: var(--foreground-1);
text-decoration: underline;
}
}
}
& > .preview{
padding-bottom: 1rem;
& > .preview {
padding-bottom: 2rem;
}
& > footer {
@ -197,6 +198,11 @@
.timeline-create {
margin: 32px 32px 8px;
& > h1 {
line-height: 1.1;
margin-bottom: 4px;
}
& > .actions {
display: flex;
gap: 4px;

View file

@ -1,53 +1,73 @@
import { VoidProps, createSignal, onCleanup } from "solid-js";
import { Show, VoidProps, createEffect, createSignal, onCleanup } from "solid-js";
import "./App.scss";
import { Client, Room, Timeline } from "sdk";
import { Client, Room } from "sdk";
import { EventTimeline, RoomView } from "./Main";
import { Contextualizer, useGlobals } from "./Context";
function App() {
function Wrapper() {
console.clear();
const client = new Client({
baseUrl: "http://localhost:6167",
deviceId: "PFXhfDYmCc",
token: "Ebh15YkyCBJSFjP11oimVEtAdqL9ZXcl",
userId: "@user:localhost",
token: "9SWeOqc5g42O0fS3JesE43s1JujCDD8S",
userId: "@asdf:localhost",
});
client.start();
onCleanup(() => client.stop());
return (
<Contextualizer>
<App client={client} />
</Contextualizer>
);
}
function App(props: VoidProps<{ client: Client }>) {
const [globals, change] = useGlobals();
const [rooms, setRooms] = createSignal({ count: 0, rooms: [] as Array<Room> });
const [room, setRoom] = createSignal(null as null | Room);
Object.assign(globalThis, { client });
setTimeout(() => {
change({
type: "focusRoom",
room: props.client.rooms.get("!T7ihGmUDiIW0AckfcL:localhost")!,
});
}, 1000)
client.lists.subscribe("rooms", {
Object.assign(globalThis, { client: props.client });
props.client.lists.subscribe("rooms", {
ranges: [[0, 120]],
required_state: [["m.room.name", ""], ["m.room.topic", ""]],
// timeline_limit: 3,
} as any);
client.on("list", (name, list) => {
props.client.on("list", (name, list) => {
if (name === "rooms") setRooms({ ...list });
});
client.start();
onCleanup(() => client.stop());
const [offset, setOffset] = createSignal(0);
const interval = setInterval(() => setOffset(offset() + 1), 100);
onCleanup(() => clearInterval(interval));
createEffect(() => {
console.log(globals.roomState.focusedThread());
})
return (
<>
<header id="header">
</header>
<main id="main">
{room() && <RoomView room={room()!} />}
<Show when={globals.globalState.focusedRoom()}>
<RoomView room={globals.globalState.focusedRoom()!} />
</Show>
</main>
<nav id="nav-rooms">
<ul>
<li onClick={() => setRoom(null)}>home</li>
{rooms().rooms.map(i => <li onClick={() => setRoom(i)}>{i.getState("m.room.name")?.content.name}</li>)}
<li onClick={() => change({ type: "focusRoom", room: null })}>home</li>
{rooms().rooms.map(i => <li onClick={() => change({ type: "focusRoom", room: i })}>{i.getState("m.room.name")?.content.name || "unnamed"}</li>)}
</ul>
</nav>
<nav id="nav-spaces"></nav>
<div id="sidebar">
<EventTimeline timeline={room()?.timelines.live} />
<EventTimeline timeline={globals.roomState.focusedThread()?.timeline} />
<div class="input">
<textarea placeholder="input text here"></textarea>
</div>
@ -57,119 +77,4 @@ function App() {
)
}
function RoomView(props: VoidProps<{ room: Room }>) {
return (
<>
<Threads room={props.room} />
<div class="actions">
<TimelineActions />
</div>
</>
)
}
function Threads(props: VoidProps<{ room: Room }>) {
return (
<div class="timeline threads">
<div class="items">
<ThreadsHeader room={props.room} />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
</div>
</div>
);
}
function EventTimeline(props: VoidProps<{ timeline: Timeline | undefined }>) {
return (
<div class="timeline">
<div class="items">
{props.timeline?.events.map(i => <Message event={i} />)}
</div>
</div>
);
}
function Message(props: VoidProps<{ event: any }>) {
const title = Math.random() > .5;
const compact = false;
return <div class="message" classList={{ title, [compact ? "compact" : "cozy"]: true }}>
{title && !compact && <div class="avatar"></div>}
{title && compact && <div class="name">{props.event.sender}</div>}
<div class="content">
{title && !compact && <div class="name">{props.event.sender}</div>}
<div class="body">{props.event.content.body || `event: (${props.event.type})`}</div>
</div>
</div>;
}
function ThreadsItem() {
return <article class="timeline-thread">
<header>
<div class="top">
<div class="icon"></div>
<div class="title">Thread title</div>
<div class="date">Feb 12</div>
</div>
<div class="bottom">
<div class="info">39374 messages &bull; 2 minutes ago</div>
</div>
</header>
<div class="preview">
<Message event={{ type: "m.message", content: { body: "hello world" }, sender: "username" }} />
<Message event={{ type: "m.message", content: { body: "hello world" }, sender: "username" }} />
<Message event={{ type: "m.message", content: { body: "hello world" }, sender: "username" }} />
<Message event={{ type: "m.message", content: { body: "hello world" }, sender: "username" }} />
<Message event={{ type: "m.message", content: { body: "hello world" }, sender: "username" }} />
</div>
<footer>39374 more messages...</footer>
</article>;
}
function ThreadsHeader(props: VoidProps<{ room: Room }>) {
const name = () => props.room.getState("m.room.name")?.content.name || "unnamed room";
return (
<div class="timeline-create">
<h1>Welcome to {name()}!</h1>
<p>This is the beginning of this room. Insert room topic here.</p>
<p style="color:var(--foreground-2)">Debug: room_id=<code style="user-select:all">{props.room.id}</code></p>
<div class="actions">
<button>edit room</button>
<button>invite people</button>
</div>
</div>
);
}
function TimelineActions() {
return <>
<select>
<option>default</option>
<option>include ignoring</option>
<option>only watching</option>
</select>
<Toggle enabled="[x] Unread" disabled="[ ] Unread" initial={false} />
<Toggle enabled="[x] Watching" disabled="[ ] Watching" initial={false} />
<div class="create-thread">
<div>New thread</div>
<div>+</div>
</div>
</>
}
function Toggle(props: { enabled: string, disabled: string, initial: boolean }) {
const [disabled, setDisabled] = createSignal(props.initial);
return (
<div
class="toggle"
classList={{ disabled: disabled() }}
onClick={() => setDisabled(!disabled())}
>{disabled() ? props.enabled : props.disabled}</div>
)
}
export default App
export default Wrapper;

View file

@ -1,222 +0,0 @@
#root {
display: grid;
height: 100vh;
width: 100vw;
grid-template-areas: "nav-spaces header header header"
"nav-spaces nav-rooms main sidebar"
"status status main sidebar";
grid-template-columns: 64px 256px 1fr 256px;
grid-template-rows: 64px 1fr 72px;
}
#header {
background: var(--background-3);
grid-area: header;
}
#main {
background: var(--background-1);
grid-area: main;
display: flex;
flex-direction: column;
& > .timeline {
flex: 1;
overflow-y: auto;
}
& > .actions {
height: 72px;
background: var(--background-2);
display: flex;
align-items: center;
padding: 0 8px;
gap: 8px;
& > .create-thread {
display: inline-flex;
gap: 2px;
& > div {
padding: 4px;
background: #46c;
&:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
}
}
#nav-rooms {
background: var(--background-2);
grid-area: nav-rooms;
& > ul {
list-style: none;
& > li {
cursor: pointer;
padding: 4px 8px;
&:hover {
background: #ffffff33;
}
}
}
}
#nav-spaces {
background: var(--background-4);
grid-area: nav-spaces;
}
#sidebar {
background: var(--background-2);
grid-area: sidebar;
}
#status {
background: var(--background-3);
grid-area: status;
}
.timeline-thread {
--title-font-size: calc(1rem * 1.2);
--info-font-size: calc(1rem * 1);
display: grid;
padding: 4px;
gap: 4px;
cursor: pointer;
grid-template-columns: var(--title-font-size) 1fr;
grid-template-rows: var(--title-font-size) var(--info-font-size);
line-height: 1;
&:hover {
background: #00000022;
}
& > .icon {
background: #34363b;
border-radius: 50%;
}
& > .title {
font-size: var(--title-font-size);
padding: 0 4px;
}
& > .info {
grid-column: 1 / 3;
font-size: var(--info-font-size);
color: var(--foreground-2);
}
}
.toggle {
padding: 4px;
background: #555;
border-radius: 4px;
cursor: pointer;
&.disabled {
background: #555;
}
}
.message {
contain: content;
// padding: 4px;
&.compact {
--name-width: 144px;
&.title {
margin-top: 8px;
}
& .name {
position: absolute;
font-weight: bold;
margin-left: 8px;
max-width: var(--name-width);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& .content {
margin-left: calc(var(--name-width) + 16px);
}
& .avatar, & .space {
margin-left: 8px;
position: absolute;
}
}
&.cozy {
&.title {
margin-top: 8px;
}
& .name {
font-weight: bold;
line-height: 1;
}
& .content {
margin-left: 54px;
}
& .avatar, & .space {
margin-left: 8px;
position: absolute;
}
}
& .avatar, & .space {
height: 36px;
width: 36px;
&.avatar {
background: #822eba;
border-radius: 4px;
}
}
& a {
color: #822eba;
}
&:hover {
background: #00000022;
}
}
.input {
height: 72px;
background: var(--background-2);
display: flex;
align-items: center;
padding: 0 8px;
gap: 8px;
& > textarea {
background: var(--background-1);
border: solid var(--background-4) 1px;
border-radius: 4px;
width: 100%;
height: 3rem;
color: inherit;
font: inherit;
padding: 4px;
}
}

View file

@ -1,134 +0,0 @@
import { VoidProps, createSignal, onCleanup } from "solid-js";
import "./Asdf.scss";
import { Client, Room, Timeline } from "sdk";
export function Asdf(props: VoidProps<{ client: Client }>) {
const [rooms, setRooms] = createSignal({ count: 0, rooms: [] as Array<Room> });
const [room, setRoom] = createSignal(null as null | Room);
const [asdf, setAsdf] = createSignal(true);
props.client.lists.subscribe("rooms", {
ranges: [[0, 120]],
required_state: [["m.room.name", ""],["m.room.topic", ""]],
// timeline_limit: 3,
} as any);
props.client.on("list", (name, list) => {
if (name === "rooms") setRooms({ ...list });
});
const [offset, setOffset] = createSignal(0);
const interval = setInterval(() => setOffset(offset() + 1), 100);
onCleanup(() => clearInterval(interval));
return (
<>
<header id="header" onClick={() => setAsdf(!asdf())}>
{room()?.id} {room()?.getState("m.room.name")?.content.name}
</header>
<main id="main">{asdf() ? <>
<Threads />
<div class="actions">
<TimelineActions />
</div>
</> : <>
<EventTimeline timeline={room()?.timelines.live || undefined} />
<div class="input">
<textarea placeholder="input text here"></textarea>
</div>
</>
}
</main>
<nav id="nav-rooms">
<ul>
<li onClick={() => setRoom(null)}>home</li>
{rooms().rooms.map(i => <li onClick={() => setRoom(i)}>{i.getState("m.room.name")?.content.name}</li>)}
</ul>
</nav>
<nav id="nav-spaces"></nav>
<div id="sidebar">
<EventTimeline timeline={room()?.timelines.live} />
<div class="input">
<textarea placeholder="input text here"></textarea>
</div>
</div>
<footer id="status"></footer>
</>
)
}
function Threads() {
return (
<div class="timeline">
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
<ThreadsItem />
</div>
);
}
function EventTimeline(props: VoidProps<{ timeline: Timeline | undefined }>) {
return (
<div class="timeline">
{props.timeline?.events.map(i => <Message event={i} />)}
</div>
);
}
function Message(props: VoidProps<{ event: any }>) {
const title = Math.random() > .5;
const compact = false;
return <div class="message" classList={{ title, [compact ? "compact" : "cozy"]: true }}>
{title && !compact && <div class="avatar"></div>}
{title && compact && <div class="name">{props.event.sender}</div>}
<div class="content">
{title && !compact && <div class="name">{props.event.sender}</div>}
<div class="body">{props.event.content.body || `event: (${props.event.type})`}</div>
</div>
</div>;
}
function ThreadsItem() {
return <div class="timeline-thread">
<div class="icon"></div>
<div class="title">Thread title</div>
<div class="info">Info here, date created, other options</div>
</div>;
}
function TimelineActions() {
return <>
<Toggle enabled="[x] Include ignored" disabled="[ ] Include ignored" initial={false} />
<Toggle enabled="[x] Unread" disabled="[ ] Unread" initial={false} />
<Toggle enabled="[x] Watching" disabled="[ ] Watching" initial={false} />
<div class="create-thread">
<div>New thread</div>
<div>+</div>
</div>
</>
}
function Toggle(props: { enabled: string, disabled: string, initial: boolean }) {
const [disabled, setDisabled] = createSignal(props.initial);
return (
<div
class="toggle"
classList={{ disabled: disabled() }}
onClick={() => setDisabled(!disabled())}
>{disabled() ? props.enabled : props.disabled}</div>
)
}

23
src/Atoms.tsx Normal file
View file

@ -0,0 +1,23 @@
import { ParentProps, VoidProps } from "solid-js";
import { useGlobals } from "./Context";
export function Text(props: ParentProps<Record<string, any>>) {
const key = () => props.children?.toString() || "unknown";
const [globals] = useGlobals();
return <>{globals.locale()?.(key(), props) ?? key()}</>;
}
export function Time(props: VoidProps<{} & ({ ts: number } | { date: Date })>) {
const date = () => "date" in props ? props.date : new Date(props.ts);
function format(date: Date): string {
const diff = Date.now() - (+date);
if (diff < 1000 * 60) return "now";
if (diff < 1000 * 60 * 60) return `${Math.round(diff / (1000 * 60))} minutes ago`;
if (diff < 1000 * 60 * 60 * 24) return `${Math.round(diff / (1000 * 60 * 60))} hours ago`;
if (diff < 1000 * 60 * 60 * 24 * 30) return `${Math.round(diff / (1000 * 60 * 60 * 24))} days ago`;
return "long ago";
}
return <time datetime={date().toISOString()}>{format(date())}</time>;
}

89
src/Context.tsx Normal file
View file

@ -0,0 +1,89 @@
import { Accessor, ParentProps, Resource, createContext, createResource, createSignal, useContext } from "solid-js";
import * as i18n from "i18next";
import { Room, Thread } from "sdk";
interface Settings {
compact: boolean,
}
interface GlobalState {
focusedRoom: Accessor<null | Room>,
}
interface RoomState {
focusedThread: Accessor<null | Thread>,
}
interface Globals {
settings: Accessor<Settings>,
globalState: GlobalState,
roomState: RoomState,
locale: Resource<i18n.TFunction>,
}
type Change =
{ type: "focusRoom", room: Room | null } |
{ type: "focusThread", thread: Thread | null }
const GlobalsContext = createContext<[Globals, (change: Change) => void]>();
export const useGlobals = () => useContext(GlobalsContext)!;
export function Contextualizer(props: ParentProps) {
const [settings, setSettings] = createSignal({
compact: false,
locale: "en",
});
let localeInit = false;
const [locale] = createResource(() => settings().locale, async (locale) => {
if (localeInit) {
return i18n.changeLanguage(locale);
} else {
localeInit = true;
return i18n.init({
lng: locale,
debug: true,
resources: {
en: {
translation: {
"message.count_one": "{{count}} message",
"message.count_other": "{{count}} messages",
"message.remaining_one": "{{remaining}} more message",
"message.remaining_other": "{{remaining}} more messages",
"timeago": "{{}}",
}
},
},
});
}
});
Object.assign(globalThis, { setSettings });
const [focusedRoom, setFocusedRoom] = createSignal<Room | null>(null);
const [focusedThread, setFocusedThread] = createSignal<Thread | null>(null);
const globals = {
settings,
globalState: { focusedRoom },
roomState: { focusedThread },
locale,
};
function redux(change: Change) {
switch (change.type) {
case "focusRoom":
setFocusedRoom(change.room);
break;
case "focusThread":
setFocusedThread(change.thread);
break;
}
}
return (
<GlobalsContext.Provider value={[globals, redux]}>
{props.children}
</GlobalsContext.Provider>
)
}

84
src/Main.tsx Normal file
View file

@ -0,0 +1,84 @@
import { Show, VoidProps, createEffect, createResource, createSignal } from "solid-js";
import "./App.scss";
import { Room, Timeline } from "sdk";
import { Message, ThreadsItem } from "./Room";
export function RoomView(props: VoidProps<{ room: Room }>) {
return (
<>
<Threads room={props.room} />
<div class="actions">
<TimelineActions />
</div>
</>
)
}
function Threads(props: VoidProps<{ room: Room }>) {
const [threadChunk] = createResource(() => props.room, async (room: Room) => {
return room.threads.paginate();
});
return (
<div class="timeline threads">
<div class="items">
<ThreadsHeader room={props.room} />
<Show when={!threadChunk.loading} fallback={"loading..."}>
{threadChunk().threads.map((thread) => <ThreadsItem thread={thread} />)}
</Show>
</div>
</div>
);
}
export function EventTimeline(props: VoidProps<{ timeline: Timeline | undefined }>) {
return (
<div class="timeline">
<div class="items">
{props.timeline?.events.map((ev, idx) => <Message event={ev} title={idx === 0} />)}
</div>
</div>
);
}
function ThreadsHeader(props: VoidProps<{ room: Room }>) {
const name = () => props.room.getState("m.room.name")?.content.name || "unnamed room";
return (
<div class="timeline-create">
<h1>Welcome to {name()}!</h1>
<p>This is the beginning of this room. Insert room topic here.</p>
<p style="color:var(--foreground-2)">Debug: room_id=<code style="user-select:all">{props.room.id}</code></p>
<div class="actions">
<button>edit room</button>
<button>invite people</button>
</div>
</div>
);
}
function TimelineActions() {
return <>
<select>
<option>default</option>
<option>include ignoring</option>
<option>only watching</option>
</select>
<Toggle enabled="[x] Unread" disabled="[ ] Unread" initial={false} />
<Toggle enabled="[x] Watching" disabled="[ ] Watching" initial={false} />
<div class="create-thread">
<div>New thread</div>
<div>+</div>
</div>
</>
}
function Toggle(props: { enabled: string, disabled: string, initial: boolean }) {
const [disabled, setDisabled] = createSignal(props.initial);
return (
<div
class="toggle"
classList={{ disabled: disabled() }}
onClick={() => setDisabled(!disabled())}
>{disabled() ? props.enabled : props.disabled}</div>
)
}

96
src/Room.tsx Normal file
View file

@ -0,0 +1,96 @@
import { Thread } from "sdk";
import { Show, VoidProps, createResource, onCleanup } from "solid-js";
import { useGlobals } from "./Context";
import { Text, Time } from "./Atoms";
export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
const [_globals, change] = useGlobals();
const [preview, { mutate }] = createResource(props.thread, async (t) => {
await t.timeline.paginate("f");
return t.timeline.events;
});
const refresh = () => {
console.log("refresh");
mutate([...props.thread.timeline.events]);
}
props.thread.room.on("timeline", refresh);
onCleanup(() => props.thread.room.off("timeline", refresh));
const info = () => props.thread.baseEvent.unsigned["m.relations"]["m.thread"];
const remaining = () => preview() && (info().count - preview()!.length);
const willOpenThread = (target: "default" | "latest") => (e: MouseEvent) => {
// TODO: target
change({
type: "focusThread",
thread: props.thread,
});
console.log("open thread", target);
e.stopPropagation();
};
return (
<article class="timeline-thread">
<header onClick={willOpenThread("default")}>
<div class="top">
<div class="icon"></div>
<div class="title"><TextBlock text={props.thread.baseEvent.content.title} fallback="Untitled thread" formatting /></div>
<div class="date"><Time ts={props.thread.baseEvent.originTs} /></div>
</div>
<div class="bottom" onClick={willOpenThread("latest")}>
<Text count={info().count}>message.count</Text> &bull; <Time ts={info().latest_event.origin_server_ts} />
</div>
</header>
<div class="preview">
<Show when={!preview.loading}>
{preview()?.filter(ev => ev.type === "m.message").map((ev, idx) => <Message event={ev} title={idx === 0} />)}
</Show>
</div>
<Show when={remaining()}>
<footer>
<Text remaining={remaining()}>message.remaining</Text>
</footer>
</Show>
</article>
);
}
function TextBlock(props: VoidProps<{ text: any, formatting?: boolean, fallback?: string }>) {
function sanitize(str: string) {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
const body = () => props.text?.[0]?.body;
// console.log(props.text, body());
// for portability, it might be better to add a custom renderer instead of using html
return <div innerHTML={body() ? sanitize(body()) : props.fallback}></div>;
}
export function Message(props: VoidProps<{ event: any, title?: boolean }>) {
const [globals] = useGlobals();
const title = () => props.title;
const compact = () => globals!.settings().compact;
if (props.event.type !== "m.message") {
console.warn("constructed Message with a non-m.message event");
}
return (
<div class="message" classList={{ title: title(), [compact() ? "compact" : "cozy"]: true }}>
{title() && !compact() && <div class="avatar"></div>}
{title() && compact() && <div class="name">{props.event.sender}</div>}
<div class="content">
{title() && !compact() && <div class="name">{props.event.sender}</div>}
<div class="body">
<TextBlock text={props.event.content.text} />
</div>
</div>
</div>
);
}

View file

@ -1,8 +0,0 @@
.tooltip {
background: #333;
color: white;
font-weight: bold;
padding: 4px 8px;
font-size: 13px;
border-radius: 4px;
}

View file

@ -1,25 +0,0 @@
import { createPopper } from "@popperjs/core";
import { onCleanup, onMount } from "solid-js";
import { Portal } from "solid-js/web";
export function Tooltip() {
const text = <div>hello world</div>;
const tooltip = <div>tooltip!</div>;
let popper;
onMount(() => {
popper = createPopper(text, tooltip);
});
onCleanup(() => popper.destroy());
return (
<>
{text}
<Portal>
<div class="tooltip">
<div data-popper-arrow></div>
{tooltip}
</div>
</Portal>
</>
);
}