import type { Action } from "@ngrx/store" ; import { BehaviorSubject , Observable , Subject , Subscription , combineLatest , lastValueFrom , merge , of , race , throwError , timer } from "rxjs" ; import { Store , select } from "@ngrx/store" ; import { URL } from "@cliqz/url-parser" ; import { concatMap , debounce , debounceTime , delayWhen , distinctUntilChanged , filter , first , map , mergeMap , pairwise , startWith , switchMap , take , takeUntil , tap , withLatestFrom , } from "rxjs/operators" ; import { pick } from "remeda" ; import { ofType } from "src/shared/ngrx-util-of-type" ; 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 "src/web/browser-window/app/_accounts/accounts.service" ; import { ChangeDetectionStrategy , Component , ComponentRef , ElementRef , HostBinding , Input , NgZone , ViewChild , ViewContainerRef , } from "@angular/core" ; import { CoreService } from "src/web/browser-window/app/_core/core.service" ; import { DbViewEntryComponent } from "src/web/browser-window/app/_db-view/db-view-entry.component" ; import { DbViewModuleResolve } from "src/web/browser-window/app/_accounts/db-view-module-resolve.service" ; import { ElectronService } from "src/web/browser-window/app/_core/electron.service" ; 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 { ONE_SECOND_MS , PRODUCT_NAME } from "src/shared/constants" ; import type { OnDestroy , OnInit , } from "@angular/core" ; import { ProtonClientSession } from "src/shared/model/proton" ; import { State } from "src/web/browser-window/app/store/reducers/accounts" ; import { WebAccount } from "src/web/browser-window/app/model" ; import { curryFunctionMembers , parseUrlOriginWithNullishCheck } from "src/shared/util" ; import { getWebLogger } from "src/web/browser-window/util" ; @ Component ( { selector : "electron-mail-account" , templateUrl : "./account.component.html" , styleUrls : [ "./account.component.scss" ] , changeDetection : ChangeDetectionStrategy . OnPush , } ) export class AccountComponent extends NgChangesObservableComponent implements OnInit , OnDestroy { @ Input ( ) readonly account ! : WebAccount ; readonly account$ : Observable < WebAccount > = this . ngChangesObservable ( "account" ) ; @ Input ( ) readonly class : string = "" ; viewModeClass : "vm-live" | "vm-database" = "vm-live" ; @ HostBinding ( "class" ) get getClass ( ) : string { return `${ this . class } ${ this . viewModeClass }` ; } readonly webViewsState : Readonly < Record < "primary" | "calendar" , { readonly src$ : BehaviorSubject < string > ; readonly domReady$ : Subject < Electron . WebviewTag > ; } > > = { 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 onlinePing$ = timer ( 0 , ONE_SECOND_MS ) . pipe ( filter ( ( ) => navigator . onLine ) , take ( 1 ) ) ; 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 loggedInCalendar$ = this . account$ . pipe ( map ( ( { notifications : { loggedInCalendar } } ) => loggedInCalendar ) , distinctUntilChanged ( ) ) ; private readonly ipcMainClient ; private readonly logger : ReturnType < typeof getWebLogger > ; private readonly subscription = new Subscription ( ) ; constructor ( private readonly dbViewModuleResolve , private readonly accountsService , private readonly electronService , private readonly core , private readonly store , private readonly zone ) { super ( ) ; this . ipcMainClient = this . electronService . ipcMainClient ( ) ; this . logger = getWebLogger ( __filename , nameof ( AccountComponent ) ) ; this . logger . info ( ) ; } ngOnInit ( ) { this . subscription . add ( this . webViewsState . primary . domReady$ . pipe ( first ( ) ) . subscribe ( ( webView ) => { this . onPrimaryViewLoadedOnce ( webView ) ; } ) ) ; this . subscription . add ( this . account$ . pipe ( distinctUntilChanged ( ( { accountConfig : { entryUrl : prev } } , { accountConfig : { entryUrl : curr } } ) => curr === prev ) , switchMap ( ( { accountConfig : { login } } ) => this . accountsService . setupLoginDelayTrigger ( { login } , this . logger ) ) , delayWhen ( ( ) => this . onlinePing$ ) , withLatestFrom ( this . account$ ) ) . subscribe ( async ( [ , { accountConfig } ] ) => { const project = "proton-mail" ; const { primary : webViewsState } = this . webViewsState ; const key = { login : accountConfig . login , apiEndpointOrigin : parseUrlOriginWithNullishCheck ( this . core . parseEntryUrl ( accountConfig , project ) . entryApiUrl ) , } as const ; const applyProtonClientSessionAndNavigateArgs = [ accountConfig , project , webViewsState . domReady$ , ( src ) => webViewsState . src$ . next ( src ) , this . logger , ] as const ; const sessionStoragePatch = await this . ipcMainClient ( "resolvedSavedSessionStoragePatch" ) ( key ) ; const baseReturn = async ( ) : Promise < void > => { await this . ipcMainClient ( "resetProtonBackendSession" ) ( { login : key . login } ) ; await this . core . applyProtonClientSessionAndNavigate ( ... applyProtonClientSessionAndNavigateArgs , { sessionStoragePatch } ) ; } ; if ( ! accountConfig . persistentSession ) { return baseReturn ( ) ; } const clientSession = await this . ipcMainClient ( "resolveSavedProtonClientSession" ) ( key ) ; if ( ! clientSession ) { return baseReturn ( ) ; } if ( ! ( await this . ipcMainClient ( "applySavedProtonBackendSession" ) ( key ) ) ) { return baseReturn ( ) ; } await this . core . applyProtonClientSessionAndNavigate ( ... [ ... applyProtonClientSessionAndNavigateArgs , { clientSession , sessionStoragePatch } , ] as const ) ; } ) ) ; this . subscription . add ( ( ( ) => { let mountedDbViewEntryComponent ; let selectedLoginResolvingDebouncedOnce = false ; return combineLatest ( [ this . account$ . pipe ( map ( ( account ) => ( { login : account . accountConfig . login , accountIndex : account . accountIndex , databaseView : account . databaseView , } ) ) , distinctUntilChanged ( ( { databaseView : prev } , { databaseView : curr } ) => prev === curr ) ) , combineLatest ( [ this . store . pipe ( select ( AccountsSelectors . FEATURED . selectedLogin ) ) , this . store . pipe ( select ( OptionsSelectors . FEATURED . mainProcessNotificationAction ) , ofType ( IPC_MAIN_API_NOTIFICATION_ACTIONS . ActivateBrowserWindow ) , startWith ( null ) ) , ] ) . pipe ( map ( ( [ selectedLogin ] ) => selectedLogin ) , debounce ( ( ) => { return selectedLoginResolvingDebouncedOnce ? of ( null ) : this . webViewsState . primary . domReady$ . pipe ( tap ( ( ) => selectedLoginResolvingDebouncedOnce = true ) ) ; } ) ) , ] ) . pipe ( withLatestFrom ( this . webViewsState . primary . domReady$ ) ) . subscribe ( async ( [ [ { login , accountIndex , databaseView } , selectedLogin ] , primaryWebView ] ) => { const viewModeClass = databaseView ? "vm-database" : "vm-live" ; if ( this . viewModeClass !== viewModeClass ) { this . viewModeClass = viewModeClass ; } if ( databaseView ) { mountedDbViewEntryComponent ??= await this . dbViewModuleResolve . mountDbViewEntryComponent ( this . tplDbViewComponentContainerRef , { login , accountIndex } ) ; } if ( login !== selectedLogin ) { return ; } await this . ipcMainClient ( "selectAccount" ) ( { databaseView , webContentId : primaryWebView . getWebContentsId ( ) , } ) ; this . focusVisibleViewModeContainerElement ( databaseView ? mountedDbViewEntryComponent ?. injector . get ( ElementRef ) . nativeElement : primaryWebView ) ; } ) ; } ) ( ) ) ; this . subscription . add ( this . account$ . pipe ( withLatestFrom ( this . store . pipe ( select ( OptionsSelectors . CONFIG . unreadNotifications ) ) ) , filter ( ( [ , unreadNotifications ] ) => Boolean ( unreadNotifications ) ) , map ( ( [ account ] ) => account ) , map ( ( { accountConfig : { login , title } , notifications : { unread } } ) => ( { login , title , unread } ) ) , pairwise ( ) , filter ( ( [ prev , curr ] ) => curr . unread > prev . unread ) , map ( ( [ , curr ] ) => curr ) , withLatestFrom ( this . store . pipe ( select ( OptionsSelectors . FEATURED . trayIconDataURL ) ) ) ) . subscribe ( ( [ { login , unread , title } , trayIconDataURL ] ) => { new Notification ( PRODUCT_NAME , { icon : trayIconDataURL , body : `Account [${ title || this . account . accountIndex }]: ${ unread } unread message${ unread > 1 ? "s" : "" }.` , } ) . onclick = ( ) => this . zone . run ( ( ) => { this . onDispatch ( ACCOUNTS_ACTIONS . Select ( { login } ) ) ; this . onDispatch ( NAVIGATION_ACTIONS . ToggleBrowserWindow ( { forcedState : true } ) ) ; } ) ; } ) ) ; this . subscription . add ( this . persistentSession$ . pipe ( mergeMap ( ( persistentSession ) => persistentSession ? [ ] : [ persistentSession ] ) , withLatestFrom ( this . account$ ) ) . subscribe ( async ( [ persistentSession , { accountConfig } ] ) => { if ( persistentSession ) { throw new Error ( `"persistentSession" value is supposed to be "false" here` ) ; } const parsedEntryUrl = this . core . parseEntryUrl ( accountConfig , "proton-mail" ) ; const key = { login : accountConfig . login , apiEndpointOrigin : new URL ( parsedEntryUrl . entryApiUrl ) . origin } as const ; await this . ipcMainClient ( "resetSavedProtonSession" ) ( key ) ; } ) ) ; } onEventChild ( event ) { if ( event . type === "log" ) { const [ level , ... args ] = event . data ; this . logger [ level ] ( ... args ) ; return ; } if ( event . type === "action" ) { this . onDispatch ( event . payload ) ; return ; } this . webViewsState [ event . viewType ] . domReady$ . next ( event . webView ) ; } onPrimaryViewLoadedOnce ( primaryWebView ) { this . logger . info ( nameof ( AccountComponent . prototype . onPrimaryViewLoadedOnce ) ) ; const resolvePrimaryWebViewApiClient = async ( ) => lastValueFrom ( this . electronService . primaryWebViewClient ( { webView : primaryWebView , accountIndex : this . account . accountIndex } ) ) ; const resolveLiveProtonClientSession = async ( ) : Promise < ProtonClientSession > => { const apiClient = await resolvePrimaryWebViewApiClient ( ) ; const value = await apiClient ( "resolveLiveProtonClientSession" ) ( pick ( this . account , [ "accountIndex" ] ) ) ; if ( ! value ) { throw new Error ( `Failed to resolve "proton client session" object` ) ; } return value ; } ; { const logger = curryFunctionMembers ( this . logger , nameof ( AccountComponent . prototype . onPrimaryViewLoadedOnce ) , "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 ) , 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 ? ( timer ( 0 , persistentSessionSavingInterval ) . pipe ( tap ( ( value ) => logger . verbose ( "trigger: interval" , value ) ) ) ) : of ( null ) ) , ] ) . 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 : parseUrlOriginWithNullishCheck ( this . core . parseEntryUrl ( accountConfig , "proton-mail" ) . entryApiUrl ) , } ) ; } ) ) ; } this . subscription . add ( combineLatest ( [ this . loggedIn$ , this . loggedInCalendar$ , this . store . pipe ( select ( OptionsSelectors . CONFIG . calendarNotification ) ) , ] ) . pipe ( withLatestFrom ( this . account$ , this . store . pipe ( select ( OptionsSelectors . CONFIG . timeouts ) ) ) ) . subscribe ( async ( [ [ loggedIn , loggedInCalendar , calendarNotification ] , { accountConfig } , { webViewApiPing : calendarGetsSignedInStateTimeoutMs } ] ) => { if ( ! calendarNotification ) { this . webViewsState . calendar . src$ . next ( "" ) ; return ; } if ( ! loggedIn || loggedInCalendar ) { return ; } const project = "proton-calendar" ; const [ clientSession , sessionStoragePatch ] = await Promise . all ( [ resolveLiveProtonClientSession ( ) , ( async ( ) => { const apiClient = await resolvePrimaryWebViewApiClient ( ) ; return apiClient ( "resolvedLiveSessionStoragePatch" ) ( pick ( this . account , [ "accountIndex" ] ) ) ; } ) ( ) , ] ) ; await Promise . all ( [ await this . core . applyProtonClientSessionAndNavigate ( accountConfig , project , this . webViewsState . calendar . domReady$ , ( src ) => this . webViewsState . calendar . src$ . next ( src ) , this . logger , { clientSession , sessionStoragePatch } ) , lastValueFrom ( race ( this . loggedInCalendar$ . pipe ( filter ( Boolean ) ) , timer ( calendarGetsSignedInStateTimeoutMs ) . pipe ( concatMap ( ( ) => throwError ( new Error ( `The Calendar has not got the signed-in stage in ${ calendarGetsSignedInStateTimeoutMs }ms` ) ) ) ) ) ) , ] ) ; } ) ) ; } onDispatch ( action ) { this . store . dispatch ( action ) ; } ngOnDestroy ( ) { super . ngOnDestroy ( ) ; this . logger . info ( nameof ( AccountComponent . prototype . ngOnDestroy ) ) ; this . subscription . unsubscribe ( ) ; } private focusVisibleViewModeContainerElement ( element ) { timer ( 0 , ONE_SECOND_MS / 10 ) . pipe ( filter ( ( ) => Boolean ( element . offsetParent ) ) , take ( 1 ) , takeUntil ( timer ( ONE_SECOND_MS * 0.7 ) ) ) . subscribe ( ( ) => { if ( typeof element . focus === "function" ) { element . focus ( ) ; } if ( this . viewModeClass !== "vm-live" || typeof element . executeJavaScript !== "function" ) { return ; } element . executeJavaScript ( `
                    setTimeout(
                        () => {
                            const el = document.querySelector(".electron-mail-mailbox-container-component");
                            if (/* visibility test */ el?.offsetParent && typeof el.focus === "function") {
                                el.focus();
                            }
                        },
                        ${ ONE_SECOND_MS / 2 },
                    );` ) . catch ( ( error ) => { this . logger . error ( "failed to focus protonmail mailbox container DOM element" , error ) ; } ) ; } ) ; } }