Fix message layout, fix uploads
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.1.2",
|
"@capacitor/android": "^6.1.2",
|
||||||
"@capacitor/cli": "^6.1.2",
|
"@capacitor/cli": "^6.1.2",
|
||||||
|
|||||||
+1
-2
@@ -113,8 +113,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
@apply overflow-hidden text-ellipsis whitespace-nowrap;
|
@apply justify-start overflow-hidden text-ellipsis whitespace-nowrap;
|
||||||
display: inline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsize {
|
.ellipsize {
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {getEditorOptions, getEditorTags, addFile} from "@lib/editor"
|
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||||
import {getPubkeyHints} from "@app/commands"
|
import {getPubkeyHints} from "@app/commands"
|
||||||
|
|
||||||
export let onSubmit
|
export let onSubmit
|
||||||
export let content = ""
|
export let content = ""
|
||||||
|
|
||||||
const loading = writable(false)
|
|
||||||
|
|
||||||
let editor: Readable<Editor>
|
let editor: Readable<Editor>
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
|
if ($loading) return
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
content: $editor.getText({blockSeparator: "\n"}),
|
content: $editor.getText({blockSeparator: "\n"}),
|
||||||
tags: getEditorTags($editor),
|
tags: getEditorTags($editor),
|
||||||
@@ -25,11 +24,12 @@
|
|||||||
$editor.chain().clearContent().run()
|
$editor.chain().clearContent().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: loading = $editor?.storage.fileUpload.loading
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(
|
editor = createEditor(
|
||||||
getEditorOptions({
|
getEditorOptions({
|
||||||
submit,
|
submit,
|
||||||
loading,
|
|
||||||
getPubkeyHints,
|
getPubkeyHints,
|
||||||
submitOnEnter: true,
|
submitOnEnter: true,
|
||||||
autofocus: !isMobile,
|
autofocus: !isMobile,
|
||||||
@@ -40,11 +40,14 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative z-feature flex gap-2 p-2">
|
<form
|
||||||
|
class="relative z-feature flex gap-2 p-2"
|
||||||
|
on:submit|preventDefault={$loading ? undefined : submit}>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
||||||
on:click={() => addFile($editor)}>
|
disabled={$loading}
|
||||||
|
on:click={$editor.commands.selectFiles}>
|
||||||
{#if $loading}
|
{#if $loading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -54,4 +57,4 @@
|
|||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<EditorContent editor={$editor} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
<Icon icon="menu-dots" size={4} />
|
<Icon icon="menu-dots" size={4} />
|
||||||
</button>
|
</button>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col" class:items-end={event.pubkey === $pubkey}>
|
||||||
<LongPress
|
<LongPress
|
||||||
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||||
onLongPress={showMobileMenu}>
|
onLongPress={showMobileMenu}>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<Field>
|
<Field>
|
||||||
<div slot="input">
|
<div slot="input">
|
||||||
<ProfileMultiSelect bind:value={pubkeys} />
|
<ProfileMultiSelect autofocus bind:value={pubkeys} />
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||||
import {randomId} from "@welshman/lib"
|
import {randomId} from "@welshman/lib"
|
||||||
import {createEvent, EVENT_DATE, EVENT_TIME} from "@welshman/util"
|
import {createEvent, EVENT_DATE, EVENT_TIME} from "@welshman/util"
|
||||||
@@ -13,18 +12,16 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
import {getPubkeyHints} from "@app/commands"
|
import {getPubkeyHints} from "@app/commands"
|
||||||
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
|
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
const startSubmit = () => uploadFiles($editor)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const loading = writable(false)
|
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
|
if ($loading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -63,12 +60,14 @@
|
|||||||
let start: Date
|
let start: Date
|
||||||
let end: Date
|
let end: Date
|
||||||
|
|
||||||
|
$: loading = $editor?.storage.fileUpload.loading
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints}))
|
editor = createEditor(getEditorOptions({submit, getPubkeyHints}))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={startSubmit}>
|
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Create an Event</div>
|
<div slot="title">Create an Event</div>
|
||||||
<div slot="info">Invite other group members to events online or in real life.</div>
|
<div slot="info">Invite other group members to events online or in real life.</div>
|
||||||
@@ -87,7 +86,10 @@
|
|||||||
<div class="input-editor flex-grow overflow-hidden">
|
<div class="input-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<EditorContent editor={$editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button data-tip="Add an image" class="center btn tooltip" on:click={() => addFile($editor)}>
|
<Button
|
||||||
|
data-tip="Add an image"
|
||||||
|
class="center btn tooltip"
|
||||||
|
on:click={$editor.commands.selectFiles}>
|
||||||
{#if $loading}
|
{#if $loading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import {pubkeyLink} from "@app/state"
|
import {pubkeyLink} from "@app/state"
|
||||||
|
|
||||||
export let value: string[]
|
export let value: string[]
|
||||||
|
export let autofocus = false
|
||||||
|
|
||||||
let term = ""
|
let term = ""
|
||||||
let input: Element
|
let input: Element
|
||||||
@@ -59,7 +60,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}>
|
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}>
|
||||||
<Icon icon="magnifer" />
|
<Icon icon="magnifer" />
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<input
|
<input
|
||||||
|
{autofocus}
|
||||||
class="grow"
|
class="grow"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for profiles..."
|
placeholder="Search for profiles..."
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||||
import {createEvent} from "@welshman/util"
|
import {createEvent} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
@@ -13,19 +12,16 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {THREAD, GENERAL, tagRoom} from "@app/state"
|
import {THREAD, GENERAL, tagRoom} from "@app/state"
|
||||||
|
|
||||||
import {getPubkeyHints} from "@app/commands"
|
import {getPubkeyHints} from "@app/commands"
|
||||||
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
|
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
const startSubmit = () => uploadFiles($editor)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const loading = writable(false)
|
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
|
if ($loading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -55,19 +51,16 @@
|
|||||||
let title: string
|
let title: string
|
||||||
let editor: Readable<Editor>
|
let editor: Readable<Editor>
|
||||||
|
|
||||||
|
$: loading = $editor?.storage.fileUpload.loading
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(
|
editor = createEditor(
|
||||||
getEditorOptions({
|
getEditorOptions({submit, getPubkeyHints, placeholder: "What's on your mind?"}),
|
||||||
submit,
|
|
||||||
loading,
|
|
||||||
getPubkeyHints,
|
|
||||||
placeholder: "What's on your mind?",
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={startSubmit}>
|
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Create a Thread</div>
|
<div slot="title">Create a Thread</div>
|
||||||
<div slot="info">Share a link, or start a discussion.</div>
|
<div slot="info">Share a link, or start a discussion.</div>
|
||||||
@@ -94,7 +87,7 @@
|
|||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
on:click={() => addFile($editor)}>
|
on:click={$editor.commands.selectFiles}>
|
||||||
{#if $loading}
|
{#if $loading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||||
import {append} from "@welshman/lib"
|
import {append} from "@welshman/lib"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
@@ -9,7 +8,7 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
|
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||||
import {getPubkeyHints, publishComment} from "@app/commands"
|
import {getPubkeyHints, publishComment} from "@app/commands"
|
||||||
import {tagRoom, GENERAL} from "@app/state"
|
import {tagRoom, GENERAL} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
@@ -19,11 +18,9 @@
|
|||||||
export let onClose
|
export let onClose
|
||||||
export let onSubmit
|
export let onSubmit
|
||||||
|
|
||||||
const startSubmit = () => uploadFiles($editor)
|
|
||||||
|
|
||||||
const loading = writable(false)
|
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
|
if ($loading) return
|
||||||
|
|
||||||
const content = $editor.getText({blockSeparator: "\n"})
|
const content = $editor.getText({blockSeparator: "\n"})
|
||||||
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
|
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
|
||||||
|
|
||||||
@@ -39,15 +36,17 @@
|
|||||||
|
|
||||||
let editor: Readable<Editor>
|
let editor: Readable<Editor>
|
||||||
|
|
||||||
|
$: loading = $editor?.storage.fileUpload.loading
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints, autofocus: !isMobile}))
|
editor = createEditor(getEditorOptions({submit, getPubkeyHints, autofocus: !isMobile}))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
in:fly
|
in:fly
|
||||||
out:slideAndFade
|
out:slideAndFade
|
||||||
on:submit|preventDefault={startSubmit}
|
on:submit|preventDefault={submit}
|
||||||
class="card2 sticky bottom-2 z-feature mx-2 mt-2 bg-neutral">
|
class="card2 sticky bottom-2 z-feature mx-2 mt-2 bg-neutral">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="note-editor flex-grow overflow-hidden">
|
<div class="note-editor flex-grow overflow-hidden">
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
on:click={() => addFile($editor)}>
|
on:click={$editor.commands.selectFiles}>
|
||||||
{#if $loading}
|
{#if $loading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
+16
-3
@@ -251,17 +251,20 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const eventIsForUrl = (url: string, event: TrustedEvent) =>
|
||||||
|
event.tags.find(nthEq(0, "~"))?.[2] === url
|
||||||
|
|
||||||
export const getEventsForUrl = (url: string, filters: Filter[]) =>
|
export const getEventsForUrl = (url: string, filters: Filter[]) =>
|
||||||
sortBy(
|
sortBy(
|
||||||
e => -e.created_at,
|
e => -e.created_at,
|
||||||
repository.query(filters).filter(e => e.tags.find(nthEq(0, "~"))?.[2] === url),
|
repository.query(filters).filter(e => eventIsForUrl(url, e)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
|
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
|
||||||
derived(deriveEvents(repository, {filters}), $events =>
|
derived(deriveEvents(repository, {filters}), $events =>
|
||||||
sortBy(
|
sortBy(
|
||||||
e => -e.created_at,
|
e => -e.created_at,
|
||||||
$events.filter(e => e.tags.find(nthEq(0, "~"))?.[2] === url),
|
$events.filter(e => eventIsForUrl(url, e)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -275,6 +278,9 @@ export type Settings = {
|
|||||||
show_media: boolean
|
show_media: boolean
|
||||||
hide_sensitive: boolean
|
hide_sensitive: boolean
|
||||||
send_delay: number
|
send_delay: number
|
||||||
|
upload_type: "nip96" | "blossom"
|
||||||
|
nip96_urls: string[]
|
||||||
|
blossom_urls: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +288,9 @@ export const defaultSettings = {
|
|||||||
show_media: true,
|
show_media: true,
|
||||||
hide_sensitive: true,
|
hide_sensitive: true,
|
||||||
send_delay: 3000,
|
send_delay: 3000,
|
||||||
|
upload_type: "nip96",
|
||||||
|
nip96_urls: ["https://nostr.build"],
|
||||||
|
blossom_urls: ["https://cdn.satellite.earth"],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settings = deriveEventsMapped<Settings>(repository, {
|
export const settings = deriveEventsMapped<Settings>(repository, {
|
||||||
@@ -519,7 +528,11 @@ export const userSettings = withGetter(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const userSettingValues = derived(userSettings, $s => $s?.values || defaultSettings)
|
export const userSettingValues = withGetter(
|
||||||
|
derived(userSettings, $s => $s?.values || defaultSettings),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const getSetting = (key: keyof Settings["values"]) => userSettingValues.get()[key]
|
||||||
|
|
||||||
export const userMembership = withGetter(
|
export const userMembership = withGetter(
|
||||||
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
|
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NodeViewWrapper class={cx("link-content inline", {"link-content-selected": selected})}>
|
<NodeViewWrapper class={cx("link-content inline", {"link-content-selected": selected})}>
|
||||||
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
{#if node.attrs.uploading}
|
||||||
|
<span class="loading loading-spinner loading-xs translate-y-[2px] scale-75" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
||||||
|
{/if}
|
||||||
{node.attrs.file.name}
|
{node.attrs.file.name}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NodeViewWrapper class="link-content inline">
|
<NodeViewWrapper class="link-content inline">
|
||||||
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
{#if node.attrs.uploading}
|
||||||
|
<span class="loading loading-spinner loading-xs translate-y-[2px] scale-75" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
||||||
|
{/if}
|
||||||
{node.attrs.file.name}
|
{node.attrs.file.name}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
import type {CommandProps, Editor} from "@tiptap/core"
|
||||||
|
import {Extension} from "@tiptap/core"
|
||||||
|
import {now} from "@welshman/lib"
|
||||||
|
import type {StampedEvent, SignedEvent} from "@welshman/util"
|
||||||
|
import type {ImageAttributes, VideoAttributes} from "nostr-editor"
|
||||||
|
import {readServerConfig, uploadFile} from "nostr-tools/nip96"
|
||||||
|
import {getToken} from "nostr-tools/nip98"
|
||||||
|
import type {Node} from "prosemirror-model"
|
||||||
|
import {Plugin, PluginKey} from "prosemirror-state"
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
uploadFile: {
|
||||||
|
selectFiles: () => ReturnType
|
||||||
|
uploadFiles: () => ReturnType
|
||||||
|
getMetaTags: () => string[][]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadOptions {
|
||||||
|
allowedMimeTypes: string[]
|
||||||
|
expiration: number
|
||||||
|
immediateUpload: boolean
|
||||||
|
hash: (file: File) => Promise<string>
|
||||||
|
sign?: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
||||||
|
onDrop: (currentEditor: Editor, file: File, pos: number) => void
|
||||||
|
onComplete: (currentEditor: Editor) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadTask {
|
||||||
|
url?: string
|
||||||
|
sha256?: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function bufferToHex(buffer: ArrayBuffer) {
|
||||||
|
return Array.from(new Uint8Array(buffer))
|
||||||
|
.map(b => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUploadExtension = Extension.create<FileUploadOptions>({
|
||||||
|
name: "fileUpload",
|
||||||
|
|
||||||
|
addStorage() {
|
||||||
|
return {
|
||||||
|
loading: writable(false),
|
||||||
|
tags: [] as string[][],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
allowedMimeTypes: [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"video/mp4",
|
||||||
|
"video/mpeg",
|
||||||
|
"video/webm",
|
||||||
|
],
|
||||||
|
immediateUpload: true,
|
||||||
|
expiration: 60000,
|
||||||
|
async hash(file: File) {
|
||||||
|
return bufferToHex(await crypto.subtle.digest("SHA-256", await file.arrayBuffer()))
|
||||||
|
},
|
||||||
|
onDrop() {},
|
||||||
|
onComplete() {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
selectFiles: () => props => {
|
||||||
|
props.tr.setMeta("selectFiles", true)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
uploadFiles: () => (props: CommandProps) => {
|
||||||
|
props.tr.setMeta("uploadFiles", true)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
getMetaTags: () =>
|
||||||
|
((props: CommandProps) => {
|
||||||
|
const tags: string[][] = []
|
||||||
|
// make sure the file uploaded is still in the editor content
|
||||||
|
props.editor.state.doc.descendants(node => {
|
||||||
|
if (!(node.type.name === "image" || node.type.name === "video")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const tag = props.editor.storage.fileUpload.tags.find((t: string[]) =>
|
||||||
|
t[1].includes(node.attrs.src),
|
||||||
|
)
|
||||||
|
if (tag) {
|
||||||
|
tags.push(tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tags
|
||||||
|
}) as any,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const uploader = new Uploader(this.editor, this.options)
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey("fileUploadPlugin"),
|
||||||
|
state: {
|
||||||
|
init() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
apply(tr) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (tr.getMeta("selectFiles")) {
|
||||||
|
uploader.selectFiles()
|
||||||
|
tr.setMeta("selectFiles", null)
|
||||||
|
} else if (tr.getMeta("uploadFiles")) {
|
||||||
|
uploader.uploadFiles()
|
||||||
|
tr.setMeta("uploadFiles", null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
handleDrop: (_, event) => {
|
||||||
|
return uploader.handleDrop(event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
class Uploader {
|
||||||
|
constructor(
|
||||||
|
public editor: Editor,
|
||||||
|
private options: FileUploadOptions,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get view() {
|
||||||
|
return this.editor.view
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(file: File, pos: number) {
|
||||||
|
if (
|
||||||
|
!this.options.allowedMimeTypes.some(amt => amt.split("*").every(s => file.type.includes(s)))
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const {tr} = this.view.state
|
||||||
|
const [mimetype] = file.type.split("/")
|
||||||
|
const node = this.view.state.schema.nodes[mimetype].create({
|
||||||
|
file,
|
||||||
|
src: URL.createObjectURL(file),
|
||||||
|
alt: "",
|
||||||
|
uploading: false,
|
||||||
|
uploadError: null,
|
||||||
|
})
|
||||||
|
tr.insert(pos, node)
|
||||||
|
this.view.dispatch(tr)
|
||||||
|
|
||||||
|
if (this.options.immediateUpload) {
|
||||||
|
this.editor.storage.fileUpload.loading.set(true)
|
||||||
|
this.upload(node).then(() => this.editor.storage.fileUpload.loading.set(false))
|
||||||
|
}
|
||||||
|
this.options.onDrop(this.editor, file, pos)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
findNodePosition(node: Node) {
|
||||||
|
let pos = -1
|
||||||
|
this.view.state.doc.descendants((n, p) => {
|
||||||
|
if (n === node) {
|
||||||
|
pos = p
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
|
findNodes(uploading: boolean) {
|
||||||
|
const nodes = [] as [Node, number][]
|
||||||
|
this.view.state.doc.descendants((node, pos) => {
|
||||||
|
if (!(node.type.name === "image" || node.type.name === "video")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.attrs.sha256) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ((node.attrs.uploading || false) !== uploading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes.push([node, pos])
|
||||||
|
})
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodeAttributes(nodeRef: Node, attrs: Record<string, unknown>) {
|
||||||
|
const {tr} = this.editor.view.state
|
||||||
|
|
||||||
|
const pos = this.findNodePosition(nodeRef)
|
||||||
|
if (pos === -1) return
|
||||||
|
|
||||||
|
Object.entries(attrs).forEach(
|
||||||
|
([key, value]) => value !== undefined && tr.setNodeAttribute(pos, key, value),
|
||||||
|
)
|
||||||
|
this.view.dispatch(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadDone(nodeRef: Node, response: UploadTask) {
|
||||||
|
this.findNodes(true).forEach(([node, pos]) => {
|
||||||
|
if (node.attrs.src === nodeRef.attrs.src) {
|
||||||
|
this.updateNodeAttributes(node, {
|
||||||
|
uploading: false,
|
||||||
|
src: response.url,
|
||||||
|
sha256: response.sha256,
|
||||||
|
uploadError: response.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload(node: Node) {
|
||||||
|
const {sign, hash, expiration} = this.options
|
||||||
|
|
||||||
|
const {
|
||||||
|
file,
|
||||||
|
alt,
|
||||||
|
uploadType,
|
||||||
|
uploadUrl: serverUrl,
|
||||||
|
} = node.attrs as ImageAttributes | VideoAttributes
|
||||||
|
|
||||||
|
this.updateNodeAttributes(node, {uploading: true, uploadError: null})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (uploadType === "nip96") {
|
||||||
|
const res = (await uploadNIP96({file, alt, sign: sign!, serverUrl}))!
|
||||||
|
|
||||||
|
// add the tags as received from nip-96 to the storage
|
||||||
|
this.editor.storage.fileUpload.tags.push(["imeta", ...res.tags!])
|
||||||
|
this.onUploadDone(node, res)
|
||||||
|
} else {
|
||||||
|
const res = await uploadBlossom({file, serverUrl, hash, sign, expiration})
|
||||||
|
this.editor.storage.fileUpload.tags.push([
|
||||||
|
"imeta",
|
||||||
|
`url ${res.url}`,
|
||||||
|
`size ${res.size}`,
|
||||||
|
`m ${res.type}`,
|
||||||
|
`x ${res.sha256}`,
|
||||||
|
])
|
||||||
|
this.onUploadDone(node, res)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error as string
|
||||||
|
this.onUploadDone(node, {error: msg})
|
||||||
|
throw new Error(msg as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFiles() {
|
||||||
|
const tasks = this.findNodes(false).map(([node]) => {
|
||||||
|
return this.upload(node)
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
this.editor.storage.fileUpload.loading.set(true)
|
||||||
|
await Promise.all(tasks)
|
||||||
|
this.options.onComplete(this.editor)
|
||||||
|
} finally {
|
||||||
|
this.editor.storage.fileUpload.loading.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFiles() {
|
||||||
|
const input = document.createElement("input")
|
||||||
|
input.type = "file"
|
||||||
|
input.multiple = true
|
||||||
|
input.accept = this.options.allowedMimeTypes.join(",")
|
||||||
|
input.onchange = event => {
|
||||||
|
const files = (event.target as HTMLInputElement).files
|
||||||
|
if (files) {
|
||||||
|
Array.from(files).forEach(file => {
|
||||||
|
if (file) {
|
||||||
|
const pos = this.view.state.selection.from + 1
|
||||||
|
this.addFile(file, pos)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop(event: DragEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const pos = this.view.posAtCoords({left: event.clientX, top: event.clientY})?.pos
|
||||||
|
|
||||||
|
if (pos === undefined) return false
|
||||||
|
|
||||||
|
const file = event.dataTransfer?.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
this.addFile(file, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NIP96Options {
|
||||||
|
file: File
|
||||||
|
alt?: string
|
||||||
|
serverUrl: string
|
||||||
|
expiration?: number
|
||||||
|
sign: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadNIP96(options: NIP96Options) {
|
||||||
|
try {
|
||||||
|
const server = await readServerConfig(options.serverUrl)
|
||||||
|
const authorization = await getToken(server.api_url, "POST", options.sign as any, true)
|
||||||
|
const res = await uploadFile(options.file, server.api_url, authorization, {
|
||||||
|
alt: options.alt || "",
|
||||||
|
expiration: options.expiration?.toString() || "",
|
||||||
|
content_type: options.file.type,
|
||||||
|
})
|
||||||
|
if (res.status === "error") {
|
||||||
|
throw new Error(res.message)
|
||||||
|
}
|
||||||
|
const url = res.nip94_event?.tags.find(x => x[0] === "url")?.[1] || ""
|
||||||
|
const sha256 = res.nip94_event?.tags.find(x => x[0] === "x")?.[1] || ""
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
sha256,
|
||||||
|
tags: res.nip94_event?.tags.flatMap(item => item.join(" ")),
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlossomOptions {
|
||||||
|
file: File
|
||||||
|
serverUrl: string
|
||||||
|
expiration?: number
|
||||||
|
hash?: (file: File) => Promise<string>
|
||||||
|
sign?: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlossomResponse {
|
||||||
|
sha256: string
|
||||||
|
size: number
|
||||||
|
type: string
|
||||||
|
uploaded: number
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlossomResponseError {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadBlossom(options: BlossomOptions) {
|
||||||
|
if (!options.hash) {
|
||||||
|
throw new Error("No hash function provided")
|
||||||
|
}
|
||||||
|
if (!options.sign) {
|
||||||
|
throw new Error("No signer provided")
|
||||||
|
}
|
||||||
|
const created_at = now()
|
||||||
|
const hash = await options.hash(options.file)
|
||||||
|
const event = await options.sign({
|
||||||
|
kind: 24242,
|
||||||
|
content: `Upload ${options.file.name}`,
|
||||||
|
created_at,
|
||||||
|
tags: [
|
||||||
|
["t", "upload"],
|
||||||
|
["x", hash],
|
||||||
|
["size", options.file.size.toString()],
|
||||||
|
["expiration", (created_at + (options.expiration || 60000)).toString()],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const data = JSON.stringify(event)
|
||||||
|
const base64 = btoa(data)
|
||||||
|
const authorization = `Nostr ${base64}`
|
||||||
|
const res = await fetch(options.serverUrl + "/upload", {
|
||||||
|
method: "PUT",
|
||||||
|
body: options.file,
|
||||||
|
headers: {
|
||||||
|
authorization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (res.status === 200) {
|
||||||
|
return json as BlossomResponse
|
||||||
|
}
|
||||||
|
throw new Error((json as BlossomResponseError).message)
|
||||||
|
}
|
||||||
+39
-37
@@ -1,4 +1,3 @@
|
|||||||
import type {Writable} from "svelte/store"
|
|
||||||
import {nprofileEncode} from "nostr-tools/nip19"
|
import {nprofileEncode} from "nostr-tools/nip19"
|
||||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||||
import Placeholder from "@tiptap/extension-placeholder"
|
import Placeholder from "@tiptap/extension-placeholder"
|
||||||
@@ -19,10 +18,10 @@ import {
|
|||||||
ImageExtension,
|
ImageExtension,
|
||||||
VideoExtension,
|
VideoExtension,
|
||||||
TagExtension,
|
TagExtension,
|
||||||
FileUploadExtension,
|
|
||||||
} from "nostr-editor"
|
} from "nostr-editor"
|
||||||
import type {StampedEvent} from "@welshman/util"
|
import type {StampedEvent} from "@welshman/util"
|
||||||
import {signer, profileSearch} from "@welshman/app"
|
import {signer, profileSearch} from "@welshman/app"
|
||||||
|
import {FileUploadExtension} from "./FileUpload"
|
||||||
import {createSuggestions} from "./Suggestions"
|
import {createSuggestions} from "./Suggestions"
|
||||||
import {LinkExtension} from "./LinkExtension"
|
import {LinkExtension} from "./LinkExtension"
|
||||||
import EditMention from "./EditMention.svelte"
|
import EditMention from "./EditMention.svelte"
|
||||||
@@ -33,7 +32,8 @@ import EditVideo from "./EditVideo.svelte"
|
|||||||
import EditLink from "./EditLink.svelte"
|
import EditLink from "./EditLink.svelte"
|
||||||
import Suggestions from "./Suggestions.svelte"
|
import Suggestions from "./Suggestions.svelte"
|
||||||
import SuggestionProfile from "./SuggestionProfile.svelte"
|
import SuggestionProfile from "./SuggestionProfile.svelte"
|
||||||
import {uploadFiles, asInline} from "./util"
|
import {asInline} from "./util"
|
||||||
|
import {getSetting} from "@app/state"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createSuggestions,
|
createSuggestions,
|
||||||
@@ -49,41 +49,28 @@ export {
|
|||||||
}
|
}
|
||||||
export * from "./util"
|
export * from "./util"
|
||||||
|
|
||||||
|
type UploadType = "nip96" | "blossom"
|
||||||
|
|
||||||
type EditorOptions = {
|
type EditorOptions = {
|
||||||
submit: () => void
|
submit: () => void
|
||||||
loading: Writable<boolean>
|
|
||||||
getPubkeyHints: (pubkey: string) => string[]
|
getPubkeyHints: (pubkey: string) => string[]
|
||||||
submitOnEnter?: boolean
|
submitOnEnter?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
|
uploadType?: UploadType
|
||||||
|
defaultUploadUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getModifiedHardBreakExtension = () =>
|
|
||||||
HardBreakExtension.extend({
|
|
||||||
addKeyboardShortcuts() {
|
|
||||||
return {
|
|
||||||
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
|
||||||
"Mod-Enter": () => this.editor.commands.setHardBreak(),
|
|
||||||
Enter: () => {
|
|
||||||
if (this.editor.getText({blockSeparator: "\n"}).trim()) {
|
|
||||||
uploadFiles(this.editor)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getEditorOptions = ({
|
export const getEditorOptions = ({
|
||||||
submit,
|
submit,
|
||||||
loading,
|
|
||||||
getPubkeyHints,
|
getPubkeyHints,
|
||||||
submitOnEnter,
|
submitOnEnter,
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
autofocus = false,
|
autofocus = false,
|
||||||
|
uploadType = getSetting("upload_type") as UploadType,
|
||||||
|
defaultUploadUrl = getSetting("upload_type") == "nip96"
|
||||||
|
? (getSetting("nip96_urls") as string[])[0] || "https://nostr.build"
|
||||||
|
: (getSetting("blossom_urls") as string[])[0] || "https://cdn.satellite.earth",
|
||||||
}: EditorOptions) => ({
|
}: EditorOptions) => ({
|
||||||
autofocus,
|
autofocus,
|
||||||
content: "",
|
content: "",
|
||||||
@@ -98,7 +85,29 @@ export const getEditorOptions = ({
|
|||||||
Text,
|
Text,
|
||||||
TagExtension,
|
TagExtension,
|
||||||
Placeholder.configure({placeholder}),
|
Placeholder.configure({placeholder}),
|
||||||
submitOnEnter ? getModifiedHardBreakExtension() : HardBreakExtension,
|
HardBreakExtension.extend({
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
||||||
|
"Mod-Enter": () => {
|
||||||
|
if (this.editor.getText().trim()) {
|
||||||
|
submit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.editor.commands.setHardBreak()
|
||||||
|
},
|
||||||
|
Enter: () => {
|
||||||
|
if (submitOnEnter && this.editor.getText().trim()) {
|
||||||
|
submit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.editor.commands.setHardBreak()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
LinkExtension.extend({addNodeView: () => SvelteNodeViewRenderer(EditLink)}),
|
LinkExtension.extend({addNodeView: () => SvelteNodeViewRenderer(EditLink)}),
|
||||||
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
|
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
|
||||||
NProfileExtension.extend({
|
NProfileExtension.extend({
|
||||||
@@ -126,21 +135,14 @@ export const getEditorOptions = ({
|
|||||||
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||||
ImageExtension.extend(
|
ImageExtension.extend(
|
||||||
asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}),
|
asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}),
|
||||||
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
|
).configure({defaultUploadUrl, defaultUploadType: uploadType}),
|
||||||
VideoExtension.extend(
|
VideoExtension.extend(
|
||||||
asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}),
|
asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}),
|
||||||
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
|
).configure({defaultUploadUrl, defaultUploadType: uploadType}),
|
||||||
FileUploadExtension.configure({
|
FileUploadExtension.configure({
|
||||||
immediateUpload: false,
|
immediateUpload: true,
|
||||||
sign: (event: StampedEvent) => {
|
allowedMimeTypes: ["image/*", "video/*"],
|
||||||
loading.set(true)
|
sign: (event: StampedEvent) => signer.get()!.sign(event),
|
||||||
|
|
||||||
return signer.get()!.sign(event)
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
loading.set(false)
|
|
||||||
submit()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -92,7 +92,3 @@ export const getEditorTags = (editor: Editor) => {
|
|||||||
|
|
||||||
return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags]
|
return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
|
|
||||||
|
|
||||||
export const uploadFiles = (editor: Editor) => editor.chain().uploadFiles().run()
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import {get, derived} from "svelte/store"
|
import {get, derived} from "svelte/store"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {dev} from "$app/environment"
|
import {dev} from "$app/environment"
|
||||||
|
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
|
||||||
import {identity, uniq, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib"
|
import {identity, uniq, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -76,6 +77,8 @@
|
|||||||
Object.assign(window, {
|
Object.assign(window, {
|
||||||
get,
|
get,
|
||||||
nip19,
|
nip19,
|
||||||
|
bytesToHex,
|
||||||
|
hexToBytes,
|
||||||
...lib,
|
...lib,
|
||||||
...welshmanSigner,
|
...welshmanSigner,
|
||||||
...util,
|
...util,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {onDestroy} from "svelte"
|
import {onDestroy} from "svelte"
|
||||||
import {isMobile} from "@lib/html"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
||||||
@@ -40,13 +39,7 @@
|
|||||||
<div slot="input" class="row-2 min-w-0 flex-grow items-center">
|
<div slot="input" class="row-2 min-w-0 flex-grow items-center">
|
||||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||||
<Icon icon="magnifer" />
|
<Icon icon="magnifer" />
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<input bind:value={term} class="grow" type="text" placeholder="Search for conversations..." />
|
||||||
<input
|
|
||||||
autofocus={!isMobile}
|
|
||||||
bind:value={term}
|
|
||||||
class="grow"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search for conversations..." />
|
|
||||||
</label>
|
</label>
|
||||||
<Button class="btn btn-primary" on:click={startChat}>
|
<Button class="btn btn-primary" on:click={startChat}>
|
||||||
<Icon icon="add-circle" />
|
<Icon icon="add-circle" />
|
||||||
|
|||||||
@@ -61,22 +61,28 @@
|
|||||||
{@const {software, version, supported_nips, limitation} = $relay.profile}
|
{@const {software, version, supported_nips, limitation} = $relay.profile}
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#if limitation?.auth_required}
|
{#if limitation?.auth_required}
|
||||||
<p class="badge badge-neutral">Authentication Required</p>
|
<p class="badge badge-neutral">
|
||||||
|
<span class="ellipsize">Authentication Required</span>
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if limitation?.payment_required}
|
{#if limitation?.payment_required}
|
||||||
<p class="badge badge-neutral">Payment Required</p>
|
<p class="badge badge-neutral"><span class="ellipsize">Payment Required</span></p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if limitation?.min_pow_difficulty}
|
{#if limitation?.min_pow_difficulty}
|
||||||
<p class="badge badge-neutral">Requires PoW {limitation?.min_pow_difficulty}</p>
|
<p class="badge badge-neutral">
|
||||||
|
<span class="ellipsize">Requires PoW {limitation?.min_pow_difficulty}</span>
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if supported_nips}
|
{#if supported_nips}
|
||||||
<p class="badge badge-neutral">NIPs: {supported_nips.join(", ")}</p>
|
<p class="badge badge-neutral">
|
||||||
|
<span class="ellipsize">NIPs: {supported_nips.join(", ")}</span>
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if software}
|
{#if software}
|
||||||
<p class="badge badge-neutral">Software: {software}</p>
|
<p class="badge badge-neutral"><span class="ellipsize">Software: {software}</span></p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if version}
|
{#if version}
|
||||||
<p class="badge badge-neutral">Version: {version}</p>
|
<p class="badge badge-neutral"><span class="ellipsize">Version: {version}</span></p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
flotilla:
|
||||||
|
android:
|
||||||
|
identifier: social.flotilla
|
||||||
|
name: Coracle
|
||||||
|
description: Self-hosted community chat and threads built on the nostr protocol.
|
||||||
|
repository: https://github.com/coracle-social/flotilla
|
||||||
|
artifacts:
|
||||||
|
- app-release-signed.apk
|
||||||
|
|
||||||
Reference in New Issue
Block a user