Add NIP-05 badges, hashtag pages, keyword muting, suggestion dismissal, notification poller

- Article cover: aspect-video replaces max-h-72 for consistent 16:9
- NIP-05 verification badge on note cards with 1-hour TTL cache
- Dedicated hashtag feed pages (clicking #tag opens live feed, not search)
- Keyword muting: word-boundary matching, applied across all feed views
- Follow suggestion dismissal: persistent "don't suggest again" per person
- Background notification poller (60s): mentions, zaps, new followers
- All notification types independently toggleable in settings
- Centralized notification firing (removed inline store notifications)
This commit is contained in:
Jure
2026-03-20 12:09:11 +01:00
parent 989ed01dfc
commit 57630227e1
20 changed files with 499 additions and 35 deletions

View File

@@ -0,0 +1,51 @@
import { useState, useEffect } from "react";
type VerifyStatus = "valid" | "invalid";
const cache = new Map<string, { status: VerifyStatus; checkedAt: number }>();
const TTL = 3600000; // 1 hour
async function verifyNip05(pubkey: string, nip05: string): Promise<VerifyStatus> {
const parts = nip05.split("@");
if (parts.length !== 2) return "invalid";
const [name, domain] = parts;
try {
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`);
if (!res.ok) return "invalid";
const json = await res.json();
const resolved = json?.names?.[name];
return resolved === pubkey ? "valid" : "invalid";
} catch {
return "invalid";
}
}
export function useNip05Verified(pubkey: string, nip05: string | undefined): "valid" | "invalid" | "checking" | null {
const [status, setStatus] = useState<"valid" | "invalid" | "checking" | null>(() => {
if (!nip05) return null;
const cached = cache.get(pubkey);
if (cached && Date.now() - cached.checkedAt < TTL) return cached.status;
return "checking";
});
useEffect(() => {
if (!nip05) { setStatus(null); return; }
const cached = cache.get(pubkey);
if (cached && Date.now() - cached.checkedAt < TTL) {
setStatus(cached.status);
return;
}
let cancelled = false;
setStatus("checking");
verifyNip05(pubkey, nip05).then((result) => {
if (cancelled) return;
cache.set(pubkey, { status: result, checkedAt: Date.now() });
setStatus(result);
});
return () => { cancelled = true; };
}, [pubkey, nip05]);
return status;
}