Hook up the ui to sdk
This commit is contained in:
parent
d2bee0b6b4
commit
7b334b3bc6
12 changed files with 375 additions and 560 deletions
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
24
src/App.scss
24
src/App.scss
|
@ -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;
|
||||
|
|
177
src/App.tsx
177
src/App.tsx
|
@ -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 • 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;
|
||||
|
|
222
src/Asdf.scss
222
src/Asdf.scss
|
@ -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;
|
||||
}
|
||||
}
|
134
src/Asdf.tsx
134
src/Asdf.tsx
|
@ -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
23
src/Atoms.tsx
Normal 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
89
src/Context.tsx
Normal 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
84
src/Main.tsx
Normal 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
96
src/Room.tsx
Normal 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> • <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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
.tooltip {
|
||||
background: #333;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue