diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7c6c72e9f5..26cbc537e6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -263,7 +263,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 */; }; @@ -301,7 +301,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 */; }; @@ -356,7 +358,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 */; }; @@ -1543,7 +1545,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 = ""; }; @@ -1581,7 +1583,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 = ""; }; @@ -1637,7 +1641,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 = ""; }; @@ -3846,8 +3850,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 = ""; @@ -3901,7 +3906,7 @@ 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, - 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, + 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */, ); name = Model; sourceTree = ""; @@ -3952,6 +3957,7 @@ 6F03CAF82C32C3AA004179A8 /* Messages */, 6FE127372C20492500EB5724 /* NewTabPage.swift */, 6FD8E51F2C5BA23200345670 /* NewTabPageViewModel.swift */, + 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */, 6FE127392C204BD000EB5724 /* NewTabPageView.swift */, 6FE127452C2054A900EB5724 /* NewTabPageViewController.swift */, 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */, @@ -7453,6 +7459,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 */, @@ -7851,7 +7858,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 */, @@ -7907,7 +7914,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 */, @@ -8018,6 +8025,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 a916d8320a..db7b3be55d 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? @@ -482,7 +478,7 @@ class MainViewController: UIViewController { @objc private func keyboardWillHide() { - if homeController?.isDragging == true, keyboardShowing { + if newTabPageViewController?.isDragging == true, keyboardShowing { Pixel.fire(pixel: .addressBarGestureDismiss) } } @@ -677,7 +673,6 @@ class MainViewController: UIViewController { } self.menuBookmarksViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode self.favoritesViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode - self.homeController?.reloadFavorites() WidgetCenter.shared.reloadAllTimelines() } } @@ -691,9 +686,6 @@ class MainViewController: UIViewController { syncUpdatesCancellable = syncDataProviders.bookmarksAdapter.syncDidCompletePublisher .sink { [weak self] _ in self?.favoritesViewModel.reloadData() - DispatchQueue.main.async { - self?.homeController?.reloadFavorites() - } } } @@ -792,53 +784,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 } @@ -1189,7 +1162,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 @@ -1428,18 +1401,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() { @@ -1749,7 +1711,7 @@ extension MainViewController: BrowserChromeDelegate { updateBlock() } } - + func setNavigationBarHidden(_ hidden: Bool) { if hidden { hideKeyboard() } @@ -1819,7 +1781,7 @@ extension MainViewController: OmniBarDelegate { func onOmniQueryUpdated(_ updatedQuery: String) { if updatedQuery.isEmpty { - if homeController != nil { + if newTabPageViewController != nil { hideSuggestionTray() } else { let didShow = tryToShowSuggestionTray(.favorites) @@ -1888,7 +1850,7 @@ extension MainViewController: OmniBarDelegate { let menuEntries: [BrowsingMenuEntry] let headerEntries: [BrowsingMenuEntry] - if isNewTabPageVisible { + if homeTabManager.isNewTabPageSectionsEnabled && newTabPageViewController != nil { menuEntries = tab.buildShortcutsMenu() headerEntries = [] } else { @@ -1929,7 +1891,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 { @@ -2001,7 +1963,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)) @@ -2018,10 +1980,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 } @@ -2061,7 +2023,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() { @@ -2084,7 +2046,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { } func autocomplete(selectedSuggestion suggestion: Suggestion) { - homeViewController?.chromeDelegate = nil + newTabPageViewController?.chromeDelegate = nil dismissOmniBar() viewCoordinator.omniBar.cancel() switch suggestion { @@ -2109,7 +2071,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) @@ -2190,49 +2152,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) @@ -2508,17 +2427,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 - } } } @@ -2536,7 +2457,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) } } } @@ -2742,7 +2663,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() @@ -2836,7 +2757,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)