From 3ad8f732893c4bf34271cbc5149eb753adfb9023 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 14:06:06 +0530 Subject: [PATCH 01/10] mandate --- web/apps/photos/src/services/feature-flag.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 web/apps/photos/src/services/feature-flag.ts diff --git a/web/apps/photos/src/services/feature-flag.ts b/web/apps/photos/src/services/feature-flag.ts new file mode 100644 index 0000000000..c6d722bf0f --- /dev/null +++ b/web/apps/photos/src/services/feature-flag.ts @@ -0,0 +1,15 @@ +/** + * Fetch feature flags (potentially user specific) from remote and save them in + * local storage for subsequent lookup. + */ +export const fetchAndSaveFeatureFlags = async () => {}; + +/** + * Return `true` if the current user is marked as an "internal" user. + */ +export const isInternalUser = async () => {}; + +/** + * Return `true` if the current user is marked as an "beta" user. + */ +export const isBetaUser = async () => {}; From c8d30323e4e91c571adcfd6ee0ac7f04f21b55a2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 14:11:16 +0530 Subject: [PATCH 02/10] Trigger --- web/apps/photos/src/pages/gallery/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index d375e48fc8..65857d5f0a 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -87,6 +87,7 @@ import { import downloadManager from "services/download"; import { syncCLIPEmbeddings } from "services/embeddingService"; import { syncEntities } from "services/entityService"; +import { fetchAndSaveFeatureFlags } from "services/feature-flag"; import { getLocalFiles, syncFiles } from "services/fileService"; import locationSearchService from "services/locationSearchService"; import { getLocalTrashedFiles, syncTrash } from "services/trashService"; @@ -340,6 +341,7 @@ export default function Gallery() { return; } preloadImage("/images/subscription-card-background"); + let ffTimeout: ReturnType | undefined; const electron = globalThis.electron; const main = async () => { const valid = await validateKey(); @@ -383,6 +385,11 @@ export default function Gallery() { syncInterval.current = setInterval(() => { syncWithRemote(false, true); }, SYNC_INTERVAL_IN_MICROSECONDS); + // Not critical, so fetch these after some delay. + ffTimeout = setTimeout(() => { + ffTimeout = undefined; + void fetchAndSaveFeatureFlags(); + }, 5000); if (electron) { // void clipService.setupOnFileUploadListener(); electron.onMainWindowFocus(() => syncWithRemote(false, true)); @@ -391,6 +398,7 @@ export default function Gallery() { main(); return () => { clearInterval(syncInterval.current); + if (ffTimeout) clearTimeout(ffTimeout); if (electron) { electron.onMainWindowFocus(undefined); clipService.removeOnFileUploadListener(); From 72a3f7f17a1401c37b62d4a1781780f038a21e77 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 14:25:12 +0530 Subject: [PATCH 03/10] Reduce noise in UI layer --- web/apps/photos/src/pages/gallery/index.tsx | 10 ++---- web/apps/photos/src/services/feature-flag.ts | 34 +++++++++++++++++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 65857d5f0a..5062aa6b6f 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -87,7 +87,7 @@ import { import downloadManager from "services/download"; import { syncCLIPEmbeddings } from "services/embeddingService"; import { syncEntities } from "services/entityService"; -import { fetchAndSaveFeatureFlags } from "services/feature-flag"; +import { fetchAndSaveFeatureFlagsIfNeeded } from "services/feature-flag"; import { getLocalFiles, syncFiles } from "services/fileService"; import locationSearchService from "services/locationSearchService"; import { getLocalTrashedFiles, syncTrash } from "services/trashService"; @@ -341,7 +341,6 @@ export default function Gallery() { return; } preloadImage("/images/subscription-card-background"); - let ffTimeout: ReturnType | undefined; const electron = globalThis.electron; const main = async () => { const valid = await validateKey(); @@ -385,11 +384,7 @@ export default function Gallery() { syncInterval.current = setInterval(() => { syncWithRemote(false, true); }, SYNC_INTERVAL_IN_MICROSECONDS); - // Not critical, so fetch these after some delay. - ffTimeout = setTimeout(() => { - ffTimeout = undefined; - void fetchAndSaveFeatureFlags(); - }, 5000); + fetchAndSaveFeatureFlagsIfNeeded(); if (electron) { // void clipService.setupOnFileUploadListener(); electron.onMainWindowFocus(() => syncWithRemote(false, true)); @@ -398,7 +393,6 @@ export default function Gallery() { main(); return () => { clearInterval(syncInterval.current); - if (ffTimeout) clearTimeout(ffTimeout); if (electron) { electron.onMainWindowFocus(undefined); clipService.removeOnFileUploadListener(); diff --git a/web/apps/photos/src/services/feature-flag.ts b/web/apps/photos/src/services/feature-flag.ts index c6d722bf0f..ffde49a6a1 100644 --- a/web/apps/photos/src/services/feature-flag.ts +++ b/web/apps/photos/src/services/feature-flag.ts @@ -1,8 +1,40 @@ +let _fetchTimeout: ReturnType | 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; + }); + }, 5000); +}; + +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. */ -export const fetchAndSaveFeatureFlags = async () => {}; +const fetchAndSaveFeatureFlags = async () => {}; /** * Return `true` if the current user is marked as an "internal" user. From a850500beb9e9a5ffcfc3a0f0cf93b3733e04972 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 14:29:28 +0530 Subject: [PATCH 04/10] Clear --- web/apps/photos/src/pages/gallery/index.tsx | 2 +- web/apps/photos/src/services/logout.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 5062aa6b6f..a1cba432c1 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -384,7 +384,6 @@ export default function Gallery() { syncInterval.current = setInterval(() => { syncWithRemote(false, true); }, SYNC_INTERVAL_IN_MICROSECONDS); - fetchAndSaveFeatureFlagsIfNeeded(); if (electron) { // void clipService.setupOnFileUploadListener(); electron.onMainWindowFocus(() => syncWithRemote(false, true)); @@ -715,6 +714,7 @@ export default function Gallery() { await syncTrash(collections, setTrashedFiles); await syncEntities(); await syncMapEnabled(); + fetchAndSaveFeatureFlagsIfNeeded(); const electron = globalThis.electron; if (electron) { await syncCLIPEmbeddings(); diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index 4e09516dee..9da892bad2 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -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) { From 9a7ba8a4069373de4b006e61f1f55c1b3e8bf022 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 14:40:44 +0530 Subject: [PATCH 05/10] Alias --- web/packages/shared/network/api.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/packages/shared/network/api.ts b/web/packages/shared/network/api.ts index 3cda6b615d..7ba49ec960 100644 --- a/web/packages/shared/network/api.ts +++ b/web/packages/shared/network/api.ts @@ -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) { From 133693d05849822e6b5774ac7e86f2cd0a9be4ea Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 15:15:51 +0530 Subject: [PATCH 06/10] Fetch beta flag --- web/apps/photos/src/services/feature-flag.ts | 58 +++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/services/feature-flag.ts b/web/apps/photos/src/services/feature-flag.ts index ffde49a6a1..0c15ec3cbb 100644 --- a/web/apps/photos/src/services/feature-flag.ts +++ b/web/apps/photos/src/services/feature-flag.ts @@ -1,3 +1,8 @@ +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 | undefined; let _haveFetched = false; @@ -34,7 +39,43 @@ export const clearFeatureFlagSessionState = () => { * Fetch feature flags (potentially user specific) from remote and save them in * local storage for subsequent lookup. */ -const fetchAndSaveFeatureFlags = async () => {}; +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. @@ -42,6 +83,17 @@ const fetchAndSaveFeatureFlags = async () => {}; export const isInternalUser = async () => {}; /** - * Return `true` if the current user is marked as an "beta" user. + * Return `true` if the current user is marked as a "beta" user. */ -export const isBetaUser = async () => {}; +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; +}; From fa06a15ad7273dc962b20e53a5f8ca9f93007cd5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 31 May 2024 15:26:36 +0530 Subject: [PATCH 07/10] Show the option for beta users too --- .../photos/src/components/ml/MLSearchSettings.tsx | 10 ++++++++-- web/apps/photos/src/services/face/indexer.ts | 15 +++++++++------ web/apps/photos/src/services/feature-flag.ts | 14 +++++++++++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/components/ml/MLSearchSettings.tsx b/web/apps/photos/src/components/ml/MLSearchSettings.tsx index d71dffab7e..1f3ad752a3 100644 --- a/web/apps/photos/src/components/ml/MLSearchSettings.tsx +++ b/web/apps/photos/src/components/ml/MLSearchSettings.tsx @@ -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 { @@ -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 ( - {isInternalUserForML() && ( + {canEnable && (