Fix message layout, fix uploads

This commit is contained in:
Jon Staab
2024-11-16 08:04:09 -08:00
parent 25b23cca8d
commit d01a08820a
19 changed files with 531 additions and 107 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "flotilla",
"version": "0.0.1",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flotilla",
"version": "0.0.1",
"version": "0.1.0",
"dependencies": {
"@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2",
+1 -2
View File
@@ -113,8 +113,7 @@
}
.badge {
@apply overflow-hidden text-ellipsis whitespace-nowrap;
display: inline;
@apply justify-start overflow-hidden text-ellipsis whitespace-nowrap;
}
.ellipsize {
+11 -8
View File
@@ -1,22 +1,21 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.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"
export let onSubmit
export let content = ""
const loading = writable(false)
let editor: Readable<Editor>
const submit = () => {
if ($loading) return
onSubmit({
content: $editor.getText({blockSeparator: "\n"}),
tags: getEditorTags($editor),
@@ -25,11 +24,12 @@
$editor.chain().clearContent().run()
}
$: loading = $editor?.storage.fileUpload.loading
onMount(() => {
editor = createEditor(
getEditorOptions({
submit,
loading,
getPubkeyHints,
submitOnEnter: true,
autofocus: !isMobile,
@@ -40,11 +40,14 @@
})
</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
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"
on:click={() => addFile($editor)}>
disabled={$loading}
on:click={$editor.commands.selectFiles}>
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
@@ -54,4 +57,4 @@
<div class="chat-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
</div>
</div>
</form>
+1 -1
View File
@@ -85,7 +85,7 @@
<Icon icon="menu-dots" size={4} />
</button>
</Tippy>
<div class="flex min-w-0 flex-col">
<div class="flex min-w-0 flex-col" class:items-end={event.pubkey === $pubkey}>
<LongPress
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
onLongPress={showMobileMenu}>
+1 -1
View File
@@ -23,7 +23,7 @@
</ModalHeader>
<Field>
<div slot="input">
<ProfileMultiSelect bind:value={pubkeys} />
<ProfileMultiSelect autofocus bind:value={pubkeys} />
</div>
</Field>
<ModalFooter>
+11 -9
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {randomId} from "@welshman/lib"
import {createEvent, EVENT_DATE, EVENT_TIME} from "@welshman/util"
@@ -13,18 +12,16 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import {getPubkeyHints} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
import {getEditorOptions, getEditorTags} from "@lib/editor"
import {pushToast} from "@app/toast"
export let url
const startSubmit = () => uploadFiles($editor)
const back = () => history.back()
const loading = writable(false)
const submit = () => {
if ($loading) return
if (!title) {
return pushToast({
theme: "error",
@@ -63,12 +60,14 @@
let start: Date
let end: Date
$: loading = $editor?.storage.fileUpload.loading
onMount(() => {
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints}))
editor = createEditor(getEditorOptions({submit, getPubkeyHints}))
})
</script>
<form class="column gap-4" on:submit|preventDefault={startSubmit}>
<form class="column gap-4" on:submit|preventDefault={submit}>
<ModalHeader>
<div slot="title">Create an Event</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">
<EditorContent editor={$editor} />
</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}
<span class="loading loading-spinner loading-xs"></span>
{:else}
@@ -13,6 +13,7 @@
import {pubkeyLink} from "@app/state"
export let value: string[]
export let autofocus = false
let term = ""
let input: Element
@@ -59,7 +60,9 @@
</div>
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}>
<Icon icon="magnifer" />
<!-- svelte-ignore a11y-autofocus -->
<input
{autofocus}
class="grow"
type="text"
placeholder="Search for profiles..."
+8 -15
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {createEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app"
@@ -13,19 +12,16 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushToast} from "@app/toast"
import {THREAD, GENERAL, tagRoom} from "@app/state"
import {getPubkeyHints} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
import {getEditorOptions, getEditorTags} from "@lib/editor"
export let url
const startSubmit = () => uploadFiles($editor)
const back = () => history.back()
const loading = writable(false)
const submit = () => {
if ($loading) return
if (!title) {
return pushToast({
theme: "error",
@@ -55,19 +51,16 @@
let title: string
let editor: Readable<Editor>
$: loading = $editor?.storage.fileUpload.loading
onMount(() => {
editor = createEditor(
getEditorOptions({
submit,
loading,
getPubkeyHints,
placeholder: "What's on your mind?",
}),
getEditorOptions({submit, getPubkeyHints, placeholder: "What's on your mind?"}),
)
})
</script>
<form class="column gap-4" on:submit|preventDefault={startSubmit}>
<form class="column gap-4" on:submit|preventDefault={submit}>
<ModalHeader>
<div slot="title">Create a Thread</div>
<div slot="info">Share a link, or start a discussion.</div>
@@ -94,7 +87,7 @@
<Button
data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2"
on:click={() => addFile($editor)}>
on:click={$editor.commands.selectFiles}>
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
+8 -9
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {append} from "@welshman/lib"
import {isMobile} from "@lib/html"
@@ -9,7 +8,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.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 {tagRoom, GENERAL} from "@app/state"
import {pushToast} from "@app/toast"
@@ -19,11 +18,9 @@
export let onClose
export let onSubmit
const startSubmit = () => uploadFiles($editor)
const loading = writable(false)
const submit = () => {
if ($loading) return
const content = $editor.getText({blockSeparator: "\n"})
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
@@ -39,15 +36,17 @@
let editor: Readable<Editor>
$: loading = $editor?.storage.fileUpload.loading
onMount(() => {
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints, autofocus: !isMobile}))
editor = createEditor(getEditorOptions({submit, getPubkeyHints, autofocus: !isMobile}))
})
</script>
<form
in:fly
out:slideAndFade
on:submit|preventDefault={startSubmit}
on:submit|preventDefault={submit}
class="card2 sticky bottom-2 z-feature mx-2 mt-2 bg-neutral">
<div class="relative">
<div class="note-editor flex-grow overflow-hidden">
@@ -56,7 +55,7 @@
<Button
data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2"
on:click={() => addFile($editor)}>
on:click={$editor.commands.selectFiles}>
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
+16 -3
View File
@@ -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[]) =>
sortBy(
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[]) =>
derived(deriveEvents(repository, {filters}), $events =>
sortBy(
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
hide_sensitive: boolean
send_delay: number
upload_type: "nip96" | "blossom"
nip96_urls: string[]
blossom_urls: string[]
}
}
@@ -282,6 +288,9 @@ export const defaultSettings = {
show_media: true,
hide_sensitive: true,
send_delay: 3000,
upload_type: "nip96",
nip96_urls: ["https://nostr.build"],
blossom_urls: ["https://cdn.satellite.earth"],
}
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(
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
+5 -1
View File
@@ -9,6 +9,10 @@
</script>
<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}
</NodeViewWrapper>
+5 -1
View File
@@ -7,6 +7,10 @@
</script>
<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}
</NodeViewWrapper>
+395
View File
@@ -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
View File
@@ -1,4 +1,3 @@
import type {Writable} from "svelte/store"
import {nprofileEncode} from "nostr-tools/nip19"
import {SvelteNodeViewRenderer} from "svelte-tiptap"
import Placeholder from "@tiptap/extension-placeholder"
@@ -19,10 +18,10 @@ import {
ImageExtension,
VideoExtension,
TagExtension,
FileUploadExtension,
} from "nostr-editor"
import type {StampedEvent} from "@welshman/util"
import {signer, profileSearch} from "@welshman/app"
import {FileUploadExtension} from "./FileUpload"
import {createSuggestions} from "./Suggestions"
import {LinkExtension} from "./LinkExtension"
import EditMention from "./EditMention.svelte"
@@ -33,7 +32,8 @@ 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"
import {asInline} from "./util"
import {getSetting} from "@app/state"
export {
createSuggestions,
@@ -49,41 +49,28 @@ export {
}
export * from "./util"
type UploadType = "nip96" | "blossom"
type EditorOptions = {
submit: () => void
loading: Writable<boolean>
getPubkeyHints: (pubkey: string) => string[]
submitOnEnter?: boolean
placeholder?: string
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 = ({
submit,
loading,
getPubkeyHints,
submitOnEnter,
placeholder = "",
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) => ({
autofocus,
content: "",
@@ -98,7 +85,29 @@ export const getEditorOptions = ({
Text,
TagExtension,
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)}),
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
NProfileExtension.extend({
@@ -126,21 +135,14 @@ export const getEditorOptions = ({
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
ImageExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
).configure({defaultUploadUrl, defaultUploadType: uploadType}),
VideoExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
).configure({defaultUploadUrl, defaultUploadType: uploadType}),
FileUploadExtension.configure({
immediateUpload: false,
sign: (event: StampedEvent) => {
loading.set(true)
return signer.get()!.sign(event)
},
onComplete: () => {
loading.set(false)
submit()
},
immediateUpload: true,
allowedMimeTypes: ["image/*", "video/*"],
sign: (event: StampedEvent) => signer.get()!.sign(event),
}),
],
})
-4
View File
@@ -92,7 +92,3 @@ export const getEditorTags = (editor: Editor) => {
return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags]
}
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
export const uploadFiles = (editor: Editor) => editor.chain().uploadFiles().run()
+3
View File
@@ -5,6 +5,7 @@
import {get, derived} from "svelte/store"
import {page} from "$app/stores"
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 type {TrustedEvent} from "@welshman/util"
import {
@@ -76,6 +77,8 @@
Object.assign(window, {
get,
nip19,
bytesToHex,
hexToBytes,
...lib,
...welshmanSigner,
...util,
+1 -8
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {page} from "$app/stores"
import {onDestroy} from "svelte"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.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">
<label class="input input-bordered flex flex-grow items-center gap-2">
<Icon icon="magnifer" />
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus={!isMobile}
bind:value={term}
class="grow"
type="text"
placeholder="Search for conversations..." />
<input bind:value={term} class="grow" type="text" placeholder="Search for conversations..." />
</label>
<Button class="btn btn-primary" on:click={startChat}>
<Icon icon="add-circle" />
+12 -6
View File
@@ -61,22 +61,28 @@
{@const {software, version, supported_nips, limitation} = $relay.profile}
<div class="flex flex-wrap gap-1">
{#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 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 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 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 software}
<p class="badge badge-neutral">Software: {software}</p>
<p class="badge badge-neutral"><span class="ellipsize">Software: {software}</span></p>
{/if}
{#if version}
<p class="badge badge-neutral">Version: {version}</p>
<p class="badge badge-neutral"><span class="ellipsize">Version: {version}</span></p>
{/if}
</div>
{/if}
+9
View File
@@ -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