import { useStore } from "@nanostores/react"; import { clsx } from "clsx"; import { Edit2, Eye, EyeOff, Flag, Folder, Github, Link2, Linkedin, Loader2, ShieldBan, ShieldOff, Volume2, VolumeX, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { blockUser, getCollections, getModerationRelationship, getProfile, muteUser, unblockUser, unmuteUser, } from "../../api/client"; import CollectionIcon from "../../components/common/CollectionIcon"; import { BlueskyIcon, TangledIcon } from "../../components/common/Icons"; import type { MoreMenuItem } from "../../components/common/MoreMenu"; import MoreMenu from "../../components/common/MoreMenu"; import RichText from "../../components/common/RichText"; import FeedItems from "../../components/feed/FeedItems"; import EditProfileModal from "../../components/modals/EditProfileModal"; import ExternalLinkModal from "../../components/modals/ExternalLinkModal"; import ReportModal from "../../components/modals/ReportModal"; import { Avatar, Button, EmptyState, Skeleton, Tabs, } from "../../components/ui"; import { $user } from "../../store/auth"; import { $preferences, loadPreferences } from "../../store/preferences"; import type { Collection, ContentLabel, ModerationRelationship, UserProfile, } from "../../types"; interface ProfileProps { did: string; } type Tab = "all" | "annotations" | "highlights" | "bookmarks" | "collections"; const motivationMap: Record = { all: undefined, annotations: "commenting", highlights: "highlighting", bookmarks: "bookmarking", collections: undefined, }; export default function Profile({ did }: ProfileProps) { const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState("all"); const [collections, setCollections] = useState([]); const [dataLoading, setDataLoading] = useState(false); const user = useStore($user); const isOwner = user?.did === did; const [showEdit, setShowEdit] = useState(false); const [externalLink, setExternalLink] = useState(null); const [showReportModal, setShowReportModal] = useState(false); const loadMoreTimerRef = useRef | null>(null); const [modRelation, setModRelation] = useState({ blocking: false, muting: false, blockedBy: false, }); const [accountLabels, setAccountLabels] = useState([]); const [profileRevealed, setProfileRevealed] = useState(false); const preferences = useStore($preferences); const formatLinkText = (url: string) => { try { const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`); const domain = urlObj.hostname.replace(/^www\./, ""); const path = urlObj.pathname.replace(/^\/|\/$/g, ""); if ( domain.includes("github.com") || domain.includes("twitter.com") || domain.includes("x.com") ) { return path ? `${domain}/${path}` : domain; } if (domain.includes("linkedin.com") && path.includes("in/")) { return `linkedin.com/${path.split("in/")[1]}`; } if (domain.includes("tangled")) { return path ? `${domain}/${path}` : domain; } return domain + (path && path.length < 20 ? `/${path}` : ""); } catch { return url; } }; useEffect(() => { setProfile(null); setCollections([]); setActiveTab("all"); setLoading(true); const loadProfile = async () => { try { const marginPromise = getProfile(did); const bskyPromise = fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`, ) .then((res) => (res.ok ? res.json() : null)) .catch(() => null); const [marginData, bskyData] = await Promise.all([ marginPromise, bskyPromise, ]); const merged: UserProfile = { did: marginData?.did || bskyData?.did || did, handle: marginData?.handle || bskyData?.handle || "", displayName: marginData?.displayName || bskyData?.displayName, avatar: marginData?.avatar || bskyData?.avatar, description: marginData?.description || bskyData?.description, banner: marginData?.banner || bskyData?.banner, website: marginData?.website, links: marginData?.links || [], followersCount: bskyData?.followersCount || marginData?.followersCount, followsCount: bskyData?.followsCount || marginData?.followsCount, postsCount: bskyData?.postsCount || marginData?.postsCount, }; if (marginData?.labels && Array.isArray(marginData.labels)) { setAccountLabels(marginData.labels); } setProfile(merged); if (user && user.did !== did) { try { const rel = await getModerationRelationship(did); setModRelation(rel); } catch { // ignore } } } catch (e) { console.error("Profile load failed", e); } finally { setLoading(false); } }; if (did) loadProfile(); }, [did, user]); useEffect(() => { loadPreferences(); }, []); useEffect(() => { const timer = loadMoreTimerRef.current; return () => { if (timer) clearTimeout(timer); }; }, []); const isHandle = !did.startsWith("did:"); const resolvedDid = isHandle ? profile?.did : did; useEffect(() => { const loadTabContent = async () => { const isHandle = !did.startsWith("did:"); const resolvedDid = isHandle ? profile?.did : did; if (!resolvedDid) return; setDataLoading(true); try { if (activeTab === "collections") { const res = await getCollections(resolvedDid); setCollections(res); } } catch (e) { console.error(e); } finally { setDataLoading(false); } }; loadTabContent(); }, [profile?.did, did, activeTab]); if (loading) { return (
); } if (!profile) { return ( ); } const tabs = [ { id: "all", label: "All" }, { id: "annotations", label: "Annotations" }, { id: "highlights", label: "Highlights" }, { id: "bookmarks", label: "Bookmarks" }, { id: "collections", label: "Collections" }, ]; const LABEL_DESCRIPTIONS: Record = { sexual: "Sexual Content", nudity: "Nudity", violence: "Violence", gore: "Graphic Content", spam: "Spam", misleading: "Misleading", }; const accountWarning = (() => { if (!accountLabels.length) return null; const priority = [ "gore", "violence", "nudity", "sexual", "misleading", "spam", ]; for (const p of priority) { const match = accountLabels.find((l) => l.val === p); if (match) { const pref = preferences.labelPreferences.find( (lp) => lp.label === p && lp.labelerDid === match.src, ); const visibility = pref?.visibility || "warn"; if (visibility === "ignore") continue; return { label: p, description: LABEL_DESCRIPTIONS[p] || p, visibility, }; } } return null; })(); const shouldBlurAvatar = accountWarning && !profileRevealed; return (

{profile.displayName || profile.handle}

@{profile.handle}

{isOwner && ( )} {!isOwner && user && ( { const items: MoreMenuItem[] = []; items.push({ label: "View profile in Bluesky", icon: , onClick: () => { const handle = profile.handle || did; window.open( `https://bsky.app/profile/${encodeURIComponent(handle)}`, "_blank", ); }, }); if (modRelation.blocking) { items.push({ label: `Unblock @${profile.handle || "user"}`, icon: , onClick: async () => { await unblockUser(did); setModRelation((prev) => ({ ...prev, blocking: false, })); }, }); } else { items.push({ label: `Block @${profile.handle || "user"}`, icon: , onClick: async () => { await blockUser(did); setModRelation((prev) => ({ ...prev, blocking: true, })); }, variant: "danger", }); } if (modRelation.muting) { items.push({ label: `Unmute @${profile.handle || "user"}`, icon: , onClick: async () => { await unmuteUser(did); setModRelation((prev) => ({ ...prev, muting: false, })); }, }); } else { items.push({ label: `Mute @${profile.handle || "user"}`, icon: , onClick: async () => { await muteUser(did); setModRelation((prev) => ({ ...prev, muting: true, })); }, }); } items.push({ label: "Report", icon: , onClick: () => setShowReportModal(true), variant: "danger", }); return items; })()} /> )}
{profile.description && (

)}
{[ ...(profile.website ? [profile.website] : []), ...(profile.links || []), ] .filter((link, index, self) => self.indexOf(link) === index) .map((link) => { let icon; if (link.includes("github.com")) { icon = ; } else if (link.includes("linkedin.com")) { icon = ; } else if ( link.includes("tangled.sh") || link.includes("tangled.org") ) { icon = ; } else { icon = ; } return ( ); })}
{accountWarning && (

Account labeled: {accountWarning.description}

This label was applied by a moderation service you subscribe to.

{!profileRevealed ? ( ) : ( )}
)} {modRelation.blocking && (

You have blocked @{profile.handle}

Their content is hidden from your feeds.

)} {modRelation.muting && !modRelation.blocking && (

You have muted @{profile.handle}

Their content is hidden from your feeds.

)} {modRelation.blockedBy && !modRelation.blocking && (

@{profile.handle} has blocked you. You cannot interact with their content.

)} setActiveTab(id as Tab)} className="mb-4" />
{dataLoading ? (

Loading...

) : activeTab === "collections" ? ( collections.length === 0 ? ( } message={ isOwner ? "You haven't created any collections yet." : "No collections" } /> ) : (
{collections.map((collection) => (

{collection.name}

{collection.itemCount}{" "} {collection.itemCount === 1 ? "item" : "items"}

))}
) ) : ( )}
{showEdit && profile && ( setShowEdit(false)} onUpdate={(updated) => setProfile(updated)} /> )} setExternalLink(null)} url={externalLink} /> setShowReportModal(false)} subjectDid={did} subjectHandle={profile?.handle} />
); }