From ec52eaae07443a9abec89c479443387b07b6741f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Wed, 23 Oct 2024 17:31:01 +0200 Subject: [PATCH] Use New Tab Page implementation without customization features (#3453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1208547223289955/f Tech Design URL: CC: **Description**: Uses `NewTabPageController` as a home controller, but removes new tab page customization capabilities based on `newTabPageSections` feature flag state. Existing features of Home Screen should remain unchanged. Summary of changes done in this PR: * `SimpleNewTabPageView` now serving as a view for new tab page without customization features. Created based on the fully featured `NewTabPageView`. * Favorites placeholders and add button are not visible when customization flag is disabled. * Report action is not added to the browsing menu when link is missing. This prevents showing option which performs no action in case menu is shown on NTP. (cc @afterxleep) * Favorites data source adapter is listening for display mode changes and updates favorites accordingly. * Current (old) Dax Onboarding is integrated with the New Tab Page. (cc @alessandroboron) **Steps to test this PR**: #### Dax onboarding 1. Install fresh app 2. Go through onboarding, make sure Dax dialogs are shown properly on empty tab. #### Basic functionality 1. Make sure NTP flag is disabled in debug menu. 3. Without favorites, Dax logo should be visible. 4. Add some favorites. 5. It should be possible to long press to edit/remove and drag and drop to reorder. #### Toolbar menu items 1. Check toolbar displays bookmarks icon when NTP sections flag disabled. 2. Enable NTP sections flag in debug menu. 3. Reopen new tab page 4. Ensure browsing menu with shortcuts is available. #### Sync 1. Enable sync on two devices. 2. Verify updates for favorites are visible. **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [x] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [x] Portrait * [x] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [x] iPhone 8 * [ ] iPhone X * [x] iPhone 14 Pro * [ ] iPad **OS Testing**: * [x] iOS 15 * [ ] iOS 16 * [x] iOS 17 **Theme Testing**: * [x] Light theme * [x] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo.xcodeproj/project.pbxproj | 24 +- .../BrowsingMenu/BrowsingMenuAnimator.swift | 2 +- DuckDuckGo/DaxDialogViewController.swift | 15 +- DuckDuckGo/FavoritesFaviconLoader.swift | 6 +- ... => FavoritesListInteractingAdapter.swift} | 24 +- DuckDuckGo/FavoritesViewModel.swift | 19 +- DuckDuckGo/HomeScreenTransition.swift | 4 +- .../MainViewController+KeyCommands.swift | 4 +- DuckDuckGo/MainViewController.swift | 177 ++++---------- DuckDuckGo/NewTabPage.swift | 3 +- DuckDuckGo/NewTabPageView.swift | 11 + DuckDuckGo/NewTabPageViewController.swift | 121 ++++++++-- DuckDuckGo/NewTabPageViewModel.swift | 10 + DuckDuckGo/SimpleNewTabPageView.swift | 220 ++++++++++++++++++ DuckDuckGo/TabSwitcherTransition.swift | 2 +- ...bViewControllerBrowsingMenuExtension.swift | 14 +- ...FavoritesListInteractingAdapterTests.swift | 91 ++++++++ ... NewTabPageControllerDaxDialogTests.swift} | 25 +- .../NewTabPageFavoritesModelTests.swift | 42 +++- 19 files changed, 622 insertions(+), 192 deletions(-) rename DuckDuckGo/{FavoriteDataSource.swift => FavoritesListInteractingAdapter.swift} (72%) create mode 100644 DuckDuckGo/SimpleNewTabPageView.swift create mode 100644 DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift rename DuckDuckGoTests/{HomeViewControllerDaxDialogTests.swift => NewTabPageControllerDaxDialogTests.swift} (93%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 436ac71509..c86d5611cf 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -268,7 +268,7 @@ 560E990F2BEE2CB800507CE0 /* SyncErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */; }; 564DE4532C3ED1B700D23241 /* NewTabDaxDialogFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4522C3ED1B700D23241 /* NewTabDaxDialogFactory.swift */; }; 564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4542C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift */; }; - 564DE4572C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */; }; + 564DE4572C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */; }; 564DE45A2C450BE600D23241 /* DaxDialogsNewTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4592C450BE600D23241 /* DaxDialogsNewTabTests.swift */; }; 564DE45E2C45218500D23241 /* OnboardingNavigationDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE45D2C45218500D23241 /* OnboardingNavigationDelegateTests.swift */; }; 564DE4602C4544CA00D23241 /* HomePageDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE45F2C4544CA00D23241 /* HomePageDependencies.swift */; }; @@ -306,7 +306,9 @@ 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */; }; 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; + 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */; }; 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */; }; + 6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */; }; 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */; }; 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; }; 6F64AA592C4818D700CF4489 /* NewTabPageShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */; }; @@ -361,7 +363,7 @@ 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */; }; 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */; }; 6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */; }; - 6FEC0B882C999961006B4F6E /* FavoriteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */; }; + 6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */; }; 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; @@ -1551,7 +1553,7 @@ 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorMessage.swift; sourceTree = ""; }; 564DE4522C3ED1B700D23241 /* NewTabDaxDialogFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabDaxDialogFactory.swift; sourceTree = ""; }; 564DE4542C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingNewTabDialogFactoryTests.swift; sourceTree = ""; }; - 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewControllerDaxDialogTests.swift; sourceTree = ""; }; + 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDaxDialogTests.swift; sourceTree = ""; }; 564DE4592C450BE600D23241 /* DaxDialogsNewTabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogsNewTabTests.swift; sourceTree = ""; }; 564DE45D2C45218500D23241 /* OnboardingNavigationDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationDelegateTests.swift; sourceTree = ""; }; 564DE45F2C4544CA00D23241 /* HomePageDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDependencies.swift; sourceTree = ""; }; @@ -1589,7 +1591,9 @@ 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageDaxLogoView.swift; sourceTree = ""; }; 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = ""; }; 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = ""; }; + 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNewTabPageView.swift; sourceTree = ""; }; 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorage.swift; sourceTree = ""; }; + 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapterTests.swift; sourceTree = ""; }; 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonStyle.swift; sourceTree = ""; }; 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = ""; }; 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcut.swift; sourceTree = ""; }; @@ -1645,7 +1649,7 @@ 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewController.swift; sourceTree = ""; }; 6FE1274A2C20943500EB5724 /* ShortcutItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutItemView.swift; sourceTree = ""; }; 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = ""; }; - 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteDataSource.swift; sourceTree = ""; }; + 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapter.swift; sourceTree = ""; }; 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = ""; }; 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = ""; }; @@ -3852,8 +3856,9 @@ 6F7FB8DF2C660B1A00867DA7 /* NewTabPageFavoritesModelTests.swift */, 6F7FB8E42C66158D00867DA7 /* NewTabPageShortcutsSettingsModelTests.swift */, 6F7FB8E62C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift */, - 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */, + 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */, 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */, + 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */, ); name = NewTabPage; sourceTree = ""; @@ -3907,7 +3912,7 @@ 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, - 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, + 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */, ); name = Model; sourceTree = ""; @@ -3958,6 +3963,7 @@ 6F03CAF82C32C3AA004179A8 /* Messages */, 6FE127372C20492500EB5724 /* NewTabPage.swift */, 6FD8E51F2C5BA23200345670 /* NewTabPageViewModel.swift */, + 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */, 6FE127392C204BD000EB5724 /* NewTabPageView.swift */, 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */, 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */, @@ -7462,6 +7468,7 @@ C185ED612BD4329700BAE9DC /* ImportPasswordsStatusHandler.swift in Sources */, CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, + 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */, D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */, AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */, @@ -7859,7 +7866,7 @@ 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */, 37C696772C4957940073E131 /* RemoteMessagingDebugViewController.swift in Sources */, 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */, - 6FEC0B882C999961006B4F6E /* FavoriteDataSource.swift in Sources */, + 6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */, F4F6DFB626E6B71300ED7E12 /* BookmarkFoldersTableViewController.swift in Sources */, 8586A11024CCCD040049720E /* TabsBarViewController.swift in Sources */, 6FEC0B852C999352006B4F6E /* FavoriteItem.swift in Sources */, @@ -7915,7 +7922,7 @@ 98DA35C4268CC81E00159906 /* DomainMatchingReportTests.swift in Sources */, 8590CB632684F10F0089F6BF /* ContentBlockerProtectionStoreTests.swift in Sources */, 83EDCC411F86B89C005CDFCD /* StatisticsLoaderTests.swift in Sources */, - 564DE4572C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift in Sources */, + 564DE4572C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift in Sources */, C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */, 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */, 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */, @@ -8026,6 +8033,7 @@ C14882EA27F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift in Sources */, 1E05D1DB29C47B3300BF9A1F /* DailyPixelTests.swift in Sources */, 564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */, + 6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */, 981FED7422046017008488D7 /* AutoClearTests.swift in Sources */, 98DDF9F322C4029D00DE38DB /* InitHelpers.swift in Sources */, B6AD9E3628D4510A0019CDE9 /* ContentBlockerRulesManagerMock.swift in Sources */, diff --git a/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift b/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift index 401993c4da..5fe45d94f5 100644 --- a/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift +++ b/DuckDuckGo/BrowsingMenu/BrowsingMenuAnimator.swift @@ -106,7 +106,7 @@ final class BrowsingMenuAnimator: NSObject, UIViewControllerAnimatedTransitionin fromViewController.view.isHidden = true - if toViewController.homeViewController != nil { + if toViewController.newTabPageViewController != nil { toViewController.presentedMenuButton.setState(.bookmarksImage, animated: true) } else { toViewController.presentedMenuButton.setState(.menuImage, animated: true) diff --git a/DuckDuckGo/DaxDialogViewController.swift b/DuckDuckGo/DaxDialogViewController.swift index e231e6c566..78c524c2f5 100644 --- a/DuckDuckGo/DaxDialogViewController.swift +++ b/DuckDuckGo/DaxDialogViewController.swift @@ -42,7 +42,7 @@ class DaxDialogViewController: UIViewController { initCTA() } } - + func calculateHeight() -> CGFloat { guard let text = message ?? cta, !text.isEmpty else { return 370.0 } @@ -59,7 +59,7 @@ class DaxDialogViewController: UIViewController { let bottomMargin: CGFloat = 24.0 return iconHeight + topMargin + size.height + buttonHeight + bottomMargin } - + var onTapCta: (() -> Void)? private var position: Int = 0 @@ -188,3 +188,14 @@ extension DaxDialogViewController { } } } + +extension DaxDialogViewController { + static func loadFromStoryboard() -> DaxDialogViewController { + let storyboard = UIStoryboard(name: "DaxOnboarding", bundle: Bundle.main) + guard let controller = storyboard.instantiateViewController(identifier: "DaxDialog") as? DaxDialogViewController else { + fatalError("Failed to instantiate DaxDialogViewController from storyboard") + } + + return controller + } +} diff --git a/DuckDuckGo/FavoritesFaviconLoader.swift b/DuckDuckGo/FavoritesFaviconLoader.swift index 7adc516ce9..4716a21a48 100644 --- a/DuckDuckGo/FavoritesFaviconLoader.swift +++ b/DuckDuckGo/FavoritesFaviconLoader.swift @@ -40,8 +40,12 @@ actor FavoritesFaviconLoader: FavoritesFaviconLoading { } tasks[domain] = newTask + let value = await newTask.value + if value == nil { + tasks[domain] = nil + } - return await newTask.value + return value } nonisolated func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { diff --git a/DuckDuckGo/FavoriteDataSource.swift b/DuckDuckGo/FavoritesListInteractingAdapter.swift similarity index 72% rename from DuckDuckGo/FavoriteDataSource.swift rename to DuckDuckGo/FavoritesListInteractingAdapter.swift index 921938d532..2f4e3fac30 100644 --- a/DuckDuckGo/FavoriteDataSource.swift +++ b/DuckDuckGo/FavoritesListInteractingAdapter.swift @@ -1,5 +1,5 @@ // -// FavoriteDataSource.swift +// FavoritesListInteractingAdapter.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -24,12 +24,30 @@ import Bookmarks final class FavoritesListInteractingAdapter: NewTabPageFavoriteDataSource { let favoritesListInteracting: FavoritesListInteracting + let appSettings: AppSettings - init(favoritesListInteracting: FavoritesListInteracting) { + private var cancellables: Set = [] + + private var displayModeSubject = PassthroughSubject() + + init(favoritesListInteracting: FavoritesListInteracting, appSettings: AppSettings = AppDependencyProvider.shared.appSettings) { self.favoritesListInteracting = favoritesListInteracting + self.appSettings = appSettings + self.externalUpdates = favoritesListInteracting.externalUpdates.merge(with: displayModeSubject).eraseToAnyPublisher() + + NotificationCenter.default.publisher(for: AppUserDefaults.Notifications.favoritesDisplayModeChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self else { + return + } + favoritesListInteracting.favoritesDisplayMode = self.appSettings.favoritesDisplayMode + displayModeSubject.send() + } + .store(in: &cancellables) } - var externalUpdates: AnyPublisher { favoritesListInteracting.externalUpdates } + let externalUpdates: AnyPublisher var favorites: [Favorite] { (try? favoritesListInteracting.favorites.map(Favorite.init)) ?? [] diff --git a/DuckDuckGo/FavoritesViewModel.swift b/DuckDuckGo/FavoritesViewModel.swift index 781d77a044..babd8e20c2 100644 --- a/DuckDuckGo/FavoritesViewModel.swift +++ b/DuckDuckGo/FavoritesViewModel.swift @@ -54,18 +54,23 @@ class FavoritesViewModel: ObservableObject { private let favoriteDataSource: NewTabPageFavoriteDataSource private let pixelFiring: PixelFiring.Type private let dailyPixelFiring: DailyPixelFiring.Type + private let isNewTabPageCustomizationEnabled: Bool var isEmpty: Bool { allFavorites.filter(\.isFavorite).isEmpty } - init(favoriteDataSource: NewTabPageFavoriteDataSource, + init(isNewTabPageCustomizationEnabled: Bool = false, + favoriteDataSource: NewTabPageFavoriteDataSource, faviconLoader: FavoritesFaviconLoading, pixelFiring: PixelFiring.Type = Pixel.self, dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { self.favoriteDataSource = favoriteDataSource self.pixelFiring = pixelFiring self.dailyPixelFiring = dailyPixelFiring + self.isNewTabPageCustomizationEnabled = isNewTabPageCustomizationEnabled + self.isCollapsed = isNewTabPageCustomizationEnabled + self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in guard let self else { return } @@ -73,8 +78,7 @@ class FavoritesViewModel: ObservableObject { self.faviconMissing() } }) - - + favoriteDataSource.externalUpdates.sink { [weak self] _ in self?.updateData() }.store(in: &cancellables) @@ -93,6 +97,10 @@ class FavoritesViewModel: ObservableObject { } func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + guard isNewTabPageCustomizationEnabled else { + return .init(items: allFavorites, isCollapsible: false) + } + let hasFavorites = allFavorites.contains(where: \.isFavorite) let maxCollapsedItemsCount = hasFavorites ? columnsCount * 2 : columnsCount let isCollapsible = allFavorites.count > maxCollapsedItemsCount @@ -170,7 +178,10 @@ class FavoritesViewModel: ObservableObject { var allFavorites = favoriteDataSource.favorites.map { FavoriteItem.favorite($0) } - allFavorites.append(.addFavorite) + + if isNewTabPageCustomizationEnabled { + allFavorites.append(.addFavorite) + } self.allFavorites = allFavorites } diff --git a/DuckDuckGo/HomeScreenTransition.swift b/DuckDuckGo/HomeScreenTransition.swift index 8de6d19b3d..6fb2e8518b 100644 --- a/DuckDuckGo/HomeScreenTransition.swift +++ b/DuckDuckGo/HomeScreenTransition.swift @@ -90,7 +90,7 @@ class FromHomeScreenTransition: HomeScreenTransition { tabSwitcherViewController.view.frame = transitionContext.finalFrame(for: tabSwitcherViewController) tabSwitcherViewController.prepareForPresentation() - guard let homeScreen = mainViewController.homeController, + guard let homeScreen = mainViewController.newTabPageViewController, let tab = mainViewController.tabManager.model.currentTab, let rowIndex = tabSwitcherViewController.tabsModel.indexOf(tab: tab), let layoutAttr = tabSwitcherViewController.collectionView.layoutAttributesForItem(at: IndexPath(row: rowIndex, section: 0)) @@ -163,7 +163,7 @@ class ToHomeScreenTransition: HomeScreenTransition { prepareSubviews(using: transitionContext) guard let mainViewController = transitionContext.viewController(forKey: .to) as? MainViewController, - let homeScreen = mainViewController.homeController, + let homeScreen = mainViewController.newTabPageViewController, let tab = mainViewController.tabManager.model.currentTab, let rowIndex = tabSwitcherViewController.tabsModel.indexOf(tab: tab), let layoutAttr = tabSwitcherViewController.collectionView.layoutAttributesForItem(at: IndexPath(row: rowIndex, section: 0)) diff --git a/DuckDuckGo/MainViewController+KeyCommands.swift b/DuckDuckGo/MainViewController+KeyCommands.swift index 45f499a9b6..3e6bb42e7c 100644 --- a/DuckDuckGo/MainViewController+KeyCommands.swift +++ b/DuckDuckGo/MainViewController+KeyCommands.swift @@ -33,7 +33,7 @@ extension MainViewController { } var browsingCommands = [UIKeyCommand]() - if homeController == nil { + if newTabPageViewController == nil { browsingCommands = [ UIKeyCommand(title: "", action: #selector(keyboardFind), input: "f", modifierFlags: [.command], discoverabilityTitle: UserText.keyCommandFind), @@ -140,7 +140,7 @@ extension MainViewController { @objc func keyboardLocation() { guard tabSwitcherController == nil else { return } - if let controller = homeController { + if let controller = newTabPageViewController { controller.launchNewSearch() } else { showBars() diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 7e90bf0129..78000f9d7b 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -77,12 +77,8 @@ class MainViewController: UIViewController { emailManager.requestDelegate = self return emailManager }() - - var homeViewController: HomeViewController? + var newTabPageViewController: NewTabPageViewController? - var homeController: (NewTabPage & HomeScreenTransitionSource)? { - homeViewController ?? newTabPageViewController - } var tabsBarController: TabsBarViewController? var suggestionTrayController: SuggestionTrayViewController? @@ -486,7 +482,7 @@ class MainViewController: UIViewController { @objc private func keyboardWillHide() { - if homeController?.isDragging == true, keyboardShowing { + if newTabPageViewController?.isDragging == true, keyboardShowing { Pixel.fire(pixel: .addressBarGestureDismiss) } } @@ -681,7 +677,6 @@ class MainViewController: UIViewController { } self.menuBookmarksViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode self.favoritesViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode - self.homeController?.reloadFavorites() WidgetCenter.shared.reloadAllTimelines() } } @@ -695,9 +690,6 @@ class MainViewController: UIViewController { syncUpdatesCancellable = syncDataProviders.bookmarksAdapter.syncDidCompletePublisher .sink { [weak self] _ in self?.favoritesViewModel.reloadData() - DispatchQueue.main.async { - self?.homeController?.reloadFavorites() - } } } @@ -796,53 +788,34 @@ class MainViewController: UIViewController { } let newTabDaxDialogFactory = NewTabDaxDialogFactory(delegate: self, contextualOnboardingLogic: DaxDialogs.shared, onboardingPixelReporter: contextualOnboardingPixelReporter) - if homeTabManager.isNewTabPageSectionsEnabled { - let controller = NewTabPageViewController(tab: tabModel, - interactionModel: favoritesViewModel, - syncService: syncService, - syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, - homePageMessagesConfiguration: homePageConfiguration, - privacyProDataReporting: privacyProDataReporter, - variantManager: variantManager, - newTabDialogFactory: newTabDaxDialogFactory, - newTabDialogTypeProvider: DaxDialogs.shared, - faviconLoader: faviconLoader) - - controller.delegate = self - controller.shortcutsDelegate = self - controller.chromeDelegate = self - - newTabPageViewController = controller - addToContentContainer(controller: controller) - viewCoordinator.logoContainer.isHidden = true - adjustNewTabPageSafeAreaInsets(for: appSettings.currentAddressBarPosition) - } else { - let homePageDependencies = HomePageDependencies(homePageConfiguration: homePageConfiguration, - model: tabModel, - favoritesViewModel: favoritesViewModel, - appSettings: appSettings, - syncService: syncService, - syncDataProviders: syncDataProviders, - privacyProDataReporter: privacyProDataReporter, - variantManager: variantManager, - newTabDialogFactory: newTabDaxDialogFactory, - newTabDialogTypeProvider: DaxDialogs.shared) - let controller = HomeViewController.loadFromStoryboard(homePageDependecies: homePageDependencies) - - controller.delegate = self - controller.chromeDelegate = self - homeViewController = controller - addToContentContainer(controller: controller) - } + let controller = NewTabPageViewController(tab: tabModel, + isNewTabPageCustomizationEnabled: homeTabManager.isNewTabPageSectionsEnabled, + interactionModel: favoritesViewModel, + syncService: syncService, + syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, + homePageMessagesConfiguration: homePageConfiguration, + privacyProDataReporting: privacyProDataReporter, + variantManager: variantManager, + newTabDialogFactory: newTabDaxDialogFactory, + newTabDialogTypeProvider: DaxDialogs.shared, + faviconLoader: faviconLoader) + + controller.delegate = self + controller.shortcutsDelegate = self + controller.chromeDelegate = self + + newTabPageViewController = controller + addToContentContainer(controller: controller) + viewCoordinator.logoContainer.isHidden = true + adjustNewTabPageSafeAreaInsets(for: appSettings.currentAddressBarPosition) refreshControls() syncService.scheduler.requestSyncImmediately() } fileprivate func removeHomeScreen() { - homeController?.willMove(toParent: nil) - homeController?.dismiss() - homeViewController = nil + newTabPageViewController?.willMove(toParent: nil) + newTabPageViewController?.dismiss() newTabPageViewController = nil } @@ -1193,7 +1166,7 @@ class MainViewController: UIViewController { func refreshMenuButtonState() { let expectedState: MenuButton.State - if homeViewController != nil { + if !homeTabManager.isNewTabPageSectionsEnabled && newTabPageViewController != nil { expectedState = .bookmarksImage viewCoordinator.lastToolbarButton.accessibilityLabel = UserText.bookmarksButtonHint viewCoordinator.omniBar.menuButton.accessibilityLabel = UserText.bookmarksButtonHint @@ -1432,18 +1405,7 @@ class MainViewController: UIViewController { attachHomeScreen() tabsBarController?.refresh(tabsModel: tabManager.model) swipeTabsCoordinator?.refresh(tabsModel: tabManager.model, scrollToSelected: true) - homeController?.openedAsNewTab(allowingKeyboard: allowingKeyboard) - } - - func animateLogoAppearance() { - viewCoordinator.logoContainer.alpha = 0 - viewCoordinator.logoContainer.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UIView.animate(withDuration: 0.2) { - self.viewCoordinator.logoContainer.alpha = 1 - self.viewCoordinator.logoContainer.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) - } - } + newTabPageViewController?.openedAsNewTab(allowingKeyboard: allowingKeyboard) } func updateFindInPage() { @@ -1753,7 +1715,7 @@ extension MainViewController: BrowserChromeDelegate { updateBlock() } } - + func setNavigationBarHidden(_ hidden: Bool) { if hidden { hideKeyboard() } @@ -1823,7 +1785,7 @@ extension MainViewController: OmniBarDelegate { func onOmniQueryUpdated(_ updatedQuery: String) { if updatedQuery.isEmpty { - if homeController != nil { + if newTabPageViewController != nil { hideSuggestionTray() } else { let didShow = tryToShowSuggestionTray(.favorites) @@ -1892,7 +1854,7 @@ extension MainViewController: OmniBarDelegate { let menuEntries: [BrowsingMenuEntry] let headerEntries: [BrowsingMenuEntry] - if isNewTabPageVisible { + if homeTabManager.isNewTabPageSectionsEnabled && newTabPageViewController != nil { menuEntries = tab.buildShortcutsMenu() headerEntries = [] } else { @@ -1933,7 +1895,7 @@ extension MainViewController: OmniBarDelegate { } func fireControllerAwarePixel(ntp: Pixel.Event, serp: Pixel.Event, website: Pixel.Event) { - if homeController != nil { + if newTabPageViewController != nil { Pixel.fire(pixel: ntp) } else if let currentTab { if currentTab.url?.isDuckDuckGoSearch == true { @@ -2005,7 +1967,7 @@ extension MainViewController: OmniBarDelegate { fireControllerAwarePixel(ntp: .addressBarClickOnNTP, serp: .addressBarClickOnSERP, website: .addressBarClickOnWebsite) } - guard homeController == nil else { return } + guard newTabPageViewController == nil else { return } if !skipSERPFlow, isSERPPresented, let query = omniBar.textField.text { tryToShowSuggestionTray(.autocomplete(query: query)) @@ -2022,10 +1984,10 @@ extension MainViewController: OmniBarDelegate { if !DaxDialogs.shared.shouldShowFireButtonPulse { ViewHighlighter.hideAll() } - guard let homeController = homeController else { + guard let newTabPageViewController = newTabPageViewController else { return selectQueryText } - homeController.launchNewSearch() + newTabPageViewController.launchNewSearch() return selectQueryText } @@ -2065,7 +2027,7 @@ extension MainViewController: FavoritesOverlayDelegate { func favoritesOverlay(_ overlay: FavoritesOverlay, didSelect favorite: BookmarkEntity) { guard let url = favorite.urlObject else { return } Pixel.fire(pixel: .favoriteLaunchedWebsite) - homeViewController?.chromeDelegate = nil + newTabPageViewController?.chromeDelegate = nil dismissOmniBar() Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) if url.isBookmarklet() { @@ -2088,7 +2050,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { } func autocomplete(selectedSuggestion suggestion: Suggestion) { - homeViewController?.chromeDelegate = nil + newTabPageViewController?.chromeDelegate = nil dismissOmniBar() viewCoordinator.omniBar.cancel() switch suggestion { @@ -2113,7 +2075,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { loadUrl(url) case .openTab(title: _, url: let url): - if homeViewController != nil, let tab = tabManager.model.currentTab { + if newTabPageViewController != nil, let tab = tabManager.model.currentTab { self.closeTab(tab) } loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: .noAttribution) @@ -2194,49 +2156,6 @@ extension MainViewController { } } -extension MainViewController: HomeControllerDelegate { - - func home(_ home: HomeViewController, didRequestQuery query: String) { - loadQueryInNewTab(query) - } - - func home(_ home: HomeViewController, didRequestUrl url: URL) { - handleRequestedURL(url) - } - - func home(_ home: HomeViewController, didRequestEdit favorite: BookmarkEntity) { - segueToEditBookmark(favorite) - } - - func home(_ home: HomeViewController, didRequestContentOverflow shouldOverflow: Bool) -> CGFloat { - allowContentUnderflow = shouldOverflow - return contentUnderflow - } - - func homeDidDeactivateOmniBar(home: HomeViewController) { - hideSuggestionTray() - dismissOmniBar() - } - - func showSettings(_ home: HomeViewController) { - segueToSettings() - } - - func home(_ home: HomeViewController, didRequestHideLogo hidden: Bool) { - viewCoordinator.logoContainer.isHidden = hidden - } - - func homeDidRequestLogoContainer(_ home: HomeViewController) -> UIView { - return viewCoordinator.logoContainer - } - - func home(_ home: HomeViewController, searchTransitionUpdated percent: CGFloat) { - viewCoordinator.statusBackground.alpha = percent - viewCoordinator.navigationBarContainer.alpha = percent - } - -} - extension MainViewController: NewTabPageControllerDelegate { func newTabPageDidOpenFavoriteURL(_ controller: NewTabPageViewController, url: URL) { handleRequestedURL(url) @@ -2512,17 +2431,19 @@ extension MainViewController: TabDelegate { extension MainViewController: TabSwitcherDelegate { + private func animateLogoAppearance() { + newTabPageViewController?.view.transform = CGAffineTransform().scaledBy(x: 0.5, y: 0.5) + newTabPageViewController?.view.alpha = 0.0 + UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveEaseInOut, .beginFromCurrentState]) { + self.newTabPageViewController?.view.transform = .identity + self.newTabPageViewController?.view.alpha = 1.0 + } + } + func tabSwitcherDidRequestNewTab(tabSwitcher: TabSwitcherViewController) { newTab() - if homeViewController != nil { + if newTabPageViewController != nil { animateLogoAppearance() - } else if newTabPageViewController != nil { - newTabPageViewController?.view.transform = CGAffineTransform().scaledBy(x: 0.5, y: 0.5) - newTabPageViewController?.view.alpha = 0.0 - UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveEaseInOut, .beginFromCurrentState]) { - self.newTabPageViewController?.view.transform = .identity - self.newTabPageViewController?.view.alpha = 1.0 - } } } @@ -2540,7 +2461,7 @@ extension MainViewController: TabSwitcherDelegate { // switcher is still presented. DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { tabSwitcher.dismiss(animated: true) { - self.homeController?.viewDidAppear(true) + self.newTabPageViewController?.viewDidAppear(true) } } } @@ -2746,7 +2667,7 @@ extension MainViewController: AutoClearWorker { // Ideally this should happen once data clearing has finished AND the animation is finished if showNextDaxDialog { - self.homeController?.showNextDaxDialog() + self.newTabPageViewController?.showNextDaxDialog() } else if KeyboardSettings().onNewTab { let showKeyboardAfterFireButton = DispatchWorkItem { self.enterSearch() @@ -2840,7 +2761,7 @@ extension MainViewController: OnboardingDelegate { markOnboardingSeen() controller.modalTransitionStyle = .crossDissolve controller.dismiss(animated: true) - homeController?.onboardingCompleted() + newTabPageViewController?.onboardingCompleted() } func markOnboardingSeen() { diff --git a/DuckDuckGo/NewTabPage.swift b/DuckDuckGo/NewTabPage.swift index 7be2446e29..eb382f9b5e 100644 --- a/DuckDuckGo/NewTabPage.swift +++ b/DuckDuckGo/NewTabPage.swift @@ -21,8 +21,7 @@ import UIKit protocol NewTabPage: UIViewController { - var isDragging: Bool { get } // TODO: Mariusz, check if needed in both - func reloadFavorites() // TODO: Mariusz: check if needed with reactive approach + var isDragging: Bool { get } func launchNewSearch() func openedAsNewTab(allowingKeyboard: Bool) diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index aaed282ca5..219ddd9c27 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -34,6 +34,8 @@ struct NewTabPageView: View { @State private var customizeButtonShowedInline = false @State private var isAddingFavorite: Bool = false + @State var isDragging: Bool = false + init(viewModel: NewTabPageViewModel, messagesModel: NewTabPageMessagesModel, favoritesViewModel: FavoritesViewModel, @@ -71,6 +73,15 @@ struct NewTabPageView: View { sectionsSettingsModel: sectionsSettingsModel) } }) + .simultaneousGesture( + DragGesture() + .onChanged({ value in + if value.translation.height > 0 { + viewModel.beginDragging() + } + }) + .onEnded({ _ in viewModel.endDragging() }) + ) } } diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index ee3d0bb4a0..0f24599de6 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -23,7 +23,7 @@ import Bookmarks import BrowserServicesKit import Core -final class NewTabPageViewController: UIHostingController, NewTabPage { +final class NewTabPageViewController: UIHostingController, NewTabPage { private let syncService: DDGSyncing private let syncBookmarksAdapter: SyncBookmarksAdapter @@ -43,7 +43,15 @@ final class NewTabPageViewController: UIHostingController, NewTa private var hostingController: UIHostingController? + private weak var daxDialogViewController: DaxDialogViewController? + private var daxDialogHeightConstraint: NSLayoutConstraint? + + var isDaxDialogVisible: Bool { + daxDialogViewController?.view.isHidden == false + } + init(tab: Tab, + isNewTabPageCustomizationEnabled: Bool, interactionModel: FavoritesListInteracting, syncService: DDGSyncing, syncBookmarksAdapter: SyncBookmarksAdapter, @@ -64,23 +72,35 @@ final class NewTabPageViewController: UIHostingController, NewTa newTabPageViewModel = NewTabPageViewModel() shortcutsSettingsModel = NewTabPageShortcutsSettingsModel() sectionsSettingsModel = NewTabPageSectionsSettingsModel() - favoritesModel = FavoritesViewModel(favoriteDataSource: FavoritesListInteractingAdapter(favoritesListInteracting: interactionModel), faviconLoader: faviconLoader) + favoritesModel = FavoritesViewModel(isNewTabPageCustomizationEnabled: isNewTabPageCustomizationEnabled, + favoriteDataSource: FavoritesListInteractingAdapter(favoritesListInteracting: interactionModel), + faviconLoader: faviconLoader) shortcutsModel = ShortcutsModel() messagesModel = NewTabPageMessagesModel(homePageMessagesConfiguration: homePageMessagesConfiguration, privacyProDataReporter: privacyProDataReporting) - let newTabPageView = NewTabPageView(viewModel: newTabPageViewModel, - messagesModel: messagesModel, - favoritesViewModel: favoritesModel, - shortcutsModel: shortcutsModel, - shortcutsSettingsModel: shortcutsSettingsModel, - sectionsSettingsModel: sectionsSettingsModel) - - super.init(rootView: newTabPageView) + if isNewTabPageCustomizationEnabled { + super.init(rootView: AnyView(NewTabPageView(viewModel: self.newTabPageViewModel, + messagesModel: self.messagesModel, + favoritesViewModel: self.favoritesModel, + shortcutsModel: self.shortcutsModel, + shortcutsSettingsModel: self.shortcutsSettingsModel, + sectionsSettingsModel: self.sectionsSettingsModel))) + } else { + super.init(rootView: AnyView(SimpleNewTabPageView(viewModel: self.newTabPageViewModel, + messagesModel: self.messagesModel, + favoritesViewModel: self.favoritesModel))) + } assignFavoriteModelActions() assignShorcutsModelActions() } + override func viewDidLoad() { + super.viewDidLoad() + + setUpDaxDialog() + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -90,6 +110,34 @@ final class NewTabPageViewController: UIHostingController, NewTa Pixel.fire(pixel: .homeScreenShown) sendDailyDisplayPixel() + + view.backgroundColor = UIColor(designSystemColor: .background) + } + + private func setUpDaxDialog() { + let daxDialogController = DaxDialogViewController.loadFromStoryboard() + guard let dialogView = daxDialogController.view else { return } + + self.addChild(daxDialogController) + self.view.addSubview(dialogView) + + dialogView.translatesAutoresizingMaskIntoConstraints = false + dialogView.isHidden = true + + let widthConstraint = dialogView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 1) + widthConstraint.priority = .defaultHigh + let heightConstraint = dialogView.heightAnchor.constraint(equalToConstant: 250) + daxDialogHeightConstraint = heightConstraint + NSLayoutConstraint.activate([ + dialogView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44.0), + dialogView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dialogView.widthAnchor.constraint(lessThanOrEqualToConstant: 375), + heightConstraint, + widthConstraint + ]) + + daxDialogController.didMove(toParent: self) + daxDialogViewController = daxDialogController } // MARK: - Private @@ -140,7 +188,7 @@ final class NewTabPageViewController: UIHostingController, NewTa // MARK: - NewTabPage - let isDragging: Bool = false + var isDragging: Bool { newTabPageViewModel.isDragging } weak var chromeDelegate: BrowserChromeDelegate? weak var delegate: NewTabPageControllerDelegate? @@ -151,12 +199,18 @@ final class NewTabPageViewController: UIHostingController, NewTa } func openedAsNewTab(allowingKeyboard: Bool) { - guard allowingKeyboard && KeyboardSettings().onNewTab else { return } + if allowingKeyboard && KeyboardSettings().onNewTab { - // The omnibar is inside a collection view so this needs a chance to do its thing - // which might also be async. Not great. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.launchNewSearch() + // The omnibar is inside a collection view so this needs a chance to do its thing + // which might also be async. Not great. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.launchNewSearch() + } + } + + if !variantManager.isContextualDaxDialogsEnabled { + // In the new onboarding this gets called twice (viewDidAppear in Tab) which then reset the spec to nil. + presentNextDaxDialog() } } @@ -165,7 +219,7 @@ final class NewTabPageViewController: UIHostingController, NewTa } func showNextDaxDialog() { - showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) + presentNextDaxDialog() } func onboardingCompleted() { @@ -181,6 +235,8 @@ final class NewTabPageViewController: UIHostingController, NewTa private func presentNextDaxDialog() { if variantManager.isContextualDaxDialogsEnabled { showNextDaxDialogNew(dialogProvider: newTabDialogTypeProvider, factory: newTabDialogFactory) + } else { + showNextDaxDialog(dialogProvider: newTabDialogTypeProvider) } } @@ -218,6 +274,37 @@ extension NewTabPageViewController: HomeScreenTransitionSource { extension NewTabPageViewController { + func showNextDaxDialog(dialogProvider: NewTabDialogSpecProvider) { + guard let spec = dialogProvider.nextHomeScreenMessage() else { return } + guard !isDaxDialogVisible else { return } + guard let daxDialogViewController = daxDialogViewController else { return } + + newTabPageViewModel.startOnboarding() + + daxDialogViewController.view.isHidden = false + daxDialogViewController.view.alpha = 0.0 + + daxDialogViewController.loadViewIfNeeded() + daxDialogViewController.message = spec.message + daxDialogViewController.accessibleMessage = spec.accessibilityLabel + + if spec == .initial { + UniquePixel.fire(pixel: .onboardingContextualTryVisitSiteUnique, includedParameters: [.appVersion, .atb]) + } + + view.addGestureRecognizer(daxDialogViewController.tapToCompleteGestureRecognizer) + + daxDialogHeightConstraint?.constant = daxDialogViewController.calculateHeight() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + UIView.animate(withDuration: 0.4, animations: { + daxDialogViewController.view.alpha = 1.0 + }, completion: { _ in + daxDialogViewController.start() + }) + } + } + func showNextDaxDialogNew(dialogProvider: NewTabDialogSpecProvider, factory: any NewTabDaxDialogProvider) { dismissHostingController(didFinishNTPOnboarding: false) diff --git a/DuckDuckGo/NewTabPageViewModel.swift b/DuckDuckGo/NewTabPageViewModel.swift index b53a7625ea..6cb387b402 100644 --- a/DuckDuckGo/NewTabPageViewModel.swift +++ b/DuckDuckGo/NewTabPageViewModel.swift @@ -26,6 +26,8 @@ final class NewTabPageViewModel: ObservableObject { @Published private(set) var isOnboarding: Bool @Published var isShowingSettings: Bool + private(set) var isDragging: Bool = false + private var introDataStorage: NewTabPageIntroDataStoring private let pixelFiring: PixelFiring.Type @@ -67,4 +69,12 @@ final class NewTabPageViewModel: ObservableObject { func finishOnboarding() { isOnboarding = false } + + func beginDragging() { + isDragging = true + } + + func endDragging() { + isDragging = false + } } diff --git a/DuckDuckGo/SimpleNewTabPageView.swift b/DuckDuckGo/SimpleNewTabPageView.swift new file mode 100644 index 0000000000..cf31697226 --- /dev/null +++ b/DuckDuckGo/SimpleNewTabPageView.swift @@ -0,0 +1,220 @@ +// +// SimpleNewTabPageView.swift +// DuckDuckGo +// +// 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 SwiftUI +import DuckUI +import RemoteMessaging + +struct SimpleNewTabPageView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + @ObservedObject private var viewModel: NewTabPageViewModel + @ObservedObject private var messagesModel: NewTabPageMessagesModel + @ObservedObject private var favoritesViewModel: FavoritesViewModel + + init(viewModel: NewTabPageViewModel, + messagesModel: NewTabPageMessagesModel, + favoritesViewModel: FavoritesViewModel) { + self.viewModel = viewModel + self.messagesModel = messagesModel + self.favoritesViewModel = favoritesViewModel + + self.messagesModel.load() + } + + private var isShowingSections: Bool { + !favoritesViewModel.allFavorites.isEmpty + } + + var body: some View { + if !viewModel.isOnboarding { + mainView + .background(Color(designSystemColor: .background)) + .simultaneousGesture( + DragGesture() + .onChanged({ value in + if value.translation.height > 0 { + viewModel.beginDragging() + } + }) + .onEnded({ _ in viewModel.endDragging() }) + ) + } + } + + @ViewBuilder + private var mainView: some View { + if isShowingSections { + sectionsView + } else { + emptyStateView + } + } +} + +private extension SimpleNewTabPageView { + // MARK: - Views + @ViewBuilder + private var sectionsView: some View { + GeometryReader { proxy in + ScrollView { + VStack(spacing: Metrics.sectionSpacing) { + + messagesSectionView + .padding(.top, Metrics.nonGridSectionTopPadding) + + favoritesSectionView(proxy: proxy) + } + .padding(Metrics.largePadding) + } + .withScrollKeyboardDismiss() + } + } + + @ViewBuilder + private var emptyStateView: some View { + ZStack { + NewTabPageDaxLogoView() + + VStack(spacing: Metrics.sectionSpacing) { + messagesSectionView + .padding(.top, Metrics.nonGridSectionTopPadding) + .frame(maxHeight: .infinity, alignment: .top) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + } + .padding(Metrics.largePadding) + } + + private var messagesSectionView: some View { + ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in + HomeMessageView(viewModel: messageModel) + .frame(maxWidth: horizontalSizeClass == .regular ? Metrics.messageMaximumWidthPad : Metrics.messageMaximumWidth) + .transition(.scale.combined(with: .opacity)) + } + } + + private func favoritesSectionView(proxy: GeometryProxy) -> some View { + FavoritesView(model: favoritesViewModel, + isAddingFavorite: .constant(false), + geometry: proxy) + } +} + +private extension View { + @ViewBuilder + func withScrollKeyboardDismiss() -> some View { + if #available(iOS 16, *) { + scrollDismissesKeyboard(.immediately) + } else { + self + } + } +} + +private struct Metrics { + + static let regularPadding = 16.0 + static let largePadding = 24.0 + static let sectionSpacing = 32.0 + static let nonGridSectionTopPadding = -8.0 + + static let messageMaximumWidth: CGFloat = 380 + static let messageMaximumWidthPad: CGFloat = 455 +} + +// MARK: - Preview + +#Preview("Regular") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesViewModel: FavoritesPreviewModel() + ) +} + +#Preview("With message") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [ + HomeMessage.remoteMessage( + remoteMessage: RemoteMessageModel( + id: "0", + content: .small(titleText: "Title", descriptionText: "Description"), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: false + ) + ) + ] + ) + ), + favoritesViewModel: FavoritesPreviewModel() + ) +} + +#Preview("No favorites") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesViewModel: FavoritesPreviewModel(favorites: []) + ) +} + +#Preview("Empty") { + SimpleNewTabPageView( + viewModel: NewTabPageViewModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesViewModel: FavoritesPreviewModel() + ) +} + +private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration { + private(set) var homeMessages: [HomeMessage] + + init(homeMessages: [HomeMessage]) { + self.homeMessages = homeMessages + } + + func refresh() { + + } + + func didAppear(_ homeMessage: HomeMessage) { + // no-op + } + + func dismissHomeMessage(_ homeMessage: HomeMessage) { + homeMessages = homeMessages.dropLast() + } +} diff --git a/DuckDuckGo/TabSwitcherTransition.swift b/DuckDuckGo/TabSwitcherTransition.swift index 5aebcad997..ea098cd149 100644 --- a/DuckDuckGo/TabSwitcherTransition.swift +++ b/DuckDuckGo/TabSwitcherTransition.swift @@ -86,7 +86,7 @@ class TabSwitcherTransitionDelegate: NSObject, UIViewControllerTransitioningDele return nil } - if mainVC.homeController != nil { + if mainVC.newTabPageViewController != nil { return FromHomeScreenTransition(mainViewController: mainVC, tabSwitcherViewController: tabSwitcherVC) } diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 12ef1e53ee..8e8d715052 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -84,12 +84,14 @@ extension TabViewController { entries.append(self.buildToggleProtectionEntry(forDomain: domain)) } - let name = UserText.actionReportBrokenSite - entries.append(BrowsingMenuEntry.regular(name: name, - image: UIImage(named: "Feedback-16")!, - action: { [weak self] in - self?.onReportBrokenSiteAction() - })) + if link != nil { + let name = UserText.actionReportBrokenSite + entries.append(BrowsingMenuEntry.regular(name: name, + image: UIImage(named: "Feedback-16")!, + action: { [weak self] in + self?.onReportBrokenSiteAction() + })) + } entries.append(.separator) diff --git a/DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift b/DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift new file mode 100644 index 0000000000..dcdebd766c --- /dev/null +++ b/DuckDuckGoTests/FavoritesListInteractingAdapterTests.swift @@ -0,0 +1,91 @@ +// +// FavoritesListInteractingAdapterTests.swift +// DuckDuckGo +// +// 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 XCTest +import Combine +import Bookmarks + +@testable import DuckDuckGo + +final class FavoritesListInteractingAdapterTests: XCTestCase { + + private var favoritesListInteracting: MockFavoritesListInteracting! + private var appSettings: AppSettingsMock! + + private var cancellables: Set = [] + + override func setUpWithError() throws { + favoritesListInteracting = MockFavoritesListInteracting() + appSettings = AppSettingsMock() + } + + override func tearDownWithError() throws { + cancellables.removeAll() + } + + func testPublishesUpdateWhenFavoritesDisplayModeChanges() { + let expectation = XCTestExpectation(description: #function) + let sut = createSUT() + + sut.externalUpdates.sink { + XCTAssertTrue(Thread.isMainThread) + expectation.fulfill() + } + .store(in: &cancellables) + + NotificationCenter.default.post(name: AppUserDefaults.Notifications.favoritesDisplayModeChange, object: nil) + + wait(for: [expectation], timeout: 0.1) + } + + func testPublishesUpdateOnExternalListUpdate() { + let expectation = XCTestExpectation(description: #function) + let publisher = PassthroughSubject() + favoritesListInteracting.externalUpdates = publisher.eraseToAnyPublisher() + + let sut = createSUT() + + sut.externalUpdates.sink { + XCTAssertTrue(Thread.isMainThread) + expectation.fulfill() + } + .store(in: &cancellables) + + publisher.send() + + wait(for: [expectation], timeout: 0.1) + } + + private func createSUT() -> FavoritesListInteractingAdapter { + return FavoritesListInteractingAdapter(favoritesListInteracting: favoritesListInteracting, appSettings: appSettings) + } +} + +private class FavoritesListInteractingMock: FavoritesListInteracting { + var favoritesDisplayMode: Bookmarks.FavoritesDisplayMode = .displayNative(.mobile) + var favorites: [Bookmarks.BookmarkEntity] = [] + func favorite(at index: Int) -> Bookmarks.BookmarkEntity? { + return nil + } + func removeFavorite(_ favorite: Bookmarks.BookmarkEntity) {} + func moveFavorite(_ favorite: Bookmarks.BookmarkEntity, fromIndex: Int, toIndex: Int) { } + var externalUpdates: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + var localUpdates: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + func reloadData() {} +} diff --git a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift similarity index 93% rename from DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift rename to DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift index 66c9506142..440a2934df 100644 --- a/DuckDuckGoTests/HomeViewControllerDaxDialogTests.swift +++ b/DuckDuckGoTests/NewTabPageControllerDaxDialogTests.swift @@ -1,5 +1,5 @@ // -// HomeViewControllerDaxDialogTests.swift +// NewTabPageControllerDaxDialogTests.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -26,12 +26,12 @@ import SwiftUI import Persistence import BrowserServicesKit -final class HomeViewControllerDaxDialogTests: XCTestCase { +final class NewTabPageControllerDaxDialogTests: XCTestCase { var variantManager: CapturingVariantManager! var dialogFactory: CapturingNewTabDaxDialogProvider! var specProvider: MockNewTabDialogSpecProvider! - var hvc: HomeViewController! + var hvc: NewTabPageViewController! override func setUpWithError() throws { let db = CoreDataDatabase.bookmarksMock @@ -56,19 +56,18 @@ final class HomeViewControllerDaxDialogTests: XCTestCase { remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProviding(), duckPlayerStorage: MockDuckPlayerStorage()) let homePageConfiguration = HomePageConfiguration(remoteMessagingClient: remoteMessagingClient, privacyProDataReporter: MockPrivacyProDataReporter()) - let dependencies = HomePageDependencies( - homePageConfiguration: homePageConfiguration, - model: Tab(), - favoritesViewModel: MockFavoritesListInteracting(), - appSettings: AppSettingsMock(), + hvc = NewTabPageViewController( + tab: Tab(), + isNewTabPageCustomizationEnabled: false, + interactionModel: MockFavoritesListInteracting(), syncService: MockDDGSyncing(authState: .active, isSyncInProgress: false), - syncDataProviders: dataProviders, - privacyProDataReporter: MockPrivacyProDataReporter(), + syncBookmarksAdapter: dataProviders.bookmarksAdapter, + homePageMessagesConfiguration: homePageConfiguration, variantManager: variantManager, newTabDialogFactory: dialogFactory, - newTabDialogTypeProvider: specProvider) - hvc = HomeViewController.loadFromStoryboard( - homePageDependecies: dependencies) + newTabDialogTypeProvider: specProvider, + faviconLoader: EmptyFaviconLoading() + ) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() diff --git a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift index 09a02148ca..758ddde88e 100644 --- a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift +++ b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift @@ -50,6 +50,13 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.newTabPageFavoritesSeeLess.name) } + func testReturnsAllFavoritesWhenCustomizationDisabled() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + XCTAssertEqual(sut.prefixedFavorites(for: 1).items.count, 10) + } + func testFiresPixelsOnFavoriteSelected() { let sut = createSUT() @@ -99,6 +106,16 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertFalse(slice.isCollapsible) } + func testPrefixFavoritesDoesNotCreatePlaceholdersWhenCustomizationDisabled() { + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + let slice = sut.prefixedFavorites(for: 3) + + XCTAssertTrue(slice.items.filter(\.isPlaceholder).isEmpty) + XCTAssertTrue(slice.items.isEmpty) + XCTAssertFalse(slice.isCollapsible) + } + func testPrefixFavoritesLimitsToTwoRows() { favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) let sut = createSUT() @@ -109,6 +126,16 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertTrue(slice.isCollapsible) } + func testListNotCollapsibleWhenCustomizationDisabled() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + let favorites = sut.prefixedFavorites(for: 1) + XCTAssertFalse(favorites.isCollapsible) + XCTAssertFalse(sut.isCollapsed) + } + func testAddItemIsLastWhenFavoritesPresent() throws { favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) let sut = createSUT() @@ -126,8 +153,19 @@ final class NewTabPageFavoritesModelTests: XCTestCase { XCTAssertTrue(firstItem == .addFavorite) } - private func createSUT() -> FavoritesViewModel { - FavoritesViewModel(favoriteDataSource: favoriteDataSource, + func testDoesNotAppendAddItemWhenCustomizationDisabled() { + let sut = createSUT(isNewTabPageCustomizationEnabled: false) + + XCTAssertNil(sut.allFavorites.first) + + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + + XCTAssertNil(sut.allFavorites.first(where: { $0 == .addFavorite })) + } + + private func createSUT(isNewTabPageCustomizationEnabled: Bool = true) -> FavoritesViewModel { + FavoritesViewModel(isNewTabPageCustomizationEnabled: isNewTabPageCustomizationEnabled, + favoriteDataSource: favoriteDataSource, faviconLoader: FavoritesFaviconLoader(), pixelFiring: PixelFiringMock.self, dailyPixelFiring: PixelFiringMock.self)