2024-09-24 21:12:38 +00:00
|
|
|
<script lang="ts">
|
2026-09-02 23:47:15 +00:00
|
|
|
import {tagSpec, tagValue} from "@welshman/util"
|
2024-09-24 21:12:38 +00:00
|
|
|
import {
|
|
|
|
|
parse,
|
|
|
|
|
truncate,
|
2024-12-05 23:32:27 +00:00
|
|
|
renderAsHtml,
|
2024-09-24 21:12:38 +00:00
|
|
|
isText,
|
2026-05-06 20:47:30 +00:00
|
|
|
isEmail,
|
2025-05-12 22:10:24 +00:00
|
|
|
isEmoji,
|
2024-09-24 21:12:38 +00:00
|
|
|
isTopic,
|
|
|
|
|
isCode,
|
2026-09-09 01:09:24 +00:00
|
|
|
isCommand,
|
2024-09-24 21:12:38 +00:00
|
|
|
isCashu,
|
|
|
|
|
isInvoice,
|
|
|
|
|
isLink,
|
|
|
|
|
isProfile,
|
2026-08-21 15:31:53 +00:00
|
|
|
isRoom,
|
2024-09-24 21:12:38 +00:00
|
|
|
isEvent,
|
|
|
|
|
isEllipsis,
|
|
|
|
|
isAddress,
|
|
|
|
|
isNewline,
|
|
|
|
|
} from "@welshman/content"
|
2025-10-06 18:23:19 +00:00
|
|
|
import type {Parsed} from "@welshman/content"
|
2025-02-04 03:28:29 +00:00
|
|
|
import {preventDefault, stopPropagation} from "@lib/html"
|
2024-10-08 18:39:16 +00:00
|
|
|
import Link from "@lib/components/Link.svelte"
|
2025-09-10 22:26:39 +00:00
|
|
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
2024-10-17 22:21:22 +00:00
|
|
|
import Icon from "@lib/components/Icon.svelte"
|
|
|
|
|
import Button from "@lib/components/Button.svelte"
|
2026-06-24 20:56:53 +00:00
|
|
|
import Card from "@lib/components/Card.svelte"
|
2024-10-08 18:39:16 +00:00
|
|
|
import ContentToken from "@app/components/ContentToken.svelte"
|
2025-05-12 22:10:24 +00:00
|
|
|
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
2026-05-06 20:47:30 +00:00
|
|
|
import ContentEmail from "@app/components/ContentEmail.svelte"
|
2024-10-08 18:39:16 +00:00
|
|
|
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"
|
2026-09-07 16:48:36 +00:00
|
|
|
import ContentCommand from "@app/components/ContentCommand.svelte"
|
2024-10-08 18:39:16 +00:00
|
|
|
import ContentMention from "@app/components/ContentMention.svelte"
|
2026-08-21 15:31:53 +00:00
|
|
|
import RoomName from "@app/components/RoomName.svelte"
|
|
|
|
|
import {makeRoomPath} from "@app/routes"
|
2026-09-09 01:09:24 +00:00
|
|
|
import {deriveValidCommands, getCommandsForInvocation} from "@app/commands"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {userSettingsValues} from "@app/settings"
|
2024-09-24 21:12:38 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
type Props = {
|
2025-02-04 00:37:14 +00:00
|
|
|
event: any
|
|
|
|
|
minLength?: number
|
|
|
|
|
maxLength?: number
|
|
|
|
|
showEntire?: boolean
|
|
|
|
|
expandMode?: string
|
2025-10-06 18:23:19 +00:00
|
|
|
trimParent?: boolean
|
2025-04-15 18:03:27 +00:00
|
|
|
url?: string
|
2025-02-04 00:37:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let {
|
|
|
|
|
event,
|
|
|
|
|
minLength = 500,
|
|
|
|
|
maxLength = 700,
|
|
|
|
|
showEntire = $bindable(false),
|
|
|
|
|
expandMode = "block",
|
2025-10-06 18:23:19 +00:00
|
|
|
trimParent = false,
|
2025-04-15 18:03:27 +00:00
|
|
|
url,
|
2025-02-04 00:37:14 +00:00
|
|
|
}: Props = $props()
|
2024-09-24 21:12:38 +00:00
|
|
|
|
2026-09-07 16:48:36 +00:00
|
|
|
// An invocation is plain text and carries no tags, so it's only recognizable against the
|
2026-09-09 01:09:24 +00:00
|
|
|
// definitions the space it was written in publishes. Anything else — including a command
|
|
|
|
|
// nobody here answers to — falls through and renders as the text it is.
|
|
|
|
|
const available = deriveValidCommands({
|
2026-09-07 16:48:36 +00:00
|
|
|
url,
|
|
|
|
|
kind: event.kind,
|
|
|
|
|
pubkey: event.pubkey,
|
|
|
|
|
tags: event.tags,
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-09 01:09:24 +00:00
|
|
|
const fullContent = $derived(parse(event))
|
2024-09-24 21:12:38 +00:00
|
|
|
|
|
|
|
|
const expand = () => {
|
|
|
|
|
showEntire = true
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-19 22:13:00 +00:00
|
|
|
const isBlock = (i: number) => {
|
|
|
|
|
const parsed = fullContent[i]
|
|
|
|
|
|
2025-10-06 18:23:19 +00:00
|
|
|
if (!parsed) return false
|
2024-11-19 22:13:00 +00:00
|
|
|
|
2025-09-25 17:12:58 +00:00
|
|
|
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
2024-11-19 22:13:00 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 18:23:19 +00:00
|
|
|
if (isQuote(parsed) && isStartAndEnd(i)) {
|
2024-11-19 22:13:00 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-24 21:12:38 +00:00
|
|
|
const isBoundary = (i: number) => {
|
|
|
|
|
const parsed = fullContent[i]
|
|
|
|
|
|
|
|
|
|
if (!parsed || isNewline(parsed)) return true
|
2024-11-20 16:53:12 +00:00
|
|
|
if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/))
|
2024-09-24 21:12:38 +00:00
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-20 16:53:12 +00:00
|
|
|
const isStart = (i: number) => isBoundary(i - 1)
|
2024-10-10 17:00:01 +00:00
|
|
|
|
2024-11-20 16:53:12 +00:00
|
|
|
const isEnd = (i: number) => isBoundary(i + 1)
|
|
|
|
|
|
|
|
|
|
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
|
|
|
|
|
|
2025-10-06 18:23:19 +00:00
|
|
|
const isQuote = (p: Parsed) => isEvent(p) || isAddress(p)
|
|
|
|
|
|
2026-07-08 21:13:04 +00:00
|
|
|
let warningDismissed = $state(false)
|
|
|
|
|
|
2024-10-17 22:21:22 +00:00
|
|
|
const ignoreWarning = () => {
|
2026-07-08 21:13:04 +00:00
|
|
|
warningDismissed = true
|
2024-10-17 22:21:22 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 21:13:04 +00:00
|
|
|
const warning = $derived(
|
|
|
|
|
warningDismissed
|
|
|
|
|
? undefined
|
2026-07-28 16:16:26 +00:00
|
|
|
: $userSettingsValues.hide_sensitive && tagValue(tagSpec("content-warning"), event.tags),
|
2025-02-04 00:37:14 +00:00
|
|
|
)
|
|
|
|
|
|
2025-10-06 18:23:19 +00:00
|
|
|
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {
|
|
|
|
|
const result: T[] = []
|
|
|
|
|
|
|
|
|
|
for (const x of xs) {
|
|
|
|
|
if (result.length === 0 && f(x)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.push(x)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const shortContent = $derived.by(() => {
|
|
|
|
|
let result = fullContent
|
|
|
|
|
|
|
|
|
|
if (trimParent && result.length > 0 && isQuote(result[0])) {
|
|
|
|
|
result = dropWhile(p => isQuote(p) || isNewline(p), result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!showEntire) {
|
|
|
|
|
result = truncate(result, {
|
|
|
|
|
minLength,
|
|
|
|
|
maxLength,
|
|
|
|
|
mediaLength: 200,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
})
|
2025-02-04 00:37:14 +00:00
|
|
|
|
2025-02-04 01:21:46 +00:00
|
|
|
const hasEllipsis = $derived(shortContent.some(isEllipsis))
|
|
|
|
|
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
|
|
|
|
const expandBlock = $derived(hasEllipsis && expandMode === "block")
|
2024-09-24 21:12:38 +00:00
|
|
|
</script>
|
|
|
|
|
|
2024-10-02 18:57:36 +00:00
|
|
|
<div class="relative">
|
2024-10-17 22:21:22 +00:00
|
|
|
{#if warning}
|
2026-06-24 20:56:53 +00:00
|
|
|
<Card sm class="shadow-none flex gap-2">
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={Danger} />
|
2024-10-17 22:21:22 +00:00
|
|
|
<p>
|
|
|
|
|
This note has been flagged by the author as "{warning}".<br />
|
2026-06-24 20:56:53 +00:00
|
|
|
<Button onclick={ignoreWarning} class="button button-link">Show anyway</Button>
|
2024-10-17 22:21:22 +00:00
|
|
|
</p>
|
2026-06-24 20:56:53 +00:00
|
|
|
</Card>
|
2024-10-17 22:21:22 +00:00
|
|
|
{:else}
|
|
|
|
|
<div
|
2026-04-07 21:53:19 +00:00
|
|
|
class="overflow-hidden text-ellipsis wrap-break-word"
|
2024-10-29 21:02:51 +00:00
|
|
|
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
2026-08-22 17:32:51 +00:00
|
|
|
{#each shortContent as parsed, i (i)}
|
2026-09-09 01:09:24 +00:00
|
|
|
{#if isCommand(parsed)}
|
|
|
|
|
{@const command = getCommandsForInvocation($available, parsed.value)[0]}
|
|
|
|
|
{#if command}
|
|
|
|
|
<ContentCommand {command} value={parsed.value} />
|
|
|
|
|
{:else}
|
|
|
|
|
{@html renderAsHtml(parsed)}
|
|
|
|
|
{/if}
|
|
|
|
|
{:else if isNewline(parsed) && !isBlock(i - 1)}
|
2025-02-06 19:33:01 +00:00
|
|
|
<ContentNewline value={parsed.value} />
|
2024-10-17 22:21:22 +00:00
|
|
|
{:else if isTopic(parsed)}
|
|
|
|
|
<ContentTopic value={parsed.value} />
|
2025-05-12 22:10:24 +00:00
|
|
|
{:else if isEmoji(parsed)}
|
|
|
|
|
<ContentEmoji value={parsed.value} />
|
2026-05-06 20:47:30 +00:00
|
|
|
{:else if isEmail(parsed)}
|
|
|
|
|
<ContentEmail value={parsed.value} />
|
2024-10-17 22:21:22 +00:00
|
|
|
{:else if isCode(parsed)}
|
2024-11-19 16:37:09 +00:00
|
|
|
<ContentCode
|
|
|
|
|
value={parsed.value}
|
|
|
|
|
isBlock={isStartAndEnd(i) || parsed.value.includes("\n")} />
|
2024-10-17 22:21:22 +00:00
|
|
|
{:else if isCashu(parsed) || isInvoice(parsed)}
|
|
|
|
|
<ContentToken value={parsed.value} />
|
|
|
|
|
{:else if isLink(parsed)}
|
2024-11-19 22:13:00 +00:00
|
|
|
{#if isBlock(i)}
|
2025-04-28 22:46:48 +00:00
|
|
|
<ContentLinkBlock value={parsed.value} {event} />
|
2024-10-17 22:21:22 +00:00
|
|
|
{:else}
|
2026-01-12 18:12:47 +00:00
|
|
|
<ContentLinkInline value={parsed.value} {event} />
|
2024-10-17 22:21:22 +00:00
|
|
|
{/if}
|
|
|
|
|
{:else if isProfile(parsed)}
|
2025-04-15 18:03:27 +00:00
|
|
|
<ContentMention value={parsed.value} {url} />
|
2026-08-21 15:31:53 +00:00
|
|
|
{:else if isRoom(parsed)}
|
|
|
|
|
<Link href={makeRoomPath(parsed.value.url, parsed.value.room)} class="link-content">
|
|
|
|
|
#<RoomName url={parsed.value.url} h={parsed.value.room} />
|
|
|
|
|
</Link>
|
2025-10-06 18:23:19 +00:00
|
|
|
{:else if isQuote(parsed)}
|
2026-09-02 23:47:15 +00:00
|
|
|
<ContentQuote {url} value={parsed.value} raw={parsed.raw} {event} inline={!isBlock(i)} />
|
2024-10-29 21:02:51 +00:00
|
|
|
{:else if isEllipsis(parsed) && expandInline}
|
2024-12-05 23:32:27 +00:00
|
|
|
{@html renderAsHtml(parsed)}
|
2026-02-03 22:18:58 +00:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-08-22 17:55:06 +00:00
|
|
|
class="pointer-events-auto text-sm underline"
|
2026-02-03 22:18:58 +00:00
|
|
|
onclick={stopPropagation(preventDefault(expand))}>
|
|
|
|
|
Read more
|
|
|
|
|
</button>
|
2024-10-02 18:57:36 +00:00
|
|
|
{:else}
|
2024-12-05 23:32:27 +00:00
|
|
|
{@html renderAsHtml(parsed)}
|
2024-10-02 18:57:36 +00:00
|
|
|
{/if}
|
2024-10-17 22:21:22 +00:00
|
|
|
{/each}
|
2024-10-02 18:57:36 +00:00
|
|
|
</div>
|
2024-10-29 21:02:51 +00:00
|
|
|
{#if expandBlock}
|
2024-11-20 23:56:24 +00:00
|
|
|
<div class="relative z-feature -mt-6 flex justify-center py-2">
|
2026-06-24 20:56:53 +00:00
|
|
|
<Button onclick={stopPropagation(preventDefault(expand))} class="button button-neutral">
|
2024-10-17 22:21:22 +00:00
|
|
|
See more
|
2026-06-24 20:56:53 +00:00
|
|
|
</Button>
|
2024-10-17 22:21:22 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
2024-10-02 18:57:36 +00:00
|
|
|
{/if}
|
|
|
|
|
</div>
|