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
+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}