[desktop] Show option to enable face indexing for beta users (#1945)

This commit is contained in:
Manav Rathi
2024-05-31 15:56:16 +05:30
committed by GitHub
6 changed files with 148 additions and 9 deletions

View File

@@ -18,11 +18,11 @@ import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import { Trans } from "react-i18next";
import { canEnableFaceIndexing } from "services/face/indexer";
import {
getFaceSearchEnabledStatus,
updateFaceSearchEnabledStatus,
} from "services/userService";
import { isInternalUserForML } from "utils/user";
export const MLSearchSettings = ({ open, onClose, onRootClose }) => {
const {
@@ -60,6 +60,7 @@ export const MLSearchSettings = ({ open, onClose, onRootClose }) => {
const enableFaceSearch = async () => {
try {
startLoading();
// Update the consent flag.
await updateFaceSearchEnabledStatus(true);
updateMlSearchEnabled(true);
closeEnableFaceSearch();
@@ -83,7 +84,6 @@ export const MLSearchSettings = ({ open, onClose, onRootClose }) => {
const disableFaceSearch = async () => {
try {
startLoading();
await updateFaceSearchEnabledStatus(false);
await disableMlSearch();
finishLoading();
} catch (e) {
@@ -258,6 +258,12 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {
// const showDetails = () =>
// openLink("https://ente.io/blog/desktop-ml-beta", true);
const [canEnable, setCanEnable] = useState(false);
useEffect(() => {
canEnableFaceIndexing().then((v) => setCanEnable(v));
}, []);
return (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
@@ -273,7 +279,7 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {
We're putting finishing touches, coming back soon!
</Typography>
</Box>
{isInternalUserForML() && (
{canEnable && (
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}

View File

@@ -87,6 +87,7 @@ import {
import downloadManager from "services/download";
import { syncCLIPEmbeddings } from "services/embeddingService";
import { syncEntities } from "services/entityService";
import { fetchAndSaveFeatureFlagsIfNeeded } from "services/feature-flag";
import { getLocalFiles, syncFiles } from "services/fileService";
import locationSearchService from "services/locationSearchService";
import { getLocalTrashedFiles, syncTrash } from "services/trashService";
@@ -713,6 +714,7 @@ export default function Gallery() {
await syncTrash(collections, setTrashedFiles);
await syncEntities();
await syncMapEnabled();
fetchAndSaveFeatureFlagsIfNeeded();
const electron = globalThis.electron;
if (electron) {
await syncCLIPEmbeddings();

View File

@@ -2,6 +2,7 @@ import { ComlinkWorker } from "@/next/worker/comlink-worker";
import { ensure } from "@/utils/ensure";
import { wait } from "@/utils/promise";
import { type Remote } from "comlink";
import { isBetaUser, isInternalUser } from "services/feature-flag";
import { getAllLocalFiles } from "services/fileService";
import mlWorkManager from "services/machineLearning/mlWorkManager";
import type { EnteFile } from "types/file";
@@ -194,6 +195,13 @@ export const unidentifiedFaceIDs = async (
return index?.faceEmbedding.faces.map((f) => f.faceID) ?? [];
};
/**
* Return true if we should show an option to the user to allow them to enable
* face search in the UI.
*/
export const canEnableFaceIndexing = async () =>
isInternalUserForML() || (await isInternalUser()) || (await isBetaUser());
/**
* Return true if the user has enabled face indexing in the app's settings.
*
@@ -204,12 +212,7 @@ export const unidentifiedFaceIDs = async (
* hand, denotes whether or not indexing is enabled on the current client.
*/
export const isFaceIndexingEnabled = async () => {
if (isInternalUserForML()) {
return localStorage.getItem("faceIndexingEnabled") == "1";
}
// Force disabled for everyone else while we finalize it to avoid redundant
// reindexing for users.
return false;
return localStorage.getItem("faceIndexingEnabled") == "1";
};
/**

View File

@@ -0,0 +1,111 @@
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { apiOrigin } from "@ente/shared/network/api";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
let _fetchTimeout: ReturnType<typeof setTimeout> | undefined;
let _haveFetched = false;
/**
* Fetch feature flags (potentially user specific) from remote and save them in
* local storage for subsequent lookup.
*
* It fetches only once per session, and so is safe to call as arbitrarily many
* times. Remember to call {@link clearFeatureFlagSessionState} on logout to
* forget that we've already fetched so that these can be fetched again on the
* subsequent login.
*/
export const fetchAndSaveFeatureFlagsIfNeeded = () => {
if (_haveFetched) return;
if (_fetchTimeout) return;
// Not critical, so fetch these after some delay.
_fetchTimeout = setTimeout(() => {
_fetchTimeout = undefined;
void fetchAndSaveFeatureFlags().then(() => {
_haveFetched = true;
});
}, 2000);
};
export const clearFeatureFlagSessionState = () => {
if (_fetchTimeout) {
clearTimeout(_fetchTimeout);
_fetchTimeout = undefined;
}
_haveFetched = false;
};
/**
* Fetch feature flags (potentially user specific) from remote and save them in
* local storage for subsequent lookup.
*/
const fetchAndSaveFeatureFlags = () =>
fetchFeatureFlags()
.then((res) => res.text())
.then(saveFlagJSONString);
const fetchFeatureFlags = async () => {
const url = `${apiOrigin()}/remote-store/feature-flags`;
const res = await fetch(url, {
headers: {
"X-Auth-Token": ensure(getToken()),
},
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
return res;
};
const saveFlagJSONString = (s: string) =>
localStorage.setItem("remoteFeatureFlags", s);
const remoteFeatureFlags = () => {
const s = localStorage.getItem("remoteFeatureFlags");
if (!s) return undefined;
return JSON.parse(s);
};
const remoteFeatureFlagsFetchingIfNeeded = async () => {
let ff = await remoteFeatureFlags();
if (!ff) {
try {
await fetchAndSaveFeatureFlags();
ff = await remoteFeatureFlags();
} catch (e) {
log.warn("Ignoring error when fetching feature flags", e);
}
}
return ff;
};
/**
* Return `true` if the current user is marked as an "internal" user.
*/
export const isInternalUser = async () => {
// TODO: Dedup
const flags = await remoteFeatureFlagsFetchingIfNeeded();
// TODO(MR): Use Yup here
if (
flags &&
typeof flags === "object" &&
"internalUser" in flags &&
typeof flags.internalUser == "boolean"
)
return flags.internalUser;
return false;
};
/**
* Return `true` if the current user is marked as a "beta" user.
*/
export const isBetaUser = async () => {
const flags = await remoteFeatureFlagsFetchingIfNeeded();
// TODO(MR): Use Yup here
if (
flags &&
typeof flags === "object" &&
"betaUser" in flags &&
typeof flags.betaUser == "boolean"
)
return flags.betaUser;
return false;
};

View File

@@ -4,6 +4,7 @@ import { clipService } from "services/clip-service";
import DownloadManager from "./download";
import exportService from "./export";
import { clearFaceData } from "./face/db";
import { clearFeatureFlagSessionState } from "./feature-flag";
import mlWorkManager from "./machineLearning/mlWorkManager";
/**
@@ -19,6 +20,12 @@ export const photosLogout = async () => {
await accountLogout();
try {
clearFeatureFlagSessionState();
} catch (e) {
ignoreError("feature-flag", e);
}
try {
await DownloadManager.logout();
} catch (e) {

View File

@@ -1,3 +1,13 @@
/**
* Return the origin (scheme, host, port triple) that should be used for making
* API requests to museum.
*
* This defaults to api.ente.io, Ente's own servers, but can be overridden when
* running locally by setting the `NEXT_PUBLIC_ENTE_ENDPOINT` environment
* variable.
*/
export const apiOrigin = () => getEndpoint();
export const getEndpoint = () => {
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
if (endpoint) {