[desktop] Show option to enable face indexing for beta users (#1945)
This commit is contained in:
@@ -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"}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
111
web/apps/photos/src/services/feature-flag.ts
Normal file
111
web/apps/photos/src/services/feature-flag.ts
Normal 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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user