Move editor stuff to its own folder

This commit is contained in:
Jon Staab
2024-09-23 13:58:01 -07:00
parent 26eb4faf37
commit ad4944d512
28 changed files with 312 additions and 397 deletions
+9 -8
View File
@@ -43,7 +43,7 @@
}
.card2 {
@apply rounded-box bg-base-100 p-6 text-base-content overflow-hidden text-ellipsis;
@apply overflow-hidden text-ellipsis rounded-box bg-base-100 p-6 text-base-content;
}
.card2.card2-sm {
@@ -96,11 +96,12 @@
/* tiptap */
.input-editor, .chat-editor, .note-editor {
@apply p-1 -m-1 min-h-12;
.input-editor,
.chat-editor,
.note-editor {
@apply -m-1 min-h-12 p-1;
}
.tiptap[contenteditable="true"] {
@apply max-h-[350px] overflow-y-auto p-2 px-4;
}
@@ -110,11 +111,11 @@
}
.input-editor .tiptap[contenteditable="true"] {
@apply input input-bordered p-[.65rem] h-auto;
@apply input input-bordered h-auto p-[.65rem];
}
.note-editor .tiptap[contenteditable="true"] {
@apply input input-bordered p-[.65rem] h-auto min-h-32 pb-6;
@apply input input-bordered h-auto min-h-32 p-[.65rem] pb-6;
}
.tiptap pre code {
@@ -136,8 +137,8 @@
/* date input */
.date-time-field {
@apply input input-bordered px-0 rounded;
@apply input input-bordered rounded px-0;
}
.date-time-field input {
@apply !bg-inherit !border-none !text-inherit !w-full !h-full;
@apply !h-full !w-full !border-none !bg-inherit !text-inherit;
}
-1
View File
@@ -107,4 +107,3 @@ export const removeSpaceMembership = (url: string) =>
export const removeRoomMembership = (url: string, room: string) =>
updateList(MEMBERSHIPS, (tags: string[][]) => tags.filter(t => !equals([ROOM, room, url], t)))
+9 -19
View File
@@ -3,35 +3,25 @@
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {NProfileExtension, ImageExtension} from "nostr-editor"
import {createEvent} from "@welshman/util"
import {publishThunk, makeThunk} from "@welshman/app"
import {findNodes} from "@lib/tiptap"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {makeMention, makeIMeta} from "@app/commands"
import {getChatEditorOptions, addFile} from "@app/editor"
import {getEditorOptions, getEditorTags, addFile} from "@lib/editor"
import {ROOM, MESSAGE, GENERAL} from "@app/state"
import {getPubkeyHints} from "@app/commands"
export let url
export let room = GENERAL
const uploading = writable(false)
const loading = writable(false)
let editor: Readable<Editor>
const sendMessage = () => {
const json = $editor.getJSON()
const mentionTags = findNodes(NProfileExtension.name, json).map(m =>
makeMention(m.attrs!.pubkey, m.attrs!.relays),
)
const imetaTags = findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) =>
makeIMeta(src, {x, ox: x}),
)
const submit = () => {
const event = createEvent(MESSAGE, {
content: $editor.getText(),
tags: [[ROOM, room], ...mentionTags, ...imetaTags],
tags: [[ROOM, room], ...getEditorTags($editor)],
})
publishThunk(makeThunk({event, relays: [url]}))
@@ -40,7 +30,7 @@
}
onMount(() => {
editor = createEditor(getChatEditorOptions({uploading, sendMessage}))
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints, submitOnEnter: true}))
})
</script>
@@ -48,15 +38,15 @@
class="shadow-top-xl relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100 p-2">
<Button
data-tip="Add an image"
class="center h-10 w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200 tooltip"
class="center tooltip h-10 w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
on:click={() => addFile($editor)}>
{#if $uploading}
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon="gallery-send" />
{/if}
</Button>
<div class="flex-grow overflow-hidden chat-editor">
<div class="chat-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
</div>
</div>
+3 -8
View File
@@ -15,19 +15,14 @@
formatTimestampAsTime,
} from "@welshman/app"
import type {PublishStatusData} from "@welshman/app"
import {
REACTION,
ZAP_RESPONSE,
displayRelayUrl,
getAncestorTags,
} from "@welshman/util"
import {REACTION, ZAP_RESPONSE, displayRelayUrl, getAncestorTags} from "@welshman/util"
import {repository} from "@welshman/app"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import {REPLY, deriveEvent, displayReaction} from "@app/state"
import {getChatViewOptions} from "@app/editor"
import {getViewOptions} from "@lib/editor"
export let event: TrustedEvent
export let showPubkey: boolean
@@ -79,7 +74,7 @@
!isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
onMount(() => {
editor = createEditor(getChatViewOptions(event.content))
editor = createEditor(getViewOptions(event))
})
</script>
+3 -8
View File
@@ -1,17 +1,12 @@
<script lang="ts">
import cx from "classnames"
import {fromPairs} from "@welshman/lib"
import {Tags, getAddress} from "@welshman/util"
import {repository, pubkey, secondsToDate, getLocale, formatTimestamp, formatTimestampAsDate, deriveProfileDisplay} from "@welshman/app"
import {getAddress} from "@welshman/util"
import {secondsToDate, getLocale, formatTimestamp, formatTimestampAsDate} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
export let event
const address = getAddress(event)
const timeFmt = new Intl.DateTimeFormat(getLocale(), {timeStyle: "short"})
const datetimeFmt = new Intl.DateTimeFormat(getLocale(), {dateStyle: "short", timeStyle: "short"})
const profileDisplay = deriveProfileDisplay(event.pubkey)
$: meta = fromPairs(event.tags) as Record<string, string>
$: end = parseInt(meta.end)
@@ -23,7 +18,7 @@
$: isSingleDay = startDateDisplay === endDateDisplay
</script>
<div class="card2 flex justify-between items-center gap-2">
<div class="card2 flex items-center justify-between gap-2">
<span>{meta.title || meta.name}</span>
<div class="flex items-center gap-2 text-sm">
<Icon icon="clock-circle" size={4} />
+14 -32
View File
@@ -3,29 +3,27 @@
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {NProfileExtension, ImageExtension} from "nostr-editor"
import {randomId} from "@welshman/lib"
import {createEvent, EVENT_DATE, EVENT_TIME} from "@welshman/util"
import {publishThunk, makeThunk, dateToSeconds} from "@welshman/app"
import {findNodes} from "@lib/tiptap"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import {makeMention, makeIMeta} from "@app/commands"
import {getNoteEditorOptions, addFile, uploadFiles} from "@app/editor"
import {pushModal, clearModal} from "@app/modal"
import {getPubkeyHints} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
import {clearModal} from "@app/modal"
import {pushToast} from "@app/toast"
export let url
const submit = () => uploadFiles($editor)
const startSubmit = () => uploadFiles($editor)
const back = () => history.back()
const uploading = writable(false)
const loading = writable(false)
const sendMessage = () => {
const submit = () => {
if (!title) {
return pushToast({
theme: "error",
@@ -40,15 +38,7 @@
})
}
const json = $editor.getJSON()
const kind = isAllDay ? EVENT_DATE : EVENT_TIME
const mentionTags = findNodes(NProfileExtension.name, json).map(m =>
makeMention(m.attrs!.pubkey, m.attrs!.relays),
)
const imetaTags = findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) =>
makeIMeta(src, {x, ox: x}),
)
const event = createEvent(kind, {
content: $editor.getText(),
tags: [
@@ -57,8 +47,7 @@
["location", location],
["start", dateToSeconds(start).toString()],
["end", dateToSeconds(end).toString()],
...mentionTags,
...imetaTags,
...getEditorTags($editor),
],
})
@@ -67,19 +56,18 @@
}
let editor: Readable<Editor>
let isAllDay = false
let file: File
const isAllDay = false
let title = ""
let location = ""
let start: Date
let end: Date
onMount(() => {
editor = createEditor(getNoteEditorOptions({uploading, sendMessage}))
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints}))
})
</script>
<form class="column gap-4" on:submit|preventDefault={submit}>
<form class="column gap-4" on:submit|preventDefault={startSubmit}>
<div class="py-2">
<h1 class="heading">Create an Event</h1>
<p class="text-center">Invite other group members to events online or in real life.</p>
@@ -95,14 +83,11 @@
<div
slot="input"
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
<div class="flex-grow overflow-hidden input-editor">
<div class="input-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
</div>
<Button
data-tip="Add an image"
class="btn center tooltip"
on:click={() => addFile($editor)}>
{#if $uploading}
<Button data-tip="Add an image" class="center btn tooltip" on:click={() => addFile($editor)}>
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon="gallery-send" />
@@ -134,9 +119,6 @@
<Icon icon="alt-arrow-left" />
Go back
</Button>
<Button type="submit" class="btn btn-primary">
Create Event
</Button>
<Button type="submit" class="btn btn-primary">Create Event</Button>
</div>
</form>
+6 -7
View File
@@ -1,12 +1,11 @@
<script lang="ts">
import cx from "classnames"
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {displayPubkey} from '@welshman/util'
import {displayPubkey} from "@welshman/util"
import {deriveProfile, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
import Avatar from '@lib/components/Avatar.svelte'
import {getChatViewOptions} from '@app/editor'
import Avatar from "@lib/components/Avatar.svelte"
import {getViewOptions} from "@lib/editor"
export let root
export let replies
@@ -17,13 +16,13 @@
let editor: Readable<Editor>
onMount(() => {
editor = createEditor(getChatViewOptions(root.content))
editor = createEditor(getViewOptions(root.content))
})
</script>
<div>
<div class="card2 flex flex-col gap-2">
<div class="flex justify-between items-center gap-2">
<div class="flex items-center justify-between gap-2">
<div class="flex gap-2">
<div class="py-1">
<Avatar src={$profile?.picture} size={10} />
@@ -40,6 +39,6 @@
</div>
</div>
{#if replies.length > 0}
Show {replies.length} {replies.length === 1 ? 'reply' : 'replies'}
Show {replies.length} {replies.length === 1 ? "reply" : "replies"}
{/if}
</div>
+14 -37
View File
@@ -3,44 +3,24 @@
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {NProfileExtension, ImageExtension} from "nostr-editor"
import {randomId} from "@welshman/lib"
import {createEvent, NOTE} from "@welshman/util"
import {publishThunk, makeThunk, dateToSeconds} from "@welshman/app"
import {findNodes} from "@lib/tiptap"
import {publishThunk, makeThunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import {makeMention, makeIMeta} from "@app/commands"
import {getNoteEditorOptions, addFile, uploadFiles} from "@app/editor"
import {pushModal, clearModal} from "@app/modal"
import {pushToast} from "@app/toast"
import {getPubkeyHints} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
import {clearModal} from "@app/modal"
export let url
const submit = () => uploadFiles($editor)
const startSubmit = () => uploadFiles($editor)
const back = () => history.back()
const uploading = writable(false)
const loading = writable(false)
const sendMessage = () => {
const json = $editor.getJSON()
const mentionTags = findNodes(NProfileExtension.name, json).map(m =>
makeMention(m.attrs!.pubkey, m.attrs!.relays),
)
const imetaTags = findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) =>
makeIMeta(src, {x, ox: x}),
)
const event = createEvent(NOTE, {
content: $editor.getText(),
tags: [
...mentionTags,
...imetaTags,
],
})
const submit = () => {
const event = createEvent(NOTE, {content: $editor.getText(), tags: getEditorTags($editor)})
publishThunk(makeThunk({event, relays: [url]}))
clearModal()
@@ -49,24 +29,24 @@
let editor: Readable<Editor>
onMount(() => {
editor = createEditor(getNoteEditorOptions({uploading, sendMessage}))
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints}))
})
</script>
<form class="column gap-4" on:submit|preventDefault={submit}>
<form class="column gap-4" on:submit|preventDefault={startSubmit}>
<div class="py-2">
<h1 class="heading">Create a Thread</h1>
<p class="text-center">Share your thoughts, or start a discussion.</p>
</div>
<div class="relative">
<div class="flex-grow overflow-hidden note-editor">
<div class="note-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
</div>
<Button
data-tip="Add an image"
class="tooltip tooltip-left absolute right-2 bottom-1"
class="tooltip tooltip-left absolute bottom-1 right-2"
on:click={() => addFile($editor)}>
{#if $uploading}
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon="paperclip" size={3} />
@@ -78,9 +58,6 @@
<Icon icon="alt-arrow-left" />
Go back
</Button>
<Button type="submit" class="btn btn-primary">
Create Thread
</Button>
<Button type="submit" class="btn btn-primary">Create Thread</Button>
</div>
</form>
-209
View File
@@ -1,209 +0,0 @@
import type {Writable} from "svelte/store"
import {nprofileEncode} from "nostr-tools/nip19"
import {SvelteNodeViewRenderer} from "svelte-tiptap"
import {Editor} from "@tiptap/core"
import Code from "@tiptap/extension-code"
import CodeBlock from "@tiptap/extension-code-block"
import Document from "@tiptap/extension-document"
import Dropcursor from "@tiptap/extension-dropcursor"
import Gapcursor from "@tiptap/extension-gapcursor"
import History from "@tiptap/extension-history"
import Paragraph from "@tiptap/extension-paragraph"
import Text from "@tiptap/extension-text"
import HardBreakExtension from "@tiptap/extension-hard-break"
import {
Bolt11Extension,
NProfileExtension,
NEventExtension,
NAddrExtension,
ImageExtension,
VideoExtension,
FileUploadExtension,
} from "nostr-editor"
import type {StampedEvent} from "@welshman/util"
import {signer, profileSearch} from "@welshman/app"
import {LinkExtension, asInline, createSuggestions} from "@lib/tiptap"
import ChatComposeMention from "@app/components/ChatComposeMention.svelte"
import ChatComposeEvent from "@app/components/ChatComposeEvent.svelte"
import ChatComposeImage from "@app/components/ChatComposeImage.svelte"
import ChatComposeBolt11 from "@app/components/ChatComposeBolt11.svelte"
import ChatComposeVideo from "@app/components/ChatComposeVideo.svelte"
import ChatComposeLink from "@app/components/ChatComposeLink.svelte"
import ChatComposeSuggestions from "@app/components/ChatComposeSuggestions.svelte"
import ChatSuggestionProfile from "@app/components/ChatSuggestionProfile.svelte"
import {getPubkeyHints} from "@app/commands"
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
export const uploadFiles = (editor: Editor) => editor.chain().uploadFiles().run()
type EditorOptions = {
uploading: Writable<boolean>
sendMessage: () => void
}
export const getChatEditorOptions = ({uploading, sendMessage}: EditorOptions) => ({
content: "",
autofocus: true,
extensions: [
Code,
CodeBlock,
Document,
Dropcursor,
Gapcursor,
History,
Paragraph,
Text,
HardBreakExtension.extend({
addKeyboardShortcuts() {
return {
"Shift-Enter": () => this.editor.commands.setHardBreak(),
"Mod-Enter": () => this.editor.commands.setHardBreak(),
Enter: () => {
if (this.editor.getText().trim()) {
uploadFiles(this.editor)
return true
}
return false
},
}
},
}),
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(ChatComposeLink),
}),
Bolt11Extension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeBolt11)}),
),
NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(ChatComposeMention),
addProseMirrorPlugins() {
return [
createSuggestions({
char: "@",
name: "nprofile",
editor: this.editor,
search: profileSearch,
select: (pubkey: string, props: any) => {
const relays = getPubkeyHints(pubkey)
const nprofile = nprofileEncode({pubkey, relays})
return props.command({pubkey, nprofile, relays})
},
suggestionComponent: ChatSuggestionProfile,
suggestionsComponent: ChatComposeSuggestions,
}),
]
},
}),
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeEvent)})),
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeEvent)})),
ImageExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeImage)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
VideoExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeVideo)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
FileUploadExtension.configure({
immediateUpload: false,
sign: (event: StampedEvent) => {
uploading.set(true)
return signer.get()!.sign(event)
},
onComplete: () => {
uploading.set(false)
sendMessage()
},
}),
],
})
export const getChatViewOptions = (content: string) => ({
content,
editable: false,
shouldRerenderOnTransaction: false,
extensions: [
Code,
CodeBlock,
Document,
Paragraph,
Text,
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(ChatComposeLink),
}),
Bolt11Extension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeBolt11)}),
),
NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(ChatComposeMention),
}),
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeEvent)})),
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeEvent)})),
ImageExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeImage)})),
VideoExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeVideo)})),
],
})
export const getNoteEditorOptions = ({uploading, sendMessage}: EditorOptions) => ({
content: "",
extensions: [
Document,
Dropcursor,
Gapcursor,
History,
Paragraph,
Text,
HardBreakExtension,
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(ChatComposeLink),
}),
Bolt11Extension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeBolt11)}),
),
NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(ChatComposeMention),
addProseMirrorPlugins() {
return [
createSuggestions({
char: "@",
name: "nprofile",
editor: this.editor,
search: profileSearch,
select: (pubkey: string, props: any) => {
const relays = getPubkeyHints(pubkey)
const nprofile = nprofileEncode({pubkey, relays})
return props.command({pubkey, nprofile, relays})
},
suggestionComponent: ChatSuggestionProfile,
suggestionsComponent: ChatComposeSuggestions,
}),
]
},
}),
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeEvent)})),
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeEvent)})),
ImageExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeImage)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
VideoExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(ChatComposeVideo)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
FileUploadExtension.configure({
immediateUpload: false,
sign: (event: StampedEvent) => {
uploading.set(true)
return signer.get()!.sign(event)
},
onComplete: () => {
uploading.set(false)
sendMessage()
},
}),
],
})
+1 -1
View File
@@ -190,7 +190,7 @@ export const {
const timestamps = chat?.messages.map(m => m.event.created_at) || []
const since = Math.max(0, max(timestamps) - 3600)
return load({...request, relays: [url], filters: [{'#~': [room], since}]})
return load({...request, relays: [url], filters: [{"#~": [room], since}]})
},
})
+4 -10
View File
@@ -1,9 +1,7 @@
<script lang="ts">
import cx from 'classnames'
import {DateInput} from "date-picker-svelte"
import {formatTimestamp} from '@welshman/app'
import Icon from '@lib/components/Icon.svelte'
import Button from '@lib/components/Button.svelte'
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
export let initialValue: Date | undefined = undefined
export let value: Date | undefined = initialValue
@@ -21,12 +19,8 @@
</script>
<Button class="relative" on:click={init}>
<DateInput
format="yyyy-MM-dd HH:mm"
timePrecision="minute"
placeholder=""
bind:value />
<div class="absolute top-0 h-12 right-2 flex gap-2 items-center cursor-pointer">
<DateInput format="yyyy-MM-dd HH:mm" timePrecision="minute" placeholder="" bind:value />
<div class="absolute right-2 top-0 flex h-12 cursor-pointer items-center gap-2">
{#if value}
<Button on:click={clear} class="h-5">
<Icon icon="close-circle" />
+3 -3
View File
@@ -113,7 +113,7 @@
login: Login,
"login-2": Login2,
magnifer: Magnifer,
'map-point': MapPoint,
"map-point": MapPoint,
"menu-dots": MenuDots,
"notes-minimalistic": NotesMinimalistic,
"pallete-2": Pallete2,
@@ -124,14 +124,14 @@
"shop-minimalistic": ShopMinimalistic,
"smile-circle": SmileCircle,
settings: Settings,
'tag-horizontal': TagHorizontal,
"tag-horizontal": TagHorizontal,
"ufo-3": UFO3,
"square-share-line": SquareShareLine,
"user-heart": UserHeart,
"user-circle": UserCircle,
"user-rounded": UserRounded,
widget: Widget,
'widget-add': WidgetAdd,
"widget-add": WidgetAdd,
"wifi-router-round": WiFiRouterRound,
})
@@ -68,7 +68,7 @@ export const createSuggestions = (options: SuggestionsOptions) =>
popover = tippy("body", {
getReferenceClientRect: props.clientRect as any,
appendTo: document.querySelector('dialog[open]') || document.body,
appendTo: document.querySelector("dialog[open]") || document.body,
content: target,
showOnCreate: true,
interactive: true,
+170
View File
@@ -0,0 +1,170 @@
import type {Writable} from "svelte/store"
import {nprofileEncode} from "nostr-tools/nip19"
import {SvelteNodeViewRenderer} from "svelte-tiptap"
import Code from "@tiptap/extension-code"
import CodeBlock from "@tiptap/extension-code-block"
import Document from "@tiptap/extension-document"
import Dropcursor from "@tiptap/extension-dropcursor"
import Gapcursor from "@tiptap/extension-gapcursor"
import History from "@tiptap/extension-history"
import Paragraph from "@tiptap/extension-paragraph"
import Text from "@tiptap/extension-text"
import HardBreakExtension from "@tiptap/extension-hard-break"
import {
Bolt11Extension,
NProfileExtension,
NEventExtension,
NAddrExtension,
ImageExtension,
VideoExtension,
FileUploadExtension,
} from "nostr-editor"
import type {StampedEvent} from "@welshman/util"
import {signer, profileSearch} from "@welshman/app"
import {createSuggestions} from "./Suggestions"
import {LinkExtension} from "./LinkExtension"
import EditMention from "./EditMention.svelte"
import EditEvent from "./EditEvent.svelte"
import EditImage from "./EditImage.svelte"
import EditBolt11 from "./EditBolt11.svelte"
import EditVideo from "./EditVideo.svelte"
import EditLink from "./EditLink.svelte"
import Suggestions from "./Suggestions.svelte"
import SuggestionProfile from "./SuggestionProfile.svelte"
import {uploadFiles, asInline} from "./util"
export {
createSuggestions,
LinkExtension,
EditMention,
EditEvent,
EditImage,
EditBolt11,
EditVideo,
EditLink,
Suggestions,
SuggestionProfile,
}
export * from "./util"
type EditorOptions = {
submit: () => void
loading: Writable<boolean>
getPubkeyHints: (pubkey: string) => string[]
submitOnEnter?: boolean
}
export const getModifiedHardBreakExtension = () =>
HardBreakExtension.extend({
addKeyboardShortcuts() {
return {
"Shift-Enter": () => this.editor.commands.setHardBreak(),
"Mod-Enter": () => this.editor.commands.setHardBreak(),
Enter: () => {
if (this.editor.getText().trim()) {
uploadFiles(this.editor)
return true
}
return false
},
}
},
})
export const getEditorOptions = ({
submit,
loading,
getPubkeyHints,
submitOnEnter,
}: EditorOptions) => {
return {
content: "",
autofocus: true,
extensions: [
Code,
CodeBlock,
Document,
Dropcursor,
Gapcursor,
History,
Paragraph,
Text,
submitOnEnter ? getModifiedHardBreakExtension() : HardBreakExtension,
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(EditLink),
}),
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(EditMention),
addProseMirrorPlugins() {
return [
createSuggestions({
char: "@",
name: "nprofile",
editor: this.editor,
search: profileSearch,
select: (pubkey: string, props: any) => {
const relays = getPubkeyHints(pubkey)
const nprofile = nprofileEncode({pubkey, relays})
return props.command({pubkey, nprofile, relays})
},
suggestionComponent: SuggestionProfile,
suggestionsComponent: Suggestions,
}),
]
},
}),
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
ImageExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
VideoExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
FileUploadExtension.configure({
immediateUpload: false,
sign: (event: StampedEvent) => {
loading.set(true)
return signer.get()!.sign(event)
},
onComplete: () => {
loading.set(false)
submit()
},
}),
],
}
}
type ViewOptions = {
content: string
}
export const getViewOptions = ({content}: ViewOptions) => ({
content,
editable: false,
shouldRerenderOnTransaction: false,
extensions: [
Code,
CodeBlock,
Document,
Paragraph,
Text,
LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(EditLink),
}),
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(EditMention),
}),
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
ImageExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)})),
VideoExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)})),
],
})
+50
View File
@@ -0,0 +1,50 @@
import type {JSONContent, PasteRuleMatch} from "@tiptap/core"
import {Editor} from "@tiptap/core"
import {choice} from "@welshman/lib"
export const asInline = (extend: Record<string, any>) => ({
inline: true,
group: "inline",
...extend,
})
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 const findNodes = (type: string, json: JSONContent) => {
const results: JSONContent[] = []
for (const node of json.content || []) {
if (node.type === type) {
results.push(node)
}
for (const result of findNodes(type, node)) {
results.push(result)
}
}
return results
}
export const getEditorTags = (editor: Editor) => {
const json = editor.getJSON()
const withAttrs = (f: any) => (attrs: any) => f(attrs as Record<string, any>)
const mentionTags = findNodes("nprofile", json).map(
withAttrs(({pubkey, relays}: any) => ["p", pubkey, choice(relays), ""]),
)
const imetaTags = findNodes("image", json).map(
withAttrs(({src, sha256}: any) => ["imeta", `url ${src}`, `x ${sha256}`, `ox ${sha256}`]),
)
return [...mentionTags, ...imetaTags]
}
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
export const uploadFiles = (editor: Editor) => editor.chain().uploadFiles().run()
-3
View File
@@ -1,3 +0,0 @@
export * from "@lib/tiptap/util"
export {createSuggestions} from "@lib/tiptap/Suggestions"
export {LinkExtension} from "@lib/tiptap/LinkExtension"
-28
View File
@@ -1,28 +0,0 @@
import type {JSONContent, PasteRuleMatch} from "@tiptap/core"
export const asInline = (extend: Record<string, any>) => ({
inline: true,
group: "inline",
...extend,
})
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 const findNodes = (type: string, json: JSONContent) => {
const results: JSONContent[] = []
for (const node of json.content || []) {
if (node.type === type) {
results.push(node)
}
for (const result of findNodes(type, node)) {
results.push(result)
}
}
return results
}
@@ -1,7 +1,7 @@
<script lang="ts">
import {page} from "$app/stores"
import {sortBy, last} from '@welshman/lib'
import type {TrustedEvent} from '@welshman/util'
import {sortBy, last} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {formatTimestampAsDate} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -17,7 +17,7 @@
const createEvent = () => pushModal(EventCreate, {url})
const getStart = (event: TrustedEvent) =>
parseInt(event.tags.find(t => t[0] === 'start')?.[1]!)
parseInt(event.tags.find(t => t[0] === "start")?.[1] || "0")
let loading = true
@@ -26,14 +26,14 @@
dateDisplay?: string
}
$: items = sortBy(getStart, $eventsByUrl.get(url) || [])
.reduce<Item[]>((r, event) => {
const prevDateDisplay = r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined
const newDateDisplay = formatTimestampAsDate(getStart(event))
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
$: items = sortBy(getStart, $eventsByUrl.get(url) || []).reduce<Item[]>((r, event) => {
const prevDateDisplay =
r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined
const newDateDisplay = formatTimestampAsDate(getStart(event))
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
return [...r, {event, dateDisplay}]
}, [])
return [...r, {event, dateDisplay}]
}, [])
setTimeout(() => {
loading = false
@@ -50,8 +50,8 @@
</div>
</div>
</div>
<div class="flex flex-grow flex-col overflow-auto p-2 gap-2">
{#each items as {event, dateDisplay}, i (event.id)}
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
{#each items as { event, dateDisplay }, i (event.id)}
{#if dateDisplay}
<Divider>{dateDisplay}</Divider>
{/if}
@@ -67,8 +67,11 @@
</Spinner>
</p>
</div>
<Button class="fixed bottom-4 right-4 tooltip tooltip-left p-1" data-tip="Create an Event" on:click={createEvent}>
<div class="w-12 h-12 flex items-center justify-center btn btn-primary btn-circle">
<Button
class="tooltip tooltip-left fixed bottom-4 right-4 p-1"
data-tip="Create an Event"
on:click={createEvent}>
<div class="btn btn-circle btn-primary flex h-12 w-12 items-center justify-center">
<Icon icon="calendar-add" />
</div>
</Button>
@@ -1,12 +1,9 @@
<script lang="ts">
import {page} from "$app/stores"
import {sortBy, last} from '@welshman/lib'
import type {TrustedEvent} from '@welshman/util'
import {formatTimestampAsDate} from "@welshman/app"
import {sortBy} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Divider from "@lib/components/Divider.svelte"
import ThreadCard from "@app/components/ThreadCard.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte"
import {pushModal} from "@app/modal"
@@ -35,8 +32,8 @@
</div>
</div>
</div>
<div class="flex flex-grow flex-col overflow-auto p-2 gap-2">
{#each threads as {root, replies} (root.id)}
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
{#each threads as { root, replies } (root.id)}
<ThreadCard {root} {replies} />
{/each}
<p class="flex h-10 items-center justify-center py-20">
@@ -49,8 +46,11 @@
</Spinner>
</p>
</div>
<Button class="fixed bottom-4 right-4 tooltip tooltip-left p-1" data-tip="Create an Event" on:click={createThread}>
<div class="w-12 h-12 flex items-center justify-center btn btn-primary btn-circle">
<Button
class="tooltip tooltip-left fixed bottom-4 right-4 p-1"
data-tip="Create an Event"
on:click={createThread}>
<div class="btn btn-circle btn-primary flex h-12 w-12 items-center justify-center">
<Icon icon="add-square" />
</div>
</Button>