Sketchy ui for creating rooms and threads
This commit is contained in:
parent
7b334b3bc6
commit
6e3ede18bf
8 changed files with 316 additions and 57 deletions
|
@ -8,6 +8,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"i18next": "^23.7.8",
|
||||
"sdk": "link:../sdk-ts",
|
||||
|
|
|
@ -5,6 +5,9 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@floating-ui/dom':
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
'@popperjs/core':
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8
|
||||
|
@ -567,6 +570,23 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/@floating-ui/core@1.5.2:
|
||||
resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==}
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.1.6
|
||||
dev: false
|
||||
|
||||
/@floating-ui/dom@1.5.3:
|
||||
resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==}
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.5.2
|
||||
'@floating-ui/utils': 0.1.6
|
||||
dev: false
|
||||
|
||||
/@floating-ui/utils@0.1.6:
|
||||
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.3:
|
||||
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
118
src/App.scss
118
src/App.scss
|
@ -1,11 +1,12 @@
|
|||
#root {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
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-columns: 64px 256px 1fr auto;
|
||||
grid-template-rows: 64px 1fr 72px;
|
||||
}
|
||||
|
||||
|
@ -18,13 +19,14 @@
|
|||
background: var(--background-1);
|
||||
grid-area: main;
|
||||
contain: content;
|
||||
overflow: hidden;
|
||||
|
||||
& > .timeline {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
&.threads > .items {
|
||||
padding: 8px 0 80px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
& > .items {
|
||||
|
@ -32,7 +34,7 @@
|
|||
flex-direction: column;
|
||||
justify-content: end;
|
||||
min-height: 100%;
|
||||
padding: 8px 0 80px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,6 +110,13 @@
|
|||
background: var(--background-2);
|
||||
border-left: solid var(--background-3) 1px;
|
||||
grid-area: sidebar;
|
||||
width: 256px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
|
||||
&.thread {
|
||||
width: 512px;
|
||||
}
|
||||
}
|
||||
|
||||
#status {
|
||||
|
@ -144,6 +153,16 @@
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
& > .title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
& > * {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
background: #34363b;
|
||||
border-radius: 50%;
|
||||
|
@ -198,11 +217,6 @@
|
|||
.timeline-create {
|
||||
margin: 32px 32px 8px;
|
||||
|
||||
& > h1 {
|
||||
line-height: 1.1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
& > .actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
@ -292,6 +306,10 @@
|
|||
}
|
||||
|
||||
.input {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
background: var(--background-2);
|
||||
display: flex;
|
||||
|
@ -310,3 +328,87 @@
|
|||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-view {
|
||||
height: 100%;
|
||||
|
||||
& > .scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
scrollbar-color: var(--background-1) var(--background-3);
|
||||
|
||||
& > .spacer {
|
||||
margin-top: auto;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
& > .spacer-bottom {
|
||||
margin-top: 72px;
|
||||
}
|
||||
|
||||
& > .thread-title {
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
z-index: 999;
|
||||
|
||||
& > * {
|
||||
padding: 1px 8px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > .fake {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
& > .real {
|
||||
position: absolute;
|
||||
transition: all .2s;
|
||||
border-bottom: solid var(--background-3) 0;
|
||||
}
|
||||
|
||||
&.stuck > .real {
|
||||
background: var(--background-1);
|
||||
border-bottom: solid var(--background-3) 1px;
|
||||
font-size: 1.2rem;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
& > header {
|
||||
padding: 8px;
|
||||
|
||||
& > p {
|
||||
opacity: 1;
|
||||
transition: all .2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
#sidebar.thread {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
box-shadow: -1px 0 5px #1114;
|
||||
|
||||
& > .thread-view > .scroll > .spacer {
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
scrollbar-color: var(--background-4) var(--background-1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.home {
|
||||
padding: 8px;
|
||||
}
|
||||
|
|
31
src/App.tsx
31
src/App.tsx
|
@ -1,7 +1,7 @@
|
|||
import { Show, VoidProps, createEffect, createSignal, onCleanup } from "solid-js";
|
||||
import { Match, Show, Switch, VoidProps, createSignal, onCleanup } from "solid-js";
|
||||
import "./App.scss";
|
||||
import { Client, Room } from "sdk";
|
||||
import { EventTimeline, RoomView } from "./Main";
|
||||
import { Home, RoomView, ThreadView } from "./Main";
|
||||
import { Contextualizer, useGlobals } from "./Context";
|
||||
|
||||
function Wrapper() {
|
||||
|
@ -27,13 +27,6 @@ function App(props: VoidProps<{ client: Client }>) {
|
|||
const [globals, change] = useGlobals();
|
||||
const [rooms, setRooms] = createSignal({ count: 0, rooms: [] as Array<Room> });
|
||||
|
||||
setTimeout(() => {
|
||||
change({
|
||||
type: "focusRoom",
|
||||
room: props.client.rooms.get("!T7ihGmUDiIW0AckfcL:localhost")!,
|
||||
});
|
||||
}, 1000)
|
||||
|
||||
Object.assign(globalThis, { client: props.client });
|
||||
|
||||
props.client.lists.subscribe("rooms", {
|
||||
|
@ -46,16 +39,14 @@ function App(props: VoidProps<{ client: Client }>) {
|
|||
if (name === "rooms") setRooms({ ...list });
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(globals.roomState.focusedThread());
|
||||
})
|
||||
const thread = () => globals.roomState.focusedThread();
|
||||
|
||||
return (
|
||||
<>
|
||||
<header id="header">
|
||||
</header>
|
||||
<main id="main">
|
||||
<Show when={globals.globalState.focusedRoom()}>
|
||||
<Show when={globals.globalState.focusedRoom()} fallback={<Home client={props.client} />}>
|
||||
<RoomView room={globals.globalState.focusedRoom()!} />
|
||||
</Show>
|
||||
</main>
|
||||
|
@ -66,11 +57,15 @@ function App(props: VoidProps<{ client: Client }>) {
|
|||
</ul>
|
||||
</nav>
|
||||
<nav id="nav-spaces"></nav>
|
||||
<div id="sidebar">
|
||||
<EventTimeline timeline={globals.roomState.focusedThread()?.timeline} />
|
||||
<div class="input">
|
||||
<textarea placeholder="input text here"></textarea>
|
||||
</div>
|
||||
<div id="sidebar" classList={{ thread: !!thread() }}>
|
||||
<Switch>
|
||||
<Match when={thread()}>
|
||||
<ThreadView thread={thread()!} />
|
||||
</Match>
|
||||
<Match when={!thread()}>
|
||||
empty sidebar
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<footer id="status"></footer>
|
||||
</>
|
||||
|
|
|
@ -48,8 +48,8 @@ export function Contextualizer(props: ParentProps) {
|
|||
translation: {
|
||||
"message.count_one": "{{count}} message",
|
||||
"message.count_other": "{{count}} messages",
|
||||
"message.remaining_one": "{{remaining}} more message",
|
||||
"message.remaining_other": "{{remaining}} more messages",
|
||||
"message.remaining_one": "{{count}} more message",
|
||||
"message.remaining_other": "{{count}} more messages",
|
||||
"timeago": "{{}}",
|
||||
}
|
||||
},
|
||||
|
|
169
src/Main.tsx
169
src/Main.tsx
|
@ -1,14 +1,17 @@
|
|||
import { Show, VoidProps, createEffect, createResource, createSignal } from "solid-js";
|
||||
import { ParentProps, Show, VoidProps, createEffect, createResource, createSignal, onCleanup, onMount } from "solid-js";
|
||||
import "./App.scss";
|
||||
import { Room, Timeline } from "sdk";
|
||||
import { Message, ThreadsItem } from "./Room";
|
||||
import { Client, Room, Thread, Timeline } from "sdk";
|
||||
import { Message, TextBlock, ThreadsItem } from "./Room";
|
||||
import { computePosition, shift, offset, autoUpdate } from "@floating-ui/dom";
|
||||
import { Portal } from "solid-js/web";
|
||||
import { Time } from "./Atoms";
|
||||
|
||||
export function RoomView(props: VoidProps<{ room: Room }>) {
|
||||
return (
|
||||
<>
|
||||
<Threads room={props.room} />
|
||||
<div class="actions">
|
||||
<TimelineActions />
|
||||
<TimelineActions room={props.room} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -24,39 +27,177 @@ function Threads(props: VoidProps<{ room: Room }>) {
|
|||
<div class="items">
|
||||
<ThreadsHeader room={props.room} />
|
||||
<Show when={!threadChunk.loading} fallback={"loading..."}>
|
||||
{threadChunk().threads.map((thread) => <ThreadsItem thread={thread} />)}
|
||||
{threadChunk()!.threads.map((thread: Thread) => <ThreadsItem thread={thread} />)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventTimeline(props: VoidProps<{ timeline: Timeline | undefined }>) {
|
||||
export function Tooltip(props: ParentProps<{ tip: string }>) {
|
||||
let tipEl: HTMLDivElement;
|
||||
let contentEl: HTMLDivElement;
|
||||
|
||||
onMount(() => queueMicrotask(async () => {
|
||||
const cleanup = autoUpdate(contentEl, tipEl, () => {
|
||||
computePosition(contentEl, tipEl, {
|
||||
middleware: [shift(), offset()],
|
||||
}).then(pos => {
|
||||
tipEl.style.translate = `${pos.x}px ${pos.y}px`;
|
||||
});
|
||||
});
|
||||
|
||||
onCleanup(() => cleanup());
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="timeline">
|
||||
<div class="items">
|
||||
{props.timeline?.events.map((ev, idx) => <Message event={ev} title={idx === 0} />)}
|
||||
<>
|
||||
<span ref={contentEl!}>{props.children}</span>
|
||||
<Portal>
|
||||
<div ref={tipEl!} style="position:fixed;top:0;left:0">{props.tip}</div>
|
||||
</Portal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ThreadView(props: VoidProps<{ thread: Thread }>) {
|
||||
let textareaEl: HTMLTextAreaElement;
|
||||
|
||||
console.log(props.thread)
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
props.thread.room.sendEvent("m.message", {
|
||||
text: [{ body: textareaEl.value }],
|
||||
"m.relations": [{ rel_type: "m.thread", event_id: props.thread.id }],
|
||||
});
|
||||
textareaEl.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
const [timeline, { mutate }] = createResource(() => props.thread.timeline, (tl) => tl, {
|
||||
storage: (init) => createSignal(init, { equals: false }),
|
||||
});
|
||||
|
||||
const refresh = () => mutate(props.thread.timeline);
|
||||
|
||||
let oldThread: Thread | undefined;
|
||||
createEffect(() => {
|
||||
oldThread?.room.off("timeline", refresh);
|
||||
props.thread.room.on("timeline", refresh);
|
||||
oldThread = props.thread;
|
||||
});
|
||||
onCleanup(() => oldThread?.room.off("timeline", refresh));
|
||||
|
||||
return (
|
||||
<div class="thread-view">
|
||||
<div class="scroll">
|
||||
<ThreadHeader thread={props.thread} />
|
||||
{timeline() && <EventTimeline timeline={timeline()!} />}
|
||||
<div class="spacer-bottom"></div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<textarea placeholder="input text here" ref={textareaEl!} onKeyDown={handleKeyDown}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadHeader(props: VoidProps<{ thread: Thread }>) {
|
||||
const event = () => props.thread.baseEvent;
|
||||
let headerEl: HTMLHeadingElement;
|
||||
const [stuck, setStuck] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(([e]) => setStuck(!e.isIntersecting), {
|
||||
threshold: 1,
|
||||
root: headerEl.parentElement,
|
||||
});
|
||||
|
||||
observer.observe(headerEl);
|
||||
onCleanup(() => observer.unobserve(headerEl));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="spacer"></div>
|
||||
<div class="thread-title" ref={headerEl!} classList={{ stuck: stuck() }}>
|
||||
<h1 class="real">
|
||||
<TextBlock text={event().content.title} fallback="Untitled thread" />
|
||||
</h1>
|
||||
<h1 class="fake" aria-hidden="true">
|
||||
<TextBlock text={event().content.title} fallback="Untitled thread" />
|
||||
</h1>
|
||||
</div>
|
||||
<header>
|
||||
<p>Created by {event().sender} <Time ts={event().originTs} /></p>
|
||||
<p style="color:var(--foreground-2)">Debug: event_id=<code style="user-select:all">{event().id}</code></p>
|
||||
</header>
|
||||
<Message event={event()} title={true} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Home(props: VoidProps<{ client: Client }>) {
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const { name } = e.target.elements;
|
||||
props.client.rooms.create({ initialState: [{ type: "m.room.name", content: { name: name.value }, stateKey: ""}] });
|
||||
name.value = "";
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="home">
|
||||
<h1>welcome home</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input type="text" placeholder="name" name="name" />
|
||||
<input type="submit" value="create room" />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventTimeline(props: VoidProps<{ timeline: Timeline }>) {
|
||||
return (
|
||||
<>
|
||||
{props.timeline.events.filter(ev => ev.type === "m.message").map((ev, idx) => <Message event={ev} title={idx === 0} />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadsHeader(props: VoidProps<{ room: Room }>) {
|
||||
const name = () => props.room.getState("m.room.name")?.content.name || "unnamed room";
|
||||
const topic = () => props.room.getState("m.room.topic")?.content.topic || "";
|
||||
return (
|
||||
<div class="timeline-create">
|
||||
<h1>Welcome to {name()}!</h1>
|
||||
<p>This is the beginning of this room. Insert room topic here.</p>
|
||||
<p>This is the beginning of this room. {topic()}</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>
|
||||
<button onClick={() => alert("todo!")}>edit room</button>
|
||||
<button onClick={() => alert("todo!")}>invite people</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelineActions() {
|
||||
function TimelineActions(props: VoidProps<{ room: Room }>) {
|
||||
async function makeThread() {
|
||||
const title = prompt("title");
|
||||
const body = prompt("body");
|
||||
const thread = await props.room.sendEvent("m.thread.message", {
|
||||
title: [{ body: title }],
|
||||
text: [{ body }],
|
||||
});
|
||||
await props.room.sendEvent("m.dummy", {
|
||||
"m.relations": [
|
||||
{ rel_type: "m.thread", event_id: thread.id },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return <>
|
||||
<select>
|
||||
<option>default</option>
|
||||
|
@ -66,7 +207,7 @@ function TimelineActions() {
|
|||
<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 onclick={makeThread}>New thread</div>
|
||||
<div>+</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
28
src/Room.tsx
28
src/Room.tsx
|
@ -1,30 +1,30 @@
|
|||
import { Thread } from "sdk";
|
||||
import { Show, VoidProps, createResource, onCleanup } from "solid-js";
|
||||
import { Show, VoidProps, createResource, createSignal, 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 [globals, change] = useGlobals();
|
||||
const [preview, { mutate }] = createResource(props.thread, async (t) => {
|
||||
await t.timeline.paginate("f");
|
||||
await t.timeline.paginate("f", 1000);
|
||||
return t.timeline.events;
|
||||
}, {
|
||||
storage: (init) => createSignal(init, { equals: false }),
|
||||
});
|
||||
|
||||
const refresh = () => {
|
||||
console.log("refresh");
|
||||
mutate([...props.thread.timeline.events]);
|
||||
}
|
||||
const 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 remaining = () => preview() && (info().count - Math.min(preview()!.length, 10));
|
||||
|
||||
const willOpenThread = (target: "default" | "latest") => (e: MouseEvent) => {
|
||||
// TODO: target
|
||||
const isSame = globals.roomState.focusedThread()?.id === props.thread.id;
|
||||
change({
|
||||
type: "focusThread",
|
||||
thread: props.thread,
|
||||
thread: isSame ? null : props.thread,
|
||||
});
|
||||
console.log("open thread", target);
|
||||
e.stopPropagation();
|
||||
|
@ -35,7 +35,7 @@ export function ThreadsItem(props: VoidProps<{ thread: 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="title"><TextBlock text={props.thread.baseEvent.content.title} fallback="Untitled thread" /></div>
|
||||
<div class="date"><Time ts={props.thread.baseEvent.originTs} /></div>
|
||||
</div>
|
||||
<div class="bottom" onClick={willOpenThread("latest")}>
|
||||
|
@ -44,19 +44,19 @@ export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
|
|||
</header>
|
||||
<div class="preview">
|
||||
<Show when={!preview.loading}>
|
||||
{preview()?.filter(ev => ev.type === "m.message").map((ev, idx) => <Message event={ev} title={idx === 0} />)}
|
||||
{preview()?.filter(ev => ev.type === "m.message").slice(0, 10).map((ev, idx) => <Message event={ev} title={idx === 0} />)}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={remaining()}>
|
||||
<footer>
|
||||
<Text remaining={remaining()}>message.remaining</Text>
|
||||
<Text count={remaining()}>message.remaining</Text>
|
||||
</footer>
|
||||
</Show>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function TextBlock(props: VoidProps<{ text: any, formatting?: boolean, fallback?: string }>) {
|
||||
export function TextBlock(props: VoidProps<{ text: any, formatting?: boolean, fallback?: string }>) {
|
||||
function sanitize(str: string) {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
|
@ -88,7 +88,7 @@ export function Message(props: VoidProps<{ event: any, title?: boolean }>) {
|
|||
<div class="content">
|
||||
{title() && !compact() && <div class="name">{props.event.sender}</div>}
|
||||
<div class="body">
|
||||
<TextBlock text={props.event.content.text} />
|
||||
<TextBlock text={props.event.content.text ?? props.event.content.body} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -43,7 +43,7 @@ button, input, textarea, select {
|
|||
font: inherit;
|
||||
}
|
||||
|
||||
button, select {
|
||||
button, input, select {
|
||||
background: var(--background-2);
|
||||
border: solid var(--background-3) 1px;
|
||||
padding: 4px 8px;
|
||||
|
|
Loading…
Reference in a new issue