diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index db91b29f90..3e40305000 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -301,6 +301,7 @@ 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 */; }; + 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 */; }; @@ -1504,6 +1505,7 @@ 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 = ""; }; + 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsStorageTests.swift; sourceTree = ""; }; 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 = ""; }; @@ -3618,6 +3620,7 @@ 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */, 6FD0C4202C5BF774000561C9 /* NewTabPageModelTests.swift */, 564DE4562C4150E600D23241 /* HomeViewControllerDaxDialogTests.swift */, + 6FABAA682C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift */, ); name = NewTabPage; sourceTree = ""; @@ -7549,6 +7552,7 @@ C1CDA31E2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift in Sources */, B6AD9E3A28D456820019CDE9 /* PrivacyConfigurationManagerMock.swift in Sources */, F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */, + 6FABAA692C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift in Sources */, 9F5E5AB22C3E606D00165F54 /* ContextualOnboardingPresenterTests.swift in Sources */, 9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */, F13B4BFB1F18E3D900814661 /* TabsModelPersistenceExtensionTests.swift in Sources */, diff --git a/DuckDuckGo/NewTabPageSettingsModel.swift b/DuckDuckGo/NewTabPageSettingsModel.swift index 4e359bc883..dad394bd4f 100644 --- a/DuckDuckGo/NewTabPageSettingsModel.swift +++ b/DuckDuckGo/NewTabPageSettingsModel.swift @@ -25,18 +25,30 @@ final class NewTabPageSettingsModel] = [] + private var filteredItemsOrder: [SettingItem] = [] + /// Enabled items, ordered. @Published private(set) var enabledItems: [SettingItem] = [] + private var indexMapping: [Int: Int] = [:] + private let settingsStorage: Storage + private let visibilityFilter: ((SettingItem) -> Bool) - init(settingsStorage: Storage) { + init(settingsStorage: Storage, visibilityFilter: @escaping ((SettingItem) -> Bool) = { _ in true }) { self.settingsStorage = settingsStorage + self.visibilityFilter = visibilityFilter updatePublishedValues() } func moveItems(from: IndexSet, to: Int) { + let from = mapIndexSet(from) + + // If index is not found it means we're moving to the end. + // Guard index range in case there's no filtering. + let to = indexMapping[to] ?? min(settingsStorage.itemsOrder.count, to + 1) + settingsStorage.moveItems(from, toOffset: to) updatePublishedValues() } @@ -51,18 +63,36 @@ final class NewTabPageSettingsModel IndexSet { + let mappedIndices = indexSet.compactMap { index in + indexMapping[index] + } + + return IndexSet(mappedIndices) } } diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift b/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift index fd960f9009..4cde878e7e 100644 --- a/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift +++ b/DuckDuckGo/NewTabPageShortcutsSettingsModel.swift @@ -18,11 +18,20 @@ // import Foundation +import BrowserServicesKit typealias NewTabPageShortcutsSettingsModel = NewTabPageSettingsModel extension NewTabPageShortcutsSettingsModel { - convenience init(storage: NewTabPageShortcutsSettingsStorage = NewTabPageShortcutsSettingsStorage()) { - self.init(settingsStorage: storage) + convenience init(storage: NewTabPageShortcutsSettingsStorage = NewTabPageShortcutsSettingsStorage(), + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger) { + self.init(settingsStorage: storage) { shortcut in + switch shortcut { + case .aiChat, .bookmarks, .downloads, .settings: + return true + case .passwords: + return featureFlagger.isFeatureOn(.autofillAccessCredentialManagement) + } + } } } diff --git a/DuckDuckGoTests/NewTabPageShortcutsSettingsStorageTests.swift b/DuckDuckGoTests/NewTabPageShortcutsSettingsStorageTests.swift new file mode 100644 index 0000000000..08ca3bd534 --- /dev/null +++ b/DuckDuckGoTests/NewTabPageShortcutsSettingsStorageTests.swift @@ -0,0 +1,121 @@ +// +// NewTabPageShortcutsSettingsStorageTests.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 NewTabPageSettingsModelTests: XCTestCase { + + private let storage = StorageMock(itemsOrder: SettingsItemMock.allCases) + + func testFiltersSettingsUsingProvidedFilter() { + let sut = createSUT { item in + item != .two + } + + XCTAssertFalse(sut.enabledItems.contains(.two)) + } + + func testMovingReordersFullSet() { + let sut = createSUT { item in + item != .two + } + + sut.moveItems(from: IndexSet(integer: 0), to: 1) + + XCTAssertEqual(storage.itemsOrder, [.two, .one, .three]) + } + + func testMovingItemToEndWhenNoFilter() { + let sut = createSUT() + + sut.moveItems(from: IndexSet(integer: 0), to: 3) + + XCTAssertEqual(storage.itemsOrder, [.two, .three, .one]) + } + + func testMovingItemToEndWhenFiltered() { + let sut = createSUT { item in + item != .two + } + + sut.moveItems(from: IndexSet(integer: 0), to: 2) + + XCTAssertEqual(storage.itemsOrder, [.two, .three, .one]) + } + + func testMovingToStartWhenFiltered() { + let sut = createSUT { item in + item != .one + } + + sut.moveItems(from: IndexSet(integer: 1), to: 0) + + XCTAssertEqual(storage.itemsOrder, [.one, .three, .two]) + } + + func testItemsSettingsAreFiltered() { + let sut = createSUT { item in + item != .one + } + + XCTAssertEqual(sut.itemsSettings.map(\.item), [.two, .three]) + } + + func testEnabledItemsAreFiltered() { + let sut = createSUT { item in + item != .one + } + + XCTAssertEqual(sut.enabledItems, [.two, .three]) + } + + private func createSUT(filter: @escaping (SettingsItemMock) -> Bool = { _ in true }) -> NewTabPageSettingsModel { + NewTabPageSettingsModel(settingsStorage: storage, visibilityFilter: filter) + } +} + +private final class StorageMock: NewTabPageSettingsStorage { + var itemsOrder: [SettingsItemMock] + + init(itemsOrder: [SettingsItemMock]) { + self.itemsOrder = itemsOrder + } + + func isEnabled(_ item: SettingsItemMock) -> Bool { + return true + } + + func setItem(_ item: SettingsItemMock, enabled: Bool) { + + } + + func moveItems(_ fromOffsets: IndexSet, toOffset: Int) { + itemsOrder.move(fromOffsets: fromOffsets, toOffset: toOffset) + } + + func save() { + } +} + +private enum SettingsItemMock: CaseIterable, NewTabPageSettingsStorageItem { + case one + case two + case three +}