[desktop] More fixes leading on to the release (#1632)

This commit is contained in:
Manav Rathi 2024-05-07 09:49:04 +05:30 committed by GitHub
commit c4756fb847
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 224 additions and 48 deletions

View file

@ -0,0 +1,70 @@
name: "Draft release"
# Build the desktop/draft-release branch and update the existing draft release
# with the resultant artifacts.
#
# This is meant for doing tests that require the app to be signed and packaged.
# Such releases should not be published to end users.
#
# Workflow:
#
# 1. Push your changes to the "desktop/draft-release" branch on
# https://github.com/ente-io/ente.
#
# 2. Create a draft release with tag equal to the version in the `package.json`.
#
# 3. Trigger this workflow. You can trigger it multiple times, each time it'll
# just update the artifacts attached to the same draft.
#
# 4. Once testing is done delete the draft.
on:
# Trigger manually or `gh workflow run desktop-draft-release.yml`.
workflow_dispatch:
jobs:
release:
runs-on: macos-latest
defaults:
run:
working-directory: desktop
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
repository: ente-io/ente
ref: desktop/draft-release
submodules: recursive
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: yarn install
- name: Build
uses: ente-io/action-electron-builder@v1.0.0
with:
package_root: desktop
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.GITHUB_TOKEN }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building.
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
mac_certs: ${{ secrets.MAC_CERTS }}
mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }}
env:
# macOS notarization credentials key details
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD:
${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
USE_HARD_LINKS: false

View file

@ -29,5 +29,4 @@ mac:
arch: [universal]
category: public.app-category.photography
hardenedRuntime: true
notarize: true
afterSign: electron-builder-notarize

View file

@ -142,7 +142,7 @@ const createMainWindow = () => {
// Create the main window. This'll show our web content.
const window = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), "preload.js"),
preload: path.join(__dirname, "preload.js"),
sandbox: true,
},
// The color to show in the window until the web content gets loaded.
@ -287,13 +287,29 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
/**
* Older versions of our app used to maintain a cache dir using the main
* process. This has been deprecated in favor of using a normal web cache.
* process. This has been removed in favor of cache on the web layer.
*
* Delete the old cache dir if it exists. This code was added March 2024, and
* can be removed after some time once most people have upgraded to newer
* versions.
* Delete the old cache dir if it exists.
*
* This will happen in two phases. The cache had three subdirectories:
*
* - Two of them, "thumbs" and "files", will be removed now (v1.7.0, May 2024).
*
* - The third one, "face-crops" will be removed once we finish the face search
* changes. See: [Note: Legacy face crops].
*
* This migration code can be removed after some time once most people have
* upgraded to newer versions.
*/
const deleteLegacyDiskCacheDirIfExists = async () => {
const removeIfExists = async (dirPath: string) => {
if (existsSync(dirPath)) {
log.info(`Removing legacy disk cache from ${dirPath}`);
await fs.rm(dirPath, { recursive: true });
}
};
// [Note: Getting the cache path]
//
// The existing code was passing "cache" as a parameter to getPath.
//
// However, "cache" is not a valid parameter to getPath. It works! (for
@ -309,8 +325,8 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
// @ts-expect-error "cache" works but is not part of the public API.
const cacheDir = path.join(app.getPath("cache"), "ente");
if (existsSync(cacheDir)) {
log.info(`Removing legacy disk cache from ${cacheDir}`);
await fs.rm(cacheDir, { recursive: true });
await removeIfExists(path.join(cacheDir, "thumbs"));
await removeIfExists(path.join(cacheDir, "files"));
}
};
@ -375,7 +391,7 @@ const main = () => {
// Continue on with the rest of the startup sequence.
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
setupTrayItem(mainWindow);
if (!isDev) setupAutoUpdater(mainWindow);
setupAutoUpdater(mainWindow);
try {
await deleteLegacyDiskCacheDirIfExists();

View file

@ -24,6 +24,7 @@ import {
updateOnNextRestart,
} from "./services/app-update";
import {
legacyFaceCrop,
openDirectory,
openLogDirectory,
selectDirectory,
@ -198,6 +199,10 @@ export const attachIPCHandlers = () => {
faceEmbedding(input),
);
ipcMain.handle("legacyFaceCrop", (_, faceID: string) =>
legacyFaceCrop(faceID),
);
// - Upload
ipcMain.handle("listZipItems", (_, zipPath: string) =>

View file

@ -10,7 +10,6 @@ import { forceCheckForAppUpdates } from "./services/app-update";
import autoLauncher from "./services/auto-launcher";
import { openLogDirectory } from "./services/dir";
import { userPreferences } from "./stores/user-preferences";
import { isDev } from "./utils/electron";
/** Create and return the entries in the app's main menu bar */
export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
@ -24,9 +23,6 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
process.platform == "darwin" ? options : [];
const devOnly = (options: MenuItemConstructorOptions[]) =>
isDev ? options : [];
const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow);
const handleViewChangelog = () =>
@ -130,11 +126,11 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
submenu: [
{
role: "startSpeaking",
label: "start speaking",
label: "Start Speaking",
},
{
role: "stopSpeaking",
label: "stop speaking",
label: "Stop Speaking",
},
],
},
@ -145,9 +141,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
label: "View",
submenu: [
{ label: "Reload", role: "reload" },
...devOnly([
{ label: "Toggle Dev Tools", role: "toggleDevTools" },
]),
{ label: "Toggle Dev Tools", role: "toggleDevTools" },
{ type: "separator" },
{ label: "Toggle Full Screen", role: "togglefullscreen" },
],

View file

@ -6,11 +6,20 @@ import { allowWindowClose } from "../../main";
import { AppUpdate } from "../../types/ipc";
import log from "../log";
import { userPreferences } from "../stores/user-preferences";
import { isDev } from "../utils/electron";
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
autoUpdater.logger = electronLog;
autoUpdater.autoDownload = false;
// Skip checking for updates automatically in dev builds. Installing an
// update would fail anyway since (at least on macOS), the auto update
// process requires signed builds.
//
// Even though this is skipped on app start, we can still use the "Check for
// updates..." menu option to trigger the update if we wish in dev builds.
if (isDev) return;
const oneDay = 1 * 24 * 60 * 60 * 1000;
setInterval(() => void checkForUpdatesAndNotify(mainWindow), oneDay);
void checkForUpdatesAndNotify(mainWindow);

View file

@ -27,14 +27,14 @@ class AutoLauncher {
}
async toggleAutoLaunch() {
const isEnabled = await this.isEnabled();
const wasEnabled = await this.isEnabled();
const autoLaunch = this.autoLaunch;
if (autoLaunch) {
if (isEnabled) await autoLaunch.disable();
if (wasEnabled) await autoLaunch.disable();
else await autoLaunch.enable();
} else {
if (isEnabled) app.setLoginItemSettings({ openAtLogin: false });
else app.setLoginItemSettings({ openAtLogin: true });
const openAtLogin = !wasEnabled;
app.setLoginItemSettings({ openAtLogin });
}
}
@ -42,8 +42,7 @@ class AutoLauncher {
if (this.autoLaunch) {
return app.commandLine.hasSwitch("hidden");
} else {
// TODO(MR): This apparently doesn't work anymore.
return app.getLoginItemSettings().wasOpenedAtLogin;
return app.getLoginItemSettings().openAtLogin;
}
}
}

View file

@ -1,5 +1,7 @@
import { shell } from "electron/common";
import { app, dialog } from "electron/main";
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import { posixPath } from "../utils/electron";
@ -38,14 +40,50 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath());
*
* [Note: Electron app paths]
*
* By default, these paths are at the following locations:
* There are three paths we need to be aware of usually.
*
* - macOS: `~/Library/Application Support/ente`
* First is the "appData". We can obtain this with `app.getPath("appData")`.
* This is per-user application data directory. This is usually the following:
*
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local`
* - Linux: `~/.config`
* - macOS: `~/Library/Application Support`
*
* Now, if we suffix the app's name onto the appData directory, we get the
* "userData" directory. This is the **primary** place applications are meant to
* store user's data, e.g. various configuration files and saved state.
*
* During development, our app name is "Electron", so this'd be, for example,
* `~/Library/Application Support/Electron` if we run using `yarn dev`. For the
* packaged production app, our app name is "ente", so this would be:
*
* - Windows: `%APPDATA%\ente`, e.g. `C:\Users\<username>\AppData\Local\ente`
* - Linux: `~/.config/ente`
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
* - macOS: `~/Library/Application Support/ente`
*
* Note that Chromium also stores the browser state, e.g. localStorage or disk
* caches, in userData.
*
* Finally, there is the "logs" directory. This is not within "appData" but has
* a slightly different OS specific path. Since our log file is named
* "ente.log", it can be found at:
*
* - macOS: ~/Library/Logs/ente/ente.log (production)
* - macOS: ~/Library/Logs/Electron/ente.log (dev)
*
* https://www.electronjs.org/docs/latest/api/app
*
*/
const logDirectoryPath = () => app.getPath("logs");
/**
* See: [Note: Legacy face crops]
*/
export const legacyFaceCrop = async (
faceID: string,
): Promise<Uint8Array | undefined> => {
// See: [Note: Getting the cache path]
// @ts-expect-error "cache" works but is not part of the public API.
const cacheDir = path.join(app.getPath("cache"), "ente");
const filePath = path.join(cacheDir, "face-crops", faceID);
return existsSync(filePath) ? await fs.readFile(filePath) : undefined;
};

View file

@ -14,6 +14,15 @@ export const clearStores = () => {
watchStore.clear();
};
/**
* [Note: Safe storage keys]
*
* On macOS, `safeStorage` stores our data under a Keychain entry named
* "<app-name> Safe Storage". Which resolves to:
*
* - Electron Safe Storage (dev)
* - ente Safe Storage (prod)
*/
export const saveEncryptionKey = (encryptionKey: string) => {
const encryptedKey = safeStorage.encryptString(encryptionKey);
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");

View file

@ -164,6 +164,9 @@ const detectFaces = (input: Float32Array) =>
const faceEmbedding = (input: Float32Array) =>
ipcRenderer.invoke("faceEmbedding", input);
const legacyFaceCrop = (faceID: string) =>
ipcRenderer.invoke("legacyFaceCrop", faceID);
// - Watch
const watchGet = () => ipcRenderer.invoke("watchGet");
@ -341,6 +344,7 @@ contextBridge.exposeInMainWorld("electron", {
clipTextEmbeddingIfAvailable,
detectFaces,
faceEmbedding,
legacyFaceCrop,
// - Watch

View file

@ -22,7 +22,7 @@ import {
getFaceSearchEnabledStatus,
updateFaceSearchEnabledStatus,
} from "services/userService";
import { isInternalUser } from "utils/user";
import { isInternalUserForML } from "utils/user";
export const MLSearchSettings = ({ open, onClose, onRootClose }) => {
const {
@ -280,7 +280,7 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {
</p>
</Typography>
</Box>
{isInternalUser() && (
{isInternalUserForML() && (
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}

View file

@ -1,11 +1,8 @@
import { cachedOrNew } from "@/next/blob-cache";
import { ensureLocalUser } from "@/next/local-user";
import log from "@/next/log";
import { Skeleton, styled } from "@mui/material";
import { Legend } from "components/PhotoViewer/styledComponents/Legend";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
import machineLearningService from "services/machineLearning/machineLearningService";
import { EnteFile } from "types/file";
import { Face, Person } from "types/machineLearning";
import { getPeopleList, getUnidentifiedFaces } from "utils/machineLearning";
@ -61,7 +58,7 @@ export const PeopleList = React.memo((props: PeopleListProps) => {
}
>
<FaceCropImageView
faceId={person.displayFaceId}
faceID={person.displayFaceId}
cacheKey={person.faceCropCacheKey}
/>
</FaceChip>
@ -140,7 +137,7 @@ export function UnidentifiedFaces(props: {
faces.map((face, index) => (
<FaceChip key={index}>
<FaceCropImageView
faceId={face.id}
faceID={face.id}
cacheKey={face.crop?.cacheKey}
/>
</FaceChip>
@ -151,20 +148,24 @@ export function UnidentifiedFaces(props: {
}
interface FaceCropImageViewProps {
faceId: string;
faceID: string;
cacheKey?: string;
}
const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({
faceId,
faceID,
cacheKey,
}) => {
const [objectURL, setObjectURL] = useState<string | undefined>();
useEffect(() => {
let didCancel = false;
const electron = globalThis.electron;
if (cacheKey) {
if (faceID && electron) {
electron
.legacyFaceCrop(faceID)
/*
cachedOrNew("face-crops", cacheKey, async () => {
const user = await ensureLocalUser();
return machineLearningService.regenerateFaceCrop(
@ -172,16 +173,20 @@ const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({
user.id,
faceId,
);
}).then((blob) => {
if (!didCancel) setObjectURL(URL.createObjectURL(blob));
});
})*/
.then((data) => {
if (data) {
const blob = new Blob([data]);
if (!didCancel) setObjectURL(URL.createObjectURL(blob));
}
});
} else setObjectURL(undefined);
return () => {
didCancel = true;
if (objectURL) URL.revokeObjectURL(objectURL);
};
}, [faceId, cacheKey]);
}, [faceID, cacheKey]);
return objectURL ? (
<img src={objectURL} />

View file

@ -10,7 +10,7 @@ import mlIDbStorage, {
ML_SYNC_CONFIG_NAME,
ML_SYNC_JOB_CONFIG_NAME,
} from "utils/storage/mlIDbStorage";
import { isInternalUser } from "utils/user";
import { isInternalUserForML } from "utils/user";
export async function getMLSyncJobConfig() {
return mlIDbStorage.getConfig(
@ -24,7 +24,7 @@ export async function getMLSyncConfig() {
}
export async function getMLSearchConfig() {
if (isInternalUser()) {
if (isInternalUserForML()) {
return mlIDbStorage.getConfig(
ML_SEARCH_CONFIG_NAME,
DEFAULT_ML_SEARCH_CONFIG,

View file

@ -1,4 +1,5 @@
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
import type { User } from "@ente/shared/user/types";
import { UserDetails } from "types/user";
export function getLocalUserDetails(): UserDetails {
@ -9,7 +10,12 @@ export const isInternalUser = () => {
const userEmail = getData(LS_KEYS.USER)?.email;
if (!userEmail) return false;
return (
userEmail.endsWith("@ente.io") || userEmail === "kr.anand619@gmail.com"
);
return userEmail.endsWith("@ente.io");
};
export const isInternalUserForML = () => {
const userId = (getData(LS_KEYS.USER) as User)?.id;
if (userId == 1) return true;
return isInternalUser();
};

View file

@ -346,6 +346,28 @@ export interface Electron {
*/
faceEmbedding: (input: Float32Array) => Promise<Float32Array>;
/**
* Return a face crop stored by a previous version of ML.
*
* [Note: Legacy face crops]
*
* Older versions of ML generated and stored face crops in a "face-crops"
* cache directory on the Electron side. For the time being, we have
* disabled the face search whilst we put finishing touches to it. However,
* it'll be nice to still show the existing faces that have been clustered
* for people who opted in to the older beta.
*
* So we retain the older "face-crops" disk cache, and use this method to
* serve faces from it when needed.
*
* @param faceID An identifier corresponding to which the face crop had been
* stored by the older version of our app.
*
* @returns the JPEG data of the face crop if a file is found for the given
* {@link faceID}, otherwise undefined.
*/
legacyFaceCrop: (faceID: string) => Promise<Uint8Array | undefined>;
// - Watch
/**