Normalize forum URLs as nostr events and rendered them as titles
This commit is contained in:
parent
fc51e33ceb
commit
2dc823d055
9 changed files with 262 additions and 57 deletions
|
|
@ -15,6 +15,9 @@
|
|||
import type { NostrUser } from "@nostr/gadgets/metadata";
|
||||
import MentionAutocomplete from "$lib/components/MentionAutocomplete.svelte";
|
||||
import ChatContent from "$lib/components/ChatContent.svelte";
|
||||
import { shortNostrEntity } from "$lib/linkify";
|
||||
import { resolveThreadRef } from "$lib/threadRefs";
|
||||
import * as nip19 from "@nostr/tools/nip19";
|
||||
|
||||
type Props = {
|
||||
expanded?: boolean;
|
||||
|
|
@ -87,9 +90,43 @@
|
|||
});
|
||||
}
|
||||
|
||||
const ENTITY_PATTERN = "nostr:(note1[a-z0-9]+|nevent1[a-z0-9]+)";
|
||||
|
||||
// Resolved thread titles, keyed by the bech32 entity (note1…/nevent1…).
|
||||
let resolvedTitles = $state<Record<string, string>>({});
|
||||
|
||||
$effect(() => {
|
||||
const re = new RegExp(ENTITY_PATTERN, "gi");
|
||||
const entities = new Set<string>();
|
||||
for (const m of messages)
|
||||
for (const match of m.content.matchAll(re))
|
||||
entities.add(match[1].toLowerCase());
|
||||
for (const entity of entities) {
|
||||
if (resolvedTitles[entity]) continue;
|
||||
let id: string | null = null;
|
||||
try {
|
||||
const d = nip19.decode(entity);
|
||||
if (d.type === "note") id = d.data;
|
||||
else if (d.type === "nevent") id = d.data.id;
|
||||
} catch {
|
||||
// Invalid bech32, skip
|
||||
}
|
||||
if (!id) continue;
|
||||
resolveThreadRef(id).then((ref) => {
|
||||
if (ref) resolvedTitles = { ...resolvedTitles, [entity]: ref.title };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function truncate(s: string, n = 60) {
|
||||
// Collapse mentions to @… so the preview stays readable.
|
||||
const stripped = s.replace(/nostr:(?:npub1|nprofile1)[a-z0-9]+/gi, "@…");
|
||||
// Collapse mentions to @… and show thread refs as their title.
|
||||
const stripped = s
|
||||
.replace(/nostr:(?:npub1|nprofile1)[a-z0-9]+/gi, "@…")
|
||||
.replace(
|
||||
new RegExp(ENTITY_PATTERN, "gi"),
|
||||
(_m, entity) =>
|
||||
resolvedTitles[entity.toLowerCase()] ?? shortNostrEntity(entity),
|
||||
);
|
||||
const t = stripped.replace(/\s+/g, " ").trim();
|
||||
return t.length > n ? t.slice(0, n) + "…" : t;
|
||||
}
|
||||
|
|
@ -190,7 +227,7 @@
|
|||
|
||||
<aside
|
||||
bind:this={asideEl}
|
||||
class="flex-1 flex-col bg-white dark:bg-neutral-900 px-4 pt-4 pb-20 min-[1540px]:rounded-tr-xl md:absolute md:top-6 md:right-0 md:z-10 md:h-[calc(100%-1.5rem)] md:flex-none md:rounded-tl-xl md:px-6 md:py-6 md:transition-all md:duration-200
|
||||
class="flex-1 flex-col bg-white px-4 pt-4 pb-20 min-[1540px]:rounded-tr-xl md:absolute md:top-6 md:right-0 md:z-10 md:h-[calc(100%-1.5rem)] md:flex-none md:rounded-tl-xl md:px-6 md:py-6 md:transition-all md:duration-200 dark:bg-neutral-900
|
||||
{mobileActive ? 'flex' : 'hidden'} md:flex
|
||||
{expanded ? 'md:w-150 md:shadow-2xl' : 'md:w-80 md:shadow-lg'}"
|
||||
>
|
||||
|
|
@ -198,7 +235,7 @@
|
|||
<span class="text-brand text-[1.5rem] leading-7">Chat</span>
|
||||
<button
|
||||
onclick={onToggle}
|
||||
class="hidden rounded bg-neutral-100 dark:bg-neutral-800 transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-700 md:block"
|
||||
class="hidden rounded bg-neutral-100 transition-colors hover:bg-neutral-200 md:block dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
aria-label={expanded ? "Collapse chat" : "Expand chat"}
|
||||
>
|
||||
<svg
|
||||
|
|
@ -228,7 +265,9 @@
|
|||
class="no-scrollbar -mr-6 flex flex-1 flex-col overflow-y-auto pr-6"
|
||||
>
|
||||
{#if messages.length === 0}
|
||||
<div class="m-auto py-8 text-center text-sm text-neutral-400 dark:text-neutral-500">
|
||||
<div
|
||||
class="m-auto py-8 text-center text-sm text-neutral-400 dark:text-neutral-500"
|
||||
>
|
||||
No messages yet.
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -247,17 +286,19 @@
|
|||
/>
|
||||
{:else}
|
||||
<span
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-700 text-xs font-semibold text-neutral-500 dark:text-neutral-400"
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-xs font-semibold text-neutral-500 dark:bg-neutral-700 dark:text-neutral-400"
|
||||
>
|
||||
{author.name[0].toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="font-medium text-neutral-500 dark:text-neutral-400">{author.name}</span>
|
||||
<span class="font-medium text-neutral-500 dark:text-neutral-400"
|
||||
>{author.name}</span
|
||||
>
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<div class="relative">
|
||||
<button
|
||||
onclick={(e) => toggleMenu(msg.id, e)}
|
||||
class="flex items-center justify-center rounded p-0.5 text-neutral-300 dark:text-neutral-600 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-500 dark:hover:text-neutral-400"
|
||||
class="flex items-center justify-center rounded p-0.5 text-neutral-300 transition-colors hover:bg-neutral-100 hover:text-neutral-500 dark:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-400"
|
||||
aria-label="Message actions"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={openMenuId === msg.id}
|
||||
|
|
@ -277,7 +318,7 @@
|
|||
<div
|
||||
role="menu"
|
||||
style={`${menuPos.top !== undefined ? `top:${menuPos.top}px` : `bottom:${menuPos.bottom}px`};right:${menuPos.right}px`}
|
||||
class="fixed z-50 w-36 rounded-lg border border-neutral-100 dark:border-neutral-800 bg-white dark:bg-neutral-900 py-1 text-sm shadow-lg"
|
||||
class="fixed z-50 w-36 rounded-lg border border-neutral-100 bg-white py-1 text-sm shadow-lg dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
<button
|
||||
role="menuitem"
|
||||
|
|
@ -310,7 +351,7 @@
|
|||
<div class="mt-0.5">
|
||||
{#if msg.replyToId}
|
||||
<div
|
||||
class="mb-1 border-l-2 border-neutral-300 dark:border-neutral-600 pl-2 text-xs text-neutral-500 dark:text-neutral-400"
|
||||
class="mb-1 border-l-2 border-neutral-300 pl-2 text-xs text-neutral-500 dark:border-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{#if parent}
|
||||
<span class="font-medium">{parentAuthor?.name}</span>:
|
||||
|
|
@ -330,14 +371,16 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-t border-neutral-200 dark:border-neutral-700 pt-4">
|
||||
<div class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
|
||||
{#if replyTarget}
|
||||
{@const replyAuthor = resolveAuthor(replyTarget.pubkey)}
|
||||
<div
|
||||
class="mb-2 flex items-start gap-2 rounded bg-neutral-50 dark:bg-neutral-800 px-2 py-1.5 text-xs text-neutral-600 dark:text-neutral-400"
|
||||
class="mb-2 flex items-start gap-2 rounded bg-neutral-50 px-2 py-1.5 text-xs text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="text-neutral-400 dark:text-neutral-500">↳ Reply to </span>
|
||||
<span class="text-neutral-400 dark:text-neutral-500"
|
||||
>↳ Reply to
|
||||
</span>
|
||||
<span class="font-medium">{replyAuthor.name}</span>:
|
||||
<span class="text-neutral-500 dark:text-neutral-400"
|
||||
>{truncate(replyTarget.content, 80)}</span
|
||||
|
|
@ -345,7 +388,7 @@
|
|||
</div>
|
||||
<button
|
||||
onclick={cancelReply}
|
||||
class="shrink-0 text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-400"
|
||||
class="shrink-0 text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-400"
|
||||
aria-label="Cancel reply"
|
||||
>
|
||||
✕
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue