mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 19:07:06 +00:00
Add nostr-editor
This commit is contained in:
1119
package-lock.json
generated
1119
package-lock.json
generated
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;
|
||||
}
|
||||
|
||||
76
src/app/components/GroupCompose.svelte
Normal file
76
src/app/components/GroupCompose.svelte
Normal 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>
|
||||
10
src/app/components/GroupComposeBolt11.svelte
Normal file
10
src/app/components/GroupComposeBolt11.svelte
Normal 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>
|
||||
22
src/app/components/GroupComposeEvent.svelte
Normal file
22
src/app/components/GroupComposeEvent.svelte
Normal 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>
|
||||
13
src/app/components/GroupComposeImage.svelte
Normal file
13
src/app/components/GroupComposeImage.svelte
Normal 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>
|
||||
18
src/app/components/GroupComposeLink.svelte
Normal file
18
src/app/components/GroupComposeLink.svelte
Normal 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>
|
||||
|
||||
16
src/app/components/GroupComposeMention.svelte
Normal file
16
src/app/components/GroupComposeMention.svelte
Normal 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>
|
||||
13
src/app/components/GroupComposeVideo.svelte
Normal file
13
src/app/components/GroupComposeVideo.svelte
Normal 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>
|
||||
@@ -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`} />
|
||||
|
||||
3
src/assets/icons/Paperclip.svg
Normal file
3
src/assets/icons/Paperclip.svg
Normal 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 |
@@ -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>
|
||||
|
||||
96
src/lib/tiptap/LinkExtension.ts
Normal file
96
src/lib/tiptap/LinkExtension.ts
Normal 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
1
src/lib/tiptap/index.ts
Normal file
@@ -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