[photosd] Add a "What's new" dialog (#2124)

This commit is contained in:
Manav Rathi
2024-06-13 16:13:06 +05:30
committed by GitHub
9 changed files with 155 additions and 33 deletions

View File

@@ -46,7 +46,12 @@ import {
computeCLIPTextEmbeddingIfAvailable,
} from "./services/ml-clip";
import { computeFaceEmbeddings, detectFaces } from "./services/ml-face";
import { encryptionKey, saveEncryptionKey } from "./services/store";
import {
encryptionKey,
lastShownChangelogVersion,
saveEncryptionKey,
setLastShownChangelogVersion,
} from "./services/store";
import {
clearPendingUploads,
listZipItems,
@@ -101,11 +106,19 @@ export const attachIPCHandlers = () => {
ipcMain.handle("selectDirectory", () => selectDirectory());
ipcMain.handle("encryptionKey", () => encryptionKey());
ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) =>
saveEncryptionKey(encryptionKey),
);
ipcMain.handle("encryptionKey", () => encryptionKey());
ipcMain.handle("lastShownChangelogVersion", () =>
lastShownChangelogVersion(),
);
ipcMain.handle("setLastShownChangelogVersion", (_, version: number) =>
setLastShownChangelogVersion(version),
);
// - App update

View File

@@ -1,12 +1,16 @@
import { safeStorage } from "electron/main";
import { safeStorageStore } from "../stores/safe-storage";
import { uploadStatusStore } from "../stores/upload-status";
import { userPreferences } from "../stores/user-preferences";
import { watchStore } from "../stores/watch";
/**
* Clear all stores except user preferences.
*
* This is useful to reset state when the user logs out.
* This function is useful to reset state when the user logs out. User
* preferences are preserved since they contain things tied to the person using
* the app or other machine specific state not tied to the account they were
* using inside the app.
*/
export const clearStores = () => {
safeStorageStore.clear();
@@ -32,3 +36,9 @@ export const encryptionKey = (): string | undefined => {
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
return safeStorage.decryptString(keyBuffer);
};
export const lastShownChangelogVersion = (): number | undefined =>
userPreferences.get("lastShownChangelogVersion");
export const setLastShownChangelogVersion = (version: number) =>
userPreferences.set("lastShownChangelogVersion", version);

View File

@@ -9,6 +9,12 @@ interface UserPreferences {
hideDockIcon?: boolean;
skipAppVersion?: string;
muteUpdateNotificationVersion?: string;
/**
* The changelog version for which we last showed the "What's new" screen.
*
* See: [Note: Conditions for showing "What's new"]
*/
lastShownChangelogVersion?: number;
/**
* The last position and size of our app's window.
*
@@ -33,6 +39,7 @@ const userPreferencesSchema: Schema<UserPreferences> = {
hideDockIcon: { type: "boolean" },
skipAppVersion: { type: "string" },
muteUpdateNotificationVersion: { type: "string" },
lastShownChangelogVersion: { type: "number" },
windowBounds: {
properties: {
x: { type: "number" },

View File

@@ -72,12 +72,18 @@ const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
const saveEncryptionKey = (encryptionKey: string) =>
ipcRenderer.invoke("saveEncryptionKey", encryptionKey);
const onMainWindowFocus = (cb?: () => void) => {
const lastShownChangelogVersion = () =>
ipcRenderer.invoke("lastShownChangelogVersion");
const setLastShownChangelogVersion = (version: number) =>
ipcRenderer.invoke("setLastShownChangelogVersion", version);
const onMainWindowFocus = (cb: (() => void) | undefined) => {
ipcRenderer.removeAllListeners("mainWindowFocus");
if (cb) ipcRenderer.on("mainWindowFocus", cb);
};
const onOpenURL = (cb?: (url: string) => void) => {
const onOpenURL = (cb: ((url: string) => void) | undefined) => {
ipcRenderer.removeAllListeners("openURL");
if (cb) ipcRenderer.on("openURL", (_, url: string) => cb(url));
};
@@ -85,7 +91,7 @@ const onOpenURL = (cb?: (url: string) => void) => {
// - App update
const onAppUpdateAvailable = (
cb?: ((update: AppUpdate) => void) | undefined,
cb: ((update: AppUpdate) => void) | undefined,
) => {
ipcRenderer.removeAllListeners("appUpdateAvailable");
if (cb) {
@@ -311,6 +317,8 @@ contextBridge.exposeInMainWorld("electron", {
logout,
encryptionKey,
saveEncryptionKey,
lastShownChangelogVersion,
setLastShownChangelogVersion,
onMainWindowFocus,
onOpenURL,

View File

@@ -1,4 +1,3 @@
import { WhatsNew } from "@/new/photos/components/WhatsNew";
import { CustomHead } from "@/next/components/Head";
import { setAppNameForAuthenticatedRequests } from "@/next/http";
import { setupI18n } from "@/next/i18n";
@@ -130,10 +129,6 @@ export default function App({ Component, pageProps }: AppProps) {
>();
useState<DialogBoxAttributes>(null);
const [messageDialogView, setMessageDialogView] = useState(false);
// TODO(MR): This is never true currently, this is the WIP ability to show
// what's new dialog on desktop app updates. The UI is done, need to hook
// this up to logic to trigger it.
const [openWhatsNew, setOpenWhatsNew] = useState(false);
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
const [watchFolderView, setWatchFolderView] = useState(false);
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
@@ -407,11 +402,6 @@ export default function App({ Component, pageProps }: AppProps) {
attributes={dialogBoxAttributeV2}
/>
<WhatsNew
open={openWhatsNew}
onClose={() => setOpenWhatsNew(false)}
/>
<Notification
open={notificationView}
onClose={closeNotification}

View File

@@ -1,3 +1,5 @@
import { WhatsNew } from "@/new/photos/components/WhatsNew";
import { shouldShowWhatsNew } from "@/new/photos/services/changelog";
import { fetchAndSaveFeatureFlagsIfNeeded } from "@/new/photos/services/feature-flags";
import log from "@/next/log";
import { CenteredFlex } from "@ente/shared/components/Container";
@@ -193,6 +195,10 @@ export default function Gallery() {
const [search, setSearch] = useState<Search>(null);
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false);
// TODO(MR): This is never true currently, this is the WIP ability to show
// what's new dialog on desktop app updates. The UI is done, need to hook
// this up to logic to trigger it.
const [openWhatsNew, setOpenWhatsNew] = useState(false);
const {
// A function to call to get the props we should apply to the container,
@@ -386,6 +392,7 @@ export default function Gallery() {
if (electron) {
// void clipService.setupOnFileUploadListener();
electron.onMainWindowFocus(() => syncWithRemote(false, true));
if (await shouldShowWhatsNew()) setOpenWhatsNew(true);
}
};
main();
@@ -1154,6 +1161,10 @@ export default function Gallery() {
sidebarView={sidebarView}
closeSidebar={closeSidebar}
/>
<WhatsNew
open={openWhatsNew}
onClose={() => setOpenWhatsNew(false)}
/>
{!isInSearchMode &&
!isFirstLoad &&
!files?.length &&

View File

@@ -6,12 +6,14 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
Typography,
styled,
useMediaQuery,
} from "@mui/material";
import Slide from "@mui/material/Slide";
import type { TransitionProps } from "@mui/material/transitions";
import React from "react";
import React, { useEffect } from "react";
import { didShowWhatsNew } from "../services/changelog";
interface WhatsNewProps {
/** If `true`, then the dialog is shown. */
@@ -21,12 +23,16 @@ interface WhatsNewProps {
}
/**
* Show a dialog showing a short summary of interesting-for-the-user things in
* this release of the desktop app.
* Show a dialog showing a short summary of interesting-for-the-user things
* since the last time this dialog was shown.
*/
export const WhatsNew: React.FC<WhatsNewProps> = ({ open, onClose }) => {
const fullScreen = useMediaQuery("(max-width: 428px)");
useEffect(() => {
if (open) void didShowWhatsNew();
}, [open]);
return (
<Dialog
{...{ open, fullScreen }}
@@ -36,12 +42,7 @@ export const WhatsNew: React.FC<WhatsNewProps> = ({ open, onClose }) => {
<DialogTitle>{"What's new"}</DialogTitle>
<DialogContent>
<DialogContentText>
<StyledUL>
<li>
The app will remember its position and size when it
is closed, and will reopen the same way.
</li>
</StyledUL>
<ChangelogContent />
</DialogContentText>
</DialogContent>
<DialogActions>
@@ -68,11 +69,37 @@ const SlideTransition = React.forwardRef(function Transition(
return <Slide direction="up" ref={ref} {...props} />;
});
const ChangelogContent: React.FC = () => {
// NOTE: Remember to update changelogVersion when changing the content
// below.
return (
<StyledUL>
<li>
<Typography>
<Typography color="primary">
Support for Passkeys
</Typography>
Passkeys can now be used as a second factor authentication
mechanism.
</Typography>
</li>
<li>
<Typography color="primary">Window size</Typography>
<Typography>
{"The app's window will remember its size and position."}
</Typography>
</li>
</StyledUL>
);
};
const StyledUL = styled("ul")`
padding-inline: 1rem;
list-style-type: circle;
margin-block-end: 20px;
li {
margin-block: 2rem;
}
`;
const StyledButton = styled(Button)`

View File

@@ -0,0 +1,39 @@
import { ensureElectron } from "@/next/electron";
/**
* The current changelog version.
*
* [Note: Conditions for showing "What's new"]
*
* We maintain a "changelog version". This version is an incrementing positive
* integer, we increment it whenever we want to show this dialog again. Usually
* we'd do this for each app update, but not necessarily.
*
* The "What's new" dialog is shown when either we do not have a previously
* saved changelog version, or if the saved changelog version is less than the
* current {@link changelogVersion}.
*
* The shown changelog version is persisted on the Node.js layer since there we
* can store it in the user preferences store, which is not cleared on logout.
*
* On app start, the Node.js layer waits for the {@link onShowWhatsNew} callback
* to get attached. When a callback is attached, it checks the above conditions
* and if they are satisfied, it invokes the callback. The callback should
* return the current {@link changelogVersion} to allow the Node.js layer to
* update the persisted state.
*/
const changelogVersion = 1;
/**
* Return true if we should show the {@link WhatsNew} dialog.
*/
export const shouldShowWhatsNew = async () => {
const electron = globalThis.electron;
if (!electron) return false;
const lastShownVersion = (await electron.lastShownChangelogVersion()) ?? 0;
return lastShownVersion < changelogVersion;
};
export const didShowWhatsNew = async () =>
// We should only have been called if we're in electron.
ensureElectron().setLastShownChangelogVersion(changelogVersion);

View File

@@ -87,12 +87,12 @@ export interface Electron {
* into the foreground. More precisely, the callback gets invoked when the
* main window gets focus.
*
* Note: Setting a callback clears any previous callbacks.
* Setting a callback clears any previous callbacks.
*
* @param cb The function to call when the main window gets focus. Pass
* `undefined` to clear the callback.
*/
onMainWindowFocus: (cb?: () => void) => void;
onMainWindowFocus: (cb: (() => void) | undefined) => void;
/**
* Set or clear the callback {@link cb} to invoke whenever the app gets
@@ -103,13 +103,13 @@ export interface Electron {
* In particular, this is necessary for handling passkey authentication.
* See: [Note: Passkey verification in the desktop app]
*
* Note: Setting a callback clears any previous callbacks.
* Setting a callback clears any previous callbacks.
*
* @param cb The function to call when the app gets asked to open a
* "ente://" URL. The URL string (a.k.a. "deeplink") we were asked to open
* is passed to the function verbatim.
*/
onOpenURL: (cb?: (url: string) => void) => void;
onOpenURL: (cb: ((url: string) => void) | undefined) => void;
// - App update
@@ -118,10 +118,10 @@ export interface Electron {
* (actionable) app update is available. This allows the Node.js layer to
* ask the renderer to show an "Update available" dialog to the user.
*
* Note: Setting a callback clears any previous callbacks.
* Setting a callback clears any previous callbacks.
*/
onAppUpdateAvailable: (
cb?: ((update: AppUpdate) => void) | undefined,
cb: ((update: AppUpdate) => void) | undefined,
) => void;
/**
@@ -148,6 +148,23 @@ export interface Electron {
*/
skipAppUpdate: (version: string) => void;
/**
* Get the persisted version for the last shown changelog.
*
* See: [Note: Conditions for showing "What's new"]
*/
lastShownChangelogVersion: () => Promise<number | undefined>;
/**
* Save the given {@link version} to disk as the version of the last shown
* changelog.
*
* The value is saved to a store which is not cleared during logout.
*
* @see {@link lastShownChangelogVersion}
*/
setLastShownChangelogVersion: (version: number) => Promise<void>;
// - FS
/**