This commit is contained in:
Jon Staab
2024-08-26 14:43:43 -07:00
parent 644c32dd09
commit 88318e9753
36 changed files with 370 additions and 311 deletions

View File

@@ -43,11 +43,11 @@
} }
.card2 { .card2 {
@apply p-6 bg-base-100 text-base-content rounded-box; @apply rounded-box bg-base-100 p-6 text-base-content;
} }
.card2.card2-sm { .card2.card2-sm {
@apply p-4 bg-base-100 text-base-content rounded-box; @apply rounded-box bg-base-100 p-4 text-base-content;
} }
.card2.card2-alt { .card2.card2-alt {
@@ -93,11 +93,11 @@
/* tiptap */ /* tiptap */
.tiptap { .tiptap {
@apply rounded-box bg-base-300 px-4 p-2 max-h-[350px] overflow-y-auto; @apply max-h-[350px] overflow-y-auto rounded-box bg-base-300 p-2 px-4;
} }
.tiptap pre code { .tiptap pre code {
@apply link-content w-full block; @apply link-content block w-full;
} }
.tiptap p code { .tiptap p code {
@@ -105,7 +105,7 @@
} }
.link-content { .link-content {
@apply text-sm rounded px-1 bg-neutral text-neutral-content no-underline; @apply rounded bg-neutral px-1 text-sm text-neutral-content no-underline;
} }
.link-content.link-content-selected { .link-content.link-content-selected {

View File

@@ -2,21 +2,15 @@ import {uniqBy, uniq, now, choice} from "@welshman/lib"
import { import {
GROUPS, GROUPS,
GROUP_JOIN, GROUP_JOIN,
asDecryptedEvent,
getGroupTags, getGroupTags,
getRelayTagValues, getRelayTagValues,
readList,
editList,
makeList,
createList,
createEvent, createEvent,
displayProfile, displayProfile,
} from "@welshman/util" } from "@welshman/util"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {pk, signer, repository, INDEXER_RELAYS} from "@app/base" import {pk, repository, INDEXER_RELAYS} from "@app/base"
import { import {
loadOne, loadOne,
subscribe,
getWriteRelayUrls, getWriteRelayUrls,
loadGroup, loadGroup,
loadGroupMembership, loadGroupMembership,
@@ -27,7 +21,6 @@ import {
loadRelaySelections, loadRelaySelections,
makeThunk, makeThunk,
publishThunk, publishThunk,
ensurePlaintext,
getProfilesByPubkey, getProfilesByPubkey,
} from "@app/state" } from "@app/state"
@@ -48,11 +41,18 @@ export const getPubkeyPetname = (pubkey: string) => {
return display return display
} }
export const makeMention = (pubkey: string, hints?: string[]) => export const makeMention = (pubkey: string, hints?: string[]) => [
["p", pubkey, choice(hints || getPubkeyHints(pubkey)), getPubkeyPetname(pubkey)] "p",
pubkey,
choice(hints || getPubkeyHints(pubkey)),
getPubkeyPetname(pubkey),
]
export const makeIMeta = (url: string, data: Record<string, string>) => export const makeIMeta = (url: string, data: Record<string, string>) => [
["imeta", `url ${url}`, ...Object.entries(data).map(([k, v]) => [k, v].join(' '))] "imeta",
`url ${url}`,
...Object.entries(data).map(([k, v]) => [k, v].join(" ")),
]
// Loaders // Loaders
@@ -83,7 +83,6 @@ export type ModifyTags = (tags: string[][]) => string[][]
export const updateList = async (kind: number, modifyTags: ModifyTags) => { export const updateList = async (kind: number, modifyTags: ModifyTags) => {
const $pk = pk.get()! const $pk = pk.get()!
const $signer = signer.get()!
const [prev] = repository.query([{kinds: [kind], authors: [$pk]}]) const [prev] = repository.query([{kinds: [kind], authors: [$pk]}])
const relays = getWriteRelayUrls(getRelaySelectionsByPubkey().get($pk)) const relays = getWriteRelayUrls(getRelaySelectionsByPubkey().get($pk))
@@ -103,15 +102,18 @@ export const removeGroupMemberships = (noms: string[]) =>
export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => { export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => {
const relays = [url] const relays = [url]
const filters = [{kinds: [9000], '#h': [nom], '#p': [pk.get()!], since: now() - 30}] const filters = [{kinds: [9000], "#h": [nom], "#p": [pk.get()!], since: now() - 30}]
const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]}) const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]})
const statusData = await publishThunk(makeThunk({event, relays})) const statusData = await publishThunk(makeThunk({event, relays}))
const {status, message} = statusData[url] const {status, message} = statusData[url]
if (message.includes('already a member')) return [true, ""] if (message.includes("already a member")) return [true, ""]
if (status !== PublishStatus.Success) return [false, message] if (status !== PublishStatus.Success) return [false, message]
if (await loadOne({filters, relays})) return [true, ""] if (await loadOne({filters, relays})) return [true, ""]
return [false, "Your request was not automatically approved, but may be approved manually later. Please try again later or contact the group admin."] return [
false,
"Your request was not automatically approved, but may be approved manually later. Please try again later or contact the group admin.",
]
} }

View File

@@ -1,45 +1,57 @@
<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 {nprofileEncode} from 'nostr-tools/nip19' import {nprofileEncode} from "nostr-tools/nip19"
import {createEditor, type Editor, EditorContent, SvelteNodeViewRenderer} from 'svelte-tiptap' import {createEditor, type Editor, EditorContent, SvelteNodeViewRenderer} from "svelte-tiptap"
import {Extension} from '@tiptap/core' import Code from "@tiptap/extension-code"
import Code from '@tiptap/extension-code' import CodeBlock from "@tiptap/extension-code-block"
import CodeBlock from '@tiptap/extension-code-block' import Document from "@tiptap/extension-document"
import Document from '@tiptap/extension-document' import Dropcursor from "@tiptap/extension-dropcursor"
import Dropcursor from '@tiptap/extension-dropcursor' import Gapcursor from "@tiptap/extension-gapcursor"
import Gapcursor from '@tiptap/extension-gapcursor' import History from "@tiptap/extension-history"
import History from '@tiptap/extension-history' import Paragraph from "@tiptap/extension-paragraph"
import Paragraph from '@tiptap/extension-paragraph' import Text from "@tiptap/extension-text"
import Text from '@tiptap/extension-text' import HardBreakExtension from "@tiptap/extension-hard-break"
import HardBreakExtension from '@tiptap/extension-hard-break' import {
import {Bolt11Extension, NProfileExtension, NEventExtension, NAddrExtension, ImageExtension, VideoExtension, FileUploadExtension} from 'nostr-editor' Bolt11Extension,
import type {StampedEvent} from '@welshman/util' NProfileExtension,
import {createEvent, CHAT_MESSAGE} from '@welshman/util' NEventExtension,
import {LinkExtension, TopicExtension, createSuggestions, findNodes} from '@lib/tiptap' NAddrExtension,
import Icon from '@lib/components/Icon.svelte' ImageExtension,
import Button from '@lib/components/Button.svelte' VideoExtension,
import GroupComposeMention from '@app/components/GroupComposeMention.svelte' FileUploadExtension,
import GroupComposeTopic from '@app/components/GroupComposeTopic.svelte' } from "nostr-editor"
import GroupComposeEvent from '@app/components/GroupComposeEvent.svelte' import type {StampedEvent} from "@welshman/util"
import GroupComposeImage from '@app/components/GroupComposeImage.svelte' import {createEvent, CHAT_MESSAGE} from "@welshman/util"
import GroupComposeBolt11 from '@app/components/GroupComposeBolt11.svelte' import {LinkExtension, TopicExtension, createSuggestions, findNodes} from "@lib/tiptap"
import GroupComposeVideo from '@app/components/GroupComposeVideo.svelte' import Icon from "@lib/components/Icon.svelte"
import GroupComposeLink from '@app/components/GroupComposeLink.svelte' import Button from "@lib/components/Button.svelte"
import GroupComposeSuggestions from '@app/components/GroupComposeSuggestions.svelte' import GroupComposeMention from "@app/components/GroupComposeMention.svelte"
import GroupComposeTopicSuggestion from '@app/components/GroupComposeTopicSuggestion.svelte' import GroupComposeTopic from "@app/components/GroupComposeTopic.svelte"
import GroupComposeProfileSuggestion from '@app/components/GroupComposeProfileSuggestion.svelte' import GroupComposeEvent from "@app/components/GroupComposeEvent.svelte"
import {signer, INDEXER_RELAYS} from '@app/base' import GroupComposeImage from "@app/components/GroupComposeImage.svelte"
import {searchProfiles, publishThunk, makeThunk, searchTopics, userRelayUrlsByNom, getWriteRelayUrls, displayProfileByPubkey, getRelaySelectionsByPubkey} from '@app/state' import GroupComposeBolt11 from "@app/components/GroupComposeBolt11.svelte"
import {getPubkeyHints, makeMention, makeIMeta} from '@app/commands' import GroupComposeVideo from "@app/components/GroupComposeVideo.svelte"
import GroupComposeLink from "@app/components/GroupComposeLink.svelte"
import GroupComposeSuggestions from "@app/components/GroupComposeSuggestions.svelte"
import GroupComposeTopicSuggestion from "@app/components/GroupComposeTopicSuggestion.svelte"
import GroupComposeProfileSuggestion from "@app/components/GroupComposeProfileSuggestion.svelte"
import {signer} from "@app/base"
import {
searchProfiles,
publishThunk,
makeThunk,
searchTopics,
userRelayUrlsByNom,
} from "@app/state"
import {getPubkeyHints, makeMention, makeIMeta} from "@app/commands"
export let nom export let nom
let editor: Readable<Editor> let editor: Readable<Editor>
let uploading = false let uploading = false
const asInline = (extend: Record<string, any>) => const asInline = (extend: Record<string, any>) => ({inline: true, group: "inline", ...extend})
({inline: true, group: 'inline', ...extend})
const addFile = () => $editor.chain().selectFiles().run() const addFile = () => $editor.chain().selectFiles().run()
@@ -53,8 +65,12 @@
tags: [ tags: [
["h", nom], ["h", nom],
...findNodes(TopicExtension.name, json).map(t => ["t", t.attrs!.name.toLowerCase()]), ...findNodes(TopicExtension.name, json).map(t => ["t", t.attrs!.name.toLowerCase()]),
...findNodes(NProfileExtension.name, json).map(m => makeMention(m.attrs!.pubkey, m.attrs!.relays)), ...findNodes(NProfileExtension.name, json).map(m =>
...findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) => makeIMeta(src, {x, ox: x})), makeMention(m.attrs!.pubkey, m.attrs!.relays),
),
...findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) =>
makeIMeta(src, {x, ox: x}),
),
], ],
}) })
@@ -78,9 +94,9 @@
HardBreakExtension.extend({ HardBreakExtension.extend({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
'Shift-Enter': () => this.editor.commands.setHardBreak(), "Shift-Enter": () => this.editor.commands.setHardBreak(),
'Mod-Enter': () => this.editor.commands.setHardBreak(), "Mod-Enter": () => this.editor.commands.setHardBreak(),
'Enter': () => { Enter: () => {
if (this.editor.getText().trim()) { if (this.editor.getText().trim()) {
uploadFiles() uploadFiles()
@@ -90,19 +106,21 @@
return false return false
}, },
} }
} },
}), }),
LinkExtension.extend({ LinkExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(GroupComposeLink), addNodeView: () => SvelteNodeViewRenderer(GroupComposeLink),
}), }),
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)})), Bolt11Extension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeBolt11)}),
),
NProfileExtension.extend({ NProfileExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(GroupComposeMention), addNodeView: () => SvelteNodeViewRenderer(GroupComposeMention),
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
createSuggestions({ createSuggestions({
char: '@', char: "@",
name: 'nprofile', name: "nprofile",
editor: this.editor, editor: this.editor,
search: searchProfiles, search: searchProfiles,
select: (pubkey: string, props: any) => { select: (pubkey: string, props: any) => {
@@ -115,23 +133,27 @@
suggestionsComponent: GroupComposeSuggestions, suggestionsComponent: GroupComposeSuggestions,
}), }),
] ]
} },
}), }),
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)})), NEventExtension.extend(
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)})), asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)}),
ImageExtension ),
.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeImage)})) NAddrExtension.extend(
.configure({defaultUploadUrl: 'https://nostr.build', defaultUploadType: 'nip96'}), asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)}),
VideoExtension ),
.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)})) ImageExtension.extend(
.configure({defaultUploadUrl: 'https://nostr.build', defaultUploadType: 'nip96'}), asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeImage)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
VideoExtension.extend(
asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)}),
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
TopicExtension.extend({ TopicExtension.extend({
addNodeView: () => SvelteNodeViewRenderer(GroupComposeTopic), addNodeView: () => SvelteNodeViewRenderer(GroupComposeTopic),
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
createSuggestions({ createSuggestions({
char: '#', char: "#",
name: 'topic', name: "topic",
editor: this.editor, editor: this.editor,
search: searchTopics, search: searchTopics,
select: (name: string, props: any) => props.command({name}), select: (name: string, props: any) => props.command({name}),
@@ -155,13 +177,16 @@
}, },
}), }),
], ],
content: '', content: "",
}) })
}) })
</script> </script>
<div class="flex gap-2 relative z-feature border-t border-solid border-base-100 p-2 shadow-top-xl bg-base-100"> <div
<Button on:click={addFile} class="bg-base-300 rounded-box w-10 h-10 center hover:bg-base-200 transition-colors"> class="shadow-top-xl relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100 p-2">
<Button
on:click={addFile}
class="center h-10 w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200">
{#if uploading} {#if uploading}
<span class="loading loading-spinner loading-xs"></span> <span class="loading loading-spinner loading-xs"></span>
{:else} {:else}

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import type {NodeViewProps} from '@tiptap/core' import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from 'svelte-tiptap' import {NodeViewWrapper} from "svelte-tiptap"
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 {clip} from '@app/toast' import {clip} from "@app/toast"
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
export let selected: NodeViewProps['selected'] export let selected: NodeViewProps["selected"]
const copy = () => clip(node.attrs.lnbc) const copy = () => clip(node.attrs.lnbc)
</script> </script>
<NodeViewWrapper class="inline"> <NodeViewWrapper class="inline">
<Button on:click={copy} class={cx("link-content", {'link-content-selected': selected})}> <Button on:click={copy} class={cx("link-content", {"link-content-selected": selected})}>
<Icon icon="bolt" size={3} class="inline-block translate-y-px" /> <Icon icon="bolt" size={3} class="inline-block translate-y-px" />
{node.attrs.lnbc.slice(0, 16)}... {node.attrs.lnbc.slice(0, 16)}...
</Button> </Button>

View File

@@ -1,15 +1,17 @@
<script lang="ts"> <script lang="ts">
import type {NodeViewProps} from '@tiptap/core' import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from 'svelte-tiptap' import {NodeViewWrapper} from "svelte-tiptap"
import {ellipsize} from '@welshman/lib' import {ellipsize} from "@welshman/lib"
import {type TrustedEvent, fromNostrURI, Address} from '@welshman/util' import {type TrustedEvent, fromNostrURI, Address} from "@welshman/util"
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
import {deriveEvent} from '@app/state' import {deriveEvent} from "@app/state"
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
const displayEvent = (e: TrustedEvent) => const displayEvent = (e: TrustedEvent) =>
e?.content.length > 1 ? ellipsize(e.content, 50) : fromNostrURI(nevent || naddr).slice(0, 16) + '...' e?.content.length > 1
? ellipsize(e.content, 50)
: fromNostrURI(nevent || naddr).slice(0, 16) + "..."
$: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs) $: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs)
$: event = deriveEvent(id || new Address(kind, pubkey, identifier).toString(), relays) $: event = deriveEvent(id || new Address(kind, pubkey, identifier).toString(), relays)

View File

@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import type {NodeViewProps} from '@tiptap/core' import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from 'svelte-tiptap' import {NodeViewWrapper} from "svelte-tiptap"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
export let selected: NodeViewProps['selected'] export let selected: NodeViewProps["selected"]
</script> </script>
<NodeViewWrapper class={cx("inline link-content", {'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" /> <Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
{node.attrs.file.name} {node.attrs.file.name}
</NodeViewWrapper> </NodeViewWrapper>

View File

@@ -1,19 +1,21 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import type {NodeViewProps} from '@tiptap/core' import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from 'svelte-tiptap' import {NodeViewWrapper} from "svelte-tiptap"
import {displayUrl} from '@welshman/lib' import {displayUrl} from "@welshman/lib"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
export let selected: NodeViewProps['selected'] export let selected: NodeViewProps["selected"]
</script> </script>
<NodeViewWrapper class="inline"> <NodeViewWrapper class="inline">
<Link external href={node.attrs.url} class={cx("link-content", {'link-content-selected': selected})}> <Link
external
href={node.attrs.url}
class={cx("link-content", {"link-content-selected": selected})}>
<Icon icon="link-round" size={3} class="inline-block translate-y-px" /> <Icon icon="link-round" size={3} class="inline-block translate-y-px" />
{displayUrl(node.attrs.url)} {displayUrl(node.attrs.url)}
</Link> </Link>
</NodeViewWrapper> </NodeViewWrapper>

View File

@@ -1,19 +1,22 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import type {NodeViewProps} from '@tiptap/core' import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from 'svelte-tiptap' import {NodeViewWrapper} from "svelte-tiptap"
import {displayProfile} from '@welshman/util' import {displayProfile} from "@welshman/util"
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
import {deriveProfile} from '@app/state' import {deriveProfile} from "@app/state"
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
export let selected: NodeViewProps['selected'] export let selected: NodeViewProps["selected"]
$: profile = deriveProfile(node.attrs.pubkey, node.attrs.relays) $: profile = deriveProfile(node.attrs.pubkey, node.attrs.relays)
</script> </script>
<NodeViewWrapper class="inline"> <NodeViewWrapper class="inline">
<Link external href="https://njump.me/{node.attrs.nprofile}" class={cx("link-content", {'link-content-selected': selected})}> <Link
external
href="https://njump.me/{node.attrs.nprofile}"
class={cx("link-content", {"link-content-selected": selected})}>
@{displayProfile($profile)} @{displayProfile($profile)}
</Link> </Link>
</NodeViewWrapper> </NodeViewWrapper>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {deriveProfileDisplay} from '@app/state' import {deriveProfileDisplay} from "@app/state"
export let value export let value

View File

@@ -4,7 +4,7 @@
import {throttle} from "throttle-debounce" import {throttle} from "throttle-debounce"
import {slide} from "svelte/transition" import {slide} from "svelte/transition"
import {clamp} from "@welshman/lib" import {clamp} from "@welshman/lib"
import {theme} from '@app/theme' import {theme} from "@app/theme"
export let term export let term
export let search export let search
@@ -23,13 +23,13 @@
items = $search.searchValues(term).slice(0, 30) items = $search.searchValues(term).slice(0, 30)
}) })
const setIndex = (newIndex: number, block: ScrollLogicalPosition) => { const setIndex = (newIndex: number, block: any) => {
index = clamp([0, items.length - 1], newIndex) index = clamp([0, items.length - 1], newIndex)
element.querySelector(`button:nth-child(${index})`)?.scrollIntoView({block}) element.querySelector(`button:nth-child(${index})`)?.scrollIntoView({block})
} }
export const onKeyDown = (e: any) => { export const onKeyDown = (e: any) => {
if (['Enter', 'Tab'].includes(e.code)) { if (["Enter", "Tab"].includes(e.code)) {
const value = items[index] const value = items[index]
if (value) { if (value) {
@@ -41,7 +41,7 @@
} }
} }
if (e.code === 'Space' && term && allowCreate) { if (e.code === "Space" && term && allowCreate) {
select(term) select(term)
return true return true
} }
@@ -68,7 +68,7 @@
class="mt-2 flex max-h-[350px] flex-col overflow-y-auto overflow-x-hidden shadow-xl"> class="mt-2 flex max-h-[350px] flex-col overflow-y-auto overflow-x-hidden shadow-xl">
{#if term && allowCreate} {#if term && allowCreate}
<button <button
class="cursor-pointer px-4 py-2 text-left hover:bg-primary hover:text-primary-content transition-colors white-space-nowrap overflow-hidden text-ellipsis min-w-0" class="white-space-nowrap min-w-0 cursor-pointer overflow-hidden text-ellipsis px-4 py-2 text-left transition-colors hover:bg-primary hover:text-primary-content"
on:mousedown|preventDefault on:mousedown|preventDefault
on:click|preventDefault={() => select(term)}> on:click|preventDefault={() => select(term)}>
Use "<svelte:component this={component} value={term} />" Use "<svelte:component this={component} value={term} />"
@@ -76,7 +76,7 @@
{/if} {/if}
{#each items as value, i (value)} {#each items as value, i (value)}
<button <button
class="cursor-pointer px-4 py-2 text-left hover:bg-primary hover:text-primary-content transition-colors white-space-nowrap overflow-hidden text-ellipsis min-w-0" class="white-space-nowrap min-w-0 cursor-pointer overflow-hidden text-ellipsis px-4 py-2 text-left transition-colors hover:bg-primary hover:text-primary-content"
class:bg-primary={index === i} class:bg-primary={index === i}
class:text-primary-content={index === i} class:text-primary-content={index === i}
on:mousedown|preventDefault on:mousedown|preventDefault
@@ -86,7 +86,7 @@
{/each} {/each}
</div> </div>
{#if loading} {#if loading}
<div transition:slide|local class="flex gap-2 bg-tinted-700 px-4 py-2 text-neutral-200"> <div transition:slide|local class="bg-tinted-700 flex gap-2 px-4 py-2 text-neutral-200">
<div> <div>
<i class="fa fa-circle-notch fa-spin" /> <i class="fa fa-circle-notch fa-spin" />
</div> </div>

View File

@@ -1,15 +1,18 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import type {NodeViewProps} from '@tiptap/core' import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from 'svelte-tiptap' import {NodeViewWrapper} from "svelte-tiptap"
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
export let selected: NodeViewProps['selected'] export let selected: NodeViewProps["selected"]
</script> </script>
<NodeViewWrapper class="inline"> <NodeViewWrapper class="inline">
<Link external href="https://coracle.social/topics/{node.attrs.name.toLowerCase()}" class={cx("link-content", {'link-content-selected': selected})}> <Link
external
href="https://coracle.social/topics/{node.attrs.name.toLowerCase()}"
class={cx("link-content", {"link-content-selected": selected})}>
#{node.attrs.name} #{node.attrs.name}
</Link> </Link>
</NodeViewWrapper> </NodeViewWrapper>

View File

@@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import type {NodeViewProps} from "@tiptap/core"
import type {NodeViewProps} from '@tiptap/core' import {NodeViewWrapper} from "svelte-tiptap"
import {NodeViewWrapper} from 'svelte-tiptap' import Icon from "@lib/components/Icon.svelte"
import Icon from '@lib/components/Icon.svelte'
export let node: NodeViewProps['node'] export let node: NodeViewProps["node"]
</script> </script>
<NodeViewWrapper class="inline link-content"> <NodeViewWrapper class="link-content inline">
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" /> <Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
{node.attrs.file.name} {node.attrs.file.name}
</NodeViewWrapper> </NodeViewWrapper>

View File

@@ -5,13 +5,19 @@
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store" import {deriveEvents} from "@welshman/store"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {GROUP_REPLY, REACTION, ZAP_RESPONSE, displayRelayUrl, getAncestorTags, displayPubkey} from "@welshman/util" import {
import {fly, fade} from "@lib/transition" GROUP_REPLY,
import {formatTimestampAsTime} from '@lib/util' REACTION,
ZAP_RESPONSE,
displayRelayUrl,
getAncestorTags,
} from "@welshman/util"
import {fly} from "@lib/transition"
import {formatTimestampAsTime} from "@lib/util"
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 Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import {repository} from '@app/base' import {repository} from "@app/base"
import type {PublishStatusData} from "@app/state" import type {PublishStatusData} from "@app/state"
import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state" import {deriveProfile, deriveProfileDisplay, deriveEvent, publishStatusData} from "@app/state"
@@ -19,31 +25,31 @@
export let showPubkey: boolean export let showPubkey: boolean
const colors = [ const colors = [
['amber', twColors.amber[600]], ["amber", twColors.amber[600]],
['blue', twColors.blue[600]], ["blue", twColors.blue[600]],
['cyan', twColors.cyan[600]], ["cyan", twColors.cyan[600]],
['emerald', twColors.emerald[600]], ["emerald", twColors.emerald[600]],
['fuchsia', twColors.fuchsia[600]], ["fuchsia", twColors.fuchsia[600]],
['green', twColors.green[600]], ["green", twColors.green[600]],
['indigo', twColors.indigo[600]], ["indigo", twColors.indigo[600]],
['sky', twColors.sky[600]], ["sky", twColors.sky[600]],
['lime', twColors.lime[600]], ["lime", twColors.lime[600]],
['orange', twColors.orange[600]], ["orange", twColors.orange[600]],
['pink', twColors.pink[600]], ["pink", twColors.pink[600]],
['purple', twColors.purple[600]], ["purple", twColors.purple[600]],
['red', twColors.red[600]], ["red", twColors.red[600]],
['rose', twColors.rose[600]], ["rose", twColors.rose[600]],
['sky', twColors.sky[600]], ["sky", twColors.sky[600]],
['teal', twColors.teal[600]], ["teal", twColors.teal[600]],
['violet', twColors.violet[600]], ["violet", twColors.violet[600]],
['yellow', twColors.yellow[600]], ["yellow", twColors.yellow[600]],
['zinc', twColors.zinc[600]], ["zinc", twColors.zinc[600]],
] ]
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], '#e': [event.id]}]}) const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
const zaps = deriveEvents(repository, {filters: [{kinds: [ZAP_RESPONSE], '#e': [event.id]}]}) const zaps = deriveEvents(repository, {filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]})
const {replies} = getAncestorTags(event.tags) const {replies} = getAncestorTags(event.tags)
const parentId = replies[0]?.[1] const parentId = replies[0]?.[1]
const parentHints = [replies[0]?.[2]].filter(Boolean) const parentHints = [replies[0]?.[2]].filter(Boolean)
@@ -52,8 +58,8 @@
const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {})) const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {}))
const displayReaction = (content: string) => { const displayReaction = (content: string) => {
if (content === '+') return "❤️" if (content === "+") return "❤️"
if (content === '-') return "👎" if (content === "-") return "👎"
return content return content
} }
@@ -65,7 +71,8 @@
$: parentProfileDisplay = deriveProfileDisplay(parentPubkey) $: parentProfileDisplay = deriveProfileDisplay(parentPubkey)
$: isPublished = findStatus($ps, [PublishStatus.Success]) $: isPublished = findStatus($ps, [PublishStatus.Success])
$: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30 $: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
$: failure = !isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout]) $: failure =
!isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
</script> </script>
<div in:fly class="group relative flex flex-col gap-1 p-2 transition-colors hover:bg-base-300"> <div in:fly class="group relative flex flex-col gap-1 p-2 transition-colors hover:bg-base-300">
@@ -86,25 +93,26 @@
{#if showPubkey} {#if showPubkey}
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} /> <Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
{:else} {:else}
<div class="min-w-10 max-w-10 w-10" /> <div class="w-10 min-w-10 max-w-10" />
{/if} {/if}
<div class="-mt-1"> <div class="-mt-1">
{#if showPubkey} {#if showPubkey}
<div class="flex gap-2 items-center"> <div class="flex items-center gap-2">
<strong class="text-sm" style="color: {colorValue}" data-color={colorName}>{$profileDisplay}</strong> <strong class="text-sm" style="color: {colorValue}" data-color={colorName}
<span class="opacity-50 text-xs">{formatTimestampAsTime(event.created_at)}</span> >{$profileDisplay}</strong>
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
</div> </div>
{/if} {/if}
<p class="text-sm"> <p class="text-sm">
{event.content} {event.content}
{#if isPending} {#if isPending}
<span class="ml-1 flex-inline gap-1"> <span class="flex-inline ml-1 gap-1">
<span class="loading loading-spinner h-3 w-3 mx-1 translate-y-px" /> <span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
<span class="opacity-50">Sending...</span> <span class="opacity-50">Sending...</span>
</span> </span>
{:else if failure} {:else if failure}
<span <span
class="ml-1 flex-inline gap-1 tooltip cursor-pointer" class="flex-inline tooltip ml-1 cursor-pointer gap-1"
data-tip="{failure.message} ({displayRelayUrl(failure.url)})"> data-tip="{failure.message} ({displayRelayUrl(failure.url)})">
<Icon icon="danger" class="translate-y-px" size={3} /> <Icon icon="danger" class="translate-y-px" size={3} />
<span class="opacity-50">Failed to send!</span> <span class="opacity-50">Failed to send!</span>
@@ -114,9 +122,9 @@
</div> </div>
</div> </div>
{#if $reactions.length > 0 || $zaps.length > 0} {#if $reactions.length > 0 || $zaps.length > 0}
<div class="text-xs ml-12"> <div class="ml-12 text-xs">
{#each groupBy(e => e.content, $reactions).entries() as [content, events]} {#each groupBy(e => e.content, $reactions).entries() as [content, events]}
<Button class="btn btn-neutral btn-xs rounded-full mr-2 flex-inline gap-1"> <Button class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full">
<span>{displayReaction(content)}</span> <span>{displayReaction(content)}</span>
{#if events.length > 1} {#if events.length > 1}
<span>{events.length}</span> <span>{events.length}</span>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl} from '@welshman/util' import {displayRelayUrl} from "@welshman/util"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {first} from '@welshman/lib'
import {makeSecret, getNip07, Nip46Broker} from "@welshman/signer" import {makeSecret, getNip07, Nip46Broker} from "@welshman/signer"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
@@ -26,10 +25,10 @@
} }
} }
const onSuccess = async (session: Session) => { const onSuccess = async (session: Session, relays: string[] = []) => {
addSession(session) addSession(session)
await loadUserData(session.pubkey) await loadUserData(session.pubkey, relays)
pushToast({message: "Successfully logged in!"}) pushToast({message: "Successfully logged in!"})
clearModal() clearModal()
@@ -50,7 +49,7 @@
const broker = Nip46Broker.get(pubkey, secret, handler) const broker = Nip46Broker.get(pubkey, secret, handler)
if (await broker.connect("", nip46Perms)) { if (await broker.connect("", nip46Perms)) {
await onSuccess({method: "nip46", pubkey, secret, handler}) await onSuccess({method: "nip46", pubkey, secret, handler}, relays)
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
@@ -93,7 +92,12 @@
<div class="flex items-center gap-2" slot="input"> <div class="flex items-center gap-2" slot="input">
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="user-rounded" /> <Icon icon="user-rounded" />
<input bind:value={username} disabled={loading} class="grow" type="text" placeholder="username" /> <input
bind:value={username}
disabled={loading}
class="grow"
type="text"
placeholder="username" />
<span>@{handler.domain}</span> <span>@{handler.domain}</span>
</label> </label>
{#if getNip07()} {#if getNip07()}

View File

@@ -37,7 +37,7 @@
} }
</script> </script>
<div class="relative w-14 bg-base-100 flex-shrink-0 pt-4" bind:this={element}> <div class="relative w-14 flex-shrink-0 bg-base-100 pt-4" bind:this={element}>
<div <div
class="absolute z-nav-active ml-2 h-[144px] w-12 bg-base-300" class="absolute z-nav-active ml-2 h-[144px] w-12 bg-base-300"
style={`top: ${$activeOffset}px`} /> style={`top: ${$activeOffset}px`} />

View File

@@ -1,15 +1,12 @@
<script lang="ts"> <script lang="ts">
import {append, remove} from "@welshman/lib" import {append, remove} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import {goto} from "$app/navigation"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import InfoNip29 from "@app/components/InfoNip29.svelte" import InfoNip29 from "@app/components/InfoNip29.svelte"
import {pushModal, clearModal} from "@app/modal" import {pushModal, clearModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import type {PublishStatusData} from "@app/state"
import {deriveGroup, displayGroup, relayUrlsByNom} from "@app/state" import {deriveGroup, displayGroup, relayUrlsByNom} from "@app/state"
import {sendJoinRequest, addGroupMemberships} from "@app/commands" import {sendJoinRequest, addGroupMemberships} from "@app/commands"
@@ -30,7 +27,7 @@
const [ok, message] = await sendJoinRequest(nom, url) const [ok, message] = await sendJoinRequest(nom, url)
if (!ok) { if (!ok) {
return pushToast({theme: 'error', message}) return pushToast({theme: "error", message})
} }
} }

View File

@@ -1,4 +1,4 @@
import {throttle} from 'throttle-debounce' import {throttle} from "throttle-debounce"
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import type {FuseResult} from "fuse.js" import type {FuseResult} from "fuse.js"
import {get, writable, readable, derived} from "svelte/store" import {get, writable, readable, derived} from "svelte/store"
@@ -6,7 +6,6 @@ import type {Maybe} from "@welshman/lib"
import { import {
max, max,
first, first,
append,
between, between,
uniqBy, uniqBy,
groupBy, groupBy,
@@ -40,17 +39,30 @@ import {
displayPubkey, displayPubkey,
GROUP_JOIN, GROUP_JOIN,
GROUP_ADD_USER, GROUP_ADD_USER,
isStampedEvent,
isEventTemplate,
} from "@welshman/util" } from "@welshman/util"
import type {SignedEvent, HashedEvent, EventTemplate, TrustedEvent, PublishedProfile, PublishedList} from "@welshman/util" import type {
import type {SubscribeRequest, PublishRequest} from "@welshman/net" SignedEvent,
HashedEvent,
EventTemplate,
TrustedEvent,
PublishedProfile,
PublishedList,
} from "@welshman/util"
import type {SubscribeRequest} from "@welshman/net"
import {publish as basePublish, subscribe as baseSubscribe, PublishStatus} from "@welshman/net" import {publish as basePublish, subscribe as baseSubscribe, PublishStatus} from "@welshman/net"
import {decrypt, stamp, own, hash} from "@welshman/signer" import {decrypt, stamp, own, hash} from "@welshman/signer"
import {custom, deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store" import {custom, deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
import {createSearch} from "@lib/util" import {createSearch} from "@lib/util"
import type {Handle, Relay} from "@app/types" import type {Handle, Relay} from "@app/types"
import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSession, getSigner, REACTION_KINDS} from "@app/base" import {
INDEXER_RELAYS,
DUFFLEPUD_URL,
repository,
pk,
getSession,
getSigner,
REACTION_KINDS,
} from "@app/base"
// Utils // Utils
@@ -156,10 +168,15 @@ thunkWorker.addGlobalHandler(async ({event, relays, resolve}: ThunkWithResolve)
const {id} = event const {id} = event
const statusByUrl: PublishStatusDataByUrl = {} const statusByUrl: PublishStatusDataByUrl = {}
pub.emitter.on('*', (status: PublishStatus, url: string, message: string) => { pub.emitter.on("*", (status: PublishStatus, url: string, message: string) => {
publishStatusData.update(assoc(id, Object.assign(statusByUrl, {[url]: {id, url, status, message}}))) publishStatusData.update(
assoc(id, Object.assign(statusByUrl, {[url]: {id, url, status, message}})),
)
if (Object.values(statusByUrl).filter(s => s.status !== PublishStatus.Pending).length === relays.length) { if (
Object.values(statusByUrl).filter(s => s.status !== PublishStatus.Pending).length ===
relays.length
) {
resolve(statusByUrl) resolve(statusByUrl)
} }
}) })
@@ -207,8 +224,7 @@ export const load = (request: SubscribeRequest) =>
sub.emitter.on("complete", () => resolve(events)) sub.emitter.on("complete", () => resolve(events))
}) })
export const loadOne = async (request: SubscribeRequest) => export const loadOne = async (request: SubscribeRequest) => first(await load(request))
first(await load(request))
// Publish status // Publish status
@@ -219,7 +235,6 @@ export type PublishStatusData = {
status: PublishStatus status: PublishStatus
} }
export type PublishStatusDataByUrl = Record<string, PublishStatusData> export type PublishStatusDataByUrl = Record<string, PublishStatusData>
export type PublishStatusDataByUrlById = Record<string, PublishStatusDataByUrl> export type PublishStatusDataByUrlById = Record<string, PublishStatusDataByUrl>
@@ -279,7 +294,7 @@ export const topics = custom<Topic[]>(setter => {
const getTopics = () => { const getTopics = () => {
const topics = new Map<string, number>() const topics = new Map<string, number>()
for (const tagString of repository.eventsByTag.keys()) { for (const tagString of repository.eventsByTag.keys()) {
if (tagString.startsWith('t:')) { if (tagString.startsWith("t:")) {
const topic = tagString.slice(2).toLowerCase() const topic = tagString.slice(2).toLowerCase()
topics.set(topic, inc(topics.get(topic))) topics.set(topic, inc(topics.get(topic)))
@@ -310,7 +325,10 @@ export const searchTopics = derived(topics, $topics =>
export const relays = writable<Relay[]>([]) export const relays = writable<Relay[]>([])
export const relaysByPubkey = derived(relays, $relays => export const relaysByPubkey = derived(relays, $relays =>
groupBy(($relay: Relay) => $relay.pubkey, $relays.filter(r => r.pubkey)), groupBy(
($relay: Relay) => $relay.pubkey,
$relays.filter(r => r.pubkey),
),
) )
export const { export const {
@@ -697,7 +715,11 @@ export type GroupMessage = {
export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => { export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => {
const nom = event.tags.find(nthEq(0, "h"))?.[1] const nom = event.tags.find(nthEq(0, "h"))?.[1]
if (!nom || between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind) || REACTION_KINDS.includes(event.kind)) { if (
!nom ||
between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind) ||
REACTION_KINDS.includes(event.kind)
) {
return undefined return undefined
} }
@@ -789,17 +811,14 @@ export const userGroupsByNom = withGetter(
}), }),
) )
export const userRelayUrlsByNom = derived( export const userRelayUrlsByNom = derived(userGroupsByNom, $userGroupsByNom => {
userGroupsByNom, const $userRelayUrlsByNom = new Map()
$userGroupsByNom => {
const $userRelayUrlsByNom = new Map()
for (const [nom, groups] of $userGroupsByNom.entries()) { for (const [nom, groups] of $userGroupsByNom.entries()) {
for (const group of groups) { for (const group of groups) {
pushToMapKey($userRelayUrlsByNom, nom, group.relay.url) pushToMapKey($userRelayUrlsByNom, nom, group.relay.url)
}
} }
return $userRelayUrlsByNom
} }
)
return $userRelayUrlsByNom
})

View File

@@ -6,7 +6,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import {switcher} from "@welshman/lib" import {switcher} from "@welshman/lib"
import AddSquare from "@assets/icons/Add Square.svg?dataurl" import AddSquare from "@assets/icons/Add Square.svg?dataurl"
import Code2 from "@assets/icons/Code 2.svg?dataurl" import Code2 from "@assets/icons/Code 2.svg?dataurl"
@@ -69,8 +69,8 @@
const data = switcher(icon, { const data = switcher(icon, {
"add-square": AddSquare, "add-square": AddSquare,
"code-2": Code2, "code-2": Code2,
"earth": Earth, earth: Earth,
"pen": Pen, pen: Pen,
"headphones-round": HeadphonesRound, "headphones-round": HeadphonesRound,
"add-circle": AddCircle, "add-circle": AddCircle,
"alt-arrow-down": AltArrowDown, "alt-arrow-down": AltArrowDown,
@@ -105,7 +105,7 @@
"menu-dots": MenuDots, "menu-dots": MenuDots,
"notes-minimalistic": NotesMinimalistic, "notes-minimalistic": NotesMinimalistic,
"pallete-2": Pallete2, "pallete-2": Pallete2,
"paperclip": Paperclip, paperclip: Paperclip,
plain: Plain, plain: Plain,
reply: Reply, reply: Reply,
"remote-controller-minimalistic": RemoteControllerMinimalistic, "remote-controller-minimalistic": RemoteControllerMinimalistic,

View File

@@ -69,7 +69,7 @@
on:dragleave|preventDefault|stopPropagation={onDragLeave} on:dragleave|preventDefault|stopPropagation={onDragLeave}
on:drop|preventDefault|stopPropagation={onDrop}> on:drop|preventDefault|stopPropagation={onDrop}>
<div <div
class="absolute right-0 top-0 overflow-hidden rounded-full bg-primary h-5 w-5" class="absolute right-0 top-0 h-5 w-5 overflow-hidden rounded-full bg-primary"
class:bg-error={file} class:bg-error={file}
class:bg-primary={!file}> class:bg-primary={!file}>
{#if file} {#if file}

View File

@@ -1,3 +1,3 @@
<div class="flex w-60 flex-col gap-1 bg-base-300 flex-shrink-0"> <div class="flex w-60 flex-shrink-0 flex-col gap-1 bg-base-300">
<slot /> <slot />
</div> </div>

View File

@@ -36,7 +36,7 @@
on:click on:click
class={cx( class={cx(
$$props.class, $$props.class,
"flex items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content text-left", "flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
)} )}
class:text-base-content={active} class:text-base-content={active}
class:bg-base-100={active}> class:bg-base-100={active}>
@@ -48,7 +48,7 @@
on:click on:click
class={cx( class={cx(
$$props.class, $$props.class,
"flex w-full items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content text-left", "flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
)} )}
class:text-base-content={active} class:text-base-content={active}
class:bg-base-100={active}> class:bg-base-100={active}>

View File

@@ -1,41 +1,42 @@
import { Node, nodePasteRule, type PasteRuleMatch } from '@tiptap/core' import {Node, nodePasteRule, type PasteRuleMatch} from "@tiptap/core"
import type { Node as ProsemirrorNode } from '@tiptap/pm/model' import type {Node as ProsemirrorNode} from "@tiptap/pm/model"
import type { MarkdownSerializerState } from 'prosemirror-markdown' import type {MarkdownSerializerState} from "prosemirror-markdown"
export const LINK_REGEX = /^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]*/gi export const LINK_REGEX =
/^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]*/gi
export const createPasteRuleMatch = <T extends Record<string, unknown>>( export const createPasteRuleMatch = <T extends Record<string, unknown>>(
match: RegExpMatchArray, match: RegExpMatchArray,
data: T, data: T,
): PasteRuleMatch => ({ index: match.index!, replaceWith: match[2], text: match[0], match, data }) ): PasteRuleMatch => ({index: match.index!, replaceWith: match[2], text: match[0], match, data})
export interface LinkAttributes { export interface LinkAttributes {
url: string url: string
} }
declare module '@tiptap/core' { declare module "@tiptap/core" {
interface Commands<ReturnType> { interface Commands<ReturnType> {
link: { link: {
insertLink: (options: { url: string }) => ReturnType insertLink: (options: {url: string}) => ReturnType
} }
} }
} }
export const LinkExtension = Node.create({ export const LinkExtension = Node.create({
atom: true, atom: true,
name: 'link', name: "link",
group: 'inline', group: "inline",
inline: true, inline: true,
selectable: true, selectable: true,
draggable: true, draggable: true,
priority: 1000, priority: 1000,
addAttributes() { addAttributes() {
return { return {
url: { default: null }, url: {default: null},
} }
}, },
renderHTML(props) { renderHTML(props) {
return ['div', { 'data-url': props.node.attrs.url }] return ["div", {"data-url": props.node.attrs.url}]
}, },
renderText(props) { renderText(props) {
return props.node.attrs.url return props.node.attrs.url
@@ -53,10 +54,10 @@ export const LinkExtension = Node.create({
addCommands() { addCommands() {
return { return {
insertLink: insertLink:
({ url }) => ({url}) =>
({ commands }) => { ({commands}) => {
return commands.insertContent( return commands.insertContent(
{ type: this.name, attrs: { url } }, {type: this.name, attrs: {url}},
{ {
updateSelection: false, updateSelection: false,
}, },
@@ -68,13 +69,13 @@ export const LinkExtension = Node.create({
return [ return [
nodePasteRule({ nodePasteRule({
type: this.type, type: this.type,
getAttributes: (match) => match.data, getAttributes: match => match.data,
find: (text) => { find: text => {
const matches = [] const matches = []
for (const match of text.matchAll(LINK_REGEX)) { for (const match of text.matchAll(LINK_REGEX)) {
try { try {
matches.push(createPasteRuleMatch(match, { url: match[0] })) matches.push(createPasteRuleMatch(match, {url: match[0]}))
} catch (e) { } catch (e) {
continue continue
} }

View File

@@ -1,19 +1,18 @@
import type {SvelteComponent, ComponentType} from 'svelte' import type {SvelteComponent, ComponentType} from "svelte"
import type {Readable} from 'svelte/store' import type {Readable} from "svelte/store"
import tippy, {type Instance} from 'tippy.js' import tippy, {type Instance} from "tippy.js"
import {mergeAttributes, Node} from '@tiptap/core' import type {Editor} from "@tiptap/core"
import type {Editor} from '@tiptap/core' import {PluginKey} from "@tiptap/pm/state"
import {PluginKey} from '@tiptap/pm/state' import Suggestion from "@tiptap/suggestion"
import Suggestion from '@tiptap/suggestion' import type {Search} from "@lib/util"
import type {Search} from '@lib/util'
export type SuggestionsOptions = { export type SuggestionsOptions = {
char: string, char: string
name: string, name: string
editor: Editor, editor: Editor
search: Readable<Search<any, any>> search: Readable<Search<any, any>>
select: (value: any, props: any) => void select: (value: any, props: any) => void
allowCreate?: boolean, allowCreate?: boolean
suggestionComponent: ComponentType suggestionComponent: ComponentType
suggestionsComponent: ComponentType suggestionsComponent: ComponentType
} }
@@ -27,7 +26,7 @@ export const createSuggestions = (options: SuggestionsOptions) =>
// increase range.to by one when the next node is of type "text" // increase range.to by one when the next node is of type "text"
// and starts with a space character // and starts with a space character
const nodeAfter = editor.view.state.selection.$to.nodeAfter const nodeAfter = editor.view.state.selection.$to.nodeAfter
const overrideSpace = nodeAfter?.text?.startsWith(' ') const overrideSpace = nodeAfter?.text?.startsWith(" ")
if (overrideSpace) { if (overrideSpace) {
range.to += 1 range.to += 1
@@ -38,13 +37,13 @@ export const createSuggestions = (options: SuggestionsOptions) =>
.focus() .focus()
.insertContentAt(range, [ .insertContentAt(range, [
{type: options.name, attrs: props}, {type: options.name, attrs: props},
{type: 'text', text: ' '}, {type: "text", text: " "},
]) ])
.run() .run()
window.getSelection()?.collapseToEnd() window.getSelection()?.collapseToEnd()
}, },
allow: ({ state, range }) => { allow: ({state, range}) => {
const $from = state.doc.resolve(range.from) const $from = state.doc.resolve(range.from)
const type = state.schema.nodes[options.name] const type = state.schema.nodes[options.name]
@@ -67,7 +66,7 @@ export const createSuggestions = (options: SuggestionsOptions) =>
onStart: props => { onStart: props => {
target = document.createElement("div") target = document.createElement("div")
popover = tippy('body', { popover = tippy("body", {
getReferenceClientRect: props.clientRect as any, getReferenceClientRect: props.clientRect as any,
appendTo: document.body, appendTo: document.body,
content: target, content: target,

View File

@@ -1,7 +1,7 @@
import {Node, nodePasteRule} from '@tiptap/core' import {Node, nodePasteRule} from "@tiptap/core"
import type {Node as ProsemirrorNode} from '@tiptap/pm/model' import type {Node as ProsemirrorNode} from "@tiptap/pm/model"
import type {MarkdownSerializerState} from 'prosemirror-markdown' import type {MarkdownSerializerState} from "prosemirror-markdown"
import {createPasteRuleMatch} from '@lib/tiptap/util' import {createPasteRuleMatch} from "@lib/tiptap/util"
export const TOPIC_REGEX = /(#[^\s]+)/g export const TOPIC_REGEX = /(#[^\s]+)/g
@@ -9,25 +9,25 @@ export interface TopicAttributes {
name: string name: string
} }
declare module '@tiptap/core' { declare module "@tiptap/core" {
interface Commands<ReturnType> { interface Commands<ReturnType> {
topic: { topic: {
insertTopic: (options: { name: string }) => ReturnType insertTopic: (options: {name: string}) => ReturnType
} }
} }
} }
export const TopicExtension = Node.create({ export const TopicExtension = Node.create({
atom: true, atom: true,
name: 'topic', name: "topic",
group: 'inline', group: "inline",
inline: true, inline: true,
selectable: true, selectable: true,
draggable: true, draggable: true,
priority: 1000, priority: 1000,
addAttributes() { addAttributes() {
return { return {
name: { default: null }, name: {default: null},
} }
}, },
renderText(props) { renderText(props) {
@@ -46,10 +46,10 @@ export const TopicExtension = Node.create({
addCommands() { addCommands() {
return { return {
insertTopic: insertTopic:
({ name }) => ({name}) =>
({ commands }) => { ({commands}) => {
return commands.insertContent( return commands.insertContent(
{ type: this.name, attrs: { name } }, {type: this.name, attrs: {name}},
{ {
updateSelection: false, updateSelection: false,
}, },
@@ -61,13 +61,13 @@ export const TopicExtension = Node.create({
return [ return [
nodePasteRule({ nodePasteRule({
type: this.type, type: this.type,
getAttributes: (match) => match.data, getAttributes: match => match.data,
find: (text) => { find: text => {
const matches = [] const matches = []
for (const match of text.matchAll(TOPIC_REGEX)) { for (const match of text.matchAll(TOPIC_REGEX)) {
try { try {
matches.push(createPasteRuleMatch(match, { name: match[0] })) matches.push(createPasteRuleMatch(match, {name: match[0]}))
} catch (e) { } catch (e) {
continue continue
} }

View File

@@ -1,4 +1,4 @@
export * from '@lib/tiptap/util' export * from "@lib/tiptap/util"
export {createSuggestions} from '@lib/tiptap/Suggestions' export {createSuggestions} from "@lib/tiptap/Suggestions"
export {TopicExtension} from '@lib/tiptap/TopicExtension' export {TopicExtension} from "@lib/tiptap/TopicExtension"
export {LinkExtension} from '@lib/tiptap/LinkExtension' export {LinkExtension} from "@lib/tiptap/LinkExtension"

View File

@@ -1,9 +1,9 @@
import type {JSONContent, PasteRuleMatch} from '@tiptap/core' import type {JSONContent, PasteRuleMatch} from "@tiptap/core"
export const createPasteRuleMatch = <T extends Record<string, unknown>>( export const createPasteRuleMatch = <T extends Record<string, unknown>>(
match: RegExpMatchArray, match: RegExpMatchArray,
data: T, data: T,
): PasteRuleMatch => ({ index: match.index!, replaceWith: match[2], text: match[0], match, data }) ): PasteRuleMatch => ({index: match.index!, replaceWith: match[2], text: match[0], match, data})
export const findNodes = (type: string, json: JSONContent) => { export const findNodes = (type: string, json: JSONContent) => {
const results: JSONContent[] = [] const results: JSONContent[] = []

View File

@@ -1,7 +1,7 @@
import type {FlyParams} from "svelte/transition" import type {FlyParams} from "svelte/transition"
import {fly as baseFly} from "svelte/transition" import {fly as baseFly} from "svelte/transition"
export {fade} from 'svelte/transition' export {fade} from "svelte/transition"
export const fly = (node: Element, params?: FlyParams | undefined) => export const fly = (node: Element, params?: FlyParams | undefined) =>
baseFly(node, {y: 20, ...params}) baseFly(node, {y: 20, ...params})

View File

@@ -33,7 +33,6 @@ export const synced = <T>(key: string, defaultValue: T, delay = 300) => {
} }
export type SearchOptions<V, T> = { export type SearchOptions<V, T> = {
getValue: (item: T) => V getValue: (item: T) => V
fuseOptions?: IFuseOptions<T> fuseOptions?: IFuseOptions<T>
sortFn?: (items: FuseResult<T>) => any sortFn?: (items: FuseResult<T>) => any

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import "@src/app.css" import "@src/app.css"
import {onMount} from "svelte" import {onMount} from "svelte"
import {get, derived} from 'svelte/store' import {get} from "svelte/store"
import {page} from "$app/stores" import {page} from "$app/stores"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {browser} from "$app/environment" import {browser} from "$app/environment"
@@ -70,8 +70,8 @@
forward: ($psd: PublishStatusDataByUrlById) => { forward: ($psd: PublishStatusDataByUrlById) => {
const data = [] const data = []
for (const [id, itemsByUrl] of Object.entries($psd)) { for (const itemsByUrl of Object.values($psd)) {
for (const [url, item] of Object.entries(itemsByUrl)) { for (const item of Object.values(itemsByUrl)) {
data.push(item) data.push(item)
} }
} }
@@ -94,7 +94,8 @@
keyPath: "key", keyPath: "key",
store: adapter({ store: adapter({
store: freshness, store: freshness,
forward: ($freshness: Record<string, number>) => Object.entries($freshness).map(([key, ts]) => ({key, ts})), forward: ($freshness: Record<string, number>) =>
Object.entries($freshness).map(([key, ts]) => ({key, ts})),
backward: (data: any[]) => { backward: (data: any[]) => {
const result: Record<string, number> = {} const result: Record<string, number> = {}
@@ -110,7 +111,8 @@
keyPath: "id", keyPath: "id",
store: adapter({ store: adapter({
store: plaintext, store: plaintext,
forward: ($plaintext: Record<string, string>) => Object.entries($plaintext).map(([id, plaintext]) => ({id, plaintext})), forward: ($plaintext: Record<string, string>) =>
Object.entries($plaintext).map(([id, plaintext]) => ({id, plaintext})),
backward: (data: any[]) => { backward: (data: any[]) => {
const result: Record<string, string> = {} const result: Record<string, string> = {}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
onMount(() => goto('/home')) onMount(() => goto("/home"))
</script> </script>

View File

@@ -57,7 +57,7 @@
{#if $userMembership?.noms.has(group.nom)} {#if $userMembership?.noms.has(group.nom)}
<div class="center absolute flex w-full"> <div class="center absolute flex w-full">
<div <div
class="tooltip relative left-8 w-5 h-5 top-[38px] rounded-full bg-primary" class="tooltip relative left-8 top-[38px] h-5 w-5 rounded-full bg-primary"
data-tip="You are already a member of this space."> data-tip="You are already a member of this space.">
<Icon icon="check-circle" class="scale-110" /> <Icon icon="check-circle" class="scale-110" />
</div> </div>

View File

@@ -3,10 +3,8 @@
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"
const hash = import.meta.env.VITE_BUILD_HASH
const nprofile = const nprofile =
"nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz9rhwden5te0wfjkcctev93xcefwdaexwtcpzdmhxue69uhhqatjwpkx2urpvuhx2ue0vamm57" "nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz9rhwden5te0wfjkcctev93xcefwdaexwtcpzdmhxue69uhhqatjwpkx2urpvuhx2ue0vamm57"
const hodlbodPubkey = "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"
</script> </script>
<div class="hero min-h-screen bg-base-200"> <div class="hero min-h-screen bg-base-200">
@@ -15,14 +13,12 @@
<p class="text-center text-2xl">Thanks for using</p> <p class="text-center text-2xl">Thanks for using</p>
<h1 class="mb-4 text-center text-5xl font-bold uppercase">Flotilla</h1> <h1 class="mb-4 text-center text-5xl font-bold uppercase">Flotilla</h1>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2"> <div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
<div class="card2 shadow-2xl flex flex-col gap-2 text-center"> <div class="card2 flex flex-col gap-2 text-center shadow-2xl">
<h3 class="text-2xl sm:h-12">Support development</h3> <h3 class="text-2xl sm:h-12">Support development</h3>
<p class="sm:h-16">Funds will be used to support development.</p> <p class="sm:h-16">Funds will be used to support development.</p>
<Button class="btn btn-primary"> <Button class="btn btn-primary">Zap the Developer</Button>
Zap the Developer
</Button>
</div> </div>
<div class="card2 shadow-2xl flex flex-col gap-2 text-center"> <div class="card2 flex flex-col gap-2 text-center shadow-2xl">
<h3 class="text-2xl sm:h-12">Get in touch</h3> <h3 class="text-2xl sm:h-12">Get in touch</h3>
<p class="sm:h-16">Having problems? Let us know by filing an issue.</p> <p class="sm:h-16">Having problems? Let us know by filing an issue.</p>
<Link <Link

View File

@@ -1,12 +1,10 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {readable} from 'svelte/store' import {readable} from "svelte/store"
import {displayRelayUrl, isShareableRelayUrl} from '@welshman/util' import {displayRelayUrl, isShareableRelayUrl} from "@welshman/util"
import type {SignedEvent} from '@welshman/util' import type {SignedEvent} from "@welshman/util"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import {clip} from "@app/toast"
import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/base" import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/base"
import {searchRelays, subscribe, loadRelay} from "@app/state" import {searchRelays, subscribe, loadRelay} from "@app/state"
@@ -20,12 +18,12 @@
onMount(() => { onMount(() => {
const sub = subscribe({ const sub = subscribe({
filters: [{kinds: [30166], '#N': ['29']}], filters: [{kinds: [30166], "#N": ["29"]}],
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS], relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
}) })
sub.emitter.on('event', (url: string, event: SignedEvent) => { sub.emitter.on("event", (url: string, event: SignedEvent) => {
const d = event.tags.find(t => t[0] === 'd')?.[1] || "" const d = event.tags.find(t => t[0] === "d")?.[1] || ""
if (isShareableRelayUrl(d)) { if (isShareableRelayUrl(d)) {
loadRelay(d) loadRelay(d)

View File

@@ -8,7 +8,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, now} from "@welshman/lib" import {sortBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
@@ -61,7 +61,7 @@
onMount(() => { onMount(() => {
const sub = subscribe({ const sub = subscribe({
filters: [{'#h': [nom], since: now() - 30}], filters: [{"#h": [nom], since: now() - 30}],
relays: $userRelayUrlsByNom.get(nom)!, relays: $userRelayUrlsByNom.get(nom)!,
}) })
@@ -70,7 +70,7 @@
</script> </script>
<div class="relative flex h-screen flex-col"> <div class="relative flex h-screen flex-col">
<div class="relative z-feature mx-2 pt-4 rounded-xl"> <div class="relative z-feature mx-2 rounded-xl pt-4">
<div class="flex min-h-12 items-center gap-4 rounded-xl bg-base-100 px-4 shadow-xl"> <div class="flex min-h-12 items-center gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Icon icon="hashtag" /> <Icon icon="hashtag" />
@@ -78,7 +78,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-grow flex-col-reverse overflow-auto -mt-2 py-2"> <div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
{#each elements as { type, id, value, showPubkey } (id)} {#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"} {#if type === "date"}
<div class="flex items-center gap-2 p-2 text-xs opacity-50"> <div class="flex items-center gap-2 p-2 text-xs opacity-50">

View File

@@ -6,7 +6,7 @@ export default {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter({ adapter: adapter({
fallback: 'index.html', fallback: "index.html",
}), }),
alias: { alias: {
"@src": "src", "@src": "src",