[desktop] People grouping - Finishing touches (#3817)
This commit is contained in:
@@ -38,7 +38,6 @@ import {
|
||||
getLocalTrashedFiles,
|
||||
sortFiles,
|
||||
} from "@/new/photos/services/files";
|
||||
import type { Person } from "@/new/photos/services/ml/people";
|
||||
import {
|
||||
filterSearchableFiles,
|
||||
setSearchCollectionsAndFiles,
|
||||
@@ -855,11 +854,6 @@ export default function Gallery() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectPerson = (person: Person | undefined) =>
|
||||
person
|
||||
? dispatch({ type: "showPerson", personID: person.id })
|
||||
: dispatch({ type: "showPeople" });
|
||||
|
||||
const handleOpenCollectionSelector = useCallback(
|
||||
(attributes: CollectionSelectorAttributes) => {
|
||||
setCollectionSelectorAttributes(attributes);
|
||||
@@ -879,7 +873,7 @@ export default function Gallery() {
|
||||
}
|
||||
|
||||
// `peopleState` will be undefined only when ML is disabled, otherwise it'll
|
||||
// be contain empty arrays (even if people are loading).
|
||||
// be present, with empty arrays, even if people data is still syncing.
|
||||
const showPeopleSectionButton = peopleState !== undefined;
|
||||
|
||||
return (
|
||||
@@ -981,7 +975,10 @@ export default function Gallery() {
|
||||
onShowSearchInput: () =>
|
||||
dispatch({ type: "enterSearchMode" }),
|
||||
onSelectSearchOption: handleSelectSearchOption,
|
||||
onSelectPerson: handleSelectPerson,
|
||||
onSelectPeople: () =>
|
||||
dispatch({ type: "showPeople" }),
|
||||
onSelectPerson: (personID) =>
|
||||
dispatch({ type: "showPerson", personID }),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -1004,7 +1001,8 @@ export default function Gallery() {
|
||||
? state.view.visiblePeople
|
||||
: undefined) ?? [],
|
||||
activePerson,
|
||||
onSelectPerson: handleSelectPerson,
|
||||
onSelectPerson: (personID) =>
|
||||
dispatch({ type: "showPerson", personID }),
|
||||
setCollectionNamerAttributes,
|
||||
setPhotoListHeader,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
@@ -1060,8 +1058,8 @@ export default function Gallery() {
|
||||
<GalleryEmptyState openUploader={openUploader} />
|
||||
) : !isInSearchMode &&
|
||||
!isFirstLoad &&
|
||||
barMode == "people" &&
|
||||
!activePerson ? (
|
||||
state.view.type == "people" &&
|
||||
!state.view.activePerson ? (
|
||||
<PeopleEmptyState />
|
||||
) : (
|
||||
<PhotoFrame
|
||||
|
||||
@@ -247,7 +247,7 @@ export const AttributedMiniDialog: React.FC<
|
||||
)}
|
||||
{children}
|
||||
<Stack
|
||||
sx={{ paddingBlockStart: "24px", gap: "12px" }}
|
||||
sx={{ paddingBlockStart: "24px", gap: "8px" }}
|
||||
direction={attributes.buttonDirection ?? "column"}
|
||||
>
|
||||
{errorIndicator}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UnstyledButton } from "./UnstyledButton";
|
||||
|
||||
export interface SearchPeopleListProps {
|
||||
people: Person[];
|
||||
onSelectPerson: (person: Person) => void;
|
||||
onSelectPerson: (personID: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ export const SearchPeopleList: React.FC<SearchPeopleListProps> = ({
|
||||
{people.slice(0, isSmallWidth ? 6 : 7).map((person) => (
|
||||
<SearchPersonButton
|
||||
key={person.id}
|
||||
onClick={() => onSelectPerson(person)}
|
||||
onClick={() => onSelectPerson(person.id)}
|
||||
>
|
||||
<FaceCropImageView
|
||||
faceID={person.displayFaceID}
|
||||
@@ -149,7 +149,7 @@ export const SuggestionFaceList: React.FC<SuggestionFaceListProps> = ({
|
||||
{faces.map(({ file, faceID }) => (
|
||||
<SuggestionFace key={faceID}>
|
||||
<FaceCropImageView
|
||||
placeholderDimension={65}
|
||||
placeholderDimension={87}
|
||||
{...{ file, faceID }}
|
||||
/>
|
||||
</SuggestionFace>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { assertionFailed } from "@/base/assert";
|
||||
import { useIsSmallWidth } from "@/base/hooks";
|
||||
import { ItemCard, PreviewItemTile } from "@/new/photos/components/Tiles";
|
||||
import { isMLSupported, mlStatusSnapshot } from "@/new/photos/services/ml";
|
||||
import type { Person } from "@/new/photos/services/ml/people";
|
||||
import { searchOptionsForString } from "@/new/photos/services/search";
|
||||
import type { SearchOption } from "@/new/photos/services/search/types";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
@@ -69,13 +68,14 @@ export interface SearchBarProps {
|
||||
*/
|
||||
onSelectSearchOption: (o: SearchOption | undefined) => void;
|
||||
/**
|
||||
* Called when the user selects a person shown in the empty state view, or
|
||||
* clicks the people list header itself.
|
||||
*
|
||||
* @param person The selected person, or `undefined` if the user clicked the
|
||||
* generic people header.
|
||||
* Called when the user selects the generic "People" header in the empty
|
||||
* state view.
|
||||
*/
|
||||
onSelectPerson: (person: Person | undefined) => void;
|
||||
onSelectPeople: () => void;
|
||||
/**
|
||||
* Called when the user selects a person shown in the empty state view.
|
||||
*/
|
||||
onSelectPerson: (personID: string | undefined) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +127,7 @@ const MobileSearchArea: React.FC<MobileSearchAreaProps> = ({ onSearch }) => (
|
||||
const SearchInput: React.FC<Omit<SearchBarProps, "onShowSearchInput">> = ({
|
||||
isInSearchMode,
|
||||
onSelectSearchOption,
|
||||
onSelectPeople,
|
||||
onSelectPerson,
|
||||
}) => {
|
||||
// A ref to the top level Select.
|
||||
@@ -185,9 +186,14 @@ const SearchInput: React.FC<Omit<SearchBarProps, "onShowSearchInput">> = ({
|
||||
onSelectSearchOption(undefined);
|
||||
};
|
||||
|
||||
const handleSelectPerson = (person: Person | undefined) => {
|
||||
const handleSelectPeople = () => {
|
||||
resetSearch();
|
||||
onSelectPerson(person);
|
||||
onSelectPeople();
|
||||
};
|
||||
|
||||
const handleSelectPerson = (personID: string | undefined) => {
|
||||
resetSearch();
|
||||
onSelectPerson(personID);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
@@ -219,7 +225,10 @@ const SearchInput: React.FC<Omit<SearchBarProps, "onShowSearchInput">> = ({
|
||||
placeholder={t("search_hint")}
|
||||
noOptionsMessage={({ inputValue }) =>
|
||||
shouldShowEmptyState(inputValue) ? (
|
||||
<EmptyState onSelectPerson={handleSelectPerson} />
|
||||
<EmptyState
|
||||
onSelectPeople={handleSelectPeople}
|
||||
onSelectPerson={handleSelectPerson}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
@@ -373,9 +382,9 @@ const shouldShowEmptyState = (inputValue: string) => {
|
||||
* The view shown in the menu area when the user has not typed anything in the
|
||||
* search box.
|
||||
*/
|
||||
const EmptyState: React.FC<Pick<SearchBarProps, "onSelectPerson">> = ({
|
||||
onSelectPerson,
|
||||
}) => {
|
||||
const EmptyState: React.FC<
|
||||
Pick<SearchBarProps, "onSelectPeople" | "onSelectPerson">
|
||||
> = ({ onSelectPeople, onSelectPerson }) => {
|
||||
const mlStatus = useMLStatusSnapshot();
|
||||
const people = usePeopleStateSnapshot()?.visiblePeople;
|
||||
|
||||
@@ -397,7 +406,6 @@ const EmptyState: React.FC<Pick<SearchBarProps, "onSelectPerson">> = ({
|
||||
label = t("indexing_fetching", mlStatus);
|
||||
break;
|
||||
case "clustering":
|
||||
// TODO-Cluster
|
||||
label = t("indexing_people", mlStatus);
|
||||
break;
|
||||
case "done":
|
||||
@@ -409,9 +417,7 @@ const EmptyState: React.FC<Pick<SearchBarProps, "onSelectPerson">> = ({
|
||||
<Box sx={{ textAlign: "left" }}>
|
||||
{people && people.length > 0 && (
|
||||
<>
|
||||
<SearchPeopleHeader
|
||||
onClick={() => onSelectPerson(undefined)}
|
||||
/>
|
||||
<SearchPeopleHeader onClick={onSelectPeople} />
|
||||
<SearchPeopleList {...{ people, onSelectPerson }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -95,10 +95,9 @@ export interface GalleryBarImplProps {
|
||||
*/
|
||||
activePerson: Person | undefined;
|
||||
/**
|
||||
* Called when the selection should be moved to a new person in the bar, or
|
||||
* reset to the default state (when {@link person} is `undefined`).
|
||||
* Called when the selection should be moved to a new person in the bar.
|
||||
*/
|
||||
onSelectPerson: (person: Person | undefined) => void;
|
||||
onSelectPerson: (personID: string) => void;
|
||||
}
|
||||
|
||||
export const GalleryBarImpl: React.FC<GalleryBarImplProps> = ({
|
||||
@@ -423,7 +422,7 @@ type ItemData =
|
||||
type: "people";
|
||||
people: Person[];
|
||||
activePerson: Person | undefined;
|
||||
onSelectPerson: (person: Person) => void;
|
||||
onSelectPerson: (personID: string) => void;
|
||||
};
|
||||
|
||||
const getItemCount = (data: ItemData) => {
|
||||
@@ -578,7 +577,7 @@ const ActiveIndicator = styled("div")`
|
||||
interface PersonCardProps {
|
||||
person: Person;
|
||||
activePerson: Person | undefined;
|
||||
onSelectPerson: (person: Person) => void;
|
||||
onSelectPerson: (personID: string) => void;
|
||||
}
|
||||
|
||||
const PersonCard: React.FC<PersonCardProps> = ({
|
||||
@@ -591,7 +590,7 @@ const PersonCard: React.FC<PersonCardProps> = ({
|
||||
TileComponent={BarItemTile}
|
||||
coverFile={person.displayFaceFile}
|
||||
coverFaceID={person.displayFaceID}
|
||||
onClick={() => onSelectPerson(person)}
|
||||
onClick={() => onSelectPerson(person.id)}
|
||||
>
|
||||
{person.name && <CardText>{person.name}</CardText>}
|
||||
</ItemCard>
|
||||
|
||||
@@ -40,6 +40,7 @@ import HideImageOutlinedIcon from "@mui/icons-material/HideImageOutlined";
|
||||
import ListAltOutlinedIcon from "@mui/icons-material/ListAltOutlined";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import RestoreIcon from "@mui/icons-material/Restore";
|
||||
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
@@ -90,26 +91,27 @@ export const PeopleHeader: React.FC<PeopleHeaderProps> = ({
|
||||
<GalleryItemsHeaderAdapter>
|
||||
<SpaceBetweenFlex>
|
||||
{person.type == "cgroup" ? (
|
||||
<CGroupPersonHeader
|
||||
person={person}
|
||||
{...{ onSelectPerson }}
|
||||
/>
|
||||
person.isHidden ? (
|
||||
<IgnoredPersonHeader person={person} />
|
||||
) : (
|
||||
<CGroupPersonHeader person={person} />
|
||||
)
|
||||
) : (
|
||||
<ClusterPersonHeader person={person} {...{ people }} />
|
||||
<ClusterPersonHeader
|
||||
person={person}
|
||||
{...{ people, onSelectPerson }}
|
||||
/>
|
||||
)}
|
||||
</SpaceBetweenFlex>
|
||||
</GalleryItemsHeaderAdapter>
|
||||
);
|
||||
};
|
||||
|
||||
type CGroupPersonHeaderProps = Pick<PeopleHeaderProps, "onSelectPerson"> & {
|
||||
interface CGroupPersonHeaderProps {
|
||||
person: CGroupPerson;
|
||||
};
|
||||
}
|
||||
|
||||
const CGroupPersonHeader: React.FC<CGroupPersonHeaderProps> = ({
|
||||
person,
|
||||
onSelectPerson,
|
||||
}) => {
|
||||
const CGroupPersonHeader: React.FC<CGroupPersonHeaderProps> = ({ person }) => {
|
||||
const cgroup = person.cgroup;
|
||||
|
||||
const { showMiniDialog } = useAppContext();
|
||||
@@ -130,17 +132,13 @@ const CGroupPersonHeader: React.FC<CGroupPersonHeaderProps> = ({
|
||||
continue: {
|
||||
text: t("reset"),
|
||||
color: "primary",
|
||||
action: async () => {
|
||||
await deleteCGroup(cgroup);
|
||||
// Reset the selection to the default state.
|
||||
onSelectPerson(undefined);
|
||||
},
|
||||
action: () => deleteCGroup(cgroup),
|
||||
},
|
||||
});
|
||||
|
||||
// While technically it is possible for the cgroup not to have a name, logic
|
||||
// wise we shouldn't be ending up here without a name (this state is
|
||||
// expected to be reached only for named persons).
|
||||
// expected to be reached only for unignored named persons).
|
||||
const name = cgroup.data.name ?? "";
|
||||
|
||||
return (
|
||||
@@ -198,12 +196,50 @@ const CGroupPersonHeader: React.FC<CGroupPersonHeaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
type ClusterPersonHeaderProps = Pick<PeopleHeaderProps, "people"> & {
|
||||
interface IgnoredPersonHeaderProps {
|
||||
person: CGroupPerson;
|
||||
}
|
||||
|
||||
const IgnoredPersonHeader: React.FC<IgnoredPersonHeaderProps> = ({
|
||||
person,
|
||||
}) => {
|
||||
const cgroup = person.cgroup;
|
||||
|
||||
const handleUndoIgnore = useWrapAsyncOperation(() => deleteCGroup(cgroup));
|
||||
|
||||
return (
|
||||
<>
|
||||
<GalleryItemsSummary
|
||||
name={pt("Ignored")}
|
||||
nameProps={{ color: "text.muted" }}
|
||||
fileCount={person.fileIDs.length}
|
||||
/>
|
||||
<OverflowMenu
|
||||
ariaControls={"person-options"}
|
||||
triggerButtonIcon={<MoreHorizIcon />}
|
||||
>
|
||||
<OverflowMenuOption
|
||||
startIcon={<VisibilityOutlinedIcon />}
|
||||
centerAlign
|
||||
onClick={handleUndoIgnore}
|
||||
>
|
||||
{pt("Show person")}
|
||||
</OverflowMenuOption>
|
||||
</OverflowMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type ClusterPersonHeaderProps = Pick<
|
||||
PeopleHeaderProps,
|
||||
"people" | "onSelectPerson"
|
||||
> & {
|
||||
person: ClusterPerson;
|
||||
};
|
||||
|
||||
const ClusterPersonHeader: React.FC<ClusterPersonHeaderProps> = ({
|
||||
people,
|
||||
onSelectPerson,
|
||||
person,
|
||||
}) => {
|
||||
const cluster = person.cluster;
|
||||
@@ -264,22 +300,19 @@ const ClusterPersonHeader: React.FC<ClusterPersonHeaderProps> = ({
|
||||
|
||||
<AddPersonDialog
|
||||
{...addPersonVisibilityProps}
|
||||
{...{ people, cluster }}
|
||||
{...{ people, onSelectPerson, cluster }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type AddPersonDialogProps = ModalVisibilityProps & {
|
||||
/**
|
||||
* The list of people from show the existing named people.
|
||||
*/
|
||||
people: Person[];
|
||||
/**
|
||||
* The cluster to add to the selected person (existing or new).
|
||||
*/
|
||||
cluster: FaceCluster;
|
||||
};
|
||||
type AddPersonDialogProps = ModalVisibilityProps &
|
||||
Pick<PeopleHeaderProps, "people" | "onSelectPerson"> & {
|
||||
/**
|
||||
* The cluster to add to the selected person (existing or new).
|
||||
*/
|
||||
cluster: FaceCluster;
|
||||
};
|
||||
|
||||
/**
|
||||
* A dialog allowing the user to select one of the existing named persons they
|
||||
@@ -290,6 +323,7 @@ const AddPersonDialog: React.FC<AddPersonDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
people,
|
||||
onSelectPerson,
|
||||
cluster,
|
||||
}) => {
|
||||
const isFullScreen = useMediaQuery("(max-width: 490px)");
|
||||
@@ -302,14 +336,19 @@ const AddPersonDialog: React.FC<AddPersonDialogProps> = ({
|
||||
|
||||
const handleAddPerson = () => setOpenNameInput(true);
|
||||
|
||||
const handleSelectPerson = useWrapAsyncOperation((id: string) =>
|
||||
addClusterToCGroup(
|
||||
ensure(cgroupPeople.find((p) => p.id == id)).cgroup,
|
||||
cluster,
|
||||
),
|
||||
const handleAddPersonBySelect = useWrapAsyncOperation(
|
||||
async (personID: string) => {
|
||||
onClose();
|
||||
const person = ensure(cgroupPeople.find((p) => p.id == personID));
|
||||
await addClusterToCGroup(person.cgroup, cluster);
|
||||
onSelectPerson(personID);
|
||||
},
|
||||
);
|
||||
|
||||
const handleAddPersonWithName = (name: string) => addCGroup(name, cluster);
|
||||
const handleAddPersonWithName = async (name: string) => {
|
||||
const personID = await addCGroup(name, cluster);
|
||||
onSelectPerson(personID);
|
||||
};
|
||||
|
||||
// [Note: Calling setState during rendering]
|
||||
//
|
||||
@@ -350,7 +389,7 @@ const AddPersonDialog: React.FC<AddPersonDialogProps> = ({
|
||||
<PersonButton
|
||||
key={person.id}
|
||||
person={person}
|
||||
onPersonClick={handleSelectPerson}
|
||||
onPersonClick={handleAddPersonBySelect}
|
||||
/>
|
||||
))}
|
||||
</DialogContent_>
|
||||
|
||||
@@ -50,7 +50,7 @@ export const PeopleEmptyState: React.FC = () => {
|
||||
const message =
|
||||
mlStatus?.phase == "done"
|
||||
? pt(
|
||||
"People will be shown here after there are sufficient photos of a person",
|
||||
"People will be shown here when there are sufficient photos of a person",
|
||||
)
|
||||
: pt("Syncing...");
|
||||
|
||||
|
||||
@@ -585,8 +585,28 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
|
||||
state.archivedCollectionIDs,
|
||||
),
|
||||
});
|
||||
case "setPeopleState":
|
||||
return { ...state, peopleState: action.peopleState };
|
||||
case "setPeopleState": {
|
||||
const peopleState = action.peopleState;
|
||||
|
||||
if (state.view?.type != "people") return { ...state, peopleState };
|
||||
|
||||
const { view, extraVisiblePerson } = derivePeopleView(
|
||||
peopleState,
|
||||
state.tempDeletedFileIDs,
|
||||
state.tempHiddenFileIDs,
|
||||
state.selectedPersonID,
|
||||
state.extraVisiblePerson,
|
||||
);
|
||||
const filteredFiles = derivePeopleFilteredFiles(state.files, view);
|
||||
return {
|
||||
...state,
|
||||
peopleState,
|
||||
selectedPersonID: view.activePerson?.id,
|
||||
extraVisiblePerson,
|
||||
view,
|
||||
filteredFiles,
|
||||
};
|
||||
}
|
||||
case "markTempDeleted":
|
||||
return refreshingFilteredFilesIfShowingAlbumsOrHiddenAlbums({
|
||||
...state,
|
||||
@@ -1101,6 +1121,15 @@ const derivePeopleView = (
|
||||
visiblePeople = filterTemp(visiblePeople);
|
||||
}
|
||||
|
||||
// We might have an extraVisiblePerson that is now part of the visible ones
|
||||
// when the user un-ignores a person. If that's the case (which we can
|
||||
// detect by its absence from the list of underlying people, since its ID
|
||||
// would've changed), clear it out, otherwise we'll end up with two entries.
|
||||
if (extraVisiblePerson) {
|
||||
if (!people.find((p) => p.id == extraVisiblePerson?.id))
|
||||
extraVisiblePerson = undefined;
|
||||
}
|
||||
|
||||
const findByIDIn = (ps: Person[]) =>
|
||||
ps.find((p) => p.id == selectedPersonID);
|
||||
let activePerson = findByIDIn(visiblePeople);
|
||||
@@ -1212,8 +1241,8 @@ const deriveAlbumsFilteredFiles = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a new state by recomputing the {@link filteredFiles} property if when
|
||||
* we're showing the "hidden-albums" view.
|
||||
* Return a new state by recomputing the {@link filteredFiles} property if we're
|
||||
* showing the "hidden-albums" view.
|
||||
*
|
||||
* See {@link refreshingFilteredFilesIfShowingAlbums} for more details.
|
||||
*/
|
||||
|
||||
@@ -704,10 +704,12 @@ const regenerateFaceCropsIfNeeded = async (file: EnteFile) => {
|
||||
* @param name Name of the new cgroup user entity.
|
||||
*
|
||||
* @param cluster The underlying cluster to use to populate the cgroup.
|
||||
*
|
||||
* @returns The entity ID of the newly created cgroup.
|
||||
*/
|
||||
export const addCGroup = async (name: string, cluster: FaceCluster) => {
|
||||
const masterKey = await masterKeyFromSession();
|
||||
await addUserEntity(
|
||||
const id = await addUserEntity(
|
||||
"cgroup",
|
||||
{
|
||||
name,
|
||||
@@ -716,7 +718,8 @@ export const addCGroup = async (name: string, cluster: FaceCluster) => {
|
||||
},
|
||||
masterKey,
|
||||
);
|
||||
return mlSync();
|
||||
await mlSync();
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -110,7 +110,7 @@ export interface CGroupUserEntityData {
|
||||
* transmission and storage.
|
||||
*/
|
||||
export type Person = (
|
||||
| { type: "cgroup"; cgroup: CGroup }
|
||||
| { type: "cgroup"; cgroup: CGroup; isHidden: boolean }
|
||||
| { type: "cluster"; cluster: FaceCluster }
|
||||
) & {
|
||||
/**
|
||||
@@ -252,15 +252,13 @@ export const reconstructPeopleState = async (): Promise<PeopleState> => {
|
||||
const cgroups = await savedCGroups();
|
||||
const cgroupPeople: Interim = cgroups.map((cgroup) => {
|
||||
const { id, data } = cgroup;
|
||||
const { name, isHidden, assigned } = data;
|
||||
const { name, assigned } = data;
|
||||
|
||||
// Hidden cgroups are clusters specifically marked so as to not be shown
|
||||
// in the UI.
|
||||
if (isHidden) return undefined;
|
||||
let isHidden = data.isHidden;
|
||||
|
||||
// Older versions of the mobile app marked hidden cgroups by setting
|
||||
// their name to an empty string.
|
||||
if (!name) return undefined;
|
||||
if (!name) isHidden = true;
|
||||
|
||||
// Person faces from all the clusters assigned to this cgroup, sorted by
|
||||
// recency (then score).
|
||||
@@ -301,6 +299,7 @@ export const reconstructPeopleState = async (): Promise<PeopleState> => {
|
||||
fileIDs,
|
||||
displayFaceID,
|
||||
displayFaceFile,
|
||||
isHidden,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -332,8 +331,21 @@ export const reconstructPeopleState = async (): Promise<PeopleState> => {
|
||||
const people = sorted(cgroupPeople).concat(sorted(clusterPeople));
|
||||
|
||||
const visiblePeople = people.filter((p) => {
|
||||
// Ignore local only clusters with too few visible faces.
|
||||
if (p.type == "cluster" && p.cluster.faces.length < 10) return false;
|
||||
switch (p.type) {
|
||||
case "cgroup":
|
||||
// Hidden cgroups are clusters specifically marked so as to not
|
||||
// be shown in the UI. The user can still see them from within
|
||||
// file info if they wish.
|
||||
if (p.isHidden) return false;
|
||||
break;
|
||||
|
||||
case "cluster":
|
||||
// Ignore local only clusters with too few visible faces.
|
||||
if (p.cluster.faces.length < 10) return false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Show it.
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -418,7 +430,6 @@ export const _suggestionsAndChoicesForPerson = async (
|
||||
const startTime = Date.now();
|
||||
|
||||
const personClusters = person.cgroup.data.assigned;
|
||||
const personClusterIDs = new Set(personClusters.map(({ id }) => id));
|
||||
const rejectedClusterIDs = new Set(
|
||||
await savedRejectedClustersForCGroup(person.cgroup.id),
|
||||
);
|
||||
@@ -461,9 +472,6 @@ export const _suggestionsAndChoicesForPerson = async (
|
||||
// Ignore singleton clusters.
|
||||
if (faces.length < 2) continue;
|
||||
|
||||
// TODO-Cluster sanity check, remove after dev
|
||||
if (personClusterIDs.has(id)) assertionFailed();
|
||||
|
||||
const sampledOtherEmbeddings = randomSample(faces, 50)
|
||||
.map((id) => embeddingByFaceID.get(id))
|
||||
.filter((e) => !!e);
|
||||
@@ -676,21 +684,11 @@ export const _applyPersonSuggestionUpdates = async (
|
||||
for (const [clusterID, assigned] of updates.entries()) {
|
||||
switch (assigned) {
|
||||
case true /* assign */:
|
||||
// TODO-Cluster sanity check, remove after wrapping up dev
|
||||
if (assignedClusters.find(({ id }) => id == clusterID)) {
|
||||
assertionFailed();
|
||||
}
|
||||
|
||||
assign(clusterID);
|
||||
unrejectIfNeeded(clusterID);
|
||||
break;
|
||||
|
||||
case false /* reject */:
|
||||
// TODO-Cluster sanity check, remove after wrapping up dev
|
||||
if (rejectedClusterIDs.includes(clusterID)) {
|
||||
assertionFailed();
|
||||
}
|
||||
|
||||
unassignIfNeeded(clusterID);
|
||||
reject(clusterID);
|
||||
break;
|
||||
|
||||
@@ -173,6 +173,8 @@ const isGzipped = (type: EntityType) => type == "cgroup";
|
||||
*
|
||||
* @param masterKey The user's masterKey, which is is used to encrypt and
|
||||
* decrypt the entity key.
|
||||
*
|
||||
* @returns The ID of the newly created entity.
|
||||
*/
|
||||
export const addUserEntity = async (
|
||||
type: EntityType,
|
||||
|
||||
@@ -150,22 +150,25 @@ export const userEntityDiff = async (
|
||||
|
||||
/**
|
||||
* Create a new user entity with the given {@link type} on remote.
|
||||
*
|
||||
* @returns The ID of the newly created entity.
|
||||
*/
|
||||
export const postUserEntity = async (
|
||||
type: EntityType,
|
||||
{ encryptedData, decryptionHeader }: EncryptedBlobB64,
|
||||
) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/user-entity/entity"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
encryptedData: encryptedData,
|
||||
header: decryptionHeader,
|
||||
}),
|
||||
) => {
|
||||
const res = await fetch(await apiURL("/user-entity/entity"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
encryptedData: encryptedData,
|
||||
header: decryptionHeader,
|
||||
}),
|
||||
);
|
||||
});
|
||||
ensureOk(res);
|
||||
return z.object({ id: z.string() }).parse(await res.json()).id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing remote user entity with the given {@link id} and
|
||||
|
||||
Reference in New Issue
Block a user