import { Location } from "@angular/common" ; import { ChangeDetectorRef , Component , NgZone , OnDestroy , OnInit } from "@angular/core" ; import { ActivatedRoute , Router } from "@angular/router" ; import { first } from "rxjs/operators" ; import { BrowserApi } from "../../browser/browserApi" ; import { CipherType } from "jslib-common/enums/cipherType" ; import { CipherView } from "jslib-common/models/view/cipherView" ; import { CollectionView } from "jslib-common/models/view/collectionView" ; import { FolderView } from "jslib-common/models/view/folderView" ; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service" ; import { CipherService } from "jslib-common/abstractions/cipher.service" ; import { CollectionService } from "jslib-common/abstractions/collection.service" ; import { FolderService } from "jslib-common/abstractions/folder.service" ; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service" ; import { SearchService } from "jslib-common/abstractions/search.service" ; import { StateService } from "jslib-common/abstractions/state.service" ; import { StorageService } from "jslib-common/abstractions/storage.service" ; import { SyncService } from "jslib-common/abstractions/sync.service" ; import { UserService } from "jslib-common/abstractions/user.service" ; import { GroupingsComponent as BaseGroupingsComponent } from "jslib-angular/components/groupings.component" ; import { PopupUtilsService } from "../services/popup-utils.service" ; const ComponentId = "GroupingsComponent" ; const ScopeStateId = ComponentId + "Scope" ; @ Component ( { selector : "app-vault-groupings" , templateUrl : "groupings.component.html" , } ) export class GroupingsComponent extends BaseGroupingsComponent implements OnInit , OnDestroy { get showNoFolderCiphers ( ) : boolean { return ( this . noFolderCiphers != null && this . noFolderCiphers . length < this . noFolderListSize && this . collections . length === 0 ) ; } get folderCount ( ) : number { return this . nestedFolders . length - ( this . showNoFolderCiphers ? 0 : 1 ) ; } ciphers : CipherView [ ] ; favoriteCiphers : CipherView [ ] ; noFolderCiphers : CipherView [ ] ; folderCounts = new Map < string , number > ( ) ; collectionCounts = new Map < string , number > ( ) ; typeCounts = new Map < CipherType , number > ( ) ; searchText : string ; state : any ; scopeState : any ; showLeftHeader = true ; searchPending = false ; searchTypeSearch = false ; deletedCount = 0 ; private loadedTimeout : number ; private selectedTimeout : number ; private preventSelected = false ; private noFolderListSize = 100 ; private searchTimeout : any = null ; private hasSearched = false ; private hasLoadedAllCiphers = false ; private allCiphers : CipherView [ ] = null ; constructor ( collectionService , folderService , storageService , userService , private cipherService , private router , private ngZone , private broadcasterService , private changeDetectorRef , private route , private stateService , private popupUtils , private syncService , private platformUtilsService , private searchService , private location ) { super ( collectionService , folderService , storageService , userService ) ; this . noFolderListSize = 100 ; } async ngOnInit ( ) { this . searchTypeSearch = ! this . platformUtilsService . isSafari ( ) ; this . showLeftHeader = ! ( this . popupUtils . inSidebar ( window ) && this . platformUtilsService . isFirefox ( ) ) ; this . stateService . remove ( "CiphersComponent" ) ; this . broadcasterService . subscribe ( ComponentId , ( message ) => { this . ngZone . run ( async ( ) => { switch ( message . command ) { case "syncCompleted" : window . setTimeout ( ( ) => { this . load ( ) ; } , 500 ) ; break ; default : break ; } this . changeDetectorRef . detectChanges ( ) ; } ) ; } ) ; const restoredScopeState = await this . restoreState ( ) ; this . route . queryParams . pipe ( first ( ) ) . subscribe ( async ( params ) => { this . state = ( await this . stateService . get < any > ( ComponentId ) ) || { } ; if ( this . state . searchText ) { this . searchText = this . state . searchText ; } else if ( params . searchText ) { this . searchText = params . searchText ; this . location . replaceState ( "vault" ) ; } if ( ! this . syncService . syncInProgress ) { this . load ( ) ; } else { this . loadedTimeout = window . setTimeout ( ( ) => { if ( ! this . loaded ) { this . load ( ) ; } } , 5000 ) ; } if ( ! this . syncService . syncInProgress || restoredScopeState ) { window . setTimeout ( ( ) => this . popupUtils . setContentScrollY ( window , this . state . scrollY ) , 0 ) ; } } ) ; } ngOnDestroy ( ) { if ( this . loadedTimeout != null ) { window . clearTimeout ( this . loadedTimeout ) ; } if ( this . selectedTimeout != null ) { window . clearTimeout ( this . selectedTimeout ) ; } this . saveState ( ) ; this . broadcasterService . unsubscribe ( ComponentId ) ; } async load ( ) { await super . load ( false ) ; await this . loadCiphers ( ) ; if ( this . showNoFolderCiphers && this . nestedFolders . length > 0 ) { this . nestedFolders = this . nestedFolders . slice ( 0 , this . nestedFolders . length - 1 ) ; } super . loaded = true ; } async loadCiphers ( ) { this . allCiphers = await this . cipherService . getAllDecrypted ( ) ; if ( ! this . hasLoadedAllCiphers ) { this . hasLoadedAllCiphers = ! this . searchService . isSearchable ( this . searchText ) ; } this . deletedCount = this . allCiphers . filter ( ( c ) => c . isDeleted ) . length ; await this . search ( null ) ; let favoriteCiphers = null ; let noFolderCiphers = null ; const folderCounts = new Map < string , number > ( ) ; const collectionCounts = new Map < string , number > ( ) ; const typeCounts = new Map < CipherType , number > ( ) ; this . ciphers . forEach ( ( c ) => { if ( c . isDeleted ) { return ; } if ( c . favorite ) { if ( favoriteCiphers == null ) { favoriteCiphers = [ ] ; } favoriteCiphers . push ( c ) ; } if ( c . folderId == null ) { if ( noFolderCiphers == null ) { noFolderCiphers = [ ] ; } noFolderCiphers . push ( c ) ; } if ( typeCounts . has ( c . type ) ) { typeCounts . set ( c . type , typeCounts . get ( c . type ) + 1 ) ; } else { typeCounts . set ( c . type , 1 ) ; } if ( folderCounts . has ( c . folderId ) ) { folderCounts . set ( c . folderId , folderCounts . get ( c . folderId ) + 1 ) ; } else { folderCounts . set ( c . folderId , 1 ) ; } if ( c . collectionIds != null ) { c . collectionIds . forEach ( ( colId ) => { if ( collectionCounts . has ( colId ) ) { collectionCounts . set ( colId , collectionCounts . get ( colId ) + 1 ) ; } else { collectionCounts . set ( colId , 1 ) ; } } ) ; } } ) ; this . favoriteCiphers = favoriteCiphers ; this . noFolderCiphers = noFolderCiphers ; this . typeCounts = typeCounts ; this . folderCounts = folderCounts ; this . collectionCounts = collectionCounts ; } async search ( timeout = null ) { this . searchPending = false ; if ( this . searchTimeout != null ) { clearTimeout ( this . searchTimeout ) ; } const filterDeleted = ( c ) => ! c . isDeleted ; if ( timeout == null ) { this . hasSearched = this . searchService . isSearchable ( this . searchText ) ; this . ciphers = await this . searchService . searchCiphers ( this . searchText , filterDeleted , this . allCiphers ) ; return ; } this . searchPending = true ; this . searchTimeout = setTimeout ( async ( ) => { this . hasSearched = this . searchService . isSearchable ( this . searchText ) ; if ( ! this . hasLoadedAllCiphers && ! this . hasSearched ) { await this . loadCiphers ( ) ; } else { this . ciphers = await this . searchService . searchCiphers ( this . searchText , filterDeleted , this . allCiphers ) ; } this . searchPending = false ; } , timeout ) ; } async selectType ( type ) { super . selectType ( type ) ; this . router . navigate ( [ "/ciphers" ] , { queryParams : { type : type } } ) ; } async selectFolder ( folder ) { super . selectFolder ( folder ) ; this . router . navigate ( [ "/ciphers" ] , { queryParams : { folderId : folder . id || "none" } } ) ; } async selectCollection ( collection ) { super . selectCollection ( collection ) ; this . router . navigate ( [ "/ciphers" ] , { queryParams : { collectionId : collection . id } } ) ; } async selectTrash ( ) { super . selectTrash ( ) ; this . router . navigate ( [ "/ciphers" ] , { queryParams : { deleted : true } } ) ; } async selectCipher ( cipher ) { this . selectedTimeout = window . setTimeout ( ( ) => { if ( ! this . preventSelected ) { this . router . navigate ( [ "/view-cipher" ] , { queryParams : { cipherId : cipher . id } } ) ; } this . preventSelected = false ; } , 200 ) ; } async launchCipher ( cipher ) { if ( cipher . type !== CipherType . Login || ! cipher . login . canLaunch ) { return ; } if ( this . selectedTimeout != null ) { window . clearTimeout ( this . selectedTimeout ) ; } this . preventSelected = true ; await this . cipherService . updateLastLaunchedDate ( cipher . id ) ; BrowserApi . createNewTab ( cipher . login . launchUri ) ; if ( this . popupUtils . inPopup ( window ) ) { BrowserApi . closePopup ( window ) ; } } async addCipher ( ) { this . router . navigate ( [ "/add-cipher" ] ) ; } showSearching ( ) { return ( this . hasSearched || ( ! this . searchPending && this . searchService . isSearchable ( this . searchText ) ) ) ; } closeOnEsc ( e ) { if ( e . key === "Escape" && ( this . searchText == null || this . searchText === "" ) ) { BrowserApi . closePopup ( window ) ; } } private async saveState ( ) { this . state = { scrollY : this . popupUtils . getContentScrollY ( window ) , searchText : this . searchText , } ; await this . stateService . save ( ComponentId , this . state ) ; this . scopeState = { favoriteCiphers : this . favoriteCiphers , noFolderCiphers : this . noFolderCiphers , ciphers : this . ciphers , collectionCounts : this . collectionCounts , folderCounts : this . folderCounts , typeCounts : this . typeCounts , folders : this . folders , collections : this . collections , deletedCount : this . deletedCount , } ; await this . stateService . save ( ScopeStateId , this . scopeState ) ; } private async restoreState ( ) { this . scopeState = await this . stateService . get < any > ( ScopeStateId ) ; if ( this . scopeState == null ) { return false ; } if ( this . scopeState . favoriteCiphers != null ) { this . favoriteCiphers = this . scopeState . favoriteCiphers ; } if ( this . scopeState . noFolderCiphers != null ) { this . noFolderCiphers = this . scopeState . noFolderCiphers ; } if ( this . scopeState . ciphers != null ) { this . ciphers = this . scopeState . ciphers ; } if ( this . scopeState . collectionCounts != null ) { this . collectionCounts = this . scopeState . collectionCounts ; } if ( this . scopeState . folderCounts != null ) { this . folderCounts = this . scopeState . folderCounts ; } if ( this . scopeState . typeCounts != null ) { this . typeCounts = this . scopeState . typeCounts ; } if ( this . scopeState . folders != null ) { this . folders = this . scopeState . folders ; } if ( this . scopeState . collections != null ) { this . collections = this . scopeState . collections ; } if ( this . scopeState . deletedCiphers != null ) { this . deletedCount = this . scopeState . deletedCount ; } return true ; } }