import Modal from "./Modal"; import Avatar from "../Avatar"; import Icon from "@/components/Icon"; import useLocalState from "@/state/state"; import { useLocation } from "wouter"; import toast from "react-hot-toast"; import { isValidPatp } from "urbit-ob"; import { isValidNostrPubkey } from "@/logic/nostrill"; import { generateNprofile } from "@/logic/nostr"; import { useState } from "react"; export default function ({ userString }: { userString: string }) { const { setModal, api, pubkey, profiles, following, followers } = useLocalState((s) => ({ setModal: s.setModal, api: s.api, pubkey: s.pubkey, profiles: s.profiles, following: s.following, followers: s.followers, })); const [_, navigate] = useLocation(); const [loading, setLoading] = useState(false); function close() { setModal(null); } const user = isValidPatp(userString) ? { urbit: userString } : isValidNostrPubkey(userString) ? { nostr: userString } : { error: "" }; if ("error" in user) { return ( Invalid user identifier ); } const itsMe = "urbit" in user ? user.urbit === api?.airlock.our : "nostr" in user ? user.nostr === pubkey : false; const profile = profiles.get(userString); const isFollowing = following.has(userString); const isFollower = followers.includes(userString); // Get follower/following counts from the user's feed if available const userFeed = following.get(userString); const postCount = userFeed ? Object.keys(userFeed.feed).length : 0; async function copy(e: React.MouseEvent) { e.stopPropagation(); await navigator.clipboard.writeText(userString); toast.success("Copied to clipboard"); } async function handleFollow(e: React.MouseEvent) { e.stopPropagation(); if (!api) return; setLoading(true); try { if (isFollowing) { const result = await api.unfollow(userString); if ("ok" in result) { toast.success(`Unfollowed ${profile?.name || userString}`); } else { toast.error(result.error); } } else { const result = await api.follow(userString); if ("ok" in result) { toast.success(`Following ${profile?.name || userString}`); } else { toast.error(result.error); } } } catch (err) { toast.error("Action failed"); } finally { setLoading(false); } } async function handleAvatarClick(e: React.MouseEvent) { e.stopPropagation(); if ("nostr" in user) { const nprof = generateNprofile(userString); const href = `https://primal.net/p/${nprof}`; window.open(href, "_blank"); } } const displayName = profile?.name || ("urbit" in user ? user.urbit : "Anon"); const truncatedId = userString.length > 20 ? `${userString.slice(0, 10)}...${userString.slice(-8)}` : userString; // Check if a string is a URL const isURL = (str: string): boolean => { try { new URL(str); return true; } catch { return str.startsWith('http://') || str.startsWith('https://'); } }; // Get banner image from profile.other const bannerImage = profile?.other?.banner || profile?.other?.Banner; // Filter out banner from other fields since we display it separately const otherFields = profile?.other ? Object.entries(profile.other).filter( ([key]) => key.toLowerCase() !== 'banner' ) : []; return ( {/* Banner Image */} {bannerImage && ( )} {/* Header with Avatar and Basic Info */} {displayName} {"urbit" in user ? user.urbit : truncatedId} {/* User type badge */} {"urbit" in user ? ( Urbit ) : ( Nostr )} {itsMe && You} {isFollower && !itsMe && Follows you} {/* Profile About Section */} {profile?.about && ( {profile.about} )} {/* Stats */} {postCount > 0 && ( {postCount} Posts )} {/* Additional stats could go here */} {/* Custom Fields */} {otherFields.length > 0 && ( Additional Info {otherFields.map(([key, value]) => ( {key}: {isURL(value) ? ( e.stopPropagation()} > {value} ) : ( {value} )} ))} )} {/* Action Buttons */} {!itsMe && ( {loading ? "..." : isFollowing ? "Following" : "Follow"} )} {"urbit" in user ? ( <> { navigate(`/feed/${userString}`); close(); }} > View Feed > ) : ( View on Primal )} ); }
Invalid user identifier
{profile.about}