diff --git a/src/components/feed/NoteContent.tsx b/src/components/feed/NoteContent.tsx
index 205c986..a33acc9 100644
--- a/src/components/feed/NoteContent.tsx
+++ b/src/components/feed/NoteContent.tsx
@@ -1,5 +1,6 @@
import { ReactNode } from "react";
import { nip19 } from "@nostr-dev-kit/ndk";
+import { useUIStore } from "../../stores/ui";
// Regex patterns
const URL_REGEX = /https?:\/\/[^\s<>"')\]]+/g;
@@ -116,7 +117,39 @@ function parseContent(content: string): ContentSegment[] {
return segments;
}
+// Returns true if we handled the URL internally (njump.me interception).
+function tryHandleUrlInternally(url: string): boolean {
+ try {
+ const u = new URL(url);
+ if (u.hostname === "njump.me") {
+ const entity = u.pathname.replace(/^\//, "");
+ if (entity) return tryOpenNostrEntity(entity);
+ }
+ } catch { /* not a valid URL */ }
+ return false;
+}
+
+// Decodes a NIP-19 bech32 string and navigates internally where possible.
+// Returns true if handled, false if the caller should fall back to a browser open.
+function tryOpenNostrEntity(raw: string): boolean {
+ try {
+ const decoded = nip19.decode(raw);
+ const { openProfile } = useUIStore.getState();
+ if (decoded.type === "npub") {
+ openProfile(decoded.data as string);
+ return true;
+ }
+ if (decoded.type === "nprofile") {
+ openProfile((decoded.data as { pubkey: string }).pubkey);
+ return true;
+ }
+ // note / nevent / naddr — no internal reader yet, fall through to njump.me
+ } catch { /* invalid entity */ }
+ return false;
+}
+
export function NoteContent({ content }: { content: string }) {
+ const { openSearch } = useUIStore();
const segments = parseContent(content);
const images: string[] = segments.filter((s) => s.type === "image").map((s) => s.value);
const videos: string[] = segments.filter((s) => s.type === "video").map((s) => s.value);
@@ -136,6 +169,9 @@ export function NoteContent({ content }: { content: string }) {
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:text-accent-hover underline underline-offset-2 decoration-accent/40"
+ onClick={(e) => {
+ if (tryHandleUrlInternally(seg.value)) e.preventDefault();
+ }}
>
{seg.display}
@@ -146,6 +182,10 @@ export function NoteContent({ content }: { content: string }) {
{
+ e.stopPropagation();
+ tryOpenNostrEntity(seg.value);
+ }}
>
@{seg.display}
@@ -156,6 +196,10 @@ export function NoteContent({ content }: { content: string }) {
{
+ e.stopPropagation();
+ openSearch(`#${seg.value}`);
+ }}
>
{seg.display}
diff --git a/src/components/search/SearchView.tsx b/src/components/search/SearchView.tsx
index 3db2b4b..0113280 100644
--- a/src/components/search/SearchView.tsx
+++ b/src/components/search/SearchView.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef } from "react";
+import { useState, useRef, useEffect } from "react";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { searchNotes, searchUsers } from "../../lib/nostr";
import { useUserStore } from "../../stores/user";
@@ -88,7 +88,8 @@ function UserRow({ user }: { user: ParsedUser }) {
}
export function SearchView() {
- const [query, setQuery] = useState("");
+ const { pendingSearch } = useUIStore();
+ const [query, setQuery] = useState(pendingSearch ?? "");
const [noteResults, setNoteResults] = useState([]);
const [userResults, setUserResults] = useState([]);
const [loading, setLoading] = useState(false);
@@ -98,14 +99,24 @@ export function SearchView() {
const isHashtag = query.trim().startsWith("#");
- const handleSearch = async () => {
- const q = query.trim();
+ // If opened with a pending search query (e.g. from a hashtag click), run it immediately
+ useEffect(() => {
+ if (pendingSearch) {
+ useUIStore.setState({ pendingSearch: null });
+ handleSearch(pendingSearch);
+ }
+ }, []);
+
+ const handleSearch = async (overrideQuery?: string) => {
+ const q = (overrideQuery ?? query).trim();
if (!q) return;
+ if (overrideQuery) setQuery(overrideQuery);
setLoading(true);
setSearched(false);
try {
+ const isTag = q.startsWith("#");
const notesPromise = searchNotes(q);
- const usersPromise = isHashtag ? Promise.resolve([]) : searchUsers(q);
+ const usersPromise = isTag ? Promise.resolve([]) : searchUsers(q);
const [notes, userEvents] = await Promise.all([notesPromise, usersPromise]);
setNoteResults(notes);
setUserResults(userEvents.map(parseUserEvent));
@@ -137,7 +148,7 @@ export function SearchView() {
className="flex-1 bg-transparent text-text text-[13px] placeholder:text-text-dim focus:outline-none"
/>