Add nostr-editor

This commit is contained in:
Jon Staab
2024-08-19 14:22:16 -07:00
parent 4d7c880576
commit d03ef264f7
17 changed files with 1396 additions and 13 deletions

1119
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,7 @@
"@noble/curves": "^1.5.0",
"@noble/hashes": "^1.4.0",
"@poppanator/sveltekit-svg": "^4.2.1",
"@tiptap/starter-kit": "^2.6.4",
"@types/throttle-debounce": "^5.0.2",
"@welshman/lib": "^0.0.14",
"@welshman/net": "^0.0.18",
@@ -46,9 +47,11 @@
"daisyui": "^4.12.10",
"fuse.js": "^7.0.0",
"idb": "^8.0.0",
"nostr-editor": "^0.0.1",
"nostr-tools": "^2.7.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte-bricks": "^0.2.1",
"svelte-tiptap": "^1.1.3",
"throttle-debounce": "^5.0.2"
}
}

View File

@@ -83,3 +83,11 @@
.shadow-top-xl {
@apply shadow-[0_20px_25px_-5px_rgb(0,0,0,0.1)_0_8px_10px_-6px_rgb(0,0,0,0.1)];
}
.tiptap {
@apply rounded-box bg-base-100 px-4 p-2;
}
.link-content {
@apply text-sm rounded px-1 bg-neutral text-neutral-content no-underline;
}

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import {onMount} from 'svelte'
import type {Readable} from 'svelte/store'
import {createEditor, type Editor, EditorContent, SvelteNodeViewRenderer} from 'svelte-tiptap'
import StarterKit from '@tiptap/starter-kit'
import {NostrExtension} from 'nostr-editor'
import type {StampedEvent} from '@welshman/util'
import {LinkExtension} from '@lib/tiptap'
import GroupComposeMention from '@app/components/GroupComposeMention.svelte'
import GroupComposeEvent from '@app/components/GroupComposeEvent.svelte'
import GroupComposeImage from '@app/components/GroupComposeImage.svelte'
import GroupComposeBolt11 from '@app/components/GroupComposeBolt11.svelte'
import GroupComposeVideo from '@app/components/GroupComposeVideo.svelte'
import GroupComposeLink from '@app/components/GroupComposeLink.svelte'
import {signer} from '@app/base'
let editor: Readable<Editor>
const asInline = (extend: Record<string, any>) =>
({inline: true, group: 'inline', draggable: false, ...extend})
onMount(() => {
editor = createEditor({
extensions: [
StarterKit.configure({
blockquote: false,
bold: false,
bulletList: false,
heading: false,
horizontalRule: false,
italic: false,
listItem: false,
orderedList: false,
strike: false,
}),
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(GroupComposeLink),
}),
NostrExtension.configure({
extend: {
bolt11: asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)}),
nprofile: asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeMention)}),
nevent: asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)}),
naddr: asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)}),
image: asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeImage)}),
video: asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)}),
},
link: false,
tweet: false,
youtube: false,
video: {defaultUploadUrl: 'https://nostr.build', defaultUploadType: 'nip96'},
image: {defaultUploadUrl: 'https://nostr.build', defaultUploadType: 'nip96'},
fileUpload: {
immediateUpload: false,
sign: async (event: StampedEvent) => $signer!.sign(event),
onDrop() {
// setPending(true)
},
onComplete(currentEditor: Editor) {
console.log('Upload Completed', currentEditor.getText())
// setPending(false)
},
},
}),
],
content: '',
onUpdate: () => {
// console.log('update', $editor.getJSON(), $editor.getText())
},
})
})
</script>
<div class="relative z-feature border-t border-solid border-base-100 p-2 shadow-top-xl">
<EditorContent editor={$editor} />
</div>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
export let node: NodeViewProps['node']
</script>
<NodeViewWrapper class="inline link-content">
{node.attrs.lnbc.slice(0, 16)}...
</NodeViewWrapper>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import {ellipsize} from '@welshman/lib'
import {type TrustedEvent, fromNostrURI, Address} from '@welshman/util'
import Link from '@lib/components/Link.svelte'
import {deriveEvent} from '@app/state'
export let node: NodeViewProps['node']
const displayEvent = (e: TrustedEvent) =>
e?.content.length > 1 ? ellipsize(e.content, 50) : fromNostrURI(nevent || naddr).slice(0, 16) + '...'
$: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs)
$: event = deriveEvent(id || new Address(kind, pubkey, identifier).toString(), relays)
</script>
<NodeViewWrapper class="inline">
<Link external href="https://njump.me/{node.attrs.nevent}" class="link-content">
{displayEvent($event)}
</Link>
</NodeViewWrapper>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import cx from 'classnames'
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import Icon from '@lib/components/Icon.svelte'
export let node: NodeViewProps['node']
</script>
<NodeViewWrapper class="inline link-content">
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
{node.attrs.file.name}
</NodeViewWrapper>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import cx from 'classnames'
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import {stripProtocol} from '@welshman/lib'
import Icon from '@lib/components/Icon.svelte'
import Link from '@lib/components/Link.svelte'
export let node: NodeViewProps['node']
</script>
<NodeViewWrapper class="inline">
<Link external href={node.attrs.url} class="link-content">
<Icon icon="link-round" size={3} class="inline-block translate-y-px" />
{stripProtocol(node.attrs.url)}
</Link>
</NodeViewWrapper>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import cx from 'classnames'
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import {displayProfile} from '@welshman/util'
import Link from '@lib/components/Link.svelte'
import {deriveProfile} from '@app/state'
export let node: NodeViewProps['node']
$: profile = deriveProfile(node.attrs.pubkey, node.attrs.relays)
</script>
<NodeViewWrapper class="inline">
<Link external href="https://njump.me/{node.attrs.nprofile}">@{displayProfile($profile)}</Link>
</NodeViewWrapper>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import cx from 'classnames'
import type {NodeViewProps} from '@tiptap/core'
import {NodeViewWrapper} from 'svelte-tiptap'
import Icon from '@lib/components/Icon.svelte'
export let node: NodeViewProps['node']
</script>
<NodeViewWrapper class="inline link-content">
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
{node.attrs.file.name}
</NodeViewWrapper>

View File

@@ -37,7 +37,7 @@
}
</script>
<div class="relative w-14 bg-base-100" bind:this={element}>
<div class="relative w-14 bg-base-100 flex-shrink-0" bind:this={element}>
<div
class="absolute z-nav-active ml-2 h-[144px] w-12 bg-base-300"
style={`top: ${$activeOffset}px`} />

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.9175 17.8068L15.8084 10.2535C16.7558 9.34668 16.7558 7.87637 15.8084 6.96951C14.861 6.06265 13.325 6.06265 12.3776 6.96951L4.54387 14.4681C2.74382 16.1911 2.74382 18.9847 4.54387 20.7077C6.34391 22.4308 9.26237 22.4308 11.0624 20.7077L19.0105 13.0997C21.6632 10.5605 21.6632 6.44362 19.0105 3.90441C16.3578 1.3652 12.0569 1.3652 9.40419 3.90441L3 10.0346" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -33,6 +33,7 @@
import Magnifer from "@assets/icons/Magnifer.svg?dataurl"
import MenuDots from "@assets/icons/Menu Dots.svg?dataurl"
import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl"
import Paperclip from "@assets/icons/Paperclip.svg?dataurl"
import Plain from "@assets/icons/Plain.svg?dataurl"
import RemoteControllerMinimalistic from "@assets/icons/Remote Controller Minimalistic.svg?dataurl"
import Reply from "@assets/icons/Reply.svg?dataurl"
@@ -77,6 +78,7 @@
magnifer: Magnifer,
"menu-dots": MenuDots,
"pallete-2": Pallete2,
"paperclip": Paperclip,
plain: Plain,
reply: Reply,
"remote-controller-minimalistic": RemoteControllerMinimalistic,

View File

@@ -1,3 +1,3 @@
<div class="flex w-60 flex-col gap-1 bg-base-300">
<div class="flex w-60 flex-col gap-1 bg-base-300 flex-shrink-0">
<slot />
</div>

View File

@@ -0,0 +1,96 @@
import { Node, nodePasteRule, type PasteRuleMatch } from '@tiptap/core'
import type { Node as ProsemirrorNode } from '@tiptap/pm/model'
import type { MarkdownSerializerState } from 'prosemirror-markdown'
export const LINK_REGEX = /^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]*/gi
export const createPasteRuleMatch = <T extends Record<string, unknown>>(
match: RegExpMatchArray,
data: T,
): PasteRuleMatch => ({ index: match.index!, replaceWith: match[2], text: match[0], match, data })
export interface LinkAttributes {
url: string
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
link: {
insertLink: (options: { url: string }) => ReturnType
}
}
}
export const LinkExtension = Node.create({
name: 'link',
group: 'inline',
atom: true,
inline: true,
priority: 1000,
addAttributes() {
return {
url: { default: null },
}
},
renderHTML(props) {
return ['div', { 'data-url': props.node.attrs.url }]
},
renderText(props) {
return props.node.attrs.url
},
addStorage() {
return {
markdown: {
serialize(state: MarkdownSerializerState, node: ProsemirrorNode) {
state.write(node.attrs.url)
},
parse: {},
},
}
},
addCommands() {
return {
insertLink:
({ url }) =>
({ commands }) => {
return commands.insertContent(
{ type: this.name, attrs: { url } },
{
updateSelection: false,
},
)
},
}
},
addPasteRules() {
return [
nodePasteRule({
type: this.type,
getAttributes: (match) => match.data,
find: (text) => {
const matches = []
for (const match of text.matchAll(LINK_REGEX)) {
try {
matches.push(createPasteRuleMatch(match, { url: match[0] }))
} catch (e) {
continue
}
}
return matches
},
}),
]
},
})

1
src/lib/tiptap/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from '@lib/tiptap/LinkExtension'

View File

@@ -15,6 +15,7 @@
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import GroupNote from "@app/components/GroupNote.svelte"
import GroupCompose from "@app/components/GroupCompose.svelte"
import {deriveGroupConversation} from "@app/state"
const {nom} = $page.params
@@ -89,7 +90,5 @@
</Spinner>
</p>
</div>
<div class="relative z-feature border-t border-solid border-base-100 px-2 py-2">
<div class="shadow-top-xl flex min-h-12 items-center gap-4 rounded-xl bg-base-100 px-4"></div>
</div>
<GroupCompose />
</div>