import "@/styles/Profile.css"; import "@/styles/UserModal.css"; 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) { if ("error" in user) return; e.stopPropagation(); if (!api) return; setLoading(true); try { if (isFollowing) { const result = await api.unfollow(user); console.log(result); // if ("ok" in result) { // toast.success(`Unfollowed ${profile?.name || userString}`); // } else { // toast.error(result.error); // } } else { const result = await api.follow(user); console.log(result); // 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 && (
Profile banner
)} {/* 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 && ( )} <> {"nostr" in user ? ( ) : null}
); }