diff --git a/import-sorter.json b/import-sorter.json index 8d832d0e7..17e58115a 100644 --- a/import-sorter.json +++ b/import-sorter.json @@ -5,7 +5,7 @@ "quoteMark": "double", "groupRules": [ {}, - "^(scripts/|src/|webpack-configs/|webpack-configs/|\\./|\\.\\./)" + "^(scripts/|src/|webpack-configs/|\\./|\\.\\./)" ], "wrappingStyle": { "maxBindingNamesPerLine": 0, diff --git a/package.json b/package.json index 68f36e237..32f3a6256 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "build:electron-main:dev": "cross-env NODE_ENV=development npm-run-all build:electron-main", "build:electron-main:watch": "cross-env WEBPACK_ENV_WATCH=1 npm-run-all build:electron-main", "build:electron-main:watch:dev": "cross-env WEBPACK_ENV_WATCH=1 npm-run-all build:electron-main:dev", - "build:electron-preload": "npm-run-all build:electron-preload:about build:electron-preload:browser-window build:electron-preload:database-indexer build:electron-preload:search-in-page-browser-view build:electron-preload:webview-primary build:electron-preload:webview-calendar", + "build:electron-preload": "npm-run-all build:electron-preload:about build:electron-preload:browser-window build:electron-preload:database-indexer build:electron-preload:search-in-page-browser-view build:electron-preload:webview-primary", "build:electron-preload:dev": "cross-env NODE_ENV=development npm-run-all build:electron-preload", "build:electron-preload:about": "pnpm run webpack:shortcut -- --config ./webpack-configs/preload/about.ts", "build:electron-preload:browser-window": "pnpm run webpack:shortcut -- --config ./webpack-configs/preload/browser-window.ts", @@ -40,8 +40,6 @@ "build:electron-preload:search-in-page-browser-view": "pnpm run webpack:shortcut -- --config ./webpack-configs/preload/search-in-page-browser-view.ts", "build:electron-preload:webview-primary": "pnpm run webpack:shortcut -- --config ./webpack-configs/preload/webview-primary.ts", "build:electron-preload:webview-primary:dev": "cross-env NODE_ENV=development npm-run-all build:electron-preload:webview-primary", - "build:electron-preload:webview-calendar": "pnpm run webpack:shortcut -- --config ./webpack-configs/preload/webview-calendar.ts", - "build:electron-preload:webview-calendar:dev": "cross-env NODE_ENV=development npm-run-all build:electron-preload:webview-calendar", "build:web": "npm-run-all build:web:browser-window build:web:about build:web:search", "build:web:dev": "cross-env NODE_ENV=development npm-run-all build:web", "build:web:about": "pnpm run webpack:shortcut -- --config ./webpack-configs/web/about.ts", @@ -79,7 +77,7 @@ "lint:ts:eslint": "pnpm run lint:ts:base:eslint \"./src/**/*.ts\" \"./scripts/**/*.ts\" \"./webpack-configs/**/*.ts\"", "start:electron": "electron ./app/electron-main/index.cjs", "start:electron:dev": "electron --inspect-brk- ./app-dev/electron-main/index.cjs", - "test:e2e:build-code": "cross-env NODE_ENV=e2e npm-run-all build:electron-main build:electron-preload:browser-window build:electron-preload:webview-primary build:electron-preload:webview-calendar", + "test:e2e:build-code": "cross-env NODE_ENV=e2e npm-run-all build:electron-main build:electron-preload:browser-window build:electron-preload:webview-primary", "test:e2e:run": "cross-env DEBUG=pw:api node ./node_modules/@playwright/test/cli.js test --timeout 1200000", "test:e2e": "npm-run-all test:e2e:build-code test:e2e:run", "scripts/prepare-native-deps": "pnpm run ts-node:shortcut ./scripts/prepare-native-deps.ts", diff --git a/patches/protonmail/session-storage-8.patch b/patches/protonmail/session-storage-8.patch index 1e3275bc5..b9710b189 100644 --- a/patches/protonmail/session-storage-8.patch +++ b/patches/protonmail/session-storage-8.patch @@ -121,13 +121,21 @@ index f24616faf9..6c05699766 100644 +/* */ diff --git a/packages/shared/lib/authentication/createAuthenticationStore.ts b/packages/shared/lib/authentication/createAuthenticationStore.ts -index b5cbba6cf7..21c710ab33 100644 +index aab52552ad..149992dfa7 100644 --- a/packages/shared/lib/authentication/createAuthenticationStore.ts +++ b/packages/shared/lib/authentication/createAuthenticationStore.ts -@@ -48,8 +48,8 @@ const defaultAuthData = { +@@ -6,6 +6,7 @@ import { stripLeadingAndTrailingSlash } from '../helpers/string'; + import { appMode } from '../webpack.constants'; + import { getBasename, getLocalIDFromPathname, stripLocalBasenameFromPathname } from './pathnameHelper'; + import { getPersistedSession } from './persistedSessionStorage'; ++import { APPS_CONFIGURATION } from '@proton/shared/lib/constants'; + + const MAILBOX_PASSWORD_KEY = 'proton:mailbox_pwd'; + const UID_KEY = 'proton:oauth:UID'; +@@ -51,8 +52,8 @@ const defaultAuthData = { basename: undefined, }; - + -const getInitialState = (mode: 'sso' | 'standalone', oldUID?: string, oldLocalID?: number): AuthData => { - if (mode === 'standalone') { +const getInitialState = (mode: 'sso' | 'standalone' | 'bundle', oldUID?: string, oldLocalID?: number): AuthData => { @@ -135,7 +143,22 @@ index b5cbba6cf7..21c710ab33 100644 return { UID: oldUID, localID: undefined, -@@ -215,7 +215,7 @@ const createAuthenticationStore = ({ mode = appMode, initialAuth, store: { set, +@@ -175,6 +176,14 @@ const createAuthenticationStore = ({ mode = appMode, initialAuth, store: { set, + basename = undefined; + } + ++ { ++ const protonApp = window.sessionStorage.getItem("electron_mail:proton_app_name"); ++ if (protonApp && protonApp in APPS_CONFIGURATION) { ++ const {publicPath} = APPS_CONFIGURATION[protonApp as keyof typeof APPS_CONFIGURATION]; ++ return publicPath || "/"; ++ } ++ } ++ + return getPath(basename, window.location.href, path); + }; + +@@ -218,7 +227,7 @@ const createAuthenticationStore = ({ mode = appMode, initialAuth, store: { set, return basename; }, get ready(): boolean { diff --git a/scripts/prepare-webclient/webclients.ts b/scripts/prepare-webclient/webclients.ts index c8bdf3602..6f8390e14 100644 --- a/scripts/prepare-webclient/webclients.ts +++ b/scripts/prepare-webclient/webclients.ts @@ -263,10 +263,9 @@ async function executeBuildFlow( : undefined; { - const webpackIndexEntryItems = repoType === "proton-mail" || repoType === "proton-calendar" + const webpackIndexEntryItems = "webpackIndexEntryItems" in PROVIDER_REPO_MAP[repoType].protonPack ? PROVIDER_REPO_MAP[repoType].protonPack.webpackIndexEntryItems - : undefined; - + : null; // https://github.com/ProtonMail/proton-pack/tree/2e44d5fd9d2df39787202fc08a90757ea47fe480#how-to-configure writeFile( path.join(appDir, "./proton.config.js"), diff --git a/src/electron-main/api/endpoints-builders/account.ts b/src/electron-main/api/endpoints-builders/account.ts index 234f83ce3..317f17d95 100644 --- a/src/electron-main/api/endpoints-builders/account.ts +++ b/src/electron-main/api/endpoints-builders/account.ts @@ -55,6 +55,7 @@ export async function buildEndpoints( proxy, loginDelayUntilSelected, loginDelaySecondsRange, + entryProtonApp, }, ) { assertEntryUrl(entryUrl); @@ -80,6 +81,7 @@ export async function buildEndpoints( proxy, loginDelayUntilSelected, loginDelaySecondsRange, + entryProtonApp, }; const result = await ctx.settingsStoreQueue.q(async () => { const settings = await ctx.settingsStore.readExisting(); @@ -114,6 +116,7 @@ export async function buildEndpoints( proxy, loginDelayUntilSelected, loginDelaySecondsRange, + entryProtonApp, }, ) { assertEntryUrl(entryUrl); @@ -151,6 +154,7 @@ export async function buildEndpoints( account.proxy = proxy; account.loginDelayUntilSelected = loginDelayUntilSelected; account.loginDelaySecondsRange = loginDelaySecondsRange; + account.entryProtonApp = entryProtonApp; if (credentials) { const {credentials: existingCredentials} = account; diff --git a/src/electron-main/context.ts b/src/electron-main/context.ts index 87f8141a5..09e7b481d 100644 --- a/src/electron-main/context.ts +++ b/src/electron-main/context.ts @@ -106,9 +106,6 @@ function initLocations(storeFs: StoreModel.StoreFs, paths?: ContextInitOptionsPa primary: formatFileUrl( appRelativePath(`./electron-preload/webview/primary/index${BUILD_ENVIRONMENT === "e2e" ? "-e2e" : ""}.js`), ), - calendar: formatFileUrl( - appRelativePath(`./electron-preload/webview/calendar/index${BUILD_ENVIRONMENT === "e2e" ? "-e2e" : ""}.js`), - ), }, // TODO electron: get rid of "baseURLForDataURL" workaround, see https://github.com/electron/electron/issues/20700 vendorsAppCssLinkHrefs: ["shared-vendor-dark", "shared-vendor-light"].map((value) => diff --git a/src/electron-main/storage-upgrade.ts b/src/electron-main/storage-upgrade.ts index 51e0945aa..40e324feb 100644 --- a/src/electron-main/storage-upgrade.ts +++ b/src/electron-main/storage-upgrade.ts @@ -360,6 +360,9 @@ const CONFIG_UPGRADES: Record void> = { } } }, + "5.2.4": (config) => { + delete (config as {calendarNotification?: unknown}).calendarNotification; + }, // last updater "100.0.0": (config) => { // ensuring default base props are set @@ -514,6 +517,11 @@ export const upgradeSettings: upgradeSettingsType = ((): upgradeSettingsType => } } }, + "5.2.4": (settings) => { + for (const account of settings.accounts) { + if (!account.entryProtonApp) account.entryProtonApp = "proton-mail"; + } + }, // last updater "100.0.0": (settings): void => { settings.accounts.forEach((account, index) => { diff --git a/src/electron-preload/lib/electron-exposure/index.ts b/src/electron-preload/lib/electron-exposure/index.ts index f73091f53..5bdedb79c 100644 --- a/src/electron-preload/lib/electron-exposure/index.ts +++ b/src/electron-preload/lib/electron-exposure/index.ts @@ -1,17 +1,17 @@ import {ElectronWindow} from "src/shared/model/electron"; import {IPC_MAIN_API} from "src/shared/api/main-process"; import {LOGGER} from "src/electron-preload/lib/electron-exposure/logger"; -import {PROTON_CALENDAR_IPC_WEBVIEW_API} from "src/shared/api/webview/calendar"; -import {PROTON_PRIMARY_IPC_WEBVIEW_API} from "src/shared/api/webview/primary"; +import {PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API} from "src/shared/api/webview/primary-common"; import {PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API} from "src/shared/api/webview/primary-login"; +import {PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API} from "src/shared/api/webview/primary-mail"; import {registerDocumentClickEventListener} from "src/electron-preload/lib/events-handling"; export const ELECTRON_WINDOW: Readonly = Object.freeze({ __ELECTRON_EXPOSURE__: Object.freeze({ buildIpcMainClient: IPC_MAIN_API.client.bind(IPC_MAIN_API), - buildIpcPrimaryWebViewClient: PROTON_PRIMARY_IPC_WEBVIEW_API.client.bind(PROTON_PRIMARY_IPC_WEBVIEW_API), + buildIpcPrimaryCommonWebViewClient: PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API.client.bind(PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API), buildIpcPrimaryLoginWebViewClient: PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API.client.bind(PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API), - buildIpcCalendarWebViewClient: PROTON_CALENDAR_IPC_WEBVIEW_API.client.bind(PROTON_CALENDAR_IPC_WEBVIEW_API), + buildIpcPrimaryMailWebViewClient: PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API.client.bind(PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API), registerDocumentClickEventListener, Logger: LOGGER, }), diff --git a/src/electron-preload/tsconfig.json b/src/electron-preload/tsconfig.json index 8794759bb..fac9778b2 100644 --- a/src/electron-preload/tsconfig.json +++ b/src/electron-preload/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "include": [ - "../../src/@types/**/*" + "../../src/@types/**/*", + "./**/*" ] } diff --git a/src/electron-preload/webview/calendar/api.ts b/src/electron-preload/webview/calendar/api.ts deleted file mode 100644 index 682dbbdd7..000000000 --- a/src/electron-preload/webview/calendar/api.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {merge} from "rxjs"; - -import {curryFunctionMembers} from "src/shared/util"; -import {getLocationHref} from "src/shared/util/web"; -import {PROTON_CALENDAR_IPC_WEBVIEW_API, ProtonCalendarApi} from "src/shared/api/webview/calendar"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; - -const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.calendar, __filename); - -export const registerApi = (/* providerApi: ProviderApi */): void => { - const endpoints: ProtonCalendarApi = { - async ping({accountIndex}) { - return {value: JSON.stringify({accountIndex})}; - }, - - notification({accountIndex}) { - const logger = curryFunctionMembers(_logger, nameof.full(endpoints.notification), accountIndex); - - logger.info(); - - return merge(); - }, - }; - - PROTON_CALENDAR_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); - - _logger.verbose(`api registered, url: ${getLocationHref()}`); -}; diff --git a/src/electron-preload/webview/calendar/index.ts b/src/electron-preload/webview/calendar/index.ts deleted file mode 100644 index 619a2101b..000000000 --- a/src/electron-preload/webview/calendar/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {ipcRenderer} from "electron"; - -import {attachUnhandledErrorHandler, documentCookiesForCustomScheme} from "src/electron-preload/webview/lib/util"; -import {curryFunctionMembers} from "src/shared/util"; -import {getLocationHref} from "src/shared/util/web"; -import {initProviderApi} from "./provider-api"; -import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; -import {registerApi} from "./api"; -import {setupProtonOpenNewTabEventHandler} from "src/electron-preload/webview/lib/custom-event"; -import {testProtonCalendarAppPage} from "src/shared/util/proton-webclient"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; - -const logger = curryFunctionMembers(WEBVIEW_LOGGERS.calendar, __filename); - -attachUnhandledErrorHandler(logger); - -const protonAppPageStatus = testProtonCalendarAppPage({url: getLocationHref(), logger}); - -documentCookiesForCustomScheme.enable(logger); -setupProtonOpenNewTabEventHandler(logger); - -// TODO throw error if "not calendar or blank.html" page loaded -if (protonAppPageStatus.shouldInitProviderApi) { - // TODO set up timeout - initProviderApi().then(registerApi).then(() => ipcRenderer.sendToHost(IPC_WEBVIEW_API_CHANNELS_MAP.calendar.registered)).catch( - (error) => { - logger.error(error); - throw error; - }, - ); -} diff --git a/src/electron-preload/webview/calendar/provider-api/internals.ts b/src/electron-preload/webview/calendar/provider-api/internals.ts deleted file mode 100644 index ff0ebe12c..000000000 --- a/src/electron-preload/webview/calendar/provider-api/internals.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {BehaviorSubject} from "rxjs"; - -import {curryFunctionMembers} from "src/shared/util"; -import {ProviderInternals} from "./model"; -import * as webpackJsonpPushUtil from "src/electron-preload/webview/lib/provider-api/webpack-jsonp-push-util"; -import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; - -const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.calendar, __filename); - -export const resolveProviderInternals = async (): Promise => { - const logger = curryFunctionMembers(_logger, nameof(resolveProviderInternals)); - - logger.info(); - - return new Promise((resolve /*, reject */) => { // TODO reject on timeout - const result: ProviderInternals = { - "./src/app/./containers/calendar/MainContainer": { - value$: new BehaviorSubject( - {privateScope: null} as Unpacked, - ), - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - _valueShape: null as any, - }, - }; - const resolveIfFullyInitialized = webpackJsonpPushUtil.buildFullyInitializedResolver(result, resolve, logger); - - webpackJsonpPushUtil.overridePushMethodGlobally({ - resultKeys: Object.keys(result) as ReadonlyArray, - preChunkItemOverridingHook({resultKey}) { - if (resultKey === "./src/app/./containers/calendar/MainContainer") { - // mark lazy-loaded modules as initialized immediately since these modules get - // loaded only after the user gets logged in but we need to resolve the promise on initial load - webpackJsonpPushUtil.markInternalsRecordAsInitialized(result, resultKey, resolveIfFullyInitialized, logger); - } - }, - chunkItemHook({resultKey, webpack_exports, webpack_require}) { - if (resultKey === "./src/app/./containers/calendar/MainContainer") { - webpackJsonpPushUtil.handleObservableValue(result, { - resultKey, - webpack_exports, - itemName: "default", - itemCallResultTypeValidation: "object", // import("react").ReactNode - itemCallResultHandler: (itemCallResult, notify, markAsInitialized) => { - const {createElement, useEffect} = webpack_require("../../node_modules/react/index.js"); - const result = [ - createElement(() => { - useEffect(() => { - notify({privateScope: {}}); - // TODO consider notifying null on component destroying stage - }); - - return null; // no rendering needed - }), - itemCallResult, - ]; - - // immediate initialization mark set since this component doesn't get instantiated right on proton - // app start but after the user gets logged-in (we need to resolve the promise on initial load) - markAsInitialized(); - - return result; - }, - resolveIfFullyInitialized, - }, logger); - } - }, - logger, - }); - }); -}; diff --git a/src/electron-preload/webview/calendar/provider-api/model.ts b/src/electron-preload/webview/calendar/provider-api/model.ts deleted file mode 100644 index 8b590d24c..000000000 --- a/src/electron-preload/webview/calendar/provider-api/model.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {AddInitializedProp, DefineObservableValue} from "src/electron-preload/webview/lib/provider-api/model"; -import {PROVIDER_REPO_MAP, PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS} from "src/shared/const/proton-apps"; - -export type Keys = StrictExclude< - (typeof PROVIDER_REPO_MAP)["proton-calendar"]["protonPack"]["webpackIndexEntryItems"][number], - typeof PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS[number] ->; - -export type LazyKeys = never; - -export type ImmediateKeys = StrictExclude; - -// TODO clone the proton project on npm postinstall hook and reference the modules signatures from their typescript code -// like: typeof import("output/git/proton-calendar/src/app/content/PrivateApp.tsx") -export type ProviderInternals = AddInitializedProp< - { - [K in StrictExtract]: DefineObservableValue< - {readonly privateScope: unknown}, - (arg: unknown) => import("react").ReactNode - >; - } ->; - -export type ProviderApi = DeepReadonly<{_custom_: {loggedIn$: import("rxjs").Observable}}>; diff --git a/src/electron-preload/webview/calendar/tsconfig.json b/src/electron-preload/webview/calendar/tsconfig.json deleted file mode 100644 index 6e420eda7..000000000 --- a/src/electron-preload/webview/calendar/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "files": [ - "./index.ts" - ] -} diff --git a/src/electron-preload/webview/lib/const.ts b/src/electron-preload/webview/lib/const.ts index 42bfb7253..bf870c1bc 100644 --- a/src/electron-preload/webview/lib/const.ts +++ b/src/electron-preload/webview/lib/const.ts @@ -2,9 +2,8 @@ import {buildLoggerBundle} from "src/electron-preload/lib/util"; import {Logger} from "src/shared/model/common"; import {PACKAGE_NAME} from "src/shared/const"; -export const WEBVIEW_LOGGERS: Readonly> = { +export const WEBVIEW_LOGGERS: Readonly> = { primary: buildLoggerBundle(`${__filename} [preload: webview/primary]`), - calendar: buildLoggerBundle(`${__filename} [preload: webview/calendar]`), }; export const RATE_LIMITED_METHOD_CALL_MESSAGE = `${PACKAGE_NAME}_RATE_LIMITED_METHOD_CALL_MESSAGE`; diff --git a/src/electron-preload/webview/lib/database-entity/mail.ts b/src/electron-preload/webview/lib/database-entity/mail.ts index 0e14bf79b..09ec2292b 100644 --- a/src/electron-preload/webview/lib/database-entity/mail.ts +++ b/src/electron-preload/webview/lib/database-entity/mail.ts @@ -6,7 +6,7 @@ import {lzutf8Util} from "src/shared/util/entity"; import {MessagesResponse} from "src/electron-preload/webview/lib/rest-model"; import {MIME_TYPES} from "src/shared/model/database"; import {ONE_SECOND_MS, PACKAGE_VERSION} from "src/shared/const"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import * as RestModel from "src/electron-preload/webview/lib/rest-model"; const logger = buildLoggerBundle(__filename); diff --git a/src/electron-preload/webview/lib/util.ts b/src/electron-preload/webview/lib/util.ts index d0e4cecd0..4b28222a6 100644 --- a/src/electron-preload/webview/lib/util.ts +++ b/src/electron-preload/webview/lib/util.ts @@ -12,7 +12,7 @@ import {FsDbAccount} from "src/shared/model/database"; import {IpcMainApiEndpoints} from "src/shared/api/main-process"; import {LOCAL_WEBCLIENT_ORIGIN, ONE_MINUTE_MS, ONE_SECOND_MS} from "src/shared/const"; import {Logger} from "src/shared/model/common"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import {RATE_LIMITED_METHOD_CALL_MESSAGE} from "src/electron-preload/webview/lib/const"; import {resolveIpcMainApi} from "src/electron-preload/lib/util"; import * as RestModel from "src/electron-preload/webview/lib/rest-model"; diff --git a/src/electron-preload/webview/primary/common/api.ts b/src/electron-preload/webview/primary/common/api.ts new file mode 100644 index 000000000..fe8b3b37c --- /dev/null +++ b/src/electron-preload/webview/primary/common/api.ts @@ -0,0 +1,72 @@ +import {EMPTY, from, merge, Observable} from "rxjs"; +import {ipcRenderer} from "electron"; +import {map, mergeMap, switchMap, tap, throttleTime} from "rxjs/operators"; + +import {curryFunctionMembers} from "src/shared/util"; +import {documentCookiesForCustomScheme} from "src/electron-preload/webview/lib/util"; +import {dumpProtonSharedSession} from "src/electron-preload/webview/primary/shared-session"; +import {getLocationHref} from "src/shared/util/web"; +import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; +import {IpcMainServiceScan} from "src/shared/api/main-process"; +import {LOGGER} from "src/electron-preload/lib/electron-exposure/logger"; +import {ONE_SECOND_MS} from "src/shared/const"; +import {PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API, ProtonPrimaryCommonApi} from "src/shared/api/webview/primary-common"; +import {ProviderApi} from "./provider-api"; +import {resolveIpcMainApi} from "src/electron-preload/lib/util"; + +const resolveCookieSessionStoragePatch = (): IpcMainServiceScan["ApiImplReturns"]["resolvedSavedSessionStoragePatch"] => { + // https://github.com/expo/tough-cookie-web-storage-store/blob/36a20183dad5f84f2c14ae87251737dbbeb2af88/WebStorageCookieStore.js#L12 + // TODO move "__cookieStore__" to external/reusable constant + const sessionStorageCookieStoreKey = {"tough-cookie-web-storage-store": {storageCookieKey: "__cookieStore__"}} as const; + const {"tough-cookie-web-storage-store": {storageCookieKey}} = sessionStorageCookieStoreKey; + const {__cookieStore__} = {[storageCookieKey]: window.sessionStorage.getItem(storageCookieKey)}; + return __cookieStore__ ? {__cookieStore__} : null; +}; + +export const registerApi = ( + providerApi: ProviderApi, + _logger: typeof LOGGER, +): void => { + const endpoints: ProtonPrimaryCommonApi = { + async resolveLiveProtonClientSession({accountIndex}) { + _logger.info(nameof(endpoints.resolveLiveProtonClientSession), accountIndex); + return dumpProtonSharedSession(); + }, + + async resolvedLiveSessionStoragePatch({accountIndex}) { + _logger.info(nameof(endpoints.resolvedLiveSessionStoragePatch), accountIndex); + return resolveCookieSessionStoragePatch(); + }, + + notification({login, apiEndpointOrigin, accountIndex}) { + const logger = curryFunctionMembers(_logger, nameof(endpoints.notification), accountIndex); + + logger.info(); + + type LoggedInOutput = Required>, "loggedIn">>; + + const observables: [Observable] = [ + providerApi._custom_.loggedIn$.pipe(map((loggedIn) => ({loggedIn}))), + ]; + const ipcMain = resolveIpcMainApi({logger}); + + return merge( + merge(...observables).pipe(tap((notification) => logger.verbose(JSON.stringify({notification})))), + documentCookiesForCustomScheme.setNotification$.pipe( + throttleTime(ONE_SECOND_MS / 4), + mergeMap(() => { + const sessionStorageItem = resolveCookieSessionStoragePatch(); + return sessionStorageItem + ? (from(ipcMain("saveSessionStoragePatch")({login, apiEndpointOrigin, sessionStorageItem})) + .pipe(switchMap(() => EMPTY))) + : EMPTY; + }), + ), + ); + }, + }; + + PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); + ipcRenderer.sendToHost(IPC_WEBVIEW_API_CHANNELS_MAP.common.registered); + _logger.verbose(`api registered, url: ${getLocationHref()}`); +}; diff --git a/src/electron-preload/webview/primary/common/provider-api/const.ts b/src/electron-preload/webview/primary/common/provider-api/const.ts new file mode 100644 index 000000000..bc9e362da --- /dev/null +++ b/src/electron-preload/webview/primary/common/provider-api/const.ts @@ -0,0 +1,16 @@ +export const PRIMARY_INTERNALS_APP_TYPES = ["proton-mail", "proton-calendar", "proton-drive"] as const; + +export const PRIMARY_INTERNALS_KEYS = { + [PRIMARY_INTERNALS_APP_TYPES[0]]: { + key: "./src/app/containers/PageContainer.tsx", + handleObservableValue: {itemName: "PageParamsParser", itemCallResultTypeValidation: "object"}, + }, + [PRIMARY_INTERNALS_APP_TYPES[1]]: { + key: "./src/app/containers/calendar/MainContainer.tsx", + handleObservableValue: {itemName: "WrappedMainContainer", itemCallResultTypeValidation: "object"}, + }, + [PRIMARY_INTERNALS_APP_TYPES[2]]: { + key: "./src/app/containers/MainContainer.tsx", + handleObservableValue: {itemName: "MainContainer", itemCallResultTypeValidation: "object"}, + }, +} as const; diff --git a/src/electron-preload/webview/calendar/provider-api/index.ts b/src/electron-preload/webview/primary/common/provider-api/index.ts similarity index 63% rename from src/electron-preload/webview/calendar/provider-api/index.ts rename to src/electron-preload/webview/primary/common/provider-api/index.ts index 009f77727..a015ff168 100644 --- a/src/electron-preload/webview/calendar/provider-api/index.ts +++ b/src/electron-preload/webview/primary/common/provider-api/index.ts @@ -2,21 +2,26 @@ import {combineLatest} from "rxjs"; import {distinctUntilChanged, map} from "rxjs/operators"; import {curryFunctionMembers} from "src/shared/util"; -import {ProviderApi} from "./model"; +import {PRIMARY_INTERNALS_KEYS} from "./const"; import {resolveProviderInternals} from "./internals"; -import {resolveStandardSetupPublicApi} from "src/electron-preload/webview/lib/provider-api/standart-setup-internals"; +import {resolveStandardSetupPublicApi} from "src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals"; import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; -const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.calendar, __filename); +const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); -export const initProviderApi = async (): Promise => { - const logger = curryFunctionMembers(_logger, nameof(initProviderApi)); +export type ProviderApi = DeepReadonly<{_custom_: {loggedIn$: import("rxjs").Observable}}>; + +export const initProviderApi = async (appType: keyof typeof PRIMARY_INTERNALS_KEYS): Promise => { + const logger = curryFunctionMembers(_logger, [nameof(initProviderApi), JSON.stringify({appType})]); logger.info(); return (async (): Promise => { - const [standardSetupPublicApi, internals] = await Promise.all([resolveStandardSetupPublicApi(logger), resolveProviderInternals()]); - const internalsPrivateScope$ = internals["./src/app/./containers/calendar/MainContainer"].value$.pipe(distinctUntilChanged()); + const [standardSetupPublicApi, internals] = await Promise.all([ + resolveStandardSetupPublicApi(logger), + resolveProviderInternals(appType), + ]); + const internalsPrivateScope$ = internals[PRIMARY_INTERNALS_KEYS[appType].key].value$.pipe(distinctUntilChanged()); const providerApi: ProviderApi = { _custom_: { loggedIn$: combineLatest([standardSetupPublicApi.authentication$, internalsPrivateScope$]).pipe( diff --git a/src/electron-preload/webview/primary/common/provider-api/internals.ts b/src/electron-preload/webview/primary/common/provider-api/internals.ts new file mode 100644 index 000000000..510840fc8 --- /dev/null +++ b/src/electron-preload/webview/primary/common/provider-api/internals.ts @@ -0,0 +1,75 @@ +import {BehaviorSubject} from "rxjs"; + +import {curryFunctionMembers} from "src/shared/util"; +import {PRIMARY_INTERNALS_KEYS} from "./const"; +import {ProviderInternals} from "./model"; +import * as webpackJsonpPushUtil from "src/electron-preload/webview/primary/lib/provider-api/webpack-jsonp-push-util"; +import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; + +const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); + +export const resolveProviderInternals = async < + T extends keyof typeof PRIMARY_INTERNALS_KEYS, + K extends typeof PRIMARY_INTERNALS_KEYS[T]["key"], +>( + appType: T, +): Promise> => { + const logger = curryFunctionMembers(_logger, nameof(resolveProviderInternals)); + + logger.info(); + + return new Promise>((resolve /*, reject */) => { // TODO reject on timeout + const result: ProviderInternals = { // eslint-disable-line @typescript-eslint/no-unsafe-assignment + [PRIMARY_INTERNALS_KEYS[appType].key]: { + value$: new BehaviorSubject({privateScope: null}), + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + _valueShape: null as any, + }, + } as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const resolveIfFullyInitialized = webpackJsonpPushUtil.buildFullyInitializedResolver(result, resolve, logger); + + webpackJsonpPushUtil.overridePushMethodGlobally({ + resultKeys: [PRIMARY_INTERNALS_KEYS[appType].key] as const, + + preChunkItemOverridingHook({resultKey}) { + if (resultKey === PRIMARY_INTERNALS_KEYS[appType].key) { + // mark lazy-loaded modules as initialized immediately since these modules get + // loaded only after a user gets signed-in, but we need to resolve the promise on initial load + webpackJsonpPushUtil.markInternalsRecordAsInitialized(result, resultKey, resolveIfFullyInitialized, logger); + } + }, + + chunkItemHook({resultKey, webpack_exports, webpack_require}) { + if (resultKey !== PRIMARY_INTERNALS_KEYS[appType].key) return; + + webpackJsonpPushUtil.handleObservableValue(result, { + resultKey, + webpack_exports, + ...PRIMARY_INTERNALS_KEYS[appType].handleObservableValue, + itemCallResultHandler: (itemCallResult, notify, markAsInitialized) => { + const {createElement, useEffect} = webpack_require("../../node_modules/react/index.js"); + const result = [ + createElement(() => { + useEffect(() => { + notify({privateScope: {}}); + // TODO consider notifying null on component destroying stage + }); + return null; // no rendering needed + }), + itemCallResult, + ]; + + // immediate initialization mark set since this component doesn't get instantiated right on proton + // app start but after the user gets logged-in (we need to resolve the promise on initial load) + markAsInitialized(); + + return result; + }, + resolveIfFullyInitialized, + }, logger); + }, + + logger, + }); + }); +}; diff --git a/src/electron-preload/webview/primary/common/provider-api/model.ts b/src/electron-preload/webview/primary/common/provider-api/model.ts new file mode 100644 index 000000000..217e20fb0 --- /dev/null +++ b/src/electron-preload/webview/primary/common/provider-api/model.ts @@ -0,0 +1,39 @@ +import {AddInitializedProp, DefineObservableValue} from "src/electron-preload/webview/primary/lib/provider-api/model"; +import {PRIMARY_INTERNALS_KEYS} from "./const"; +import {PROVIDER_REPO_MAP, PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS} from "src/shared/const/proton-apps"; + +export type Keys = StrictExclude< + | (typeof PROVIDER_REPO_MAP)["proton-mail"]["protonPack"]["webpackIndexEntryItems"][number] + | (typeof PROVIDER_REPO_MAP)["proton-calendar"]["protonPack"]["webpackIndexEntryItems"][number] + | (typeof PROVIDER_REPO_MAP)["proton-drive"]["protonPack"]["webpackIndexEntryItems"][number], + typeof PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS[number] +>; + +export type LazyKeys = never; + +export type ImmediateKeys = StrictExclude; + +// TODO clone the proton project on npm postinstall hook and reference the modules signatures from their typescript code +// like: typeof import("output/git/proton-calendar/src/app/content/PrivateApp.tsx") +export type ProviderInternals = AddInitializedProp< + & { + [K in StrictExtract]: DefineObservableValue< + {readonly privateScope: unknown}, + (arg: unknown) => import("react").ReactNode + >; + } + & { + [K in StrictExtract]: DefineObservableValue< + {readonly privateScope: unknown}, + (arg: unknown) => import("react").ReactNode + >; + } + & { + [K in StrictExtract]: DefineObservableValue< + {readonly privateScope: unknown}, + (arg: unknown) => import("react").ReactNode + >; + } +>; + +export type ProviderApi = DeepReadonly<{_custom_: {loggedIn$: import("rxjs").Observable}}>; diff --git a/src/electron-preload/webview/primary/index.ts b/src/electron-preload/webview/primary/index.ts index 436713779..840fcc893 100644 --- a/src/electron-preload/webview/primary/index.ts +++ b/src/electron-preload/webview/primary/index.ts @@ -1,60 +1,71 @@ -import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; +import {disableBrowserNotificationFeature} from "src/electron-preload/webview/lib/util"; +import {PRIMARY_INTERNALS_APP_TYPES} from "./common/provider-api/const"; import {PROTON_APP_MAIL_LOGIN_PATHNAME} from "src/shared/const/proton-url"; const main = async (): Promise => { const [ - {ipcRenderer}, {WEBVIEW_LOGGERS}, {curryFunctionMembers}, - {testProtonMailAppPage}, + {testProtonAppPage}, {documentCookiesForCustomScheme, attachUnhandledErrorHandler}, {getLocationHref}, - {initProviderApi}, - {registerApi}, + {initProviderApi: initCommonProviderApi}, + {initProviderApi: initMailProviderApi}, + {registerApi: registerCommonApi}, + {registerApi: registerMailApi}, {registerApi: registerLoginApi}, {setupProtonOpenNewTabEventHandler}, {setupProviderIntegration}, ] = await Promise.all([ - import("electron"), import("src/electron-preload/webview/lib/const"), import("src/shared/util"), import("src/shared/util/proton-webclient"), import("src/electron-preload/webview/lib/util"), import("src/shared/util/web"), - import("./provider-api"), - import("./api"), - import("./api/login"), + import("./common/provider-api"), + import("./mail/provider-api"), + import("./common/api"), + import("./mail/api"), + import("./mail/api/login"), import("src/electron-preload/webview/lib/custom-event"), - import("./provider-api/setup"), + import("./mail/provider-api/setup"), ]); const logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); attachUnhandledErrorHandler(logger); - - const protonAppPageStatus = testProtonMailAppPage({url: getLocationHref(), logger}); - documentCookiesForCustomScheme.enable(logger); setupProtonOpenNewTabEventHandler(logger); - setupProviderIntegration(protonAppPageStatus); - if (protonAppPageStatus.packagedWebClientUrl?.pathname === PROTON_APP_MAIL_LOGIN_PATHNAME) { - registerLoginApi(); - return; + // TODO use "mapToObj" + const protonAppPageStatuses = { + ["proton-mail"]: testProtonAppPage("proton-mail", {url: getLocationHref(), logger}), + ["proton-calendar"]: testProtonAppPage("proton-calendar", {url: getLocationHref(), logger}), + ["proton-drive"]: testProtonAppPage("proton-drive", {url: getLocationHref(), logger}), + } as const; + + if (!Object.values(protonAppPageStatuses).some((v) => v.blankHtmlPage)) { + setupProviderIntegration(); } - if (!protonAppPageStatus.shouldInitProviderApi) { + if (Object.values(protonAppPageStatuses).some((v) => v.packagedWebClientUrl?.pathname === PROTON_APP_MAIL_LOGIN_PATHNAME)) { + registerLoginApi(); return; } - try { + if (protonAppPageStatuses["proton-mail"].targetedProtonProject) { + disableBrowserNotificationFeature(logger); // TODO set up timeout - registerApi(await initProviderApi()); - } catch (error) { - logger.error(error); - throw error; + const commonProviderApi = await initCommonProviderApi("proton-mail"); + registerCommonApi(commonProviderApi, logger); + registerMailApi(commonProviderApi, await initMailProviderApi()); + } else { + for (const type of PRIMARY_INTERNALS_APP_TYPES) { + if (!protonAppPageStatuses[type].targetedProtonProject) continue; + const commonProviderApi = await initCommonProviderApi(type); + registerCommonApi(commonProviderApi, logger); + break; + } } - - ipcRenderer.sendToHost(IPC_WEBVIEW_API_CHANNELS_MAP.primary.registered); }; (async () => { diff --git a/src/electron-preload/webview/lib/provider-api/const.ts b/src/electron-preload/webview/primary/lib/provider-api/const.ts similarity index 100% rename from src/electron-preload/webview/lib/provider-api/const.ts rename to src/electron-preload/webview/primary/lib/provider-api/const.ts diff --git a/src/electron-preload/webview/lib/provider-api/model.ts b/src/electron-preload/webview/primary/lib/provider-api/model.ts similarity index 100% rename from src/electron-preload/webview/lib/provider-api/model.ts rename to src/electron-preload/webview/primary/lib/provider-api/model.ts diff --git a/src/electron-preload/webview/lib/provider-api/standart-setup-internals/index.ts b/src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/index.ts similarity index 87% rename from src/electron-preload/webview/lib/provider-api/standart-setup-internals/index.ts rename to src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/index.ts index d063fc4c2..f41c4fe15 100644 --- a/src/electron-preload/webview/lib/provider-api/standart-setup-internals/index.ts +++ b/src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/index.ts @@ -3,8 +3,8 @@ import {Observable} from "rxjs"; import {curryFunctionMembers} from "src/shared/util"; import {Logger} from "src/shared/model/common"; -import {resolveStandardSetupStandardSetupProviderInternals} from "./internals"; -import {StandardSetupPublicScope} from "src/electron-preload/webview/lib/provider-api/standart-setup-internals/model"; +import {resolveStandardSetupProviderInternals} from "./internals"; +import {StandardSetupPublicScope} from "src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/model"; export * from "./internals"; export * from "./model"; @@ -23,7 +23,7 @@ export const resolveStandardSetupPublicApi = async ( logger.info("init"); - const internals = await resolveStandardSetupStandardSetupProviderInternals(logger); + const internals = await resolveStandardSetupProviderInternals(logger); const scope$ = internals["../../packages/components/containers/app/StandardPrivateApp.tsx"].value$.pipe( map(({publicScope}) => publicScope), distinctUntilChanged(), diff --git a/src/electron-preload/webview/lib/provider-api/standart-setup-internals/internals.ts b/src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/internals.ts similarity index 95% rename from src/electron-preload/webview/lib/provider-api/standart-setup-internals/internals.ts rename to src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/internals.ts index 03d5a7a71..9abbfdfe4 100644 --- a/src/electron-preload/webview/lib/provider-api/standart-setup-internals/internals.ts +++ b/src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/internals.ts @@ -3,10 +3,10 @@ import {ReplaySubject} from "rxjs"; import {curryFunctionMembers} from "src/shared/util"; import {Logger} from "src/shared/model/common"; import {StandardSetupProviderInternals, StandardSetupProviderInternalsLazy} from "./model"; -import * as webpackJsonpPushUtil from "src/electron-preload/webview/lib/provider-api/webpack-jsonp-push-util"; +import * as webpackJsonpPushUtil from "src/electron-preload/webview/primary/lib/provider-api/webpack-jsonp-push-util"; -export const resolveStandardSetupStandardSetupProviderInternals = async (_logger: Logger): Promise => { - const logger = curryFunctionMembers(_logger, nameof(resolveStandardSetupStandardSetupProviderInternals)); +export const resolveStandardSetupProviderInternals = async (_logger: Logger): Promise => { + const logger = curryFunctionMembers(_logger, nameof(resolveStandardSetupProviderInternals)); logger.info(); diff --git a/src/electron-preload/webview/lib/provider-api/standart-setup-internals/model.ts b/src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/model.ts similarity index 98% rename from src/electron-preload/webview/lib/provider-api/standart-setup-internals/model.ts rename to src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/model.ts index 2421339ec..bb130dbdc 100644 --- a/src/electron-preload/webview/lib/provider-api/standart-setup-internals/model.ts +++ b/src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/model.ts @@ -1,4 +1,4 @@ -import {AddInitializedProp, DefineObservableValue} from "src/electron-preload/webview/lib/provider-api/model"; +import {AddInitializedProp, DefineObservableValue} from "src/electron-preload/webview/primary/lib/provider-api/model"; import {PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS} from "src/shared/const/proton-apps"; /* eslint-disable max-len */ diff --git a/src/electron-preload/webview/lib/provider-api/webpack-jsonp-push-util.ts b/src/electron-preload/webview/primary/lib/provider-api/webpack-jsonp-push-util.ts similarity index 100% rename from src/electron-preload/webview/lib/provider-api/webpack-jsonp-push-util.ts rename to src/electron-preload/webview/primary/lib/provider-api/webpack-jsonp-push-util.ts diff --git a/src/electron-preload/webview/primary/api/db-patch/bootstrap.ts b/src/electron-preload/webview/primary/mail/api/db-patch/bootstrap.ts similarity index 99% rename from src/electron-preload/webview/primary/api/db-patch/bootstrap.ts rename to src/electron-preload/webview/primary/mail/api/db-patch/bootstrap.ts index 36b5079d4..0a573cb88 100644 --- a/src/electron-preload/webview/primary/api/db-patch/bootstrap.ts +++ b/src/electron-preload/webview/primary/mail/api/db-patch/bootstrap.ts @@ -8,10 +8,10 @@ import * as DatabaseModel from "src/shared/model/database"; import {DbPatchBundle} from "./model"; import {DEFAULT_MESSAGES_STORE_PORTION_SIZE, ONE_SECOND_MS, PACKAGE_VERSION} from "src/shared/const"; import {FsDbAccount, LABEL_TYPE, SYSTEM_FOLDER_IDENTIFIERS} from "src/shared/model/database"; -import {isIgnorable404Error} from "../../util"; +import {isIgnorable404Error} from "../../../util"; import {Logger} from "src/shared/model/common"; import {PROTON_MAX_CONCURRENT_FETCH, PROTON_MAX_QUERY_PORTION_LIMIT} from "src/electron-preload/webview/lib/const"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import {resolveCachedConfig, resolveIpcMainApi, sanitizeProtonApiError} from "src/electron-preload/lib/util"; import * as RestModel from "src/electron-preload/webview/lib/rest-model"; diff --git a/src/electron-preload/webview/primary/api/db-patch/index.ts b/src/electron-preload/webview/primary/mail/api/db-patch/index.ts similarity index 92% rename from src/electron-preload/webview/primary/api/db-patch/index.ts rename to src/electron-preload/webview/primary/mail/api/db-patch/index.ts index 7c7b7687d..baaa6299a 100644 --- a/src/electron-preload/webview/primary/api/db-patch/index.ts +++ b/src/electron-preload/webview/primary/mail/api/db-patch/index.ts @@ -5,19 +5,21 @@ import {bootstrapDbPatch} from "./bootstrap"; import {buildDbPatch} from "./patch"; import {BuildDbPatchMethodReturnType, DbPatchBundle} from "./model"; import {buildDbPatchRetryPipeline, fetchEvents, persistDatabasePatch} from "src/electron-preload/webview/lib/util"; +import {ProviderApi as CommonProviderApi} from "src/electron-preload/webview/primary/common/provider-api"; import {curryFunctionMembers, isDatabaseBootstrapped} from "src/shared/util"; import {EVENT_ACTION} from "src/electron-preload/webview/lib/rest-model"; import {ONE_SECOND_MS} from "src/shared/const"; import {preprocessError} from "src/electron-preload/webview/primary/util"; -import {ProtonPrimaryApi} from "src/shared/api/webview/primary"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {ProtonPrimaryMailApi} from "src/shared/api/webview/primary-mail"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); const buildDbPatchEndpoint = ( + commonProviderApi: CommonProviderApi, providerApi: ProviderApi, -): Pick => { +): Pick => { const endpoints: ReturnType = { async throwErrorOnRateLimitedMethodCall(input) { curryFunctionMembers(_logger, nameof(endpoints.throwErrorOnRateLimitedMethodCall), input.accountIndex).info(); @@ -39,7 +41,7 @@ const buildDbPatchEndpoint = ( // the account state keeps the "signed-in" state despite of page still being reloaded // so we need to reset "signed-in" state with "account.entryUrl" value change await lastValueFrom(race( - providerApi._custom_.loggedIn$.pipe( + commonProviderApi._custom_.loggedIn$.pipe( filter(Boolean), // should be logged in first(), ), diff --git a/src/electron-preload/webview/primary/api/db-patch/model.ts b/src/electron-preload/webview/primary/mail/api/db-patch/model.ts similarity index 50% rename from src/electron-preload/webview/primary/api/db-patch/model.ts rename to src/electron-preload/webview/primary/mail/api/db-patch/model.ts index 8a7e91545..0944123a6 100644 --- a/src/electron-preload/webview/primary/api/db-patch/model.ts +++ b/src/electron-preload/webview/primary/mail/api/db-patch/model.ts @@ -1,6 +1,6 @@ import {IpcMainApiEndpoints} from "src/shared/api/main-process"; -import {ProtonPrimaryApiScan} from "src/shared/api/webview/primary"; +import {ProtonPrimaryMailApiScan} from "src/shared/api/webview/primary-mail"; export type DbPatchBundle = NoExtraProps[0], "patch" | "metadata">>; -export type BuildDbPatchMethodReturnType = ProtonPrimaryApiScan["ApiImplReturns"]["buildDbPatch"]; +export type BuildDbPatchMethodReturnType = ProtonPrimaryMailApiScan["ApiImplReturns"]["buildDbPatch"]; diff --git a/src/electron-preload/webview/primary/api/db-patch/patch.ts b/src/electron-preload/webview/primary/mail/api/db-patch/patch.ts similarity index 99% rename from src/electron-preload/webview/primary/api/db-patch/patch.ts rename to src/electron-preload/webview/primary/mail/api/db-patch/patch.ts index c9c55d611..05aace7cd 100644 --- a/src/electron-preload/webview/primary/api/db-patch/patch.ts +++ b/src/electron-preload/webview/primary/mail/api/db-patch/patch.ts @@ -4,7 +4,7 @@ import {DbPatch} from "src/shared/api/common"; import {isIgnorable404Error} from "src/electron-preload/webview/primary/util"; import {LABEL_TYPE, SYSTEM_FOLDER_IDENTIFIERS} from "src/shared/model/database"; import {Logger} from "src/shared/model/common"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import * as RestModel from "src/electron-preload/webview/lib/rest-model"; import {sanitizeProtonApiError} from "src/electron-preload/lib/util"; diff --git a/src/electron-preload/webview/primary/api/index.ts b/src/electron-preload/webview/primary/mail/api/index.ts similarity index 79% rename from src/electron-preload/webview/primary/api/index.ts rename to src/electron-preload/webview/primary/mail/api/index.ts index a6fb53417..f9f134493 100644 --- a/src/electron-preload/webview/primary/api/index.ts +++ b/src/electron-preload/webview/primary/mail/api/index.ts @@ -1,19 +1,20 @@ -import {buffer, concatMap, debounceTime, distinctUntilChanged, filter, map, mergeMap, switchMap, tap, throttleTime} from "rxjs/operators"; +import {buffer, concatMap, debounceTime, distinctUntilChanged, filter, map, mergeMap, tap} from "rxjs/operators"; import {EMPTY, from, merge, Observable} from "rxjs"; +import {ipcRenderer} from "electron"; import {pick} from "remeda"; import {serializeError} from "serialize-error"; import {buildDbPatch, buildDbPatchEndpoint} from "./db-patch"; +import {ProviderApi as CommonProviderApi} from "src/electron-preload/webview/primary/common/provider-api"; import {curryFunctionMembers, isEntityUpdatesPatchNotEmpty} from "src/shared/util"; -import {documentCookiesForCustomScheme} from "src/electron-preload/webview/lib/util"; -import {dumpProtonSharedSession} from "src/electron-preload/webview/primary/shared-session"; -import {FETCH_NOTIFICATION$} from "src/electron-preload/webview/primary/provider-api/notifications"; +import {FETCH_NOTIFICATION$} from "../provider-api/notifications"; import {getLocationHref} from "src/shared/util/web"; +import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; import {IpcMainServiceScan} from "src/shared/api/main-process"; import {ONE_SECOND_MS} from "src/shared/const"; import {parseProtonRestModel} from "src/shared/util/entity"; -import {PROTON_PRIMARY_IPC_WEBVIEW_API, ProtonPrimaryApi, ProtonPrimaryNotificationOutput} from "src/shared/api/webview/primary"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API, ProtonPrimaryMailApi} from "src/shared/api/webview/primary-mail"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import {resolveIpcMainApi} from "src/electron-preload/lib/util"; import * as RestModel from "src/electron-preload/webview/lib/rest-model"; import {SYSTEM_FOLDER_IDENTIFIERS} from "src/shared/model/database"; @@ -21,22 +22,12 @@ import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); -const resolveCookieSessionStoragePatch = (): IpcMainServiceScan["ApiImplReturns"]["resolvedSavedSessionStoragePatch"] => { - // https://github.com/expo/tough-cookie-web-storage-store/blob/36a20183dad5f84f2c14ae87251737dbbeb2af88/WebStorageCookieStore.js#L12 - // TODO move "__cookieStore__" to external/reusable constant - const sessionStorageCookieStoreKey = {"tough-cookie-web-storage-store": {storageCookieKey: "__cookieStore__"}} as const; - const {"tough-cookie-web-storage-store": {storageCookieKey}} = sessionStorageCookieStoreKey; - const {__cookieStore__} = {[storageCookieKey]: window.sessionStorage.getItem(storageCookieKey)}; - return __cookieStore__ ? {__cookieStore__} : null; -}; - -export function registerApi(providerApi: ProviderApi): void { - const endpoints: ProtonPrimaryApi = { - ...buildDbPatchEndpoint(providerApi), - - async ping({accountIndex}) { - return {value: JSON.stringify({accountIndex})}; - }, +export function registerApi( + commonProviderApi: CommonProviderApi, + providerApi: ProviderApi, +): void { + const endpoints: ProtonPrimaryMailApi = { + ...buildDbPatchEndpoint(commonProviderApi, providerApi), async selectMailOnline(input) { _logger.info(nameof(endpoints.selectMailOnline), input.accountIndex); @@ -158,28 +149,12 @@ export function registerApi(providerApi: ProviderApi): void { await ipcMain("dbExportMailAttachmentsNotification")({uuid, accountPk: {login}, attachments: loadedAttachments}); }, - async resolveLiveProtonClientSession({accountIndex}) { - _logger.info(nameof(endpoints.resolveLiveProtonClientSession), accountIndex); - return dumpProtonSharedSession(); - }, - - async resolvedLiveSessionStoragePatch({accountIndex}) { - _logger.info(nameof(endpoints.resolvedLiveSessionStoragePatch), accountIndex); - return resolveCookieSessionStoragePatch(); - }, - - notification({login, entryApiUrl, apiEndpointOriginSS, accountIndex}) { + notification({entryApiUrl, accountIndex}) { const logger = curryFunctionMembers(_logger, nameof(endpoints.notification), accountIndex); logger.info(); - type LoggedInOutput = Required>; - type UnreadOutput = Required>; - type BatchEntityUpdatesCounterOutput = Required>; - - const observables: [Observable, Observable, Observable] = [ - providerApi._custom_.loggedIn$.pipe(map((loggedIn) => ({loggedIn}))), - + const observables = [ (() => { const isEventsApiUrl = providerApi._custom_.buildEventsApiUrlTester({entryApiUrl}); const isMessagesCountApiUrl = providerApi._custom_.buildMessagesCountApiUrlTester({entryApiUrl}); @@ -235,7 +210,7 @@ export function registerApi(providerApi: ProviderApi): void { return typeof value === "number" ? accumulator.concat([{unread: value}]) : accumulator; - }, [] as UnreadOutput[]); + }, [] as Array<{unread: number}>); })); }), distinctUntilChanged(({unread: prev}, {unread: curr}) => curr === prev), @@ -274,26 +249,14 @@ export function registerApi(providerApi: ProviderApi): void { }), ); })(), - ]; - const ipcMain = resolveIpcMainApi({logger}); - - return merge( - merge(...observables).pipe(tap((notification) => logger.verbose(JSON.stringify({notification})))), - documentCookiesForCustomScheme.setNotification$.pipe( - throttleTime(ONE_SECOND_MS / 4), - mergeMap(() => { - const sessionStorageItem = resolveCookieSessionStoragePatch(); - return sessionStorageItem - ? (from(ipcMain("saveSessionStoragePatch")({login, apiEndpointOrigin: apiEndpointOriginSS, sessionStorageItem})) - .pipe(switchMap(() => EMPTY))) - : EMPTY; - }), - ), - ); + ] as const; + + return merge(...observables) + .pipe(tap((notification) => logger.verbose(JSON.stringify({notification})))); }, }; - PROTON_PRIMARY_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); - + PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); + ipcRenderer.sendToHost(IPC_WEBVIEW_API_CHANNELS_MAP.mail.registered); _logger.verbose(`api registered, url: ${getLocationHref()}`); } diff --git a/src/electron-preload/webview/primary/api/login.ts b/src/electron-preload/webview/primary/mail/api/login.ts similarity index 79% rename from src/electron-preload/webview/primary/api/login.ts rename to src/electron-preload/webview/primary/mail/api/login.ts index 3f3cbabab..083084241 100644 --- a/src/electron-preload/webview/primary/api/login.ts +++ b/src/electron-preload/webview/primary/mail/api/login.ts @@ -1,16 +1,16 @@ import {ipcRenderer} from "electron"; -import * as Api from "src/shared/api/webview/primary-login"; import {curryFunctionMembers} from "src/shared/util"; import {fillInputValue, getLocationHref, resolveDomElements} from "src/shared/util/web"; import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; import {ONE_SECOND_MS} from "src/shared/const"; +import {PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API, ProtonPrimaryLoginApi} from "src/shared/api/webview/primary-login"; import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); export function registerApi(): void { - const endpoints: Api.ProtonPrimaryLoginApi = { + const endpoints: ProtonPrimaryLoginApi = { async fillLogin({accountIndex, login}) { const logger = curryFunctionMembers(_logger, nameof(endpoints.fillLogin), accountIndex); @@ -28,8 +28,7 @@ export function registerApi(): void { }, }; - Api.PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); - ipcRenderer.sendToHost(IPC_WEBVIEW_API_CHANNELS_MAP["primary-login"].registered); - + PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API.register(endpoints, {logger: _logger}); + ipcRenderer.sendToHost(IPC_WEBVIEW_API_CHANNELS_MAP.login.registered); _logger.verbose(`api registered, url: ${getLocationHref()}`); } diff --git a/src/electron-preload/webview/primary/provider-api/const.ts b/src/electron-preload/webview/primary/mail/provider-api/const.ts similarity index 100% rename from src/electron-preload/webview/primary/provider-api/const.ts rename to src/electron-preload/webview/primary/mail/provider-api/const.ts diff --git a/src/electron-preload/webview/primary/provider-api/index.ts b/src/electron-preload/webview/primary/mail/provider-api/index.ts similarity index 94% rename from src/electron-preload/webview/primary/provider-api/index.ts rename to src/electron-preload/webview/primary/mail/provider-api/index.ts index 08eb81dd3..079486e01 100644 --- a/src/electron-preload/webview/primary/provider-api/index.ts +++ b/src/electron-preload/webview/primary/mail/provider-api/index.ts @@ -1,11 +1,11 @@ import {chunk} from "remeda"; -import {combineLatest, lastValueFrom} from "rxjs"; import {distinctUntilChanged, first, map} from "rxjs/operators"; +import {lastValueFrom} from "rxjs"; import {attachRateLimiting} from "./rate-limiting"; import {curryFunctionMembers} from "src/shared/util"; import {FETCH_NOTIFICATION_SKIP_SYMBOL} from "./const"; -import {HttpApi, resolveStandardSetupPublicApi} from "src/electron-preload/webview/lib/provider-api/standart-setup-internals"; +import {HttpApi, resolveStandardSetupPublicApi} from "src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals"; import {Logger} from "src/shared/model/common"; import {MessageVerification, ProviderApi, VerificationPreferences} from "./model"; import {PROTON_MAX_QUERY_PORTION_LIMIT, WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; @@ -55,15 +55,6 @@ export const initProviderApi = async (): Promise => { const resolveHttpApi = async (): Promise => lastValueFrom(standardSetupPublicApi.httpApi$.pipe(first())); const providerApi: ProviderApi = { _custom_: { - loggedIn$: combineLatest([standardSetupPublicApi.authentication$, internalsPrivateScope$]).pipe( - map(([authentication, {privateScope}]) => { - const isPrivateScopeActive = Boolean(privateScope); - const isAuthenticationSessionActive = Boolean(authentication.hasSession?.call(authentication)); - logger.verbose(JSON.stringify({isPrivateScopeActive, isAuthenticationSessionActive})); - return isPrivateScopeActive && isAuthenticationSessionActive; - }), - distinctUntilChanged(), - ), async getMailSettingsModel() { const privateApi = await resolvePrivateApi(); const [mailSettings /*, loadingMailSettings */] = privateApi.mailSettings; diff --git a/src/electron-preload/webview/primary/provider-api/internals.ts b/src/electron-preload/webview/primary/mail/provider-api/internals.ts similarity index 97% rename from src/electron-preload/webview/primary/provider-api/internals.ts rename to src/electron-preload/webview/primary/mail/provider-api/internals.ts index 54aba3b79..7551c7474 100644 --- a/src/electron-preload/webview/primary/provider-api/internals.ts +++ b/src/electron-preload/webview/primary/mail/provider-api/internals.ts @@ -1,9 +1,9 @@ import {BehaviorSubject} from "rxjs"; import {curryFunctionMembers} from "src/shared/util"; -import {NEVER_FN} from "src/electron-preload/webview/lib/provider-api/const"; +import {NEVER_FN} from "src/electron-preload/webview/primary/lib/provider-api/const"; import {ProviderInternals, ProviderInternalsLazy} from "./model"; -import * as webpackJsonpPushUtil from "src/electron-preload/webview/lib/provider-api/webpack-jsonp-push-util"; +import * as webpackJsonpPushUtil from "src/electron-preload/webview/primary/lib/provider-api/webpack-jsonp-push-util"; import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); @@ -49,7 +49,7 @@ export const resolveProviderInternals = async (): Promise => || resultKey === "./src/app/helpers/message/messageDecrypt.ts" ) { // mark lazy-loaded modules as initialized immediately since these modules get - // loaded only after the user gets logged in but we need to resolve the promise on initial load + // loaded only after a user gets signed-in, but we need to resolve the promise on initial load webpackJsonpPushUtil.markInternalsRecordAsInitialized(result, resultKey, resolveIfFullyInitialized, logger); } }, diff --git a/src/electron-preload/webview/primary/provider-api/model.ts b/src/electron-preload/webview/primary/mail/provider-api/model.ts similarity index 98% rename from src/electron-preload/webview/primary/provider-api/model.ts rename to src/electron-preload/webview/primary/mail/provider-api/model.ts index a15ada539..3aebd0d4c 100644 --- a/src/electron-preload/webview/primary/provider-api/model.ts +++ b/src/electron-preload/webview/primary/mail/provider-api/model.ts @@ -1,6 +1,6 @@ -import {AddInitializedProp, DefineObservableValue, WrapToValueProp} from "src/electron-preload/webview/lib/provider-api/model"; +import {AddInitializedProp, DefineObservableValue, WrapToValueProp} from "src/electron-preload/webview/primary/lib/provider-api/model"; import * as DatabaseModel from "src/shared/model/database/index"; -import {HttpApi, HttpApiArg} from "src/electron-preload/webview/lib/provider-api/standart-setup-internals/model"; +import {HttpApi, HttpApiArg} from "src/electron-preload/webview/primary/lib/provider-api/standart-setup-internals/model"; import {PROVIDER_REPO_MAP, PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS} from "src/shared/const/proton-apps"; import * as RestModel from "src/electron-preload/webview/lib/rest-model"; @@ -156,7 +156,6 @@ export type ProviderApi = { _custom_: Readonly< { - loggedIn$: import("rxjs").Observable; getMailSettingsModel: () => Promise<{ViewMode: unknown}>; buildEventsApiUrlTester: (options: {entryApiUrl: string}) => (url: string) => boolean; buildMessagesCountApiUrlTester: (options: {entryApiUrl: string}) => (url: string) => boolean; diff --git a/src/electron-preload/webview/primary/provider-api/notifications.ts b/src/electron-preload/webview/primary/mail/provider-api/notifications.ts similarity index 100% rename from src/electron-preload/webview/primary/provider-api/notifications.ts rename to src/electron-preload/webview/primary/mail/provider-api/notifications.ts diff --git a/src/electron-preload/webview/primary/provider-api/rate-limiting.ts b/src/electron-preload/webview/primary/mail/provider-api/rate-limiting.ts similarity index 99% rename from src/electron-preload/webview/primary/provider-api/rate-limiting.ts rename to src/electron-preload/webview/primary/mail/provider-api/rate-limiting.ts index 669e4b476..0b7c01f48 100644 --- a/src/electron-preload/webview/primary/provider-api/rate-limiting.ts +++ b/src/electron-preload/webview/primary/mail/provider-api/rate-limiting.ts @@ -6,7 +6,7 @@ import {assertTypeOf, asyncDelay, consumeMemoryRateLimiter, curryFunctionMembers import {Logger} from "src/shared/model/common"; import {ONE_SECOND_MS} from "src/shared/const"; import {PROVIDER_APP_NAMES} from "src/shared/const/proton-apps"; -import {ProviderApi} from "src/electron-preload/webview/primary/provider-api/model"; +import {ProviderApi} from "src/electron-preload/webview/primary/mail/provider-api/model"; import {RATE_LIMITED_METHOD_CALL_MESSAGE} from "src/electron-preload/webview/lib/const"; import {resolveCachedConfig} from "src/electron-preload/lib/util"; diff --git a/src/electron-preload/webview/primary/provider-api/setup.ts b/src/electron-preload/webview/primary/mail/provider-api/setup.ts similarity index 78% rename from src/electron-preload/webview/primary/provider-api/setup.ts rename to src/electron-preload/webview/primary/mail/provider-api/setup.ts index 93714e723..df6626e3a 100644 --- a/src/electron-preload/webview/primary/provider-api/setup.ts +++ b/src/electron-preload/webview/primary/mail/provider-api/setup.ts @@ -3,33 +3,20 @@ import {fromEvent, NEVER, of, race, timer} from "rxjs"; import {applyZoomFactor} from "src/electron-preload/lib/util"; import {curryFunctionMembers} from "src/shared/util"; -import {disableBrowserNotificationFeature} from "src/electron-preload/webview/lib/util"; -import {IFRAME_NOTIFICATION$} from "src/electron-preload/webview/primary/provider-api/notifications"; +import {IFRAME_NOTIFICATION$} from "src/electron-preload/webview/primary/mail/provider-api/notifications"; import {ONE_SECOND_MS} from "src/shared/const"; import {registerDocumentClickEventListener, registerDocumentKeyDownEventListener} from "src/electron-preload/lib/events-handling"; -import {testProtonMailAppPage} from "src/shared/util/proton-webclient"; import {WEBVIEW_LOGGERS} from "src/electron-preload/webview/lib/const"; const _logger = curryFunctionMembers(WEBVIEW_LOGGERS.primary, __filename); -export const setupProviderIntegration = ( - {shouldDisableBrowserNotificationFeature, blankHtmlPage}: ReturnType, -): void => { +export const setupProviderIntegration = (): void => { const logger = curryFunctionMembers(_logger, nameof(setupProviderIntegration)); - if (blankHtmlPage) { - logger.info("skipped"); - return; - } - applyZoomFactor(logger); registerDocumentKeyDownEventListener(document, logger); registerDocumentClickEventListener(document, logger); - if (shouldDisableBrowserNotificationFeature) { - disableBrowserNotificationFeature(logger); - } - IFRAME_NOTIFICATION$.subscribe((iframeDocument) => { registerDocumentKeyDownEventListener(iframeDocument, logger); registerDocumentClickEventListener(iframeDocument, logger); diff --git a/src/shared/api/webview/calendar.ts b/src/shared/api/webview/calendar.ts deleted file mode 100644 index 85b87f749..000000000 --- a/src/shared/api/webview/calendar.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {ActionType, createWebViewApiService, ScanService} from "electron-rpc-api"; - -import {buildLoggerBundle} from "src/electron-preload/lib/util"; -import {IPC_WEBVIEW_API_CHANNELS_MAP} from "./const"; - -const channel = IPC_WEBVIEW_API_CHANNELS_MAP.calendar.communication; - -export const PROTON_CALENDAR_IPC_WEBVIEW_API = createWebViewApiService({ - // TODO drop "{ accountIndex: number}" use - apiDefinition: { - ping: ActionType.Promise, {value: string}>(), - notification: ActionType.Observable<{accountIndex: number}, ProtonCalendarNotificationOutput>(), - } as const, - channel, - logger: buildLoggerBundle(`${__filename} [${channel}]`), -}); - -export type ProtonCalendarApiScan = ScanService; - -export type ProtonCalendarApi = ProtonCalendarApiScan["ApiClient"]; - -export type ProtonCalendarNotificationOutput = unknown; diff --git a/src/shared/api/webview/const.ts b/src/shared/api/webview/const.ts index b42448a6d..a6a65e09d 100644 --- a/src/shared/api/webview/const.ts +++ b/src/shared/api/webview/const.ts @@ -2,7 +2,7 @@ import {mapToObj} from "remeda"; import {PACKAGE_NAME} from "src/shared/const"; -const IPC_WEBVIEW_API_CHANNELS = ["primary", "primary-login", "calendar"] as const; +const IPC_WEBVIEW_API_CHANNELS = ["login", "common", "mail"] as const; export const IPC_WEBVIEW_API_CHANNELS_MAP = mapToObj( IPC_WEBVIEW_API_CHANNELS, diff --git a/src/shared/api/webview/primary-common.ts b/src/shared/api/webview/primary-common.ts new file mode 100644 index 000000000..3eb6de45b --- /dev/null +++ b/src/shared/api/webview/primary-common.ts @@ -0,0 +1,36 @@ +import {ActionType, createWebViewApiService, ScanService} from "electron-rpc-api"; + +import {buildLoggerBundle} from "src/electron-preload/lib/util"; +import {IPC_WEBVIEW_API_CHANNELS_MAP} from "./const"; +import type {IpcMainServiceScan} from "src/shared/api/main-process"; +import type {LoginFieldContainer} from "src/shared/model/container"; +import type {Notifications} from "src/shared/model/account"; +import type {ProtonClientSession} from "src/shared/model/proton"; + +// TODO drop "{ accountIndex: number}" use +const apiDefinition = { + notification: ActionType.Observable< + DeepReadonly, + Partial> + >(), + resolveLiveProtonClientSession: ActionType.Promise< + DeepReadonly<{accountIndex: number}>, + ProtonClientSession | null + >(), + resolvedLiveSessionStoragePatch: ActionType.Promise< + DeepReadonly<{accountIndex: number}>, + IpcMainServiceScan["ApiImplReturns"]["resolvedSavedSessionStoragePatch"] | null + >(), +} as const; + +const channel = IPC_WEBVIEW_API_CHANNELS_MAP.common.communication; + +export const PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API = createWebViewApiService({ + apiDefinition, // WARN referenced from "export const" to prevent "electron" injection + channel, + logger: buildLoggerBundle(`${__filename} [${channel}]`), +}); + +export type ProtonPrimaryCommonApiScan = ScanService; + +export type ProtonPrimaryCommonApi = ProtonPrimaryCommonApiScan["ApiClient"]; diff --git a/src/shared/api/webview/primary-login.ts b/src/shared/api/webview/primary-login.ts index 20070070b..08d71e16d 100644 --- a/src/shared/api/webview/primary-login.ts +++ b/src/shared/api/webview/primary-login.ts @@ -3,15 +3,15 @@ import {ActionType, createWebViewApiService, ScanService} from "electron-rpc-api import {buildLoggerBundle} from "src/electron-preload/lib/util"; import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; -const channel = IPC_WEBVIEW_API_CHANNELS_MAP["primary-login"].communication; +const channel = IPC_WEBVIEW_API_CHANNELS_MAP.login.communication; +// TODO drop "{ accountIndex: number}" use export const PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API = createWebViewApiService({ - // TODO drop "{ accountIndex: number}" use apiDefinition: {fillLogin: ActionType.Promise>()} as const, channel, logger: buildLoggerBundle(`${__filename} [${channel}]`), }); -export type ProtonPrimaryApiScan = ScanService; +export type ProtonPrimaryMailApiScan = ScanService; -export type ProtonPrimaryLoginApi = ProtonPrimaryApiScan["ApiClient"]; +export type ProtonPrimaryLoginApi = ProtonPrimaryMailApiScan["ApiClient"]; diff --git a/src/shared/api/webview/primary-mail.ts b/src/shared/api/webview/primary-mail.ts new file mode 100644 index 000000000..3ef50c874 --- /dev/null +++ b/src/shared/api/webview/primary-mail.ts @@ -0,0 +1,56 @@ +import {ActionType, createWebViewApiService, ScanService} from "electron-rpc-api"; + +import {buildLoggerBundle} from "src/electron-preload/lib/util"; +import type {DbAccountPk, Folder, FsDbAccount, Mail} from "src/shared/model/database"; +import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; +import type {Notifications} from "src/shared/model/account"; + +// TODO drop "{ accountIndex: number}" use +const apiDefinition = { + notification: ActionType.Observable< + DeepReadonly<{entryApiUrl: string; accountIndex: number}>, + Partial + >(), + buildDbPatch: ActionType.Observable< + DeepReadonly | null} & {accountIndex: number}>, + {progress: string} + >(), + throwErrorOnRateLimitedMethodCall: ActionType.Promise< + DeepReadonly<{accountIndex: number}>, + void + >(), + selectMailOnline: ActionType.Promise< + DeepReadonly< + {mail: Pick; selectedFolderId: Folder["id"] | null} & { + accountIndex: number; + } + > + >(), + fetchSingleMail: ActionType.Promise< + DeepReadonly + >(), + deleteMessages: ActionType.Promise< + DeepReadonly<{messageIds: Array} & {accountIndex: number}> + >(), + makeMailRead: ActionType.Promise< + DeepReadonly<{messageIds: Array} & {accountIndex: number}> + >(), + setMailFolder: ActionType.Promise< + DeepReadonly<{folderId: Folder["id"]; messageIds: Array} & {accountIndex: number}> + >(), + exportMailAttachments: ActionType.Promise< + DeepReadonly + >(), +} as const; + +const channel = IPC_WEBVIEW_API_CHANNELS_MAP.mail.communication; + +export const PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API = createWebViewApiService({ + apiDefinition, // WARN referenced from "export const" to prevent "electron" injection + channel, + logger: buildLoggerBundle(`${__filename} [${channel}]`), +}); + +export type ProtonPrimaryMailApiScan = ScanService; + +export type ProtonPrimaryMailApi = ProtonPrimaryMailApiScan["ApiClient"]; diff --git a/src/shared/api/webview/primary.ts b/src/shared/api/webview/primary.ts deleted file mode 100644 index 0bec966d9..000000000 --- a/src/shared/api/webview/primary.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {ActionType, createWebViewApiService, ScanService} from "electron-rpc-api"; - -import {buildLoggerBundle} from "src/electron-preload/lib/util"; -import type {DbAccountPk, Folder, FsDbAccount, Mail} from "src/shared/model/database"; -import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; -import type {IpcMainServiceScan} from "src/shared/api/main-process"; -import type {LoginFieldContainer} from "src/shared/model/container"; -import type {Notifications} from "src/shared/model/account"; -import type {ProtonClientSession} from "src/shared/model/proton"; - -const {Promise, Observable} = ActionType; - -// TODO drop "{ accountIndex: number}" use -const PROTON_PRIMARY_IPC_WEBVIEW_API_DEFINITION = { - ping: Promise, {value: string}>(), - buildDbPatch: Observable< - DeepReadonly | null} & {accountIndex: number}>, - {progress: string} - >(), - throwErrorOnRateLimitedMethodCall: Promise, void>(), - selectMailOnline: Promise< - DeepReadonly< - {mail: Pick; selectedFolderId: Folder["id"] | null} & { - accountIndex: number; - } - > - >(), - fetchSingleMail: Promise>(), - deleteMessages: Promise} & {accountIndex: number}>>(), - makeMailRead: Promise} & {accountIndex: number}>>(), - setMailFolder: Promise} & {accountIndex: number}>>(), - exportMailAttachments: Promise>(), - notification: Observable< - DeepReadonly, - ProtonPrimaryNotificationOutput - >(), - resolveLiveProtonClientSession: ActionType.Promise, ProtonClientSession | null>(), - resolvedLiveSessionStoragePatch: ActionType.Promise< - DeepReadonly<{accountIndex: number}>, - IpcMainServiceScan["ApiImplReturns"]["resolvedSavedSessionStoragePatch"] | null - >(), -} as const; - -const channel = IPC_WEBVIEW_API_CHANNELS_MAP.primary.communication; - -export const PROTON_PRIMARY_IPC_WEBVIEW_API = createWebViewApiService({ - apiDefinition: PROTON_PRIMARY_IPC_WEBVIEW_API_DEFINITION, // WARN referenced from "export const" to prevent "electron" injection - channel, - logger: buildLoggerBundle(`${__filename} [${channel}]`), -}); - -export type ProtonPrimaryApiScan = ScanService; - -export type ProtonPrimaryApi = ProtonPrimaryApiScan["ApiClient"]; - -export type ProtonPrimaryNotificationOutput = - & Partial - & Partial<{batchEntityUpdatesCounter: number}>; diff --git a/src/shared/const/proton-apps.ts b/src/shared/const/proton-apps.ts index 4065ff9bd..6e4b74531 100644 --- a/src/shared/const/proton-apps.ts +++ b/src/shared/const/proton-apps.ts @@ -1,3 +1,5 @@ +import {PRIMARY_INTERNALS_KEYS} from "src/electron-preload/webview/primary/common/provider-api/const"; + export const PROVIDER_APP_NAMES = ["proton-mail", "proton-account", "proton-calendar", "proton-drive", "proton-vpn-settings"] as const; export const PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS = [ @@ -26,7 +28,7 @@ export const PROVIDER_REPO_MAP = { "../../packages/shared/lib/api/labels.ts", "../../packages/shared/lib/api/messages.ts", "../../packages/shared/lib/mail/mailSettings.ts", - "./src/app/containers/PageContainer.tsx", + PRIMARY_INTERNALS_KEYS["proton-mail"].key, "./src/app/helpers/mailboxUrl.ts", "./src/app/helpers/message/messageDecrypt.ts", // lazy/dynamic @@ -55,7 +57,7 @@ export const PROVIDER_REPO_MAP = { protonPack: { webpackIndexEntryItems: [ // immediate - "./src/app/./containers/calendar/MainContainer", + PRIMARY_INTERNALS_KEYS["proton-calendar"].key, ...PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS, ], }, @@ -65,7 +67,13 @@ export const PROVIDER_REPO_MAP = { apiSubdomain: "drive-api", repoRelativeDistDir: "./dist", tag: "proton-drive@5.2.0+5c841022", - protonPack: {}, + protonPack: { + webpackIndexEntryItems: [ + // immediate + PRIMARY_INTERNALS_KEYS["proton-drive"].key, + ...PROVIDER_REPO_STANDARD_SETUP_WEBPACK_INDEX_ENTRY_ITEMS, + ], + }, }, [PROVIDER_APP_NAMES[4]]: { basePath: "account/vpn", diff --git a/src/shared/model/account.ts b/src/shared/model/account.ts index 3b39a58d0..459e40a3f 100644 --- a/src/shared/model/account.ts +++ b/src/shared/model/account.ts @@ -27,6 +27,7 @@ export type AccountConfig = NoExtraProps<{ customNotificationCode?: string; notificationShellExec?: boolean; notificationShellExecCode?: string; + entryProtonApp: "proton-mail" | "proton-calendar" | "proton-drive"; }>; export type AccountPersistentSession = NoExtraProps<{ diff --git a/src/shared/model/container.ts b/src/shared/model/container.ts index 8d3792b71..7c2bd776a 100644 --- a/src/shared/model/container.ts +++ b/src/shared/model/container.ts @@ -42,5 +42,6 @@ export type AccountConfigCreateUpdatePatch = NoExtraProps< | "customUserAgent" | "proxy" | "title" + | "entryProtonApp" > >; diff --git a/src/shared/model/electron.ts b/src/shared/model/electron.ts index 3ffe2e4e9..03859e3f4 100644 --- a/src/shared/model/electron.ts +++ b/src/shared/model/electron.ts @@ -1,10 +1,12 @@ export type ElectronExposure = Readonly< { buildIpcMainClient: (typeof import("src/shared/api/main-process"))["IPC_MAIN_API"]["client"]; - buildIpcPrimaryWebViewClient: (typeof import("src/shared/api/webview/primary"))["PROTON_PRIMARY_IPC_WEBVIEW_API"]["client"]; + buildIpcPrimaryCommonWebViewClient: + (typeof import("src/shared/api/webview/primary-common"))["PROTON_PRIMARY_COMMON_IPC_WEBVIEW_API"]["client"]; + buildIpcPrimaryMailWebViewClient: + (typeof import("src/shared/api/webview/primary-mail"))["PROTON_PRIMARY_MAIL_IPC_WEBVIEW_API"]["client"]; buildIpcPrimaryLoginWebViewClient: (typeof import("src/shared/api/webview/primary-login"))["PROTON_PRIMARY_LOGIN_IPC_WEBVIEW_API"]["client"]; - buildIpcCalendarWebViewClient: (typeof import("src/shared/api/webview/calendar"))["PROTON_CALENDAR_IPC_WEBVIEW_API"]["client"]; registerDocumentClickEventListener: (typeof import("src/electron-preload/lib/events-handling"))["registerDocumentClickEventListener"]; Logger: Readonly; @@ -34,7 +36,6 @@ export type ElectronContextLocations = Readonly< searchInPageBrowserView: string; fullTextSearchBrowserWindow: string; primary: string; - calendar: string; } >; } diff --git a/src/shared/util/proton-webclient.ts b/src/shared/util/proton-webclient.ts index 2daf6d58f..7fbcc3773 100644 --- a/src/shared/util/proton-webclient.ts +++ b/src/shared/util/proton-webclient.ts @@ -10,47 +10,46 @@ import {resolveProtonAppTypeFromUrlHref} from "src/shared/util/proton-url"; export const getWebViewPartitionName = ({login, entryUrl}: DeepReadonly>): string => `partition/webview/${login}/${entryUrl}`; -const testProtonAppPage = ( - targetProjectType: keyof typeof PROVIDER_REPO_MAP, +type testProtonAppPageResult = P extends true ? { + targetedProtonProject: P; + blankHtmlPage: false; + packagedWebClientUrl: ReturnType; + projectType: T; + } + : { + targetedProtonProject: false; + blankHtmlPage: boolean; + packagedWebClientUrl: ReturnType; + projectType?: keyof typeof PROVIDER_REPO_MAP; + }; + +export const testProtonAppPage = ( + targetProjectType: T, {url, logger}: {url: string; logger: import("src/shared/model/common").Logger}, -): { - shouldInitProviderApi: boolean; - blankHtmlPage: boolean; - packagedWebClientUrl: ReturnType; - projectType?: keyof typeof PROVIDER_REPO_MAP; -} => { +): testProtonAppPageResult => { let projectType: keyof typeof PROVIDER_REPO_MAP | undefined; const packagedWebClientUrl = parsePackagedWebClientUrl(url); const blankHtmlPage = packagedWebClientUrl?.pathname === `/${WEB_CLIENTS_BLANK_HTML_FILE_NAME}`; - const protonMailProject = Boolean( + const targetedProtonProject = Boolean( !blankHtmlPage + && packagedWebClientUrl?.pathname !== PROTON_APP_MAIL_LOGIN_PATHNAME && packagedWebClientUrl && (projectType = resolvePackagedWebClientApp(packagedWebClientUrl).project) === targetProjectType, ); const result = { - shouldInitProviderApi: protonMailProject && packagedWebClientUrl?.pathname !== PROTON_APP_MAIL_LOGIN_PATHNAME, + targetedProtonProject, blankHtmlPage, packagedWebClientUrl, projectType, - } as const; + }; logger.verbose(nameof(testProtonAppPage), JSON.stringify({...result, url, projectType})); - return result; -}; - -export const testProtonMailAppPage = ( - params: {url: string; logger: import("src/shared/model/common").Logger}, -): ReturnType & {shouldDisableBrowserNotificationFeature: boolean} => { - const baseResult = testProtonAppPage("proton-mail", params); - - return {...baseResult, shouldDisableBrowserNotificationFeature: baseResult.shouldInitProviderApi}; -}; + if (targetedProtonProject && !blankHtmlPage && projectType === targetProjectType) { + return result as testProtonAppPageResult; + } -export const testProtonCalendarAppPage = ( - params: {url: string; logger: import("src/shared/model/common").Logger}, -): ReturnType => { - return testProtonAppPage("proton-calendar", params); + return result as testProtonAppPageResult; }; export const parsePackagedWebClientUrl = (url: string): null | Readonly> => { diff --git a/src/web/browser-window/app/_accounts/account-view-abstract-component.directive.ts b/src/web/browser-window/app/_accounts/account-view-abstract.directive.ts similarity index 87% rename from src/web/browser-window/app/_accounts/account-view-abstract-component.directive.ts rename to src/web/browser-window/app/_accounts/account-view-abstract.directive.ts index 2e4ca592f..5860d9fd7 100644 --- a/src/web/browser-window/app/_accounts/account-view-abstract-component.directive.ts +++ b/src/web/browser-window/app/_accounts/account-view-abstract.directive.ts @@ -1,35 +1,27 @@ import type {Action} from "@ngrx/store"; -import {combineLatest, EMPTY, Observable, of, race, Subscription} from "rxjs"; +import {combineLatest, Observable, of, race} from "rxjs"; import {Directive, ElementRef, EventEmitter, Injector, Input, Output, Renderer2} from "@angular/core"; -import {distinctUntilChanged, filter, map, mergeMap, switchMap, take} from "rxjs/operators"; +import {distinctUntilChanged, filter, map, mergeMap, take} from "rxjs/operators"; import {DOCUMENT} from "@angular/common"; import type {OnDestroy} from "@angular/core"; import {pick} from "remeda"; -import {select, Store} from "@ngrx/store"; +import {AccountLoginAwareDirective} from "./account.ng-changes-observable.directive"; import {ACCOUNTS_ACTIONS} from "src/web/browser-window/app/store/actions"; -import {AccountsSelectors} from "src/web/browser-window/app/store/selectors"; -import {AccountViewComponent} from "./account-view.component"; +import {AccountViewComponent} from "./account.component"; import {CoreService} from "src/web/browser-window/app/_core/core.service"; import {depersonalizeLoggedUrlsInString} from "src/shared/util/proton-url"; import {ElectronService} from "src/web/browser-window/app/_core/electron.service"; import {getWebViewPartitionName} from "src/shared/util/proton-webclient"; import {LogLevel} from "src/shared/model/common"; import {lowerConsoleMessageEventLogLevel} from "src/shared/util"; -import {NgChangesObservableComponent} from "src/web/browser-window/app/components/ng-changes-observable.component"; -import {WebAccount} from "src/web/browser-window/app/model"; type ChildEvent = Parameters[0]; @Directive() // so weird not single-purpose directive huh, https://github.com/angular/angular/issues/30080#issuecomment-539194668 // eslint-disable-next-line @angular-eslint/directive-class-suffix -export abstract class AccountViewAbstractComponent extends NgChangesObservableComponent implements OnDestroy { - @Input({required: true}) - readonly login: string = ""; - - readonly account$: Observable; - +export abstract class AccountViewAbstractDirective extends AccountLoginAwareDirective implements OnDestroy { @Input({required: true}) webViewSrc!: string; @@ -42,27 +34,16 @@ export abstract class AccountViewAbstractComponent extends NgChangesObservableCo protected readonly core: CoreService; - private readonly subscription = new Subscription(); - protected constructor( - private readonly viewType: Extract, + private readonly viewType: Extract, protected readonly injector: Injector, ) { - super(); + super(injector); + this.log("info", [nameof(AccountViewAbstractDirective), "constructor"]); - this.account$ = this.ngChangesObservable("login").pipe( - switchMap((login) => - this.injector.get(Store).pipe( - select(AccountsSelectors.ACCOUNTS.pickAccount({login})), - mergeMap((account) => account ? [account] : EMPTY), - ) - ), - ); this.api = this.injector.get(ElectronService); this.core = this.injector.get(CoreService); - this.log("info", [nameof(AccountViewAbstractComponent), "constructor"]); - this.webViewConstruction(); { @@ -101,8 +82,7 @@ export abstract class AccountViewAbstractComponent extends NgChangesObservableCo ngOnDestroy(): void { super.ngOnDestroy(); // eslint-disable-next-line @typescript-eslint/unbound-method - this.log("info", [nameof(AccountViewAbstractComponent.prototype.ngOnDestroy)]); - this.subscription.unsubscribe(); + this.log("info", [nameof(AccountViewAbstractDirective.prototype.ngOnDestroy)]); this.action( ACCOUNTS_ACTIONS.Patch({ login: this.login, @@ -138,12 +118,6 @@ export abstract class AccountViewAbstractComponent extends NgChangesObservableCo ); } - protected addSubscription( - ...[teardown]: Parameters - ): ReturnType { - return this.subscription.add(teardown); - } - private webViewConstruction(): void { this.addSubscription( combineLatest([ @@ -186,7 +160,7 @@ export abstract class AccountViewAbstractComponent extends NgChangesObservableCo private registerWebViewEventsHandlingOnce(webView: Electron.WebviewTag): void { // eslint-disable-next-line @typescript-eslint/unbound-method - this.log("info", [nameof(AccountViewAbstractComponent.prototype.registerWebViewEventsHandlingOnce)]); + this.log("info", [nameof(AccountViewAbstractDirective.prototype.registerWebViewEventsHandlingOnce)]); const didStartNavigationArgs = [ "did-start-navigation", diff --git a/src/web/browser-window/app/_accounts/account-view-calendar.component.ts b/src/web/browser-window/app/_accounts/account-view-calendar.component.ts deleted file mode 100644 index 86a9bd221..000000000 --- a/src/web/browser-window/app/_accounts/account-view-calendar.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {Component, Injector} from "@angular/core"; -import {filter, withLatestFrom} from "rxjs/operators"; -import {firstValueFrom} from "rxjs"; -import type {OnInit} from "@angular/core"; - -import {ACCOUNTS_ACTIONS} from "src/web/browser-window/app/store/actions"; -import {AccountViewAbstractComponent} from "./account-view-abstract-component.directive"; -import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; - -@Component({ - standalone: false, - selector: "electron-mail-account-view-calendar", - template: "", -}) -export class AccountViewCalendarComponent extends AccountViewAbstractComponent implements OnInit { - // private readonly logger = getWebLogger(__filename, nameof(AccountViewCalendarComponent)); - - constructor( - injector: Injector, - ) { - super("calendar", injector); - } - - ngOnInit(): void { - this.addSubscription( - this.filterEvent("ipc-message") - .pipe( - filter(({channel}) => channel === IPC_WEBVIEW_API_CHANNELS_MAP.calendar.registered), - withLatestFrom(this.account$), - ) - .subscribe(([{webView}, account]) => { - this.action( - ACCOUNTS_ACTIONS.SetupCalendarNotificationChannel({ - account, - webView, - finishPromise: firstValueFrom(this.buildNavigationOrDestroyingSingleNotification()), - }), - ); - }), - ); - } -} diff --git a/src/web/browser-window/app/_accounts/account-view-primary.component.ts b/src/web/browser-window/app/_accounts/account-view-primary.component.ts index b1d6a416c..ed464ac80 100644 --- a/src/web/browser-window/app/_accounts/account-view-primary.component.ts +++ b/src/web/browser-window/app/_accounts/account-view-primary.component.ts @@ -1,102 +1,201 @@ +import {combineLatest, firstValueFrom, merge, of, race, Subject, timer} from "rxjs"; import {Component, Injector} from "@angular/core"; -import {distinctUntilChanged, map, takeUntil, withLatestFrom} from "rxjs/operators"; -import {firstValueFrom, race, Subject} from "rxjs"; +import {debounceTime, distinctUntilChanged, filter, first, map, switchMap, takeUntil, tap, withLatestFrom} from "rxjs/operators"; import {isDeepEqual} from "remeda"; import type {OnInit} from "@angular/core"; +import {select, Store} from "@ngrx/store"; import {ACCOUNTS_ACTIONS} from "src/web/browser-window/app/store/actions"; import {AccountsService} from "./accounts.service"; -import {AccountViewAbstractComponent} from "./account-view-abstract-component.directive"; +import {AccountViewAbstractDirective} from "./account-view-abstract.directive"; +import {curryFunctionMembers} from "src/shared/util"; import {ElectronService} from "src/web/browser-window/app/_core/electron.service"; +import {getWebLogger} from "src/web/browser-window/util"; +import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main-process/actions"; import {IPC_WEBVIEW_API_CHANNELS_MAP} from "src/shared/api/webview/const"; +import {ofType} from "src/shared/util/ngrx-of-type"; import {ONE_SECOND_MS} from "src/shared/const"; +import {OptionsSelectors} from "src/web/browser-window/app/store/selectors"; +import {ProtonClientSession} from "src/shared/model/proton"; +import {State} from "src/web/browser-window/app/store/reducers/accounts"; @Component({ standalone: false, selector: "electron-mail-account-view-primary", template: "", }) -export class AccountViewPrimaryComponent extends AccountViewAbstractComponent implements OnInit { - // private readonly logger = getWebLogger(__filename, nameof(AccountViewPrimaryComponent)); +export class AccountViewPrimaryComponent extends AccountViewAbstractDirective implements OnInit { + private readonly logger = getWebLogger(__filename, nameof(AccountViewPrimaryComponent)); - private readonly accountsService: AccountsService; + private readonly loggedIn$ = this.account$.pipe( + map(({notifications: {loggedIn}}) => loggedIn), + distinctUntilChanged(), + ); constructor( injector: Injector, + private readonly store: Store, ) { super("primary", injector); - this.accountsService = injector.get(AccountsService); } ngOnInit(): void { - const cancelPreviousSyncing$ = new Subject(); - this.addSubscription( - this.filterEvent("ipc-message") - .pipe(withLatestFrom(this.account$)) - .subscribe(async ([{webView, channel}, account]) => { - if (channel === IPC_WEBVIEW_API_CHANNELS_MAP["primary-login"].registered) { - // TODO move to "effects" - const {login} = account.accountConfig; - await this.injector.get(ElectronService).primaryLoginWebViewClient( - {webView}, - { - finishPromise: firstValueFrom(this.buildNavigationOrDestroyingSingleNotification()), - timeoutMs: ONE_SECOND_MS * 10, - }, - )("fillLogin")({accountIndex: account.accountIndex, login}); - this.action(ACCOUNTS_ACTIONS.Patch({login, patch: {loginFilledOnce: true}})); - } else if (channel === IPC_WEBVIEW_API_CHANNELS_MAP.primary.registered) { - this.action( - ACCOUNTS_ACTIONS.SetupPrimaryNotificationChannel( + this.filterEvent("dom-ready").pipe( + first(), + ).subscribe(({webView}) => { + this.onLoadedOnce(webView); + }), + ); + + { + const cancelPreviousSyncing$ = new Subject(); + + this.addSubscription( + this.filterEvent("ipc-message") + .pipe(withLatestFrom(this.account$)) + .subscribe(async ([{webView, channel}, account]) => { + if (channel === IPC_WEBVIEW_API_CHANNELS_MAP.login.registered) { + // TODO move "fillLogin" call to "effects" + const {login} = account.accountConfig; + await this.injector.get(ElectronService).primaryLoginWebViewClient( + {webView}, { - account, - webView, finishPromise: firstValueFrom(this.buildNavigationOrDestroyingSingleNotification()), + timeoutMs: ONE_SECOND_MS * 10, }, - ), - ); - - this.account$ - .pipe( - map(({notifications, accountConfig, accountIndex}) => ({ - pk: {login: accountConfig.login, accountIndex}, - data: {loggedIn: notifications.loggedIn, database: accountConfig.database}, - })), - // process switching of either "loggedIn" or "database" flags - distinctUntilChanged(({data: prev}, {data: curr}) => isDeepEqual(prev, curr)), - takeUntil(this.buildNavigationOrDestroyingSingleNotification()), - ) - .subscribe(({pk, data: {loggedIn, database}}) => { - cancelPreviousSyncing$.next(void 0); - if (!loggedIn || !database) { - return; // syncing disabled - } - this.action( - ACCOUNTS_ACTIONS.ToggleSyncing({ - pk, + )("fillLogin")({accountIndex: account.accountIndex, login}); + this.action(ACCOUNTS_ACTIONS.Patch({login, patch: {loginFilledOnce: true}})); + } else if (channel === IPC_WEBVIEW_API_CHANNELS_MAP.common.registered) { + this.action( + ACCOUNTS_ACTIONS.SetupCommonNotificationChannel( + { + account, webView, - finishPromise: firstValueFrom( - race(this.buildNavigationOrDestroyingSingleNotification(), cancelPreviousSyncing$), - ), - }), - ); - }); - } - }), - ); + finishPromise: firstValueFrom(this.buildNavigationOrDestroyingSingleNotification()), + }, + ), + ); + } else if (channel === IPC_WEBVIEW_API_CHANNELS_MAP.mail.registered) { + this.action( + ACCOUNTS_ACTIONS.SetupMailNotificationChannel( + { + account, + webView, + finishPromise: firstValueFrom(this.buildNavigationOrDestroyingSingleNotification()), + }, + ), + ); + + this.account$ + .pipe( + map(({notifications, accountConfig, accountIndex}) => ({ + pk: {login: accountConfig.login, accountIndex}, + data: {loggedIn: notifications.loggedIn, database: accountConfig.database}, + })), + // process switching of either "loggedIn" or "database" flags + distinctUntilChanged(({data: prev}, {data: curr}) => isDeepEqual(prev, curr)), + takeUntil(this.buildNavigationOrDestroyingSingleNotification()), + ) + .subscribe(({pk, data: {loggedIn, database}}) => { + cancelPreviousSyncing$.next(void 0); + if (!loggedIn || !database) { + return; // syncing disabled + } + this.action( + ACCOUNTS_ACTIONS.ToggleSyncing({ + pk, + webView, + finishPromise: firstValueFrom( + race(this.buildNavigationOrDestroyingSingleNotification(), cancelPreviousSyncing$), + ), + }), + ); + }); + } + }), + ); + } this.addSubscription( this.filterEvent("dom-ready") .pipe(withLatestFrom(this.account$)) .subscribe(([, account]) => { - // app set's app notification channel on webview.dom-ready event + // app set's app notificatioprivate readonly store: Store, channel on webview.dom-ready event // which means user is not logged-in yet at this moment, so resetting the state this.action( - this.accountsService - .generatePrimaryNotificationsStateResetAction({login: account.accountConfig.login}), + this.injector.get(AccountsService) + .generateMailNotificationsStateResetAction({login: account.accountConfig.login}), ); }), ); } + + onLoadedOnce(webView: Electron.WebviewTag): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + this.logger.info(nameof(AccountViewPrimaryComponent.prototype.onLoadedOnce)); + + const commonWebViewClient = this.injector.get(ElectronService).primaryCommonWebViewClient({webView: webView}); + const resolveLiveProtonClientSession = async (): Promise => { + const value = await commonWebViewClient("resolveLiveProtonClientSession")(await this.resolveAccountIndex()); + if (!value) { + throw new Error(`Failed to resolve "proton client session" object`); + } + return value; + }; + + { + const logger = curryFunctionMembers( + this.logger, + nameof(AccountViewPrimaryComponent.prototype.onLoadedOnce), // eslint-disable-line @typescript-eslint/unbound-method + "saving proton session", + ); + this.addSubscription( + this.store.pipe( + select(OptionsSelectors.CONFIG.persistentSessionSavingInterval), + switchMap((persistentSessionSavingInterval) => { + return combineLatest([ + this.loggedIn$.pipe( + tap((value) => logger.verbose("trigger: loggedIn$", value)), + ), + this.persistentSession$.pipe( + tap((value) => logger.verbose("trigger: persistentSession$", value)), + ), + merge( + of(null), // fired once to unblock the "combineLatest" + this.store.pipe( + select(OptionsSelectors.FEATURED.mainProcessNotificationAction), + ofType(IPC_MAIN_API_NOTIFICATION_ACTIONS.ProtonSessionTokenCookiesModified), + debounceTime(ONE_SECOND_MS), + withLatestFrom(this.account$), + filter(([{payload: {key}}, {accountConfig: {login}}]) => key.login === login), + tap(() => logger.verbose("trigger: proton session token cookies modified")), + ), + ), + ( + persistentSessionSavingInterval > 0 // negative value skips the interval-based trigger + ? ( + timer(0, persistentSessionSavingInterval).pipe( + tap((value) => logger.verbose("trigger: interval", value)), + ) + ) + : of(null) // fired once to unblock the "combineLatest" + ), + ]).pipe( + filter(([loggedIn, persistentSession]) => persistentSession && loggedIn), + withLatestFrom(this.account$), + ); + }), + ).subscribe(async ([, {accountConfig}]) => { + const ipcMainAction = "saveProtonSession"; + logger.verbose(ipcMainAction); + await this.ipcMainClient(ipcMainAction)({ + login: accountConfig.login, + clientSession: await resolveLiveProtonClientSession(), + apiEndpointOrigin: this.core.parseEntryUrl(accountConfig, "proton-mail").sessionStorage.apiEndpointOrigin, + }); + }), + ); + } + } } diff --git a/src/web/browser-window/app/_accounts/account-view.component.html b/src/web/browser-window/app/_accounts/account.component.html similarity index 54% rename from src/web/browser-window/app/_accounts/account-view.component.html rename to src/web/browser-window/app/_accounts/account.component.html index 5067831bc..8e9a1baba 100644 --- a/src/web/browser-window/app/_accounts/account-view.component.html +++ b/src/web/browser-window/app/_accounts/account.component.html @@ -4,11 +4,4 @@ [webViewSrc]="(webViewsState.primary.src$ | async) || ''" > - - diff --git a/src/web/browser-window/app/_accounts/account-view.component.scss b/src/web/browser-window/app/_accounts/account.component.scss similarity index 87% rename from src/web/browser-window/app/_accounts/account-view.component.scss rename to src/web/browser-window/app/_accounts/account.component.scss index 45c234d2d..b350cfdd5 100644 --- a/src/web/browser-window/app/_accounts/account-view.component.scss +++ b/src/web/browser-window/app/_accounts/account.component.scss @@ -30,8 +30,4 @@ display: none; } } - - &::ng-deep #{$app-prefix}-account-view-calendar { - display: none; - } } diff --git a/src/web/browser-window/app/_accounts/account-view.component.ts b/src/web/browser-window/app/_accounts/account.component.ts similarity index 67% rename from src/web/browser-window/app/_accounts/account-view.component.ts rename to src/web/browser-window/app/_accounts/account.component.ts index ab45164aa..8cfbd58e7 100644 --- a/src/web/browser-window/app/_accounts/account-view.component.ts +++ b/src/web/browser-window/app/_accounts/account.component.ts @@ -1,66 +1,39 @@ import type {Action} from "@ngrx/store"; -import {BehaviorSubject, combineLatest, EMPTY, lastValueFrom, merge, of, Subject, Subscription, timer} from "rxjs"; -import {Component, ComponentRef, ElementRef, HostBinding, Input, NgZone, ViewChild, ViewContainerRef} from "@angular/core"; +import {BehaviorSubject, combineLatest, Subject, timer} from "rxjs"; +import {Component, ComponentRef, ElementRef, HostBinding, Injector, Input, NgZone, ViewChild, ViewContainerRef} from "@angular/core"; import { - debounceTime, - distinctUntilChanged, - filter, - first, - map, - mergeMap, - pairwise, - startWith, - switchMap, - take, - takeUntil, - tap, - withLatestFrom, + distinctUntilChanged, filter, first, map, mergeMap, pairwise, startWith, switchMap, take, takeUntil, withLatestFrom, } from "rxjs/operators"; -import type {Observable} from "rxjs"; import type {OnDestroy, OnInit} from "@angular/core"; import {select, Store} from "@ngrx/store"; +import {AccountLoginAwareDirective} from "./account.ng-changes-observable.directive"; import {ACCOUNTS_ACTIONS, NAVIGATION_ACTIONS} from "src/web/browser-window/app/store/actions"; import {AccountsSelectors, OptionsSelectors} from "src/web/browser-window/app/store/selectors"; import {AccountsService} from "./accounts.service"; import {CoreService} from "src/web/browser-window/app/_core/core.service"; -import {curryFunctionMembers} from "src/shared/util"; import {DbViewEntryComponent} from "src/web/browser-window/app/_db-view/db-view-entry.component"; import {DbViewModuleResolve} from "./db-view-module-resolve.service"; import {DESKTOP_NOTIFICATION_ICON_URL} from "src/web/constants"; -import {ElectronService} from "src/web/browser-window/app/_core/electron.service"; import {getWebLogger, sha256} from "src/web/browser-window/util"; import {IPC_MAIN_API_NOTIFICATION_ACTIONS} from "src/shared/api/main-process/actions"; import {LogLevel} from "src/shared/model/common"; -import {NgChangesObservableComponent} from "src/web/browser-window/app/components/ng-changes-observable.component"; import {ofType} from "src/shared/util/ngrx-of-type"; import {ONE_SECOND_MS, PRODUCT_NAME} from "src/shared/const"; -import {ProtonClientSession} from "src/shared/model/proton"; import {State} from "src/web/browser-window/app/store/reducers/accounts"; -import type {WebAccount} from "src/web/browser-window/app/model"; const componentDestroyingNotificationSubject$ = new Subject(); @Component({ standalone: false, - selector: "electron-mail-account-view", - templateUrl: "./account-view.component.html", - styleUrls: ["./account-view.component.scss"], + selector: "electron-mail-account", + templateUrl: "./account.component.html", + styleUrls: ["./account.component.scss"], }) -export class AccountViewComponent extends NgChangesObservableComponent implements OnInit, OnDestroy { +export class AccountViewComponent extends AccountLoginAwareDirective implements OnInit, OnDestroy { static componentDestroyingNotification$ = componentDestroyingNotificationSubject$.asObservable(); - @Input({required: true}) - readonly login: string = ""; - - readonly account$: Observable = this.ngChangesObservable("login").pipe( - switchMap((login) => - this.store.pipe( - select(AccountsSelectors.ACCOUNTS.pickAccount({login})), - mergeMap((account) => account ? [account] : EMPTY), - ) - ), - ); + private readonly logger = getWebLogger(__filename, nameof(AccountViewComponent)); // TODO angular: get rid of @HostBinding("class") / @Input() workaround https://github.com/angular/angular/issues/7289 @Input({required: false}) @@ -68,27 +41,12 @@ export class AccountViewComponent extends NgChangesObservableComponent implement viewModeClass: "vm-live" | "vm-database" = "vm-live"; readonly webViewsState: Readonly< - Record<"primary" | "calendar", { - readonly src$: BehaviorSubject; - readonly domReady$: Subject; - }> + Record<"primary", {readonly src$: BehaviorSubject; readonly domReady$: Subject}> > = { primary: {src$: new BehaviorSubject(""), domReady$: new Subject()}, - calendar: {src$: new BehaviorSubject(""), domReady$: new Subject()}, }; @ViewChild("tplDbViewComponentContainerRef", {read: ViewContainerRef, static: true}) private readonly tplDbViewComponentContainerRef!: ViewContainerRef; - private readonly persistentSession$ = this.account$.pipe( - map(({accountConfig: {persistentSession}}) => Boolean(persistentSession)), - distinctUntilChanged(), - ); - private readonly loggedIn$ = this.account$.pipe( - map(({notifications: {loggedIn}}) => loggedIn), - distinctUntilChanged(), - ); - private readonly ipcMainClient; - private readonly logger: ReturnType; - private readonly subscription = new Subscription(); @HostBinding("class") get getClass(): string { @@ -96,41 +54,24 @@ export class AccountViewComponent extends NgChangesObservableComponent implement } constructor( - private readonly dbViewModuleResolve: DbViewModuleResolve, - private readonly accountsService: AccountsService, - private readonly electronService: ElectronService, private readonly core: CoreService, private readonly store: Store, private readonly zone: NgZone, - // private readonly changeDetectorRef: ChangeDetectorRef, + injector: Injector, ) { - super(); - this.ipcMainClient = this.electronService.ipcMainClient(); - this.logger = getWebLogger(__filename, nameof(AccountViewComponent)); + super(injector); this.logger.info(); } - private async resolveAccountIndex(): Promise> { - return {accountIndex: (await lastValueFrom(this.account$.pipe(take(1)))).accountIndex}; - } - ngOnInit(): void { - this.subscription.add( - this.webViewsState.primary.domReady$.pipe( - first(), - ).subscribe((webView) => { - this.onPrimaryViewLoadedOnce(webView); - }), - ); - - this.subscription.add( + this.addSubscription( this.account$ .pipe( // processing just first/initial value here // account reloading with a new session in the case of "entryUrl" change gets handled via the "unload" action call first(), switchMap(({accountConfig: {login}}) => { - return this.accountsService.setupLoginDelayTrigger( + return this.injector.get(AccountsService).setupLoginDelayTrigger( {login, takeUntil$: this.webViewsState.primary.domReady$.asObservable()}, this.logger, ); @@ -139,15 +80,13 @@ export class AccountViewComponent extends NgChangesObservableComponent implement ) // TODO move subscribe handler logic to "_accounts/*.service" .subscribe(async ([, {accountConfig}]) => { - const project = "proton-mail"; const {primary: webViewsState} = this.webViewsState; const key = { login: accountConfig.login, - apiEndpointOrigin: this.core.parseEntryUrl(accountConfig, project).sessionStorage.apiEndpointOrigin, + ...this.core.parseSessionStorageOrigin(accountConfig), } as const; const applyProtonClientSessionAndNavigateArgs = [ accountConfig, - project, webViewsState.domReady$, (src: string) => webViewsState.src$.next(src), this.logger, @@ -182,7 +121,7 @@ export class AccountViewComponent extends NgChangesObservableComponent implement }), ); - this.subscription.add( + this.addSubscription( (() => { let mountedDbViewEntryComponent: ComponentRef | undefined; return combineLatest([ @@ -216,7 +155,7 @@ export class AccountViewComponent extends NgChangesObservableComponent implement this.viewModeClass = viewModeClass; } if (databaseView) { - mountedDbViewEntryComponent ??= await this.dbViewModuleResolve.mountDbViewEntryComponent( + mountedDbViewEntryComponent ??= await this.injector.get(DbViewModuleResolve).mountDbViewEntryComponent( this.tplDbViewComponentContainerRef, {login, accountIndex}, ); @@ -239,7 +178,7 @@ export class AccountViewComponent extends NgChangesObservableComponent implement })(), ); - this.subscription.add( + this.addSubscription( this.account$ .pipe( withLatestFrom(this.store.pipe(select(OptionsSelectors.CONFIG.unreadNotifications))), @@ -309,7 +248,7 @@ export class AccountViewComponent extends NgChangesObservableComponent implement // removing "proton session" on "persistent session" toggle gets disabled notification // WARN don't put this logic in "onPrimaryViewLoadedOnce" since it should work in offline mode too - this.subscription.add( + this.addSubscription( this.persistentSession$ .pipe( mergeMap((persistentSession) => persistentSession ? [] : [persistentSession]), @@ -349,78 +288,9 @@ export class AccountViewComponent extends NgChangesObservableComponent implement } } - onPrimaryViewLoadedOnce(primaryWebView: Electron.WebviewTag): void { - // eslint-disable-next-line @typescript-eslint/unbound-method - this.logger.info(nameof(AccountViewComponent.prototype.onPrimaryViewLoadedOnce)); - - const primaryWebViewClient = this.electronService.primaryWebViewClient({webView: primaryWebView}); - const resolveLiveProtonClientSession = async (): Promise => { - const value = await primaryWebViewClient("resolveLiveProtonClientSession")(await this.resolveAccountIndex()); - if (!value) { - throw new Error(`Failed to resolve "proton client session" object`); - } - return value; - }; - - { - const logger = curryFunctionMembers( - this.logger, - nameof(AccountViewComponent.prototype.onPrimaryViewLoadedOnce), // eslint-disable-line @typescript-eslint/unbound-method - "saving proton session", - ); - this.subscription.add( - this.store.pipe( - select(OptionsSelectors.CONFIG.persistentSessionSavingInterval), - switchMap((persistentSessionSavingInterval) => { - return combineLatest([ - this.loggedIn$.pipe( - tap((value) => logger.verbose("trigger: loggedIn$", value)), - ), - this.persistentSession$.pipe( - tap((value) => logger.verbose("trigger: persistentSession$", value)), - ), - merge( - of(null), // fired once to unblock the "combineLatest" - this.store.pipe( - select(OptionsSelectors.FEATURED.mainProcessNotificationAction), - ofType(IPC_MAIN_API_NOTIFICATION_ACTIONS.ProtonSessionTokenCookiesModified), - debounceTime(ONE_SECOND_MS), - withLatestFrom(this.account$), - filter(([{payload: {key}}, {accountConfig: {login}}]) => key.login === login), - tap(() => logger.verbose("trigger: proton session token cookies modified")), - ), - ), - ( - persistentSessionSavingInterval > 0 // negative value skips the interval-based trigger - ? ( - timer(0, persistentSessionSavingInterval).pipe( - tap((value) => logger.verbose("trigger: interval", value)), - ) - ) - : of(null) // fired once to unblock the "combineLatest" - ), - ]).pipe( - filter(([loggedIn, persistentSession]) => persistentSession && loggedIn), - withLatestFrom(this.account$), - ); - }), - ).subscribe(async ([, {accountConfig}]) => { - const ipcMainAction = "saveProtonSession"; - logger.verbose(ipcMainAction); - await this.ipcMainClient(ipcMainAction)({ - login: accountConfig.login, - clientSession: await resolveLiveProtonClientSession(), - apiEndpointOrigin: this.core.parseEntryUrl(accountConfig, "proton-mail").sessionStorage.apiEndpointOrigin, - }); - }), - ); - } - } - ngOnDestroy(): void { super.ngOnDestroy(); this.logger.info(nameof(AccountViewComponent.prototype.ngOnDestroy)); // eslint-disable-line @typescript-eslint/unbound-method - this.subscription.unsubscribe(); componentDestroyingNotificationSubject$.next(); } diff --git a/src/web/browser-window/app/_accounts/account.ng-changes-observable.directive.ts b/src/web/browser-window/app/_accounts/account.ng-changes-observable.directive.ts new file mode 100644 index 000000000..0b0312d35 --- /dev/null +++ b/src/web/browser-window/app/_accounts/account.ng-changes-observable.directive.ts @@ -0,0 +1,45 @@ +import {Directive, Injector, Input} from "@angular/core"; +import {distinctUntilChanged, map, mergeMap, switchMap, take} from "rxjs/operators"; +import {EMPTY, lastValueFrom} from "rxjs"; +import type {Observable} from "rxjs"; +import {select, Store} from "@ngrx/store"; + +import {AccountsSelectors} from "src/web/browser-window/app/store/selectors"; +import {ElectronService} from "src/web/browser-window/app/_core/electron.service"; +import {NgChangesObservableDirective} from "src/web/browser-window/app/components/ng-changes-observable.directive"; +import type {WebAccount} from "src/web/browser-window/app/model"; + +@Directive() +// so weird not single-purpose directive huh, https://github.com/angular/angular/issues/30080#issuecomment-539194668 +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export abstract class AccountLoginAwareDirective extends NgChangesObservableDirective { + @Input({required: true}) + readonly login: string = ""; + + protected readonly account$: Observable = this.ngChangesObservable("login").pipe( + switchMap((login) => + this.injector.get(Store).pipe( + select(AccountsSelectors.ACCOUNTS.pickAccount({login})), + mergeMap((account) => account ? [account] : EMPTY), + ) + ), + ); + + protected readonly persistentSession$ = this.account$.pipe( + map(({accountConfig: {persistentSession}}) => Boolean(persistentSession)), + distinctUntilChanged(), + ); + + protected readonly ipcMainClient; + + constructor( + protected readonly injector: Injector, + ) { + super(); + this.ipcMainClient = this.injector.get(ElectronService).ipcMainClient(); + } + + protected async resolveAccountIndex(): Promise> { + return {accountIndex: (await lastValueFrom(this.account$.pipe(take(1)))).accountIndex}; + } +} diff --git a/src/web/browser-window/app/_accounts/accounts.component.html b/src/web/browser-window/app/_accounts/accounts.component.html index e35a9846b..291b3df90 100644 --- a/src/web/browser-window/app/_accounts/accounts.component.html +++ b/src/web/browser-window/app/_accounts/accounts.component.html @@ -77,11 +77,11 @@ -
- - {{ item.title }} -
- web client: -
-
- - {{ item.title }} -
- web client: -
-
-
- -
- #29, - #79, - #80, - #164 -
+ +
+
+ +
- - - - - -
+
+ {{ item.title }} +
+ +
+ #29, + #79, + #80, + #164 +
+
+ + -
- - + + +
+ +
+
+ +
+
| keyof Pick["proxy"]>, "proxyRules" | "proxyBypassRules"> | keyof AccountConfig["credentials"], @@ -122,6 +123,10 @@ export class AccountEditComponent implements OnInit, OnDestroy { return null; }, ), + entryProtonApp: new FormControl( + null, + Validators.required, // eslint-disable-line @typescript-eslint/unbound-method + ), }; form = new FormGroup(this.controls); account?: AccountConfig; @@ -132,6 +137,12 @@ export class AccountEditComponent implements OnInit, OnDestroy { customNotificationCodeEditable$: Observable; notificationShellExecCodeEditable$: Observable; + readonly entryProtonApps = [ + {value: "proton-mail", title: "Mail"}, + {value: "proton-calendar", title: "Calendar"}, + {value: "proton-drive", title: "Drive"}, + ] as const; + private readonly logger = getWebLogger(__filename, nameof(AccountEditComponent)); private readonly subscription = new Subscription(); @@ -239,6 +250,7 @@ export class AccountEditComponent implements OnInit, OnDestroy { "blockNonEntryUrlBasedRequests", "externalContentProxyUrlPattern", "enableExternalContentProxy", + "entryProtonApp", ] as const ) { controls[prop].patchValue(account[prop]); @@ -329,6 +341,7 @@ export class AccountEditComponent implements OnInit, OnDestroy { } return validated; })(), + entryProtonApp: controls.entryProtonApp.value, }; /* eslint-enable @typescript-eslint/no-unsafe-assignment */ diff --git a/src/web/browser-window/app/components/abstract-monaco-editor.component.ts b/src/web/browser-window/app/components/abstract-monaco-editor.directive.ts similarity index 96% rename from src/web/browser-window/app/components/abstract-monaco-editor.component.ts rename to src/web/browser-window/app/components/abstract-monaco-editor.directive.ts index b3c36d511..a7e028f71 100644 --- a/src/web/browser-window/app/components/abstract-monaco-editor.component.ts +++ b/src/web/browser-window/app/components/abstract-monaco-editor.directive.ts @@ -7,7 +7,7 @@ import {select, Store} from "@ngrx/store"; import {AccountConfig} from "src/shared/model/account"; import {LABEL_TYPE, View} from "src/shared/model/database"; -import {NgChangesObservableComponent} from "src/web/browser-window/app/components/ng-changes-observable.component"; +import {NgChangesObservableDirective} from "src/web/browser-window/app/components/ng-changes-observable.directive"; import {ONE_SECOND_MS} from "src/shared/const"; import {OptionsSelectors} from "src/web/browser-window/app/store/selectors"; import {State} from "src/web/browser-window/app/store/reducers/root"; @@ -15,7 +15,7 @@ import {State} from "src/web/browser-window/app/store/reducers/root"; @Directive() // so weird not single-purpose directive huh, https://github.com/angular/angular/issues/30080#issuecomment-539194668 // eslint-disable-next-line @angular-eslint/directive-class-suffix -export abstract class AbstractMonacoEditorComponent extends NgChangesObservableComponent implements OnInit, OnDestroy { +export abstract class AbstractMonacoEditorDirective extends NgChangesObservableDirective implements OnInit, OnDestroy { @Input({required: true}) login!: AccountConfig["login"]; @@ -135,7 +135,7 @@ export abstract class AbstractMonacoEditorComponent extends NgChangesObservableC } protected folderDtsCodeInclude( - folders: Unpacked, + folders: Unpacked, type: Unpacked, ): string { return ` @@ -156,7 +156,7 @@ export abstract class AbstractMonacoEditorComponent extends NgChangesObservableC } private initializeEditorFunctionMailDts( - folders: Unpacked, + folders: Unpacked, extraMailPropsDts = "", ): string { return ` @@ -174,7 +174,7 @@ export abstract class AbstractMonacoEditorComponent extends NgChangesObservableC // TODO turn to "abstract protected" method and apply it per editor instance // see the respective issue/blocker: https://github.com/microsoft/monaco-editor/issues/2098 - private initializeEditorFunctionDts(folders: Unpacked): string { + private initializeEditorFunctionDts(folders: Unpacked): string { return [ ` declare const filterMessage = ( @@ -204,7 +204,7 @@ export abstract class AbstractMonacoEditorComponent extends NgChangesObservableC ].join(""); } - private initializeEditor(folders: Unpacked): void { + private initializeEditor(folders: Unpacked): void { monacoLanguages.typescript.typescriptDefaults.setCompilerOptions({ allowNonTsExtensions: true, target: monacoLanguages.typescript.ScriptTarget.ESNext, diff --git a/src/web/browser-window/app/components/ng-changes-observable.component.ts b/src/web/browser-window/app/components/ng-changes-observable.directive.ts similarity index 77% rename from src/web/browser-window/app/components/ng-changes-observable.component.ts rename to src/web/browser-window/app/components/ng-changes-observable.directive.ts index bea16d62e..f568be6bc 100644 --- a/src/web/browser-window/app/components/ng-changes-observable.component.ts +++ b/src/web/browser-window/app/components/ng-changes-observable.directive.ts @@ -1,4 +1,4 @@ -import {BehaviorSubject, EMPTY, Observable, of, Subject} from "rxjs"; +import {BehaviorSubject, EMPTY, Observable, of, Subject, Subscription} from "rxjs"; import {Directive} from "@angular/core"; import {distinctUntilChanged, mergeMap, takeUntil} from "rxjs/operators"; import type {OnChanges, OnDestroy, SimpleChanges} from "@angular/core"; @@ -6,12 +6,14 @@ import type {OnChanges, OnDestroy, SimpleChanges} from "@angular/core"; @Directive() // so weird not single-purpose directive huh, https://github.com/angular/angular/issues/30080#issuecomment-539194668 // eslint-disable-next-line @angular-eslint/directive-class-suffix -export abstract class NgChangesObservableComponent implements OnChanges, OnDestroy { +export abstract class NgChangesObservableDirective implements OnChanges, OnDestroy { protected ngChanges = new BehaviorSubject>({}); // TODO angular v16: use "takeUntilDestroyed" protected ngOnDestroy$ = new Subject(); + private readonly subscription = new Subscription(); + ngOnChanges(changes: SimpleChanges): void { const props: Record // eslint-disable-line @typescript-eslint/no-explicit-any = {}; @@ -33,6 +35,13 @@ export abstract class NgChangesObservableComponent implements OnChanges, OnDestr this.ngChanges.complete(); this.ngOnDestroy$.next(void 0); this.ngOnDestroy$.complete(); + this.subscription.unsubscribe(); + } + + protected addSubscription( + ...[teardown]: Parameters + ): ReturnType { + return this.subscription.add(teardown); } protected ngChangesObservable(propertyName: K): Observable { diff --git a/src/web/browser-window/app/store/actions/accounts.ts b/src/web/browser-window/app/store/actions/accounts.ts index 058dd6c3b..2ef1abc97 100644 --- a/src/web/browser-window/app/store/actions/accounts.ts +++ b/src/web/browser-window/app/store/actions/accounts.ts @@ -1,7 +1,7 @@ import {AccountConfig} from "src/shared/model/account"; import {Folder, Mail} from "src/shared/model/database"; import {props, propsRecordToActionsRecord} from "src/shared/util/ngrx"; -import {ProtonPrimaryApiScan} from "src/shared/api/webview/primary"; +import {ProtonPrimaryMailApiScan} from "src/shared/api/webview/primary-mail"; import {State} from "src/web/browser-window/app/store/reducers/accounts"; import {WebAccount, WebAccountPk, WebAccountProgress} from "src/web/browser-window/app/model"; @@ -36,12 +36,15 @@ export const ACCOUNTS_ACTIONS = propsRecordToActionsRecord( ToggleDatabaseView: props<{login: string; forced?: Pick}>(), ToggleSyncing: props<{pk: WebAccountPk; webView: Electron.WebviewTag; finishPromise: Promise}>(), Synced: props<{pk: WebAccountPk}>(), - SetupPrimaryNotificationChannel: props<{account: WebAccount; webView: Electron.WebviewTag; finishPromise: Promise}>(), + SetupCommonNotificationChannel: props<{account: WebAccount; webView: Electron.WebviewTag; finishPromise: Promise}>(), + SetupMailNotificationChannel: props<{account: WebAccount; webView: Electron.WebviewTag; finishPromise: Promise}>(), SetupCalendarNotificationChannel: props<{account: WebAccount; webView: Electron.WebviewTag; finishPromise: Promise}>(), WireUpConfigs: props>(), PatchGlobalProgress: props<{patch: State["globalProgress"]}>(), - SelectMailOnline: props<{pk: WebAccountPk} & Omit>(), - DeleteMessages: props<{pk: WebAccountPk} & Omit>(), + SelectMailOnline: props< + {pk: WebAccountPk} & Omit + >(), + DeleteMessages: props<{pk: WebAccountPk} & Omit>(), FetchSingleMail: props<{pk: WebAccountPk} & {mailPk: Mail["pk"]}>(), MakeMailRead: props<{pk: WebAccountPk} & {messageIds: Array}>(), SetMailFolder: props<{pk: WebAccountPk} & {folderId: Folder["id"]; messageIds: Array}>(), diff --git a/src/web/browser-window/app/store/reducers/accounts.ts b/src/web/browser-window/app/store/reducers/accounts.ts index d773f0108..c999bb2a8 100644 --- a/src/web/browser-window/app/store/reducers/accounts.ts +++ b/src/web/browser-window/app/store/reducers/accounts.ts @@ -70,7 +70,6 @@ export function reducer(state = initialState, action: UnionOf