forked from coracle/flotilla
Add nostr-editor
This commit is contained in:
Generated
+1111
-8
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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`} />
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@lib/tiptap/LinkExtension'
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user