Add render support

This commit is contained in:
Jon Staab
2024-09-24 14:12:38 -07:00
parent 148208f072
commit 605273d7c7
19 changed files with 355 additions and 25 deletions
+15
View File
@@ -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",
+1
View File
@@ -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",
+125
View File
@@ -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}
+9
View File
@@ -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>
+16
View File
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
export let value
</script>
{#each value as _}
<br />
{/each}
+21
View File
@@ -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>
+17
View File
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
export let value
</script>
<span class="link-content">
#{value}
</span>
+16
View File
@@ -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>
+20
View File
@@ -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>
+6 -20
View File
@@ -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}
+20
View File
@@ -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({
+2 -2
View File
@@ -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>
+2 -1
View File
@@ -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 -1
View File
@@ -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>