import angular from 'angular' ; import autobind from 'autobind-decorator' ; import browser , { Bookmarks as NativeBookmarks } from 'webextension-polyfill' ; import { BookmarkChangeType , BookmarkContainer , BookmarkType } from '../../../shared/bookmark/bookmark.enum' ; import { AddNativeBookmarkChangeData , Bookmark , BookmarkChange , BookmarkMetadata , BookmarkService , ModifyNativeBookmarkChangeData , MoveNativeBookmarkChangeData , OnChildrenReorderedReorderInfoType , RemoveNativeBookmarkChangeData , ReorderNativeBookmarkChangeData , UpdateBookmarksResult } from '../../../shared/bookmark/bookmark.interface' ; import { BookmarkHelperService } from '../../../shared/bookmark/bookmark-helper/bookmark-helper.service' ; import { AmbiguousSyncRequestError , BookmarkNotFoundError , ContainerChangedError , ContainerNotFoundError , FailedCreateNativeBookmarksError , FailedGetNativeBookmarksError , FailedRemoveNativeBookmarksError , FailedUpdateNativeBookmarksError , NativeBookmarkNotFoundError } from '../../../shared/errors/errors' ; import { MessageCommand } from '../../../shared/global-shared.enum' ; import { PlatformService , WebpageMetadata } from '../../../shared/global-shared.interface' ; import { LogService } from '../../../shared/log/log.service' ; import { SettingsService } from '../../../shared/settings/settings.service' ; import { StoreService } from '../../../shared/store/store.service' ; import { SyncType } from '../../../shared/sync/sync.enum' ; import { Sync } from '../../../shared/sync/sync.interface' ; import { SyncService } from '../../../shared/sync/sync.service' ; import { UtilityService } from '../../../shared/utility/utility.service' ; import { BookmarkIdMapping } from '../bookmark-id-mapper/bookmark-id-mapper.interface' ; import { BookmarkIdMapperService } from '../bookmark-id-mapper/bookmark-id-mapper.service' ; @ autobind export abstract class WebExtBookmarkService implements BookmarkService { $injector : ng . auto . IInjectorService ; $q : ng . IQService ; $timeout : ng . ITimeoutService ; bookmarkIdMapperSvc : BookmarkIdMapperService ; bookmarkHelperSvc : BookmarkHelperService ; logSvc : LogService ; platformSvc : PlatformService ; settingsSvc : SettingsService ; storeSvc : StoreService ; _syncSvc : SyncService ; utilitySvc : UtilityService ; nativeBookmarkEventsQueue : any [ ] = [ ] ; processNativeBookmarkEventsTimeout : ng . IPromise < void > ; unsupportedContainers = [ ] ; static $inject = [ '$injector' , '$q' , '$timeout' , 'BookmarkHelperService' , 'BookmarkIdMapperService' , 'LogService' , 'PlatformService' , 'SettingsService' , 'StoreService' , 'UtilityService' ] ; constructor ( $injector , $q , $timeout , BookmarkHelperSvc , BookmarkIdMapperSvc , LogSvc , PlatformSvc , SettingsSvc , StoreSvc , UtilitySvc ) { this . $injector = $injector ; this . $q = $q ; this . $timeout = $timeout ; this . bookmarkIdMapperSvc = BookmarkIdMapperSvc ; this . bookmarkHelperSvc = BookmarkHelperSvc ; this . logSvc = LogSvc ; this . platformSvc = PlatformSvc ; this . settingsSvc = SettingsSvc ; this . storeSvc = StoreSvc ; this . utilitySvc = UtilitySvc ; } get syncSvc ( ) : SyncService { if ( angular . isUndefined ( this . _syncSvc ) ) { this . _syncSvc = this . $injector . get ( 'SyncService' ) ; } return this . _syncSvc ; } addBookmark ( bookmark , parentId , index , bookmarks ) { const updatedBookmarks = angular . copy ( bookmarks ) ; const parent = this . bookmarkHelperSvc . findBookmarkById ( parentId , updatedBookmarks ) ; if ( ! parent ) { throw new BookmarkNotFoundError ( ) ; } parent . children . splice ( index , 0 , bookmark ) ; return { bookmark , bookmarks : updatedBookmarks } as UpdateBookmarksResult ; } buildIdMappings ( bookmarks ) { const mapIds = ( nativeBookmarks , syncedBookmarks ) : BookmarkIdMapping [ ] => { return nativeBookmarks . reduce ( ( acc , val , index ) => { const mapping = this . bookmarkIdMapperSvc . createMapping ( syncedBookmarks [ index ] . id , val . id ) ; acc . push ( mapping ) ; return val . children ?. length ? acc . concat ( mapIds ( val . children , syncedBookmarks [ index ] . children ) ) : acc ; } , [ ] as BookmarkIdMapping [ ] ) ; } ; return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const menuBookmarksId = nativeContainerIds . get ( BookmarkContainer . Menu ) ; const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; const toolbarBookmarksId = nativeContainerIds . get ( BookmarkContainer . Toolbar ) ; const getMenuBookmarks = menuBookmarksId == null ? this . $q . resolve ( [ ] as BookmarkIdMapping [ ] ) : browser . bookmarks . getSubTree ( menuBookmarksId ) . then ( ( subTree ) => { const [ menuContainer ] = subTree ; if ( ! menuContainer . children ?. length ) { return [ ] as BookmarkIdMapping [ ] ; } const menuBookmarksContainer = bookmarks . find ( ( x ) => { return x . title === BookmarkContainer . Menu ; } ) ; return menuBookmarksContainer ?. children ?. length ? mapIds ( menuContainer . children , menuBookmarksContainer . children ) : ( [ ] as BookmarkIdMapping [ ] ) ; } ) ; const getOtherBookmarks = otherBookmarksId == null ? this . $q . resolve ( [ ] as BookmarkIdMapping [ ] ) : browser . bookmarks . getSubTree ( otherBookmarksId ) . then ( ( subTree ) => { const [ otherContainer ] = subTree ; if ( ! otherContainer . children ?. length ) { return [ ] as BookmarkIdMapping [ ] ; } const nodes = otherContainer . children . filter ( ( x ) => ! this . unsupportedContainers . includes ( x . title ) ) ; const otherBookmarksContainer = bookmarks . find ( ( x ) => { return x . title === BookmarkContainer . Other ; } ) ; return otherBookmarksContainer ?. children ?. length ? mapIds ( nodes , otherBookmarksContainer . children ) : ( [ ] as BookmarkIdMapping [ ] ) ; } ) ; const getToolbarBookmarks = toolbarBookmarksId == null ? this . $q . resolve ( [ ] as BookmarkIdMapping [ ] ) : browser . bookmarks . getSubTree ( toolbarBookmarksId ) . then ( ( results ) => { return this . settingsSvc . syncBookmarksToolbar ( ) . then ( ( syncBookmarksToolbar ) => { const [ toolbarContainer ] = results ; if ( ! syncBookmarksToolbar || ! toolbarContainer . children ?. length ) { return [ ] as BookmarkIdMapping [ ] ; } const toolbarBookmarksContainer = bookmarks . find ( ( x ) => { return x . title === BookmarkContainer . Toolbar ; } ) ; return toolbarBookmarksContainer ?. children ?. length ? mapIds ( toolbarContainer . children , toolbarBookmarksContainer . children ) : ( [ ] as BookmarkIdMapping [ ] ) ; } ) ; } ) ; return this . $q . all ( [ getMenuBookmarks , getOtherBookmarks , getToolbarBookmarks ] ) ; } ) . then ( ( results ) => { const combinedMappings = results . reduce ( ( acc , val ) => { return acc . concat ( val ) ; } , [ ] ) ; return this . bookmarkIdMapperSvc . set ( combinedMappings ) ; } ) ; } checkIfBookmarkChangeShouldBeSynced ( changedBookmark , bookmarks ) { return this . settingsSvc . syncBookmarksToolbar ( ) . then ( ( syncBookmarksToolbar ) => { const container = this . bookmarkHelperSvc . getContainerByBookmarkId ( changedBookmark . id , bookmarks ) ; if ( ! container ) { throw new ContainerNotFoundError ( ) ; } if ( container . title === BookmarkContainer . Toolbar && ! syncBookmarksToolbar ) { this . logSvc . logInfo ( 'Not syncing toolbar' ) ; return false ; } return true ; } ) ; } checkPermsAndGetPageMetadata ( ) { return this . platformSvc . checkOptionalNativePermissions ( ) . then ( ( hasPermissions ) => { if ( ! hasPermissions ) { this . logSvc . logInfo ( 'Do not have permission to read active tab content' ) ; } return hasPermissions ? this . platformSvc . getPageMetadata ( true ) : this . platformSvc . getPageMetadata ( false ) ; } ) ; } abstract clearNativeBookmarks ( ) ; convertNativeBookmarkToBookmark ( nativeBookmark , bookmarks , takenIds ? ) { if ( ! nativeBookmark ) { return ; } const id = this . bookmarkHelperSvc . getNewBookmarkId ( bookmarks , takenIds ) ; if ( angular . isUndefined ( takenIds ) ) { takenIds = [ ] ; } takenIds . push ( id ) ; const bookmark = this . bookmarkHelperSvc . newBookmark ( nativeBookmark . title , nativeBookmark . url ) ; bookmark . id = id ; if ( nativeBookmark . children ?. length ) { bookmark . children = nativeBookmark . children . map ( ( childBookmark ) => { return this . convertNativeBookmarkToBookmark ( childBookmark , bookmarks , takenIds ) ; } ) ; } return bookmark ; } countNativeContainersBeforeIndex ( parentId , index ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { if ( parentId !== nativeContainerIds . get ( BookmarkContainer . Other ) ) { return 0 ; } return browser . bookmarks . getSubTree ( parentId ) . then ( ( subTree ) => { const [ parent ] = subTree ; const numContainers = parent . children . filter ( ( child , childIndex ) => { return childIndex < index && Array . from ( nativeContainerIds . values ( ) ) . includes ( child . id ) ; } ) . length ; return numContainers ; } ) ; } ) ; } createBookmarkFromNativeBookmarkId ( id , bookmarks ) { return browser . bookmarks . get ( id ) . then ( ( results ) => { if ( results ?. length === 0 ) { throw new NativeBookmarkNotFoundError ( ) ; } const [ nativeBookmark ] = results ; const convertedBookmark = this . convertNativeBookmarkToBookmark ( nativeBookmark , bookmarks ) ; return convertedBookmark ; } ) ; } createNativeBookmark ( parentId , title , url , index ? ) { const nativeBookmarkInfo = { index , parentId , title } ; if ( ! angular . isUndefined ( url ?? undefined ) ) { nativeBookmarkInfo . url = this . getSupportedUrl ( url ) ; } return browser . bookmarks . create ( nativeBookmarkInfo ) . catch ( ( err ) => { this . logSvc . logWarning ( `Failed to create native bookmark: ${ JSON . stringify ( nativeBookmarkInfo ) }` ) ; throw new FailedCreateNativeBookmarksError ( undefined , err ) ; } ) ; } abstract createNativeBookmarksFromBookmarks ( bookmarks ) ; createNativeBookmarkTree ( parentId , bookmarks , nativeToolbarContainerId ? ) { let processError ; let total = 0 ; const createRecursive = ( id , toolbarId , bookmarksToCreate = [ ] ) => { const createChildBookmarksPromises = [ ] ; return bookmarksToCreate . reduce ( ( p , bookmark ) => { return p . then ( ( ) => { if ( processError ) { return this . $q . resolve ( ) ; } return this . bookmarkHelperSvc . getBookmarkType ( bookmark ) === BookmarkType . Separator ? this . createNativeSeparator ( id , toolbarId ) . then ( ( ) => { } ) : this . createNativeBookmark ( id , bookmark . title , bookmark . url ) . then ( ( newNativeBookmark ) => { if ( bookmark . children ?. length ) { createChildBookmarksPromises . push ( createRecursive ( newNativeBookmark . id , toolbarId , bookmark . children ) ) ; } } ) ; } ) ; } , this . $q . resolve ( ) ) . then ( ( ) => this . $q . all ( createChildBookmarksPromises ) ) . then ( ( ) => { total += bookmarksToCreate . length ; } ) . catch ( ( err ) => { processError = err ; throw err ; } ) ; } ; return createRecursive ( parentId , nativeToolbarContainerId , bookmarks ) . then ( ( ) => total ) ; } abstract createNativeSeparator ( parentId , nativeToolbarContainerId ) ; abstract disableEventListeners ( ) ; abstract enableEventListeners ( ) ; abstract ensureContainersExist ( bookmarks ) ; getBookmarksForExport ( ) { return this . utilitySvc . isSyncEnabled ( ) . then ( ( syncEnabled ) => { return syncEnabled ? this . bookmarkHelperSvc . getCachedBookmarks ( ) : this . getNativeBookmarksAsBookmarks ( ) ; } ) . then ( ( bookmarks ) => { return this . bookmarkHelperSvc . cleanAllBookmarks ( this . bookmarkHelperSvc . removeEmptyContainers ( bookmarks ) ) ; } ) ; } getContainerNameFromNativeId ( nativeBookmarkId ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const menuBookmarksId = nativeContainerIds . get ( BookmarkContainer . Menu ) ; const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; const toolbarBookmarksId = nativeContainerIds . get ( BookmarkContainer . Toolbar ) ; const nativeContainers = [ { nativeId : otherBookmarksId , containerName : BookmarkContainer . Other } , { nativeId : toolbarBookmarksId , containerName : BookmarkContainer . Toolbar } ] ; if ( menuBookmarksId ) { nativeContainers . push ( { nativeId : menuBookmarksId , containerName : BookmarkContainer . Menu } ) ; } const result = nativeContainers . find ( ( x ) => x . nativeId === nativeBookmarkId ) ; return result ? result . containerName : '' ; } ) ; } getIdsFromDescendants ( bookmark ) { const ids = [ ] ; if ( angular . isUndefined ( bookmark . children ?? undefined ) || bookmark . children . length === 0 ) { return ids ; } this . bookmarkHelperSvc . eachBookmark ( ( child ) => { ids . push ( child . id ) ; } , bookmark . children ) ; return ids ; } getNativeBookmarkByTitle ( title ) { if ( ! title ) { return this . $q . resolve ( null ) ; } return browser . bookmarks . search ( { title } ) . then ( ( results ) => { const [ bookmark ] = results ?? [ ] ; return bookmark ; } ) ; } abstract getNativeBookmarksAsBookmarks ( ) ; abstract getNativeContainerIds ( ) ; getSupportedUrl ( url ) { if ( angular . isUndefined ( url ?? undefined ) ) { return '' ; } let returnUrl = url ; if ( ! this . platformSvc . urlIsSupported ( url ) ) { this . logSvc . logInfo ( `Bookmark url unsupported: ${ url }` ) ; returnUrl = this . platformSvc . getNewTabUrl ( ) ; } return returnUrl ; } isNativeBookmarkInToolbarContainer ( nativeBookmark ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { return nativeBookmark . parentId === nativeContainerIds . get ( BookmarkContainer . Toolbar ) ; } ) ; } modifyNativeBookmark ( id , newMetadata ) { const updateInfo = { title : newMetadata . title } ; if ( ! angular . isUndefined ( updateInfo . url ?? undefined ) ) { updateInfo . url = this . getSupportedUrl ( updateInfo . url ) ; } return browser . bookmarks . update ( id , updateInfo ) . catch ( ( err ) => { this . logSvc . logInfo ( `Failed to modify native bookmark: ${ JSON . stringify ( newMetadata ) }` ) ; throw new FailedUpdateNativeBookmarksError ( undefined , err ) ; } ) ; } onNativeBookmarkChanged ( ... args ) { this . logSvc . logInfo ( 'onChanged event detected' ) ; this . queueNativeBookmarkEvent ( BookmarkChangeType . Modify , ... args ) ; } onNativeBookmarkCreated ( ... args ) { this . logSvc . logInfo ( 'onCreated event detected' ) ; this . queueNativeBookmarkEvent ( BookmarkChangeType . Add , ... args ) ; } onNativeBookmarkMoved ( ... args ) { this . logSvc . logInfo ( 'onMoved event detected' ) ; this . queueNativeBookmarkEvent ( BookmarkChangeType . Move , ... args ) ; } onNativeBookmarkRemoved ( ... args ) { this . logSvc . logInfo ( 'onRemoved event detected' ) ; this . queueNativeBookmarkEvent ( BookmarkChangeType . Remove , ... args ) ; } processChangeOnNativeBookmarks ( id , changeType , changeInfo ) { switch ( changeType ) { case BookmarkChangeType . Add : return this . processChangeTypeAddOnNativeBookmarks ( id , changeInfo ) ; case BookmarkChangeType . Modify : return this . processChangeTypeModifyOnNativeBookmarks ( id , changeInfo ) ; case BookmarkChangeType . Remove : return this . processChangeTypeRemoveOnNativeBookmarks ( id ) ; default : throw new AmbiguousSyncRequestError ( ) ; } } processChangeTypeAddOnBookmarks ( bookmarks , changeData ) { return this . wasContainerChanged ( changeData . nativeBookmark ) . then ( ( changedBookmarkIsContainer ) => { if ( changedBookmarkIsContainer ) { throw new ContainerChangedError ( ) ; } return this . getContainerNameFromNativeId ( changeData . nativeBookmark . parentId ) . then ( ( containerName ) => { if ( containerName ) { const container = this . bookmarkHelperSvc . getContainer ( containerName , bookmarks , true ) ; return container . id as number ; } return this . bookmarkIdMapperSvc . get ( changeData . nativeBookmark . parentId ) . then ( ( idMapping ) => { if ( ! idMapping ) { this . logSvc . logInfo ( 'No id mapping found, skipping sync' ) ; return ; } return idMapping . syncedId ; } ) ; } ) . then ( ( parentId ) => { if ( ! parentId ) { return bookmarks ; } const newBookmarkMetadata = this . bookmarkHelperSvc . extractBookmarkMetadata ( changeData . nativeBookmark ) ; const newBookmark = this . bookmarkHelperSvc . newBookmark ( newBookmarkMetadata . title , newBookmarkMetadata . url , newBookmarkMetadata . description , newBookmarkMetadata . tags , bookmarks ) ; const addBookmarkResult = this . addBookmark ( newBookmark , parentId , changeData . nativeBookmark . index , bookmarks ) ; return this . checkIfBookmarkChangeShouldBeSynced ( addBookmarkResult . bookmark , addBookmarkResult . bookmarks ) . then ( ( syncThisChange ) => { if ( ! syncThisChange ) { return bookmarks ; } const idMapping = this . bookmarkIdMapperSvc . createMapping ( addBookmarkResult . bookmark . id , changeData . nativeBookmark . id ) ; return this . bookmarkIdMapperSvc . add ( idMapping ) . then ( ( ) => { return addBookmarkResult . bookmarks ; } ) ; } ) ; } ) ; } ) ; } /**
     * Triggered when using "sort by name" in Chromium to sort bookmarks.
     *
     * Note: Firefox does not implement this, see https://bugzilla.mozilla.org/show_bug.cgi?id=1319530
     */ processChangeTypeChildrenReorderedOnBookmarks ( bookmarks , changeData ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const containerName = [ ... nativeContainerIds ] . find ( ( { 1 : x } ) => x === changeData . parentId ) ?. [ 0 ] ; if ( angular . isUndefined ( containerName ) ) { return this . bookmarkIdMapperSvc . get ( changeData . parentId ) . then ( ( idMapping ) => this . bookmarkHelperSvc . findBookmarkById ( idMapping . syncedId , bookmarks ) ) ; } return this . $q . resolve ( ) . then ( ( ) => this . bookmarkHelperSvc . getContainer ( containerName , bookmarks ) ) ; } ) . then ( ( parentBookmark ) => { return this . $q . all ( changeData . childIds . map ( ( childId ) => this . bookmarkIdMapperSvc . get ( childId ) ) ) . then ( ( idMappings ) => { const childIds = idMappings . filter ( Boolean ) . map ( ( idMapping ) => idMapping . syncedId ) ; parentBookmark . children = childIds . map < Bookmark > ( ( childId ) => { return ( parentBookmark . children as Bookmark [ ] ) . find ( ( x ) => x . id === childId ) ; } ) ; return bookmarks ; } ) ; } ) ; } processChangeTypeAddOnNativeBookmarks ( id , createInfo ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; return this . createNativeBookmark ( otherBookmarksId , createInfo . title , createInfo . url ) ; } ) . then ( ( newNativeBookmark ) => { const idMapping = this . bookmarkIdMapperSvc . createMapping ( id , newNativeBookmark . id ) ; return this . bookmarkIdMapperSvc . add ( idMapping ) ; } ) ; } processChangeTypeModifyOnBookmarks ( bookmarks , changeData ) { return this . wasContainerChanged ( changeData . nativeBookmark ) . then ( ( changedBookmarkIsContainer ) => { if ( changedBookmarkIsContainer ) { throw new ContainerChangedError ( ) ; } return this . bookmarkIdMapperSvc . get ( changeData . nativeBookmark . id ) . then ( ( idMapping ) => { if ( ! idMapping ) { this . logSvc . logInfo ( 'No id mapping found, skipping sync' ) ; return bookmarks ; } const bookmarkToUpdate = this . bookmarkHelperSvc . findBookmarkById ( idMapping . syncedId , bookmarks ) as Bookmark ; return this . checkIfBookmarkChangeShouldBeSynced ( bookmarkToUpdate , bookmarks ) . then ( ( syncThisChange ) => { if ( ! syncThisChange ) { return bookmarks ; } const updateInfo = this . bookmarkHelperSvc . extractBookmarkMetadata ( changeData . nativeBookmark ) ; return this . bookmarkHelperSvc . modifyBookmarkById ( idMapping . syncedId , updateInfo , bookmarks ) ; } ) ; } ) ; } ) ; } processChangeTypeModifyOnNativeBookmarks ( id , updateInfo ) { return this . bookmarkIdMapperSvc . get ( null , id ) . then ( ( idMapping ) => { if ( ! idMapping ) { this . logSvc . logWarning ( `No id mapping found for synced id '${ id }'` ) ; return ; } return this . modifyNativeBookmark ( idMapping . nativeId , updateInfo ) . then ( ( ) => { } ) ; } ) ; } processChangeTypeMoveOnBookmarks ( bookmarks , changeData ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { if ( Array . from ( nativeContainerIds . values ( ) ) . includes ( undefined ) ) { throw new ContainerChangedError ( ) ; } return browser . bookmarks . get ( changeData . id ) . then ( ( results ) => { const [ movedBookmark ] = results ; if ( Array . from ( nativeContainerIds . values ( ) ) . includes ( movedBookmark . id ) ) { return ; } return this . $q . all ( [ this . bookmarkIdMapperSvc . get ( changeData . id ) , this . getContainerNameFromNativeId ( changeData . parentId ) . then ( ( parentNameAsContainer ) => { if ( parentNameAsContainer ) { const container = this . bookmarkHelperSvc . getContainer ( parentNameAsContainer , bookmarks , true ) ; return { syncedId : container . id } ; } return this . bookmarkIdMapperSvc . get ( changeData . parentId ) ; } ) ] ) . then ( ( idMappings ) => { const [ movedBookmarkMapping , parentMapping ] = idMappings ; if ( ! movedBookmarkMapping && ! parentMapping ) { this . logSvc . logInfo ( 'No id mappings found, skipping sync' ) ; return ; } let changesMade = false ; return ( ! movedBookmarkMapping ? this . createBookmarkFromNativeBookmarkId ( changeData . id , bookmarks ) : this . $q . resolve ( ) . then ( ( ) => this . bookmarkHelperSvc . findBookmarkById ( movedBookmarkMapping . syncedId , bookmarks ) as Bookmark ) ) . then ( ( bookmarkToRemove ) => { let removeBookmarkPromise ; if ( ! movedBookmarkMapping ) { removeBookmarkPromise = this . $q . resolve ( bookmarks ) ; } else { removeBookmarkPromise = this . $q ( ( resolve , reject ) => { this . checkIfBookmarkChangeShouldBeSynced ( bookmarkToRemove , bookmarks ) . then ( ( syncThisChange ) => { if ( ! syncThisChange ) { return resolve ( bookmarks ) ; } return this . bookmarkHelperSvc . removeBookmarkById ( movedBookmarkMapping . syncedId , bookmarks ) . then ( ( updatedBookmarks ) => { changesMade = true ; resolve ( updatedBookmarks ) ; } ) ; } ) . catch ( reject ) ; } ) ; } return ( removeBookmarkPromise . then ( ( bookmarksAfterRemoval ) => { let addBookmarkPromise ; if ( ! parentMapping ) { addBookmarkPromise = this . $q . resolve ( bookmarksAfterRemoval ) ; } else { addBookmarkPromise = this . countNativeContainersBeforeIndex ( changeData . parentId , changeData . index ) . then ( ( numContainers ) => { const index = changeData . index - numContainers ; const addBookmarkResult = this . addBookmark ( bookmarkToRemove , parentMapping . syncedId , index , bookmarksAfterRemoval ) ; addBookmarkResult . bookmark . id = bookmarkToRemove . id ; return this . checkIfBookmarkChangeShouldBeSynced ( addBookmarkResult . bookmark , addBookmarkResult . bookmarks ) . then ( ( syncThisChange ) => { if ( ! syncThisChange ) { return bookmarksAfterRemoval ; } changesMade = true ; if ( movedBookmarkMapping ) { return addBookmarkResult . bookmarks ; } const idMapping = this . bookmarkIdMapperSvc . createMapping ( addBookmarkResult . bookmark . id , changeData . id ) ; return this . bookmarkIdMapperSvc . add ( idMapping ) . then ( ( ) => { return addBookmarkResult . bookmarks ; } ) ; } ) ; } ) ; } return addBookmarkPromise ; } ) . then ( ( updatedBookmarks ) => ( ! changesMade ? bookmarks : updatedBookmarks ) ) ) ; } ) ; } ) ; } ) ; } ) ; } processChangeTypeRemoveOnBookmarks ( bookmarks , changeData ) { return this . wasContainerChanged ( changeData . nativeBookmark ) . then ( ( changedBookmarkIsContainer ) => { if ( changedBookmarkIsContainer ) { throw new ContainerChangedError ( ) ; } return this . bookmarkIdMapperSvc . get ( changeData . nativeBookmark . id ) . then ( ( idMapping ) => { if ( ! idMapping ) { this . logSvc . logInfo ( 'No id mapping found, skipping sync' ) ; return bookmarks ; } const bookmarkToRemove = this . bookmarkHelperSvc . findBookmarkById ( idMapping . syncedId , bookmarks ) as Bookmark ; return this . checkIfBookmarkChangeShouldBeSynced ( bookmarkToRemove , bookmarks ) . then ( ( syncThisChange ) => { if ( ! syncThisChange ) { return bookmarks ; } const descendantsIds = this . getIdsFromDescendants ( bookmarkToRemove ) ; return this . bookmarkHelperSvc . removeBookmarkById ( idMapping . syncedId , bookmarks ) . then ( ( updatedBookmarks ) => { const syncedIds = descendantsIds . concat ( [ idMapping . syncedId ] ) ; return this . bookmarkIdMapperSvc . remove ( syncedIds ) . then ( ( ) => { return updatedBookmarks ; } ) ; } ) ; } ) ; } ) ; } ) ; } processChangeTypeRemoveOnNativeBookmarks ( id ) { return this . bookmarkIdMapperSvc . get ( null , id ) . then ( ( idMapping ) => { if ( ! idMapping ) { this . logSvc . logWarning ( `No id mapping found for synced id '${ id }'` ) ; return ; } return this . removeNativeBookmarks ( idMapping . nativeId ) . then ( ( ) => { return this . bookmarkIdMapperSvc . remove ( id ) ; } ) ; } ) ; } processNativeBookmarkEventsQueue ( ) { const condition = ( ) : ng . IPromise < boolean > => { return this . $q . resolve ( this . nativeBookmarkEventsQueue . length > 0 ) ; } ; const action = ( ) : ng . IPromise < void > => { const currentEvent = this . nativeBookmarkEventsQueue . shift ( ) ; switch ( currentEvent . changeType ) { case BookmarkChangeType . Add : return this . syncNativeBookmarkCreated ( ... currentEvent . eventArgs ) ; case BookmarkChangeType . ChildrenReordered : return this . syncNativeBookmarkChildrenReordered ( ... currentEvent . eventArgs ) ; case BookmarkChangeType . Remove : return this . syncNativeBookmarkRemoved ( ... currentEvent . eventArgs ) ; case BookmarkChangeType . Move : return this . syncNativeBookmarkMoved ( ... currentEvent . eventArgs ) ; case BookmarkChangeType . Modify : return this . syncNativeBookmarkChanged ( ... currentEvent . eventArgs ) ; default : throw new AmbiguousSyncRequestError ( ) ; } } ; this . utilitySvc . asyncWhile < any > ( this . nativeBookmarkEventsQueue , condition , action ) . then ( ( ) => { this . $timeout ( ( ) => { this . syncSvc . executeSync ( ) . then ( ( ) => { return this . disableEventListeners ( ) . then ( this . reorderUnsupportedContainers ) . then ( this . enableEventListeners ) ; } ) ; } , 100 ) ; } ) ; } processNativeChangeOnBookmarks ( changeInfo , bookmarks ) { switch ( changeInfo . type ) { case BookmarkChangeType . Add : return this . processChangeTypeAddOnBookmarks ( bookmarks , changeInfo . changeData as AddNativeBookmarkChangeData ) ; case BookmarkChangeType . ChildrenReordered : return this . processChangeTypeChildrenReorderedOnBookmarks ( bookmarks , changeInfo . changeData as ReorderNativeBookmarkChangeData ) ; case BookmarkChangeType . Modify : return this . processChangeTypeModifyOnBookmarks ( bookmarks , changeInfo . changeData as ModifyNativeBookmarkChangeData ) ; case BookmarkChangeType . Move : return this . processChangeTypeMoveOnBookmarks ( bookmarks , changeInfo . changeData as MoveNativeBookmarkChangeData ) ; case BookmarkChangeType . Remove : return this . processChangeTypeRemoveOnBookmarks ( bookmarks , changeInfo . changeData as RemoveNativeBookmarkChangeData ) ; default : throw new AmbiguousSyncRequestError ( ) ; } } queueNativeBookmarkEvent ( changeType , ... eventArgs ) { if ( this . processNativeBookmarkEventsTimeout ) { this . $timeout . cancel ( this . processNativeBookmarkEventsTimeout ) ; } this . nativeBookmarkEventsQueue . push ( { changeType , eventArgs } ) ; this . processNativeBookmarkEventsTimeout = this . $timeout ( this . processNativeBookmarkEventsQueue , 200 ) ; } removeNativeBookmarks ( id ) { return browser . bookmarks . removeTree ( id ) . catch ( ( err ) => { this . logSvc . logInfo ( `Failed to remove native bookmark: ${ id }` ) ; throw new FailedRemoveNativeBookmarksError ( undefined , err ) ; } ) ; } reorderUnsupportedContainers ( ) { return this . $q . all ( this . unsupportedContainers . map ( this . getNativeBookmarkByTitle ) ) . then ( ( results ) => { return this . $q . all ( results . filter ( ( x ) => x ) . map ( ( container , index ) => { return browser . bookmarks . move ( container . id , { index , parentId : container . parentId } ) ; } ) ) . then ( ( ) => { } ) ; } ) ; } syncChange ( changeInfo ) { const sync = { changeInfo , type : SyncType . Remote } ; return this . platformSvc . queueSync ( sync , MessageCommand . SyncBookmarks , false ) . catch ( ( ) => { } ) ; } abstract syncNativeBookmarkChanged ( id ? ) ; syncNativeBookmarkChildrenReordered ( id ? , reorderInfo ? ) { const data = { childIds : reorderInfo . childIds , parentId : id } ; const changeInfo = { changeData : data , type : BookmarkChangeType . ChildrenReordered } ; this . syncChange ( changeInfo ) ; return this . $q . resolve ( ) ; } abstract syncNativeBookmarkCreated ( id ? , nativeBookmark ? ) ; abstract syncNativeBookmarkMoved ( id ? , moveInfo ? ) ; syncNativeBookmarkRemoved ( id ? , removeInfo ? ) { const data = { nativeBookmark : { ... removeInfo . node , parentId : removeInfo . parentId } } ; const changeInfo = { changeData : data , type : BookmarkChangeType . Remove } ; this . syncChange ( changeInfo ) ; return this . $q . resolve ( ) ; } wasContainerChanged ( changedNativeBookmark ) { return this . getNativeContainerIds ( ) . then ( ( nativeContainerIds ) => { const otherBookmarksId = nativeContainerIds . get ( BookmarkContainer . Other ) ; if ( ( changedNativeBookmark as NativeBookmarks . BookmarkTreeNode ) . parentId !== otherBookmarksId ) { return false ; } if ( Array . from ( nativeContainerIds . values ( ) ) . includes ( undefined ) ) { return true ; } return browser . bookmarks . getChildren ( otherBookmarksId ) . then ( ( children ) => { const containers = children . filter ( ( x ) => this . unsupportedContainers . includes ( x . title ) ) . map ( ( x ) => x . title ) ; return containers . length !== new Set ( containers ) . size ; } ) . catch ( ( err ) => { this . logSvc . logInfo ( `Failed to detect whether container changed: ${ JSON . stringify ( changedNativeBookmark ) }` ) ; throw new FailedGetNativeBookmarksError ( undefined , err ) ; } ) ; } ) ; } }