tweak and edit more stuff

This commit is contained in:
tezlm 2023-12-12 12:59:50 -08:00
parent 47d5c2fee2
commit 1ae824fa4b
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
9 changed files with 409 additions and 149 deletions

View file

@ -15,12 +15,16 @@
"@tanstack/virtual-core": "^3.0.1",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-document": "^2.1.13",
"@tiptap/extension-dropcursor": "^2.1.13",
"@tiptap/extension-gapcursor": "^2.1.13",
"@tiptap/extension-hard-break": "^2.1.13",
"@tiptap/extension-history": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13",
"@tiptap/extension-paragraph": "^2.1.13",
"@tiptap/extension-placeholder": "^2.1.13",
"@tiptap/extension-text": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"i18next": "^23.7.8",
"marked": "^11.0.1",

View file

@ -26,6 +26,15 @@ dependencies:
'@tiptap/extension-document':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-dropcursor':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-gapcursor':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-hard-break':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-history':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
@ -44,6 +53,9 @@ dependencies:
'@tiptap/pm':
specifier: ^2.1.13
version: 2.1.13
'@tiptap/starter-kit':
specifier: ^2.1.13
version: 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/suggestion':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
@ -831,6 +843,22 @@ packages:
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-blockquote@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-oe6wSQACmODugoP9XH3Ouffjy4BsOBWfTC+dETHNCG6ZED6ShHN3CB9Vr7EwwRgmm2WLaKAjMO1sVumwH+Z1rg==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-bold@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-6cHsQTh/rUiG4jkbJer3vk7g60I5tBwEBSGpdxmEHh83RsvevD8+n92PjA24hYYte5RNlATB011E1wu8PVhSvw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-bubble-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-Hm7e1GX3AI6lfaUmr6WqsS9MMyXIzCkhh+VQi6K8jj4Q4s8kY4KPoAyD/c3v9pZ/dieUtm2TfqrOCkbHzsJQBg==}
peerDependencies:
@ -842,6 +870,32 @@ packages:
tippy.js: 6.3.7
dev: false
/@tiptap/extension-bullet-list@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-code-block@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-E3tweNExPOV+t1ODKX0MDVsS0aeHGWc1ECt+uyp6XwzsN0bdF2A5+pttQqM7sTcMnQkVACGFbn9wDeLRRcfyQg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-code@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-document@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-wLwiTWsVmZTGIE5duTcHRmW4ulVxNW4nmgfpk95+mPn1iKyNGtrVhGWleLhBlTj+DWXDtcfNWZgqZkZNzhkqYQ==}
peerDependencies:
@ -850,6 +904,16 @@ packages:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-dropcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-NAyJi4BJxH7vl/2LNS1X0ndwFKjEtX+cRgshXCnMyh7qNpIRW6Plczapc/W1OiMncOEhZJfpZfkRSfwG01FWFg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-floating-menu@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-9Oz7pk1Nts2+EyY+rYfnREGbLzQ5UFazAvRhF6zAJdvyuDmAYm0Jp6s0GoTrpV0/dJEISoFaNpPdMJOb9EBNRw==}
peerDependencies:
@ -861,6 +925,32 @@ packages:
tippy.js: 6.3.7
dev: false
/@tiptap/extension-gapcursor@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-Cl5apsoTcyPPCgE3ThufxQxZ1wyqqh+9uxUN9VF9AbeTkid6oPZvKXwaILf6AFnkSy+SuKrb9kZD2iaezxpzXw==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-hard-break@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-TGkMzMQayuKg+vN4du0x1ahEItBLcCT1jdWeRsjdM8gHfzbPLdo4PQhVsvm1I0xaZmbJZelhnVsUwRZcIu1WNA==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-heading@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-PEmc19QLmlVUTiHWoF0hpgNTNPNU0nlaFmMKskzO+cx5Df4xvHmv/UqoIwp7/UFbPMkfVJT1ozQU7oD1IWn9Hg==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-history@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-1ouitThGTBUObqw250aDwGLMNESBH5PRXIGybsCFO1bktdmWtEw7m72WY41EuX2BH8iKJpcYPerl3HfY1vmCNw==}
peerDependencies:
@ -871,6 +961,32 @@ packages:
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-horizontal-rule@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-7OgjgNqZXvBejgULNdMSma2M1nzv4bbZG+FT5XMFZmEOxR9IB1x/RzChjPdeicff2ZK2sfhMBc4Y9femF5XkUg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-italic@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-list-item@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-6e8iiCWXOiJTl1XOwVW2tc0YG18h70HUtEHFCx2m5HspOGFKsFEaSS3qYxOheM9HxlmQeDt8mTtqftRjEFRxPQ==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-mention@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(@tiptap/suggestion@2.1.13):
resolution: {integrity: sha512-OYqaucyBiCN/CmDYjpOVX74RJcIEKmAqiZxUi8Gfaq7ryEO5a8Gk93nK+8uZ0onaqHE+mHpoLFFbcAFbOPgkUQ==}
peerDependencies:
@ -883,6 +999,14 @@ packages:
'@tiptap/suggestion': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-ordered-list@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-UO4ZAL5Vrr1WwER5VjgmeNIWHpqy9cnIRo1En07gZ0OWTjs1eITPcu+4TCn1ZG6DhoFvAQzE5DTxxdhIotg+qw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-paragraph@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-cEoZBJrsQn69FPpUMePXG/ltGXtqKISgypj70PEHXt5meKDjpmMVSY4/8cXvFYEYsI9GvIwyAK0OrfAHiSoROA==}
peerDependencies:
@ -901,6 +1025,14 @@ packages:
'@tiptap/pm': 2.1.13
dev: false
/@tiptap/extension-strike@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-VN6zlaCNCbyJUCDyBFxavw19XmQ4LkCh8n20M8huNqW77lDGXA2A7UcWLHaNBpqAijBRu9mWI8l4Bftyf2fcAw==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-text@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-zzsTTvu5U67a8WjImi6DrmpX2Q/onLSaj+LRWPh36A1Pz2WaxW5asZgaS+xWCnR+UrozlCALWa01r7uv69jq0w==}
peerDependencies:
@ -932,6 +1064,32 @@ packages:
prosemirror-view: 1.32.6
dev: false
/@tiptap/starter-kit@2.1.13(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-ph/mUR/OwPtPkZ5rNHINxubpABn8fHnvJSdhXFrY/q6SKoaO11NZXgegRaiG4aL7O6Sz4LsZVw6Sm0Ae+GJmrg==}
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-blockquote': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-bold': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-bullet-list': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-code': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-code-block': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-document': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-dropcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-gapcursor': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-hard-break': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-heading': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-history': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-horizontal-rule': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-italic': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-list-item': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-ordered-list': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-paragraph': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-strike': 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-text': 2.1.13(@tiptap/core@2.1.13)
transitivePeerDependencies:
- '@tiptap/pm'
dev: false
/@tiptap/suggestion@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-Y05TsiXTFAJ5SrfoV+21MAxig5UNbY0AVa03lQlh/yicTRPpIc6hgZzblB0uxDSYoj6+kaHE4MIZvPvhUD8BJQ==}
peerDependencies:

View file

@ -7,7 +7,7 @@
"nav-spaces nav-rooms main sidebar"
"status status main sidebar";
grid-template-columns: 64px 256px 1fr auto;
grid-template-rows: 64px 1fr 72px;
grid-template-rows: 48px 1fr 72px;
}
#header {
@ -95,7 +95,11 @@
padding: 4px 8px;
&:hover {
background: #ffffff33;
background: #ffffff11;
}
&.selected {
background: #ffffff22;
}
}
}
@ -339,11 +343,21 @@
height: 100%;
overflow-y: scroll;
scrollbar-color: var(--background-1) var(--background-3);
overflow-anchor: none;
// overflow-anchor: none;
& > .spacer {
margin-top: auto;
margin-bottom: 32px;
min-height: 32px;
padding: 0 4px;
display: flex;
flex-direction: column;
align-items: start;
justify-content: end;
& > button {
padding: 0 4px;
}
}
& > .spacer-bottom {
@ -354,6 +368,7 @@
position: sticky;
top: -1px;
z-index: 999;
visibility: hidden;
& > * {
padding: 1px 8px 0;
@ -366,8 +381,25 @@
& > .real {
position: absolute;
transition: all .2s;
border-bottom: solid var(--background-3) 0;
display: flex;
& > div {
flex: 1;
}
& > button {
display: none;
font-size: 1rem;
margin: 0;
padding: 0 4px;
font-weight: initial;
}
}
&.setup > .real {
transition: all .2s;
visibility: visible;
}
&.stuck > .real {
@ -375,6 +407,17 @@
border-bottom: solid var(--background-3) 1px;
font-size: 1.2rem;
padding: 8px;
& > div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 8px;
}
& > button {
display: block;
}
}
}

View file

@ -1,4 +1,4 @@
import { Match, Show, Switch, VoidProps, createSignal, onCleanup } from "solid-js";
import { Match, Show, Switch, VoidProps, createSelector, createSignal, onCleanup } from "solid-js";
import "./App.scss";
import { Client, Room } from "sdk";
import { Home, RoomView } from "./Main";
@ -31,7 +31,7 @@ function App(props: VoidProps<{ client: Client }>) {
Object.assign(globalThis, { client: props.client });
props.client.lists.subscribe("rooms", {
ranges: [[0, 120]],
ranges: [[0, 99999]],
required_state: [["m.room.name", ""], ["m.room.topic", ""]],
// timeline_limit: 3,
} as any);
@ -41,11 +41,12 @@ function App(props: VoidProps<{ client: Client }>) {
});
const thread = () => globals.roomState.focusedThread();
const isSelected = createSelector(() => globals.globalState.focusedRoom());
return (
<>
<header id="header">
</header>
<header id="header"></header>
<main id="main">
<Show when={globals.globalState.focusedRoom()} fallback={<Home client={props.client} />}>
<RoomView room={globals.globalState.focusedRoom()!} />
@ -53,8 +54,8 @@ function App(props: VoidProps<{ client: Client }>) {
</main>
<nav id="nav-rooms">
<ul>
<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>)}
<li classList={{ selected: isSelected(null) }} onClick={() => change({ type: "focusRoom", room: null })}>home</li>
{rooms().rooms.map(i => <li classList={{ selected: isSelected(i) }} onClick={() => change({ type: "focusRoom", room: i })}>{i.getState("m.room.name")?.content.name || "unnamed"}</li>)}
</ul>
</nav>
<nav id="nav-spaces"></nav>

View file

@ -1,114 +1,3 @@
import { createEditor, EditorContent } from "tiptap-solid";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import History from "@tiptap/extension-history";
// import Mention from "@tiptap/extension-mention";
import Placeholder from "@tiptap/extension-placeholder";
import "./Editor.scss";
// import { Accessor, createEffect, createSignal, VoidProps } from "solid-js";
// import { Node } from "@tiptap/core";
import { Extension, Mark, markInputRule, wrappingInputRule } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { marked } from "marked";
// function Suggestions(props: VoidProps<{ suggestions: Accessor<Array<string>> }>) {
// createEffect(() => console.log(props.suggestions()));
// return (
// <ul>
// {props.suggestions().map(i => <li>{i}</li>)}
// </ul>
// )
// }
const Markdown = Extension.create({
addKeyboardShortcuts() {
return {
"Mod-b": () => {
const { state } = this.editor.view;
const { selection } = state;
const { from, to } = selection;
this.editor.chain()
.insertContentAt({ from: to, to }, "**", { updateSelection: true })
.insertContentAt({ from, to: from }, "**", { updateSelection: true })
.setTextSelection({ from: from + 2, to: to + 2 })
.focus()
.run();
return true;
},
}
},
addProseMirrorPlugins() {
return [new Plugin({
key: new PluginKey("markdown"),
props: {
decorations(state) {
const decorations = [];
let pos = 0;
function walk(token) {
if (token.type === "em") {
decorations.push(Decoration.inline(pos, pos + 2, { class: "syn" }));
decorations.push(Decoration.inline(pos + 2, pos + token.raw.length, { style: "font-style:italic" }));
decorations.push(Decoration.inline(pos + token.raw.length, pos + token.raw.length + 1, { class: "syn" }));
pos += token.raw.length;
} else if (token.type === "strong") {
decorations.push(Decoration.inline(pos + 1, pos + 3, { class: "syn" }));
decorations.push(Decoration.inline(pos + 3, pos + token.raw.length - 1, { style: "font-weight:bold" }));
decorations.push(Decoration.inline(pos + token.raw.length - 1, pos + token.raw.length + 1, { class: "syn" }));
pos += token.raw.length;
} else if (token.type === "codespan") {
decorations.push(Decoration.inline(pos, pos + 2, { class: "syn" }));
decorations.push(Decoration.inline(pos + 2, pos + token.raw.length, { nodeName: "code" }));
decorations.push(Decoration.inline(pos + token.raw.length, pos + token.raw.length + 1, { class: "syn" }));
pos += token.raw.length;
} else if (token.type === "text") {
pos += token.raw.length;
} else {
for (const t of token.tokens || []) {
walk(t);
}
}
}
// console.log(state.doc.textContent)
// console.log(marked.lexer(state.doc.textContent));
for (const token of marked.lexer(state.doc.textContent)) walk(token);
return DecorationSet.create(state.doc, decorations);
}
}
})]
}
});
export function Editor() {
let editorEl;
const editor = createEditor({
content: "hello world *asdf*",
autofocus: true,
extensions: [
Document,
Paragraph,
Text,
History,
Placeholder.configure({
placeholder: "write something nice...",
}),
Markdown,
]
});
function doThing() {
const ed = editor()!;
// ed.chain().focus().insertContent("hello world!").run();
ed.state.selection.$from
ed.state.selection.$to
ed.chain().focus().insertContentAt(0, "content").run();
}
return (
<div class="editor" ref={editorEl}>
<button onClick={doThing}>do thing</button>
<EditorContent editor={editor()} />
</div>
);
return <textarea />;
}

View file

@ -34,11 +34,19 @@ function Threads(props: VoidProps<{ room: Room }>) {
);
}
const ROOM_TYPE_CHAT = "jw.chat";
const ROOM_TYPE_SPACE = "m.space";
export function Home(props: VoidProps<{ client: Client }>) {
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
const name = (e.target as HTMLFormElement).elements.namedItem("name")! as HTMLInputElement;
props.client.rooms.create({ initialState: [{ type: "m.room.name", content: { name: name.value }, stateKey: ""}] });
props.client.rooms.create({
creationContent: {
type: ROOM_TYPE_CHAT,
},
initialState: [{ type: "m.room.name", content: { name: name.value }, stateKey: ""}]
});
name.value = "";
}

View file

@ -6,8 +6,9 @@ import { Text, Time } from "./Atoms";
export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
const [globals, change] = useGlobals();
const THREAD_PREVIEW_COUNT = 10;
const [preview, { mutate }] = createResource(props.thread, async (thread) => {
return thread.room.timelines.forThread(thread, "start");
return thread.timelines.fetch("start");
}, {
storage: (init) => createSignal(init, { equals: false }),
});
@ -21,7 +22,7 @@ export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
onCleanup(() => props.thread.room.off("timeline", refresh));
const [thread, setThread] = createSignal(props.thread, { equals: false });
const remaining = () => preview() && (thread().messageCount - Math.min(preview()!.getEvents().length, 10));
const remaining = () => preview() && (thread().messageCount - Math.min(preview()!.getEvents().length, THREAD_PREVIEW_COUNT ));
const willOpenThread = (target: "default" | "latest") => (e: MouseEvent) => {
// TODO: target
@ -48,7 +49,7 @@ export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
</header>
<div class="preview">
<Show when={!preview.loading}>
{preview()?.getEvents().filter(ev => ev.type === "m.message").slice(0, 10).map((ev, idx) => <Message event={ev} title={idx === 0} />)}
{preview()?.getEvents().filter(ev => ev.type === "m.message").slice(0, THREAD_PREVIEW_COUNT).map((ev, idx) => <Message event={ev} title={idx === 0} />)}
</Show>
</div>
<Show when={remaining() && false}>

View file

@ -1,6 +1,6 @@
import { For, ParentProps, Show, VoidProps, createEffect, createResource, createSignal, onCleanup, onMount } from "solid-js";
import "./App.scss";
import { Client, Room, Thread, Timeline } from "sdk";
import { Client, Room, Thread } from "sdk";
import { Message, TextBlock, ThreadsItem } from "./Room";
// import { computePosition, shift, offset, autoUpdate } from "@floating-ui/dom";
// import { Portal } from "solid-js/web";
@ -8,6 +8,7 @@ import { Time } from "./Atoms";
import { ThreadTimeline } from "sdk/dist/src/timeline";
import { debounce } from "@solid-primitives/scheduled";
import { Editor } from "./Editor";
import { useGlobals } from "./Context";
// import { createVirtualizer } from "@tanstack/solid-virtual";
export function ThreadView(props: VoidProps<{ thread: Thread }>) {
@ -25,9 +26,13 @@ export function ThreadView(props: VoidProps<{ thread: Thread }>) {
}
}
const [timeline, { mutate }] = createResource(() => props.thread, async (thread) => {
return thread.room.timelines.forThread(thread, "end");
}, {
// updates when the thread changes
const [timelineReal] = createResource(() => props.thread, async (thread) => {
return thread.timelines.fetch("end", 50);
});
// updates every time there's new events
const [timeline, { mutate }] = createResource(() => timelineReal(), (t) => t, {
storage: (init) => createSignal(init, { equals: false }),
});
@ -43,15 +48,14 @@ export function ThreadView(props: VoidProps<{ thread: Thread }>) {
let oldTimeline: ThreadTimeline | undefined;
createEffect(() => {
oldTimeline?.off("timelineUpdate", refresh);
oldTimeline?.off("timelineAppend", refresh);
timeline()?.on("timelineUpdate", refresh);
timeline()?.on("timelineAppend", refresh);
oldTimeline = timeline();
setIsAtBeginning(timeline()?.isAtBeginning || false);
}, timeline);
timelineReal()?.on("timelineAppend", refresh);
oldTimeline = timelineReal();
setIsAtBeginning(timelineReal()?.isAtBeginning || false);
scrollEl.scrollBy(0, 999999);
// paginate();
}, timelineReal);
onCleanup(() => {
oldTimeline?.off("timelineUpdate", refresh);
oldTimeline?.off("timelineAppend", refresh);
});
@ -63,19 +67,21 @@ export function ThreadView(props: VoidProps<{ thread: Thread }>) {
if (scrollEl.scrollTop < PAGINATE_MARGIN && !isPaginating() && !isAtBeginning()) {
const scrollRefEl = scrollEl.querySelector(".message") as HTMLElement | undefined;
const oldOffsetTop = (scrollRefEl?.offsetTop || 0);
setIsPaginating(true);
await timeline()?.paginate("b", 10);
const newOffsetTop = scrollRefEl?.offsetTop || 0;
await timeline()?.paginate("b");
const oldOffsetTop = scrollRefEl?.offsetTop || 0;
refresh();
const newOffsetTop = scrollRefEl?.offsetTop || 0;
console.log("scroll diff ", scrollRefEl, newOffsetTop, oldOffsetTop);
console.log("element moved by px", newOffsetTop - oldOffsetTop);
console.log("isAutoscrolling?", isAutoscrolling);
scrollEl.scrollBy(0, newOffsetTop - oldOffsetTop);
// scrollEl.scrollTo(0, oldOffsetTop);
setIsPaginating(false);
if (timeline()?.isAtBeginning) setIsAtBeginning(true);
}
};
const handleScroll = debounce(paginate, 100);
const handleScroll = () => paginate();
const events = () => timeline()?.getEvents().filter(ev => ev.type === "m.message");
// const eventsLength = () => events()?.length || 0;
@ -119,26 +125,32 @@ export function ThreadView(props: VoidProps<{ thread: Thread }>) {
function ThreadInfo(props: VoidProps<{ thread: Thread, showHeader: boolean }>) {
const event = () => props.thread.baseEvent;
let headerEl: HTMLHeadingElement;
const [_globals, change] = useGlobals();
const [setup, setSetup] = createSignal(false);
const [stuck, setStuck] = createSignal(false);
// FIXME: this feels hacky, and has a flash of "unstuck" style + 1px offsets occasionally
onMount(() => {
const observer = new IntersectionObserver(([e]) => setStuck(!e.isIntersecting), {
const observer = new IntersectionObserver(([e]) => {
setStuck(!e.isIntersecting);
setTimeout(() => setSetup(true), 10);
}, {
threshold: 1,
root: headerEl.parentElement,
});
observer.observe(headerEl);
onCleanup(() => observer.unobserve(headerEl));
});
return (
<>
{props.showHeader && <div class="spacer"></div>}
<div class="thread-title" ref={headerEl!} classList={{ stuck: stuck() || !props.showHeader }}>
{props.showHeader && <div class="spacer">
<button onClick={() => change({ type: "focusThread", thread: null })}>close thread</button>
</div>}
<div class="thread-title" ref={headerEl!} classList={{ stuck: stuck() || !props.showHeader, setup: setup() }}>
<h1 class="real">
<TextBlock text={event().content.title} fallback="Untitled thread" />
<button onClick={() => change({ type: "focusThread", thread: null })}>close thread</button>
</h1>
<h1 class="fake" aria-hidden="true">
<TextBlock text={event().content.title} fallback="Untitled thread" />

144
src/_Editor.tsx Normal file
View file

@ -0,0 +1,144 @@
import { createEditor, EditorContent } from "tiptap-solid";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import History from "@tiptap/extension-history";
// import Mention from "@tiptap/extension-mention";
import Placeholder from "@tiptap/extension-placeholder";
import "./Editor.scss";
// import { Accessor, createEffect, createSignal, VoidProps } from "solid-js";
import { Extension, Mark, markInputRule, wrappingInputRule } from "@tiptap/core";
import * as tiptap from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { Node } from "@tiptap/pm/model";
import { Marked, marked, Token } from "marked";
import { Dropcursor } from "@tiptap/extension-dropcursor";
import { Gapcursor } from "@tiptap/extension-gapcursor";
import { HardBreak } from "@tiptap/extension-hard-break";
// function Suggestions(props: VoidProps<{ suggestions: Accessor<Array<string>> }>) {
// createEffect(() => console.log(props.suggestions()));
// return (
// <ul>
// {props.suggestions().map(i => <li>{i}</li>)}
// </ul>
// )
// }
const Markdown = Extension.create({
addKeyboardShortcuts() {
return {
"Mod-b": () => {
const { state } = this.editor.view;
const { selection } = state;
const { from, to } = selection;
this.editor.chain()
.insertContentAt({ from: to, to }, "**", { updateSelection: true })
.insertContentAt({ from, to: from }, "**", { updateSelection: true })
.setTextSelection({ from: from + 2, to: to + 2 })
.focus()
.run();
return true;
},
"Shift-Enter": () => {
const { state } = this.editor.view;
const { selection } = state;
const { from, to } = selection;
const { paragraph } = state.schema.nodes;
console.log(paragraph)
// this.editor.chain().deleteSelection().unsetCode(from, paragraph.create())
this.editor.chain().setNode("paragraph").run();
console.log("s+enter");
return false;
},
"Enter": () => {
console.log("enter");
return true;
},
}
},
addProseMirrorPlugins() {
return [new Plugin({
key: new PluginKey("markdown"),
props: {
decorations(state) {
const decorations: Array<Decoration> = [];
console.log(state.doc, state.doc.textContent)
state.doc.descendants((node, nodePos) => {
if (!node.isText) return;
let pos = nodePos - 1;
const walk = (ast: Token) => {
switch (ast.type) {
case "em": {
const end = pos + ast.raw.length + 1;
decorations.push(Decoration.inline(pos, pos + 2, { class: "syn" }));
decorations.push(Decoration.inline(pos + 2, end - 1, { style: "font-style: italic" }));
decorations.push(Decoration.inline(end - 1, end, { class: "syn" }));
break;
}
case "strong": {
const end = pos + ast.raw.length + 1;
decorations.push(Decoration.inline(pos, pos + 3, { class: "syn" }));
decorations.push(Decoration.inline(pos + 3, end - 2, { style: "font-weight: bold" }));
decorations.push(Decoration.inline(end - 2, end, { class: "syn" }));
break;
}
case "codespan": {
const end = pos + ast.raw.length + 1;
decorations.push(Decoration.inline(pos, pos + 2, { class: "syn" }));
decorations.push(Decoration.inline(pos + 2, end - 1, { style: "font-style: italic" }));
decorations.push(Decoration.inline(end - 1, end, { class: "syn" }));
break;
}
case "code": {
// console.log(ast)
// const end = pos + ast.raw.length + 1;
// decorations.push(Decoration.inline(pos, pos + 2, { class: "syn" }));
// decorations.push(Decoration.inline(pos + 2, end - 1, { style: "font-style: italic" }));
// decorations.push(Decoration.inline(end - 1, end, { class: "syn" }));
break;
}
case "paragraph": ast.tokens?.forEach(walk); return;
// default: console.log(ast);
}
pos += ast.raw.length;
};
// console.log(node, marked.lexer(node.text!))
marked.lexer(node.text!).forEach(walk);
});
return DecorationSet.create(state.doc, decorations);
}
}
})]
}
});
export function Editor() {
let editorEl;
const editor = createEditor({
content: "hello world *asdf*",
autofocus: true,
extensions: [
Document,
Paragraph,
Text,
History,
Gapcursor,
Dropcursor,
HardBreak,
Placeholder.configure({
placeholder: "write something nice...",
}),
Markdown,
]
});
return (
<div class="editor" ref={editorEl}>
<EditorContent editor={editor()} />
</div>
);
}