mirror of
https://github.com/hoornet/vega.git
synced 2026-06-29 21:52:08 -07:00
Polish DM view: clickable links, inline images, nostr entity rendering
- DM messages now parse and render URLs as clickable accent links - Image URLs show inline in the bubble (max-h-48) - nostr:naddr and mentions rendered as styled clickable links - nostr:nevent quotes show a subtle indicator - Media URLs (video/audio/youtube etc.) rendered as clickable links - parseContent regex now case-insensitive for nostr: prefix; entity lowercased before nip19.decode for robustness - renderTextSegments: add case for quote type (↩ note indicator)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { NDKEvent, NDKKind, nip19 } from "@nostr-dev-kit/ndk";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
@@ -7,6 +8,8 @@ import { fetchDMConversations, fetchDMThread, sendDM, decryptDM, getNDK } from "
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { timeAgo, shortenPubkey, profileName } from "../../lib/utils";
|
||||
import { debug } from "../../lib/debug";
|
||||
import { parseContent } from "../../lib/parsing";
|
||||
import { tryHandleUrlInternally, tryOpenNostrEntity } from "../feed/TextSegments";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -77,6 +80,75 @@ function ConvRow({
|
||||
);
|
||||
}
|
||||
|
||||
// ── DM text renderer ─────────────────────────────────────────────────────────
|
||||
|
||||
function DMText({ text }: { text: string }) {
|
||||
const segments = useMemo(() => parseContent(text), [text]);
|
||||
return (
|
||||
<span className="whitespace-pre-wrap break-words">
|
||||
{segments.map((seg, i) => {
|
||||
if (seg.type === "link") {
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
href={seg.value}
|
||||
className="text-accent hover:text-accent-hover underline underline-offset-2 decoration-accent/40"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (!tryHandleUrlInternally(seg.value)) openUrl(seg.value).catch(() => {});
|
||||
}}
|
||||
>
|
||||
{seg.display || seg.value}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (seg.type === "image") {
|
||||
return (
|
||||
<img
|
||||
key={i}
|
||||
src={seg.value}
|
||||
className="max-h-48 max-w-full rounded-md mt-1.5 block"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (seg.type === "naddr" || seg.type === "mention") {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="text-accent hover:text-accent-hover cursor-pointer underline-offset-2"
|
||||
onClick={() => tryOpenNostrEntity(seg.value)}
|
||||
>
|
||||
{seg.type === "naddr" ? "🔗 " : "@"}{String(seg.display ?? seg.value).slice(0, 20)}…
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (seg.type === "quote") {
|
||||
return (
|
||||
<span key={i} className="text-accent/60 text-xs cursor-pointer hover:text-accent italic">
|
||||
↩ quoted note
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// video/audio/youtube/etc. — show URL as a plain link
|
||||
if (["video", "audio", "youtube", "vimeo", "spotify", "tidal", "fountain"].includes(seg.type)) {
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
href={seg.value}
|
||||
className="text-accent hover:text-accent-hover underline underline-offset-2 decoration-accent/40"
|
||||
onClick={(e) => { e.preventDefault(); openUrl(seg.value).catch(() => {}); }}
|
||||
>
|
||||
{seg.value}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <span key={i}>{seg.value}</span>;
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Message bubble ────────────────────────────────────────────────────────────
|
||||
|
||||
function MessageBubble({ event, myPubkey }: { event: NDKEvent; myPubkey: string }) {
|
||||
@@ -106,7 +178,7 @@ function MessageBubble({ event, myPubkey }: { event: NDKEvent; myPubkey: string
|
||||
) : text === null ? (
|
||||
<span className="text-text-dim">…</span>
|
||||
) : (
|
||||
text
|
||||
<DMText text={text} />
|
||||
)}
|
||||
<div className={`text-[10px] mt-1 ${isMine ? "text-accent/60" : "text-text-dim"}`}>
|
||||
{time}
|
||||
|
||||
@@ -165,6 +165,19 @@ export function renderTextSegments(
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case "quote":
|
||||
// Inline text placeholder — the QuotePreview card renders separately below
|
||||
elements.push(
|
||||
<span
|
||||
key={i}
|
||||
className="text-accent/60 text-xs cursor-pointer hover:text-accent"
|
||||
onClick={(e) => { e.stopPropagation(); tryOpenNostrEntity(`note1${seg.value.slice(0, 8)}`); }}
|
||||
title="Quoted note"
|
||||
>
|
||||
↩ note
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case "hashtag":
|
||||
elements.push(
|
||||
<span
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ const TIDAL_REGEX = /tidal\.com\/(?:browse\/)?(?:track|album|playlist)\/([a-zA-Z
|
||||
const SPOTIFY_REGEX = /open\.spotify\.com\/(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/;
|
||||
const VIMEO_REGEX = /vimeo\.com\/(\d+)/;
|
||||
const FOUNTAIN_REGEX = /fountain\.fm\/(episode|show)\/([a-zA-Z0-9-]+)/;
|
||||
const NOSTR_MENTION_REGEX = /nostr:(npub1[a-z0-9]+|note1[a-z0-9]+|nevent1[a-z0-9]+|nprofile1[a-z0-9]+|naddr1[a-z0-9]+)/g;
|
||||
const NOSTR_MENTION_REGEX = /nostr:(npub1[a-z0-9]+|note1[a-z0-9]+|nevent1[a-z0-9]+|nprofile1[a-z0-9]+|naddr1[a-z0-9]+)/gi;
|
||||
const HASHTAG_REGEX = /(?<=\s|^)#(\w{2,})/g;
|
||||
|
||||
export interface ContentSegment {
|
||||
@@ -114,9 +114,9 @@ export function parseContent(content: string): ContentSegment[] {
|
||||
}
|
||||
|
||||
// Find nostr: mentions
|
||||
const mentionRegex = new RegExp(NOSTR_MENTION_REGEX.source, "g");
|
||||
const mentionRegex = new RegExp(NOSTR_MENTION_REGEX.source, "gi");
|
||||
while ((match = mentionRegex.exec(content)) !== null) {
|
||||
const raw = match[1];
|
||||
const raw = match[1].toLowerCase();
|
||||
let display = raw.slice(0, 12) + "…";
|
||||
let mentionPubkey: string | undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user