import { useMemo, useRef, useState } from 'react'; import { Link, useSearchParams } from 'react-router'; import { ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; import { AssetViewer } from './AssetViewer'; import { SelectionBar } from './SelectionBar'; import { TimelineGrid } from './TimelineGrid'; import { PERSON_PARAM, personThumbUrl } from './shared'; import { useBucketAssets, usePeople, usePerson, useTimeBuckets } from './usePhotosData'; import { useSelection } from './useSelection'; // Faces. The wall of people, and one person's photos behind `?person=`. // // A person's photos come from the same bucketed timeline endpoint with `personId` set, so a face with 12,000 // photos loads the same way the main timeline does rather than as one enormous response. export const PeopleSection = () => { const [params] = useSearchParams(); const personId = params.get(PERSON_PARAM); return personId ? : ; }; const PeopleListView = () => { const { token } = useClient(); const { data, isLoading } = usePeople(); // Immich hides unnamed faces behind a toggle; the named ones are what anyone actually browses by. const [showUnnamed, setShowUnnamed] = useState(false); const people = useMemo( () => (data?.people ?? []).filter((person) => showUnnamed || person.name), [data, showUnnamed], ); return (

People

{people.length}
{isLoading &&

Loading…

} {!isLoading && people.length === 0 && (

No people yet — Immich populates this as faces are recognised.

)}
{people.map((person) => ( {person.name || 'Unnamed'} ))}
); }; const PersonDetailView = ({ personId }: { personId: string }) => { const { token } = useClient(); const [params] = useSearchParams(); const scrollRef = useRef(null); const [loaded, setLoaded] = useState([]); const { data: person } = usePerson(personId); const filter = useMemo(() => ({ personId }), [personId]); const { data: buckets } = useTimeBuckets(filter); const assetsByBucket = useBucketAssets(filter, loaded); const orderedIds = useMemo( () => (buckets ?? []).flatMap((bucket) => (assetsByBucket[bucket.timeBucket] ?? []).map((asset) => asset.id)), [buckets, assetsByBucket], ); const selection = useSelection(orderedIds); const back = (() => { const next = new URLSearchParams(params); next.delete(PERSON_PARAM); const search = next.toString(); return search ? `?${search}` : '?'; })(); const total = (buckets ?? []).reduce((sum, bucket) => sum + bucket.count, 0); return (
{selection.isSelecting ? ( ) : (

{person?.name || 'Unnamed'}

{total.toLocaleString()} items
)}
setLoaded((prev) => (prev.includes(bucket) ? prev : [...prev, bucket]))} token={token} selection={selection} scrollRef={scrollRef} />
); };