import type { Action } from "@ngrx/store" ; import { Observable , Subscription , combineLatest , lastValueFrom , race } from "rxjs" ; import { distinctUntilChanged , filter , map , take } from "rxjs/operators" ; import { equals , pick } from "remeda" ; import { ACCOUNTS_ACTIONS , NAVIGATION_ACTIONS } from "src/web/browser-window/app/store/actions" ; import { AccountComponent } from "src/web/browser-window/app/_accounts/account.component" ; import { CoreService } from "src/web/browser-window/app/_core/core.service" ; import { DOCUMENT } from "@angular/common" ; import { Directive , ElementRef , EventEmitter , Injector , Input , Output , Renderer2 } from "@angular/core" ; import { ElectronService } from "src/web/browser-window/app/_core/electron.service" ; import { LogLevel } from "src/shared/model/common" ; import { NgChangesObservableComponent } from "src/web/browser-window/app/components/ng-changes-observable.component" ; import type { OnDestroy } from "@angular/core" ; import { WebAccount } from "src/web/browser-window/app/model" ; import { depersonalizeLoggedUrlsInString , getWebViewPartition , lowerConsoleMessageEventLogLevel } from "src/shared/util" ; type ChildEvent = Parameters < typeof AccountComponent . prototype . onEventChild > [ 0 ] ; @ Directive ( ) export abstract class AccountViewAbstractComponent extends NgChangesObservableComponent implements OnDestroy { @ Input ( ) readonly account ! : WebAccount ; readonly account$ : Observable < WebAccount > = this . ngChangesObservable ( "account" ) ; @ Input ( ) webViewSrc ! : string ; @ Output ( ) private readonly event = new EventEmitter < ChildEvent > ( ) ; protected readonly api : ElectronService ; protected readonly core : CoreService ; private readonly subscription = new Subscription ( ) ; protected constructor ( private readonly viewType , private readonly injector ) { super ( ) ; this . api = this . injector . get ( ElectronService ) ; this . core = this . injector . get ( CoreService ) ; this . log ( "info" , [ nameof ( AccountViewAbstractComponent ) , "constructor" ] ) ; this . webViewConstruction ( ) ; { let customCssKey ; this . subscription . add ( combineLatest ( [ this . filterDomReadyEvent ( ) , this . account$ . pipe ( map ( ( { accountConfig : { customCSS } } ) => customCSS ) , distinctUntilChanged ( ) ) , ] ) . subscribe ( async ( [ { webView } , customCSS ] ) => { if ( customCssKey ) { this . log ( "verbose" , [ "removing custom css" ] ) ; await webView . removeInsertedCSS ( customCssKey ) ; customCssKey = undefined ; } if ( ! customCSS ?. trim ( ) ) { return ; } this . log ( "verbose" , [ "inserting custom css" ] ) ; customCssKey = await webView . insertCSS ( customCSS ) ; } ) ) ; } if ( BUILD_ENVIRONMENT === "development" ) { this . subscription . add ( this . filterDomReadyEvent ( ) . pipe ( take ( 1 ) ) . subscribe ( ( { webView } ) => webView . openDevTools ( ) ) ) ; } } ngOnDestroy ( ) { super . ngOnDestroy ( ) ; this . log ( "info" , [ nameof ( AccountViewAbstractComponent . prototype . ngOnDestroy ) ] ) ; this . subscription . unsubscribe ( ) ; this . action ( ACCOUNTS_ACTIONS . Patch ( { login : this . account . accountConfig . login , patch : { webviewSrcValues : { [ this . viewType ] : "" } } , optionalAccount : true } ) ) ; } protected log ( level , args ) { this . event . emit ( { type : "log" , data : [ level , ... args ] } ) ; } protected action ( payload ) { this . event . emit ( { type : "action" , payload } ) ; } protected filterDomReadyEvent ( ) { return this . event . pipe ( filter ( ( { type } ) => type === "dom-ready" ) , map ( ( event ) => event as Extract < Unpacked < typeof AccountViewAbstractComponent . prototype . event > , { type : "dom-ready" ; } > ) ) ; } protected async filterDomReadyOrDestroyedPromise ( ) { return lastValueFrom ( race ( [ this . filterDomReadyEvent ( ) . pipe ( take ( 1 ) ) , this . ngOnDestroy$ , ] ) ) . then ( ( ) => { } ) ; } protected addSubscription ( ... [ teardown ] ) { return this . subscription . add ( teardown ) ; } private webViewConstruction ( ) { let webView ; this . subscription . add ( combineLatest ( [ this . ngChangesObservable ( "webViewSrc" ) , this . account$ . pipe ( map ( ( { accountConfig : { login } } ) => getWebViewPartition ( login ) ) ) , ] ) . pipe ( map ( ( [ src , partition ] ) => ( { src , partition } ) ) , distinctUntilChanged ( ( prev , curr ) => equals ( prev , curr ) ) ) . subscribe ( ( { src , partition } ) => { if ( ! webView ) { const document = this . injector . get ( DOCUMENT ) ; webView = document . createElement ( "webView" ) as Electron . WebviewTag ; this . registerWebViewEventsHandlingOnce ( webView ) ; Object . assign ( webView , { src , partition , preload : __METADATA__ . electronLocations . preload [ this . viewType ] } ) ; } else { webView . src = src ; } if ( ! webView . parentNode ) { const elementRef = this . injector . get < ElementRef < HTMLElement > > ( ElementRef ) ; const renderer2 = this . injector . get ( Renderer2 ) ; renderer2 . appendChild ( elementRef . nativeElement , webView ) ; } } ) ) ; } private registerWebViewEventsHandlingOnce ( webView ) { this . log ( "info" , [ nameof ( AccountViewAbstractComponent . prototype . registerWebViewEventsHandlingOnce ) ] ) ; const didNavigateArgs = [ "did-navigate" , ( { url } ) => { this . action ( ACCOUNTS_ACTIONS . Patch ( { login : this . account . accountConfig . login , patch : { webviewSrcValues : { [ this . viewType ] : url } } } ) ) ; } , ] as const ; const domReadyArgs = [ "dom-ready" , ( { type } ) => { this . event . emit ( { type : "dom-ready" , viewType : this . viewType , webView } ) ; this . log ( "verbose" , [ "webview event" , JSON . stringify ( { type , src : webView . src } ) ] ) ; } , ] as const ; const newWindowArgs = [ "new-window" , ( { url } ) => { this . event . emit ( { type : "action" , payload : NAVIGATION_ACTIONS . OpenExternal ( { url } ) } ) ; } , ] as const ; const consoleMessageArgs = [ "console-message" , ( { type , level , message , line , sourceId } ) => { const isWarn = Number ( level ) === 2 ; const isError = Number ( level ) === 3 ; if ( isWarn || isError ) { this . log ( lowerConsoleMessageEventLogLevel ( isWarn ? "warn" : "error" , message ) , [ "webview event" , JSON . stringify ( { type , level , message : depersonalizeLoggedUrlsInString ( message ) , line , sourceId } ) ] ) ; } } , ] as const ; const didFailLoadArgs = [ "did-fail-load" , ( event ) => { this . log ( "error" , [ "webview event" , JSON . stringify ( pick ( event , [ "type" , "errorCode" , "errorDescription" , "validatedURL" , "isMainFrame" ] ) ) , ] ) ; } , ] as const ; const crashedArgs = [ "crashed" , ( event ) => { this . log ( "error" , [ "webview event" , JSON . stringify ( pick ( event , [ "type" ] ) ) ] ) ; } , ] as const ; const pluginCrashedArgs = [ "plugin-crashed" , ( event ) => { this . log ( "error" , [ "webview event" , JSON . stringify ( pick ( event , [ "type" , "name" , "version" ] ) ) ] ) ; } , ] as const ; webView . addEventListener ( ... didNavigateArgs ) ; webView . addEventListener ( ... domReadyArgs ) ; webView . addEventListener ( ... newWindowArgs ) ; webView . addEventListener ( ... consoleMessageArgs ) ; webView . addEventListener ( ... didFailLoadArgs ) ; webView . addEventListener ( ... crashedArgs ) ; webView . addEventListener ( ... pluginCrashedArgs ) ; this . log ( "info" , [ "webview handlers subscribed" ] ) ; this . subscription . add ( { unsubscribe : ( ) => { webView . removeEventListener ( ... didNavigateArgs ) ; webView . removeEventListener ( ... domReadyArgs ) ; webView . removeEventListener ( ... newWindowArgs ) ; webView . removeEventListener ( ... consoleMessageArgs ) ; webView . removeEventListener ( ... didFailLoadArgs ) ; webView . removeEventListener ( ... crashedArgs ) ; webView . removeEventListener ( ... pluginCrashedArgs ) ; this . log ( "info" , [ "webview handlers unsubscribed" ] ) ; } , } ) ; } }