nostr-editor-coracle-workaround
v0.0.4-pre.18
Published
[](https://github.com/cesardeazevedo/nostr-editor/actions/workflows/ci-checks.yml)
Readme
nostr-editor
nostr-editor is a collection of Tiptap extensions designed to enhance the user experience when creating and editing nostr notes. It also provides tools for parsing existing notes into a structured content schema.
What is tiptap?
Tiptap is a headless wrapper around ProseMirror, offering a more developer-friendly API for building rich text editors. nostr-editor uses Tiptap to simplify integration with frameworks like React and Svelte, making it easy to create customized nostr-compatible editors.
What is prosemirror?
ProseMirror is the underlying core framework that powers Tiptap and other WYSIWYG (what-you-see-is-what-you-get) editors.
Features
- Fully customizable extensions
- Parse existing nostr events, including
imetatags (NIP-94) - Automatically convert nostr links to their appropriate nodes during paste operations (
nostr:nevent1,nostr:nprofile1,nostr:naddr,nostr:npub,nostr:note1) - Handle file uploads to a NIP-96 or blossom compatible server
- Supports markdown long-form content
- Supports bolt11 invoices
- Supports youtube and tweet links
- Automatically rejects and alerts if the user mistakenly pastes an
nsec1key.
Demo
https://cesardeazevedo.github.io/nostr-editor/
- React: source-code
- Svelte (WIP): source-code
Installing
To use nostr-editor, you'll need to install a few dependencies:
pnpm add nostr-editor @tiptap/starter-kit @tiptap/core tiptap-markdownreact dependencies
pnpm add @tiptap/reactsvelte dependencies
pnpm add svelte-tiptapUsage
React
Here's a basic setup example using React:
import { Editor } from '@tiptap/core'
import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
function MyEditor() {
const editor = useEditor({
autofocus: true,
extensions: [
StarterKit,
NostrExtension.configure({
extend: {
nprofile: { addNodeView: () => ReactNodeViewRenderer(MyReactMentionComponent) },
nevent: { addNodeView: () => ReactNodeViewRenderer(MyReactNEventComponent) },
naddr: { addNodeView: () => ReactNodeViewRenderer(MyReactNaddrComponent) },
image: { addNodeView: () => ReactNodeViewRenderer(MyReactImageComponent) },
video: { addNodeView: () => ReactNodeViewRenderer(MyReactVideoComponent) },
tweet: { addNodeView: () => ReactNodeViewRenderer(MyReactTweetComponent) },
},
link: { autolink: true }
}),
],
onUpdate: () => {
const contentSchema = editor.getJSON()
const contentText = editor.getText()
},
})
return (
<EditorContent editor={editor} />
)
}svelte
<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'
let editor: Readable<Editor>
onMount(() => {
editor = new Editor({
extensions: [
StarterKit,
NostrExtension.configure({
extend: {
nprofile: { addNodeView: () => SvelteNodeViewRenderer(MySvelteMentionComponent) },
nevent: { addNodeView: () => SvelteNodeViewRenderer(MySvelteNEventComponent) },
naddr: { addNodeView: () => SvelteNodeViewRenderer(MySvelteNaddrComponent) },
image: { addNodeView: () => SvelteNodeViewRenderer(MySvelteImageComponent) },
video: { addNodeView: () => SvelteNodeViewRenderer(MySvelteVideoComponent) },
tweet: { addNodeView: () => SvelteNodeViewRenderer(MySvelteTweetComponent) },
},
}),
],
content: '',
onUpdate: () => {
contentSchema = $editor.getJSON()
contentText = $editor.getText()
},
})
})
</script>
<main>
<EditorContent editor={$editor} />
</main>Rendering node views
nostr-editor is framework-agnostic and does not ship with pre-built components (yet). You should provide your own React or Svelte components for each extension.
NostrExtension.configure({
extend: {
nprofile: { addNodeView: () => ReactNodeViewRenderer(MyReactMentionComponent) },
...
},
}),import type { NodeViewProps } from '@tiptap/core'
import { NodeViewWrapper } from '@tiptap/react'
export function MyReactMentionComponent(props: NodeViewProps) {
const { pubkey, relays } = props.node.attrs
const { getProfile } = useNDK() // nostr-tools or other nostr client library
return (
<NodeViewWrapper as='span'>
@{getProfile(pubkey).display_name}
</NodeViewWrapper>
)
}Image Upload
To handle image uploads with nostr-editor, you can configure the extension as follows:
NostrExtension.configure({
fileUpload: {
uploadFile: (attrs: FileAttributes) => {
// If something went wrong, return an error string
if (error) {
return {error}
}
// Upload attrs.file and return an UploadTask
return {
result: {
url, // The file url
sha256, // The file hash
tags, // Additional nostr tags to be added to imeta. Refer to NIP 94 for recommended tags.
// url, x, ox, m, and size are automatically inferred but can be overridden
},
}
},
immediateUpload: true, // It will automatically upload when a file is added to the editor, if false, call `editor.commands.uploadFiles()` manually
sign: async (event) => {
if ('nostr' in window) {
const nostr = window.nostr as NostrExtension
return await nostr.signEvent(event)
}
},
onDrop() {
// File added to the editor
},
onComplete() {
// All files were successfully uploaded
},
},
}),Trigger a input type='file' popup
Parsing existing notes
You can set the editor an existing nostr event in order to parse it's contents
const event = {
kind: 1,
content: 'Hello nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcprfmhxue69uhhyetvv9ujuem9w3skccne9e3k7mf0wccsqgxxvqas78x0a339m8qgkaf7fam5atmarne8dy3rzfd4l4x6w2qpncmfs8zh'
...
}
editor.commands.setEventContent(event)
editor.getJSON()Response
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Hello " },
{
"type": "nprofile",
"attrs": {
"nprofile": "nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcprfmhxue69uhhyetvv9ujuem9w3skccne9e3k7mf0wccsqgxxvqas78x0a339m8qgkaf7fam5atmarne8dy3rzfd4l4x6w2qpncmfs8zh",
"pubkey": "c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e",
"relays": ["wss://nos.lol/", "wss://relay.damus.io/", "wss://relay.getalby.com/v1"]
}
}
]
}
]
}Parsing existing long-form content notes
The same thing as a normal note, just make sure your added the Markdown extension from tiptap-markdown
import { Markdown } from 'tiptap-markdown'
const editor = useEditor({
autofocus: true,
extensions: [
StarterKit,
Markdown.configure({
transformCopiedText: true,
transformPastedText: true,
}),
NostrExtension.configure({
link: { autolink: true }, // needed for markdown links
}),
],
})Commands
nostr-editor provides several commands to insert various types of content and manage media uploads.
insertNevent
editor.commands.insertNEvent({ bech32: 'nostr:nevent1...' })insertNprofile
editor.commands.insertNProfile({ bech32: 'nostr:nprofile1...' })insertNAddr
editor.commands.insertNAddr({ bech32 'nostr:naddr1...' })insertBolt11
editor.commands.insertBolt11({ lnbc: 'lnbc...' })selectFiles
Triggers a input type='file' click
editor.commands.selectFiles()uploadFiles
Upload all pending images and videos,
editor.commands.uploadFiles()This command returns true when the upload starts, not when the upload is completed. You can use onComplete() callback in the fileUpload extension options.
const editor = useEditor({
extensions: [
NostrExtension.configure({
fileUpload: {
onComplete: () => console.log('All files uploaded'),
},
}),
],
})Note: all nostr: prefixes are optional
Roadmap
References
- tiptap docs
- prosemirror docs
- tiptap-markdown
- svelte-tiptap
- nip19 - bech32-encoded entities
- nip94 - File Integration
- nip96 - HTTP File Storage Integration
- blossom
