Skip to content

Commit

Permalink
enable support for dark/light mode, closes #242
Browse files Browse the repository at this point in the history
* Defaults to OS's dark mode preference.
  • Loading branch information
vladimiry committed Apr 6, 2021
1 parent 5948557 commit b203fa4
Show file tree
Hide file tree
Showing 86 changed files with 918 additions and 537 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Some Linux package types are available for installing from the repositories (`Pa
- :package: **Batch emails export** to EML files (attachments can optionally be exported in `online / live` mode, not available in `offline` mode since not stored locally). Feature released with [v2.0.0-beta.4](https://github.com/vladimiry/ElectronMail/releases/tag/v2.0.0-beta.4) version, requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
- :closed_lock_with_key: **Built-in/prepackaged web clients**. The prepackaged with the app proton web clients assembled from source code, see the respective [official repositories](https://github.com/ProtonMail/). See [79](https://github.com/vladimiry/ElectronMail/issues/79) and [80](https://github.com/vladimiry/ElectronMail/issues/80) issues for details.
- :gear: **Configuring proxy per account** support. Enabled since [v3.0.0](https://github.com/vladimiry/ElectronMail/releases/tag/v3.0.0) release. See [113](https://github.com/vladimiry/ElectronMail/issues/113) and [120](https://github.com/vladimiry/ElectronMail/issues/120) issues for details.
- :moon: **Dark mode** support. See details in [#242](https://github.com/vladimiry/ElectronMail/issues/242).
- :bell: **System tray icon** with a total number of unread messages shown on top of it. Enabling [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) improves this feature, see [#30](https://github.com/vladimiry/ElectronMail/issues/30).
- :gear: **Starting minimized to tray** and **closing to tray** opt-out features.
- :bell: **Native notifications** for individual accounts clicking on which focuses the app window and selects respective account in the accounts list.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "electron-mail",
"description": "Unofficial ProtonMail Desktop App",
"version": "4.11.1",
"version": "4.12.0",
"author": "Vladimir Yakovlev <desktop-app@protonmail.ch>",
"license": "MIT",
"homepage": "https://github.com/vladimiry/ElectronMail",
Expand Down
6 changes: 3 additions & 3 deletions src/electron-main/api/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Subject} from "rxjs";
import {UnionOf} from "@vladimiry/unionize";

import {IPC_MAIN_API_DB_INDEXER_ON_ACTIONS, IpcMainServiceScan} from "src/shared/api/main";
import {IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS, IpcMainServiceScan} from "src/shared/api/main";

export const IPC_MAIN_API_NOTIFICATION$ = new Subject<IpcMainServiceScan["ApiImplReturns"]["notification"]>();

export const IPC_MAIN_API_DB_INDEXER_NOTIFICATION$ = new Subject<IpcMainServiceScan["ApiImplReturns"]["dbIndexerNotification"]>();
export const IPC_MAIN_API_DB_INDEXER_REQUEST$ = new Subject<IpcMainServiceScan["ApiImplReturns"]["dbIndexerNotification"]>();

export const IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$ = new Subject<UnionOf<typeof IPC_MAIN_API_DB_INDEXER_ON_ACTIONS>>();
export const IPC_MAIN_API_DB_INDEXER_RESPONSE$ = new Subject<UnionOf<typeof IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS>>();
12 changes: 6 additions & 6 deletions src/electron-main/api/endpoints-builders/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {omit} from "remeda";
import {Context} from "src/electron-main/model";
import {DB_DATA_CONTAINER_FIELDS, IndexableMail} from "src/shared/model/database";
import {Database} from "src/electron-main/database";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION$, IPC_MAIN_API_NOTIFICATION$} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS, IPC_MAIN_API_NOTIFICATION_ACTIONS, IpcMainApiEndpoints} from "src/shared/api/main";
import {IPC_MAIN_API_DB_INDEXER_REQUEST$, IPC_MAIN_API_NOTIFICATION$} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS, IPC_MAIN_API_NOTIFICATION_ACTIONS, IpcMainApiEndpoints} from "src/shared/api/main";
import {buildDbExportEndpoints} from "./export/api";
import {buildDbIndexingEndpoints} from "./indexing/api";
import {buildDbSearchEndpoints} from "./search/api";
Expand Down Expand Up @@ -80,8 +80,8 @@ export async function buildEndpoints(ctx: Context): Promise<Pick<IpcMainApiEndpo
setTimeout(() => {
// send mails to indexing process
// TODO performance optimization: send mails to indexing process if indexing feature activated
IPC_MAIN_API_DB_INDEXER_NOTIFICATION$.next(
IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS.Index(
IPC_MAIN_API_DB_INDEXER_REQUEST$.next(
IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS.Index(
{
uid: new UUID(4).format(),
...narrowIndexActionPayload({
Expand Down Expand Up @@ -124,8 +124,8 @@ export async function buildEndpoints(ctx: Context): Promise<Pick<IpcMainApiEndpo
setTimeout(() => {
// removing stale mails form the full text search index
// TODO performance optimization: send mails to indexing process if indexing feature activated
IPC_MAIN_API_DB_INDEXER_NOTIFICATION$.next(
IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS.Index(
IPC_MAIN_API_DB_INDEXER_REQUEST$.next(
IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS.Index(
{
uid: new UUID(4).format(),
...narrowIndexActionPayload({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import {filter, first, startWith, takeUntil} from "rxjs/operators";

import {Context} from "src/electron-main/model";
import {
IPC_MAIN_API_DB_INDEXER_NOTIFICATION$,
IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$,
IPC_MAIN_API_DB_INDEXER_REQUEST$,
IPC_MAIN_API_DB_INDEXER_RESPONSE$,
IPC_MAIN_API_NOTIFICATION$,
} from "src/electron-main/api/constants";
import {
IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS,
IPC_MAIN_API_DB_INDEXER_ON_ACTIONS,
IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS,
IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS,
IPC_MAIN_API_NOTIFICATION_ACTIONS,
IpcMainApiEndpoints,
} from "src/shared/api/main";
Expand All @@ -29,10 +29,10 @@ export async function buildDbIndexingEndpoints(

// propagating action to custom stream
setTimeout(() => {
IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$.next(action);
IPC_MAIN_API_DB_INDEXER_RESPONSE$.next(action);
});

IPC_MAIN_API_DB_INDEXER_ON_ACTIONS.match(action, {
IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS.match(action, {
Bootstrapped() {
const indexAccounts$ = defer(
async () => {
Expand Down Expand Up @@ -87,8 +87,8 @@ export async function buildDbIndexingEndpoints(

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
dbIndexerNotification() {
return IPC_MAIN_API_DB_INDEXER_NOTIFICATION$.asObservable().pipe(
startWith(IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS.Bootstrap({})),
return IPC_MAIN_API_DB_INDEXER_REQUEST$.asObservable().pipe(
startWith(IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS.Bootstrap({})),
);
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {race, throwError, timer} from "rxjs";

import {Config} from "src/shared/model/options";
import {DbAccountPk, FsDbAccount, INDEXABLE_MAIL_FIELDS, Mail} from "src/shared/model/database";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION$, IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$,} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS, IPC_MAIN_API_DB_INDEXER_ON_ACTIONS,} from "src/shared/api/main";
import {IPC_MAIN_API_DB_INDEXER_REQUEST$, IPC_MAIN_API_DB_INDEXER_RESPONSE$,} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS, IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS,} from "src/shared/api/main";
import {curryFunctionMembers} from "src/shared/util";
import {hrtimeDuration} from "src/electron-main/util";

const logger = curryFunctionMembers(electronLog, __filename);

export const narrowIndexActionPayload: (
payload: StrictOmit<Extract<UnionOf<typeof IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS>, { type: "Index" }>["payload"], "uid">,
payload: StrictOmit<Extract<UnionOf<typeof IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS>, { type: "Index" }>["payload"], "uid">,
) => typeof payload = ((): typeof narrowIndexActionPayload => {
type Fn = typeof narrowIndexActionPayload;
type Mails = ReturnType<Fn>["add"];
Expand Down Expand Up @@ -46,8 +46,8 @@ async function indexMails(
const duration = hrtimeDuration();
const uid = new UUID(4).format();
const result$ = race(
IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$.pipe(
filter(IPC_MAIN_API_DB_INDEXER_ON_ACTIONS.is.IndexingResult),
IPC_MAIN_API_DB_INDEXER_RESPONSE$.pipe(
filter(IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS.is.IndexingResult),
filter(({payload}) => payload.uid === uid),
first(),
),
Expand All @@ -56,8 +56,8 @@ async function indexMails(
),
);

IPC_MAIN_API_DB_INDEXER_NOTIFICATION$.next(
IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS.Index({
IPC_MAIN_API_DB_INDEXER_REQUEST$.next(
IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS.Index({
uid,
...narrowIndexActionPayload({
key,
Expand Down
12 changes: 6 additions & 6 deletions src/electron-main/api/endpoints-builders/database/search/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {Observable, from, of, race, throwError, timer} from "rxjs";
import {concatMap, filter, first, mergeMap, switchMap} from "rxjs/operators";

import {Context} from "src/electron-main/model";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION$, IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS, IPC_MAIN_API_DB_INDEXER_ON_ACTIONS, IpcMainApiEndpoints} from "src/shared/api/main";
import {IPC_MAIN_API_DB_INDEXER_REQUEST$, IPC_MAIN_API_DB_INDEXER_RESPONSE$} from "src/electron-main/api/constants";
import {IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS, IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS, IpcMainApiEndpoints} from "src/shared/api/main";
import {IndexableMailId} from "src/shared/model/database";
import {curryFunctionMembers} from "src/shared/util";
import {searchRootConversationNodes, secondSearchStep} from "src/electron-main/api/endpoints-builders/database/search/service";
Expand Down Expand Up @@ -44,8 +44,8 @@ export async function buildDbSearchEndpoints(
: null;
const fullTextSearch$: Observable<Map<IndexableMailId, number> | null> = query
? race(
IPC_MAIN_API_DB_INDEXER_ON_NOTIFICATION$.pipe(
filter(IPC_MAIN_API_DB_INDEXER_ON_ACTIONS.is.SearchResult),
IPC_MAIN_API_DB_INDEXER_RESPONSE$.pipe(
filter(IPC_MAIN_API_DB_INDEXER_RESPONSE_ACTIONS.is.SearchResult),
filter(({payload}) => payload.uid === fullTextSearchUid),
first(),
mergeMap(({payload: {data: {items}}}) => [new Map<IndexableMailId, number>(
Expand Down Expand Up @@ -74,8 +74,8 @@ export async function buildDbSearchEndpoints(
);

if (fullTextSearchUid) {
IPC_MAIN_API_DB_INDEXER_NOTIFICATION$.next(
IPC_MAIN_API_DB_INDEXER_NOTIFICATION_ACTIONS.Search({query, uid: fullTextSearchUid}),
IPC_MAIN_API_DB_INDEXER_REQUEST$.next(
IPC_MAIN_API_DB_INDEXER_REQUEST_ACTIONS.Search({query, uid: fullTextSearchUid}),
);
}

Expand Down
5 changes: 2 additions & 3 deletions src/electron-main/api/endpoints-builders/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ProxyAgent from "proxy-agent";
import compareVersions from "compare-versions";
import electronLog from "electron-log";
import fetch from "node-fetch";
import {app, dialog, shell} from "electron";
import {app, dialog, nativeTheme, shell} from "electron";
import {first, map, startWith} from "rxjs/operators";
import {from, merge, of, throwError} from "rxjs";
import {inspect} from "util";
Expand Down Expand Up @@ -374,8 +374,7 @@ export async function buildEndpoints(
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
notification() {
return IPC_MAIN_API_NOTIFICATION$.asObservable().pipe(
// TODO replace "startWith" with "defaultIfEmpty" (simply some response needed to avoid timeout error)
startWith(IPC_MAIN_API_NOTIFICATION_ACTIONS.Bootstrap({})),
startWith(IPC_MAIN_API_NOTIFICATION_ACTIONS.NativeTheme({shouldUseDarkColors: nativeTheme.shouldUseDarkColors})),
);
},

Expand Down
5 changes: 5 additions & 0 deletions src/electron-main/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {curryFunctionMembers} from "src/shared/util";
import {deletePassword, getPassword, setPassword} from "src/electron-main/keytar";
import {initSessionByAccount} from "src/electron-main/session";
import {upgradeDatabase, upgradeSettings} from "src/electron-main/storage-upgrade";
import {applyThemeSource} from "src/electron-main/native-theme";

const logger = curryFunctionMembers(electronLog, __filename);

Expand Down Expand Up @@ -186,6 +187,10 @@ export const initApi = async (ctx: Context): Promise<IpcMainApiEndpoints> => {
}
}

if (updatedConfig.themeSource !== previousConfig.themeSource) {
applyThemeSource(updatedConfig.themeSource);
}

return updatedConfig;
},

Expand Down
8 changes: 5 additions & 3 deletions src/electron-main/bootstrap/app-ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {initTray} from "src/electron-main/tray";
import {initWebContentsCreatingHandlers} from "src/electron-main/web-contents";
import {registerWebFolderFileProtocol} from "src/electron-main/protocol";
import {setUpPowerMonitorNotification} from "src/electron-main/power-monitor";
import {initNativeThemeNotification} from "src/electron-main/native-theme";

export async function appReadyHandler(ctx: Context): Promise<void> {
registerWebFolderFileProtocol(ctx, getDefaultSession());
Expand All @@ -19,13 +20,14 @@ export async function appReadyHandler(ctx: Context): Promise<void> {

const endpoints = await initApi(ctx);

// "endpoints.readConfig()" call initializes the config.json file
// so consequent "ctx.configStore.readExisting()" calls don't fail
const {spellCheckLocale, customTrayIconColor, logLevel} = await endpoints.readConfig();
// so consequent "ctx.configStore.readExisting()" calls don't fail since "endpoints.readConfig()" call initializes the config
const {spellCheckLocale, customTrayIconColor, logLevel, themeSource} = await endpoints.readConfig();

// TODO test "logger.transports.file.level" update
electronLog.transports.file.level = logLevel;

initNativeThemeNotification(themeSource);

await (async (): Promise<void> => {
const spellCheckController = await initSpellCheckController(spellCheckLocale);
ctx.getSpellCheckController = (): typeof spellCheckController => spellCheckController;
Expand Down
32 changes: 10 additions & 22 deletions src/electron-main/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,20 @@ import asap from "asap-es";
import logger from "electron-log";
import path from "path";
import {Deferred} from "ts-deferred";
import {ReplaySubject, merge} from "rxjs";
import {merge, ReplaySubject} from "rxjs";
import {Fs as StoreFs, Model as StoreModel, Store} from "fs-json-store";
import {app} from "electron";
import {distinctUntilChanged, take} from "rxjs/operators";

import {
BINARY_NAME,
LOCAL_WEBCLIENT_PROTOCOL_PREFIX,
RUNTIME_ENV_USER_DATA_DIR,
WEB_CHUNK_NAMES,
WEB_PROTOCOL_SCHEME
} from "src/shared/constants";
import {BINARY_NAME, LOCAL_WEBCLIENT_PROTOCOL_PREFIX, RUNTIME_ENV_USER_DATA_DIR, WEB_PROTOCOL_SCHEME} from "src/shared/constants";
import {Config, Settings} from "src/shared/model/options";
import {Context, ContextInitOptions, ContextInitOptionsPaths, ProperLockfileError} from "./model";
import {Database} from "./database";
import {ElectronContextLocations} from "src/shared/model/electron";
import {INITIAL_STORES, configEncryptionPresetValidator, settingsAccountLoginUniquenessValidator} from "./constants";
import {configEncryptionPresetValidator, INITIAL_STORES, settingsAccountLoginUniquenessValidator} from "./constants";
import {SessionStorage} from "src/electron-main/session-storage";
import {formatFileUrl} from "./util";
import {WEBPACK_WEB_CHUNK_NAMES} from "src/shared/webpack-conts";

function exists(file: string, storeFs: StoreModel.StoreFs): boolean {
try {
Expand Down Expand Up @@ -122,10 +117,10 @@ function initLocations(
trayIcon: icon,
trayIconFont: appRelativePath("./assets/fonts/tray-icon/roboto-derivative.ttf"),
browserWindowPage: formatFileUrl(
appRelativePath("./web/", WEB_CHUNK_NAMES["browser-window"], "index.html"),
appRelativePath("./web/", WEBPACK_WEB_CHUNK_NAMES["browser-window"], "index.html"),
),
aboutBrowserWindowPage: appRelativePath("./web/", WEB_CHUNK_NAMES.about, "index.html"),
searchInPageBrowserViewPage: appRelativePath("./web/", WEB_CHUNK_NAMES["search-in-page-browser-view"], "index.html"),
aboutBrowserWindowPage: appRelativePath("./web/", WEBPACK_WEB_CHUNK_NAMES.about, "index.html"),
searchInPageBrowserViewPage: appRelativePath("./web/", WEBPACK_WEB_CHUNK_NAMES["search-in-page-browser-view"], "index.html"),
preload: {
aboutBrowserWindow: appRelativePath("./electron-preload/about.js"),
browserWindow: appRelativePath("./electron-preload/browser-window.js"),
Expand All @@ -135,16 +130,9 @@ function initLocations(
primary: formatFileUrl(appRelativePath("./electron-preload/webview/primary.js")),
calendar: formatFileUrl(appRelativePath("./electron-preload/webview/calendar.js")),
},
vendorsAppCssLinkHref: ((): string => {
// TODO electron: get rid of "baseURLForDataURL" workaround, see https://github.com/electron/electron/issues/20700
const webRelativeCssFilePath = "browser-window/shared-vendor.css";
const file = appRelativePath("web", webRelativeCssFilePath);
const stat = storeFs._impl.statSync(file);
if (!stat.isFile()) {
throw new Error(`Location "${file}" exists but it's not a file`);
}
return `${WEB_PROTOCOL_SCHEME}://${webRelativeCssFilePath}`;
})(),
// TODO electron: get rid of "baseURLForDataURL" workaround, see https://github.com/electron/electron/issues/20700
vendorsAppCssLinkHrefs: ["shared-vendor-dark", "shared-vendor-light"]
.map((value) => `${WEB_PROTOCOL_SCHEME}://browser-window/${value}.css`),
...((): NoExtraProps<Pick<ElectronContextLocations, "protocolBundles" | "webClients">> => {
const {protocolBundles, webClients}:
{
Expand Down
18 changes: 18 additions & 0 deletions src/electron-main/native-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Config} from "src/shared/model/options";
import {nativeTheme} from "electron";
import {IPC_MAIN_API_NOTIFICATION$} from "src/electron-main/api/constants";
import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main";

export const applyThemeSource = (themeSource: Config["themeSource"]): void => {
nativeTheme.themeSource = themeSource;
};

export const initNativeThemeNotification = (themeSource: Config["themeSource"]): void => {
nativeTheme.on("updated", () => {
IPC_MAIN_API_NOTIFICATION$.next(
IPC_MAIN_API_NOTIFICATION_ACTIONS.NativeTheme({shouldUseDarkColors: nativeTheme.shouldUseDarkColors}),
);
});

applyThemeSource(themeSource);
};
Loading

0 comments on commit b203fa4

Please sign in to comment.