Improve quote rendering

This commit is contained in:
Jon Staab 2024-12-05 15:32:27 -08:00
parent 14cd49caf3
commit 1e0cb93183
9 changed files with 74 additions and 35 deletions

View file

@ -51,7 +51,6 @@ import {
} from "@welshman/app" } from "@welshman/app"
import { import {
COMMENT, COMMENT,
tagRoom,
userMembership, userMembership,
MEMBERSHIPS, MEMBERSHIPS,
INDEXER_RELAYS, INDEXER_RELAYS,
@ -242,7 +241,9 @@ export const removeSpaceMembership = async (url: string) => {
export const addRoomMembership = async (url: string, room: string) => { export const addRoomMembership = async (url: string, room: string) => {
const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, ["r", url], ["group", room, url]).reconcile(
nip44EncryptToSelf,
)
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)]) const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
return publishThunk({event, relays}) return publishThunk({event, relays})
@ -250,7 +251,7 @@ export const addRoomMembership = async (url: string, room: string) => {
export const removeRoomMembership = async (url: string, room: string) => { export const removeRoomMembership = async (url: string, room: string) => {
const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
const pred = (t: string[]) => equals(tagRoom(room, url), t) const pred = (t: string[]) => equals(["group", room, url], t)
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
const relays = uniq([ const relays = uniq([
url, url,

View file

@ -10,6 +10,8 @@
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state" import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
import {clearModals} from "@app/modal" import {clearModals} from "@app/modal"
export let next
let loading = false let loading = false
const enableChat = async () => { const enableChat = async () => {
@ -20,7 +22,7 @@
} }
clearModals() clearModals()
goto("/chat") goto(next)
} }
const submit = async () => { const submit = async () => {
@ -38,16 +40,16 @@
<form class="column gap-4" on:submit|preventDefault={submit}> <form class="column gap-4" on:submit|preventDefault={submit}>
<ModalHeader> <ModalHeader>
<div slot="title">Enable Chat</div> <div slot="title">Enable Messages</div>
<div slot="info">Do you want to enable direct messages?</div> <div slot="info">Do you want to enable notes and direct messages?</div>
</ModalHeader> </ModalHeader>
<p> <p>
By default, direct messages are disabled, since loading them requires By default, notes and direct messages are disabled, since loading them requires
{PLATFORM_NAME} to download and decrypt a lot of data. {PLATFORM_NAME} to download and decrypt a lot of data.
</p> </p>
<p> <p>
If you'd like to enable direct messages, please make sure your signer is set up to to If you'd like to enable them, please make sure your signer is set up to to auto-approve requests
auto-approve requests to decrypt data. to decrypt data.
</p> </p>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" on:click={back}> <Button class="btn btn-link" on:click={back}>
@ -55,7 +57,7 @@
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Enable Chat</Spinner> <Spinner {loading}>Enable Messages</Spinner>
<Icon icon="alt-arrow-right" /> <Icon icon="alt-arrow-right" />
</Button> </Button>
</ModalFooter> </ModalFooter>

View file

@ -4,7 +4,7 @@
import { import {
parse, parse,
truncate, truncate,
render as renderParsed, renderAsHtml,
isText, isText,
isTopic, isTopic,
isCode, isCode,
@ -146,10 +146,10 @@
</Link> </Link>
{/if} {/if}
{:else if isEllipsis(parsed) && expandInline} {:else if isEllipsis(parsed) && expandInline}
{@html renderParsed(parsed)} {@html renderAsHtml(parsed)}
<button type="button" class="text-sm underline"> Read more </button> <button type="button" class="text-sm underline"> Read more </button>
{:else} {:else}
{@html renderParsed(parsed)} {@html renderAsHtml(parsed)}
{/if} {/if}
{/each} {/each}
</div> </div>

View file

@ -2,12 +2,13 @@
import {nip19} from "nostr-tools" import {nip19} from "nostr-tools"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {ctx, nthEq} from "@welshman/lib" import {ctx, nthEq} from "@welshman/lib"
import {tracker, repository} from "@welshman/app"
import {Address, DIRECT_MESSAGE} from "@welshman/util" import {Address, DIRECT_MESSAGE} from "@welshman/util"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import {deriveEvent, entityLink, MESSAGE, THREAD} from "@app/state" import {deriveEvent, entityLink, ROOM, MESSAGE, THREAD} from "@app/state"
import {makeThreadPath} from "@app/routes" import {makeThreadPath, makeRoomPath} from "@app/routes"
export let value export let value
export let event export let event
@ -41,13 +42,27 @@
return Boolean(element) return Boolean(element)
} }
const openMessage = (url: string, room: string, id: string) => {
const event = repository.getEvent(id)
if (event) {
goto(makeRoomPath(url, room))
// TODO: if the event doesn't immediately load, this won't work. Scroll up until it's found
setTimeout(() => scrollToEvent(id), 300)
}
return Boolean(event)
}
const onClick = (e: Event) => { const onClick = (e: Event) => {
if ($quote) { if ($quote) {
if ($quote.kind === DIRECT_MESSAGE) { if ($quote.kind === DIRECT_MESSAGE) {
return scrollToEvent($quote.id) return scrollToEvent($quote.id)
} }
const [room, url] = $quote.tags.find(nthEq(0, "~"))?.slice(1) || [] const [url] = tracker.getRelays($quote.id)
const room = $quote.tags.find(nthEq(0, ROOM))?.[1]
if (url && room) { if (url && room) {
if ($quote.kind === THREAD) { if ($quote.kind === THREAD) {
@ -55,7 +70,7 @@
} }
if ($quote.kind === MESSAGE) { if ($quote.kind === MESSAGE) {
return scrollToEvent($quote.id) return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
} }
const kind = $quote.tags.find(nthEq(0, "K"))?.[1] const kind = $quote.tags.find(nthEq(0, "K"))?.[1]
@ -67,7 +82,7 @@
} }
if (parseInt(kind) === MESSAGE) { if (parseInt(kind) === MESSAGE) {
return scrollToEvent(id) return scrollToEvent(id) || openMessage(url, room, id)
} }
} }
} }

View file

@ -29,7 +29,7 @@
<NoteCard {event} class="card2 bg-alt"> <NoteCard {event} class="card2 bg-alt">
<Content {event} expandMode="inline" /> <Content {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2"> <div class="flex w-full justify-between gap-2">
<ReactionSummary relays={[url]} {event} {onReactionClick}> <ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right">
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box"> <EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
<Icon icon="smile-circle" size={4} /> <Icon icon="smile-circle" size={4} />
</EmojiButton> </EmojiButton>

View file

@ -28,7 +28,9 @@
const showSettingsMenu = () => pushModal(MenuSettings) const showSettingsMenu = () => pushModal(MenuSettings)
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable)) const openNotes = () => ($canDecrypt ? goto("/notes") : pushModal(ChatEnable, {next: "/notes"}))
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
</script> </script>
<div class="relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block"> <div class="relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
@ -57,7 +59,7 @@
class="tooltip-right"> class="tooltip-right">
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" /> <Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem title="Notes" href="/notes" class="tooltip-right"> <PrimaryNavItem title="Notes" on:click={openNotes} class="tooltip-right">
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" /> <Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem

View file

@ -16,12 +16,18 @@
</script> </script>
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}> <Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
<div class="flex w-full items-center justify-between gap-2"> {#if title}
<p class="text-xl">{title}</p> <div class="flex w-full items-center justify-between gap-2">
<p class="text-sm opacity-75"> <p class="text-xl">{title}</p>
<p class="text-sm opacity-75">
{formatTimestamp(event.created_at)}
</p>
</div>
{:else}
<p class="-mb-3 h-0 text-end text-xs opacity-75">
{formatTimestamp(event.created_at)} {formatTimestamp(event.created_at)}
</p> </p>
</div> {/if}
<Content {event} expandMode="inline" /> <Content {event} expandMode="inline" />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75"> <span class="whitespace-nowrap py-1 text-sm opacity-75">

View file

@ -24,6 +24,7 @@ import {
REACTION, REACTION,
ZAP_RESPONSE, ZAP_RESPONSE,
DIRECT_MESSAGE, DIRECT_MESSAGE,
getGroupTags,
getRelayTagValues, getRelayTagValues,
getPubkeyTagValues, getPubkeyTagValues,
isHashedEvent, isHashedEvent,
@ -334,17 +335,19 @@ export const hasMembershipUrl = (list: List | undefined, url: string) =>
return false return false
}) })
export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getListTags(list))) export const getMembershipUrls = (list?: List) => {
const tags = getListTags(list)
return sort(uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]))
}
export const getMembershipRooms = (list?: List) => export const getMembershipRooms = (list?: List) =>
getListTags(list) getGroupTags(getListTags(list)).map(t => ({url: t[2], room: t[1]}))
.filter(t => t[0] === "group")
.map(t => ({url: t[2], room: t[1]}))
export const getMembershipRoomsByUrl = (url: string, list?: List) => export const getMembershipRoomsByUrl = (url: string, list?: List) =>
sort( sort(
getListTags(list) getGroupTags(getListTags(list))
.filter(t => t[0] === "group" && t[2] === url) .filter(t => t[2] === url)
.map(nth(1)), .map(nth(1)),
) )

View file

@ -2,7 +2,8 @@
import cx from "classnames" import cx from "classnames"
import type {NodeViewProps} from "@tiptap/core" import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from "svelte-tiptap" import {NodeViewWrapper} from "svelte-tiptap"
import {ellipsize, nthEq} from "@welshman/lib" import {always, nthEq} from "@welshman/lib"
import {parse, renderAsText, ParsedType} from "@welshman/content"
import {type TrustedEvent, fromNostrURI, Address} from "@welshman/util" import {type TrustedEvent, fromNostrURI, Address} from "@welshman/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {deriveEvent, entityLink} from "@app/state" import {deriveEvent, entityLink} from "@app/state"
@ -10,12 +11,21 @@
export let node: NodeViewProps["node"] export let node: NodeViewProps["node"]
export let selected: NodeViewProps["selected"] export let selected: NodeViewProps["selected"]
const renderLink = (href: string, display: string) => display
const displayEvent = (e: TrustedEvent) => { const displayEvent = (e: TrustedEvent) => {
const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content || "" const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content || ""
return content.length > 1 if (content.length < 1) {
? ellipsize(content, 30) return fromNostrURI(nevent || naddr).slice(0, 16) + "..."
: fromNostrURI(nevent || naddr).slice(0, 16) + "..." }
const parsed = parse({...e, content})
// Try stripping entities, but if we get nothing back go ahead and show them
const renderEntity = always(parsed.find(p => p.type === ParsedType.Text) ? "" : "[quote]")
return renderAsText(parsed, {renderLink, renderEntity})
} }
$: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs) $: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs)