From a465c73178dc621578d10312f263355f0f2d5309 Mon Sep 17 00:00:00 2001 From: polwex Date: Tue, 18 Nov 2025 09:43:16 +0700 Subject: fixes to profile handling and rendering --- gui/src/components/modals/UserModal.tsx | 256 ++++++++++++++++++++++++++++---- 1 file changed, 228 insertions(+), 28 deletions(-) (limited to 'gui/src/components/modals') diff --git a/gui/src/components/modals/UserModal.tsx b/gui/src/components/modals/UserModal.tsx index 6e3089d..0694f1e 100644 --- a/gui/src/components/modals/UserModal.tsx +++ b/gui/src/components/modals/UserModal.tsx @@ -4,59 +4,259 @@ import Icon from "@/components/Icon"; import useLocalState from "@/state/state"; import { useLocation } from "wouter"; import toast from "react-hot-toast"; -import type { UserType } from "@/types/nostrill"; - -export default function ({ - user, - userString, -}: { - user: UserType; - userString: string; -}) { - const { setModal, api, pubkey } = useLocalState((s) => ({ +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 && ( +
+ 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} +
+
-
- - - {itsMe && ( + + {/* 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 && ( + + )} + + {"urbit" in user ? ( <> - + ) : ( + )}
-- cgit v1.2.3