forked from coracle/flotilla
Add render support
This commit is contained in:
Generated
+15
@@ -24,6 +24,7 @@
|
||||
"@tiptap/suggestion": "^2.6.4",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/app": "^0.0.7",
|
||||
"@welshman/content": "^0.0.9",
|
||||
"@welshman/lib": "^0.0.17",
|
||||
"@welshman/net": "^0.0.22",
|
||||
"@welshman/signer": "^0.0.5",
|
||||
@@ -85,6 +86,11 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@braintree/sanitize-url": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz",
|
||||
"integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg=="
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -1679,6 +1685,15 @@
|
||||
"nostr-tools": "^2.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/content": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.9.tgz",
|
||||
"integrity": "sha512-tmzSRvVmOdet+X9W1vmjqHf4tkyhxotZ0qG7+iVPd7SjRSvuDmq09odT19rQtWn5Pl8mmEREyQgqzTRubDbsxg==",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.2",
|
||||
"nostr-tools": "^2.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/lib": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.17.tgz",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@tiptap/suggestion": "^2.6.4",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/app": "^0.0.7",
|
||||
"@welshman/content": "^0.0.9",
|
||||
"@welshman/lib": "^0.0.17",
|
||||
"@welshman/net": "^0.0.22",
|
||||
"@welshman/signer": "^0.0.5",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import {fromNostrURI} from "@welshman/util"
|
||||
import {
|
||||
parse,
|
||||
truncate,
|
||||
render as renderParsed,
|
||||
isText,
|
||||
isTopic,
|
||||
isCode,
|
||||
isCashu,
|
||||
isInvoice,
|
||||
isLink,
|
||||
isProfile,
|
||||
isEvent,
|
||||
isEllipsis,
|
||||
isAddress,
|
||||
isNewline,
|
||||
} from "@welshman/content"
|
||||
import Link from '@lib/components/Link.svelte'
|
||||
import Button from '@lib/components/Button.svelte'
|
||||
import ContentToken from '@app/components/ContentToken.svelte'
|
||||
import ContentCode from '@app/components/ContentCode.svelte'
|
||||
import ContentLinkInline from '@app/components/ContentLinkInline.svelte'
|
||||
import ContentLinkBlock from '@app/components/ContentLinkBlock.svelte'
|
||||
import ContentNewline from '@app/components/ContentNewline.svelte'
|
||||
import ContentQuote from '@app/components/ContentQuote.svelte'
|
||||
import ContentTopic from '@app/components/ContentTopic.svelte'
|
||||
import ContentMention from '@app/components/ContentMention.svelte'
|
||||
import {nostr} from '@app/state'
|
||||
|
||||
export let event
|
||||
export let minLength = 500
|
||||
export let maxLength = 700
|
||||
export let showEntire = false
|
||||
export let skipMedia = false
|
||||
export let expandable = true
|
||||
export let depth = 0
|
||||
|
||||
const fullContent = parse(event)
|
||||
|
||||
const expand = () => {
|
||||
showEntire = true
|
||||
}
|
||||
|
||||
const isBoundary = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
if (!parsed || isNewline(parsed)) return true
|
||||
if (isText(parsed)) return parsed.value.match(/^\s+$/)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const isStartAndEnd = (i: number) => Boolean(isBoundary(i - 1) && isBoundary(i + 1))
|
||||
|
||||
const isStartOrEnd = (i: number) => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
||||
|
||||
const isBlock = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
return isEvent(parsed) || isAddress(parsed) || isLink(parsed)
|
||||
}
|
||||
|
||||
const isNextToBlock = (i: number) => isBlock(i - 1) || isBlock(i + 1)
|
||||
|
||||
$: shortContent = showEntire
|
||||
? fullContent
|
||||
: truncate(
|
||||
fullContent.filter(p => !skipMedia || (isLink(p) && p.value.isMedia)),
|
||||
{
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: 200,
|
||||
},
|
||||
)
|
||||
|
||||
$: ellipsize = expandable && shortContent.find(isEllipsis)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="overflow-hidden text-ellipsis"
|
||||
style={ellipsize ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
||||
{#each shortContent as parsed, i}
|
||||
{#if isNewline(parsed)}
|
||||
<ContentNewline value={parsed.value.slice(isNextToBlock(i) ? 1 : 0)} />
|
||||
{:else if isTopic(parsed)}
|
||||
<ContentTopic value={parsed.value} />
|
||||
{:else if isCode(parsed)}
|
||||
<ContentCode value={parsed.value} />
|
||||
{:else if isCashu(parsed) || isInvoice(parsed)}
|
||||
<ContentToken value={parsed.value} />
|
||||
{:else if isLink(parsed)}
|
||||
{#if isStartOrEnd(i)}
|
||||
<ContentLinkBlock value={parsed.value} />
|
||||
{:else}
|
||||
<ContentLinkInline value={parsed.value} />
|
||||
{/if}
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} />
|
||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||
{#if isStartOrEnd(i) && depth < 2}
|
||||
<ContentQuote value={parsed.value} {depth}>
|
||||
<div slot="note-content" let:event>
|
||||
<svelte:self {event} depth={depth + 1} />
|
||||
</div>
|
||||
</ContentQuote>
|
||||
{:else}
|
||||
<Link
|
||||
external
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap underline"
|
||||
href={nostr(parsed.raw)}>
|
||||
{fromNostrURI(parsed.raw).slice(0, 16) + "…"}
|
||||
</Link>
|
||||
{/if}
|
||||
{:else}
|
||||
{@html renderParsed(parsed)}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if ellipsize}
|
||||
<div class="z-feature relative -mt-24 mb-0 flex justify-center bg-gradient-to-t from-base-100 pt-12" class:-ml-12={depth > 0}>
|
||||
<Button class="btn" on:click={expand}>See more</Button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<pre>
|
||||
<code class="link-content block w-full">
|
||||
{value}
|
||||
</code>
|
||||
</pre>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||
import {dufflepud, imgproxy} from "@app/state"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
|
||||
export let value
|
||||
|
||||
const url = value.url.toString()
|
||||
|
||||
const loadPreview = async () => {
|
||||
const json = await postJson(dufflepud("link/preview"), {url})
|
||||
|
||||
if (!json?.title && !json?.image) {
|
||||
throw new Error("Failed to load link preview")
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
</script>
|
||||
|
||||
<Link
|
||||
external
|
||||
href={url}
|
||||
style="background-color: rgba(15, 15, 14, 0.5)"
|
||||
class="relative flex w-full flex-grow flex-col overflow-hidden rounded-xl my-2">
|
||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
||||
<video controls src={url} class="max-h-96 object-contain object-center">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||
<img
|
||||
alt="Link preview"
|
||||
src={imgproxy(url)}
|
||||
class="object-cover object-center max-h-96" />
|
||||
{:else}
|
||||
{#await loadPreview()}
|
||||
<span class="loading loading-spinner" />
|
||||
{:then preview}
|
||||
{#if preview.image}
|
||||
<img
|
||||
alt="Link preview"
|
||||
src={imgproxy(preview.image)}
|
||||
class="max-h-96 object-contain object-center" />
|
||||
{/if}
|
||||
<div class="h-px bg-neutral-600" />
|
||||
{#if preview.title}
|
||||
<div class="flex flex-col bg-white px-4 py-2 text-black">
|
||||
<strong class="overflow-hidden text-ellipsis whitespace-nowrap">{preview.title}</strong>
|
||||
<small>{ellipsize(preview.description, 140)}</small>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
</Link>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
|
||||
export let value
|
||||
|
||||
const url = value.url.toString()
|
||||
</script>
|
||||
|
||||
<Link external href={url} class="link-content">
|
||||
<Icon icon="link-round" size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
</Link>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {displayProfile} from "@welshman/util"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import {nostr} from '@app/state'
|
||||
|
||||
export let value
|
||||
|
||||
const profile = deriveProfile(value.pubkey)
|
||||
const nprofile = nip19.nprofileEncode(value)
|
||||
</script>
|
||||
|
||||
<Link external href={nostr(nprofile)} class="link-content">
|
||||
@{displayProfile($profile)}
|
||||
</Link>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
</script>
|
||||
|
||||
{#each value as _}
|
||||
<br />
|
||||
{/each}
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import {getAddress, Address} from "@welshman/util"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import {deriveEvent} from "@app/state"
|
||||
|
||||
export let value
|
||||
export let depth = 0
|
||||
|
||||
const {id, identifier, kind, pubkey, relays} = value
|
||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||
const event = deriveEvent(idOrAddress, relays)
|
||||
|
||||
$: address = $event ? getAddress($event) : ""
|
||||
$: isGroup = address.match(/^(34550|35834):/)
|
||||
</script>
|
||||
|
||||
<button class="text-left my-2" on:click|stopPropagation>
|
||||
<NoteCard event={$event} class="p-4 rounded-box bg-base-300">
|
||||
<slot name="note-content" event={$event} {depth} />
|
||||
</NoteCard>
|
||||
</button>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {clip} from "@app/toast"
|
||||
|
||||
export let value
|
||||
|
||||
const copy = () => clip(value)
|
||||
</script>
|
||||
|
||||
<Button on:click={copy} class="link-content">
|
||||
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
||||
{value.slice(0, 16)}...
|
||||
</Button>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<span class="link-content">
|
||||
#{value}
|
||||
</span>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {deriveProfile, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
|
||||
export let event
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 {$$props.class}">
|
||||
<div class="flex justify-between gap-2">
|
||||
<Profile pubkey={event.pubkey} />
|
||||
<span class="text-sm opacity-75">{formatTimestamp(event.created_at)}</span>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {deriveProfile, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
|
||||
export let pubkey
|
||||
|
||||
const profile = deriveProfile(pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(pubkey)
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="py-1">
|
||||
<Avatar src={$profile?.picture} size={10} />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-bold">{$profileDisplay}</div>
|
||||
<div class="text-sm opacity-75">{displayPubkey(pubkey)}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,31 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {displayPubkey} from "@welshman/util"
|
||||
import {deriveProfile, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
|
||||
export let root
|
||||
export let replies
|
||||
|
||||
const profile = deriveProfile(root.pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(root.pubkey)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="card2 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="py-1">
|
||||
<Avatar src={$profile?.picture} size={10} />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-bold">{$profileDisplay}</div>
|
||||
<div class="text-sm opacity-75">{displayPubkey(root.pubkey)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm opacity-75">{formatTimestamp(root.created_at)}</span>
|
||||
<NoteCard event={root} class="card2">
|
||||
<div class="ml-12">
|
||||
<Content event={root} />
|
||||
</div>
|
||||
<div class="ml-12"></div>
|
||||
</div>
|
||||
</NoteCard>
|
||||
{#if replies.length > 0}
|
||||
Show {replies.length} {replies.length === 1 ? "reply" : "replies"}
|
||||
{/if}
|
||||
|
||||
@@ -47,8 +47,28 @@ export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "
|
||||
|
||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
export const IMGPROXY_URL = "https://imgproxy.coracle.social"
|
||||
|
||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||
|
||||
export const dufflepud = (path: string) => DUFFLEPUD_URL + '/' + path
|
||||
|
||||
export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
|
||||
if (!url || url.match("gif$")) {
|
||||
return url
|
||||
}
|
||||
|
||||
url = url.split("?")[0]
|
||||
|
||||
try {
|
||||
return url ? `${IMGPROXY_URL}/x/s:${w}:${h}/${btoa(url)}` : url
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export const nostr = (entity: string) => `https://coracle.social/${entity}`
|
||||
|
||||
setContext({
|
||||
net: getDefaultNetContext(),
|
||||
app: getDefaultAppContext({
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {ellipsize} from "@welshman/lib"
|
||||
import {type TrustedEvent, fromNostrURI, Address} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import {deriveEvent} from "@app/state"
|
||||
import {deriveEvent, nostr} from "@app/state"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class="inline">
|
||||
<Link external href="https://njump.me/{node.attrs.nevent}" class="link-content">
|
||||
<Link external href={nostr(node.attrs.nevent)} class="link-content">
|
||||
{displayEvent($event)}
|
||||
</Link>
|
||||
</NodeViewWrapper>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import {displayProfile} from "@welshman/util"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import {nostr} from '@app/state'
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
export let selected: NodeViewProps["selected"]
|
||||
@@ -15,7 +16,7 @@
|
||||
<NodeViewWrapper class="inline">
|
||||
<Link
|
||||
external
|
||||
href="https://njump.me/{node.attrs.nprofile}"
|
||||
href={nostr(node.attrs.nprofile)}
|
||||
class={cx("link-content", {"link-content-selected": selected})}>
|
||||
@{displayProfile($profile)}
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {nostr} from '@app/state'
|
||||
|
||||
const nprofile =
|
||||
"nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz9rhwden5te0wfjkcctev93xcefwdaexwtcpzdmhxue69uhhqatjwpkx2urpvuhx2ue0vamm57"
|
||||
@@ -33,7 +34,7 @@
|
||||
<p class="text-center">
|
||||
Built with 💜 by
|
||||
<span class="text-primary">
|
||||
@<Link external href="https://njump.me/{nprofile}" class="link">hodlbod</Link>
|
||||
@<Link external href={nostr(nprofile)} class="link">hodlbod</Link>
|
||||
</span>
|
||||
</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
data-tip="Create an Event"
|
||||
on:click={createThread}>
|
||||
<div class="btn btn-circle btn-primary flex h-12 w-12 items-center justify-center">
|
||||
<Icon icon="add-square" />
|
||||
<Icon icon="notes-minimalistic" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user