diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index d7ff07905b..5f450c2359 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -766,7 +766,6 @@ extension Pixel { case newTabPageMessageDismissed case newTabPageFavoritesPlaceholderTapped - case newTabPageFavoritesInfoTooltip case newTabPageFavoritesSeeMore case newTabPageFavoritesSeeLess @@ -1581,7 +1580,6 @@ extension Pixel.Event { case .newTabPageMessageDismissed: return "m_new_tab_page_message_dismissed" case .newTabPageFavoritesPlaceholderTapped: return "m_new_tab_page_favorites_placeholder_click" - case .newTabPageFavoritesInfoTooltip: return "m_new_tab_page_favorites_info_tooltip" case .newTabPageFavoritesSeeMore: return "m_new_tab_page_favorites_see_more" case .newTabPageFavoritesSeeLess: return "m_new_tab_page_favorites_see_less" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5ed067951d..af3f96fe35 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -326,10 +326,9 @@ 6FABAA692C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */; }; 6FB1FE9E2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */; }; 6FB1FEA22C256ACD0075B68B /* NewTabPageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */; }; - 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */; }; - 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */; }; + 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */; }; 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */; }; - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */; }; + 6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */; }; 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; }; 6FD0C41F2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */; }; 6FD0C4212C5BF774000561C9 /* NewTabPageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */; }; @@ -338,7 +337,6 @@ 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */; }; - 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */; }; 6FD3F8132C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */; }; 6FD3F8192C41252900DA5797 /* NewTabPageControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */; }; 6FD8E51E2C5B84DE00345670 /* NewTabPageIntroMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */; }; @@ -347,8 +345,7 @@ 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */; }; 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */; }; - 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */; }; - 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */; }; + 6FDC64052C98515E00DB71B3 /* FavoriteAddItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */; }; 6FE095D82BD90AFB00490FF8 /* UniversalOmniBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */; }; 6FE127382C20492500EB5724 /* NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127372C20492500EB5724 /* NewTabPage.swift */; }; 6FE1273A2C204BD000EB5724 /* NewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE127392C204BD000EB5724 /* NewTabPageView.swift */; }; @@ -1606,10 +1603,9 @@ 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsDebugView.swift; sourceTree = ""; }; 6FB1FEA12C256ACD0075B68B /* NewTabPageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageManager.swift; sourceTree = ""; }; - 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteEmptyStateItem.swift; sourceTree = ""; }; - 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesEmptyStateView.swift; sourceTree = ""; }; + 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritePlaceholderItemView.swift; sourceTree = ""; }; 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageGridView.swift; sourceTree = ""; }; - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDefaultViewModel.swift; sourceTree = ""; }; + 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetupTests.swift; sourceTree = ""; }; 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewModelTests.swift; sourceTree = ""; }; @@ -1618,7 +1614,6 @@ 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOrientationEnvironmentValue.swift; sourceTree = ""; }; - 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewDataSource.swift; sourceTree = ""; }; 6FD3F8182C41252900DA5797 /* NewTabPageControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDelegate.swift; sourceTree = ""; }; 6FD8E51D2C5B84DE00345670 /* NewTabPageIntroMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageView.swift; sourceTree = ""; }; @@ -1627,8 +1622,7 @@ 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; 6FDC64002C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroDataStoring.swift; sourceTree = ""; }; 6FDC64022C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStore.swift; sourceTree = ""; }; - 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFavoritePlaceholderItemView.swift; sourceTree = ""; }; - 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesSectionHeader.swift; sourceTree = ""; }; + 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteAddItemView.swift; sourceTree = ""; }; 6FE095D72BD90AFB00490FF8 /* UniversalOmniBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarState.swift; sourceTree = ""; }; 6FE127372C20492500EB5724 /* NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPage.swift; sourceTree = ""; }; 6FE127392C204BD000EB5724 /* NewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageView.swift; sourceTree = ""; }; @@ -3893,10 +3887,9 @@ 6FA3438D2C3D3BB800470677 /* Model */ = { isa = PBXGroup; children = ( - 6FB2A67F2C2EA950004D20C8 /* FavoritesDefaultViewModel.swift */, + 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */, 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */, 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */, - 6FD3F8102C3EFCDB00DA5797 /* FavoritesViewModel.swift */, 6FA3438E2C3D3BC300470677 /* Favorite.swift */, 6FEC0B842C999352006B4F6E /* FavoriteItem.swift */, 6FEC0B872C999961006B4F6E /* FavoriteDataSource.swift */, @@ -3909,7 +3902,8 @@ children = ( 6FE127422C204DF700EB5724 /* FavoriteItemView.swift */, 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */, - 6FDC64042C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift */, + 6FB2A6792C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift */, + 6FDC64042C98515E00DB71B3 /* FavoriteAddItemView.swift */, ); name = Item; sourceTree = ""; @@ -3922,16 +3916,6 @@ name = NewTabPageSectionsDebugView; sourceTree = ""; }; - 6FB2A6782C2C5B9E004D20C8 /* EmptyState */ = { - isa = PBXGroup; - children = ( - 6FE0183F2C25CB3F001F680D /* FavoritesSectionHeader.swift */, - 6FB2A6792C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift */, - 6FB2A67B2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift */, - ); - name = EmptyState; - sourceTree = ""; - }; 6FD1BAE02B87A0E8000C475C /* AdAttribution */ = { isa = PBXGroup; children = ( @@ -3989,7 +3973,6 @@ 6F691CC82C4979DD002E9553 /* Tooltip */, 6FA343902C3D3C2500470677 /* Item */, 6FA3438D2C3D3BB800470677 /* Model */, - 6FB2A6782C2C5B9E004D20C8 /* EmptyState */, 6FE1273C2C204C2500EB5724 /* FavoritesView.swift */, ); name = Favorites; @@ -7344,7 +7327,6 @@ 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */, 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */, F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, - 6FB2A67C2C2D9DF0004D20C8 /* FavoritesEmptyStateView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, @@ -7424,7 +7406,7 @@ 6F9FFE302C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, - 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */, + 6FB2A67A2C2C5BAE004D20C8 /* FavoritePlaceholderItemView.swift in Sources */, 6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */, 9F4CC5242C4A4F0D006A96EB /* SwiftUITestUtilities.swift in Sources */, 6FDC64032C92F4D600DB71B3 /* NewTabPageSettingsPersistentStore.swift in Sources */, @@ -7576,7 +7558,6 @@ 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, - 6FD3F8112C3EFCDB00DA5797 /* FavoritesViewModel.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckPlayerNavigationHandling.swift in Sources */, 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, @@ -7607,7 +7588,7 @@ D6E83C662B23936F006C8AFB /* SettingsDebugView.swift in Sources */, C1641EB12BC2F52B0012607A /* ImportPasswordsView.swift in Sources */, CBFCB30E2B2CD47800253E9E /* ConfigurationURLDebugViewController.swift in Sources */, - 6FDC64052C98515E00DB71B3 /* AddFavoritePlaceholderItemView.swift in Sources */, + 6FDC64052C98515E00DB71B3 /* FavoriteAddItemView.swift in Sources */, 982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */, F446B9B5251150AC00324016 /* HomeMessageViewSectionRenderer.swift in Sources */, D6E0C1852B7A2B9400D5E1E9 /* DesktopDownloadPlatformConstants.swift in Sources */, @@ -7697,7 +7678,7 @@ 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, - 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultViewModel.swift in Sources */, + 6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */, 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, @@ -7859,7 +7840,6 @@ D6E0C1892B7A2E0D00D5E1E9 /* DesktopDownloadViewModel.swift in Sources */, 8524CC9A246DA81700E59D45 /* FullscreenDaxDialogViewController.swift in Sources */, 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */, - 6FE018402C25CB3F001F680D /* FavoritesSectionHeader.swift in Sources */, 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */, F17669D71E43401C003D3222 /* MainViewController.swift in Sources */, 6FE127462C2054A900EB5724 /* NewTabPageViewController.swift in Sources */, diff --git a/DuckDuckGo/AddFavoritePlaceholderItemView.swift b/DuckDuckGo/FavoriteAddItemView.swift similarity index 89% rename from DuckDuckGo/AddFavoritePlaceholderItemView.swift rename to DuckDuckGo/FavoriteAddItemView.swift index 09b11802c0..85d9b92c1d 100644 --- a/DuckDuckGo/AddFavoritePlaceholderItemView.swift +++ b/DuckDuckGo/FavoriteAddItemView.swift @@ -1,5 +1,5 @@ // -// AddFavoritePlaceholderItemView.swift +// FavoriteAddItemView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -20,7 +20,7 @@ import SwiftUI import DesignResourcesKit -struct AddFavoritePlaceholderItemView: View { +struct FavoriteAddItemView: View { var body: some View { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(.clear) @@ -33,6 +33,6 @@ struct AddFavoritePlaceholderItemView: View { } #Preview { - AddFavoritePlaceholderItemView() + FavoriteAddItemView() .frame(width: 100) } diff --git a/DuckDuckGo/FavoriteItem.swift b/DuckDuckGo/FavoriteItem.swift index 9e6c433d53..0edefc1147 100644 --- a/DuckDuckGo/FavoriteItem.swift +++ b/DuckDuckGo/FavoriteItem.swift @@ -23,6 +23,7 @@ import UniformTypeIdentifiers enum FavoriteItem { case favorite(Favorite) case addFavorite + case placeholder(_ id: String) } extension FavoriteItem: Identifiable { @@ -32,6 +33,8 @@ extension FavoriteItem: Identifiable { return favorite.id case .addFavorite: return "addFavorite" + case .placeholder(let id): + return id } } } @@ -43,7 +46,7 @@ extension FavoriteItem: Reorderable { let itemProvider = NSItemProvider(object: (favorite.urlObject?.absoluteString ?? "") as NSString) let metadata = MoveMetadata(itemProvider: itemProvider, type: .plainText) return .movable(metadata) - case .addFavorite: + case .addFavorite, .placeholder: return .stationary } } diff --git a/DuckDuckGo/FavoriteEmptyStateItem.swift b/DuckDuckGo/FavoritePlaceholderItemView.swift similarity index 89% rename from DuckDuckGo/FavoriteEmptyStateItem.swift rename to DuckDuckGo/FavoritePlaceholderItemView.swift index e5b69afd8f..d009558b07 100644 --- a/DuckDuckGo/FavoriteEmptyStateItem.swift +++ b/DuckDuckGo/FavoritePlaceholderItemView.swift @@ -1,5 +1,5 @@ // -// FavoriteEmptyStateItem.swift +// FavoritePlaceholderItemView.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -19,7 +19,7 @@ import SwiftUI -struct FavoriteEmptyStateItem: View { +struct FavoritePlaceholderItemView: View { var body: some View { RoundedRectangle(cornerRadius: 8, style: .continuous) .stroke(Color(designSystemColor: .lines), @@ -29,5 +29,5 @@ struct FavoriteEmptyStateItem: View { } #Preview { - FavoriteEmptyStateItem() + FavoritePlaceholderItemView() } diff --git a/DuckDuckGo/FavoritesDefaultViewModel.swift b/DuckDuckGo/FavoritesDefaultViewModel.swift deleted file mode 100644 index c9088dc5e7..0000000000 --- a/DuckDuckGo/FavoritesDefaultViewModel.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// FavoritesDefaultViewModel.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 Foundation -import Bookmarks -import Combine -import SwiftUI -import Core -import WidgetKit - -protocol NewTabPageFavoriteDataSource { - var externalUpdates: AnyPublisher { get } - var favorites: [Favorite] { get } - - func moveFavorite(_ favorite: Favorite, - fromIndex: Int, - toIndex: Int) - - func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? - func favorite(at index: Int) throws -> Favorite? - func removeFavorite(_ favorite: Favorite) -} - -class FavoritesDefaultViewModel: FavoritesViewModel, FavoritesEmptyStateModel { - - @Published private(set) var allFavorites: [FavoriteItem] = [] - @Published private(set) var isCollapsed: Bool = true - @Published private(set) var isShowingTooltip: Bool = false - - private(set) var faviconLoader: FavoritesFaviconLoading? - - private var cancellables = Set() - - private let favoriteDataSource: NewTabPageFavoriteDataSource - private let pixelFiring: PixelFiring.Type - private let dailyPixelFiring: DailyPixelFiring.Type - - var isEmpty: Bool { - allFavorites.filter(\.isFavorite).isEmpty - } - - init(favoriteDataSource: NewTabPageFavoriteDataSource, - faviconLoader: FavoritesFaviconLoading, - pixelFiring: PixelFiring.Type = Pixel.self, - dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { - self.favoriteDataSource = favoriteDataSource - self.pixelFiring = pixelFiring - self.dailyPixelFiring = dailyPixelFiring - self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in - guard let self else { return } - - await MainActor.run { - self.faviconMissing() - } - }) - - - favoriteDataSource.externalUpdates.sink { [weak self] _ in - self?.updateData() - }.store(in: &cancellables) - - updateData() - } - - func toggleCollapse() { - isCollapsed.toggle() - - if isCollapsed { - pixelFiring.fire(.newTabPageFavoritesSeeLess, withAdditionalParameters: [:]) - } else { - pixelFiring.fire(.newTabPageFavoritesSeeMore, withAdditionalParameters: [:]) - } - } - - func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { - let maxCollapsedItemsCount = columnsCount * 2 - let favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites - let isCollapsible = allFavorites.count > maxCollapsedItemsCount - - return .init(items: favorites, isCollapsible: isCollapsible) - } - - // MARK: - External actions - - var onFaviconMissing: () -> Void = {} - func faviconMissing() { - onFaviconMissing() - } - - var onFavoriteURLSelected: ((URL) -> Void)? - func favoriteSelected(_ favorite: Favorite) { - guard let url = favorite.urlObject else { return } - - pixelFiring.fire(.favoriteLaunchedNTP, withAdditionalParameters: [:]) - dailyPixelFiring.fireDaily(.favoriteLaunchedNTPDaily) - Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) - - onFavoriteURLSelected?(url) - } - - var onFavoriteDeleted: ((BookmarkEntity) -> Void)? - func deleteFavorite(_ favorite: Favorite) { - guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } - - pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) - - favoriteDataSource.removeFavorite(favorite) - - WidgetCenter.shared.reloadAllTimelines() - updateData() - - onFavoriteDeleted?(entity) - } - - var onFavoriteEdit: ((BookmarkEntity) -> Void)? - func editFavorite(_ favorite: Favorite) { - guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } - - pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) - - onFavoriteEdit?(entity) - } - - func moveFavorites(from indexSet: IndexSet, to index: Int) { - guard indexSet.count == 1, - let fromIndex = indexSet.first else { return } - - let favoriteItem = allFavorites[fromIndex] - guard case let .favorite(favorite) = favoriteItem else { return } - - favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) - allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) - } - - // MARK: - Empty state model - - func placeholderTapped() { - pixelFiring.fire(.newTabPageFavoritesPlaceholderTapped, withAdditionalParameters: [:]) - } - - func toggleTooltip() { - isShowingTooltip.toggle() - if isShowingTooltip { - pixelFiring.fire(.newTabPageFavoritesInfoTooltip, withAdditionalParameters: [:]) - } - } - - // MARK: - - - private func updateData() { - var allFavorites = favoriteDataSource.favorites.map { - FavoriteItem.favorite($0) - } - allFavorites.append(.addFavorite) - - self.allFavorites = allFavorites - } -} - -enum FavoriteMappingError: Error { - case missingUUID -} - -private final class MissingFaviconWrapper: FavoritesFaviconLoading { - let loader: FavoritesFaviconLoading - - private(set) var onFaviconMissing: (() async -> Void) - - init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { - self.onFaviconMissing = onFaviconMissing - self.loader = loader - } - - func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { - let favicon = await loader.loadFavicon(for: favorite, size: size) - - if favicon == nil { - await onFaviconMissing() - } - - return favicon - } - - func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { - loader.fakeFavicon(for: favorite, size: size) - } - - func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { - loader.existingFavicon(for: favorite, size: size) - } -} - -private extension FavoriteItem { - var isFavorite: Bool { - switch self { - case .favorite: - return true - case .addFavorite: - return false - } - } -} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift deleted file mode 100644 index 8c81ce6765..0000000000 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// FavoritesEmptyStateView.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 - -struct FavoritesEmptyStateView: View { - @ObservedObject var model: Model - @Binding var isAddingFavorite: Bool - - let geometry: GeometryProxy? - - var body: some View { - ZStack(alignment: .topTrailing) { - VStack(spacing: 16) { - FavoritesSectionHeader(model: model) - - NewTabPageGridView(geometry: geometry) { placeholdersCount in - Button(action: { - isAddingFavorite = true - }, label: { - AddFavoritePlaceholderItemView() - }) - .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) - .frame(width: NewTabPageGrid.Item.edgeSize) - - let placeholders = Array(0..: View { .frame(width: NewTabPageGrid.Item.edgeSize) .previewShape() .transition(.opacity) - case .addFavorite: + case .addFavorite, .placeholder: EmptyView() } } @@ -110,10 +110,17 @@ struct FavoritesView: View { Button(action: { isAddingFavorite = true }, label: { - AddFavoritePlaceholderItemView() + FavoriteAddItemView() }) .buttonStyle(SecondaryFillButtonStyle(isFreeform: true)) .frame(width: NewTabPageGrid.Item.edgeSize) + case .placeholder: + FavoritePlaceholderItemView() + .frame(width: NewTabPageGrid.Item.edgeSize, height: NewTabPageGrid.Item.edgeSize) + .contentShape(.rect) + .onTapGesture { + model.placeholderTapped() + } } } } diff --git a/DuckDuckGo/FavoritesViewModel.swift b/DuckDuckGo/FavoritesViewModel.swift index 624186edb6..781d77a044 100644 --- a/DuckDuckGo/FavoritesViewModel.swift +++ b/DuckDuckGo/FavoritesViewModel.swift @@ -18,37 +18,204 @@ // import Foundation +import Bookmarks +import Combine +import SwiftUI +import Core +import WidgetKit -protocol FavoritesViewModel: AnyObject, ObservableObject { - var allFavorites: [FavoriteItem] { get } - var faviconLoader: FavoritesFaviconLoading? { get } +protocol NewTabPageFavoriteDataSource { + var externalUpdates: AnyPublisher { get } + var favorites: [Favorite] { get } - var isEmpty: Bool { get } - var isCollapsed: Bool { get } + func moveFavorite(_ favorite: Favorite, + fromIndex: Int, + toIndex: Int) - func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice + func bookmarkEntity(for favorite: Favorite) -> BookmarkEntity? + func favorite(at index: Int) throws -> Favorite? + func removeFavorite(_ favorite: Favorite) +} + +struct FavoritesSlice { + let items: [FavoriteItem] + let isCollapsible: Bool +} + +class FavoritesViewModel: ObservableObject { + + @Published private(set) var allFavorites: [FavoriteItem] = [] + @Published private(set) var isCollapsed: Bool = true + + private(set) var faviconLoader: FavoritesFaviconLoading? + + private var cancellables = Set() + + private let favoriteDataSource: NewTabPageFavoriteDataSource + private let pixelFiring: PixelFiring.Type + private let dailyPixelFiring: DailyPixelFiring.Type + + var isEmpty: Bool { + allFavorites.filter(\.isFavorite).isEmpty + } + + init(favoriteDataSource: NewTabPageFavoriteDataSource, + faviconLoader: FavoritesFaviconLoading, + pixelFiring: PixelFiring.Type = Pixel.self, + dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { + self.favoriteDataSource = favoriteDataSource + self.pixelFiring = pixelFiring + self.dailyPixelFiring = dailyPixelFiring + self.faviconLoader = MissingFaviconWrapper(loader: faviconLoader, onFaviconMissing: { [weak self] in + guard let self else { return } + + await MainActor.run { + self.faviconMissing() + } + }) + + + favoriteDataSource.externalUpdates.sink { [weak self] _ in + self?.updateData() + }.store(in: &cancellables) + + updateData() + } + + func toggleCollapse() { + isCollapsed.toggle() + + if isCollapsed { + pixelFiring.fire(.newTabPageFavoritesSeeLess, withAdditionalParameters: [:]) + } else { + pixelFiring.fire(.newTabPageFavoritesSeeMore, withAdditionalParameters: [:]) + } + } + + func prefixedFavorites(for columnsCount: Int) -> FavoritesSlice { + let hasFavorites = allFavorites.contains(where: \.isFavorite) + let maxCollapsedItemsCount = hasFavorites ? columnsCount * 2 : columnsCount + let isCollapsible = allFavorites.count > maxCollapsedItemsCount + + var favorites = isCollapsed ? Array(allFavorites.prefix(maxCollapsedItemsCount)) : allFavorites + + if !hasFavorites { + for _ in favorites.count ..< maxCollapsedItemsCount { + favorites.append(.placeholder(UUID().uuidString)) + } + } + + return .init(items: favorites, isCollapsible: isCollapsible) + } + + // MARK: - External actions + + var onFaviconMissing: () -> Void = {} + func faviconMissing() { + onFaviconMissing() + } - func faviconMissing() + var onFavoriteURLSelected: ((URL) -> Void)? + func favoriteSelected(_ favorite: Favorite) { + guard let url = favorite.urlObject else { return } - // MARK: - Interactions + pixelFiring.fire(.favoriteLaunchedNTP, withAdditionalParameters: [:]) + dailyPixelFiring.fireDaily(.favoriteLaunchedNTPDaily) + Favicons.shared.loadFavicon(forDomain: url.host, intoCache: .fireproof, fromCache: .tabs) - func toggleCollapse() + onFavoriteURLSelected?(url) + } - func favoriteSelected(_ favorite: Favorite) - func editFavorite(_ favorite: Favorite) - func deleteFavorite(_ favorite: Favorite) - func moveFavorites(from indexSet: IndexSet, to index: Int) + var onFavoriteDeleted: ((BookmarkEntity) -> Void)? + func deleteFavorite(_ favorite: Favorite) { + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } + + pixelFiring.fire(.homeScreenDeleteFavorite, withAdditionalParameters: [:]) + + favoriteDataSource.removeFavorite(favorite) + + WidgetCenter.shared.reloadAllTimelines() + updateData() + + onFavoriteDeleted?(entity) + } + + var onFavoriteEdit: ((BookmarkEntity) -> Void)? + func editFavorite(_ favorite: Favorite) { + guard let entity = favoriteDataSource.bookmarkEntity(for: favorite) else { return } + + pixelFiring.fire(.homeScreenEditFavorite, withAdditionalParameters: [:]) + + onFavoriteEdit?(entity) + } + + func moveFavorites(from indexSet: IndexSet, to index: Int) { + guard indexSet.count == 1, + let fromIndex = indexSet.first else { return } + + let favoriteItem = allFavorites[fromIndex] + guard case let .favorite(favorite) = favoriteItem else { return } + + favoriteDataSource.moveFavorite(favorite, fromIndex: fromIndex, toIndex: index) + allFavorites.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: index) + } + + func placeholderTapped() { + pixelFiring.fire(.newTabPageFavoritesPlaceholderTapped, withAdditionalParameters: [:]) + } + + // MARK: - + + private func updateData() { + var allFavorites = favoriteDataSource.favorites.map { + FavoriteItem.favorite($0) + } + allFavorites.append(.addFavorite) + + self.allFavorites = allFavorites + } +} + +enum FavoriteMappingError: Error { + case missingUUID } -protocol FavoritesEmptyStateModel: AnyObject, ObservableObject { +private final class MissingFaviconWrapper: FavoritesFaviconLoading { + let loader: FavoritesFaviconLoading + + private(set) var onFaviconMissing: (() async -> Void) + + init(loader: FavoritesFaviconLoading, onFaviconMissing: @escaping (() async -> Void)) { + self.onFaviconMissing = onFaviconMissing + self.loader = loader + } - var isShowingTooltip: Bool { get } + func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { + let favicon = await loader.loadFavicon(for: favorite, size: size) - func placeholderTapped() - func toggleTooltip() + if favicon == nil { + await onFaviconMissing() + } + + return favicon + } + + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { + loader.fakeFavicon(for: favorite, size: size) + } + + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + loader.existingFavicon(for: favorite, size: size) + } } -struct FavoritesSlice { - let items: [FavoriteItem] - let isCollapsible: Bool +private extension FavoriteItem { + var isFavorite: Bool { + switch self { + case .favorite: + return true + case .addFavorite, .placeholder: + return false + } + } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index 14d97ce3e2..aaed282ca5 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -21,12 +21,12 @@ import SwiftUI import DuckUI import RemoteMessaging -struct NewTabPageView: View { +struct NewTabPageView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject private var viewModel: NewTabPageViewModel @ObservedObject private var messagesModel: NewTabPageMessagesModel - @ObservedObject private var favoritesModel: FavoritesModelType + @ObservedObject private var favoritesViewModel: FavoritesViewModel @ObservedObject private var shortcutsModel: ShortcutsModel @ObservedObject private var shortcutsSettingsModel: NewTabPageShortcutsSettingsModel @ObservedObject private var sectionsSettingsModel: NewTabPageSectionsSettingsModel @@ -36,13 +36,13 @@ struct NewTabPageView some View { - Group { - if favoritesModel.isEmpty { - FavoritesEmptyStateView(model: favoritesModel, - isAddingFavorite: $isAddingFavorite, - geometry: proxy) - .padding(.top, Metrics.nonGridSectionTopPadding) - } else { - FavoritesView(model: favoritesModel, + FavoritesView(model: favoritesViewModel, isAddingFavorite: $isAddingFavorite, geometry: proxy) - } - } } @ViewBuilder @@ -274,7 +260,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -299,7 +285,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { ] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -314,7 +300,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(favorites: []), + favoritesViewModel: FavoritesPreviewModel(favorites: []), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel() @@ -329,7 +315,7 @@ private struct CustomizeButtonPrefKey: PreferenceKey { homeMessages: [] ) ), - favoritesModel: FavoritesPreviewModel(), + favoritesViewModel: FavoritesPreviewModel(), shortcutsModel: ShortcutsModel(), shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), sectionsSettingsModel: NewTabPageSectionsSettingsModel(storage: .emptyStorage()) diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index fabd1ee3ed..22a238bc23 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 @@ -35,7 +35,7 @@ final class NewTabPageViewController: UIHostingController FavoritesDefaultViewModel { - FavoritesDefaultViewModel(favoriteDataSource: favoriteDataSource, - faviconLoader: FavoritesFaviconLoader(), - pixelFiring: PixelFiringMock.self, - dailyPixelFiring: PixelFiringMock.self) + func testPrefixFavoritesLimitsToTwoRows() { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT() + + let slice = sut.prefixedFavorites(for: 4) + + XCTAssertEqual(slice.items.count, 8) + XCTAssertTrue(slice.isCollapsible) + } + + func testAddItemIsLastWhenFavoritesPresent() throws { + favoriteDataSource.favorites.append(contentsOf: Array(repeating: Favorite.stub(), count: 10)) + let sut = createSUT() + + let lastItem = try XCTUnwrap(sut.allFavorites.last) + + XCTAssertTrue(lastItem == .addFavorite) + } + + func testAddItemIsFirstWhenFavoritesEmpty() throws { + let sut = createSUT() + + let firstItem = try XCTUnwrap(sut.allFavorites.first) + + XCTAssertTrue(firstItem == .addFavorite) + } + + private func createSUT() -> FavoritesViewModel { + FavoritesViewModel(favoriteDataSource: favoriteDataSource, + faviconLoader: FavoritesFaviconLoader(), + pixelFiring: PixelFiringMock.self, + dailyPixelFiring: PixelFiringMock.self) } } @@ -129,3 +157,12 @@ private extension Favorite { Favorite(id: UUID().uuidString, title: "foo", domain: "bar") } } + +private extension FavoriteItem { + var isPlaceholder: Bool { + switch self { + case .placeholder: return true + case .favorite, .addFavorite: return false + } + } +}