Improve timeline scrolling

This commit is contained in:
tezlm 2023-12-11 15:22:53 -08:00
parent 6e3ede18bf
commit 47d5c2fee2
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
12 changed files with 904 additions and 138 deletions

View file

@ -10,9 +10,24 @@
"dependencies": {
"@floating-ui/dom": "^1.5.3",
"@popperjs/core": "^2.11.8",
"@solid-primitives/scheduled": "^1.4.1",
"@tanstack/solid-virtual": "^3.0.1",
"@tanstack/virtual-core": "^3.0.1",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-document": "^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/suggestion": "^2.1.13",
"i18next": "^23.7.8",
"marked": "^11.0.1",
"nanoid": "^5.0.4",
"sdk": "link:../sdk-ts",
"solid-js": "^1.8.7"
"solid-js": "^1.8.7",
"tiptap-solid": "^0.1.8"
},
"devDependencies": {
"sass": "^1.69.5",

View file

@ -11,15 +11,60 @@ dependencies:
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
'@solid-primitives/scheduled':
specifier: ^1.4.1
version: 1.4.1(solid-js@1.8.7)
'@tanstack/solid-virtual':
specifier: ^3.0.1
version: 3.0.1(solid-js@1.8.7)
'@tanstack/virtual-core':
specifier: ^3.0.1
version: 3.0.1
'@tiptap/core':
specifier: ^2.1.13
version: 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-document':
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)
'@tiptap/extension-mention':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)(@tiptap/suggestion@2.1.13)
'@tiptap/extension-paragraph':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)
'@tiptap/extension-placeholder':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-text':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)
'@tiptap/pm':
specifier: ^2.1.13
version: 2.1.13
'@tiptap/suggestion':
specifier: ^2.1.13
version: 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
i18next:
specifier: ^23.7.8
version: 23.7.8
marked:
specifier: ^11.0.1
version: 11.0.1
nanoid:
specifier: ^5.0.4
version: 5.0.4
sdk:
specifier: link:../sdk-ts
version: link:../sdk-ts
solid-js:
specifier: ^1.8.7
version: 1.8.7
tiptap-solid:
specifier: ^0.1.8
version: 0.1.8(@tiptap/core@2.1.13)(@tiptap/extension-bubble-menu@2.1.13)(@tiptap/extension-floating-menu@2.1.13)(solid-js@1.8.7)
devDependencies:
sass:
@ -621,6 +666,34 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@remirror/core-constants@2.0.2:
resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==}
dev: false
/@remirror/core-helpers@3.0.0:
resolution: {integrity: sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==}
dependencies:
'@remirror/core-constants': 2.0.2
'@remirror/types': 1.0.1
'@types/object.omit': 3.0.3
'@types/object.pick': 1.3.4
'@types/throttle-debounce': 2.1.0
case-anything: 2.1.13
dash-get: 1.0.2
deepmerge: 4.3.1
fast-deep-equal: 3.1.3
make-error: 1.3.6
object.omit: 3.0.0
object.pick: 1.3.0
throttle-debounce: 3.0.1
dev: false
/@remirror/types@1.0.1:
resolution: {integrity: sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==}
dependencies:
type-fest: 2.19.0
dev: false
/@rollup/rollup-android-arm-eabi@4.7.0:
resolution: {integrity: sha512-rGku10pL1StFlFvXX5pEv88KdGW6DHUghsxyP/aRYb9eH+74jTGJ3U0S/rtlsQ4yYq1Hcc7AMkoJOb1xu29Fxw==}
cpu: [arm]
@ -725,6 +798,150 @@ packages:
dev: true
optional: true
/@solid-primitives/scheduled@1.4.1(solid-js@1.8.7):
resolution: {integrity: sha512-OLcNXwYpX7HUOEqNPcmR31dkyI1E2imkMDBRlqsGT0ZhJV1L2g0TEREpo4nm/kUhh8LVQzkfnxS+GONx9kh90A==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
solid-js: 1.8.7
dev: false
/@tanstack/solid-virtual@3.0.1(solid-js@1.8.7):
resolution: {integrity: sha512-DxP3GUBEDUNdCH50Q2RgRkaol3bAGpkMcJAdUIPWywEL37TkH/MC748nees0EXRylrC7RMP0zVNN3Z94WFBULA==}
peerDependencies:
solid-js: ^1.3.0
dependencies:
'@tanstack/virtual-core': 3.0.0
solid-js: 1.8.7
dev: false
/@tanstack/virtual-core@3.0.0:
resolution: {integrity: sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==}
dev: false
/@tanstack/virtual-core@3.0.1:
resolution: {integrity: sha512-By6TTR3u6rmAWRD7STXqI8WP9q1jYrqVCz88lNTgOf/cUm5cNF6Uj7dej/1+LUj42KMwFusyxGS908HlGBhE2Q==}
dev: false
/@tiptap/core@2.1.13(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-cMC8bgTN63dj1Mv82iDeeLl6sa9kY0Pug8LSalxVEptRmyFVsVxGgu2/6Y3T+9aCYScxfS06EkA8SdzFMAwYTQ==}
peerDependencies:
'@tiptap/pm': ^2.0.0
dependencies:
'@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:
'@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
tippy.js: 6.3.7
dev: false
/@tiptap/extension-document@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-wLwiTWsVmZTGIE5duTcHRmW4ulVxNW4nmgfpk95+mPn1iKyNGtrVhGWleLhBlTj+DWXDtcfNWZgqZkZNzhkqYQ==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 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:
'@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
tippy.js: 6.3.7
dev: false
/@tiptap/extension-history@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-1ouitThGTBUObqw250aDwGLMNESBH5PRXIGybsCFO1bktdmWtEw7m72WY41EuX2BH8iKJpcYPerl3HfY1vmCNw==}
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-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:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
'@tiptap/suggestion': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/pm': 2.1.13
'@tiptap/suggestion': 2.1.13(@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:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/extension-placeholder@2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13):
resolution: {integrity: sha512-vIY7y7UbqsrAW/y8bDE9eRenbQEU16kNHB5Wri8RU1YiUZpkPgdXP/pLqyjIIq95SwP/vdTIHjHoQ77VLRl1hA==}
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-text@2.1.13(@tiptap/core@2.1.13):
resolution: {integrity: sha512-zzsTTvu5U67a8WjImi6DrmpX2Q/onLSaj+LRWPh36A1Pz2WaxW5asZgaS+xWCnR+UrozlCALWa01r7uv69jq0w==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
dev: false
/@tiptap/pm@2.1.13:
resolution: {integrity: sha512-zNbA7muWsHuVg12GrTgN/j119rLePPq5M8dZgkKxUwdw8VmU3eUyBp1SihPEXJ2U0MGdZhNhFX7Y74g11u66sg==}
dependencies:
prosemirror-changeset: 2.2.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.5.2
prosemirror-dropcursor: 1.8.1
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.3.2
prosemirror-inputrules: 1.3.0
prosemirror-keymap: 1.2.2
prosemirror-markdown: 1.11.2
prosemirror-menu: 1.2.4
prosemirror-model: 1.19.3
prosemirror-schema-basic: 1.2.2
prosemirror-schema-list: 1.3.0
prosemirror-state: 1.4.3
prosemirror-tables: 1.3.5
prosemirror-trailing-node: 2.0.7(prosemirror-model@1.19.3)(prosemirror-state@1.4.3)(prosemirror-view@1.32.6)
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.6
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:
'@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
/@types/babel__core@7.20.5:
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
dependencies:
@ -754,6 +971,18 @@ packages:
'@babel/types': 7.23.5
dev: true
/@types/object.omit@3.0.3:
resolution: {integrity: sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==}
dev: false
/@types/object.pick@1.3.4:
resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==}
dev: false
/@types/throttle-debounce@2.1.0:
resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==}
dev: false
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@ -769,6 +998,10 @@ packages:
picomatch: 2.3.1
dev: true
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: false
/babel-plugin-jsx-dom-expressions@0.37.9(@babel/core@7.23.5):
resolution: {integrity: sha512-6w+zs2i14fVanj4e1hXCU5cp+x0U0LJ5jScknpMZZUteHhwFRGJflHMVJ+xAcW7ku41FYjr7DgtK9mnc2SXlJg==}
peerDependencies:
@ -818,6 +1051,11 @@ packages:
resolution: {integrity: sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==}
dev: true
/case-anything@2.1.13:
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
engines: {node: '>=12.13'}
dev: false
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@ -856,9 +1094,17 @@ packages:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: true
/crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
dev: false
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
/dash-get@1.0.2:
resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==}
dev: false
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@ -871,10 +1117,20 @@ packages:
ms: 2.1.2
dev: true
/deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
dev: false
/electron-to-chromium@1.4.609:
resolution: {integrity: sha512-ihiCP7PJmjoGNuLpl7TjNA8pCQWu09vGyjlPYw1Rqww4gvNuCcmvl+44G+2QyJ6S2K4o+wbTS++Xz0YN8Q9ERw==}
dev: true
/entities@3.0.1:
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
engines: {node: '>=0.12'}
dev: false
/esbuild@0.19.8:
resolution: {integrity: sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==}
engines: {node: '>=12'}
@ -915,6 +1171,15 @@ packages:
engines: {node: '>=0.8.0'}
dev: true
/escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
dev: false
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: false
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
@ -973,6 +1238,13 @@ packages:
binary-extensions: 2.2.0
dev: true
/is-extendable@1.0.1:
resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==}
engines: {node: '>=0.10.0'}
dependencies:
is-plain-object: 2.0.4
dev: false
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -990,11 +1262,23 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/is-plain-object@2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'}
dependencies:
isobject: 3.0.1
dev: false
/is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
dev: true
/isobject@3.0.1:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
@ -1011,12 +1295,43 @@ packages:
hasBin: true
dev: true
/linkify-it@4.0.1:
resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
dependencies:
uc.micro: 1.0.6
dev: false
/lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies:
yallist: 3.1.1
dev: true
/make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: false
/markdown-it@13.0.2:
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==}
hasBin: true
dependencies:
argparse: 2.0.1
entities: 3.0.1
linkify-it: 4.0.1
mdurl: 1.0.1
uc.micro: 1.0.6
dev: false
/marked@11.0.1:
resolution: {integrity: sha512-P4kDhFEMlvLePBPRwOcMOv6+lYUbhfbSxJFs3Jb4Qx7v6K7l+k8Dxh9CEGfRvK71tL+qIFz5y7Pe4uzt4+/A3A==}
engines: {node: '>= 18'}
hasBin: true
dev: false
/mdurl@1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
dev: false
/merge-anything@5.1.7:
resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
engines: {node: '>=12.13'}
@ -1034,6 +1349,12 @@ 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 +1364,24 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/object.omit@3.0.0:
resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==}
engines: {node: '>=0.10.0'}
dependencies:
is-extendable: 1.0.1
dev: false
/object.pick@1.3.0:
resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
engines: {node: '>=0.10.0'}
dependencies:
isobject: 3.0.1
dev: false
/orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
dev: false
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true
@ -1061,6 +1400,149 @@ packages:
source-map-js: 1.0.2
dev: true
/prosemirror-changeset@2.2.1:
resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==}
dependencies:
prosemirror-transform: 1.8.0
dev: false
/prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
dependencies:
prosemirror-state: 1.4.3
dev: false
/prosemirror-commands@1.5.2:
resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==}
dependencies:
prosemirror-model: 1.19.3
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/prosemirror-dropcursor@1.8.1:
resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==}
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.6
dev: false
/prosemirror-gapcursor@1.3.2:
resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
dependencies:
prosemirror-keymap: 1.2.2
prosemirror-model: 1.19.3
prosemirror-state: 1.4.3
prosemirror-view: 1.32.6
dev: false
/prosemirror-history@1.3.2:
resolution: {integrity: sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==}
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.6
rope-sequence: 1.3.4
dev: false
/prosemirror-inputrules@1.3.0:
resolution: {integrity: sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==}
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/prosemirror-keymap@1.2.2:
resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==}
dependencies:
prosemirror-state: 1.4.3
w3c-keyname: 2.2.8
dev: false
/prosemirror-markdown@1.11.2:
resolution: {integrity: sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ==}
dependencies:
markdown-it: 13.0.2
prosemirror-model: 1.19.3
dev: false
/prosemirror-menu@1.2.4:
resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==}
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.5.2
prosemirror-history: 1.3.2
prosemirror-state: 1.4.3
dev: false
/prosemirror-model@1.19.3:
resolution: {integrity: sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==}
dependencies:
orderedmap: 2.1.1
dev: false
/prosemirror-schema-basic@1.2.2:
resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==}
dependencies:
prosemirror-model: 1.19.3
dev: false
/prosemirror-schema-list@1.3.0:
resolution: {integrity: sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==}
dependencies:
prosemirror-model: 1.19.3
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/prosemirror-state@1.4.3:
resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
dependencies:
prosemirror-model: 1.19.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.6
dev: false
/prosemirror-tables@1.3.5:
resolution: {integrity: sha512-JSZ2cCNlApu/ObAhdPyotrjBe2cimniniTpz60YXzbL0kZ+47nEYk2LWbfKU2lKpBkUNquta2PjteoNi4YCluQ==}
dependencies:
prosemirror-keymap: 1.2.2
prosemirror-model: 1.19.3
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
prosemirror-view: 1.32.6
dev: false
/prosemirror-trailing-node@2.0.7(prosemirror-model@1.19.3)(prosemirror-state@1.4.3)(prosemirror-view@1.32.6):
resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==}
peerDependencies:
prosemirror-model: ^1.19.0
prosemirror-state: ^1.4.2
prosemirror-view: ^1.31.2
dependencies:
'@remirror/core-constants': 2.0.2
'@remirror/core-helpers': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.19.3
prosemirror-state: 1.4.3
prosemirror-view: 1.32.6
dev: false
/prosemirror-transform@1.8.0:
resolution: {integrity: sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==}
dependencies:
prosemirror-model: 1.19.3
dev: false
/prosemirror-view@1.32.6:
resolution: {integrity: sha512-26r5LvyDlPgUNVf7ZdNdGrMJnylwjJtUJTfDuYOANIVx9lqWD1WCBlGg283weYQGKUC64DXR25LeAmliB9CrFQ==}
dependencies:
prosemirror-model: 1.19.3
prosemirror-state: 1.4.3
prosemirror-transform: 1.8.0
dev: false
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@ -1093,6 +1575,10 @@ packages:
fsevents: 2.3.3
dev: true
/rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
dev: false
/sass@1.69.5:
resolution: {integrity: sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==}
engines: {node: '>=14.0.0'}
@ -1141,6 +1627,31 @@ packages:
has-flag: 3.0.0
dev: true
/throttle-debounce@3.0.1:
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
engines: {node: '>=10'}
dev: false
/tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
dependencies:
'@popperjs/core': 2.11.8
dev: false
/tiptap-solid@0.1.8(@tiptap/core@2.1.13)(@tiptap/extension-bubble-menu@2.1.13)(@tiptap/extension-floating-menu@2.1.13)(solid-js@1.8.7):
resolution: {integrity: sha512-gUFkp9CxNVqrUkXkra3PN2NQhJoQ+wch2aJSUHE0vDW/1FloPB8Rz+P4K7RUxS0D++R3dhn0cY2dpzcZN+VXTw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.125
'@tiptap/extension-bubble-menu': ^2.0.0-beta.42
'@tiptap/extension-floating-menu': ^2.0.0-beta.36
solid-js: ^1.1.6
dependencies:
'@tiptap/core': 2.1.13(@tiptap/pm@2.1.13)
'@tiptap/extension-bubble-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
'@tiptap/extension-floating-menu': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
solid-js: 1.8.7
dev: false
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@ -1153,12 +1664,21 @@ packages:
is-number: 7.0.0
dev: true
/type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
dev: false
/typescript@5.3.3:
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/uc.micro@1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
dev: false
/update-browserslist-db@1.0.13(browserslist@4.22.2):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true
@ -1240,6 +1760,10 @@ packages:
vite: 5.0.7(sass@1.69.5)
dev: true
/w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
dev: false
/yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true

View file

@ -195,7 +195,7 @@
}
& > .preview {
padding-bottom: 2rem;
// padding-bottom: 2rem;
}
& > footer {
@ -316,6 +316,7 @@
align-items: center;
padding: 0 8px;
gap: 8px;
border-top: solid var(--background-4) 1px;
& > textarea {
background: var(--background-1);
@ -338,6 +339,7 @@
height: 100%;
overflow-y: scroll;
scrollbar-color: var(--background-1) var(--background-3);
overflow-anchor: none;
& > .spacer {
margin-top: auto;

View file

@ -1,7 +1,8 @@
import { Match, Show, Switch, VoidProps, createSignal, onCleanup } from "solid-js";
import "./App.scss";
import { Client, Room } from "sdk";
import { Home, RoomView, ThreadView } from "./Main";
import { Home, RoomView } from "./Main";
import { ThreadView } from "./Thread";
import { Contextualizer, useGlobals } from "./Context";
function Wrapper() {
@ -63,7 +64,7 @@ function App(props: VoidProps<{ client: Client }>) {
<ThreadView thread={thread()!} />
</Match>
<Match when={!thread()}>
empty sidebar
<div style="padding: 4px">empty sidebar</div>
</Match>
</Switch>
</div>

View file

@ -21,3 +21,29 @@ export function Time(props: VoidProps<{} & ({ ts: number } | { date: Date })>) {
return <time datetime={date().toISOString()}>{format(date())}</time>;
}
// 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 (
// <>
// <span ref={contentEl!}>{props.children}</span>
// <Portal>
// <div ref={tipEl!} style="position:fixed;top:0;left:0">{props.tip}</div>
// </Portal>
// </>
// )
// }

View file

@ -70,6 +70,9 @@ export function Contextualizer(props: ParentProps) {
locale,
};
// FIXME: swap out per room state on focusRoom
// const perRoom = new Map();
function redux(change: Change) {
switch (change.type) {
case "focusRoom":

34
src/Editor.scss Normal file
View file

@ -0,0 +1,34 @@
.editor {
border: solid var(--background-3) 1px;
background: var(--background-1);
width: 100%;
& .tiptap:focus {
outline: solid #4af 1px;
}
& span[data-mention] {
background: #9ad5;
padding: 0 2px;
border-radius: 2px;
}
& p.is-editor-empty:first-child::before {
color: var(--foreground-1);
content: attr(data-placeholder);
float: left;
height: 0;
opacity: .3;
pointer-events: none;
}
& .syn {
color: #777;
}
& code {
font-family: "Iosevka Zesty", 'Courier New', Courier, monospace;
background: var(--background-3);
padding: 0 2px;
}
}

114
src/Editor.tsx Normal file
View file

@ -0,0 +1,114 @@
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>
);
}

View file

@ -11,7 +11,7 @@ export function RoomView(props: VoidProps<{ room: Room }>) {
<>
<Threads room={props.room} />
<div class="actions">
<TimelineActions room={props.room} />
<RoomActions room={props.room} />
</div>
</>
)
@ -25,7 +25,7 @@ function Threads(props: VoidProps<{ room: Room }>) {
return (
<div class="timeline threads">
<div class="items">
<ThreadsHeader room={props.room} />
<RoomHeader room={props.room} />
<Show when={!threadChunk.loading} fallback={"loading..."}>
{threadChunk()!.threads.map((thread: Thread) => <ThreadsItem thread={thread} />)}
</Show>
@ -34,116 +34,10 @@ function Threads(props: VoidProps<{ room: Room }>) {
);
}
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 (
<>
<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) {
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
const { name } = e.target.elements;
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: ""}] });
name.value = "";
}
@ -159,15 +53,7 @@ export function Home(props: VoidProps<{ client: Client }>) {
);
}
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 }>) {
function RoomHeader(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 (
@ -183,7 +69,7 @@ function ThreadsHeader(props: VoidProps<{ room: Room }>) {
);
}
function TimelineActions(props: VoidProps<{ room: Room }>) {
function RoomActions(props: VoidProps<{ room: Room }>) {
async function makeThread() {
const title = prompt("title");
const body = prompt("body");

View file

@ -1,23 +1,27 @@
import { Thread } from "sdk";
import { Show, VoidProps, createResource, createSignal, onCleanup } from "solid-js";
import { Show, VoidProps, createEffect, 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 [preview, { mutate }] = createResource(props.thread, async (t) => {
await t.timeline.paginate("f", 1000);
return t.timeline.events;
const [preview, { mutate }] = createResource(props.thread, async (thread) => {
return thread.room.timelines.forThread(thread, "start");
}, {
storage: (init) => createSignal(init, { equals: false }),
});
const refresh = () => mutate(props.thread.timeline.events);
const refresh = () => {
mutate(preview());
setThread(thread());
};
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 - Math.min(preview()!.length, 10));
const [thread, setThread] = createSignal(props.thread, { equals: false });
const remaining = () => preview() && (thread().messageCount - Math.min(preview()!.getEvents().length, 10));
const willOpenThread = (target: "default" | "latest") => (e: MouseEvent) => {
// TODO: target
@ -26,7 +30,7 @@ export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
type: "focusThread",
thread: isSame ? null : props.thread,
});
console.log("open thread", target);
// console.log("open thread", target);
e.stopPropagation();
};
@ -39,15 +43,15 @@ export function ThreadsItem(props: VoidProps<{ thread: Thread }>) {
<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} />
<Text count={thread().messageCount}>message.count</Text> &bull; <Time ts={thread().latestEvent.originTs} />
</div>
</header>
<div class="preview">
<Show when={!preview.loading}>
{preview()?.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, 10).map((ev, idx) => <Message event={ev} title={idx === 0} />)}
</Show>
</div>
<Show when={remaining()}>
<Show when={remaining() && false}>
<footer>
<Text count={remaining()}>message.remaining</Text>
</footer>
@ -82,7 +86,7 @@ export function Message(props: VoidProps<{ event: any, title?: boolean }>) {
}
return (
<div class="message" classList={{ title: title(), [compact() ? "compact" : "cozy"]: true }}>
<div class="message" classList={{ title: title(), [compact() ? "compact" : "cozy"]: true }} data-event-id={props.event.id}>
{title() && !compact() && <div class="avatar"></div>}
{title() && compact() && <div class="name">{props.event.sender}</div>}
<div class="content">

159
src/Thread.tsx Normal file
View file

@ -0,0 +1,159 @@
import { For, ParentProps, Show, VoidProps, createEffect, createResource, createSignal, onCleanup, onMount } from "solid-js";
import "./App.scss";
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";
import { ThreadTimeline } from "sdk/dist/src/timeline";
import { debounce } from "@solid-primitives/scheduled";
import { Editor } from "./Editor";
// import { createVirtualizer } from "@tanstack/solid-virtual";
export function ThreadView(props: VoidProps<{ thread: Thread }>) {
let textareaEl: HTMLTextAreaElement;
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, async (thread) => {
return thread.room.timelines.forThread(thread, "end");
}, {
storage: (init) => createSignal(init, { equals: false }),
});
let scrollEl: HTMLDivElement;
let isAutoscrolling = false;
const [isPaginating, setIsPaginating] = createSignal(false);
const [isAtBeginning, setIsAtBeginning] = createSignal(false);
const refresh = () => {
mutate(timeline());
if (isAutoscrolling) scrollEl.scrollBy(0, 999999);
};
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);
onCleanup(() => {
oldTimeline?.off("timelineUpdate", refresh);
oldTimeline?.off("timelineAppend", refresh);
});
const AUTOSCROLL_MARGIN = 5;
const PAGINATE_MARGIN = 500;
const paginate = async () => {
isAutoscrolling = scrollEl.scrollTop + scrollEl.offsetHeight > scrollEl.scrollHeight - AUTOSCROLL_MARGIN;
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;
refresh();
scrollEl.scrollBy(0, newOffsetTop - oldOffsetTop);
// scrollEl.scrollTo(0, oldOffsetTop);
setIsPaginating(false);
if (timeline()?.isAtBeginning) setIsAtBeginning(true);
}
};
const handleScroll = debounce(paginate, 100);
const events = () => timeline()?.getEvents().filter(ev => ev.type === "m.message");
// const eventsLength = () => events()?.length || 0;
// const virt = createVirtualizer({
// count: 100,
// estimateSize: () => 50,
// getScrollElement: () => scrollEl,
// });
// const items = () => virt.getVirtualItems();
// <Show when={timeline() && !timeline.loading}>
// {items().map(it => {
// return <div style={`position:fixed;translate:0 ${it.start}px`}>
// {it.index}
// </div>;
// })}
// </Show>
return (
<div class="thread-view">
<div class="scroll" onScroll={handleScroll} ref={scrollEl!}>
<ThreadInfo thread={props.thread} showHeader={isAtBeginning()} />
{/* FIXME: loading placeholders */}
{false && (timeline.loading || isPaginating()) && new Array(5).fill(0).map(() => <div style="min-height:1rem;margin:8px;background:var(--background-3)"></div>)}
{!isAtBeginning() && <div style={`min-height:${PAGINATE_MARGIN}px`}></div>}
<For each={events()}>
{(ev, idx) => <Message event={ev} title={idx() === 0 && false} />}
</For>
<div class="spacer-bottom"></div>
</div>
<div class="input">
<button onClick={handleScroll}>more</button>
<Editor />
<textarea placeholder="write something nice..." ref={textareaEl!} onKeyDown={handleKeyDown}></textarea>
</div>
</div>
);
}
function ThreadInfo(props: VoidProps<{ thread: Thread, showHeader: boolean }>) {
const event = () => props.thread.baseEvent;
let headerEl: HTMLHeadingElement;
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), {
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 }}>
<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>
<Show when={props.showHeader}>
<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} />
</Show>
<Show when={!props.showHeader}>
<div style="flex:1;"></div>
</Show>
</>
);
}

View file

@ -18,8 +18,6 @@
/* Linting */
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],