diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index b8f4d63b85..3756b727b4 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -154,7 +154,9 @@ public struct UserDefaultsWrapper { case vpnRedditWorkaroundInstalled = "com.duckduckgo.ios.vpn.workaroundInstalled" - + case newTabPageSectionsSettings = "com.duckduckgo.ios.newTabPage.sections.settings" + case newTabPageShortcutsSettings = "com.duckduckgo.ios.newTabPage.shortcuts.settings" + // Debug keys case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7fc2ee925c..ff88f67afa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -258,8 +258,11 @@ 6F03CB052C32EFCC004179A8 /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */; }; 6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB062C32F173004179A8 /* PixelFiring.swift */; }; 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */; }; + 6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */; }; + 6F3537A22C4AB97A009F8717 /* NewTabPagePreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A12C4AB97A009F8717 /* NewTabPagePreferencesModel.swift */; }; 6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; }; 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; }; + 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */; }; 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */; }; 6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; }; 6F64AA592C4818D700CF4489 /* NewTabPageShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA582C4818D700CF4489 /* NewTabPageShortcut.swift */; }; @@ -269,7 +272,12 @@ 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; }; 6F691CCA2C4979EC002E9553 /* FavoritesTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F691CC92C4979EC002E9553 /* FavoritesTooltip.swift */; }; 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; }; + 6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */; }; 6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; }; + 6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */; }; + 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */; }; + 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE2C2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift */; }; + 6F9FFE302C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE2F2C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift */; }; 6FA3438F2C3D3BC300470677 /* Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA3438E2C3D3BC300470677 /* Favorite.swift */; }; 6FA343922C3D3C3B00470677 /* FavoriteIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */; }; 6FB1FE9E2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1FE9D2C24D41D0075B68B /* NewTabPageSectionsDebugView.swift */; }; @@ -1412,8 +1420,11 @@ 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPixelFiring.swift; sourceTree = ""; }; 6F03CB062C32F173004179A8 /* PixelFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFiring.swift; sourceTree = ""; }; 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFiringAsync.swift; sourceTree = ""; }; + 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsStorage.swift; sourceTree = ""; }; + 6F3537A12C4AB97A009F8717 /* NewTabPagePreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPagePreferencesModel.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 = ""; }; + 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorage.swift; sourceTree = ""; }; 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleExpandButtonView.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 = ""; }; @@ -1423,7 +1434,12 @@ 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = ""; }; 6F691CC92C4979EC002E9553 /* FavoritesTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesTooltip.swift; sourceTree = ""; }; 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = ""; }; + 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorageTests.swift; sourceTree = ""; }; 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = ""; }; + 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsStorage.swift; sourceTree = ""; }; + 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsStorage.swift; sourceTree = ""; }; + 6F9FFE2C2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsModel.swift; sourceTree = ""; }; + 6F9FFE2F2C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsModel.swift; sourceTree = ""; }; 6FA3438E2C3D3BC300470677 /* Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favorite.swift; sourceTree = ""; }; 6FA343912C3D3C3B00470677 /* FavoriteIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteIconView.swift; sourceTree = ""; }; 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; @@ -3494,10 +3510,20 @@ children = ( 6F03CB002C32ED42004179A8 /* NewTabPageMessagesModelTests.swift */, 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */, + 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */, ); name = NewTabPage; sourceTree = ""; }; + 6F35379C2C4AAF1C009F8717 /* Preferences */ = { + isa = PBXGroup; + children = ( + 6F9FFE2E2C57B14100A238BE /* Model */, + 6F9FFE2B2C57AE4200A238BE /* Storage */, + ); + name = Preferences; + sourceTree = ""; + }; 6F691CC82C4979DD002E9553 /* Tooltip */ = { isa = PBXGroup; children = ( @@ -3506,6 +3532,27 @@ name = Tooltip; sourceTree = ""; }; + 6F9FFE2B2C57AE4200A238BE /* Storage */ = { + isa = PBXGroup; + children = ( + 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */, + 6F5345AE2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift */, + 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */, + 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */, + ); + name = Storage; + sourceTree = ""; + }; + 6F9FFE2E2C57B14100A238BE /* Model */ = { + isa = PBXGroup; + children = ( + 6F9FFE2C2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift */, + 6F9FFE2F2C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift */, + 6F3537A12C4AB97A009F8717 /* NewTabPagePreferencesModel.swift */, + ); + name = Model; + sourceTree = ""; + }; 6FA3438D2C3D3BB800470677 /* Model */ = { isa = PBXGroup; children = ( @@ -3572,6 +3619,7 @@ 6FE1273B2C204C0D00EB5724 /* Subviews */ = { isa = PBXGroup; children = ( + 6F35379C2C4AAF1C009F8717 /* Preferences */, 6FE127472C20941A00EB5724 /* Shortcuts */, 6FE127412C204DE900EB5724 /* Favorites */, 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */, @@ -6788,6 +6836,7 @@ 853C5F6121C277C7001F7A05 /* global.swift in Sources */, EE9D68D82AE15AD600B55EF4 /* UIApplicationExtension.swift in Sources */, 566B73702BECD46800FF1959 /* MainViewController+SyncAlerts.swift in Sources */, + 6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */, F13B4BD31F1822C700814661 /* Tab.swift in Sources */, BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */, F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */, @@ -6804,11 +6853,13 @@ C1B924B72ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift in Sources */, 3132FA2A27A0788F00DD7A12 /* QuickLookPreviewHelper.swift in Sources */, D670E5BB2BB6A75300941A42 /* SubscriptionNavigationCoordinator.swift in Sources */, + 6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */, C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */, 4B53648A26718D0E001AA041 /* EmailWaitlist.swift in Sources */, D63677F52BBDB1C300605BA5 /* DaxLogoNavbarTitle.swift in Sources */, 8524CC98246D66E100E59D45 /* String+Markdown.swift in Sources */, CBEFB9142AE0844700DEDE7B /* CriticalAlerts.swift in Sources */, + 6F9FFE302C57B34800A238BE /* NewTabPageSectionsSettingsModel.swift in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, 6FB2A67A2C2C5BAE004D20C8 /* FavoriteEmptyStateItem.swift in Sources */, @@ -6892,6 +6943,7 @@ 83BE9BC3215D69C1009844D9 /* AppConfigurationFetch.swift in Sources */, 37CF91622BB474AA00BADCAE /* CrashCollectionOnboardingView.swift in Sources */, 1EEC460627A9499600E75FCB /* DownloadsList.swift in Sources */, + 6F3537A22C4AB97A009F8717 /* NewTabPagePreferencesModel.swift in Sources */, 9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */, C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, @@ -6948,12 +7000,14 @@ 85C11E4C2090888C00BFFEB4 /* HomeRowReminder.swift in Sources */, 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, + 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, 6FD3F8112C3EFCDB00DA5797 /* FavoritesModel.swift in Sources */, D62EC3C22C248AF800FC9D04 /* DuckNavigationHandling.swift in Sources */, 9FB027142C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift in Sources */, D664C7B62B289AA200CBFA76 /* SubscriptionFlowViewModel.swift in Sources */, 1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */, C1836CE12C359EC90016D057 /* AutofillBreakageReportCellContentView.swift in Sources */, + 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */, 85374D3C21AC41E700FF5A1E /* FavoritesHomeViewSectionRenderer.swift in Sources */, 85DFEDF124C7EEA400973FE7 /* LargeOmniBarState.swift in Sources */, 9880722A25FA497B0039EF4B /* MenuButton.swift in Sources */, @@ -7142,6 +7196,7 @@ C12726F22A5FF8CB00215B02 /* EmailSignupPromptViewController.swift in Sources */, 983EABB8236198F6003948D1 /* DatabaseMigration.swift in Sources */, 314C92B827C3DD660042EC96 /* QuickLookPreviewView.swift in Sources */, + 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */, F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */, 560E990F2BEE2CB800507CE0 /* SyncErrorMessage.swift in Sources */, 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, @@ -7293,6 +7348,7 @@ 569437362BE5160600C0881B /* SyncSettingsViewControllerErrorTests.swift in Sources */, 850559D223CF710C0055C0D5 /* WebCacheManagerTests.swift in Sources */, EEC02C162B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift in Sources */, + 6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */, 987130C5294AAB9F00AB05E0 /* BookmarkEditorViewModelTests.swift in Sources */, BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index cacc794096..bf770fd5ab 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -82,6 +82,9 @@ protocol AppSettings: AnyObject, AppDebugSettings { var duckPlayerMode: DuckPlayerMode { get set } var duckPlayerAskModeOverlayHidden: Bool { get set } + + var newTabPageShortcutsSettings: Data? { get set } + var newTabPageSectionsSettings: Data? { get set } } protocol AppDebugSettings { diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index a9cefce342..fb15f4e095 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -409,6 +409,12 @@ public class AppUserDefaults: AppSettings { object: duckPlayerMode) } } + + @UserDefaultsWrapper(key: .newTabPageShortcutsSettings, defaultValue: nil) + var newTabPageShortcutsSettings: Data? + + @UserDefaultsWrapper(key: .newTabPageSectionsSettings, defaultValue: nil) + var newTabPageSectionsSettings: Data? } extension AppUserDefaults: AppConfigurationFetchStatistics { diff --git a/DuckDuckGo/NewTabPagePreferencesModel.swift b/DuckDuckGo/NewTabPagePreferencesModel.swift new file mode 100644 index 0000000000..9a79cb6ec6 --- /dev/null +++ b/DuckDuckGo/NewTabPagePreferencesModel.swift @@ -0,0 +1,74 @@ +// +// NewTabPagePreferencesModel.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 SwiftUI + +final class NewTabPagePreferencesModel: ObservableObject where Storage.SettingItem == SettingItem { + + /// Preferences page settings collection with bindings + @Published private(set) var itemsSettings: [NTPSetting] = [] + + /// Enabled items, ordered. + @Published private(set) var enabledItems: [SettingItem] = [] + + private let preferencesStorage: Storage + + init(preferencesStorage: Storage) { + self.preferencesStorage = preferencesStorage + + updatePublishedValues() + } + + func moveItems(from: IndexSet, to: Int) { + preferencesStorage.moveItems(from, toOffset: to) + updatePublishedValues() + } + + func save() { + preferencesStorage.save() + } + + private func updatePublishedValues() { + populateSettings() + populateEnabledItems() + } + + private func populateEnabledItems() { + enabledItems = preferencesStorage.enabledItems + } + + private func populateSettings() { + itemsSettings = preferencesStorage.itemsOrder.map { item in + NTPSetting(item: item, isEnabled: Binding(get: { + self.preferencesStorage.isEnabled(item) + }, set: { newValue in + self.preferencesStorage.setItem(item, enabled: newValue) + self.updatePublishedValues() + })) + } + } +} + +extension NewTabPagePreferencesModel { + struct NTPSetting { + let item: Item + let isEnabled: Binding + } +} diff --git a/DuckDuckGo/NewTabPageSectionsSettingsModel.swift b/DuckDuckGo/NewTabPageSectionsSettingsModel.swift new file mode 100644 index 0000000000..680c8afb05 --- /dev/null +++ b/DuckDuckGo/NewTabPageSectionsSettingsModel.swift @@ -0,0 +1,28 @@ +// +// NewTabPageSectionsSettingsModel.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 + +typealias NewTabPageSectionsSettingsModel = NewTabPagePreferencesModel + +extension NewTabPageSectionsSettingsModel { + convenience init(storage: NewTabPageSectionsSettingsStorage = NewTabPageSectionsSettingsStorage()) { + self.init(preferencesStorage: storage) + } +} diff --git a/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift b/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift new file mode 100644 index 0000000000..12f38c47e6 --- /dev/null +++ b/DuckDuckGo/NewTabPageSectionsSettingsStorage.swift @@ -0,0 +1,35 @@ +// +// NewTabPageSectionsSettingsStorage.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 + +enum NewTabPageSection: String, Codable, CaseIterable { + case favorites + case shortcuts +} + +typealias NewTabPageSectionsSettingsStorage = NewTabPageSettingsPersistentStorage + +extension NewTabPageSettingsPersistentStorage { + convenience init() { + self.init(keyPath: \.newTabPageSectionsSettings, + defaultOrder: NewTabPageSection.allCases, + defaultEnabledItems: NewTabPageSection.allCases) + } +} diff --git a/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift b/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift new file mode 100644 index 0000000000..9c746bccf6 --- /dev/null +++ b/DuckDuckGo/NewTabPageSettingsPersistentStorage.swift @@ -0,0 +1,76 @@ +// +// NewTabPageSettingsPersistentStorage.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 + +private struct NewTabPageItemSettings: Codable { + let itemsOrder: [Item] + let enabledItems: Set +} + +final class NewTabPageSettingsPersistentStorage: NewTabPageSettingsStorage { + private(set) var itemsOrder: [Item] + private var enabledItems: Set + + private var appSettings: AppSettings + private let keyPath: WritableKeyPath + + init(appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + keyPath: WritableKeyPath, + defaultOrder: [Item], + defaultEnabledItems: [Item]) { + self.appSettings = appSettings + self.keyPath = keyPath + self.itemsOrder = defaultOrder + self.enabledItems = Set(defaultEnabledItems) + + self.load() + } + + func isEnabled(_ item: Item) -> Bool { + enabledItems.contains(item) + } + + func setItem(_ item: Item, enabled: Bool) { + if enabled { + enabledItems.insert(item) + } else { + enabledItems.remove(item) + } + } + + func moveItems(_ fromOffsets: IndexSet, toOffset: Int) { + itemsOrder.move(fromOffsets: fromOffsets, toOffset: toOffset) + } + + func save() { + let newSettings = NewTabPageItemSettings(itemsOrder: itemsOrder, enabledItems: enabledItems) + if let data = try? JSONEncoder().encode(newSettings) { + appSettings[keyPath: keyPath] = data + } + } + + private func load() { + if let settingsData = appSettings[keyPath: keyPath], + let settings = try? JSONDecoder().decode(NewTabPageItemSettings.self, from: settingsData) { + itemsOrder = settings.itemsOrder + enabledItems = settings.enabledItems + } + } +} diff --git a/DuckDuckGo/NewTabPageSettingsStorage.swift b/DuckDuckGo/NewTabPageSettingsStorage.swift new file mode 100644 index 0000000000..cfd20f3e1a --- /dev/null +++ b/DuckDuckGo/NewTabPageSettingsStorage.swift @@ -0,0 +1,40 @@ +// +// NewTabPageSettingsStorage.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 + +typealias NewTabPageSettingsStorageItem = Codable & Hashable + +protocol NewTabPageSettingsStorage { + + associatedtype SettingItem: NewTabPageSettingsStorageItem + + var itemsOrder: [SettingItem] { get } + + func isEnabled(_ item: SettingItem) -> Bool + func setItem(_ item: SettingItem, enabled: Bool) + + func moveItems(_ fromOffsets: IndexSet, toOffset: Int) + + func save() +} + +extension NewTabPageSettingsStorage { + var enabledItems: [SettingItem] { itemsOrder.filter(isEnabled) } +} diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift b/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift new file mode 100644 index 0000000000..1ad779fa68 --- /dev/null +++ b/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift @@ -0,0 +1,28 @@ +// +// NewTabPageShortcutsSettingsModel.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 + +typealias NewTabPageShortcutsSettingsModel = NewTabPagePreferencesModel + +extension NewTabPageShortcutsSettingsModel { + convenience init(storage: NewTabPageShortcutsSettingsStorage = NewTabPageShortcutsSettingsStorage()) { + self.init(preferencesStorage: storage) + } +} diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift new file mode 100644 index 0000000000..c769e31b44 --- /dev/null +++ b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift @@ -0,0 +1,30 @@ +// +// NewTabPageShortcutsSettingsStorage.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 + +typealias NewTabPageShortcutsSettingsStorage = NewTabPageSettingsPersistentStorage + +extension NewTabPageSettingsPersistentStorage { + convenience init() { + self.init(keyPath: \.newTabPageShortcutsSettings, + defaultOrder: NewTabPageShortcut.allCases, + defaultEnabledItems: NewTabPageShortcut.allCases) + } +} diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index fc8099f289..700ee3fdcc 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -86,4 +86,7 @@ class AppSettingsMock: AppSettings { var duckPlayerMode: DuckDuckGo.DuckPlayerMode = .alwaysAsk var duckPlayerAskModeOverlayHidden: Bool = false + + var newTabPageShortcutsSettings: Data? + var newTabPageSectionsSettings: Data? } diff --git a/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift b/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift new file mode 100644 index 0000000000..30f58cf733 --- /dev/null +++ b/DuckDuckGoTests/NewTabPageSettingsPersistentStorageTests.swift @@ -0,0 +1,109 @@ +// +// NewTabPageSettingsPersistentStorageTests.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 + +@testable import DuckDuckGo + +final class NewTabPageSettingsPersistentStorageTests: XCTestCase { + + private var appSettings = AppSettingsMock() + + func testLoadsInitialStateFromDefaults() { + let sut = createSUT() + + XCTAssertEqual(sut.itemsOrder, Constant.defaultItems) + XCTAssertEqual(sut.enabledItems, Constant.defaultItems) + } + + func testUsesDefaultsIfDataCorrupted() { + let sut = createSUT() + + appSettings[keyPath: Constant.keyPath] = "Random data".data(using: .utf8) + + XCTAssertEqual(sut.itemsOrder, Constant.defaultItems) + XCTAssertEqual(sut.enabledItems, Constant.defaultItems) + } + + func testDisableItem() { + let sut = createSUT() + + sut.setItem(.one, enabled: false) + + XCTAssertFalse(sut.isEnabled(.one)) + XCTAssertFalse(sut.enabledItems.contains(.one)) + } + + func testEnableItem() { + let sut = createSUT(defaultEnabledItems: []) + + sut.setItem(.one, enabled: true) + + XCTAssertTrue(sut.isEnabled(.one)) + XCTAssertTrue(sut.enabledItems.contains(.one)) + } + + func testSaveAndRestore() { + var sut = createSUT(defaultOrder: [.three, .two, .one], defaultEnabledItems: [.two]) + + sut.save() + + sut = createSUT() + + XCTAssertEqual(sut.enabledItems, [.two]) + XCTAssertEqual(sut.itemsOrder, [.three, .two, .one]) + } + + func testMove() { + let sut = createSUT() + + sut.moveItems(IndexSet(integer: 0), toOffset: 2) + + XCTAssertEqual(sut.itemsOrder, [.two, .one, .three]) + } + + func testEnabledItemsPreserveOrder() { + let order = [StorageItem.one, .three, .two] + let sut = createSUT(defaultOrder: order) + + XCTAssertEqual(sut.enabledItems, order) + + sut.moveItems(IndexSet(integer: 0), toOffset: 2) + + XCTAssertEqual(sut.enabledItems, [.three, .one, .two]) + } + + private func createSUT(defaultOrder: [StorageItem] = Constant.defaultItems, defaultEnabledItems: [StorageItem] = Constant.defaultItems) -> NewTabPageSettingsPersistentStorage { + NewTabPageSettingsPersistentStorage(appSettings: appSettings, + keyPath: Constant.keyPath, + defaultOrder: defaultOrder, + defaultEnabledItems: defaultEnabledItems) + } + + private enum Constant { + static let defaultItems = [StorageItem.one, .two, .three] + static let keyPath = \AppSettings.newTabPageSectionsSettings + } +} + +private enum StorageItem: NewTabPageSettingsStorageItem, CaseIterable { + case one + case two + case three +}