mirror of
https://github.com/hoornet/vega.git
synced 2026-05-13 02:08:36 -07:00
Fix React error #31 crash from malformed Nostr profiles
Malformed profiles with non-string fields (e.g. nip05: {}) crashed
React's entire render tree in production. Add typeof guards and
profileName() utility across all components that render profile data.
Add ErrorBoundary in main.tsx to show crash details instead of blank screen.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { NDKEvent, nip19 } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, nip19 } from "@nostr-dev-kit/ndk";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { useUIStore } from "../../stores/ui";
|
import { useUIStore } from "../../stores/ui";
|
||||||
import { shortenPubkey } from "../../lib/utils";
|
import { shortenPubkey, profileName } from "../../lib/utils";
|
||||||
|
|
||||||
function getTag(event: NDKEvent, name: string): string {
|
function getTag(event: NDKEvent, name: string): string {
|
||||||
return event.tags.find((t) => t[0] === name)?.[1] ?? "";
|
return event.tags.find((t) => t[0] === name)?.[1] ?? "";
|
||||||
@@ -32,7 +32,7 @@ export function ArticleCard({ event }: { event: NDKEvent }) {
|
|||||||
const publishedAt = parseInt(getTag(event, "published_at")) || event.created_at || null;
|
const publishedAt = parseInt(getTag(event, "published_at")) || event.created_at || null;
|
||||||
const naddr = buildNaddr(event);
|
const naddr = buildNaddr(event);
|
||||||
|
|
||||||
const authorName = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
|
const authorName = profileName(profile, shortenPubkey(event.pubkey));
|
||||||
const date = publishedAt
|
const date = publishedAt
|
||||||
? new Date(publishedAt * 1000).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
|
? new Date(publishedAt * 1000).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useBookmarkStore } from "../../stores/bookmark";
|
|||||||
import { fetchArticle, publishReaction, publishRepost, publishNote } from "../../lib/nostr";
|
import { fetchArticle, publishReaction, publishRepost, publishNote } from "../../lib/nostr";
|
||||||
import { nip19 } from "@nostr-dev-kit/ndk";
|
import { nip19 } from "@nostr-dev-kit/ndk";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
|
import { profileName } from "../../lib/utils";
|
||||||
import { ZapModal } from "../zap/ZapModal";
|
import { ZapModal } from "../zap/ZapModal";
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
@@ -33,7 +34,7 @@ function getTags(event: NDKEvent, name: string): string[] {
|
|||||||
function AuthorRow({ pubkey, publishedAt, readingTime }: { pubkey: string; publishedAt: number | null; readingTime?: number }) {
|
function AuthorRow({ pubkey, publishedAt, readingTime }: { pubkey: string; publishedAt: number | null; readingTime?: number }) {
|
||||||
const { openProfile } = useUIStore();
|
const { openProfile } = useUIStore();
|
||||||
const profile = useProfile(pubkey);
|
const profile = useProfile(pubkey);
|
||||||
const name = profile?.displayName || profile?.name || pubkey.slice(0, 12) + "…";
|
const name = profileName(profile, pubkey.slice(0, 12) + "…");
|
||||||
const date = publishedAt
|
const date = publishedAt
|
||||||
? new Date(publishedAt * 1000).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })
|
? new Date(publishedAt * 1000).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })
|
||||||
: null;
|
: null;
|
||||||
@@ -122,7 +123,7 @@ export function ArticleView() {
|
|||||||
const articleTags = event ? getTags(event, "t") : [];
|
const articleTags = event ? getTags(event, "t") : [];
|
||||||
const authorPubkey = event?.pubkey ?? "";
|
const authorPubkey = event?.pubkey ?? "";
|
||||||
const authorProfile = useProfile(authorPubkey);
|
const authorProfile = useProfile(authorPubkey);
|
||||||
const authorName = authorProfile?.displayName || authorProfile?.name || authorPubkey.slice(0, 12) + "…";
|
const authorName = profileName(authorProfile, authorPubkey.slice(0, 12) + "…");
|
||||||
|
|
||||||
const bodyHtml = event?.content ? renderMarkdown(event.content) : "";
|
const bodyHtml = event?.content ? renderMarkdown(event.content) : "";
|
||||||
const wordCount = event?.content?.trim().split(/\s+/).length ?? 0;
|
const wordCount = event?.content?.trim().split(/\s+/).length ?? 0;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useUIStore } from "../../stores/ui";
|
|||||||
import { useNotificationsStore } from "../../stores/notifications";
|
import { useNotificationsStore } from "../../stores/notifications";
|
||||||
import { fetchDMConversations, fetchDMThread, sendDM, decryptDM, getNDK } from "../../lib/nostr";
|
import { fetchDMConversations, fetchDMThread, sendDM, decryptDM, getNDK } from "../../lib/nostr";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
import { timeAgo, shortenPubkey, profileName } from "../../lib/utils";
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ function ConvRow({
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}) {
|
}) {
|
||||||
const profile = useProfile(partnerPubkey);
|
const profile = useProfile(partnerPubkey);
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(partnerPubkey);
|
const name = profileName(profile, shortenPubkey(partnerPubkey));
|
||||||
const time = lastEvent.created_at ? timeAgo(lastEvent.created_at) : "";
|
const time = lastEvent.created_at ? timeAgo(lastEvent.created_at) : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -126,7 +126,7 @@ function ThreadPanel({
|
|||||||
}) {
|
}) {
|
||||||
const { openProfile } = useUIStore();
|
const { openProfile } = useUIStore();
|
||||||
const profile = useProfile(partnerPubkey);
|
const profile = useProfile(partnerPubkey);
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(partnerPubkey);
|
const name = profileName(profile, shortenPubkey(partnerPubkey));
|
||||||
|
|
||||||
const [messages, setMessages] = useState<NDKEvent[]>([]);
|
const [messages, setMessages] = useState<NDKEvent[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { uploadImage, uploadBytes } from "../../lib/upload";
|
|||||||
import { useAutoResize } from "../../hooks/useAutoResize";
|
import { useAutoResize } from "../../hooks/useAutoResize";
|
||||||
import { useUserStore } from "../../stores/user";
|
import { useUserStore } from "../../stores/user";
|
||||||
import { useFeedStore } from "../../stores/feed";
|
import { useFeedStore } from "../../stores/feed";
|
||||||
import { shortenPubkey } from "../../lib/utils";
|
import { shortenPubkey, profileName } from "../../lib/utils";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { readFile } from "@tauri-apps/plugin-fs";
|
import { readFile } from "@tauri-apps/plugin-fs";
|
||||||
import { EmojiPicker } from "../shared/EmojiPicker";
|
import { EmojiPicker } from "../shared/EmojiPicker";
|
||||||
@@ -25,8 +25,8 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
|
|||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const { profile, npub } = useUserStore();
|
const { profile, npub } = useUserStore();
|
||||||
const avatar = profile?.picture;
|
const avatar = typeof profile?.picture === "string" ? profile.picture : undefined;
|
||||||
const name = profile?.displayName || profile?.name || (npub ? shortenPubkey(npub) : "");
|
const name = profileName(profile, npub ? shortenPubkey(npub) : "");
|
||||||
|
|
||||||
// Auto-save draft with debounce
|
// Auto-save draft with debounce
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useZapCount } from "../../hooks/useZapCount";
|
|||||||
import { useUserStore } from "../../stores/user";
|
import { useUserStore } from "../../stores/user";
|
||||||
import { useBookmarkStore } from "../../stores/bookmark";
|
import { useBookmarkStore } from "../../stores/bookmark";
|
||||||
import { publishReaction, publishRepost } from "../../lib/nostr";
|
import { publishReaction, publishRepost } from "../../lib/nostr";
|
||||||
|
import { profileName } from "../../lib/utils";
|
||||||
import { ZapModal } from "../zap/ZapModal";
|
import { ZapModal } from "../zap/ZapModal";
|
||||||
import { QuoteModal } from "./QuoteModal";
|
import { QuoteModal } from "./QuoteModal";
|
||||||
|
|
||||||
@@ -20,8 +21,8 @@ interface NoteActionsProps {
|
|||||||
|
|
||||||
export function NoteActions({ event, onReplyToggle, showReply }: NoteActionsProps) {
|
export function NoteActions({ event, onReplyToggle, showReply }: NoteActionsProps) {
|
||||||
const profile = useProfile(event.pubkey);
|
const profile = useProfile(event.pubkey);
|
||||||
const name = profile?.displayName || profile?.name || event.pubkey.slice(0, 8) + "…";
|
const name = profileName(profile, event.pubkey.slice(0, 8) + "…");
|
||||||
const avatar = profile?.picture;
|
const avatar = typeof profile?.picture === "string" ? profile.picture : undefined;
|
||||||
const { loggedIn } = useUserStore();
|
const { loggedIn } = useUserStore();
|
||||||
const { bookmarkedIds, addBookmark, removeBookmark } = useBookmarkStore();
|
const { bookmarkedIds, addBookmark, removeBookmark } = useBookmarkStore();
|
||||||
const isBookmarked = bookmarkedIds.includes(event.id!);
|
const isBookmarked = bookmarkedIds.includes(event.id!);
|
||||||
|
|||||||
@@ -20,15 +20,17 @@ interface NoteCardProps {
|
|||||||
|
|
||||||
function ParentAuthorName({ pubkey }: { pubkey: string }) {
|
function ParentAuthorName({ pubkey }: { pubkey: string }) {
|
||||||
const profile = useProfile(pubkey);
|
const profile = useProfile(pubkey);
|
||||||
const name = profile?.displayName || profile?.name || pubkey.slice(0, 8) + "…";
|
const raw = profile?.displayName || profile?.name;
|
||||||
|
const name = (typeof raw === "string" ? raw : null) || pubkey.slice(0, 8) + "…";
|
||||||
return <span className="text-accent">@{name}</span>;
|
return <span className="text-accent">@{name}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
|
export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
|
||||||
const profile = useProfile(event.pubkey);
|
const profile = useProfile(event.pubkey);
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
|
const rawName = profile?.displayName || profile?.name;
|
||||||
|
const name = (typeof rawName === "string" ? rawName : null) || shortenPubkey(event.pubkey);
|
||||||
const avatar = profile?.picture;
|
const avatar = profile?.picture;
|
||||||
const nip05 = profile?.nip05;
|
const nip05 = typeof profile?.nip05 === "string" ? profile.nip05 : null;
|
||||||
const verified = useNip05Verified(event.pubkey, nip05);
|
const verified = useNip05Verified(event.pubkey, nip05);
|
||||||
const time = event.created_at ? timeAgo(event.created_at) : "";
|
const time = event.created_at ? timeAgo(event.created_at) : "";
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ function QuotePreview({ eventId }: { eventId: string }) {
|
|||||||
|
|
||||||
if (!event) return null;
|
if (!event) return null;
|
||||||
|
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
|
const rawName = profile?.displayName || profile?.name;
|
||||||
|
const name = (typeof rawName === "string" ? rawName : null) || shortenPubkey(event.pubkey);
|
||||||
const preview = event.content.slice(0, 160) + (event.content.length > 160 ? "…" : "");
|
const preview = event.content.slice(0, 160) + (event.content.length > 160 ? "…" : "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export function tryOpenNostrEntity(raw: string): boolean {
|
|||||||
export function MentionName({ pubkey, fallback }: { pubkey?: string; fallback: string }) {
|
export function MentionName({ pubkey, fallback }: { pubkey?: string; fallback: string }) {
|
||||||
const profile = useProfile(pubkey ?? "");
|
const profile = useProfile(pubkey ?? "");
|
||||||
if (!pubkey) return <>{fallback}</>;
|
if (!pubkey) return <>{fallback}</>;
|
||||||
const name = profile?.displayName || profile?.name;
|
const raw = profile?.displayName || profile?.name;
|
||||||
|
const name = typeof raw === "string" ? raw : null;
|
||||||
return <>{name || fallback}</>;
|
return <>{name || fallback}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ export function renderTextSegments(
|
|||||||
segments.forEach((seg, i) => {
|
segments.forEach((seg, i) => {
|
||||||
switch (seg.type) {
|
switch (seg.type) {
|
||||||
case "text":
|
case "text":
|
||||||
elements.push(<span key={i}>{seg.value}</span>);
|
elements.push(<span key={i}>{typeof seg.value === "string" ? seg.value : String(seg.value)}</span>);
|
||||||
break;
|
break;
|
||||||
case "link":
|
case "link":
|
||||||
elements.push(
|
elements.push(
|
||||||
@@ -79,7 +80,7 @@ export function renderTextSegments(
|
|||||||
if (tryHandleUrlInternally(seg.value)) e.preventDefault();
|
if (tryHandleUrlInternally(seg.value)) e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{seg.display}
|
{typeof seg.display === "string" ? seg.display : String(seg.display)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -91,8 +92,8 @@ export function renderTextSegments(
|
|||||||
onClick={(e) => { e.stopPropagation(); tryOpenNostrEntity(seg.value); }}
|
onClick={(e) => { e.stopPropagation(); tryOpenNostrEntity(seg.value); }}
|
||||||
>
|
>
|
||||||
@{resolveMentions
|
@{resolveMentions
|
||||||
? <MentionName pubkey={seg.mentionPubkey} fallback={seg.display ?? seg.value.slice(0, 12) + "…"} />
|
? <MentionName pubkey={seg.mentionPubkey} fallback={String(seg.display ?? seg.value).slice(0, 12) + "…"} />
|
||||||
: seg.display}
|
: String(seg.display ?? seg.value)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -103,7 +104,7 @@ export function renderTextSegments(
|
|||||||
className="text-accent/80 cursor-pointer hover:text-accent"
|
className="text-accent/80 cursor-pointer hover:text-accent"
|
||||||
onClick={(e) => { e.stopPropagation(); openHashtag(seg.value); }}
|
onClick={(e) => { e.stopPropagation(); openHashtag(seg.value); }}
|
||||||
>
|
>
|
||||||
{seg.display}
|
{String(seg.display ?? seg.value)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useProfile } from "../../hooks/useProfile";
|
|||||||
import { useNip05Verified } from "../../hooks/useNip05Verified";
|
import { useNip05Verified } from "../../hooks/useNip05Verified";
|
||||||
import { fetchFollowers, ensureConnected } from "../../lib/nostr";
|
import { fetchFollowers, ensureConnected } from "../../lib/nostr";
|
||||||
import { dbLoadFollowers, dbSaveFollowers } from "../../lib/db";
|
import { dbLoadFollowers, dbSaveFollowers } from "../../lib/db";
|
||||||
import { shortenPubkey } from "../../lib/utils";
|
import { shortenPubkey, profileName } from "../../lib/utils";
|
||||||
|
|
||||||
function FollowRow({
|
function FollowRow({
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -16,9 +16,9 @@ function FollowRow({
|
|||||||
followsYou?: boolean;
|
followsYou?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const profile = useProfile(pubkey);
|
const profile = useProfile(pubkey);
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(pubkey);
|
const name = profileName(profile, shortenPubkey(pubkey));
|
||||||
const avatar = profile?.picture;
|
const avatar = typeof profile?.picture === "string" ? profile.picture : undefined;
|
||||||
const nip05 = profile?.nip05;
|
const nip05 = typeof profile?.nip05 === "string" ? profile.nip05 : undefined;
|
||||||
const verified = useNip05Verified(pubkey, nip05);
|
const verified = useNip05Verified(pubkey, nip05);
|
||||||
|
|
||||||
const { follows, follow, unfollow, pubkey: ownPubkey } = useUserStore();
|
const { follows, follow, unfollow, pubkey: ownPubkey } = useUserStore();
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import { Nip05Field } from "./Nip05Field";
|
|||||||
|
|
||||||
export function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => void }) {
|
export function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => void }) {
|
||||||
const { profile, fetchOwnProfile } = useUserStore();
|
const { profile, fetchOwnProfile } = useUserStore();
|
||||||
const [name, setName] = useState(profile?.name || "");
|
const safeStr = (v: unknown) => (typeof v === "string" ? v : "");
|
||||||
const [displayName, setDisplayName] = useState(profile?.displayName || "");
|
const [name, setName] = useState(safeStr(profile?.name));
|
||||||
const [about, setAbout] = useState(profile?.about || "");
|
const [displayName, setDisplayName] = useState(safeStr(profile?.displayName));
|
||||||
const [picture, setPicture] = useState(profile?.picture || "");
|
const [about, setAbout] = useState(safeStr(profile?.about));
|
||||||
const [banner, setBanner] = useState(profile?.banner || "");
|
const [picture, setPicture] = useState(safeStr(profile?.picture));
|
||||||
const [website, setWebsite] = useState(profile?.website || "");
|
const [banner, setBanner] = useState(safeStr(profile?.banner));
|
||||||
const [nip05, setNip05] = useState(profile?.nip05 || "");
|
const [website, setWebsite] = useState(safeStr(profile?.website));
|
||||||
const [lud16, setLud16] = useState(profile?.lud16 || "");
|
const [nip05, setNip05] = useState(safeStr(profile?.nip05));
|
||||||
|
const [lud16, setLud16] = useState(safeStr(profile?.lud16));
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useMuteStore } from "../../stores/mute";
|
|||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { useReputation } from "../../hooks/useReputation";
|
import { useReputation } from "../../hooks/useReputation";
|
||||||
import { fetchUserNotesNIP65, fetchAuthorArticles, getNDK } from "../../lib/nostr";
|
import { fetchUserNotesNIP65, fetchAuthorArticles, getNDK } from "../../lib/nostr";
|
||||||
import { shortenPubkey } from "../../lib/utils";
|
import { shortenPubkey, profileName } from "../../lib/utils";
|
||||||
import { NoteCard } from "../feed/NoteCard";
|
import { NoteCard } from "../feed/NoteCard";
|
||||||
import { ArticleCard } from "../article/ArticleCard";
|
import { ArticleCard } from "../article/ArticleCard";
|
||||||
import { ZapModal } from "../zap/ZapModal";
|
import { ZapModal } from "../zap/ZapModal";
|
||||||
@@ -17,7 +17,7 @@ import { ProfileMediaGallery } from "./ProfileMediaGallery";
|
|||||||
function TopFollowerAvatar({ pubkey }: { pubkey: string }) {
|
function TopFollowerAvatar({ pubkey }: { pubkey: string }) {
|
||||||
const profile = useProfile(pubkey);
|
const profile = useProfile(pubkey);
|
||||||
const { openProfile } = useUIStore();
|
const { openProfile } = useUIStore();
|
||||||
const name = profile?.displayName || profile?.name || pubkey.slice(0, 8) + "…";
|
const name = profileName(profile, pubkey.slice(0, 8) + "…");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -85,12 +85,12 @@ export function ProfileView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(pubkey);
|
const name = profileName(profile, shortenPubkey(pubkey));
|
||||||
const avatar = profile?.picture;
|
const avatar = typeof profile?.picture === "string" ? profile.picture : undefined;
|
||||||
const about = profile?.about;
|
const about = typeof profile?.about === "string" ? profile.about : undefined;
|
||||||
const nip05 = profile?.nip05;
|
const nip05 = typeof profile?.nip05 === "string" ? profile.nip05 : undefined;
|
||||||
const website = profile?.website;
|
const website = typeof profile?.website === "string" ? profile.website : undefined;
|
||||||
const lud16 = profile?.lud16;
|
const lud16 = typeof profile?.lud16 === "string" ? profile.lud16 : undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { useUIStore } from "../../stores/ui";
|
import { useUIStore } from "../../stores/ui";
|
||||||
import { shortenPubkey, timeAgo } from "../../lib/utils";
|
import { shortenPubkey, timeAgo, profileName } from "../../lib/utils";
|
||||||
|
|
||||||
function AncestorCard({ event }: { event: NDKEvent }) {
|
function AncestorCard({ event }: { event: NDKEvent }) {
|
||||||
const profile = useProfile(event.pubkey);
|
const profile = useProfile(event.pubkey);
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(event.pubkey);
|
const name = profileName(profile, shortenPubkey(event.pubkey));
|
||||||
const avatar = profile?.picture;
|
const avatar = typeof profile?.picture === "string" ? profile.picture : undefined;
|
||||||
const time = event.created_at ? timeAgo(event.created_at) : "";
|
const time = event.created_at ? timeAgo(event.created_at) : "";
|
||||||
const { openThread } = useUIStore();
|
const { openThread } = useUIStore();
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ThreadNode as ThreadNodeType } from "../../lib/threadTree";
|
|||||||
import { NoteCard } from "../feed/NoteCard";
|
import { NoteCard } from "../feed/NoteCard";
|
||||||
import { publishReply } from "../../lib/nostr";
|
import { publishReply } from "../../lib/nostr";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { shortenPubkey } from "../../lib/utils";
|
import { shortenPubkey, profileName } from "../../lib/utils";
|
||||||
import { EmojiPicker } from "../shared/EmojiPicker";
|
import { EmojiPicker } from "../shared/EmojiPicker";
|
||||||
import { useAutoResize } from "../../hooks/useAutoResize";
|
import { useAutoResize } from "../../hooks/useAutoResize";
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ function InlineThreadReply({ replyTo, rootEvent, onPublished }: {
|
|||||||
onPublished: (reply: NDKEvent) => void;
|
onPublished: (reply: NDKEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
const profile = useProfile(replyTo.pubkey);
|
const profile = useProfile(replyTo.pubkey);
|
||||||
const name = profile?.displayName || profile?.name || shortenPubkey(replyTo.pubkey);
|
const name = profileName(profile, shortenPubkey(replyTo.pubkey));
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [replying, setReplying] = useState(false);
|
const [replying, setReplying] = useState(false);
|
||||||
const [sent, setSent] = useState(false);
|
const [sent, setSent] = useState(false);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useUserStore } from "../../stores/user";
|
|||||||
import { useUIStore } from "../../stores/ui";
|
import { useUIStore } from "../../stores/ui";
|
||||||
import { fetchZapsReceived, fetchZapsSent, fetchNoteById } from "../../lib/nostr";
|
import { fetchZapsReceived, fetchZapsSent, fetchNoteById } from "../../lib/nostr";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { timeAgo, shortenPubkey } from "../../lib/utils";
|
import { timeAgo, shortenPubkey, profileName } from "../../lib/utils";
|
||||||
|
|
||||||
// ── Parsing helpers ──────────────────────────────────────────────────────────
|
// ── Parsing helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -53,9 +53,9 @@ function ZapRow({
|
|||||||
const { openProfile, openThread } = useUIStore();
|
const { openProfile, openThread } = useUIStore();
|
||||||
const profile = useProfile(pubkey ?? "");
|
const profile = useProfile(pubkey ?? "");
|
||||||
const name = pubkey
|
const name = pubkey
|
||||||
? profile?.displayName || profile?.name || shortenPubkey(pubkey)
|
? profileName(profile, shortenPubkey(pubkey))
|
||||||
: "anonymous";
|
: "anonymous";
|
||||||
const avatar = profile?.picture;
|
const avatar = typeof profile?.picture === "string" ? profile.picture : undefined;
|
||||||
const [notePreview, setNotePreview] = useState<string | null>(null);
|
const [notePreview, setNotePreview] = useState<string | null>(null);
|
||||||
const [noteEvent, setNoteEvent] = useState<NDKEvent | null>(null);
|
const [noteEvent, setNoteEvent] = useState<NDKEvent | null>(null);
|
||||||
const [loadingNote, setLoadingNote] = useState(false);
|
const [loadingNote, setLoadingNote] = useState(false);
|
||||||
|
|||||||
@@ -11,3 +11,9 @@ export function timeAgo(timestamp: number): string {
|
|||||||
export function shortenPubkey(pubkey: string): string {
|
export function shortenPubkey(pubkey: string): string {
|
||||||
return pubkey.slice(0, 8) + "…" + pubkey.slice(-4);
|
return pubkey.slice(0, 8) + "…" + pubkey.slice(-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Safely extract display name from a Nostr profile (handles non-string values from malformed profiles). */
|
||||||
|
export function profileName(profile: any, fallback: string): string {
|
||||||
|
const raw = profile?.displayName || profile?.name;
|
||||||
|
return (typeof raw === "string" ? raw : null) || fallback;
|
||||||
|
}
|
||||||
|
|||||||
31
src/main.tsx
31
src/main.tsx
@@ -1,15 +1,42 @@
|
|||||||
import "./lib/tauri-dev-mock"; // must be first — mocks Tauri invoke() in browser dev mode
|
import "./lib/tauri-dev-mock"; // must be first — mocks Tauri invoke() in browser dev mode
|
||||||
import { StrictMode } from "react";
|
import { StrictMode, Component, type ReactNode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { useUserStore } from "./stores/user";
|
import { useUserStore } from "./stores/user";
|
||||||
|
|
||||||
|
// Error boundary to catch React error #31 and show details instead of blank screen
|
||||||
|
class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
|
||||||
|
state: { error: Error | null } = { error: null };
|
||||||
|
static getDerivedStateFromError(error: Error) { return { error }; }
|
||||||
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
console.error("[Vega] React error boundary caught:", error, info.componentStack);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 20, color: "#ff6b6b", fontFamily: "monospace", fontSize: 13 }}>
|
||||||
|
<h2>Vega crashed</h2>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
|
||||||
|
{this.state.error.message}
|
||||||
|
</pre>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all", color: "#aaa", marginTop: 10 }}>
|
||||||
|
{this.state.error.stack}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore session — pubkey (read-only) or nsec via OS keychain
|
// Restore session — pubkey (read-only) or nsec via OS keychain
|
||||||
useUserStore.getState().restoreSession();
|
useUserStore.getState().restoreSession();
|
||||||
|
|
||||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<ErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user