From 683bb254313dd432d1a0ef72b57b3050cff645da Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 29 Feb 2024 20:50:19 +0600 Subject: [PATCH 01/19] enable sandbox tests (#2291) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206696533817490/f --- .../xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme | 6 ------ 1 file changed, 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme index a37fddc40f..a280d3c9f1 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme @@ -133,12 +133,6 @@ - - - - From 20b8f3298daa04220685f0b16d77fc56ff7bf5fe Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Fri, 1 Mar 2024 00:53:45 +0000 Subject: [PATCH 02/19] Move history model to BSK (#2280) Task/Issue URL: https://app.asana.com/0/414235014887631/1206696399092926/f Tech Design URL: CC: **Description**: Moves history model to BSK **Steps to test this PR**: 1. Run the app 2. Browse sites, ensure history updated as expected 3. Select items from history and ensure they open as expected 4. Use fire button and ensure history is cleared --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Alexey Martemyanov --- DuckDuckGo.xcodeproj/project.pbxproj | 98 ++--- .../xcshareddata/swiftpm/Package.resolved | 6 +- DuckDuckGo/Application/AppDelegate.swift | 6 +- DuckDuckGo/Common/Database/Stored.swift | 30 -- DuckDuckGo/Common/Logging/Logging.swift | 2 - .../Favicons/Model/FaviconManager.swift | 1 + DuckDuckGo/Fire/Model/Fire.swift | 1 + .../Fire/View/FirePopoverViewController.swift | 1 + .../Fire/ViewModel/FirePopoverViewModel.swift | 1 + DuckDuckGo/History/Model/History.swift | 21 - .../History/Model/HistoryCoordinator.swift | 375 ------------------ DuckDuckGo/History/Model/HistoryEntry.swift | 142 ------- DuckDuckGo/History/Model/Visit.swift | 60 --- ...tore.swift => EncryptedHistoryStore.swift} | 18 +- .../HistoryCoordinatorExtension.swift | 56 +++ .../History/ViewModel/VisitViewModel.swift | 1 + .../Model/HomePageRecentlyVisitedModel.swift | 1 + .../View/HomePageViewController.swift | 1 + .../Menus/CleanThisHistoryMenuItem.swift | 1 + DuckDuckGo/Menus/HistoryMenu.swift | 1 + DuckDuckGo/Menus/MainMenuActions.swift | 1 + DuckDuckGo/Menus/VisitMenuItem.swift | 1 + .../View/NavigationButtonMenuDelegate.swift | 1 + .../BackForwardListItemViewModel.swift | 1 + .../Model/SuggestionContainer.swift | 7 +- DuckDuckGo/Tab/Model/Tab.swift | 1 + .../TabExtensions/HistoryTabExtension.swift | 1 + .../Tab/TabExtensions/TabExtensions.swift | 1 + DuckDuckGo/TabBar/Model/TabCollection.swift | 1 + .../ViewModel/TabCollectionViewModel.swift | 1 + .../History/HistoryIntegrationTests.swift | 1 + .../Tab/SearchNonexistentDomainTests.swift | 1 + .../DataBrokerProtection/Package.swift | 2 +- LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- UnitTests/Fire/Model/FireTests.swift | 6 +- .../Model/HistoryCoordinatingMock.swift | 13 +- .../Model/HistoryCoordinatorTests.swift | 8 +- .../History/Services/HistoryStoreTests.swift | 5 +- .../History/Services/HistoryStoringMock.swift | 7 +- .../Tab/Services/FaviconManagerMock.swift | 3 +- .../TabBar/Model/TabCollectionTests.swift | 3 +- 45 files changed, 170 insertions(+), 728 deletions(-) delete mode 100644 DuckDuckGo/Common/Database/Stored.swift delete mode 100644 DuckDuckGo/History/Model/History.swift delete mode 100644 DuckDuckGo/History/Model/HistoryCoordinator.swift delete mode 100644 DuckDuckGo/History/Model/HistoryEntry.swift delete mode 100644 DuckDuckGo/History/Model/Visit.swift rename DuckDuckGo/History/Services/{HistoryStore.swift => EncryptedHistoryStore.swift} (96%) create mode 100644 DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 92dddde1b0..c58f2df7d1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -380,7 +380,6 @@ 3706FB68293F65D500E42796 /* NSNotificationName+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */; }; 3706FB69293F65D500E42796 /* NavigationBarBadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F7F2A5288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift */; }; 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F025D6BF10007F5990 /* AddressBarButton.swift */; }; - 3706FB6B293F65D500E42796 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; }; 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */; }; 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; @@ -456,7 +455,7 @@ 3706FBC2293F65D500E42796 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */; }; 3706FBC5293F65D500E42796 /* CallToAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F21276A32B600DC0649 /* CallToAction.swift */; }; 3706FBC6293F65D500E42796 /* MouseOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953D26F04BE70015B914 /* MouseOverView.swift */; }; - 3706FBC7293F65D500E42796 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; }; + 3706FBC7293F65D500E42796 /* EncryptedHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */; }; 3706FBC8293F65D500E42796 /* FirePopoverCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */; }; 3706FBC9293F65D500E42796 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */; }; 3706FBCA293F65D500E42796 /* CrashReportSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC30A2B268F1ECD00D2D9CD /* CrashReportSender.swift */; }; @@ -496,7 +495,6 @@ 3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CC1D7C26A05F250062F04E /* PasswordManagementItemModel.swift */; }; 3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A0118125AF60E700FA6A0C /* FindInPageModel.swift */; }; 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929826670D2A00AD2C21 /* PseudoFolder.swift */; }; - 3706FBF4293F65D500E42796 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E91992875B39300AB6B62 /* Visit.swift */; }; 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; 3706FBF6293F65D500E42796 /* Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E45226142B070067D1B9 /* Pixel.swift */; }; 3706FBF7293F65D500E42796 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E47626146A570067D1B9 /* PixelEvent.swift */; }; @@ -504,7 +502,6 @@ 3706FBF9293F65D500E42796 /* BookmarksBarCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE5336A286912D40019DBFD /* BookmarksBarCollectionViewItem.swift */; }; 3706FBFA293F65D500E42796 /* FileDownloadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23826E742610031CB7F /* FileDownloadError.swift */; }; 3706FBFB293F65D500E42796 /* MoreOrLessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E9F27BFE60E0038AD11 /* MoreOrLessView.swift */; }; - 3706FBFD293F65D500E42796 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46F26146A250067D1B9 /* DateExtension.swift */; }; 3706FBFE293F65D500E42796 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; }; 3706FBFF293F65D500E42796 /* PermissionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C853C26944B940048FEBE /* PermissionStore.swift */; }; 3706FC00293F65D500E42796 /* PrivacyIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */; }; @@ -562,7 +559,6 @@ 3706FC38293F65D500E42796 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; 3706FC3A293F65D500E42796 /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; }; 3706FC3B293F65D500E42796 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; - 3706FC3C293F65D500E42796 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; }; 3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50372726A12000758A2B /* VariantManager.swift */; }; 3706FC3F293F65D500E42796 /* ApplicationDockMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA97BF4525135DD30014931A /* ApplicationDockMenu.swift */; }; 3706FC40293F65D500E42796 /* SaveIdentityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8A4DFE27C83B29005F40E8 /* SaveIdentityViewController.swift */; }; @@ -608,7 +604,6 @@ 3706FC6E293F65D500E42796 /* DuckURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */; }; 3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13DCB3271480B0006D48D3 /* FirePopoverViewModel.swift */; }; 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; - 3706FC72293F65D500E42796 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919B2875C65000AB6B62 /* Stored.swift */; }; 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */; }; 3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; }; 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853014D525E671A000FB8205 /* PageObserverUserScript.swift */; }; @@ -1559,7 +1554,6 @@ 4B957A602AC7AE700062CA31 /* NSNotificationName+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */; }; 4B957A612AC7AE700062CA31 /* NavigationBarBadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F7F2A5288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift */; }; 4B957A622AC7AE700062CA31 /* AddressBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F025D6BF10007F5990 /* AddressBarButton.swift */; }; - 4B957A632AC7AE700062CA31 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; }; 4B957A642AC7AE700062CA31 /* FaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */; }; 4B957A652AC7AE700062CA31 /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B957A662AC7AE700062CA31 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; @@ -1664,7 +1658,7 @@ 4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */; }; 4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F21276A32B600DC0649 /* CallToAction.swift */; }; 4B957AD02AC7AE700062CA31 /* MouseOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953D26F04BE70015B914 /* MouseOverView.swift */; }; - 4B957AD12AC7AE700062CA31 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; }; + 4B957AD12AC7AE700062CA31 /* EncryptedHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */; }; 4B957AD22AC7AE700062CA31 /* FirePopoverCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */; }; 4B957AD32AC7AE700062CA31 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */; }; 4B957AD42AC7AE700062CA31 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; @@ -1717,7 +1711,6 @@ 4B957B042AC7AE700062CA31 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD86E51267A0DFF005C11BE /* UpdateController.swift */; }; 4B957B052AC7AE700062CA31 /* FindInPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A0118125AF60E700FA6A0C /* FindInPageModel.swift */; }; 4B957B062AC7AE700062CA31 /* PseudoFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929826670D2A00AD2C21 /* PseudoFolder.swift */; }; - 4B957B072AC7AE700062CA31 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E91992875B39300AB6B62 /* Visit.swift */; }; 4B957B082AC7AE700062CA31 /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; 4B957B092AC7AE700062CA31 /* WaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */; }; 4B957B0A2AC7AE700062CA31 /* Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E45226142B070067D1B9 /* Pixel.swift */; }; @@ -1727,7 +1720,6 @@ 4B957B0E2AC7AE700062CA31 /* BookmarksBarCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE5336A286912D40019DBFD /* BookmarksBarCollectionViewItem.swift */; }; 4B957B0F2AC7AE700062CA31 /* FileDownloadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23826E742610031CB7F /* FileDownloadError.swift */; }; 4B957B102AC7AE700062CA31 /* MoreOrLessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E9F27BFE60E0038AD11 /* MoreOrLessView.swift */; }; - 4B957B112AC7AE700062CA31 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46F26146A250067D1B9 /* DateExtension.swift */; }; 4B957B122AC7AE700062CA31 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; }; 4B957B132AC7AE700062CA31 /* PermissionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C853C26944B940048FEBE /* PermissionStore.swift */; }; 4B957B142AC7AE700062CA31 /* PrivacyIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */; }; @@ -1794,7 +1786,6 @@ 4B957B512AC7AE700062CA31 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; 4B957B522AC7AE700062CA31 /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; }; 4B957B532AC7AE700062CA31 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; - 4B957B542AC7AE700062CA31 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; }; 4B957B552AC7AE700062CA31 /* NetworkProtectionOnboardingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */; }; 4B957B562AC7AE700062CA31 /* VariantManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50372726A12000758A2B /* VariantManager.swift */; }; 4B957B572AC7AE700062CA31 /* ApplicationDockMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA97BF4525135DD30014931A /* ApplicationDockMenu.swift */; }; @@ -1856,7 +1847,6 @@ 4B957B932AC7AE700062CA31 /* FirePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13DCB3271480B0006D48D3 /* FirePopoverViewModel.swift */; }; 4B957B942AC7AE700062CA31 /* BWCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB37292B636E0065E5D6 /* BWCommand.swift */; }; 4B957B952AC7AE700062CA31 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; - 4B957B962AC7AE700062CA31 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919B2875C65000AB6B62 /* Stored.swift */; }; 4B957B972AC7AE700062CA31 /* AddressBarButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */; }; 4B957B982AC7AE700062CA31 /* BWError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF076028F815AD00EDFBE3 /* BWError.swift */; }; 4B957B9A2AC7AE700062CA31 /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; }; @@ -2359,10 +2349,16 @@ 85C6A29625CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; 85CC1D7B26A05ECF0062F04E /* PasswordManagementItemListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CC1D7A26A05ECF0062F04E /* PasswordManagementItemListModel.swift */; }; 85CC1D7D26A05F250062F04E /* PasswordManagementItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CC1D7C26A05F250062F04E /* PasswordManagementItemModel.swift */; }; + 85D0327B2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; }; + 85D0327C2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; }; + 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; }; 85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D33F1125C82EB3002B91A6 /* ConfigurationManager.swift */; }; 85D438B6256E7C9E00F3BAF8 /* ContextMenuUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */; }; 85D885B026A590A90077C374 /* NSNotificationName+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885AF26A590A90077C374 /* NSNotificationName+PasswordManager.swift */; }; 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885B226A5A9DE0077C374 /* NSAlert+PasswordManager.swift */; }; + 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBCD2B8F534000DBEC7A /* History */; }; + 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBCF2B8F534A00DBEC7A /* History */; }; + 85E2BBD22B8F536F00DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBD12B8F536F00DBEC7A /* History */; }; 85F0FF1327CFAB04001C7C6E /* RecentlyVisitedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0FF1227CFAB04001C7C6E /* RecentlyVisitedView.swift */; }; 85F1B0C925EF9759004792B6 /* URLEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1B0C825EF9759004792B6 /* URLEventHandlerTests.swift */; }; 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F487B4276A8F2E003CE668 /* OnboardingTests.swift */; }; @@ -2493,8 +2489,6 @@ AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */; }; AA7E9176286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E9175286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift */; }; AA7E919728746BCC00AB6B62 /* HistoryMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */; }; - AA7E919A2875B39300AB6B62 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E91992875B39300AB6B62 /* Visit.swift */; }; - AA7E919C2875C65000AB6B62 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919B2875C65000AB6B62 /* Stored.swift */; }; AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919E287872EA00AB6B62 /* VisitViewModel.swift */; }; AA7EB6DF27E7C57D00036718 /* MouseOverAnimationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7EB6DE27E7C57D00036718 /* MouseOverAnimationButton.swift */; }; AA7EB6E227E7D05500036718 /* flame-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6E027E7D05500036718 /* flame-mouse-over.json */; }; @@ -2572,9 +2566,7 @@ AAE246F8270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */; }; AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE39D1A24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift */; }; AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; }; - AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; }; - AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; }; - AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; }; + AAE7527C263B056C00B973F8 /* EncryptedHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */; }; AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */; }; @@ -2902,7 +2894,6 @@ B6A924D92664C72E001A28CA /* WebKitDownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */; }; B6A9E45326142B070067D1B9 /* Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E45226142B070067D1B9 /* Pixel.swift */; }; B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */; }; - B6A9E47026146A250067D1B9 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46F26146A250067D1B9 /* DateExtension.swift */; }; B6A9E47726146A570067D1B9 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E47626146A570067D1B9 /* PixelEvent.swift */; }; B6A9E47F26146A800067D1B9 /* PixelArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E47E26146A800067D1B9 /* PixelArguments.swift */; }; B6A9E48426146AAB0067D1B9 /* PixelParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E48326146AAB0067D1B9 /* PixelParameters.swift */; }; @@ -3921,6 +3912,7 @@ 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWrapper.swift; sourceTree = ""; }; 85CC1D7A26A05ECF0062F04E /* PasswordManagementItemListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagementItemListModel.swift; sourceTree = ""; }; 85CC1D7C26A05F250062F04E /* PasswordManagementItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagementItemModel.swift; sourceTree = ""; }; + 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatorExtension.swift; sourceTree = ""; }; 85D33F1125C82EB3002B91A6 /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuUserScript.swift; sourceTree = ""; }; 85D885AF26A590A90077C374 /* NSNotificationName+PasswordManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNotificationName+PasswordManager.swift"; sourceTree = ""; }; @@ -4043,8 +4035,6 @@ AA7E9175286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyClosedCoordinatorMock.swift; sourceTree = ""; }; AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryMenu.swift; sourceTree = ""; }; AA7E91982875AB4700AB6B62 /* History 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "History 6.xcdatamodel"; sourceTree = ""; }; - AA7E91992875B39300AB6B62 /* Visit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Visit.swift; sourceTree = ""; }; - AA7E919B2875C65000AB6B62 /* Stored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stored.swift; sourceTree = ""; }; AA7E919E287872EA00AB6B62 /* VisitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitViewModel.swift; sourceTree = ""; }; AA7EB6DE27E7C57D00036718 /* MouseOverAnimationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseOverAnimationButton.swift; sourceTree = ""; }; AA7EB6E027E7D05500036718 /* flame-mouse-over.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "flame-mouse-over.json"; sourceTree = ""; }; @@ -4124,9 +4114,7 @@ AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverCollectionViewHeader.swift; sourceTree = ""; }; AAE39D1A24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollectionViewModelDelegateMock.swift; sourceTree = ""; }; AAE75279263B046100B973F8 /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = ""; }; - AAE7527B263B056C00B973F8 /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; }; - AAE7527D263B05C600B973F8 /* HistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntry.swift; sourceTree = ""; }; - AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = ""; }; + AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryStore.swift; sourceTree = ""; }; AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewViewController.swift; sourceTree = ""; }; AAE99B8827088A19008B6BD9 /* FirePopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopover.swift; sourceTree = ""; }; AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatingMock.swift; sourceTree = ""; }; @@ -4338,7 +4326,6 @@ B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitDownloadTask.swift; sourceTree = ""; }; B6A9E45226142B070067D1B9 /* Pixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pixel.swift; sourceTree = ""; }; B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingSystemVersionExtension.swift; sourceTree = ""; }; - B6A9E46F26146A250067D1B9 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; B6A9E47626146A570067D1B9 /* PixelEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelEvent.swift; sourceTree = ""; }; B6A9E47E26146A800067D1B9 /* PixelArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelArguments.swift; sourceTree = ""; }; B6A9E48326146AAB0067D1B9 /* PixelParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelParameters.swift; sourceTree = ""; }; @@ -4492,6 +4479,7 @@ 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, 3706FCAB293F65D500E42796 /* TrackerRadarKit in Frameworks */, + 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, 3739326529AE4B39009346AE /* DDGSync in Frameworks */, 37DF000729F9C061002B7D3E /* SyncDataProviders in Frameworks */, @@ -4625,6 +4613,7 @@ 1E21F8E32B73E48600FB272E /* Subscription in Frameworks */, 4B957BE42AC7AE700062CA31 /* DDGSync in Frameworks */, 4B957BE52AC7AE700062CA31 /* OpenSSL in Frameworks */, + 85E2BBD22B8F536F00DBEC7A /* History in Frameworks */, 4B957BE62AC7AE700062CA31 /* PrivacyDashboard in Frameworks */, 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */, 4B957BE72AC7AE700062CA31 /* SyncDataProviders in Frameworks */, @@ -4687,6 +4676,7 @@ buildActionMask = 2147483647; files = ( 373FB4B12B4D6C42004C88D6 /* PreferencesViews in Frameworks */, + 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */, @@ -5620,7 +5610,6 @@ children = ( 4B677440255DBEEA00025BD8 /* Database.swift */, B6085D052743905F00A9C456 /* CoreDataStore.swift */, - AA7E919B2875C65000AB6B62 /* Stored.swift */, ); path = Database; sourceTree = ""; @@ -7582,7 +7571,6 @@ 4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */, B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */, 85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */, - B6A9E46F26146A250067D1B9 /* DateExtension.swift */, B6040855274B830F00680351 /* DictionaryExtension.swift */, B63D467025BFA6C100874977 /* DispatchQueueExtensions.swift */, 4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */, @@ -7695,7 +7683,6 @@ isa = PBXGroup; children = ( AA7E919D287872DB00AB6B62 /* ViewModel */, - AAE75277263B038F00B973F8 /* Model */, AAE75276263B038A00B973F8 /* Services */, ); path = History; @@ -7705,21 +7692,12 @@ isa = PBXGroup; children = ( AAE75278263B046100B973F8 /* History.xcdatamodeld */, - AAE7527B263B056C00B973F8 /* HistoryStore.swift */, + AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */, + 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */, ); path = Services; sourceTree = ""; }; - AAE75277263B038F00B973F8 /* Model */ = { - isa = PBXGroup; - children = ( - AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */, - AAE7527D263B05C600B973F8 /* HistoryEntry.swift */, - AA7E91992875B39300AB6B62 /* Visit.swift */, - ); - path = Model; - sourceTree = ""; - }; AAE8B0FD258A416F00E81239 /* TabPreview */ = { isa = PBXGroup; children = ( @@ -8353,6 +8331,7 @@ 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */, 312978892B64131200B67619 /* DataBrokerProtection */, 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, + 85E2BBCF2B8F534A00DBEC7A /* History */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8632,6 +8611,7 @@ 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, 1E21F8E22B73E48600FB272E /* Subscription */, 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, + 85E2BBD12B8F536F00DBEC7A /* History */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8794,6 +8774,7 @@ 3722177F2B3337FE00B8E9C2 /* TestUtils */, 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, + 85E2BBCD2B8F534000DBEC7A /* History */, 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */, 1EA7B8D42B7E078C000330A4 /* Subscription */, ); @@ -9894,7 +9875,6 @@ 1D1A334A2A6FEB170080ACED /* BurnerMode.swift in Sources */, B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, - 3706FB6B293F65D500E42796 /* HistoryEntry.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, @@ -10029,7 +10009,7 @@ 56D145EC29E6C99B00E3488A /* DataImportStatusProviding.swift in Sources */, 85774B002A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */, B602E81E2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */, - 3706FBC7293F65D500E42796 /* HistoryStore.swift in Sources */, + 3706FBC7293F65D500E42796 /* EncryptedHistoryStore.swift in Sources */, 3706FBC8293F65D500E42796 /* FirePopoverCollectionViewItem.swift in Sources */, 3706FBC9293F65D500E42796 /* ArrayExtension.swift in Sources */, 3706FBCA293F65D500E42796 /* CrashReportSender.swift in Sources */, @@ -10094,7 +10074,6 @@ 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, - 3706FBF4293F65D500E42796 /* Visit.swift in Sources */, 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */, 3706FBF6293F65D500E42796 /* Pixel.swift in Sources */, 3706FBF7293F65D500E42796 /* PixelEvent.swift in Sources */, @@ -10104,7 +10083,6 @@ 3706FBFA293F65D500E42796 /* FileDownloadError.swift in Sources */, 379E877729E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */, 3706FBFB293F65D500E42796 /* MoreOrLessView.swift in Sources */, - 3706FBFD293F65D500E42796 /* DateExtension.swift in Sources */, 987799FA29999973005D8EB6 /* LocalBookmarkStore.swift in Sources */, B602E7D02A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, 3706FBFE293F65D500E42796 /* History.xcdatamodeld in Sources */, @@ -10194,7 +10172,6 @@ 3706FC3A293F65D500E42796 /* NSOpenPanelExtensions.swift in Sources */, 3706FC3B293F65D500E42796 /* FirePopover.swift in Sources */, 4B4D60C12A0C848E00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, - 3706FC3C293F65D500E42796 /* HistoryCoordinator.swift in Sources */, 3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */, 3706FC3F293F65D500E42796 /* ApplicationDockMenu.swift in Sources */, B68412152B694BA10092F66A /* NSObject+performSelector.m in Sources */, @@ -10204,6 +10181,7 @@ 1DB67F2E2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */, 3706FC42293F65D500E42796 /* PixelArguments.swift in Sources */, 3706FC43293F65D500E42796 /* PinnedTabsViewModel.swift in Sources */, + 85D0327C2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, B6685E4329A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, 3706FC44293F65D500E42796 /* BookmarkList.swift in Sources */, 3706FC45293F65D500E42796 /* BookmarkTableRowView.swift in Sources */, @@ -10263,7 +10241,6 @@ 37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */, 3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */, 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */, - 3706FC72293F65D500E42796 /* Stored.swift in Sources */, 1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */, 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, 3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */, @@ -11011,6 +10988,7 @@ 4B957A222AC7AE700062CA31 /* FirefoxBookmarksReader.swift in Sources */, 4B0526622B1D55320054955A /* VPNFeedbackSender.swift in Sources */, 4B957A232AC7AE700062CA31 /* DeviceIdleStateDetector.swift in Sources */, + 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, 4B957A242AC7AE700062CA31 /* FlatButton.swift in Sources */, 4B957A252AC7AE700062CA31 /* PinnedTabView.swift in Sources */, 4B957A262AC7AE700062CA31 /* DataEncryption.swift in Sources */, @@ -11081,7 +11059,6 @@ 4B957A602AC7AE700062CA31 /* NSNotificationName+Debug.swift in Sources */, 4B957A612AC7AE700062CA31 /* NavigationBarBadgeAnimationView.swift in Sources */, 4B957A622AC7AE700062CA31 /* AddressBarButton.swift in Sources */, - 4B957A632AC7AE700062CA31 /* HistoryEntry.swift in Sources */, 4B957A642AC7AE700062CA31 /* FaviconStore.swift in Sources */, 4B957A652AC7AE700062CA31 /* WaitlistTermsAndConditionsView.swift in Sources */, B62B48592ADE730D000DECE5 /* FileImportView.swift in Sources */, @@ -11203,7 +11180,7 @@ 4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */, 4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */, 4B957AD02AC7AE700062CA31 /* MouseOverView.swift in Sources */, - 4B957AD12AC7AE700062CA31 /* HistoryStore.swift in Sources */, + 4B957AD12AC7AE700062CA31 /* EncryptedHistoryStore.swift in Sources */, 4B957AD22AC7AE700062CA31 /* FirePopoverCollectionViewItem.swift in Sources */, 4B957AD32AC7AE700062CA31 /* ArrayExtension.swift in Sources */, 4B957AD42AC7AE700062CA31 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, @@ -11260,7 +11237,6 @@ 4B957B042AC7AE700062CA31 /* UpdateController.swift in Sources */, 4B957B052AC7AE700062CA31 /* FindInPageModel.swift in Sources */, 4B957B062AC7AE700062CA31 /* PseudoFolder.swift in Sources */, - 4B957B072AC7AE700062CA31 /* Visit.swift in Sources */, 4B2F565D2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 4B957B082AC7AE700062CA31 /* PixelDataStore.swift in Sources */, 4B957B092AC7AE700062CA31 /* WaitlistStorage.swift in Sources */, @@ -11273,7 +11249,6 @@ B69A14FC2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */, 4B957B0F2AC7AE700062CA31 /* FileDownloadError.swift in Sources */, 4B957B102AC7AE700062CA31 /* MoreOrLessView.swift in Sources */, - 4B957B112AC7AE700062CA31 /* DateExtension.swift in Sources */, 4B957B122AC7AE700062CA31 /* History.xcdatamodeld in Sources */, 4B957B132AC7AE700062CA31 /* PermissionStore.swift in Sources */, EEC4A6612B277F1100F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -11350,7 +11325,6 @@ 4B957B522AC7AE700062CA31 /* NSOpenPanelExtensions.swift in Sources */, EEC4A66F2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 4B957B532AC7AE700062CA31 /* FirePopover.swift in Sources */, - 4B957B542AC7AE700062CA31 /* HistoryCoordinator.swift in Sources */, 4B957B552AC7AE700062CA31 /* NetworkProtectionOnboardingMenu.swift in Sources */, 4B957B562AC7AE700062CA31 /* VariantManager.swift in Sources */, 4B957B572AC7AE700062CA31 /* ApplicationDockMenu.swift in Sources */, @@ -11429,7 +11403,6 @@ 4B957B932AC7AE700062CA31 /* FirePopoverViewModel.swift in Sources */, 4B957B942AC7AE700062CA31 /* BWCommand.swift in Sources */, 4B957B952AC7AE700062CA31 /* NSColorExtension.swift in Sources */, - 4B957B962AC7AE700062CA31 /* Stored.swift in Sources */, 4B957B972AC7AE700062CA31 /* AddressBarButtonsViewController.swift in Sources */, 4B957B982AC7AE700062CA31 /* BWError.swift in Sources */, 4B957B9A2AC7AE700062CA31 /* PixelDataRecord.swift in Sources */, @@ -11586,6 +11559,7 @@ B66CA41E2AD910B300447CF0 /* DataImportView.swift in Sources */, B637273D26CCF0C200C8CB02 /* OptionalExtension.swift in Sources */, 4BE65477271FCD41008D1D63 /* PasswordManagementLoginItemView.swift in Sources */, + 85D0327B2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, AA80EC54256BE3BC007083E7 /* UserText.swift in Sources */, B61EF3EC266F91E700B4D78F /* WKWebView+Download.swift in Sources */, 311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */, @@ -11862,7 +11836,6 @@ B690152C2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */, 31F7F2A6288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift in Sources */, AAC5E4F125D6BF10007F5990 /* AddressBarButton.swift in Sources */, - AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */, AA5FA69D275F945C00DCE9C9 /* FaviconStore.swift in Sources */, 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */, @@ -11991,7 +11964,7 @@ AA585D84248FD31100E9A3E2 /* BrowserTabViewController.swift in Sources */, 85707F22276A32B600DC0649 /* CallToAction.swift in Sources */, B693954B26F04BEB0015B914 /* MouseOverView.swift in Sources */, - AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */, + AAE7527C263B056C00B973F8 /* EncryptedHistoryStore.swift in Sources */, AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */, 4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */, @@ -12051,7 +12024,6 @@ 85A0118225AF60E700FA6A0C /* FindInPageModel.swift in Sources */, 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, - AA7E919A2875B39300AB6B62 /* Visit.swift in Sources */, 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, B6DA44022616B28300DD1EC2 /* PixelDataStore.swift in Sources */, @@ -12063,7 +12035,6 @@ 4BE5336C286912D40019DBFD /* BookmarksBarCollectionViewItem.swift in Sources */, B6C0B23926E742610031CB7F /* FileDownloadError.swift in Sources */, 85589EA027BFE60E0038AD11 /* MoreOrLessView.swift in Sources */, - B6A9E47026146A250067D1B9 /* DateExtension.swift in Sources */, AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */, B64C853D26944B940048FEBE /* PermissionStore.swift in Sources */, AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */, @@ -12143,7 +12114,6 @@ B6DB3AF6278EA0130024C5C4 /* BundleExtension.swift in Sources */, 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */, AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */, - AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */, 4B0BD7B72A9FE6E500EF609D /* NetworkProtectionOnboardingMenu.swift in Sources */, B69B503D2726A12500758A2B /* VariantManager.swift in Sources */, AA97BF4625135DD30014931A /* ApplicationDockMenu.swift in Sources */, @@ -12210,7 +12180,6 @@ AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */, 1D43EB38292B636E0065E5D6 /* BWCommand.swift in Sources */, F41D174125CB131900472416 /* NSColorExtension.swift in Sources */, - AA7E919C2875C65000AB6B62 /* Stored.swift in Sources */, AAC5E4F625D6BF2C007F5990 /* AddressBarButtonsViewController.swift in Sources */, B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */, 1D2DC0072901679C008083A1 /* BWError.swift in Sources */, @@ -13583,7 +13552,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 113.0.0; + version = 114.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -14093,6 +14062,21 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 85E2BBCD2B8F534000DBEC7A /* History */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = History; + }; + 85E2BBCF2B8F534A00DBEC7A /* History */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = History; + }; + 85E2BBD12B8F536F00DBEC7A /* History */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = History; + }; 9807F644278CA16F00E1547B /* BrowserServicesKit */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b43fb91512..fb698a0306 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "f903ffcbc51e85ac262c355b56726e3387957a80", - "version" : "113.0.0" + "revision" : "c9eae1b4a4ce5b6854d6934e57f900f62d2f3917", + "version" : "114.0.0" } }, { @@ -156,7 +156,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 54df1cd906..5fe69f607f 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -30,6 +30,7 @@ import ServiceManagement import SyncDataProviders import UserNotifications import PixelKit +import History #if NETWORK_PROTECTION import NetworkProtection @@ -212,7 +213,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel didFinishLaunching = true } - HistoryCoordinator.shared.loadHistory() + HistoryCoordinator.shared.loadHistory { + HistoryCoordinator.shared.migrateModelV5toV6IfNeeded() + } + PrivacyFeatures.httpsUpgrade.loadDataAsync() bookmarksManager.loadBookmarks() if case .normal = NSApp.runType { diff --git a/DuckDuckGo/Common/Database/Stored.swift b/DuckDuckGo/Common/Database/Stored.swift deleted file mode 100644 index 69f2c40de1..0000000000 --- a/DuckDuckGo/Common/Database/Stored.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Stored.swift -// -// Copyright © 2022 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -internal class Stored { - - var savingState = SavingState.initialized - - enum SavingState { - case initialized - case saved - } - -} diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift index 25f340d0ae..e6c9d18b36 100644 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ b/DuckDuckGo/Common/Logging/Logging.swift @@ -26,7 +26,6 @@ extension OSLog { case atb = "ATB" case config = "Configuration Downloading" case fire = "Fire" - case history = "History" case dataImportExport = "Data Import/Export" case pixel = "Pixel" case contentBlocking = "Content Blocking" @@ -55,7 +54,6 @@ extension OSLog { @OSLogWrapper(.atb) static var atb @OSLogWrapper(.config) static var config @OSLogWrapper(.fire) static var fire - @OSLogWrapper(.history) static var history @OSLogWrapper(.dataImportExport) static var dataImportExport @OSLogWrapper(.pixel) static var pixel @OSLogWrapper(.contentBlocking) static var contentBlocking diff --git a/DuckDuckGo/Favicons/Model/FaviconManager.swift b/DuckDuckGo/Favicons/Model/FaviconManager.swift index 28203bafb7..87842b8e88 100644 --- a/DuckDuckGo/Favicons/Model/FaviconManager.swift +++ b/DuckDuckGo/Favicons/Model/FaviconManager.swift @@ -21,6 +21,7 @@ import Cocoa import Combine import BrowserServicesKit import Common +import History @MainActor protocol FaviconManagement: AnyObject { diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index f67d4072e4..29c2ca70f0 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -23,6 +23,7 @@ import DDGSync import PrivacyDashboard import WebKit import SecureStorage +import History final class Fire { diff --git a/DuckDuckGo/Fire/View/FirePopoverViewController.swift b/DuckDuckGo/Fire/View/FirePopoverViewController.swift index abf613a442..99376f1039 100644 --- a/DuckDuckGo/Fire/View/FirePopoverViewController.swift +++ b/DuckDuckGo/Fire/View/FirePopoverViewController.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import Common +import History protocol FirePopoverViewControllerDelegate: AnyObject { diff --git a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift index c9209c528a..3ed92a3ec1 100644 --- a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift +++ b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift @@ -19,6 +19,7 @@ import Cocoa import BrowserServicesKit import Common +import History @MainActor final class FirePopoverViewModel { diff --git a/DuckDuckGo/History/Model/History.swift b/DuckDuckGo/History/Model/History.swift deleted file mode 100644 index 6ae4eeded0..0000000000 --- a/DuckDuckGo/History/Model/History.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// History.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -typealias History = Set diff --git a/DuckDuckGo/History/Model/HistoryCoordinator.swift b/DuckDuckGo/History/Model/HistoryCoordinator.swift deleted file mode 100644 index 9954e13af1..0000000000 --- a/DuckDuckGo/History/Model/HistoryCoordinator.swift +++ /dev/null @@ -1,375 +0,0 @@ -// -// HistoryCoordinator.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Combine -import Common -import BrowserServicesKit - -typealias History = [HistoryEntry] - -protocol HistoryCoordinating: AnyObject { - - var history: History? { get } - var allHistoryVisits: [Visit]? { get } - var historyDictionaryPublisher: Published<[URL: HistoryEntry]?>.Publisher { get } - - func addVisit(of url: URL) -> Visit? - func addBlockedTracker(entityName: String, on url: URL) - func trackerFound(on: URL) - func updateTitleIfNeeded(title: String, url: URL) - func markFailedToLoadUrl(_ url: URL) - func commitChanges(url: URL) - - func title(for url: URL) -> String? - - func burnAll(completion: @escaping () -> Void) - func burnDomains(_ baseDomains: Set, tld: TLD, completion: @escaping () -> Void) - func burnVisits(_ visits: [Visit], completion: @escaping () -> Void) - -} - -/// Coordinates access to History. Uses its own queue with high qos for all operations. -final class HistoryCoordinator: HistoryCoordinating { - static let shared = HistoryCoordinator() - - init() {} - - init(historyStoring: HistoryStoring) { - self.historyStoring = historyStoring - historyDictionary = [:] - } - - func loadHistory() { - cleanOldAndLoad { [weak self] _ in - self?.migrateModelV5toV6IfNeeded() - } - scheduleRegularCleaning() - } - - private lazy var historyStoring: HistoryStoring = HistoryStore() - private var regularCleaningTimer: Timer? - - // Source of truth - @Published private(set) var historyDictionary: [URL: HistoryEntry]? - var historyDictionaryPublisher: Published<[URL: HistoryEntry]?>.Publisher { $historyDictionary } - - // Output - var history: History? { - guard let historyDictionary = historyDictionary else { - return nil - } - - return makeHistory(from: historyDictionary) - } - - var allHistoryVisits: [Visit]? { - history?.flatMap { $0.visits } - } - - private var cancellables = Set() - - @discardableResult func addVisit(of url: URL) -> Visit? { - guard let historyDictionary = historyDictionary else { - os_log("Visit of %s ignored", log: .history, url.absoluteString) - return nil - } - - let entry = historyDictionary[url] ?? HistoryEntry(url: url) - let visit = entry.addVisit() - entry.failedToLoad = false - - self.historyDictionary?[url] = entry - - commitChanges(url: url) - return visit - } - - func addBlockedTracker(entityName: String, on url: URL) { - guard let historyDictionary = historyDictionary else { - os_log("Add tracker to %s ignored, no history", log: .history, url.absoluteString) - return - } - - guard let entry = historyDictionary[url] else { - os_log("Add tracker to %s ignored, no entry", log: .history, url.absoluteString) - return - } - - entry.addBlockedTracker(entityName: entityName) - } - - func trackerFound(on url: URL) { - guard let historyDictionary = historyDictionary else { - os_log("Add tracker to %s ignored, no history", log: .history, url.absoluteString) - return - } - - guard let entry = historyDictionary[url] else { - os_log("Add tracker to %s ignored, no entry", log: .history, url.absoluteString) - return - } - - entry.trackersFound = true - } - - func updateTitleIfNeeded(title: String, url: URL) { - guard let historyDictionary = historyDictionary else { return } - guard let entry = historyDictionary[url] else { - os_log("Title update ignored - URL not part of history yet", log: .history, type: .debug) - return - } - guard !title.isEmpty, entry.title != title else { return } - - entry.title = title - } - - func markFailedToLoadUrl(_ url: URL) { - mark(url: url, keyPath: \HistoryEntry.failedToLoad, value: true) - } - - func commitChanges(url: URL) { - guard let historyDictionary = historyDictionary, - let entry = historyDictionary[url] else { - return - } - - save(entry: entry) - } - - func title(for url: URL) -> String? { - guard let historyEntry = historyDictionary?[url] else { - return nil - } - - return historyEntry.title - } - - func burnAll(completion: @escaping () -> Void) { - guard let historyDictionary = historyDictionary else { return } - - let entries = Array(historyDictionary.values) - - removeEntries(entries, completionHandler: { _ in - completion() - }) - } - - func burnDomains(_ baseDomains: Set, tld: TLD, completion: @escaping () -> Void) { - guard let historyDictionary = historyDictionary else { return } - - let entries: [HistoryEntry] = historyDictionary.values.filter { historyEntry in - guard let host = historyEntry.url.host, let baseDomain = tld.eTLDplus1(host) else { - return false - } - - return baseDomains.contains(baseDomain) - } - - removeEntries(entries, completionHandler: { _ in - completion() - }) - } - - func burnVisits(_ visits: [Visit], completion: @escaping () -> Void) { - removeVisits(visits) { _ in - completion() - } - } - - var cleaningDate: Date { .monthAgo } - - @objc private func cleanOld() { - clean(until: cleaningDate) - } - - private func cleanOldAndLoad(completionHandler: ((Error?) -> Void)?) { - clean(until: cleaningDate, completionHandler: completionHandler) - } - - private func clean(until date: Date, - completionHandler: ((Error?) -> Void)? = nil) { - historyStoring.cleanOld(until: date) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - os_log("History cleaned successfully", log: .history) - completionHandler?(nil) - case .failure(let error): - os_log("Cleaning of history failed: %s", log: .history, type: .error, error.localizedDescription) - completionHandler?(error) - } - }, receiveValue: { [weak self] history in - self?.historyDictionary = self?.makeHistoryDictionary(from: history) - }) - .store(in: &cancellables) - } - - private func removeEntries(_ entries: [HistoryEntry], - completionHandler: ((Error?) -> Void)? = nil) { - // Remove from the local memory - entries.forEach { entry in - historyDictionary?.removeValue(forKey: entry.url) - } - - // Remove from the storage - historyStoring.removeEntries(entries) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - os_log("Entries removed successfully", log: .history) - completionHandler?(nil) - case .failure(let error): - assertionFailure("Removal failed") - os_log("Removal failed: %s", log: .history, type: .error, error.localizedDescription) - completionHandler?(error) - } - }, receiveValue: {}) - .store(in: &cancellables) - } - - private func removeVisits(_ visits: [Visit], - completionHandler: ((Error?) -> Void)? = nil) { - var entriesToRemove = [HistoryEntry]() - - // Remove from the local memory - visits.forEach { visit in - if let historyEntry = visit.historyEntry { - historyEntry.visits.remove(visit) - - if historyEntry.visits.count > 0 { - if let newLastVisit = historyEntry.visits.map({ $0.date }).max() { - historyEntry.lastVisit = newLastVisit - save(entry: historyEntry) - } else { - assertionFailure("No history entry") - } - } else { - entriesToRemove.append(historyEntry) - } - } else { - assertionFailure("No history entry") - } - } - - // Remove from the storage - historyStoring.removeVisits(visits) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - switch completion { - case .finished: - os_log("Visits removed successfully", log: .history) - // Remove entries with no remaining visits - self?.removeEntries(entriesToRemove, completionHandler: completionHandler) - case .failure(let error): - assertionFailure("Removal failed") - os_log("Removal failed: %s", log: .history, type: .error, error.localizedDescription) - completionHandler?(error) - } - }, receiveValue: {}) - .store(in: &cancellables) - } - - private func scheduleRegularCleaning() { - let timer = Timer(fireAt: .startOfDayTomorrow, - interval: .day, - target: self, - selector: #selector(cleanOld), - userInfo: nil, - repeats: true) - RunLoop.main.add(timer, forMode: RunLoop.Mode.common) - regularCleaningTimer = timer - } - - private func makeHistoryDictionary(from history: History) -> [URL: HistoryEntry] { - dispatchPrecondition(condition: .onQueue(.main)) - - return history.reduce(into: [URL: HistoryEntry](), { $0[$1.url] = $1 }) - } - - private func makeHistory(from dictionary: [URL: HistoryEntry]) -> History { - dispatchPrecondition(condition: .onQueue(.main)) - - return History(dictionary.values) - } - - private func save(entry: HistoryEntry) { - guard let entryCopy = entry.copy() as? HistoryEntry else { - assertionFailure("Copying HistoryEntry failed") - return - } - entry.visits.forEach { $0.savingState = .saved } - - historyStoring.save(entry: entryCopy) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { completion in - switch completion { - case .finished: - os_log("Visit entry updated successfully. URL: %s, Title: %s, Number of visits: %d, failed to load: %s", - log: .history, - entry.url.absoluteString, - entry.title ?? "", - entry.numberOfTotalVisits, - entry.failedToLoad ? "yes" : "no") - case .failure(let error): - os_log("Saving of history entry failed: %s", log: .history, type: .error, error.localizedDescription) - } - }, receiveValue: { result in - for (id, date) in result { - if let visit = entry.visits.first(where: { $0.date == date }) { - visit.identifier = id - } - } - }) - .store(in: &cancellables) - } - - /// Sets boolean value for the keyPath in HistroryEntry for the specified url - /// Does the same for the root URL if it has no visits - private func mark(url: URL, keyPath: WritableKeyPath, value: Bool) { - guard let historyDictionary = historyDictionary, var entry = historyDictionary[url] else { - os_log("Marking of %s not saved. History not loaded yet or entry doesn't exist", - log: .history, url.absoluteString) - return - } - - entry[keyPath: keyPath] = value - } - - // V5 to V6 custom migration - - @UserDefaultsWrapper(key: .historyV5toV6Migration, defaultValue: false) - private var historyV5toV6Migration: Bool - - private func migrateModelV5toV6IfNeeded() { - guard let historyDictionary = historyDictionary, - !historyV5toV6Migration else { - return - } - - historyV5toV6Migration = true - - for entry in historyDictionary.values where entry.visits.isEmpty { - entry.addOldVisit(date: entry.lastVisit) - save(entry: entry) - } - } - -} diff --git a/DuckDuckGo/History/Model/HistoryEntry.swift b/DuckDuckGo/History/Model/HistoryEntry.swift deleted file mode 100644 index a3c3dc78b6..0000000000 --- a/DuckDuckGo/History/Model/HistoryEntry.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// HistoryEntry.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import BrowserServicesKit - -final class HistoryEntry { - - init(identifier: UUID, - url: URL, - title: String? = nil, - failedToLoad: Bool, - numberOfTotalVisits: Int, - lastVisit: Date, - visits: Set, - numberOfTrackersBlocked: Int, - blockedTrackingEntities: Set, - trackersFound: Bool) { - self.identifier = identifier - self.url = url - self.title = title - self.failedToLoad = failedToLoad - self.numberOfTotalVisits = numberOfTotalVisits - self.lastVisit = lastVisit - self.visits = visits - self.numberOfTrackersBlocked = numberOfTrackersBlocked - self.blockedTrackingEntities = blockedTrackingEntities - self.trackersFound = trackersFound - } - - let identifier: UUID - let url: URL - var title: String? - var failedToLoad: Bool - - // MARK: - Visits - - // Kept here because of migration. Can be used as computed property once visits of HistoryEntryMO are filled with all necessary info - // (In use for 1 month by majority of users) - var numberOfTotalVisits: Int - var lastVisit: Date - - var visits: Set - - func addVisit() -> Visit { - let visit = Visit(date: Date(), historyEntry: self) - visits.insert(visit) - - numberOfTotalVisits += 1 - lastVisit = Date() - - return visit - } - - // Used for migration - func addOldVisit(date: Date) { - let visit = Visit(date: date, historyEntry: self) - visits.insert(visit) - } - - // MARK: - Tracker blocking info - - var numberOfTrackersBlocked: Int - var blockedTrackingEntities: Set - var trackersFound: Bool - - func addBlockedTracker(entityName: String) { - numberOfTrackersBlocked += 1 - - guard !entityName.trimmingWhitespace().isEmpty else { - return - } - blockedTrackingEntities.insert(entityName) - } - -} - -extension HistoryEntry { - - convenience init(url: URL) { - self.init(identifier: UUID(), - url: url, - title: nil, - failedToLoad: false, - numberOfTotalVisits: 0, - lastVisit: Date.startOfMinuteNow, - visits: Set(), - numberOfTrackersBlocked: 0, - blockedTrackingEntities: Set(), - trackersFound: false) - } - -} - -extension HistoryEntry: Hashable { - - static func == (lhs: HistoryEntry, rhs: HistoryEntry) -> Bool { - lhs === rhs - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - -} - -extension HistoryEntry: Identifiable {} - -extension HistoryEntry: NSCopying { - - func copy(with zone: NSZone? = nil) -> Any { - let visits = visits.compactMap { $0.copy() as? Visit } - let entry = HistoryEntry(identifier: identifier, - url: url, - title: title, - failedToLoad: failedToLoad, - numberOfTotalVisits: numberOfTotalVisits, - lastVisit: lastVisit, - visits: Set(visits), - numberOfTrackersBlocked: numberOfTrackersBlocked, - blockedTrackingEntities: blockedTrackingEntities, - trackersFound: trackersFound) - entry.visits.forEach { $0.historyEntry = entry } - return entry - } - -} diff --git a/DuckDuckGo/History/Model/Visit.swift b/DuckDuckGo/History/Model/Visit.swift deleted file mode 100644 index 6b646a1197..0000000000 --- a/DuckDuckGo/History/Model/Visit.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Visit.swift -// -// Copyright © 2022 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -final class Visit: Stored { - - typealias ID = URL - - init(date: Date, identifier: ID? = nil, historyEntry: HistoryEntry? = nil) { - self.date = date - self.identifier = identifier - self.historyEntry = historyEntry - } - - let date: Date - - var identifier: ID? - weak var historyEntry: HistoryEntry? - -} - -extension Visit: Hashable { - - static func == (lhs: Visit, rhs: Visit) -> Bool { - lhs === rhs - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - -} - -extension Visit: NSCopying { - - func copy(with zone: NSZone? = nil) -> Any { - let visit = Visit(date: date, - identifier: identifier, - historyEntry: nil) - visit.savingState = savingState - return visit - } - -} diff --git a/DuckDuckGo/History/Services/HistoryStore.swift b/DuckDuckGo/History/Services/EncryptedHistoryStore.swift similarity index 96% rename from DuckDuckGo/History/Services/HistoryStore.swift rename to DuckDuckGo/History/Services/EncryptedHistoryStore.swift index ca5695fb9c..68cbbea58a 100644 --- a/DuckDuckGo/History/Services/HistoryStore.swift +++ b/DuckDuckGo/History/Services/EncryptedHistoryStore.swift @@ -1,5 +1,5 @@ // -// HistoryStore.swift +// EncryptedHistoryStore.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,19 +20,9 @@ import Common import Foundation import CoreData import Combine +import History -protocol HistoryStoring { - - func cleanOld(until date: Date) -> Future - func save(entry: HistoryEntry) -> Future<[(id: Visit.ID, date: Date)], Error> - func removeEntries(_ entries: [HistoryEntry]) -> Future - func removeVisits(_ visits: [Visit]) -> Future - -} - -final class HistoryStore: HistoryStoring { - - init() {} +final class EncryptedHistoryStore: HistoryStoring { init(context: NSManagedObjectContext) { self.context = context @@ -43,7 +33,7 @@ final class HistoryStore: HistoryStoring { case savingFailed } - private lazy var context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History") + let context: NSManagedObjectContext func removeEntries(_ entries: [HistoryEntry]) -> Future { return Future { [weak self] promise in diff --git a/DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift b/DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift new file mode 100644 index 0000000000..9ca366fb72 --- /dev/null +++ b/DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift @@ -0,0 +1,56 @@ +// +// HistoryCoordinatorExtension.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import History + +extension HistoryCoordinator { + + static let shared = HistoryCoordinator(historyStoring: EncryptedHistoryStore(context: Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History"))) + + func migrateModelV5toV6IfNeeded() { + let defaults = MigrationDefaults() + + guard let historyDictionary = historyDictionary, + !defaults.historyV5toV6Migration else { + return + } + + defaults.historyV5toV6Migration = true + + for entry in historyDictionary.values where entry.visits.isEmpty { + entry.addOldVisit(date: entry.lastVisit) + save(entry: entry) + } + } + + final class MigrationDefaults { + @UserDefaultsWrapper(key: .historyV5toV6Migration, defaultValue: false) + var historyV5toV6Migration: Bool + } + +} + +extension HistoryEntry { + + func addOldVisit(date: Date) { + let visit = Visit(date: date, historyEntry: self) + visits.insert(visit) + } + +} diff --git a/DuckDuckGo/History/ViewModel/VisitViewModel.swift b/DuckDuckGo/History/ViewModel/VisitViewModel.swift index c8cd8e6ed7..5dcef2fc44 100644 --- a/DuckDuckGo/History/ViewModel/VisitViewModel.swift +++ b/DuckDuckGo/History/ViewModel/VisitViewModel.swift @@ -17,6 +17,7 @@ // import Cocoa +import History final class VisitViewModel { diff --git a/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift b/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift index f92a544eaf..3c57e55f26 100644 --- a/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift @@ -18,6 +18,7 @@ import Foundation import SwiftUI +import History extension HomePage.Models { diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 6317449878..ae40aff5df 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import SwiftUI +import History @MainActor final class HomePageViewController: NSViewController { diff --git a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift index f21f662eea..202bee5105 100644 --- a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift +++ b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift @@ -18,6 +18,7 @@ import AppKit import Foundation +import History final class ClearThisHistoryMenuItem: NSMenuItem { diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index 9351d1e578..aca38c3e10 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import Common +import History @MainActor final class HistoryMenu: NSMenu { diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index d1e50ed4a9..e75ddfafb7 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -21,6 +21,7 @@ import Cocoa import Common import WebKit import Configuration +import History // Actions are sent to objects of responder chain diff --git a/DuckDuckGo/Menus/VisitMenuItem.swift b/DuckDuckGo/Menus/VisitMenuItem.swift index 9beb7c5fbf..d73c8bbf37 100644 --- a/DuckDuckGo/Menus/VisitMenuItem.swift +++ b/DuckDuckGo/Menus/VisitMenuItem.swift @@ -17,6 +17,7 @@ // import AppKit +import History final class VisitMenuItem: NSMenuItem { diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift index ebf24cfbfb..c6b005c2fa 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift @@ -19,6 +19,7 @@ import Cocoa import Common import WebKit +import History @MainActor final class NavigationButtonMenuDelegate: NSObject { diff --git a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift index 2f3358c167..3b61528b94 100644 --- a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift +++ b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift @@ -17,6 +17,7 @@ // import Cocoa +import History final class BackForwardListItemViewModel { diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index ff2eb60258..d1be46c375 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -19,6 +19,7 @@ import Foundation import BrowserServicesKit import Common +import History final class SuggestionContainer { @@ -85,7 +86,7 @@ final class SuggestionContainer { extension SuggestionContainer: SuggestionLoadingDataSource { - func history(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.HistoryEntry] { + func history(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.HistorySuggestion] { return historyCoordinating.history ?? [] } @@ -108,9 +109,9 @@ extension SuggestionContainer: SuggestionLoadingDataSource { } -extension HistoryEntry: BrowserServicesKit.HistoryEntry { +extension HistoryEntry: HistorySuggestion { - var numberOfVisits: Int { + public var numberOfVisits: Int { return numberOfTotalVisits } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 297e4d55eb..55cbe5f303 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -24,6 +24,7 @@ import Foundation import Navigation import UserScript import WebKit +import History #if SUBSCRIPTION import Subscription diff --git a/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift index 59ee066407..90a23daa73 100644 --- a/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift @@ -21,6 +21,7 @@ import Common import ContentBlocking import Foundation import Navigation +import History final class HistoryTabExtension: NSObject { diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index dd6f107694..56351d7c11 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -21,6 +21,7 @@ import Combine import ContentBlocking import Foundation import PrivacyDashboard +import History /** Tab Extensions should conform to TabExtension protocol diff --git a/DuckDuckGo/TabBar/Model/TabCollection.swift b/DuckDuckGo/TabBar/Model/TabCollection.swift index 9bc2d27e74..94b632fa86 100644 --- a/DuckDuckGo/TabBar/Model/TabCollection.swift +++ b/DuckDuckGo/TabBar/Model/TabCollection.swift @@ -18,6 +18,7 @@ import Foundation import Combine +import History final class TabCollection: NSObject { diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index 92e3814677..e0319a25e8 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -19,6 +19,7 @@ import Common import Foundation import Combine +import History /** * The delegate callbacks are triggered for events related to unpinned tabs only. diff --git a/IntegrationTests/History/HistoryIntegrationTests.swift b/IntegrationTests/History/HistoryIntegrationTests.swift index 95eb98129d..65bedef872 100644 --- a/IntegrationTests/History/HistoryIntegrationTests.swift +++ b/IntegrationTests/History/HistoryIntegrationTests.swift @@ -20,6 +20,7 @@ import Combine import Common import Navigation import XCTest +import History @testable import DuckDuckGo_Privacy_Browser @available(macOS 12.0, *) diff --git a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift index 346231ede7..b715d829e5 100644 --- a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift +++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift @@ -21,6 +21,7 @@ import Carbon import Combine import Navigation import XCTest +import History @testable import DuckDuckGo_Privacy_Browser diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8ce6dfa876..a7379fd36f 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "113.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index e8489be491..1fe740a36b 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "1.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "113.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 0e494741c2..93dab2b900 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "113.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index f93f27be34..f05a6e354a 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "113.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 30421c20c1..3051644e58 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "1.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "113.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index ac6b0aa0ba..618cd73010 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "1.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "113.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/UnitTests/Fire/Model/FireTests.swift b/UnitTests/Fire/Model/FireTests.swift index fa3bbf8efa..0afbbbbc7a 100644 --- a/UnitTests/Fire/Model/FireTests.swift +++ b/UnitTests/Fire/Model/FireTests.swift @@ -165,7 +165,8 @@ final class FireTests: XCTestCase { shouldRestorePreviousSession: false) appStateRestorationManager.applicationDidFinishLaunching() - let fire = Fire(stateRestorationManager: appStateRestorationManager, + let fire = Fire(historyCoordinating: HistoryCoordinatingMock(), + stateRestorationManager: appStateRestorationManager, tld: ContentBlocking.shared.tld) XCTAssertTrue(appStateRestorationManager.canRestoreLastSessionState) @@ -182,7 +183,8 @@ final class FireTests: XCTestCase { shouldRestorePreviousSession: false) appStateRestorationManager.applicationDidFinishLaunching() - let fire = Fire(stateRestorationManager: appStateRestorationManager, + let fire = Fire(historyCoordinating: HistoryCoordinatingMock(), + stateRestorationManager: appStateRestorationManager, tld: ContentBlocking.shared.tld) XCTAssertTrue(appStateRestorationManager.canRestoreLastSessionState) diff --git a/UnitTests/History/Model/HistoryCoordinatingMock.swift b/UnitTests/History/Model/HistoryCoordinatingMock.swift index 178933c6bb..a14a6ea6c2 100644 --- a/UnitTests/History/Model/HistoryCoordinatingMock.swift +++ b/UnitTests/History/Model/HistoryCoordinatingMock.swift @@ -19,18 +19,23 @@ import XCTest import BrowserServicesKit import Common +import History @testable import DuckDuckGo_Privacy_Browser final class HistoryCoordinatingMock: HistoryCoordinating { + func loadHistory(onCleanFinished: @escaping () -> Void) { + onCleanFinished() + } + var history: History? - var allHistoryVisits: [DuckDuckGo_Privacy_Browser.Visit]? - @Published private(set) var historyDictionary: [URL: DuckDuckGo_Privacy_Browser.HistoryEntry]? - var historyDictionaryPublisher: Published<[URL: DuckDuckGo_Privacy_Browser.HistoryEntry]?>.Publisher { $historyDictionary } + var allHistoryVisits: [Visit]? + @Published private(set) var historyDictionary: [URL: HistoryEntry]? + var historyDictionaryPublisher: Published<[URL: HistoryEntry]?>.Publisher { $historyDictionary } var addVisitCalled = false var visit: Visit? - func addVisit(of url: URL) -> DuckDuckGo_Privacy_Browser.Visit? { + func addVisit(of url: URL) -> Visit? { addVisitCalled = true return visit } diff --git a/UnitTests/History/Model/HistoryCoordinatorTests.swift b/UnitTests/History/Model/HistoryCoordinatorTests.swift index 5f53be663d..46615a9c97 100644 --- a/UnitTests/History/Model/HistoryCoordinatorTests.swift +++ b/UnitTests/History/Model/HistoryCoordinatorTests.swift @@ -17,6 +17,7 @@ // import XCTest +import History @testable import DuckDuckGo_Privacy_Browser @MainActor @@ -31,7 +32,7 @@ class HistoryCoordinatorTests: XCTestCase { func testWhenAddVisitIsCalledBeforeHistoryIsLoadedFromStorage_ThenVisitIsIgnored() { let historyStoringMock = HistoryStoringMock() historyStoringMock.cleanOldResult = nil - let historyCoordinator = HistoryCoordinator() + let historyCoordinator = HistoryCoordinator(historyStoring: historyStoringMock) let url = URL.duckDuckGo historyCoordinator.addVisit(of: url) @@ -177,8 +178,9 @@ class HistoryCoordinatorTests: XCTestCase { func testWhenBurningVisits_DoesntDeleteHistoryBeforeVisits() { // Needs real store to catch assertion which can be raised by improper call ordering in the coordinator let context = CoreData.historyStoreContainer().newBackgroundContext() - let historyStore = HistoryStore(context: context) + let historyStore = EncryptedHistoryStore(context: context) let historyCoordinator = HistoryCoordinator(historyStoring: historyStore) + historyCoordinator.loadHistory { } let url1 = URL(string: "https://duckduckgo.com")! historyCoordinator.addVisit(of: url1) @@ -261,7 +263,7 @@ fileprivate extension HistoryCoordinator { historyStoringMock.cleanOldResult = .success(History()) historyStoringMock.removeEntriesResult = .success(()) let historyCoordinator = HistoryCoordinator(historyStoring: historyStoringMock) - historyCoordinator.loadHistory() + historyCoordinator.loadHistory { } return (historyStoringMock, historyCoordinator) } diff --git a/UnitTests/History/Services/HistoryStoreTests.swift b/UnitTests/History/Services/HistoryStoreTests.swift index b0a502bb89..2bc504a654 100644 --- a/UnitTests/History/Services/HistoryStoreTests.swift +++ b/UnitTests/History/Services/HistoryStoreTests.swift @@ -20,13 +20,14 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser import Combine import class Persistence.CoreDataDatabase +import History final class HistoryStoreTests: XCTestCase { private var cancellables = Set() private var context: NSManagedObjectContext! - private var historyStore: HistoryStore! + private var historyStore: EncryptedHistoryStore! private var location: URL! override func setUp() { @@ -40,7 +41,7 @@ final class HistoryStoreTests: XCTestCase { } } context = database.makeContext(concurrencyType: .mainQueueConcurrencyType) - historyStore = HistoryStore(context: context) + historyStore = EncryptedHistoryStore(context: context) } override func tearDownWithError() throws { diff --git a/UnitTests/History/Services/HistoryStoringMock.swift b/UnitTests/History/Services/HistoryStoringMock.swift index 8b98a3d1b9..baabb996fe 100644 --- a/UnitTests/History/Services/HistoryStoringMock.swift +++ b/UnitTests/History/Services/HistoryStoringMock.swift @@ -19,6 +19,7 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser import Combine +import History final class HistoryStoringMock: HistoryStoring { @@ -40,6 +41,10 @@ final class HistoryStoringMock: HistoryStoring { } } + func load() { + // no-op + } + var removeEntriesCalled = false var removeEntriesArray = [HistoryEntry]() var removeEntriesResult: Result? @@ -72,7 +77,7 @@ final class HistoryStoringMock: HistoryStoring { var saveCalled = false var savedHistoryEntries = [HistoryEntry]() - func save(entry: DuckDuckGo_Privacy_Browser.HistoryEntry) -> Future<[(id: DuckDuckGo_Privacy_Browser.Visit.ID, date: Date)], Error> { + func save(entry: HistoryEntry) -> Future<[(id: Visit.ID, date: Date)], Error> { saveCalled = true savedHistoryEntries.append(entry) for visit in entry.visits { diff --git a/UnitTests/Tab/Services/FaviconManagerMock.swift b/UnitTests/Tab/Services/FaviconManagerMock.swift index 45afa591fd..70356792c6 100644 --- a/UnitTests/Tab/Services/FaviconManagerMock.swift +++ b/UnitTests/Tab/Services/FaviconManagerMock.swift @@ -20,6 +20,7 @@ import XCTest import Combine import BrowserServicesKit import Common +import History @testable import DuckDuckGo_Privacy_Browser final class FaviconManagerMock: FaviconManagement { @@ -53,7 +54,7 @@ final class FaviconManagerMock: FaviconManagement { } // swiftlint:disable:next function_parameter_count - func burnDomains(_ domains: Set, exceptBookmarks bookmarkManager: DuckDuckGo_Privacy_Browser.BookmarkManager, exceptSavedLogins: Set, exceptExistingHistory history: DuckDuckGo_Privacy_Browser.History, tld: Common.TLD, completion: @escaping () -> Void) { + func burnDomains(_ domains: Set, exceptBookmarks bookmarkManager: DuckDuckGo_Privacy_Browser.BookmarkManager, exceptSavedLogins: Set, exceptExistingHistory history: History, tld: Common.TLD, completion: @escaping () -> Void) { completion() } } diff --git a/UnitTests/TabBar/Model/TabCollectionTests.swift b/UnitTests/TabBar/Model/TabCollectionTests.swift index be6ba7be27..983091b856 100644 --- a/UnitTests/TabBar/Model/TabCollectionTests.swift +++ b/UnitTests/TabBar/Model/TabCollectionTests.swift @@ -17,6 +17,7 @@ // import XCTest +import History @testable import DuckDuckGo_Privacy_Browser @MainActor @@ -187,7 +188,7 @@ extension Tab { class HistoryTabExtensionMock: TabExtension, HistoryExtensionProtocol { - var localHistory: [DuckDuckGo_Privacy_Browser.Visit] = [] + var localHistory: [Visit] = [] func getPublicProtocol() -> HistoryExtensionProtocol { self } } From 21b8be18bb3466c5930dd57e38e26b00ed4f60fb Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 11:15:00 +0600 Subject: [PATCH 03/19] PDF saving, printing (#2247) Task/Issue URL: https://app.asana.com/0/0/1206643452552001/f - Added "Save As" and "Print" menu items for PDF viewer - Fixed Save As for PDFs - Always request file download location for local `file://` being downloaded --- DuckDuckGo.xcodeproj/project.pbxproj | 20 + .../Extensions/FileManagerExtension.swift | 6 + .../Extensions/WKPDFHUDViewWrapper.swift | 97 +++++ .../Extensions/WKWebViewExtension.swift | 10 + .../Model/FileDownloadManager.swift | 7 +- DuckDuckGo/MainWindow/MainView.swift | 42 +++ DuckDuckGo/Menus/MainMenuActions.swift | 30 +- DuckDuckGo/Tab/Model/Tab+UIDelegate.swift | 44 ++- .../TabExtensions/DownloadsTabExtension.swift | 156 +++++--- IntegrationTests/Tab/ErrorPageTests.swift | 54 +-- IntegrationTests/Tab/TabContentTests.swift | 349 ++++++++++++++++++ IntegrationTests/Tab/test.pdf | Bin 0 -> 10855 bytes .../DownloadListCoordinatorTests.swift | 10 +- .../DownloadsTabExtensionTests.swift | 70 +++- .../FileDownloadManagerTests.swift | 46 ++- .../Helpers/DownloadsTabExtensionMock.swift | 9 +- .../FileDownload/Helpers/WKDownloadMock.swift | 4 + .../FileDownload/Tab+WKUIDelegateTests.swift | 11 +- UnitTests/Tab/Model/TabTests.swift | 8 +- ...bViewPrivateMethodsAvailabilityTests.swift | 6 + 20 files changed, 874 insertions(+), 105 deletions(-) create mode 100644 DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift create mode 100644 IntegrationTests/Tab/TabContentTests.swift create mode 100644 IntegrationTests/Tab/test.pdf diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c58f2df7d1..aae4f772c1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2720,6 +2720,8 @@ B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C85412694590B0048FEBE /* PermissionButton.swift */; }; B64CE01E2B8622D700126CA5 /* AddressBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */; }; B64CE01F2B8622D700126CA5 /* AddressBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */; }; + B64E42AB2B909DC9006C1346 /* test.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B64E42AA2B909DC9006C1346 /* test.pdf */; }; + B64E42AC2B909DC9006C1346 /* test.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B64E42AA2B909DC9006C1346 /* test.pdf */; }; B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; }; B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; }; B65211272B29A43000B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; }; @@ -2736,6 +2738,9 @@ B658BAB62B0F845D00D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; }; B658BAB72B0F848D00D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; }; B658BAB92B0F849100D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; }; + B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; }; + B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; }; + B65C7DFD2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; }; B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CA2B316DF100A595BB /* SnapshotTesting */; }; B65CD8CD2B316DFC00A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CC2B316DFC00A595BB /* SnapshotTesting */; }; B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CE2B316E0200A595BB /* SnapshotTesting */; }; @@ -3021,6 +3026,8 @@ B6EC37FC29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; }; B6EC37FD29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; }; B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = B6EC37FE29B8D915001ACE79 /* Configuration */; }; + B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; + B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F41030264D2B23003DA42C /* ProgressExtension.swift */; }; B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; }; @@ -4213,6 +4220,7 @@ B64C853C26944B940048FEBE /* PermissionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionStore.swift; sourceTree = ""; }; B64C85412694590B0048FEBE /* PermissionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionButton.swift; sourceTree = ""; }; B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarTests.swift; sourceTree = ""; }; + B64E42AA2B909DC9006C1346 /* test.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test.pdf; sourceTree = ""; }; B65349A9265CF45000DCC645 /* DispatchQueueExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueExtensionsTests.swift; sourceTree = ""; }; B6553691268440D700085A79 /* WKProcessPool+GeolocationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKProcessPool+GeolocationProvider.swift"; sourceTree = ""; }; B65536962684413900085A79 /* WKGeolocationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKGeolocationProvider.h; sourceTree = ""; }; @@ -4224,6 +4232,7 @@ B657841925FA484B00D8DB33 /* NSException+Catch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSException+Catch.m"; sourceTree = ""; }; B657841E25FA497600D8DB33 /* NSException+Catch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSException+Catch.swift"; sourceTree = ""; }; B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKPDFHUDViewWrapper.swift; sourceTree = ""; }; B65CD8D42B316FCA00A595BB /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; B65CD8D72B341FD300A595BB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; B65E6B9D26D9EC0800095F96 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; @@ -4406,6 +4415,7 @@ B6EC37EA29B5DA2A001ACE79 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; B6EC37FA29B6447F001ACE79 /* TestsServer.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TestsServer.xcconfig; sourceTree = ""; }; B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = ""; }; + B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabContentTests.swift; sourceTree = ""; }; B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; }; B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; @@ -7633,6 +7643,7 @@ B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */, B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */, B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, + B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, @@ -7869,8 +7880,10 @@ B644B43C29D56811003FA9AB /* Tab */ = { isa = PBXGroup; children = ( + B64E42AA2B909DC9006C1346 /* test.pdf */, B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */, B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */, + B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */, B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */, ); path = Tab; @@ -9022,6 +9035,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B64E42AC2B909DC9006C1346 /* test.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9036,6 +9050,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B64E42AB2B909DC9006C1346 /* test.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9725,6 +9740,7 @@ 1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B6BCC5242AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, 3706FEBC293F6EFF00E42796 /* BWResponse.swift in Sources */, + B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, 3706FAF4293F65D500E42796 /* SafariBookmarksReader.swift in Sources */, 31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */, B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */, @@ -10567,6 +10583,7 @@ B62A234129C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */, B603973929BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift in Sources */, B644B43E29D5682B003FA9AB /* SearchNonexistentDomainTests.swift in Sources */, + B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */, 3706FEA5293F662100E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, B603973D29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */, B60C6F8729B1CAB2007BFAA8 /* TestRunHelper.swift in Sources */, @@ -10609,6 +10626,7 @@ 4B1AD92125FC474E00261379 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, B62A234029C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */, B603973829BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift in Sources */, + B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */, B644B43D29D56829003FA9AB /* SearchNonexistentDomainTests.swift in Sources */, B603973C29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */, B60C6F8629B1CAB0007BFAA8 /* TestRunHelper.swift in Sources */, @@ -10979,6 +10997,7 @@ 4B957A192AC7AE700062CA31 /* PasswordManagerCoordinator.swift in Sources */, 4B957A1A2AC7AE700062CA31 /* PasswordManagementIdentityModel.swift in Sources */, 4B957A1B2AC7AE700062CA31 /* UserDefaultsWrapper.swift in Sources */, + B65C7DFD2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, 4B957A1C2AC7AE700062CA31 /* PasswordManagementPopover.swift in Sources */, 4B957A1D2AC7AE700062CA31 /* BWCommunicator.swift in Sources */, 4B957A1E2AC7AE700062CA31 /* HomePageRecentlyVisitedModel.swift in Sources */, @@ -11592,6 +11611,7 @@ AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */, 1D43EB3A292B63B00065E5D6 /* BWRequest.swift in Sources */, B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, + B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */, 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index 5b39680e13..4705706483 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -21,10 +21,16 @@ import Foundation extension FileManager { + @discardableResult func moveItem(at srcURL: URL, to destURL: URL, incrementingIndexIfExists flag: Bool, pathExtension: String? = nil) throws -> URL { return try self.perform(self.moveItem, from: srcURL, to: destURL, incrementingIndexIfExists: flag, pathExtension: pathExtension) } + @discardableResult + func copyItem(at srcURL: URL, to destURL: URL, incrementingIndexIfExists flag: Bool, pathExtension: String? = nil) throws -> URL { + return try self.perform(self.copyItem, from: srcURL, to: destURL, incrementingIndexIfExists: flag, pathExtension: pathExtension) + } + private func perform(_ operation: (URL, URL) throws -> Void, from srcURL: URL, to destURL: URL, diff --git a/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift new file mode 100644 index 0000000000..ef4919b1fd --- /dev/null +++ b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift @@ -0,0 +1,97 @@ +// +// WKPDFHUDViewWrapper.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +/// A wrapper for the PDF HUD window with Zoom controls, Download and Open in Preview buttons +/// Used to trigger Save PDF +struct WKPDFHUDViewWrapper { + + static let WKPDFHUDViewClass: AnyClass? = NSClassFromString("WKPDFHUDView") + static let performActionForControlSelector = NSSelectorFromString("_performActionForControl:") + static let visibleKey = "_visible" + static let setVisibleSelector = NSSelectorFromString("_setVisible:") + static let savePDFControlId = "arrow.down.circle" + + private let hudView: NSView + + var isVisible: Bool { + get { + hudView.layer?.sublayers?.first?.opacity ?? 0 > 0 + } + nonmutating set { + guard hudView.responds(to: Self.setVisibleSelector) else { return } + hudView.perform(Self.setVisibleSelector, with: newValue) + } + } + + /// Create a wrapper over the PDF HUD view validating its class is `WKPDFHUDView` + /// - parameter view: the WKPDFHUDView to wrap + /// - returns nil if the view + init?(view: NSView) { + guard type(of: view) == Self.WKPDFHUDViewClass else { return nil } + + guard Self.WKPDFHUDViewClass?.instancesRespond(to: Self.performActionForControlSelector) == true else { + assertionFailure("WKPDFHUDView doesn‘t respond to _performActionForControl:") + return nil + } + self.hudView = view + } + + /// Find WebView‘s PDF HUD view at a clicked point + /// + /// Used to get PDF controls view of a clicked WebView frame for `Print…` and `Save As…` PDF context menu commands + static func getPdfHudView(in webView: WKWebView, at location: NSPoint? = nil) -> Self? { + guard let hudView = webView.subviews.last(where: { type(of: $0) == Self.WKPDFHUDViewClass && $0.frame.contains(location ?? $0.frame.origin) }) else { +#if DEBUG + Task { + if await webView.mimeType == "application/pdf" { + assertionFailure("WebView doesn‘t have PDF HUD View") + } + } +#endif + return nil + } + return self.init(view: hudView) + } + + func savePDF() { + let wasVisible = isVisible + self.setIsVisibleIVar(true) + defer { + if !wasVisible { + self.setIsVisibleIVar(false) + } + } + hudView.perform(Self.performActionForControlSelector, with: Self.savePDFControlId) + } + + // try to set _visible ivar value directly to avoid actually showing the HUD + private func setIsVisibleIVar(_ value: Bool) { + do { + try NSException.catch { + hudView.setValue(value, forKey: Self.visibleKey) + } + } catch { + assertionFailure("\(error)") + self.isVisible = value + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 4c70d4bacc..fcd19de900 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -325,6 +325,16 @@ extension WKWebView { return self.printOperation(with: printInfo) } + func hudView(at point: NSPoint? = nil) -> WKPDFHUDViewWrapper? { + WKPDFHUDViewWrapper.getPdfHudView(in: self, at: point) + } + + func savePDF(_ pdfHUD: WKPDFHUDViewWrapper? = nil) -> Bool { + guard let hudView = pdfHUD ?? hudView() else { return false } + hudView.savePDF() + return true + } + var fullScreenPlaceholderView: NSView? { guard self.responds(to: Selector.fullScreenPlaceholderView) else { return nil } return self.value(forKey: NSStringFromSelector(Selector.fullScreenPlaceholderView)) as? NSView diff --git a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift index 9ae1945b25..eb271ab5a6 100644 --- a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift +++ b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift @@ -83,10 +83,11 @@ final class FileDownloadManager: FileDownloadManagerProtocol { return url } - var promptForLocation: Bool { + func shouldPromptForLocation(for url: URL?) -> Bool { switch self { case .prompt: return true - case .preset, .auto: return false + case .preset: return false + case .auto: return url?.isFileURL ?? true // always prompt when "downloading" a local file } } } @@ -96,7 +97,7 @@ final class FileDownloadManager: FileDownloadManagerProtocol { dispatchPrecondition(condition: .onQueue(.main)) let task = WebKitDownloadTask(download: download, - promptForLocation: location.promptForLocation, + promptForLocation: location.shouldPromptForLocation(for: download.originalRequest?.url), destinationURL: location.destinationURL, tempURL: location.tempURL, isBurner: fromBurnerWindow) diff --git a/DuckDuckGo/MainWindow/MainView.swift b/DuckDuckGo/MainWindow/MainView.swift index 6ef7452e5d..5dc064ac99 100644 --- a/DuckDuckGo/MainWindow/MainView.swift +++ b/DuckDuckGo/MainWindow/MainView.swift @@ -18,6 +18,7 @@ import Cocoa import Combine +import WebKit final class MainView: NSView { let tabBarContainerView = NSView() @@ -107,6 +108,8 @@ final class MainView: NSView { // PDF Plugin context menu override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { setupSearchContextMenuItem(menu: menu) + setupSaveAsAndPrintMenuItems(menu: menu, with: event) + super.willOpenMenu(menu, with: event) } private func setupSearchContextMenuItem(menu: NSMenu) { @@ -130,6 +133,45 @@ final class MainView: NSView { } } + private func setupSaveAsAndPrintMenuItems(menu: NSMenu, with event: NSEvent) { + guard let window else { return } + + // try to find PDF HUD view at the right-click location (it might be a frame click) + let hudView: WKPDFHUDViewWrapper? = { + for point in [event.locationInWindow, window.mouseLocationOutsideOfEventStream] { + let locationInView = convert(point, from: nil) + guard let view = self.hitTest(locationInView) else { continue } + + if let hudView = WKPDFHUDViewWrapper(view: view) { + return hudView + } else if let webView = view as? WKWebView, + let hudView = webView.hudView(at: webView.convert(locationInView, from: self)) { + return hudView + } + } + return (self.hitTest(bounds.center) as? WKWebView)?.hudView() + }() + assert(hudView != nil) + + // insert Save As… and Print… items after `Open with Preview` + // 1. find `Copy` + let idxAfterCopy = menu.indexOfItem(withTitle: UserText.copy) + /* will become 0 if no copy (-1 + 1) */ 1 + let insertionIdx: Int + if idxAfterCopy > 0 { + // 2. find separator below `Copy` + let separatorIdx = (idxAfterCopy.. NSDragOperation { diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index e75ddfafb7..80bb0ee02a 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -266,7 +266,16 @@ extension MainViewController { /// Finds currently active Tab even if it‘s playing a Full Screen video private func getActiveTabAndIndex() -> (tab: Tab, index: TabIndex)? { - guard let tab = WindowControllersManager.shared.lastKeyMainWindowController?.activeTab else { + var tab: Tab? { + // popup windows don‘t get to lastKeyMainWindowController so try getting their WindowController directly fron a key window + if let window = self.view.window, + let mainWindowController = window.nextResponder as? MainWindowController, + let tab = mainWindowController.activeTab { + return tab + } + return WindowControllersManager.shared.lastKeyMainWindowController?.activeTab + } + guard let tab else { assertionFailure("Could not get currently active Tab") return nil } @@ -622,13 +631,15 @@ extension MainViewController { // MARK: - Printing @objc func printWebView(_ sender: Any?) { - getActiveTabAndIndex()?.tab.print() + let pdfHUD = (sender as? NSMenuItem)?.pdfHudRepresentedObject // if printing a PDF (may be from a frame context menu) + getActiveTabAndIndex()?.tab.print(pdfHUD: pdfHUD) } // MARK: - Saving @objc func saveAs(_ sender: Any) { - getActiveTabAndIndex()?.tab.saveWebContentAs() + let pdfHUD = (sender as? NSMenuItem)?.pdfHudRepresentedObject // if saving a PDF (may be from a frame context menu) + getActiveTabAndIndex()?.tab.saveWebContent(pdfHUD: pdfHUD, location: .prompt) } // MARK: - Debug @@ -1018,3 +1029,16 @@ extension AppDelegate: PrivacyDashboardViewControllerSizeDelegate { privacyDashboardWindow?.setFrame(NSRect(origin: .zero, size: size), display: true, animate: true) } } + +extension NSMenuItem { + + var pdfHudRepresentedObject: WKPDFHUDViewWrapper? { + guard let representedObject = representedObject else { return nil } + + return representedObject as? WKPDFHUDViewWrapper ?? { + assertionFailure("Unexpected SaveAs/Print menu item represented object: \(representedObject)") + return nil + }() + } + +} diff --git a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift index 97133045a0..3de0df0c9b 100644 --- a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift +++ b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift @@ -22,6 +22,7 @@ import Foundation import Navigation import UniformTypeIdentifiers import WebKit +import PDFKit extension Tab: WKUIDelegate, PrintingUserScriptDelegate { @@ -35,9 +36,22 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { self.value(forKey: Tab.objcNewWindowPolicyDecisionMakersKeyPath) as? [NewWindowPolicyDecisionMaker] } + @MainActor private static var expectedSaveDataToFileCallback: (@MainActor (URL?) -> Void)? + @MainActor + private static func consumeExpectedSaveDataToFileCallback() -> (@MainActor (URL?) -> Void)? { + defer { + expectedSaveDataToFileCallback = nil + } + return expectedSaveDataToFileCallback + } + @objc(_webView:saveDataToFile:suggestedFilename:mimeType:originatingURL:) func webView(_ webView: WKWebView, saveDataToFile data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) { - saveDownloaded(data: data, suggestedFilename: suggestedFilename, mimeType: mimeType) + Task { + let result = try? await saveDownloadedData(data, suggestedFilename: suggestedFilename, mimeType: mimeType, originatingURL: originatingURL) + // when print function saves a PDF setting the callback, return the saved temporary file to it + await Self.consumeExpectedSaveDataToFileCallback()?(result) + } } @MainActor @@ -299,6 +313,10 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { printOperation.view?.frame = webView.bounds } + runPrintOperation(printOperation, completionHandler: completionHandler) + } + + func runPrintOperation(_ printOperation: NSPrintOperation, completionHandler: ((Bool) -> Void)? = nil) { let dialog = UserDialogType.print(.init(printOperation) { result in completionHandler?((try? result.get()) ?? false) }) @@ -315,7 +333,29 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { self.runPrintOperation(for: frameHandle, in: webView) { _ in completionHandler() } } - func print() { + @MainActor(unsafe) + func print(pdfHUD: WKPDFHUDViewWrapper? = nil) { + if let pdfHUD { + Self.expectedSaveDataToFileCallback = { [weak self] url in + guard let self, let url, + let pdfDocument = PDFDocument(url: url) else { + assertionFailure("Could not load PDF document from \(url?.path ?? "")") + return + } + // Set up NSPrintOperation + guard let printOperation = pdfDocument.printOperation(for: .shared, scalingMode: .pageScaleNone, autoRotate: false) else { + assertionFailure("Could not print PDF document") + return + } + + self.runPrintOperation(printOperation) { _ in + try? FileManager.default.removeItem(at: url) + } + } + saveWebContent(pdfHUD: pdfHUD, location: .temporary) + return + } + self.runPrintOperation(for: nil, in: self.webView) } diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 79ce4aed1d..c0be988432 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -34,10 +34,17 @@ final class DownloadsTabExtension: NSObject { private let isBurner: Bool private var isRestoringSessionState = false + enum DownloadLocation { + case auto + case prompt + case temporary + } + private var nextSaveDataRequestDownloadLocation: DownloadLocation = .auto + @Published private(set) var savePanelDialogRequest: SavePanelDialogRequest? { - didSet { - savePanelDialogRequest?.addCompletionHandler { [weak self, weak savePanelDialogRequest] _ in + willSet { + newValue?.addCompletionHandler { [weak self, weak savePanelDialogRequest=newValue] _ in if let self, let savePanelDialogRequest, self.savePanelDialogRequest === savePanelDialogRequest { @@ -57,44 +64,88 @@ final class DownloadsTabExtension: NSObject { super.init() } - func saveWebViewContentAs(_ webView: WKWebView) { + func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadLocation) { Task { @MainActor in - await saveWebViewContentAs(webView) + await saveWebViewContent(from: webView, pdfHUD: pdfHUD, location: location) } } @MainActor - private func saveWebViewContentAs(_ webView: WKWebView) async { - guard await webView.mimeType == UTType.html.preferredMIMEType else { - if let url = webView.url { - webView.startDownload(using: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)) { download in - self.downloadManager.add(download, - fromBurnerWindow: self.isBurner, - delegate: self, location: .prompt) - } + private func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadLocation) async { + let mimeType = pdfHUD != nil ? UTType.pdf.preferredMIMEType : await webView.mimeType + switch mimeType { + case UTType.html.preferredMIMEType: + assert([.prompt, .auto].contains(location)) + + let parameters = SavePanelParameters(suggestedFilename: webView.suggestedFilename, fileTypes: [.html, .webArchive, .pdf]) + self.savePanelDialogRequest = SavePanelDialogRequest(parameters) { result in + guard let (url, fileType) = try? result.get() else { return } + webView.exportWebContent(to: url, as: fileType.flatMap(WKWebView.ContentExportType.init) ?? .html) } - return + + case UTType.pdf.preferredMIMEType: + self.nextSaveDataRequestDownloadLocation = location + let success = webView.savePDF(pdfHUD) // calls `saveDownloadedData(_:suggestedFilename:mimeType:originatingURL)` + guard success else { fallthrough } + + default: + guard let url = webView.url else { + assertionFailure("Can‘t save web content without URL loaded") + return + } + if url.isFileURL { + self.nextSaveDataRequestDownloadLocation = location + _=try? await self.saveDownloadedData(nil, suggestedFilename: url.lastPathComponent, mimeType: mimeType ?? "text/html", originatingURL: url) + return + } + + let download = await webView.startDownload(using: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)) + + let location = self.downloadLocation(for: location, suggestedFilename: download.webView?.suggestedFilename ?? "") + self.downloadManager.add(download, + fromBurnerWindow: self.isBurner, + delegate: self, + location: location) } - let parameters = SavePanelParameters(suggestedFilename: webView.suggestedFilename, fileTypes: [.html, .webArchive, .pdf]) - self.savePanelDialogRequest = SavePanelDialogRequest(parameters) { result in - guard let (url, fileType) = try? result.get() else { return } - webView.exportWebContent(to: url, as: fileType.flatMap(WKWebView.ContentExportType.init) ?? .html) + } + + private func downloadLocation(for location: DownloadLocation, suggestedFilename: String) -> FileDownloadManager.DownloadLocationPreference { + switch location { + case .auto: + return .auto + case .prompt: + return .prompt + case .temporary: + let suggestedFilename = suggestedFilename.isEmpty ? UUID().uuidString : suggestedFilename + let fm = FileManager.default + let dirURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename()) + try? fm.createDirectory(at: dirURL, withIntermediateDirectories: true) + return .preset(destinationURL: dirURL.appendingPathComponent(suggestedFilename), tempURL: nil) } } - private func saveDownloaded(data: Data, to toURL: URL) { + private func saveDownloadedData(_ data: Data?, to toURL: URL, originatingURL: URL) throws { let fm = FileManager.default - let tempURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename()) - do { - // First save file in a temporary directory - try data.write(to: tempURL) - // Then move the file to the download location and show a bounce if the file is in a location on the user's dock. + + // if no data provided - copy file from local url to the destination url + guard let data else { + guard originatingURL.isFileURL else { + assertionFailure("No data provided for non-file URL") + return + } try Progress.withPublishedProgress(url: toURL) { - _ = try fm.moveItem(at: tempURL, to: toURL, incrementingIndexIfExists: true) + try fm.copyItem(at: originatingURL, to: toURL, incrementingIndexIfExists: true) } - } catch { - os_log("Failed to save PDF file to Downloads folder", type: .error) + return + } + + let tempURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename()) + // First save file in a temporary directory + try data.write(to: tempURL) + // Then move the file to the download location and show a bounce if the file is in a location on the user's dock. + try Progress.withPublishedProgress(url: toURL) { + try fm.moveItem(at: tempURL, to: toURL, incrementingIndexIfExists: true) } } @@ -129,7 +180,7 @@ extension DownloadsTabExtension: NavigationResponder { let firstNavigationAction = navigationResponse.mainFrameNavigation?.redirectHistory.first ?? navigationResponse.mainFrameNavigation?.navigationAction - guard navigationResponse.httpResponse?.isSuccessful == true, + guard navigationResponse.httpResponse?.isSuccessful != false, !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload else { return .next // proceed with normal page loading } @@ -206,8 +257,7 @@ extension DownloadsTabExtension: DownloadTaskDelegate { @MainActor func chooseDestination(suggestedFilename: String?, directoryURL: URL?, fileTypes: [UTType], callback: @escaping @MainActor (URL?, UTType?) -> Void) { - savePanelDialogRequest = SavePanelDialogRequest(SavePanelParameters(suggestedFilename: suggestedFilename, fileTypes: fileTypes)) { [weak self] result in - self?.savePanelDialogRequest = nil + savePanelDialogRequest = SavePanelDialogRequest(SavePanelParameters(suggestedFilename: suggestedFilename, fileTypes: fileTypes)) { result in guard case let .success(.some( (url: url, fileType: fileType) )) = result else { callback(nil, nil) return @@ -226,9 +276,9 @@ protocol DownloadsTabExtensionProtocol: AnyObject, NavigationResponder, Download var delegate: TabDownloadsDelegate? { get set } var savePanelDialogPublisher: AnyPublisher { get } - func saveWebViewContentAs(_ webView: WKWebView) + func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) + func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? } extension DownloadsTabExtension: TabExtension, DownloadsTabExtensionProtocol { @@ -241,19 +291,35 @@ extension DownloadsTabExtension: TabExtension, DownloadsTabExtensionProtocol { } @MainActor - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) { - if !downloadsPreferences.alwaysRequestDownloadLocation, - let location = downloadsPreferences.effectiveDownloadLocation { - let url = location.appendingPathComponent(suggestedFilename) - saveDownloaded(data: data, to: url) - return + func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? { + defer { + self.nextSaveDataRequestDownloadLocation = .auto } + switch downloadLocation(for: nextSaveDataRequestDownloadLocation, suggestedFilename: suggestedFilename) { + case .auto: + guard !downloadsPreferences.alwaysRequestDownloadLocation, + let location = downloadsPreferences.effectiveDownloadLocation else { fallthrough /* prompt */ } + + let url = location.appendingPathComponent(suggestedFilename) + try saveDownloadedData(data, to: url, originatingURL: originatingURL) + return url + + case .prompt: + let fileTypes = UTType(mimeType: mimeType).map { [$0] } ?? [] + let url: URL? = await withCheckedContinuation { continuation in + chooseDestination(suggestedFilename: suggestedFilename, directoryURL: nil, fileTypes: fileTypes) { url, _ in + continuation.resume(returning: url) + } + } + + guard let url else { return nil } - let fileTypes = UTType(mimeType: mimeType).map { [$0] } ?? [] - chooseDestination(suggestedFilename: suggestedFilename, directoryURL: nil, fileTypes: fileTypes) { [weak self] url, _ in - guard let url else { return } + try saveDownloadedData(data, to: url, originatingURL: originatingURL) + return url - self?.saveDownloaded(data: data, to: url) + case .preset(destinationURL: let destinationURL, tempURL: _): + try saveDownloadedData(data, to: destinationURL, originatingURL: originatingURL) + return destinationURL } } } @@ -266,12 +332,12 @@ extension TabExtensions { extension Tab { - func saveWebContentAs() { - self.downloads?.saveWebViewContentAs(webView) + func saveWebContent(pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) { + self.downloads?.saveWebViewContent(from: webView, pdfHUD: pdfHUD, location: location) } - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) { - self.downloads?.saveDownloaded(data: data, suggestedFilename: suggestedFilename, mimeType: mimeType) + func saveDownloadedData(_ data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? { + try await self.downloads?.saveDownloadedData(data, suggestedFilename: suggestedFilename, mimeType: mimeType, originatingURL: originatingURL) } } diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift index 8e6bf5c61b..bb89e9c535 100644 --- a/IntegrationTests/Tab/ErrorPageTests.swift +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -143,23 +143,25 @@ class ErrorPageTests: XCTestCase { NSError.disableSwizzledDescription = false } + // MARK: - Tests + func testWhenPageFailsToLoad_errorPageShown() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, fail with error schemeHandler.middleware = [{ _ in .failure(NSError.hostNotFound) }] - tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) let error = try await eNavigationFailed.value _=try await eNavigationFinished.value @@ -187,39 +189,43 @@ class ErrorPageTests: XCTestCase { // open 2 Tabs with newtab page let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let eNewtab2PageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + tabsViewModel.select(at: .unpinned(0)) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait until Home page loads - let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value + try await eNewtab2PageLoaded.value // navigate to a failing url + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let eErrorPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() schemeHandler.middleware = [{ _ in .failure(NSError.noConnection) }] + tab1.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) // wait for error page to open - let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() - _=try await eNavigationFailed.value + _=try await eErrorPageLoaded.value // switch to tab 2 tabsViewModel.select(at: .unpinned(1)) // next load should be ok let eServerQueried = expectation(description: "server request sent") + let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() schemeHandler.middleware = [{ _ in eServerQueried.fulfill() return .ok(.html(Self.testHtml)) }] // coming back to the failing tab 1 should trigger its reload - let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() - tabsViewModel.select(at: .unpinned(0)) + await fulfillment(of: [eServerQueried], timeout: 5) _=try await eNavigationSucceeded.value - await fulfillment(of: [eServerQueried], timeout: 1) XCTAssertEqual(tab1.content.url, .test) XCTAssertNil(tab1.error) } @@ -232,13 +238,13 @@ class ErrorPageTests: XCTestCase { }] let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait for error page to open - let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() - let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() - _=try await eNavigationFailed.value _=try await eNavigationFinished.value @@ -285,12 +291,12 @@ class ErrorPageTests: XCTestCase { }] let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) window = WindowsManager.openNewWindow(with: tabsViewModel)! // wait for error page to open - let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() - let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() _=try await eNavigationFailed.value _=try await errorNavigationFinished.value @@ -314,11 +320,11 @@ class ErrorPageTests: XCTestCase { .failure(NSError.hostNotFound) }] let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() window = WindowsManager.openNewWindow(with: tab)! // wait for navigation to fail - let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() - let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() _=try await eNavigationFailed.value _=try await errorNavigationFinished.value @@ -366,11 +372,11 @@ class ErrorPageTests: XCTestCase { .failure(NSError.hostNotFound) }] let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() window = WindowsManager.openNewWindow(with: tab)! // wait for navigation to fail - let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() - let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() _=try await eNavigationFailed.value _=try await errorNavigationFinished.value @@ -420,9 +426,9 @@ class ErrorPageTests: XCTestCase { func testWhenPageLoadedAndFailsOnRefreshAndOnConsequentRefresh_errorPageIsUpdatedKeepingForwardHistory() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -496,9 +502,9 @@ class ErrorPageTests: XCTestCase { func testWhenPageLoadedAndFailsOnRefreshAndSucceedsOnConsequentRefresh_forwardHistoryIsPreserved() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -564,9 +570,9 @@ class ErrorPageTests: XCTestCase { func testWhenReloadingBySubmittingSameURL_errorPageRemainsSame() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -639,9 +645,9 @@ class ErrorPageTests: XCTestCase { func testWhenGoingToAnotherUrlFails_newBackForwardHistoryItemIsAdded() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -714,9 +720,9 @@ class ErrorPageTests: XCTestCase { func testWhenGoingToAnotherUrlSucceeds_newBackForwardHistoryItemIsAdded() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value // navigate to test url, success @@ -786,9 +792,9 @@ class ErrorPageTests: XCTestCase { }] let tab = Tab(content: .url(.test, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock, interactionStateData: Self.sessionStateData) + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) window = WindowsManager.openNewWindow(with: viewModel)! - let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() try await eNewtabPageLoaded.value XCTAssertTrue(tab.canReload) @@ -871,8 +877,8 @@ class ErrorPageTests: XCTestCase { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) - window = WindowsManager.openNewWindow(with: viewModel)! let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + window = WindowsManager.openNewWindow(with: viewModel)! try await eNewtabPageLoaded.value // navigate to alt url, redirect to test url, fail with error diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift new file mode 100644 index 0000000000..13ba1fc393 --- /dev/null +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -0,0 +1,349 @@ +// +// TabContentTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Carbon +import Combine +import Common +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@available(macOS 12.0, *) +@MainActor +class TabContentTests: XCTestCase { + + var window: NSWindow! + + var mainViewController: MainViewController { + (window.contentViewController as! MainViewController) + } + + var tabViewModel: TabViewModel { + mainViewController.browserTabViewController.tabViewModel! + } + + @MainActor + override func setUp() async throws { + } + + @MainActor + override func tearDown() async throws { + window?.close() + window = nil + NSView.swizzleWillOpenMenu(with: nil) + } + + func sendRightMouseClick(to view: NSView) { + let point = view.convert(view.bounds.center, to: nil) + + let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown, + location: point, + modifierFlags: [], + timestamp: CACurrentMediaTime(), + windowNumber: window.windowNumber, + context: nil, + eventNumber: -22966, + clickCount: 1, + pressure: 1)! + let mouseUp = NSEvent.mouseEvent(with: .rightMouseUp, + location: point, + modifierFlags: [], + timestamp: CACurrentMediaTime(), + windowNumber: window.windowNumber, + context: nil, + eventNumber: -22966, + clickCount: 1, + pressure: 1)! + view.window!.sendEvent(mouseDown) + view.window!.sendEvent(mouseUp) + } + + // MARK: - Tests + + @MainActor + func testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // wait for context menu to appear + let eMenuShown = expectation(description: "menu shown") + var menuItems = [NSMenuItem]() + NSView.swizzleWillOpenMenu { menu, event in + menuItems = menu.items + menu.removeAllItems() + eMenuShown.fulfill() + } + + // right-click + NSApp.activate(ignoringOtherApps: true) + sendRightMouseClick(to: tab.webView) + await fulfillment(of: [eMenuShown]) + + // find Print, Save As + guard let printMenuItem = menuItems.first(where: { $0.title == UserText.printMenuItem }) else { + XCTFail("No print menu item") + return + } + let saveAsMenuItem = menuItems.first(where: { $0.title == UserText.mainMenuFileSaveAs }) + XCTAssertNotNil(saveAsMenuItem) + + // wait for print dialog to appear + let ePrintDialogShown = expectation(description: "Print dialog shown") + let getPrintDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first { + ePrintDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + let printOperationPromise = tab.$userInteractionDialog.compactMap { (dialog: Tab.UserDialog?) -> NSPrintOperation? in + guard case .print(let request) = dialog?.dialog else { return nil } + return request.parameters + }.timeout(5).first().promise() + + XCTAssertNotNil(printMenuItem.action) + XCTAssertNotNil(printMenuItem.pdfHudRepresentedObject) + + // Click Print… + _=printMenuItem.action.map { action in + NSApp.sendAction(action, to: printMenuItem.target, from: printMenuItem) + } + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) { + getPrintDialog.cancel() + } + let printDialog = try await getPrintDialog.value + defer { + window.endSheet(printDialog, returnCode: .cancel) + } + let printOperation = try await printOperationPromise.value + + XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) + XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3)) + } + + @MainActor + func testWhenPDFMainMenuPrintChosen_printDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // wait for print dialog to appear + let ePrintDialogShown = expectation(description: "Print dialog shown") + let getPrintDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first { + ePrintDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + let printOperationPromise = tab.$userInteractionDialog.compactMap { (dialog: Tab.UserDialog?) -> NSPrintOperation? in + guard case .print(let request) = dialog?.dialog else { return nil } + return request.parameters + }.timeout(5).first().promise() + + // Hit Cmd+P + let keyDown = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "p", charactersIgnoringModifiers: "p", isARepeat: false, keyCode: UInt16(kVK_ANSI_P))! + let keyUp = NSEvent.keyEvent(with: .keyUp, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "p", charactersIgnoringModifiers: "p", isARepeat: false, keyCode: UInt16(kVK_ANSI_P))! + window.sendEvent(keyDown) + window.sendEvent(keyUp) + + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) { + getPrintDialog.cancel() + } + let printDialog = try await getPrintDialog.value + defer { + window.endSheet(printDialog, returnCode: .cancel) + } + let printOperation = try await printOperationPromise.value + + XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "…")) + XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3)) + } + + @MainActor + func testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // wait for context menu to appear + let eMenuShown = expectation(description: "menu shown") + var menuItems = [NSMenuItem]() + NSView.swizzleWillOpenMenu { menu, event in + menuItems = menu.items + menu.removeAllItems() + eMenuShown.fulfill() + } + + // right-click + NSApp.activate(ignoringOtherApps: true) + sendRightMouseClick(to: tab.webView) + await fulfillment(of: [eMenuShown]) + + // find Print, Save As + let printMenuItem = menuItems.first(where: { $0.title == UserText.printMenuItem }) + XCTAssertNotNil(printMenuItem) + guard let saveAsMenuItem = menuItems.first(where: { $0.title == UserText.mainMenuFileSaveAs }) else { + XCTFail("No Save As menu item") + return + } + + // wait for print dialog to appear + let eSaveDialogShown = expectation(description: "Save dialog shown") + let getSaveDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first as? NSSavePanel { + eSaveDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + + XCTAssertNotNil(saveAsMenuItem.action) + XCTAssertNotNil(saveAsMenuItem.pdfHudRepresentedObject) + + // Click Save As… + _=saveAsMenuItem.action.map { action in + NSApp.sendAction(action, to: saveAsMenuItem.target, from: saveAsMenuItem) + } + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) { + getSaveDialog.cancel() + } + let saveDialog = try await getSaveDialog.value + + guard let url = saveDialog.url else { + XCTFail("no Save Dialog url") + return + } + try? FileManager.default.removeItem(at: url) + + // wait until file is saved + let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in + FileManager.default.fileExists(atPath: url.path) + }.timeout(5).first().promise() + defer { + try? FileManager.default.removeItem(at: url) + } + + window.endSheet(saveDialog, returnCode: .OK) + + _=try await fileSavedPromise.value + try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) + } + + @MainActor + func testWhenPDFMainMenuSaveAsChosen_saveDialogOpens() async throws { + let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")! + // open Tab with PDF + let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // wait for print dialog to appear + let eSaveDialogShown = expectation(description: "Save dialog shown") + let getSaveDialog = Task { @MainActor in + while true { + if let sheet = self.window.sheets.first as? NSSavePanel { + eSaveDialogShown.fulfill() + return sheet + } + try await Task.sleep(interval: 0.01) + } + } + + // Hit Cmd+S + let keyDown = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "s", charactersIgnoringModifiers: "s", isARepeat: false, keyCode: UInt16(kVK_ANSI_S))! + let keyUp = NSEvent.keyEvent(with: .keyUp, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "s", charactersIgnoringModifiers: "s", isARepeat: false, keyCode: UInt16(kVK_ANSI_S))! + window.sendEvent(keyDown) + window.sendEvent(keyUp) + + if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) { + getSaveDialog.cancel() + } + let saveDialog = try await getSaveDialog.value + + guard let url = saveDialog.url else { + XCTFail("no Save Dialog url") + return + } + try? FileManager.default.removeItem(at: url) + + // wait until file is saved + let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in + FileManager.default.fileExists(atPath: url.path) + }.timeout(5).first().promise() + defer { + try? FileManager.default.removeItem(at: url) + } + + window.endSheet(saveDialog, returnCode: .OK) + + _=try await fileSavedPromise.value + try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl)) + } + +} + +private extension NSView { + + private static var willOpenMenuWithEvent: ((NSMenu, NSEvent) -> Void)? + + private static let originalWillOpenMenu = { + class_getInstanceMethod(NSView.self, #selector(NSView.willOpenMenu))! + }() + private static let swizzledWillOpenMenu = { + class_getInstanceMethod(NSView.self, #selector(NSView.swizzled_willOpenMenu))! + }() + private static let swizzleWillOpenMenuOnce: Void = { + method_exchangeImplementations(originalWillOpenMenu, swizzledWillOpenMenu) + }() + + static func swizzleWillOpenMenu(with willOpenMenuWithEvent: ((NSMenu, NSEvent) -> Void)?) { + _=swizzleWillOpenMenuOnce + self.willOpenMenuWithEvent = willOpenMenuWithEvent + } + + @objc dynamic func swizzled_willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + if let willOpenMenuWithEvent = Self.willOpenMenuWithEvent { + willOpenMenuWithEvent(menu, event) + } else { + self.swizzled_willOpenMenu(menu, with: event) // call original + } + } + +} diff --git a/IntegrationTests/Tab/test.pdf b/IntegrationTests/Tab/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fd772c0fc07b00ee2c977018a9fa236bfa52f6a8 GIT binary patch literal 10855 zcmdUVWk8f$*ES`{kP1=~1JXSMLr8a*grqPGLpMW%2oh2f(g;Y4N~3hQK`9_2B`qzA zNXT~&9?!w!dC&8_Ki(hT3^B82@3q!mYwftM*(}=fiaZcr0T4^q;-|&6;@t;BT|FQH zFhAJE(jFu(4(3y|b3`HCf&Y$h6jC0EaIr#y`P7llHYi&#L=*~^lmvO8+>vl6kPoI= zTsx>knoxf0IBKT|uQt=GHdA`F{bT#>2Nkn;$_iber{ATDo-(Nt_Y>Y*pCraZRT$rB z>Ze-otC$_0y(2c#z;@qn$naRjL-)aG*oQ05x0O@I;xECsTwl+|HOEsR=>Zn}A&_%%MggCDfYEUT8fYM`?BKF4K424m zKmZek@j^wwB7zWJ5m7S`5WhdHp?m*~y0*IuLKlewn*b)|6~TOZNFNl~49q9z;^^Y8 z>k3C8!RX}3c|gFhQzMd+K#Bmnr(!)l2v`U$2Jz|XLBYa5Tl{59^o%V{z)wCIbe5fg z6odKXkzRHPRK6pe-`4Wy! zq{=OAtng4)FZpwd(qqHxN z_2#HqB0c?~a!>W%Chsk)#!2p0ZB~!5lb{49VL4`843NU8koZkj%0;Xs-Rf>?=n2mA z&&eFna0rwl7eDU_(|lLC<6iL+S7U!#D(gZnjnM#glVEmCqxN(L%Upg8POfPh%dAa) z03NbgPbC9m@hki5*ZcR?G5E>T&0j9F<~l|~709VKBR^qboKNZ!ZE`1${w8Xhjp0*d zvk-+7sbX1lVLZ)lai!1tV5eI5nkQLdb}#OEt0nuhacw+|lR4S@x5k1+#7FQj3;h7-NTcy?=|LX7sjQpbx~d7n%7 z@hICS=Tz$`B#CP~DCaJ3V{nI=cicNK-FF^)SkdtO%UiF@0qKhV0~tfMl8I>X9vYDvap8gH$263wyh z_%v}&lW&EPw0mq@1~9gOf@p*2XW|%oTG`_9jd2`8NElPV%#O@@q%`N3&mV_g=n&Ki zeyD|F3McjLGPUHWy!%$BNrjeG^B%cMI5XunW{&ilU_BLkCNowE)@H_g{a3A&+^LMD zZ)lY<=YY*wND`&Alh|3wwNz;pWG6I5RqLgFrG4cJuMs_Teq;D3ts%=;t64r!HIPr~ zoU@9cy~ha-$!n?pELut9sotsPyg6*`jA-u?jV)?sN%3{I@Qs8u4pS~0>>$Vemmy3lu|u12Hg>O15+emW)L zg2UJMR=YnltTcbdN3tMI7TFhR7iku&pI^i6;J4TpxJF$Vb1$YbCT>@7seW*Na8jt; z?#4+INAfpLXU>3Rrey16%;dO7>iRCD^v)j4 zG8a5qIEwgK(GclZcVPde^P|Mado&p|Ei}|L`ZTG6JuvDF=ZyZ0#f)Xa-WpqDf~Ppf zi^j`OZ}2ClZ@$#CW~q)A%`Gk4DCQZKESP#MQHpq2ZD^CPSv^2zmdI-&EK_S*tLrVR zrN(E-XI`$el`E-}Q<_uIs?=%{^t^2d{LUg;CbRuvZd7h7JR6=rhq&@6;qfqMzHz=- zj|k$KThxx^ya{VXWR^;zN^HM|?%OxhdKL|XHG_l>OSY_nk713yc@^)CjGictJrL~_ z?+eKH&HTniAQ5dCokcN4Va~n5t;@5<&0xe~bk5MJ=3~V{=ew>92Qx%qZ@B$DqI#fc zYUycd3D1mI>qekhhFQfFMWbfp;2!ZF(;@321WzKuC!(47>syo;dt+$e=!LbFxw?U! zn0Y)lf40ikD#bHdMgtrJL<9K*gp}KqZ+NKOhu!AtXx$Tdt$1V1eOz4p~UX_fL zjFlv7@oIS-$hQwa#ya*onA#oRUOpH<)WQ+Ld4j(JD!@_1F~E}~fZ|^~=ZVwVj@jNA z!oW9T&TRUI0!#Kt&MQnDA4X_)>A-`Ems{`&>$-29zRyh`ZyX*=1$nuyM&-D^kv^s9 zSPC@?Z^eG4ZiP`L4F!LNR0XwELvEw$1;rTS7;upU1;gb_{>)zPC0|FT)FS5BKY->p zc?C>Us@LDSKV2+&F<2IF-frZ^Nq?DvZie(ElB}a-QN8T5dA;`{&iK8?h{MF?9W zti?Hjos=iGUnstaMP8n9j+%a>^FXJf#lp6FYpF-JURGzWnP4Y5ebsg-m2v8Zzn^>3Ik=Yhv7yx_$UI1XLwP4*P$^8Qu(u=5 zb5!ijxbQ>aB%zMj#GcA|w_cay*)MYbWRF+11@i0naYPtJcRuv)Gi+rOick1P1wLA3 zJI>jAHe?)SoICb@Eb>`_ke$D}|IF&G*I9G}bUad-Cjp;h_R=$|pWCPnCJbDnO>FnuU?9vWl|l7s09JH+JgXOD3BSkMvU?+mwl#i3Mf_HgAL< z$BxlAW~Z31-?UqOy5-co>RNrAGGBVP_VbtwIeRMqH*^B!=l>fz zfjD$Z6NY)8L@5<}Jq$?P;-(8Fbc3SK*SHSVhZYoeF_xW3=`9z@MdPkYWDgbN^QDnC zUDIU7jaEuaW zZAKLH;H<%fCgGwvSEEuyuYBn;q8^TEXajA8GNaxd&6l)c4hV=E($#P78C6Nc5)?c( zFJxl1oV#gEo%t;3KvQ(?`@!DLQ1J%=9cK0Q-qDp@1J5SK=7c{HNooBaGYA@0`~(jF z4NL(J^QXAvPn>cZ(lbB>^bbG<9m=1u2;Kju!T)Qd0yztd{w>|VgVAPtY#GAPL;om_ z`HqIs@2VE2`|C_$?ZIJh+TY^0QwG$3jj-J)xRWkRlO~hH+GG(Tz@o8m?qa(9PGARD z%MgjcyvS{rjJTA0D;$(D27N(GBkHv%@+SM|G12EvCP?lNGgKJo&X5e6;5-?B>liWl zjhsEIiV11S6T>ds?k0nu-9%96KWo=U+#^73Xj1Q4?e~S9B|#nnBpdXd=)DqMW6Ri{ zro{XzkZf7Q2RALR6CrDbA27VXC3fc@(tH+`|5s`LfeZo6_y-(44dwqL%|9n28fpHY zrx^`Wf7aBG(dnYxJrO89cO>$fiwg?Orv`?cg4(l{h!*@)F9k=W6PQmI0JAO#PbV~3 z=hFs5egI)WkqHHAk9dHg$3f#x1Ofj9zOftsC_ z2Rej5B6#X72=c>}*;zAZ7wR8&z}+2GoUL6>xeN3gV9^J1 zj|6l9IsByxK!4=$uf^zRmVWvJ<})-h20Z)0uK+Lp+&F%(l~clmPuJ5DbxQ8&0XBd5 ziQagAd?M7S^p6Eq!5N^(?3{t|X}}TJ zT%6#}e@PVW+&xfows3c_0KlcF!~b{#0cx7>x}6ou79CQkum}hp&wqcyU_xN@@4vU= z|1UKCPa0qX=&6E!p1uOXgwd<;uTPjLI^{qMV4D?yz<|l~S5i&`@@uK0!}(XpeuPjH z?gRw$e+aQ7+y-FKfLNh>$^!Hm$b?4}1_AR3ia-G7QA8LF;TIMLI58EVrtAW3TXVNoej|PC$;+2(8t9LI2r0Svu3)j3nOR7uhlH8gLiT~WY&YzIzB<29 za36`JyguW1a4!A^-pBo?tm4b>Dh-T_Kty73gZ|SVoBq?g_rC@X>I>x@ez;1_lzJ!f zW!BYJuV5;M$7HYgG|k>8H3S|6)_fPB9LV1KQhvcQTjcO1flWi;n$-EqEypM2S(|(P zy|&SpwNZyCkP~YDXlZwoqHNAAG5Ys;ZfbE|=Fv($R@4+{rvCjaOof^4Ev< zaa-MK){-YIzW&Z?R(Y34)!gan>a^O%ReZCs+9I}`w9t|oW}d9aF0YG+;CAeZP-b0Y z<7uJp6b5Hb9y_&nS5bay#wkK$nl70L#pvKKr3MD?SbH;;xBZ(qTn3+p1iCekKI*ue zKG}XHtz|SMZ14;B1DM`~Nt%^Px$E}K_ie9Ha&opB7U`Djut??DChy=7Jt-g8y}03EV$#mD-ZjMTo|%}9CAR3HURfxV zB{s4k=MhD_08%ip+8=(@xhL(nXy}2T-S?jCkS;0lfQ_AQT`a0z&edh;0cu|$?~NN@ z(%tuDg##_cwK|*~Hf+UjJiS$uwHRv#x8IvlwY)*@kC;)>H^;)i77MJe9ul!NZ;v7P z*AHW;_GSn=gZ(Y{rDtAGQW1A7W8a^vl~s(jU(wh=ak<_6K&{J-Pz&KQXHKfEj4)+ekb{eDlqcNw9d?fmibQYh3`QO9)mUK zO26DiI)*FqA1zyA&(QgWnF(m5m1X8s`|w;If?fkBeGfmT$$r;&wS1VJ%jk`^v}@kL zI>yA~G}{LJ(p}DvkW^e}Qu2X9){kaFeaU;qkBci@!@|<)J7VxEK=c*TvKy`GuOD(h z&(Gr?dC}Nf7}l|AwD-!Syz)gY=ek+NN4zP9D)YRqpi&LiYP#FDWUZL9ub4^egFm?= zre(|S$$Y|W(;UF7d?j-cL9S0YjO^ZNBP;ntxHvjhUW z(T#pRnu(+1FpGd4H+tXP=C~E!{f9b*a`)v0@4Hh`q6sEye!WuIKEk}e{A6`XV5qXJrM2v? zr{|N$2s661kA~NK8?KIxR-O^{P~*UzHgQE5SZ^TII^Sliev+6LD~MWIu+i?8CzJNZ z5TOWfu@~u4BHo-074JUY)nRheue>|oWUrnR_1G_Vli^+|fA`?J zDMgj7sP}ev{=s`PVj(|~Vi>fg@8ync-F-qmPQ>ca`N5|v(@HM~4pccWrkI9SGp+bA2MtTFAim4$+{$70k zbx{drafXBX>^H15>-Hku%wrF~dWiJs3Mq!(Bo-)ZGl82kx`wq~qFWHU0=d0MY$_duy{Kwoi;3N-2~I6rh<<&AhzZhwohpH%1PpMwAtd@RbCm5%H3=>{`QztpMThh&N=@Vk&cEY-1HFVg=&+h<*$g^8#i}qvruwv6U zC?-=7o>`h9&_`>m>SGdb>P)Wb>m1Z27pW zoS7nZ7mq(JMfAIOG4?!)jI8=Vp8wkJV7aqIZ!Y&~%=d-B{(e*DQT9;W_lbN)u~*HH z!mk+_Sbiy~DBzRNi;e0)Si8AoXoMi7RCh@xnX!r8oMn zldWsC@z_%!dg*f{+f9qLwr%^Y+a{RAm^`zz+gRHmS85-!I#3hdwn>tSf!8hWAg)_t z`659BtL!3l5k|amyO;I2@0`=5EAJQo9v|OOsKhy_H?Z)5(}Nj0F}ym4vJGf}=?1*% zT&W?`D|C3I7Bg6I5pPt-x?aD)%ls`1Gl_&5r$lq2f$odO9Ys+l%B^UnRJ98wbzL4Y ziTs;GQX%D>-(-5Vx1-HIA~SF(VSR}gIE*P|P;^lwJ}Rs3dGJ&kSvhPr8j<0)NL)=p z*SugdqWiY9@~0v~P26^o!7*lI68`hP;k`F1Q%e-MY2~KfoqZrRSU6;jVUXhF7^=04 zh1T15hmpP4`_lX%cGD!i8aHz2)k{cTJ<45|u?t|O^iuoyknO@wmA-C)y9k{TC&#zs zCDmyLwMG)#M<-(S-JuT5H(vC7s=CbLAh_U(r}zZhSu{Q?HujFH(W}(XJT|7oPR++t z#UV;f1g}XCh3o}6FDZcC$9*+-nigH6j@+N+E(S6@gA%64HG2EGK}NXnnk>`Z5?ryZ z)xl}DGHY|*b$6R~$>kT`3y#KKAq+k39HadfI=pvS3i+&Ji1d$slHA*|OvbfoO}K0kTNKPW{|(sLj{ zIW9byW$f=tcXEm81Q!$d;gbi{+aF+NY4YZv%^Gv>BE>5XmPS|0VBw~WN zV)v|WCS@7u%bw3qPpnGPmz9JajT;J&GDux~$5VK*EUR#_%vXGF-W}3)aPu`ukh8}> zFpJQ$+}jUi{2yT4?>Rk%aX@1Jzd~_8IGr;n?ksok6U9M90Ob1<#fiYsWBiA^GG{?t zslJ95nGSigi}k@dedCrV#Lv|^Lx&O#wh0i*kvtb7al5-?2y%mi$F#x5vAlv)b?ISl z^H}pFuA8fJl#0&ZkMyvlR3f?MfM$E;Oxt3FB~HS*vZ?FCTt2Fl4ey zZqfC4{{DrHQgY8wbH*D4&5QaU!)@=_Bvb z#L&3=RgoNF5sUKEMYtIEnd}#5APvDz0c*4{5M~sJf&H$l-^-%Dy1l4MwuV>F=qG)z zHefybu<%MUNtH`A_kQhL@>}uY`PaKO)w!;uyoI@IsC|qoNy`@~g61Vf38cJPG{hm) z9b~7_uth!@hmAg8t!jm#j%hzCHxb_jPrcEZOSz6Y=7toCCs&T+NO>z!W;Xi7_{KAL zUo!*!LW|N^hcV+D2A*_AGC3SnyK9a2}c;JOh=ZoSTr`79+ta0 zl%=d^;?okF5PyXzoA`_mE4K?FXiMC==1a%{T}luy)4Qs+uA!+gccDzWrDsx;Qg;Lu zUrjsRZi*$|bz}4#PHVN%d-1xP3->$bs&Km9BhlY?B{^1J3*= z6A=BauK*Oyum4S-h#;^B{3;WH!T{3sSDBy)|L- z-}=I!{J+_S2@3yaS40>%y!@@N2;lT@GEvdr{1X)wJYyH-4hNV>_tU$Zx^{j@poV}e zis&y;pg#zEzEi6GbdmsHR;b7Ww6i!u$O (WKDownloadMock, WebKitDownloadTask, UUID) { setUpCoordinator() - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) let task = WebKitDownloadTask(download: download, promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: isBurner) let e = expectation(description: "download added") @@ -144,7 +144,7 @@ final class DownloadListCoordinatorTests: XCTestCase { func testWhenDownloadAddedThenDownloadItemIsPublished() { setUpCoordinator() - let task = WebKitDownloadTask(download: WKDownloadMock(), promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: false) + let task = WebKitDownloadTask(download: WKDownloadMock(url: .duckDuckGo), promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: false) let e = expectation(description: "download added") let c = coordinator.updates.sink { [coordinator] (kind, item) in @@ -245,7 +245,7 @@ final class DownloadListCoordinatorTests: XCTestCase { webView.resumeDownloadBlock = { data in resumeCalled.fulfill() XCTAssertEqual(data, .resumeData) - return WKDownloadMock() + return WKDownloadMock(url: .duckDuckGo) } webView.startDownloadBlock = { _ in XCTFail("unexpected start call") @@ -298,7 +298,7 @@ final class DownloadListCoordinatorTests: XCTestCase { webView.resumeDownloadBlock = { data in resumeCalled.fulfill() XCTAssertEqual(data, .resumeData) - return WKDownloadMock() + return WKDownloadMock(url: .duckDuckGo) } webView.startDownloadBlock = { _ in XCTFail("unexpected start call") @@ -354,7 +354,7 @@ final class DownloadListCoordinatorTests: XCTestCase { webView.startDownloadBlock = { request in startCalled.fulfill() XCTAssertEqual(request?.url, item.url) - return WKDownloadMock() + return WKDownloadMock(url: .duckDuckGo) } let downloadAdded = expectation(description: "download addeed") diff --git a/UnitTests/FileDownload/DownloadsTabExtensionTests.swift b/UnitTests/FileDownload/DownloadsTabExtensionTests.swift index 55b835e12f..c3747aea31 100644 --- a/UnitTests/FileDownload/DownloadsTabExtensionTests.swift +++ b/UnitTests/FileDownload/DownloadsTabExtensionTests.swift @@ -22,8 +22,10 @@ import UniformTypeIdentifiers @testable import DuckDuckGo_Privacy_Browser +@MainActor final class DownloadsTabExtensionTests: XCTestCase { private var testData: Data! + private var testOriginatingURL: URL! private let filename = "Document.pdf" private let fileManager = FileManager.default private var testDirectory: URL! @@ -34,6 +36,9 @@ final class DownloadsTabExtensionTests: XCTestCase { testData = try XCTUnwrap("test".data(using: .utf8)) testDirectory = fileManager.temporaryDirectory + testOriginatingURL = testDirectory.appendingPathComponent(UUID().uuidString + ".pdf") + try testData.write(to: testOriginatingURL) + cancellables = [] } @@ -44,27 +49,61 @@ final class DownloadsTabExtensionTests: XCTestCase { try super.tearDownWithError() } - @MainActor - func testWhenAlwaysRequestDownloadLocationIsTrueThenShouldAskDownloadsTabExtensionToSaveData() throws { + func testWhenAlwaysRequestDownloadLocationIsTrueThenShouldAskDownloadsTabExtensionToSaveData() async throws { // GIVEN let expectedURL = testDirectory.appendingPathComponent(filename) let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: true) XCTAssertFalse(fileManager.fileExists(atPath: expectedURL.path)) // WHEN - sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf") sut.$savePanelDialogRequest .sink { request in request?.submit( (url: expectedURL, fileType: nil)) } .store(in: &cancellables) + _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf", + originatingURL: testOriginatingURL.appendingPathExtension("fake")) // THEN XCTAssertTrue(fileManager.fileExists(atPath: expectedURL.path)) } - @MainActor - func testWhenSaveDataAndFileExistThenURLShouldIncrementIndex() throws { + func testWhenAlwaysRequestDownloadLocationIsTrueAndNoDataProvidedAndOriginatingFileURLProvidedThenShouldAskDownloadsTabExtensionToSaveData() async throws { + // GIVEN + let expectedURL = testDirectory.appendingPathComponent(filename) + let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: true) + XCTAssertFalse(fileManager.fileExists(atPath: expectedURL.path)) + + // WHEN + sut.$savePanelDialogRequest + .sink { request in + request?.submit( (url: expectedURL, fileType: nil)) + } + .store(in: &cancellables) + _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL) + + // THEN + XCTAssertTrue(fileManager.fileExists(atPath: expectedURL.path)) + } + + func testWhenSaveDataAndFileExistThenURLShouldIncrementIndex() async throws { + // GIVEN + let destURL = testDirectory.appendingPathComponent(filename) + let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) + let expectedFilename = "Document 1.pdf" + let expectedDestURL = testDirectory.appendingPathComponent(expectedFilename) + try testData.write(to: destURL) + XCTAssertFalse(fileManager.fileExists(atPath: expectedDestURL.path)) + + // WHEN + _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf", + originatingURL: testOriginatingURL.appendingPathExtension("fake")) + + // THEN + XCTAssertTrue(fileManager.fileExists(atPath: expectedDestURL.path)) + } + + func testWhenNoDataProvidedAndOriginatingFileURLProvidedAndFileExistsThenFileShouldBeCopiedIncrementingIndex() async throws { // GIVEN let destURL = testDirectory.appendingPathComponent(filename) let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) @@ -74,21 +113,34 @@ final class DownloadsTabExtensionTests: XCTestCase { XCTAssertFalse(fileManager.fileExists(atPath: expectedDestURL.path)) // WHEN - sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf") + _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL) // THEN XCTAssertTrue(fileManager.fileExists(atPath: expectedDestURL.path)) } - @MainActor - func testWhenSaveDataAndFileDoesNotExistThenURLShouldNotIncrementIndex() throws { + func testWhenSaveDataAndFileDoesNotExistThenURLShouldNotIncrementIndex() async throws { + // GIVEN + let destURL = testDirectory.appendingPathComponent(filename) + let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) + XCTAssertFalse(fileManager.fileExists(atPath: destURL.path)) + + // WHEN + _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf", + originatingURL: testOriginatingURL.appendingPathExtension("fake")) + + // THEN + XCTAssertTrue(fileManager.fileExists(atPath: destURL.path)) + } + + func testNoDataProvidedAndOriginatingFileURLProvidedAndFileDoesNotExistThenFileShouldBeCopiedNotIncrementingIndex() async throws { // GIVEN let destURL = testDirectory.appendingPathComponent(filename) let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false) XCTAssertFalse(fileManager.fileExists(atPath: destURL.path)) // WHEN - sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf") + _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL) // THEN XCTAssertTrue(fileManager.fileExists(atPath: destURL.path)) diff --git a/UnitTests/FileDownload/FileDownloadManagerTests.swift b/UnitTests/FileDownload/FileDownloadManagerTests.swift index 547646db35..02cc8b0c95 100644 --- a/UnitTests/FileDownload/FileDownloadManagerTests.swift +++ b/UnitTests/FileDownload/FileDownloadManagerTests.swift @@ -56,6 +56,7 @@ final class FileDownloadManagerTests: XCTestCase { FileManager.restoreUrlsForIn() self.chooseDestination = nil self.fileIconFlyAnimationOriginalRect = nil + preferences.alwaysRequestDownloadLocation = false } func testWhenDownloadIsAddedThenItsPublished() { @@ -64,7 +65,7 @@ final class FileDownloadManagerTests: XCTestCase { e.fulfill() } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto) withExtendedLifetime(cancellable) { @@ -78,7 +79,7 @@ final class FileDownloadManagerTests: XCTestCase { e.fulfill() } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto) struct TestError: Error {} @@ -96,7 +97,7 @@ final class FileDownloadManagerTests: XCTestCase { e.fulfill() } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto) download.delegate?.downloadDidFinish!(download.asWKDownload()) @@ -122,7 +123,7 @@ final class FileDownloadManagerTests: XCTestCase { callback(nil, nil) } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .prompt) let url = URL(string: "https://duckduckgo.com/somefile.html")! @@ -155,7 +156,7 @@ final class FileDownloadManagerTests: XCTestCase { callback(localURL, .html) } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) let url = URL(string: "https://duckduckgo.com/somefile.html")! @@ -177,7 +178,7 @@ final class FileDownloadManagerTests: XCTestCase { fm.createFile(atPath: localURL.path, contents: nil, attributes: nil) XCTAssertTrue(fm.fileExists(atPath: localURL.path)) - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .prompt) self.chooseDestination = { _, _, _, callback in callback(localURL, nil) @@ -194,11 +195,38 @@ final class FileDownloadManagerTests: XCTestCase { XCTAssertFalse(fm.fileExists(atPath: localURL.path)) } + func testWhenDownloadingLocalFileThenLocationChooserIsCalled() { + let downloadsURL = fm.temporaryDirectory + preferences.selectedDownloadLocation = downloadsURL + + let download = WKDownloadMock(url: URL(fileURLWithPath: "/some/path")) + + let localURL = downloadsURL.appendingPathComponent(testFile) + let e1 = expectation(description: "chooseDestinationCallback called") + self.chooseDestination = { suggestedFilename, directoryURL, fileTypes, callback in + dispatchPrecondition(condition: .onQueue(.main)) + XCTAssertEqual(directoryURL, downloadsURL) + e1.fulfill() + + callback(localURL, .html) + } + + dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) + + let e2 = expectation(description: "WKDownload called") + download.delegate?.download(download.asWKDownload(), decideDestinationUsing: response, suggestedFilename: testFile) { [testFile] url in + XCTAssertEqual(url, downloadsURL.appendingPathComponent(testFile).appendingPathExtension(WebKitDownloadTask.downloadExtension)) + e2.fulfill() + } + + waitForExpectations(timeout: 0.3) + } + func testWhenNotRequiredByPreferencesThenDefaultDownloadLocationIsChosen() { let downloadsURL = fm.temporaryDirectory preferences.selectedDownloadLocation = downloadsURL - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) self.chooseDestination = { _, _, _, _ in XCTFail("Unpected chooseDestination call") @@ -217,7 +245,7 @@ final class FileDownloadManagerTests: XCTestCase { let downloadsURL = fm.temporaryDirectory preferences.selectedDownloadLocation = downloadsURL - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) self.chooseDestination = { _, _, _, _ in XCTFail("Unpected chooseDestination call") @@ -240,7 +268,7 @@ final class FileDownloadManagerTests: XCTestCase { [URL(fileURLWithPath: "/")] } - let download = WKDownloadMock() + let download = WKDownloadMock(url: .duckDuckGo) dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto) let e = expectation(description: "WKDownload called") diff --git a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift index ed2ce03187..f2e14bf8d2 100644 --- a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift +++ b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift @@ -27,10 +27,12 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol { private(set) var didCallSaveWebViewContent = false private(set) var capturedWebView: WKWebView? + @Published private(set) var didCallSaveDownloadedData = false private(set) var capturedSavedDownloadData: Data? private(set) var capturedSuggestedFilename: String? private(set) var capturedMimeType: String? + private(set) var capturedOriginatingURL: URL? var savePanelDialogSubject = PassthroughSubject() @@ -40,16 +42,19 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol { savePanelDialogSubject.eraseToAnyPublisher() } - func saveWebViewContentAs(_ webView: WKWebView) { + func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) { didCallSaveWebViewContent = true capturedWebView = webView } - func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) { + func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? { didCallSaveDownloadedData = true capturedSavedDownloadData = data capturedSuggestedFilename = suggestedFilename capturedMimeType = mimeType + capturedOriginatingURL = originatingURL + + return nil } func chooseDestination(suggestedFilename: String?, directoryURL: URL?, fileTypes: [UTType], callback: @escaping @MainActor (URL?, UTType?) -> Void) {} diff --git a/UnitTests/FileDownload/Helpers/WKDownloadMock.swift b/UnitTests/FileDownload/Helpers/WKDownloadMock.swift index a00a8a1bf4..acff9884a1 100644 --- a/UnitTests/FileDownload/Helpers/WKDownloadMock.swift +++ b/UnitTests/FileDownload/Helpers/WKDownloadMock.swift @@ -26,6 +26,10 @@ final class WKDownloadMock: NSObject, WebKitDownload, ProgressReporting { var progress = Progress() weak var delegate: WKDownloadDelegate? + init(url: URL) { + self.originalRequest = URLRequest(url: url) + } + var cancelBlock: (() -> Void)? @objc func cancel() { cancelBlock?() diff --git a/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift b/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift index 7fe993f1e2..c0f52330be 100644 --- a/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift +++ b/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift @@ -57,13 +57,22 @@ final class TabWKUIDelegateTests: XCTestCase { XCTAssertNil(downloadExtensionMock.capturedSavedDownloadData) XCTAssertNil(downloadExtensionMock.capturedMimeType) + let eDidCallSaveDownloadedData = expectation(description: "didCallSaveDownloadedData") + let c = downloadExtensionMock.$didCallSaveDownloadedData.sink { didCall in + if didCall { + eDidCallSaveDownloadedData.fulfill() + } + } + // WHEN sut.webView(WKWebView(), saveDataToFile: testData, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: originatingURL) // THEN - XCTAssertTrue(downloadExtensionMock.didCallSaveDownloadedData) + waitForExpectations(timeout: 1) XCTAssertEqual(downloadExtensionMock.capturedSavedDownloadData, testData) XCTAssertNotNil(downloadExtensionMock.capturedMimeType, "application/pdf") + + withExtendedLifetime(c) {} } } diff --git a/UnitTests/Tab/Model/TabTests.swift b/UnitTests/Tab/Model/TabTests.swift index faf05897e4..ed15925879 100644 --- a/UnitTests/Tab/Model/TabTests.swift +++ b/UnitTests/Tab/Model/TabTests.swift @@ -120,11 +120,15 @@ final class TabTests: XCTestCase { DownloadsPreferences().alwaysRequestDownloadLocation = true tab.webView(WebViewMock(), saveDataToFile: Data(), suggestedFilename: "anything", mimeType: "application/pdf", originatingURL: .duckDuckGo) var expectedDialog: Tab.UserDialog? + let expectation = expectation(description: "savePanelDialog published") tab.downloads?.savePanelDialogPublisher.sink(receiveValue: { userDialog in - expectedDialog = userDialog + if let userDialog { + expectation.fulfill() + expectedDialog = userDialog + } }).store(in: &cancellables) - XCTAssertNotNil(expectedDialog) + waitForExpectations(timeout: 1) // WHEN tab.url = .duckDuckGoMorePrivacyInfo diff --git a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift index 806c139493..b569bdb58d 100644 --- a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift +++ b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift @@ -49,4 +49,10 @@ final class WKWebViewPrivateMethodsAvailabilityTests: XCTestCase { XCTAssertEqual(pagePrefs.customHeaderFields, customHeaderFields.map { [$0] }) } + func testWKPDFHUDViewClassAvailable() { + XCTAssertNotNil(WKPDFHUDViewWrapper.WKPDFHUDViewClass) + XCTAssertTrue(WKPDFHUDViewWrapper.WKPDFHUDViewClass?.instancesRespond(to: WKPDFHUDViewWrapper.performActionForControlSelector) == true) + XCTAssertTrue(WKPDFHUDViewWrapper.WKPDFHUDViewClass?.instancesRespond(to: WKPDFHUDViewWrapper.setVisibleSelector) == true) + } + } From be1526113865a5ea4e4f1af365fe34ef8abecd99 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Fri, 1 Mar 2024 00:53:27 -0500 Subject: [PATCH 04/19] Check entitlement periodically and while rekeying NetP (iOS) (#2167) Task/Issue URL: https://app.asana.com/0/0/1206409081785856/f **Description**: This PR points to the latest BSK that adds subscription support to the iOS version. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../EventMapping+NetworkProtectionError.swift | 3 +++ .../NetworkProtection+ConvenienceInitializers.swift | 12 +++++++++--- .../NetworkProtectionTunnelController.swift | 3 +++ .../VPNLocation/VPNLocationViewModel.swift | 3 ++- .../NetworkProtectionUNNotificationsPresenter.swift | 4 ++++ .../MacPacketTunnelProvider.swift | 10 ++++++++-- DuckDuckGoVPN/NetworkProtectionBouncer.swift | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/LoginItems/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- LocalPackages/SystemExtensionManager/Package.swift | 2 +- ...etworkProtectionAgentNotificationsPresenter.swift | 4 ++++ 16 files changed, 43 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index aae4f772c1..6e8e3b5e8b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13572,7 +13572,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 114.0.0; + version = 114.1.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fb698a0306..926e61f746 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "c9eae1b4a4ce5b6854d6934e57f900f62d2f3917", - "version" : "114.0.0" + "revision" : "045a8782c3dbbf79fc088b38120dea1efadc13e1", + "version" : "114.1.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift index 60b9120fb0..b363c3d843 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift @@ -98,6 +98,9 @@ extension EventMapping where Event == NetworkProtectionError { domainEvent = .networkProtectionUnhandledError(function: function, line: line, error: error) frequency = .standard return + case .vpnAccessRevoked: + // todo + return } let debugEvent = DebugEvent(eventType: .custom(domainEvent)) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 0838bb0c2d..a32ca7ee85 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -28,7 +28,11 @@ extension NetworkProtectionDeviceManager { let settings = VPNSettings(defaults: .netP) let keyStore = NetworkProtectionKeychainKeyStore() let tokenStore = NetworkProtectionKeychainTokenStore() - return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents) + return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, + tokenStore: tokenStore, + keyStore: keyStore, + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false) } } @@ -37,14 +41,16 @@ extension NetworkProtectionCodeRedemptionCoordinator { let settings = VPNSettings(defaults: .netP) self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents) + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false) } } extension NetworkProtectionKeychainTokenStore { convenience init() { self.init(keychainType: .default, - errorEvents: .networkProtectionAppDebugEvents) + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index e86a508f2d..3eb54f6f2b 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -242,6 +242,9 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr .setDisableRekeying: // Intentional no-op as this is handled by the extension or the agent's app delegate break + case .setShowEntitlementAlert, .setShowEntitlementNotification: + // todo + break } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index bc0a0e042f..f77d8fd032 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -191,7 +191,8 @@ extension NetworkProtectionLocationListCompositeRepository { self.init( environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false ) } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index 4237c816d2..3f8776fab2 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -140,6 +140,10 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.test, content) } + func showEntitlementNotification(completion: @escaping (Error?) -> Void) { + // todo + } + private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) { let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none) diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index af8f42cb74..ee8e936733 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -123,6 +123,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { .failedToParseLocationListResponse: // Needs Privacy triage for macOS Geoswitching pixels return + case .vpnAccessRevoked: + // todo + return } PixelKit.fire(domainEvent, frequency: .dailyAndContinuous, includeAppVersionParameter: true) @@ -226,7 +229,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, - errorEvents: debugEvents) + errorEvents: debugEvents, + isSubscriptionEnabled: false) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) super.init(notificationsPresenter: notificationsPresenter, @@ -236,7 +240,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { tokenStore: tokenStore, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, - settings: settings) + settings: settings, + isSubscriptionEnabled: false, + entitlementCheck: nil) observeServerChanges() observeStatusUpdateRequests() diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 261121df37..2f3d8d2bdb 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -30,7 +30,7 @@ final class NetworkProtectionBouncer { /// current app. /// func requireAuthTokenOrKillApp() { - let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil) + let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false) guard keychainStore.isFeatureActivated else { os_log(.error, log: .networkProtection, "🔴 Stopping: Network Protection not authorized.") diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index a7379fd36f..5ee8f07b79 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index 1fe740a36b..5cfb2fdc98 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "1.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.1.0"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 93dab2b900..06a84d4629 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index f05a6e354a..87ca60df92 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 3051644e58..e970226600 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "1.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.1.0"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 618cd73010..c109fc05ba 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,7 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "1.0.0"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "114.1.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift index 7d788892e1..f13d10b2a0 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift +++ b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift @@ -51,4 +51,8 @@ final class NetworkProtectionAgentNotificationsPresenter: NetworkProtectionNotif func showTestNotification() { notificationCenter.post(.showTestNotification) } + + func showEntitlementNotification(completion: @escaping (Error?) -> Void) { + // todo + } } From 0547b52a44ade0ee8f334b292d62d48affadba38 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 12:14:21 +0600 Subject: [PATCH 05/19] Open last valid downloads folder if no files downloaded to default (#2264) Task/Issue URL: https://app.asana.com/0/1199230911884351/1203968847972801/f --- .../View/DownloadsViewController.swift | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift index 21c286ce61..5af745c713 100644 --- a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift +++ b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift @@ -142,13 +142,36 @@ final class DownloadsViewController: NSViewController { // MARK: User Actions @IBAction func openDownloadsFolderAction(_ sender: Any) { - guard let url = DownloadsPreferences().effectiveDownloadLocation - ?? FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first - else { - return + let prefs = DownloadsPreferences() + var url: URL? + var itemToSelect: URL? + + if prefs.alwaysRequestDownloadLocation { + url = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + + if let lastDownloaded = viewModel.items.first/* last added */(where: { + // should still exist + $0.localURL != nil && FileManager.default.fileExists(atPath: $0.localURL!.deletingLastPathComponent().path) + }), + let lastDownloadedURL = lastDownloaded.localURL, + // if no downloads are from the default Downloads folder - open the last downloaded item folder + !viewModel.items.contains(where: { $0.localURL?.deletingLastPathComponent().path == url?.path }) || url == nil { + + url = lastDownloadedURL.deletingLastPathComponent() + // select last downloaded item + itemToSelect = lastDownloadedURL + + } /* else fallback to default User‘s Downloads */ + + } else { + // open preferred downlod location + url = prefs.effectiveDownloadLocation ?? FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first } + + guard let url else { return } + self.dismiss() - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path) + NSWorkspace.shared.selectFile(itemToSelect?.path, inFileViewerRootedAtPath: url.path) } @IBAction func clearDownloadsAction(_ sender: Any) { From 659f95f43f0dd68a5735ac030d7ddecef50b7536 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 12:48:52 +0600 Subject: [PATCH 06/19] AddressBar opt+return to download, cmd+opt to open new window (#2262) Task/Issue URL: https://app.asana.com/0/0/1202118286733117/f --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../NavigationActionExtension.swift | 1 + .../Model/WebKitDownloadTask.swift | 2 +- .../MainWindow/MainViewController.swift | 7 ++ .../MainWindow/MainWindowController.swift | 6 +- .../View/AddressBarTextField.swift | 81 ++++++++++--------- DuckDuckGo/Tab/Model/Tab.swift | 16 +++- ...NonexistentDomainNavigationResponder.swift | 2 +- .../TabExtensions/DownloadsTabExtension.swift | 7 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 4 + 10 files changed, 83 insertions(+), 45 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 926e61f746..c89d4445e6 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -156,7 +156,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift b/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift index f633e99656..41a11f3a84 100644 --- a/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift +++ b/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift @@ -43,4 +43,5 @@ extension NavigationAction { extension CustomNavigationType { static let userEnteredUrl = CustomNavigationType(rawValue: "userEnteredUrl") static let tabContentUpdate = CustomNavigationType(rawValue: "tabContentUpdate") + static let userRequestedPageDownload = CustomNavigationType(rawValue: "userRequestedPageDownload") } diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift index 4e96b1f482..00bec47783 100644 --- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift +++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift @@ -269,7 +269,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { // sometimes suggesteFilename has an extension appended to already present URL file extension // e.g. feed.xml.rss for www.domain.com/rss.xml if let urlSuggestedFilename = response.url?.suggestedFilename, - !urlSuggestedFilename.pathExtension.isEmpty, + !(urlSuggestedFilename.pathExtension.isEmpty || (self.suggestedFileType == .html && urlSuggestedFilename.pathExtension == "html")), suggestedFilename.hasPrefix(urlSuggestedFilename) { suggestedFilename = urlSuggestedFilename } diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 3b25f0524c..53fb90c68b 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -481,6 +481,13 @@ extension MainViewController { .subtracting(.capsLock) switch Int(event.keyCode) { + case kVK_Return where navigationBarViewController.addressBarViewController? + .addressBarTextField.isFirstResponder == true: + + navigationBarViewController.addressBarViewController?.addressBarTextField.addressBarEnterPressed() + + return true + case kVK_Escape: var isHandled = false if !mainView.findInPageContainerView.isHidden { diff --git a/DuckDuckGo/MainWindow/MainWindowController.swift b/DuckDuckGo/MainWindow/MainWindowController.swift index fd42a89970..a5830b5053 100644 --- a/DuckDuckGo/MainWindow/MainWindowController.swift +++ b/DuckDuckGo/MainWindow/MainWindowController.swift @@ -176,7 +176,11 @@ final class MainWindowController: NSWindowController { } func orderWindowBack(_ sender: Any?) { - window?.orderBack(sender) + if let lastKeyWindow = WindowControllersManager.shared.lastKeyMainWindowController?.window { + window?.order(.below, relativeTo: lastKeyWindow.windowNumber) + } else { + window?.orderFront(sender) + } register() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 36d445afbf..73ebe99437 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -285,22 +285,22 @@ final class AddressBarTextField: NSTextField { clearUndoManager() } - private func addressBarEnterPressed() { + func addressBarEnterPressed() { suggestionContainerViewModel?.clearUserStringValue() let suggestion = suggestionContainerViewModel?.selectedSuggestionViewModel?.suggestion - if NSApp.isCommandPressed { - openNewTab(selected: NSApp.isShiftPressed, suggestion: suggestion) - } else { - navigate(suggestion: suggestion) - } + navigate(suggestion: suggestion) hideSuggestionWindow() } private func navigate(suggestion: Suggestion?) { - hideSuggestionWindow() - updateTabUrl(suggestion: suggestion) + if NSApp.isCommandPressed { + openNew(NSApp.isOptionPressed ? .window : .tab, selected: NSApp.isShiftPressed, suggestion: suggestion) + } else { + hideSuggestionWindow() + updateTabUrl(suggestion: suggestion, downloadRequested: NSApp.isOptionPressed && !NSApp.isShiftPressed) + } currentEditor()?.selectAll(self) } @@ -322,7 +322,7 @@ final class AddressBarTextField: NSTextField { } } - private func updateTabUrlWithUrl(_ providedUrl: URL, userEnteredValue: String, suggestion: Suggestion?) { + private func updateTabUrlWithUrl(_ providedUrl: URL, userEnteredValue: String, downloadRequested: Bool, suggestion: Suggestion?) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { os_log("%s: Selected tab view model is nil", type: .error, className) return @@ -355,44 +355,57 @@ final class AddressBarTextField: NSTextField { #endif self.window?.makeFirstResponder(nil) - selectedTabViewModel.tab.setUrl(providedUrl, source: .userEntered(userEnteredValue)) + selectedTabViewModel.tab.setUrl(providedUrl, source: .userEntered(userEnteredValue, downloadRequested: downloadRequested)) + if downloadRequested { + updateValue(selectedTabViewModel: nil, addressBarString: nil) + } } - private func updateTabUrl(suggestion: Suggestion?) { + private func updateTabUrl(suggestion: Suggestion?, downloadRequested: Bool) { makeUrl(suggestion: suggestion, stringValueWithoutSuffix: stringValueWithoutSuffix, completion: { [weak self] url, userEnteredValue, isUpgraded in guard let url = url else { return } - if isUpgraded { self?.updateTabUpgradedToUrl(url) } - self?.updateTabUrlWithUrl(url, userEnteredValue: userEnteredValue, suggestion: suggestion) + if isUpgraded { + self?.updateTab(self?.tabCollectionViewModel.selectedTabViewModel?.tab, upgradedTo: url) + } + self?.updateTabUrlWithUrl(url, userEnteredValue: userEnteredValue, downloadRequested: downloadRequested, suggestion: suggestion) }) } - private func updateTabUpgradedToUrl(_ url: URL?) { - if url == nil { return } - let tab = tabCollectionViewModel.selectedTabViewModel?.tab - tab?.setMainFrameConnectionUpgradedTo(url) + private func updateTab(_ tab: Tab?, upgradedTo url: URL?) { + guard let tab, let url else { return } + tab.setMainFrameConnectionUpgradedTo(url) } - private func openNewTabWithUrl(_ providedUrl: URL?, userEnteredValue: String, selected: Bool, suggestion: Suggestion?) { - guard let url = providedUrl else { - os_log("%s: Making url from address bar string failed", type: .error, className) - return - } - - let tab = Tab(content: .url(url, source: .userEntered(userEnteredValue)), - shouldLoadInBackground: true, - burnerMode: tabCollectionViewModel.burnerMode) - tabCollectionViewModel.append(tab: tab, selected: selected) - } - - private func openNewTab(selected: Bool, suggestion: Suggestion?) { + enum TabOrWindow { case tab, window } + private func openNew(_ tabOrWindow: TabOrWindow, selected: Bool, suggestion: Suggestion?) { makeUrl(suggestion: suggestion, stringValueWithoutSuffix: stringValueWithoutSuffix) { [weak self] url, userEnteredValue, isUpgraded in + guard let self, let url else { + os_log("%s: Making url from address bar string failed", type: .error) + return + } + let tab = Tab(content: .url(url, source: .userEntered(userEnteredValue)), + shouldLoadInBackground: true, + burnerMode: tabCollectionViewModel.burnerMode) + + if isUpgraded { + updateTab(tab, upgradedTo: url) + } - if isUpgraded { self?.updateTabUpgradedToUrl(url) } - self?.openNewTabWithUrl(url, userEnteredValue: userEnteredValue, selected: selected, suggestion: suggestion) + if selected { + // reset address bar value + updateValue(selectedTabViewModel: nil, addressBarString: nil) + window?.makeFirstResponder(nil) + } + switch tabOrWindow { + case .tab: + tabCollectionViewModel.append(tab: tab, selected: selected) + case .window: + WindowsManager.openNewWindow(with: tab, showWindow: selected, popUp: false) + } } } @@ -1122,10 +1135,6 @@ extension AddressBarTextField: SuggestionViewControllerDelegate { func suggestionViewControllerDidConfirmSelection(_ suggestionViewController: SuggestionViewController) { let suggestion = suggestionContainerViewModel?.selectedSuggestionViewModel?.suggestion - if NSApp.isCommandPressed { - openNewTab(selected: NSApp.isShiftPressed, suggestion: suggestion) - return - } navigate(suggestion: suggestion) } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 55cbe5f303..09fc3be7a4 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -67,7 +67,7 @@ protocol NewWindowPolicyDecisionMaker { enum URLSource: Equatable { case pendingStateRestoration case loadedByStateRestoration - case userEntered(String) + case userEntered(String, downloadRequested: Bool = false) case historyEntry case bookmark case ui @@ -78,7 +78,7 @@ protocol NewWindowPolicyDecisionMaker { case webViewUpdated var userEnteredValue: String? { - if case .userEntered(let userEnteredValue) = self { + if case .userEntered(let userEnteredValue, _) = self { userEnteredValue } else { nil @@ -91,6 +91,8 @@ protocol NewWindowPolicyDecisionMaker { var navigationType: NavigationType { switch self { + case .userEntered(_, downloadRequested: true): + .custom(.userRequestedPageDownload) case .userEntered: .custom(.userEnteredUrl) case .pendingStateRestoration: @@ -260,6 +262,14 @@ protocol NewWindowPolicyDecisionMaker { userEnteredValue != nil } + var isUserRequestedPageDownload: Bool { + if case .url(_, credential: _, source: .userEntered(_, downloadRequested: true)) = self { + return true + } else { + return false + } + } + var displaysContentInWebView: Bool { isUrl } @@ -1010,7 +1020,7 @@ protocol NewWindowPolicyDecisionMaker { return nil } - if webView.url == url, webView.backForwardList.currentItem?.url == url, !webView.isLoading { + if webView.url == url, webView.backForwardList.currentItem?.url == url, !webView.isLoading, !content.isUserRequestedPageDownload { return reload() } if restoreInteractionStateIfNeeded() { return nil /* session restored */ } diff --git a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift index 713f737e12..52b1021406 100644 --- a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift @@ -33,7 +33,7 @@ final class SearchNonexistentDomainNavigationResponder { self.setContent = setContent cancellable = contentPublisher.sink { [weak self] tabContent in - if case .url(_, credential: .none, source: .userEntered(let userEnteredValue)) = tabContent { + if case .url(_, credential: .none, source: .userEntered(let userEnteredValue, _)) = tabContent { self?.lastUserEnteredValue = userEnteredValue } } diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index c0be988432..c8a3057d70 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -180,8 +180,11 @@ extension DownloadsTabExtension: NavigationResponder { let firstNavigationAction = navigationResponse.mainFrameNavigation?.redirectHistory.first ?? navigationResponse.mainFrameNavigation?.navigationAction - guard navigationResponse.httpResponse?.isSuccessful != false, - !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload else { + guard navigationResponse.httpResponse?.isSuccessful != false, // download non-http responses + !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload + // if user pressed Opt+Enter in the Address bar to download from a URL + || (navigationResponse.mainFrameNavigation?.redirectHistory.last ?? navigationResponse.mainFrameNavigation?.navigationAction)?.navigationType == .custom(.userRequestedPageDownload) + else { return .next // proceed with normal page loading } diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 3f9cb377d9..7ed73a4504 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -112,6 +112,10 @@ final class TabViewModel { // Update the address bar only after the tab did commit navigation to prevent Address Bar Spoofing return tab.webViewDidCommitNavigationPublisher.map { .didCommit }.eraseToAnyPublisher() + case .url(_, _, source: .userEntered(_, downloadRequested: true)): + // don‘t update the address bar for download navigations + return Empty().eraseToAnyPublisher().eraseToAnyPublisher() + case .url(_, _, source: .pendingStateRestoration), .url(_, _, source: .loadedByStateRestoration), .url(_, _, source: .userEntered), From a1ddacea3a39d6cf82fbd8f8a5d63440c114041b Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 1 Mar 2024 12:58:12 +0600 Subject: [PATCH 07/19] Add estimated download time (#2263) Task/Issue URL: https://app.asana.com/0/1199178362774117/1201661861647413/f --- DuckDuckGo/Common/Localizables/UserText.swift | 2 + .../Model/WebKitDownloadTask.swift | 43 ++++++++-- .../FileDownload/View/Downloads.storyboard | 49 ++++++----- .../FileDownload/View/DownloadsCellView.swift | 81 ++++++++++++++----- .../View/DownloadsViewController.swift | 5 ++ DuckDuckGo/Localizable.xcstrings | 37 ++++++++- UnitTests/Statistics/PixelTests.swift | 51 +++++++----- 7 files changed, 198 insertions(+), 70 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 7a9cdced0c..41e654127f 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -671,6 +671,8 @@ struct UserText { static let downloadCanceled = NSLocalizedString("downloads.error.canceled", value: "Canceled", comment: "Short error description when downloaded file download was canceled") static let downloadFailedToMoveFileToDownloads = NSLocalizedString("downloads.error.move.failed", value: "Could not move file to Downloads", comment: "Short error description when could not move downloaded file to the Downloads folder") static let downloadFailed = NSLocalizedString("downloads.error.other", value: "Error", comment: "Short error description when Download failed") + static let downloadBytesLoadedFormat = NSLocalizedString("downloads.bytes.format", value: "%@ of %@", comment: "Number of bytes out of total bytes downloaded (1Mb of 2Mb)") + static let downloadSpeedFormat = NSLocalizedString("downloads.speed.format", value: "%@/s", comment: "Download speed format (1Mb/sec)") static let cancelDownloadToolTip = NSLocalizedString("downloads.tooltip.cancel", value: "Cancel Download", comment: "Mouse-over tooltip for Cancel Download button") static let restartDownloadToolTip = NSLocalizedString("downloads.tooltip.restart", value: "Restart Download", comment: "Mouse-over tooltip for Restart Download button") diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift index 00bec47783..b47d4ec531 100644 --- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift +++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift @@ -33,6 +33,10 @@ protocol WebKitDownloadTaskDelegate: AnyObject { final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable { static let downloadExtension = "duckload" + private enum Constants { + static let remainingDownloadTimeEstimationDelay: TimeInterval = 1 + static let downloadSpeedSmoothingFactor = 0.1 + } let progress: Progress let shouldPromptForLocation: Bool @@ -72,7 +76,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable private weak var delegate: WebKitDownloadTaskDelegate? private let download: WebKitDownload - private var cancellables = Set() + private var progressCancellable: AnyCancellable? private var decideDestinationCompletionHandler: ((URL?) -> Void)? @@ -114,12 +118,37 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable private func start() { self.progress.fileDownloadingSourceURL = download.originalRequest?.url if let progress = (self.download as? ProgressReporting)?.progress { - progress.publisher(for: \.totalUnitCount) - .assign(to: \.totalUnitCount, onWeaklyHeld: self.progress) - .store(in: &self.cancellables) - progress.publisher(for: \.completedUnitCount) - .assign(to: \.completedUnitCount, onWeaklyHeld: self.progress) - .store(in: &self.cancellables) + + var startTime: Date? + progressCancellable = progress.publisher(for: \.totalUnitCount) + .combineLatest(progress.publisher(for: \.completedUnitCount)) + .sink { [weak progress=self.progress] total, completed in + guard let progress else { return } + if progress.totalUnitCount != total { + progress.totalUnitCount = total + } + progress.completedUnitCount = completed + + if total > 0, completed > 0 { + guard let startTime else { + startTime = Date() + return + } + let elapsedTime = Date().timeIntervalSince(startTime) + // delay before we start calculating the estimated time - because initially it‘s not reliable + guard elapsedTime > Constants.remainingDownloadTimeEstimationDelay else { return } + + // calculate instantaneous download speed + var throughput = Double(completed) / elapsedTime + + // calculate the moving average of download speed + if let oldThroughput = progress.throughput.map(Double.init) { + throughput = Constants.downloadSpeedSmoothingFactor * throughput + (1 - Constants.downloadSpeedSmoothingFactor) * oldThroughput + } + progress.throughput = Int(throughput) + progress.estimatedTimeRemaining = Double(total - completed) / throughput + } + } } } diff --git a/DuckDuckGo/FileDownload/View/Downloads.storyboard b/DuckDuckGo/FileDownload/View/Downloads.storyboard index 411575e7a9..41e5511da3 100644 --- a/DuckDuckGo/FileDownload/View/Downloads.storyboard +++ b/DuckDuckGo/FileDownload/View/Downloads.storyboard @@ -11,14 +11,14 @@ - + - + - + @@ -29,7 +29,7 @@ - + - + - + - + @@ -111,7 +111,7 @@ - + @@ -123,7 +123,7 @@ - + @@ -134,7 +134,7 @@ - + @@ -142,7 +142,7 @@ - + - + @@ -247,12 +247,12 @@ - + - + @@ -270,11 +270,11 @@ - + - + @@ -282,7 +282,7 @@