/** * Renders the header of a user's profile, where the signed-in user also edits * their own avatar and email. * * @module */ import { useRef, useState } from "react"; import { Link } from "react-router"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../../config/api.ts"; import type { Wire } from "../../../shared/api.ts"; import type { PublicUser, SelfUser, UpdateUserRequest, } from "../../../shared/user.ts"; import { expectOk, parseAPIResponse } from "../../utils/shared/api.ts"; import { deserializeSelfUser } from "../../utils/shared/deserialize.ts"; import { useAuth } from "../../hooks/auth/useAuth.ts"; import { Avatar } from "../shared/Avatar.tsx"; import { ErrorCard } from "../shared/ErrorCard.tsx"; import { FollowUserButton } from "../social/FollowButton.tsx"; import { InlineEdit } from "../form/InlineEdit.tsx"; import { ProfileOwnActions } from "./ProfileOwnActions.tsx"; import { RoleChip } from "./RoleSwitcher.tsx"; import { userPath } from "../../../shared/paths.ts"; import { splitHandle } from "../../../shared/validation.ts"; import { homeUrl } from "../../utils/auth/home.ts"; /** Passed to {@link ProfileHeader} by `UserPublicProfile`, once loaded. */ export interface ProfileHeaderProps { user: PublicUser; isOwnProfile: boolean; /** Called with the user as a new avatar left them. */ onUserChange: (user: PublicUser) => void; } /** * Renders the avatar, username, role and inviter, and for a user from another * instance, a link to their profile there. Visitors get a follow button. On * their own profile, users change their avatar (`PUT /api/me/avatar`) and * email (`PATCH /api/me`), unless their account comes from another instance, and invite * someone; their settings, the site's tools and logging out are in the * header's avatar menu. */ export function ProfileHeader( { user, isOwnProfile, onUserChange }: ProfileHeaderProps, ): React.JSX.Element { const { user: me, authFetch, updateSession } = useAuth(); const [uploading, setUploading] = useState(false); const [avatarError, setAvatarError] = useState(null); const fileInputRef = useRef(null); const { name, host } = splitHandle(user.username); // A user from another instance changes their avatar and email there. const editable = isOwnProfile && !host; const handleAvatarUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setAvatarError(null); setUploading(true); try { const formData = new FormData(); formData.append("file", file); const res = await authFetch(`${API_URL}/api/me/avatar`, { method: "PUT", body: formData, }); const body = parseAPIResponse>(await res.json()); if (!body.success) { setAvatarError(body.error.message); return; } const self = deserializeSelfUser(body.data); updateSession({ user: self }); onUserChange(self); } catch { setAvatarError(t`Upload failed`); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ""; } }; const handleEmailSave = async (email: string) => { await expectOk>( await authFetch(`${API_URL}/api/me`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email } satisfies UpdateUserRequest), }), ); if (me) updateSession({ user: { ...me, email } }); }; return (
{editable && ( )}

{name} {host && @{host}}

{user.invitedByUsername ? (

invited by{" "} @{user.invitedByUsername}

) // Without an inviter, a local user was here first; one from a // trusted instance just came in. : !host && (

O.G.

)} {host && (

Account on{" "} {host}

)} {editable && ( {me?.email ?? t`Add email…`} )} {avatarError && ( )} {!isOwnProfile && ( )} {isOwnProfile && }
); }