forked from coracle/flotilla
Move editor stuff to its own folder
This commit is contained in:
+9
-8
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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}]})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
@@ -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)})),
|
||||
],
|
||||
})
|
||||
@@ -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()
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "@lib/tiptap/util"
|
||||
export {createSuggestions} from "@lib/tiptap/Suggestions"
|
||||
export {LinkExtension} from "@lib/tiptap/LinkExtension"
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user