Add render support
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user