From 7be8bf67f10241a206556f612ca399b846672755 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Fri, 13 Sep 2024 16:11:24 +0100 Subject: [PATCH 1/5] SKAD4 crash fix (#3361) Task/Issue URL: https://app.asana.com/0/414709148257752/1208303110742725/f Fix for a logging string interpolation in SKAD4 attribution. I think this could be an internal AdAttributionKit error because every thrown error should be not nil and responding to `localisedDescription`, but here looks like something is going wrong when we try to log `Logger.general.error("Attribution: SKAN 4 postback failed \(error.localizedDescription, privacy: .public)")` and accessing `error.localizedDescription ` This attempt tries not to assume anything about the error and just logs a string representation. --- DuckDuckGo/AppDelegate+Attribution.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/AppDelegate+Attribution.swift b/DuckDuckGo/AppDelegate+Attribution.swift index e82dd002e3..bfef272bf4 100644 --- a/DuckDuckGo/AppDelegate+Attribution.swift +++ b/DuckDuckGo/AppDelegate+Attribution.swift @@ -43,7 +43,7 @@ extension AppDelegate { try await AdAttributionKit.Postback.updateConversionValue(conversionValue, coarseConversionValue: .high, lockPostback: true) Logger.general.debug("Attribution: AdAttributionKit postback succeeded") } catch { - Logger.general.error("Attribution: AdAttributionKit postback failed \(error.localizedDescription, privacy: .public)") + Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") } } @@ -52,8 +52,8 @@ extension AppDelegate { do { try await SKAdNetwork.updatePostbackConversionValue(conversionValue, coarseValue: .high, lockWindow: true) Logger.general.debug("Attribution: SKAN 4 postback succeeded") - } catch { - Logger.general.error("Attribution: SKAN 4 postback failed \(error.localizedDescription, privacy: .public)") + } catch let error { + Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") } } From 283411894ef58ed258e0ad80f09228927d7f8eb2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 13 Sep 2024 20:55:02 +0100 Subject: [PATCH 2/5] Add Marketplace Postback handling (#3357) Task/Issue URL: https://app.asana.com/0/72649045549333/1208126219488944/f Tech Design URL: https://app.asana.com/0/72649045549333/1208274586553401/f **Description**: Add Marketplace Postback handling --- Core/MarketplaceAdPostback.swift | 93 +++++++++++++++++ Core/MarketplaceAdPostbackManager.swift | 74 ++++++++++++++ Core/MarketplaceAdPostbackStorage.swift | 62 ++++++++++++ Core/MarketplaceAdPostbackUpdater.swift | 81 +++++++++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 40 +++++++- DuckDuckGo/AppDelegate+Attribution.swift | 60 ----------- DuckDuckGo/AppDelegate.swift | 17 +++- DuckDuckGo/Info.plist | 2 + .../MarketplaceAdPostbackManagerTests.swift | 99 +++++++++++++++++++ 9 files changed, 460 insertions(+), 68 deletions(-) create mode 100644 Core/MarketplaceAdPostback.swift create mode 100644 Core/MarketplaceAdPostbackManager.swift create mode 100644 Core/MarketplaceAdPostbackStorage.swift create mode 100644 Core/MarketplaceAdPostbackUpdater.swift delete mode 100644 DuckDuckGo/AppDelegate+Attribution.swift create mode 100644 DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift diff --git a/Core/MarketplaceAdPostback.swift b/Core/MarketplaceAdPostback.swift new file mode 100644 index 0000000000..d8d9a5ff5a --- /dev/null +++ b/Core/MarketplaceAdPostback.swift @@ -0,0 +1,93 @@ +// +// MarketplaceAdPostback.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 StoreKit +import AdAttributionKit + +enum MarketplaceAdPostback { + case installNewUser + case installReturningUser + + /// An enumeration representing coarse conversion values for both SKAdNetwork and AdAttributionKit. + /// + /// This enum provides a unified interface to handle coarse conversion values, which are used in both SKAdNetwork and AdAttributionKit. + /// Despite having the same value names (`low`, `medium`, `high`), the types for these values differ between the two frameworks. + /// This wrapper simplifies the usage by providing a common interface. + /// + /// - Cases: + /// - `low`: Represents a low conversion value. + /// - `medium`: Represents a medium conversion value. + /// - `high`: Represents a high conversion value. + /// + /// - Properties: + /// - `coarseConversionValue`: Available on iOS 17.4 and later, this property returns the corresponding `CoarseConversionValue` from AdAttributionKit. + /// - `skAdCoarseConversionValue`: Available on iOS 16.1 and later, this property returns the corresponding `SKAdNetwork.CoarseConversionValue`. + /// + enum CoarseConversion { + case low + case medium + case high + + /// Returns the corresponding `CoarseConversionValue` from AdAttributionKit. + @available(iOS 17.4, *) + var coarseConversionValue: CoarseConversionValue { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + + /// Returns the corresponding `SKAdNetwork.CoarseConversionValue`. + @available(iOS 16.1, *) + var skAdCoarseConversionValue: SKAdNetwork.CoarseConversionValue { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + } + + // https://app.asana.com/0/0/1208126219488943/f + var fineValue: Int { + switch self { + case .installNewUser: return 0 + case .installReturningUser: return 1 + } + } + + var coarseValue: CoarseConversion { + switch self { + case .installNewUser: return .high + case .installReturningUser: return .low + } + } + + @available(iOS 17.4, *) + var adAttributionKitCoarseValue: CoarseConversionValue { + return coarseValue.coarseConversionValue + } + + @available(iOS 16.1, *) + var SKAdCoarseValue: SKAdNetwork.CoarseConversionValue { + return coarseValue.skAdCoarseConversionValue + } +} diff --git a/Core/MarketplaceAdPostbackManager.swift b/Core/MarketplaceAdPostbackManager.swift new file mode 100644 index 0000000000..32cfbdea7f --- /dev/null +++ b/Core/MarketplaceAdPostbackManager.swift @@ -0,0 +1,74 @@ +// +// MarketplaceAdPostbackManager.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 + +public protocol MarketplaceAdPostbackManaging { + + /// Updates the install postback based on the return user measurement + /// + /// This method determines whether the user is a returning user or a new user and sends the appropriate postback value: + /// - If the user is returning, it sends the `appLaunchReturningUser` postback value. + /// - If the user is new, it sends the `appLaunchNewUser` postback value. + /// + /// > For the time being, we're also sending `lockPostback` to `true`. + /// > More information can be found [here](https://app.asana.com/0/0/1208126219488943/1208289369964239/f). + func sendAppLaunchPostback() + + /// Updates the stored value for the returning user state. + /// + /// This method updates the storage with the current state of the user (returning or new). + /// Since `ReturnUserMeasurement` will always return `isReturningUser` as `false` after the first run, + /// `MarketplaceAdPostbackManaging` maintains its own storage of the user's state across app launches. + func updateReturningUserValue() +} + +public struct MarketplaceAdPostbackManager: MarketplaceAdPostbackManaging { + private let storage: MarketplaceAdPostbackStorage + private let updater: MarketplaceAdPostbackUpdating + private let returningUserMeasurement: ReturnUserMeasurement + + internal init(storage: MarketplaceAdPostbackStorage = UserDefaultsMarketplaceAdPostbackStorage(), + updater: MarketplaceAdPostbackUpdating = MarketplaceAdPostbackUpdater(), + returningUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement()) { + self.storage = storage + self.updater = updater + self.returningUserMeasurement = returningUserMeasurement + } + + public init() { + self.storage = UserDefaultsMarketplaceAdPostbackStorage() + self.updater = MarketplaceAdPostbackUpdater() + self.returningUserMeasurement = KeychainReturnUserMeasurement() + } + + public func sendAppLaunchPostback() { + guard let isReturningUser = storage.isReturningUser else { return } + + if isReturningUser { + updater.updatePostback(.installReturningUser, lockPostback: true) + } else { + updater.updatePostback(.installNewUser, lockPostback: true) + } + } + + public func updateReturningUserValue() { + storage.updateReturningUserValue(returningUserMeasurement.isReturningUser) + } +} diff --git a/Core/MarketplaceAdPostbackStorage.swift b/Core/MarketplaceAdPostbackStorage.swift new file mode 100644 index 0000000000..66734f1818 --- /dev/null +++ b/Core/MarketplaceAdPostbackStorage.swift @@ -0,0 +1,62 @@ +// +// MarketplaceAdPostbackStorage.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 + +/// A protocol defining the storage for marketplace ad postback data. +protocol MarketplaceAdPostbackStorage { + + /// A Boolean value indicating whether the user is a returning user. + /// + /// If the value is `nil`, it means the storage was never set. + var isReturningUser: Bool? { get } + + /// Updates the stored value indicating whether the user is a returning user. + /// + /// - Parameter value: A Boolean value indicating whether the user is a returning user. + func updateReturningUserValue(_ value: Bool) +} + +/// A concrete implementation of `MarketplaceAdPostbackStorage` that uses `UserDefaults` for storage. +struct UserDefaultsMarketplaceAdPostbackStorage: MarketplaceAdPostbackStorage { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + var isReturningUser: Bool? { + userDefaults.isReturningUser + } + + func updateReturningUserValue(_ value: Bool) { + userDefaults.isReturningUser = value + } +} + +private extension UserDefaults { + enum Keys { + static let isReturningUser = "marketplaceAdPostback.isReturningUser" + } + + var isReturningUser: Bool? { + get { object(forKey: Keys.isReturningUser) as? Bool } + set { set(newValue, forKey: Keys.isReturningUser) } + } +} diff --git a/Core/MarketplaceAdPostbackUpdater.swift b/Core/MarketplaceAdPostbackUpdater.swift new file mode 100644 index 0000000000..0b8d3a819b --- /dev/null +++ b/Core/MarketplaceAdPostbackUpdater.swift @@ -0,0 +1,81 @@ +// +// MarketplaceAdPostbackUpdater.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 AdAttributionKit +import os.log +import StoreKit + +/// Updates anonymous attribution values. +/// +/// DuckDuckGo uses the SKAdNetwork framework to monitor anonymous install attribution data. +/// No personally identifiable data is involved. +/// DuckDuckGo does not use the App Tracking Transparency framework at any point. +/// See https://developer.apple.com/documentation/storekit/skadnetwork/ for details. +/// + +protocol MarketplaceAdPostbackUpdating { + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) +} + +struct MarketplaceAdPostbackUpdater: MarketplaceAdPostbackUpdating { + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { +#if targetEnvironment(simulator) + Logger.general.debug("Attribution: Postback doesn't work on simulators, returning early...") +#else + if #available(iOS 17.4, *) { + // https://developer.apple.com/documentation/adattributionkit/adattributionkit-skadnetwork-interoperability + Task { + await updateAdAttributionKitPostback(postback, lockPostback: lockPostback) + } + updateSKANPostback(postback, lockPostback: lockPostback) + } else if #available(iOS 16.1, *) { + updateSKANPostback(postback, lockPostback: lockPostback) + } +#endif + } + + @available(iOS 17.4, *) + private func updateAdAttributionKitPostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) async { + do { + try await AdAttributionKit.Postback.updateConversionValue(postback.fineValue, + coarseConversionValue: postback.adAttributionKitCoarseValue, + lockPostback: lockPostback) + Logger.general.debug("Attribution: AdAttributionKit postback succeeded") + } catch { + Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") + } + } + + @available(iOS 16.1, *) + private func updateSKANPostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { + /// Switched to using the completion handler API instead of async due to an encountered error. + /// Error report: + /// https://errors.duckduckgo.com/organizations/ddg/issues/104096/events/ab29c80e711f11efbf32499bdc26619c/ + + SKAdNetwork.updatePostbackConversionValue(postback.fineValue, + coarseValue: postback.SKAdCoarseValue) { error in + if let error = error { + Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") + } else { + Logger.general.debug("Attribution: SKAN 4 postback succeeded") + } + } + } +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d9f651fc28..d751612bbe 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -154,10 +154,12 @@ 3158461A281B08F5004ADB8B /* AutofillLoginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31584619281B08F5004ADB8B /* AutofillLoginListViewModel.swift */; }; 3161D13227AC161B00285CF6 /* DownloadMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3161D13127AC161B00285CF6 /* DownloadMetadata.swift */; }; 31669B9A28020A460071CC18 /* SaveLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31669B9928020A460071CC18 /* SaveLoginViewModel.swift */; }; + 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */; }; 316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */; }; 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */; }; 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; }; 317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; }; + 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; }; 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; }; 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; }; 319A371028299A850079FBCE /* PasswordHider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319A370F28299A850079FBCE /* PasswordHider.swift */; }; @@ -166,6 +168,9 @@ 31A42564285A09E800049386 /* FaviconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42563285A09E800049386 /* FaviconView.swift */; }; 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42565285A0A6300049386 /* FaviconViewModel.swift */; }; 31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */; }; + 31B2F10F2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */; }; + 31B2F1112C92FEE000CD30E3 /* MarketplaceAdPostback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */; }; + 31B2F1132C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */; }; 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F11E287846320040427A /* NoMicPermissionAlert.swift */; }; 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B524562715BB23002225AB /* WebJSAlert.swift */; }; 31BC5F412C2B0B540004DF37 /* DuckPlayer.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31BC5F402C2B0B540004DF37 /* DuckPlayer.xcassets */; }; @@ -1065,7 +1070,6 @@ F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */; }; F15531942BF215F60029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F15531932BF215F60029ED04 /* Subscription */; }; F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */; }; - F1564F032B7B915F00D454A6 /* AppDelegate+Attribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */; }; F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */; }; F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */; }; F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C141E57336D00DEDCAF /* TabManager.swift */; }; @@ -1433,11 +1437,13 @@ 31584619281B08F5004ADB8B /* AutofillLoginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListViewModel.swift; sourceTree = ""; }; 3161D13127AC161B00285CF6 /* DownloadMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMetadata.swift; sourceTree = ""; }; 31669B9928020A460071CC18 /* SaveLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveLoginViewModel.swift; sourceTree = ""; }; + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManagerTests.swift; sourceTree = ""; }; 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToDownloadsAlert.swift; sourceTree = ""; }; 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadActionMessageViewHelper.swift; sourceTree = ""; }; 3170048127A9504F00C03F35 /* DownloadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMocks.swift; sourceTree = ""; }; 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = ""; }; 31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = ""; }; + 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = ""; }; 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = ""; }; 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = ""; }; 319A370F28299A850079FBCE /* PasswordHider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordHider.swift; sourceTree = ""; }; @@ -1446,6 +1452,9 @@ 31A42563285A09E800049386 /* FaviconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconView.swift; sourceTree = ""; }; 31A42565285A0A6300049386 /* FaviconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconViewModel.swift; sourceTree = ""; }; 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; + 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManager.swift; sourceTree = ""; }; + 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostback.swift; sourceTree = ""; }; + 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackUpdater.swift; sourceTree = ""; }; 31B2F11E287846320040427A /* NoMicPermissionAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoMicPermissionAlert.swift; sourceTree = ""; }; 31B524562715BB23002225AB /* WebJSAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebJSAlert.swift; sourceTree = ""; }; 31BC5F402C2B0B540004DF37 /* DuckPlayer.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DuckPlayer.xcassets; sourceTree = ""; }; @@ -2891,7 +2900,6 @@ F143C32C1E4A9A4800CFDE3A /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIViewControllerExtension.swift; path = ../Core/UIViewControllerExtension.swift; sourceTree = ""; }; F143C3451E4AA32D00CFDE3A /* SearchBarExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchBarExtension.swift; path = ../Core/SearchBarExtension.swift; sourceTree = ""; }; F14E491E1E391CE900DC037C /* URLExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtensionTests.swift; sourceTree = ""; }; - F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Attribution.swift"; sourceTree = ""; }; F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteViewController.swift; sourceTree = ""; }; F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherViewController.swift; sourceTree = ""; }; @@ -3514,6 +3522,14 @@ name = LoginDetails; sourceTree = ""; }; + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 316931DA27BD24B60095F5ED /* Alerts */ = { isa = PBXGroup; children = ( @@ -3564,6 +3580,17 @@ name = Table; sourceTree = ""; }; + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */, + 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */, + 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */, + 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 31C138A127A334F600FFD4B2 /* Downloads */ = { isa = PBXGroup; children = ( @@ -5774,6 +5801,7 @@ F143C2E51E4A4CD400CFDE3A /* Core */ = { isa = PBXGroup; children = ( + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */, F1CE42A71ECA0A520074A8DF /* Bookmarks */, 837774491F8E1ECE00E17A29 /* ContentBlocker */, F143C2E61E4A4CD400CFDE3A /* Core.h */, @@ -6097,7 +6125,6 @@ CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */, - F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */, 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, 85BA58541F34F49E00C6E8CA /* AppUserDefaults.swift */, @@ -6242,6 +6269,7 @@ F1E092B31E92A6B900732CCC /* Core */ = { isa = PBXGroup; children = ( + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */, 858479CA2B8795BF00D156C1 /* History */, EA7EFE662677F5BD0075464E /* PrivacyReferenceTests */, 83EDCC3E1F86B363005CDFCD /* API */, @@ -7330,7 +7358,6 @@ 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, 98D98A7425ED88D100D8E3DF /* BrowsingMenuEntryViewCell.swift in Sources */, - F1564F032B7B915F00D454A6 /* AppDelegate+Attribution.swift in Sources */, 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */, 4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */, CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */, @@ -7920,6 +7947,7 @@ 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */, 314A3EFC293905EC00D3D4C8 /* BrokenSiteReportingTests.swift in Sources */, 851B1283221FE65E004781BC /* ImproveOnboardingExperiment1Tests.swift in Sources */, + 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */, F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */, BBFF18B12C76448100C48D7D /* QuerySubmittedTests.swift in Sources */, 854A8D812C7F4452001D62E5 /* AtbTests.swift in Sources */, @@ -8100,6 +8128,7 @@ 851624C22B95F8BD002D5CD7 /* HistoryCapture.swift in Sources */, 85CA53AC24BBD39300A6288C /* FaviconRequestModifier.swift in Sources */, CB258D1D29A52AF900DEBA24 /* EtagStorage.swift in Sources */, + 31B2F10F2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift in Sources */, C1B7B52D2894469D0098FD6A /* DefaultVariantManager.swift in Sources */, 9833913727AC400800DAF119 /* AppTrackerDataSetProvider.swift in Sources */, 83004E802193BB8200DA013C /* WKNavigationExtension.swift in Sources */, @@ -8134,6 +8163,7 @@ 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */, 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */, EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */, + 31B2F1112C92FEE000CD30E3 /* MarketplaceAdPostback.swift in Sources */, CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */, BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */, 98629D312C21765A001E6031 /* BookmarksStateValidation.swift in Sources */, @@ -8158,6 +8188,7 @@ 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */, 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */, B652DF10287C2C1600C12A9C /* ContentBlocking.swift in Sources */, + 31B2F1132C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift in Sources */, 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, 85BA79911F6FF75000F59015 /* ContentBlockerStoreConstants.swift in Sources */, 85E242172AB1B54D000F3E28 /* ReturnUserMeasurement.swift in Sources */, @@ -8184,6 +8215,7 @@ 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */, 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */, 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */, + 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */, EE50053029C3BA0800AE0773 /* InternalUserStore.swift in Sources */, F1D477CB1F2149C40031ED49 /* Type.swift in Sources */, 983C52E42C2C050B007B5747 /* BookmarksStateRepair.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate+Attribution.swift b/DuckDuckGo/AppDelegate+Attribution.swift deleted file mode 100644 index bfef272bf4..0000000000 --- a/DuckDuckGo/AppDelegate+Attribution.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AppDelegate+Attribution.swift -// DuckDuckGo -// -// Copyright © 2023 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 Common -import StoreKit -import AdAttributionKit -import os.log - -extension AppDelegate { - - func updateAttribution(conversionValue: Int) { - Task { - if #available(iOS 17.4, *) { - // https://developer.apple.com/documentation/adattributionkit/adattributionkit-skadnetwork-interoperability - await updateAdAttributionKitPostback(conversionValue: conversionValue) - await updateSKANPostback(conversionValue: conversionValue) - } else if #available(iOS 16.1, *) { - await updateSKANPostback(conversionValue: conversionValue) - } - } - } - - @available(iOS 17.4, *) - private func updateAdAttributionKitPostback(conversionValue: Int) async { - do { - try await AdAttributionKit.Postback.updateConversionValue(conversionValue, coarseConversionValue: .high, lockPostback: true) - Logger.general.debug("Attribution: AdAttributionKit postback succeeded") - } catch { - Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") - } - } - - @available(iOS 16.1, *) - private func updateSKANPostback(conversionValue: Int) async { - do { - try await SKAdNetwork.updatePostbackConversionValue(conversionValue, coarseValue: .high, lockWindow: true) - Logger.general.debug("Attribution: SKAN 4 postback succeeded") - } catch let error { - Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") - } - } - -} diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 9bd1828a58..50b89c4358 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -104,6 +104,7 @@ import os.log private let launchOptionsHandler = LaunchOptionsHandler() private let onboardingPixelReporter = OnboardingPixelReporter() + private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() override init() { super.init() @@ -117,9 +118,6 @@ import os.log // swiftlint:disable:next function_body_length func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Attribution support - updateAttribution(conversionValue: 1) - #if targetEnvironment(simulator) if ProcessInfo.processInfo.environment["UITESTING"] == "true" { // Disable hardware keyboards. @@ -523,6 +521,7 @@ import os.log } AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { ContentBlocking.shared.contentBlockingManager.scheduleCompilation() } @@ -757,6 +756,14 @@ import os.log // MARK: private + private func sendAppLaunchPostback() { + // Attribution support + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { + marketplaceAdPostbackManager.sendAppLaunchPostback() + } + } + private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { let historyMessageManager = HistoryMessageManager() @@ -772,6 +779,9 @@ import os.log // New users don't see the message historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() } } @@ -968,7 +978,6 @@ import os.log UIApplication.shared.shortcutItems = nil } } - } extension AppDelegate: BlankSnapshotViewRecoveringDelegate { diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index a19b12c025..6d3941b0ca 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -240,6 +240,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAdvertisingAttributionReportEndpoint + https://duckduckgo.com UIViewControllerBasedStatusBarAppearance diff --git a/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift b/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift new file mode 100644 index 0000000000..332aa04d7b --- /dev/null +++ b/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift @@ -0,0 +1,99 @@ +// +// MarketplaceAdPostbackManagerTests.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 Core +import Foundation + +class MarketplaceAdPostbackManagerTests: XCTestCase { + func testSendAppLaunchPostback_NewUser() { + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: false) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: false) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installNewUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .high) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } + + func testSendAppLaunchPostback_ReturningUser() { + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: true) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: true) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installReturningUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .low) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } + + func testSendAppLaunchPostback_AfterMeasurementChangesState() { + /// Sets return user to true to mock the situation where the user is opening the app again + /// If the storage is set to false, it should still be set as new user + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: true) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: false) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installNewUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .high) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } +} + +private final class MockReturnUserMeasurement: ReturnUserMeasurement { + func installCompletedWithATB(_ atb: Core.Atb) { } + + func updateStoredATB(_ atb: Core.Atb) { } + + var isReturningUser: Bool + + init(isReturningUser: Bool) { + self.isReturningUser = isReturningUser + } +} + +private final class MockMarketplaceAdPostbackUpdater: MarketplaceAdPostbackUpdating { + var postbackSent: MarketplaceAdPostback? + var lockPostbackSent: Bool? + + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { + postbackSent = postback + lockPostbackSent = lockPostback + } +} + +private final class MockMarketplaceAdPostbackStorage: MarketplaceAdPostbackStorage { + var isReturningUser: Bool? + + init(isReturningUser: Bool?) { + self.isReturningUser = isReturningUser + } + + func updateReturningUserValue(_ value: Bool) { + isReturningUser = value + } +} From 98d546fa90d1dfcb68ec103332dad29510014c9d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 13 Sep 2024 16:07:53 -0700 Subject: [PATCH 3/5] Remove VPN feature flag checks (#3334) Task/Issue URL: https://app.asana.com/0/414235014887631/1208254974939835/f Tech Design URL: CC: Description: This PR cleans up VPN feature flag checks after the subscription launch. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- DuckDuckGo/AppDependencyProvider.swift | 15 +---------- .../DefaultNetworkProtectionVisibility.swift | 27 ++----------------- ...orkProtectionConvenienceInitialisers.swift | 18 +------------ ...NetworkProtectionDebugViewController.swift | 3 --- .../NetworkProtectionFeatureVisibility.swift | 7 +---- DuckDuckGo/SettingsViewModel.swift | 12 +++------ ...etworkProtectionPacketTunnelProvider.swift | 1 - 9 files changed, 12 insertions(+), 77 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d751612bbe..632258f36e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10885,7 +10885,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 194.1.0; + version = 195.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c5522d379b..d785772d67 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "09b4901eeab71625c4796c0819d0066278b7b6d6", - "version" : "194.1.0" + "revision" : "f9134f887b1215779a1050134d09d7e824a8abc0", + "version" : "195.0.0" } }, { diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 2577e88ea6..a6c5e6b403 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -125,25 +125,12 @@ class AppDependencyProvider: DependencyProvider { privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, purchasePlatform: .appStore) let accessTokenProvider: () -> String? = { - func isSubscriptionEnabled() -> Bool { -#if ALPHA || DEBUG - if let subscriptionOverrideEnabled = UserDefaults.networkProtectionGroupDefaults.subscriptionOverrideEnabled { - return subscriptionOverrideEnabled - } -#endif - return subscriptionFeatureAvailability.isFeatureAvailable - } - - if isSubscriptionEnabled() { - return { accountManager.accessToken } - } - return { nil } + return { accountManager.accessToken } }() #if os(macOS) networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), serviceName: "\(Bundle.main.bundleIdentifier!).authToken", errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: true, accessTokenProvider: accessTokenProvider) #else networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider) diff --git a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift index 29f988a2b3..baa0cebf53 100644 --- a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift +++ b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift @@ -34,33 +34,10 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } var token: String? { - if shouldMonitorEntitlement() { - return accountManager.accessToken - } - return nil - } - - func isPrivacyProLaunched() -> Bool { - if let subscriptionOverrideEnabled = userDefaults.subscriptionOverrideEnabled { -#if ALPHA || DEBUG - return subscriptionOverrideEnabled -#else - return false -#endif - } - - return AppDependencyProvider.shared.subscriptionFeatureAvailability.isFeatureAvailable - } - - func shouldMonitorEntitlement() -> Bool { - isPrivacyProLaunched() + return accountManager.accessToken } func shouldShowVPNShortcut() -> Bool { - if isPrivacyProLaunched() { - return accountManager.isUserAuthenticated - } else { - return false - } + return accountManager.isUserAuthenticated } } diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index d44f9dbc10..e440446044 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -55,21 +55,6 @@ extension ConnectionServerInfoObserverThroughSession { } } -extension NetworkProtectionCodeRedemptionCoordinator { - - convenience init(isManualCodeRedemptionFlow: Bool = false, accountManager: AccountManager) { - let settings = AppDependencyProvider.shared.vpnSettings - let networkProtectionVisibility = AppDependencyProvider.shared.vpnFeatureVisibility - self.init( - environment: settings.selectedEnvironment, - tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, - isManualCodeRedemptionFlow: isManualCodeRedemptionFlow, - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: networkProtectionVisibility.isPrivacyProLaunched() - ) - } -} - extension NetworkProtectionVPNSettingsViewModel { convenience init() { self.init( @@ -86,8 +71,7 @@ extension NetworkProtectionLocationListCompositeRepository { self.init( environment: settings.selectedEnvironment, tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() + errorEvents: .networkProtectionAppDebugEvents ) } } diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index c48a3e55f3..9af0df4f9f 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -634,9 +634,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { cell.textLabel?.text = """ Endpoint: \(AppDependencyProvider.shared.vpnSettings.selectedEnvironment.endpointURL.absoluteString) -isPrivacyProLaunched: \(vpnVisibility.isPrivacyProLaunched() ? "YES" : "NO") - -shouldMonitorEntitlement: \(vpnVisibility.shouldMonitorEntitlement() ? "YES" : "NO") shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") """ case .none: diff --git a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift index 7bcff5c708..854dcdc9de 100644 --- a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift @@ -21,13 +21,8 @@ import Foundation import Subscription public protocol NetworkProtectionFeatureVisibility { - func isPrivacyProLaunched() -> Bool - - /// Whether to enforce entitlement check and show entitlement-related messaging - /// This should always happen after 100% roll out - /// N.B. Backend will independently check for valid entitlement regardless of this value - func shouldMonitorEntitlement() -> Bool /// Whether to show VPN shortcut on the home screen func shouldShowVPNShortcut() -> Bool + } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index da6ae1abb2..8dbc850832 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -464,14 +464,10 @@ extension SettingsViewModel { } private func updateNetPStatus(connectionStatus: ConnectionStatus) { - if AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() { - switch connectionStatus { - case .connected: - self.state.networkProtectionConnected = true - default: - self.state.networkProtectionConnected = false - } - } else { + switch connectionStatus { + case .connected: + self.state.networkProtectionConnected = true + default: self.state.networkProtectionConnected = false } } diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 71069a06fe..b64d694bb6 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -374,7 +374,6 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: .networkProtectionGroupDefaults, - isSubscriptionEnabled: true, entitlementCheck: { return await Self.entitlementCheck(accountManager: accountManager) }) accountManager.delegate = self From 9ed705c9641d1d16d9477494b15c34c568622d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 16 Sep 2024 12:40:14 +0200 Subject: [PATCH 4/5] New Tab Page post-review fixes (#3221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1208023858138598/f Tech Design URL: CC: **Description**: This PR contains fixes for issues and glitches found for New Tab Page Improvements. **Steps to test this PR**: No specific steps but rather a set of things that were fixed/modified. Overall feature smoke check is required. 1. Missing translations for options in NTP settings. 2. Favorites blinking when NTP becomes visible. 3. Display issues on reordering shortcuts in settings. 4. Animations glitches for expand button and favorites/shortcuts collections. 5. Customize button placement based on the contents. 6. NTP content margins and layout. 7. Spacings and list separators padding in NTP Settings. 8. Add haptics on items reordering. 9. Dark mode color for section settings icon. 10. Color adjustments for shortcuts in edit mode. 11. NTP empty state layout. 12. Grid layout based on orientation. 13. Grid padding / item spacing. 14. A few code-related improvements. For a full list and details see completed subtasks of https://app.asana.com/0/72649045549333/1207539163549343/f. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../24px/Shortcut-24.imageset/Contents.json | 3 +- DuckDuckGo/EditableShortcutsView.swift | 21 +- DuckDuckGo/FaviconsHelper.swift | 65 ++--- DuckDuckGo/Favorite.swift | 3 +- DuckDuckGo/FavoriteIconView.swift | 8 +- DuckDuckGo/FavoritesDefaultModel.swift | 48 +++- DuckDuckGo/FavoritesEmptyStateView.swift | 39 +-- DuckDuckGo/FavoritesFaviconLoader.swift | 45 ++-- DuckDuckGo/FavoritesPreviewModel.swift | 4 + DuckDuckGo/FavoritesSectionHeader.swift | 2 +- DuckDuckGo/FavoritesView.swift | 14 +- DuckDuckGo/MainViewController.swift | 7 +- DuckDuckGo/NewTabPageGridView.swift | 41 ++- DuckDuckGo/NewTabPageIntroMessageView.swift | 2 +- DuckDuckGo/NewTabPageManager.swift | 11 +- DuckDuckGo/NewTabPageSectionsDebugView.swift | 2 +- .../NewTabPageSettingsSectionItemView.swift | 4 +- DuckDuckGo/NewTabPageSettingsView.swift | 19 +- DuckDuckGo/NewTabPageShortcut.swift | 4 + .../NewTabPageShortcutsSettingsStorage.swift | 2 +- DuckDuckGo/NewTabPageView.swift | 253 ++++++++++++------ DuckDuckGo/NewTabPageViewController.swift | 5 +- DuckDuckGo/ReorderableForEach.swift | 29 +- DuckDuckGo/ShortcutAccessoryView.swift | 35 +-- DuckDuckGo/ShortcutItemView.swift | 48 ++-- DuckDuckGo/ShortcutsView.swift | 5 +- ...ew.swift => ToggleExpandButtonStyle.swift} | 39 +-- DuckDuckGo/UserText.swift | 6 +- DuckDuckGo/ViewExtension.swift | 3 +- DuckDuckGo/bg.lproj/Localizable.strings | 27 +- DuckDuckGo/cs.lproj/Localizable.strings | 29 +- DuckDuckGo/da.lproj/Localizable.strings | 29 +- DuckDuckGo/de.lproj/Localizable.strings | 27 +- DuckDuckGo/el.lproj/Localizable.strings | 29 +- DuckDuckGo/en.lproj/Localizable.strings | 11 +- DuckDuckGo/es.lproj/Localizable.strings | 29 +- DuckDuckGo/et.lproj/Localizable.strings | 29 +- DuckDuckGo/fi.lproj/Localizable.strings | 29 +- DuckDuckGo/fr.lproj/Localizable.strings | 29 +- DuckDuckGo/hr.lproj/Localizable.strings | 29 +- DuckDuckGo/hu.lproj/Localizable.strings | 27 +- DuckDuckGo/it.lproj/Localizable.strings | 29 +- DuckDuckGo/lt.lproj/Localizable.strings | 29 +- DuckDuckGo/lv.lproj/Localizable.strings | 29 +- DuckDuckGo/nb.lproj/Localizable.strings | 29 +- DuckDuckGo/nl.lproj/Localizable.strings | 27 +- DuckDuckGo/pl.lproj/Localizable.strings | 29 +- DuckDuckGo/pt.lproj/Localizable.strings | 29 +- DuckDuckGo/ro.lproj/Localizable.strings | 29 +- DuckDuckGo/ru.lproj/Localizable.strings | 29 +- DuckDuckGo/sk.lproj/Localizable.strings | 29 +- DuckDuckGo/sl.lproj/Localizable.strings | 29 +- DuckDuckGo/sv.lproj/Localizable.strings | 29 +- DuckDuckGo/tr.lproj/Localizable.strings | 29 +- .../NewTabPageFavoritesModelTests.swift | 1 + 56 files changed, 809 insertions(+), 666 deletions(-) rename DuckDuckGo/{ToggleExpandButtonView.swift => ToggleExpandButtonStyle.swift} (65%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 632258f36e..23688459f1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -294,7 +294,7 @@ 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 */; }; + 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 */; }; 6F64AA5B2C481AAA00CF4489 /* Shortcuts.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */; }; @@ -1562,7 +1562,7 @@ 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 = ""; }; + 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 = ""; }; 6F64AA5A2C481AAA00CF4489 /* Shortcuts.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Shortcuts.xcassets; sourceTree = ""; }; @@ -3938,7 +3938,7 @@ 6F35379C2C4AAF1C009F8717 /* Settings */, 6FE127472C20941A00EB5724 /* Shortcuts */, 6FE127412C204DE900EB5724 /* Favorites */, - 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonView.swift */, + 6F5CC0802C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift */, 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */, 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */, 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */, @@ -7495,7 +7495,7 @@ 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, - 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonView.swift in Sources */, + 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */, D68A21462B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift in Sources */, 31DE43C22C2C480D00F8C51F /* DuckPlayerFeaturePresentationView.swift in Sources */, F4C9FBF528340DDA002281CC /* AutofillInterfaceEmailTruncator.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json index 74079951c3..0d82d94b3b 100644 --- a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Shortcut-24.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" } } diff --git a/DuckDuckGo/EditableShortcutsView.swift b/DuckDuckGo/EditableShortcutsView.swift index f907a06d5b..1ceca96f54 100644 --- a/DuckDuckGo/EditableShortcutsView.swift +++ b/DuckDuckGo/EditableShortcutsView.swift @@ -23,19 +23,28 @@ import UniformTypeIdentifiers struct EditableShortcutsView: View { @ObservedObject private(set) var model: NewTabPageShortcutsSettingsModel + let geometry: GeometryProxy? + + private let haptics = UIImpactFeedbackGenerator() var body: some View { - NewTabPageGridView { _ in + NewTabPageGridView(geometry: geometry) { _ in ReorderableForEach(model.itemsSettings, id: \.item.id, isReorderingEnabled: true) { setting in let isEnabled = model.enabledItems.contains(setting.item) Button { setting.isEnabled.wrappedValue.toggle() } label: { ShortcutItemView(shortcut: setting.item, accessoryType: isEnabled ? .selected : .add) + .frame(width: NewTabPageGrid.Item.edgeSize) } + .padding([.horizontal, .top], 6) // Adjust for the accessory being cut-off when lifting for preview + .previewShape() } preview: { setting in - ShortcutIconView(shortcut: setting.item).previewShape() + ShortcutIconView(shortcut: setting.item) + .previewShape() + .frame(width: NewTabPageGrid.Item.edgeSize) } onMove: { indices, newOffset in + haptics.impactOccurred() withAnimation { model.moveItems(from: indices, to: newOffset) } @@ -46,7 +55,7 @@ struct EditableShortcutsView: View { private extension View { func previewShape() -> some View { - contentShape(.dragPreview, RoundedRectangle(cornerRadius: 8)) + contentShape([.dragPreview, .contextMenuPreview], RoundedRectangle(cornerRadius: 8)) } } @@ -68,8 +77,10 @@ extension NewTabPageSettingsModel.NTPSetting: Reorderable, H } #Preview { - ScrollView { - EditableShortcutsView(model: NewTabPageShortcutsSettingsModel()) + GeometryReader { proxy in + ScrollView { + EditableShortcutsView(model: NewTabPageShortcutsSettingsModel(), geometry: proxy) + } } .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/FaviconsHelper.swift b/DuckDuckGo/FaviconsHelper.swift index 4f64181e46..051acccb6a 100644 --- a/DuckDuckGo/FaviconsHelper.swift +++ b/DuckDuckGo/FaviconsHelper.swift @@ -24,15 +24,14 @@ import Common struct FaviconsHelper { - private static let tld: TLD = AppDependencyProvider.shared.storageCache.tld + private static let tld: TLD = AppDependencyProvider.shared.storageCache.tld static func loadFaviconSync(forDomain domain: String?, usingCache cacheType: Favicons.CacheType, useFakeFavicon: Bool, - preferredFakeFaviconLetters: String? = nil, - completion: ((UIImage?, Bool) -> Void)? = nil) { - - func complete(_ image: UIImage?) { + preferredFakeFaviconLetters: String? = nil) -> (image: UIImage?, isFake: Bool) { + + func complete(_ image: UIImage?) -> (UIImage?, Bool) { var fake = false var resultImage: UIImage? @@ -44,72 +43,60 @@ struct FaviconsHelper { backgroundColor: UIColor.forDomain(domain), preferredFakeFaviconLetters: preferredFakeFaviconLetters) } - completion?(resultImage, fake) + return (resultImage, fake) } if domain == "player" { - complete(UIImage(named: "DuckPlayer")) - return + return complete(UIImage(named: "DuckPlayer")) } if URL.isDuckDuckGo(domain: domain) { - complete(UIImage(named: "Logo")) - return + return complete(UIImage(named: "Logo")) } guard let cache = Favicons.Constants.caches[cacheType] else { - complete(nil) - return + return complete(nil) } guard let resource = Favicons.shared.defaultResource(forDomain: domain) else { - complete(nil) - return + return complete(nil) } if let image = cache.retrieveImageInMemoryCache(forKey: resource.cacheKey) { - complete(image) + return complete(image) } else { // Load manually otherwise Kingfisher won't load it if the file's modification date > current date let url = cache.diskStorage.cacheFileURL(forKey: resource.cacheKey) guard let data = (try? Data(contentsOf: url)), let image = UIImage(data: data) else { - complete(nil) - return + return complete(nil) } - - complete(image) // Cache in memory with the original expiry date so that the image will be refreshed on user interaction. - guard let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)), - let fileModificationDate = attributes[.modificationDate] as? Date else { - return + if let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)), + let fileModificationDate = attributes[.modificationDate] as? Date { + + cache.store(image, forKey: resource.cacheKey, options: KingfisherParsedOptionsInfo([ + .cacheMemoryOnly, + .diskCacheAccessExtendingExpiration(.none), + .memoryCacheExpiration(.date(fileModificationDate)) + ]), toDisk: false) } - - cache.store(image, forKey: resource.cacheKey, options: KingfisherParsedOptionsInfo([ - .cacheMemoryOnly, - .diskCacheAccessExtendingExpiration(.none), - .memoryCacheExpiration(.date(fileModificationDate)) - ]), toDisk: false) - + + return complete(image) } } - @MainActor static func loadFaviconSync(forDomain domain: String?, usingCache cacheType: Favicons.CacheType, useFakeFavicon: Bool, - preferredFakeFaviconLetters: String? = nil) async -> (image: UIImage?, isFake: Bool) { - await withCheckedContinuation { continuation in - loadFaviconSync(forDomain: domain, - usingCache: cacheType, - useFakeFavicon: useFakeFavicon, - preferredFakeFaviconLetters: preferredFakeFaviconLetters) { image, isFake in - continuation.resume(returning: (image, isFake)) - } - } + preferredFakeFaviconLetters: String? = nil, + completion: ((UIImage?, Bool) -> Void)? = nil) { + let result = loadFaviconSync(forDomain: domain, usingCache: cacheType, useFakeFavicon: useFakeFavicon, preferredFakeFaviconLetters: preferredFakeFaviconLetters) + + completion?(result.image, result.isFake) } static func createFakeFavicon(forDomain domain: String, diff --git a/DuckDuckGo/Favorite.swift b/DuckDuckGo/Favorite.swift index 5b13b1f484..29d5eb70da 100644 --- a/DuckDuckGo/Favorite.swift +++ b/DuckDuckGo/Favorite.swift @@ -38,8 +38,9 @@ struct Favorite: Identifiable, Equatable, Hashable { struct Favicon: Equatable, Hashable { let image: UIImage let isUsingBorder: Bool + let isFake: Bool - static let empty = Self.init(image: UIImage(), isUsingBorder: false) + static let empty = Self.init(image: UIImage(), isUsingBorder: false, isFake: true) } extension Favorite { diff --git a/DuckDuckGo/FavoriteIconView.swift b/DuckDuckGo/FavoriteIconView.swift index 88f4351280..a640fcf95e 100644 --- a/DuckDuckGo/FavoriteIconView.swift +++ b/DuckDuckGo/FavoriteIconView.swift @@ -22,6 +22,8 @@ import SwiftUI protocol FavoritesFaviconLoading { func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon + + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? } struct FavoriteIconView: View { @@ -46,7 +48,7 @@ struct FavoriteIconView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } .task { - if let favicon = await faviconLoading?.loadFavicon(for: favorite, size: Constant.faviconSize) { + if favicon.isFake, let favicon = await faviconLoading?.loadFavicon(for: favorite, size: Constant.faviconSize) { self.favicon = favicon } } @@ -74,7 +76,9 @@ private extension Favorite { extension FavoriteIconView { init(favorite: Favorite, faviconLoading: FavoritesFaviconLoading? = nil) { - let favicon = faviconLoading?.fakeFavicon(for: favorite, size: Constant.faviconSize) ?? .empty + let favicon = faviconLoading?.existingFavicon(for: favorite, size: Constant.faviconSize) + ?? faviconLoading?.fakeFavicon(for: favorite, size: Constant.faviconSize) + ?? .empty self.init(favicon: favicon, favorite: favorite, faviconLoading: faviconLoading) } } diff --git a/DuckDuckGo/FavoritesDefaultModel.swift b/DuckDuckGo/FavoritesDefaultModel.swift index 794bb34d1a..92cf6e3ab3 100644 --- a/DuckDuckGo/FavoritesDefaultModel.swift +++ b/DuckDuckGo/FavoritesDefaultModel.swift @@ -30,15 +30,7 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { @Published private(set) var isCollapsed: Bool = true @Published private(set) var isShowingTooltip: Bool = false - private(set) lazy var faviconLoader: FavoritesFaviconLoading? = { - FavoritesFaviconLoader(onFaviconMissing: { [weak self] in - guard let self else { return } - - await MainActor.run { - self.faviconMissing() - } - }) - }() + private(set) var faviconLoader: FavoritesFaviconLoading? private var cancellables = Set() @@ -51,11 +43,20 @@ final class FavoritesDefaultModel: FavoritesModel, FavoritesEmptyStateModel { } init(interactionModel: FavoritesListInteracting, + faviconLoader: FavoritesFaviconLoading, pixelFiring: PixelFiring.Type = Pixel.self, dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self) { self.interactionModel = interactionModel 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() + } + }) + interactionModel.externalUpdates.sink { [weak self] _ in try? self?.updateData() @@ -203,3 +204,32 @@ private extension BookmarkEntity { } } + +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) + } +} diff --git a/DuckDuckGo/FavoritesEmptyStateView.swift b/DuckDuckGo/FavoritesEmptyStateView.swift index 60d528104c..15db8ab2ca 100644 --- a/DuckDuckGo/FavoritesEmptyStateView.swift +++ b/DuckDuckGo/FavoritesEmptyStateView.swift @@ -25,15 +25,14 @@ struct FavoritesEmptyStateView: View { @ObservedObject var model: Model - @State private var headerPadding: CGFloat = 10 + let geometry: GeometryProxy? var body: some View { ZStack(alignment: .topTrailing) { VStack(spacing: 16) { FavoritesSectionHeader(model: model) - .padding(.horizontal, headerPadding) - NewTabPageGridView { placeholdersCount in + NewTabPageGridView(geometry: geometry) { placeholdersCount in let placeholders = Array(0..: View { model.placeholderTapped() } } - }.overlay( - GeometryReader(content: { geometry in - Color.clear.preference(key: WidthKey.self, value: geometry.frame(in: .local).width) - }) - ) - .onPreferenceChange(WidthKey.self, perform: { fullWidth in - let columnsCount = Double(NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape)) - let allColumnsWidth = columnsCount * NewTabPageGrid.Item.edgeSize - let leftoverWidth = fullWidth - allColumnsWidth - let spacingSize = leftoverWidth / (columnsCount) - self.headerPadding = spacingSize / 2 - }) + } } if model.isShowingTooltip { FavoritesTooltip() - .offset(x: -headerPadding + 18, y: 24) + .offset(x: 18, y: 24) .frame(maxWidth: .infinity, alignment: .bottomTrailing) } } @@ -67,22 +55,5 @@ struct FavoritesEmptyStateView: View { } #Preview { - return FavoritesEmptyStateView(model: FavoritesPreviewModel()) -} - -private struct WidthKey: PreferenceKey { - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } - static var defaultValue: CGFloat = .zero -} - -private final class PreviewEmptyStateModel: FavoritesEmptyStateModel { - @Published var isShowingTooltip: Bool = true - - func toggleTooltip() { - } - - func placeholderTapped() { - } + return FavoritesEmptyStateView(model: FavoritesPreviewModel(), geometry: nil) } diff --git a/DuckDuckGo/FavoritesFaviconLoader.swift b/DuckDuckGo/FavoritesFaviconLoader.swift index 874ff59421..7adc516ce9 100644 --- a/DuckDuckGo/FavoritesFaviconLoader.swift +++ b/DuckDuckGo/FavoritesFaviconLoader.swift @@ -20,41 +20,35 @@ import UIKit actor FavoritesFaviconLoader: FavoritesFaviconLoading { - private var tasks: [URL: Task] = [:] - private(set) var onFaviconMissing: (() async -> Void)? - - init(onFaviconMissing: (() async -> Void)? = nil) { - self.onFaviconMissing = onFaviconMissing - } + + private var tasks: [String: Task] = [:] func loadFavicon(for favorite: Favorite, size: CGFloat) async -> Favicon? { - guard let url = favorite.urlObject else { return nil } + let domain = favorite.domain - if let task = tasks[url] { + if let task = tasks[domain] { if task.isCancelled { - tasks.removeValue(forKey: url) + tasks.removeValue(forKey: domain) } else { return await task.value } } let newTask = Task { - let faviconResult = await FaviconsHelper.loadFaviconSync(forDomain: favorite.domain, usingCache: .fireproof, useFakeFavicon: false) - if let iconImage = faviconResult.image { - let useBorder = URL.isDuckDuckGo(domain: favorite.domain) || iconImage.size.width < size - - return Favicon(image: iconImage, isUsingBorder: useBorder) - } else { - await onFaviconMissing?() - return nil - } + let faviconResult = FaviconsHelper.loadFaviconSync(forDomain: domain, usingCache: .fireproof, useFakeFavicon: false) + return Favicon(domain: domain, expectedSize: size, faviconResult: faviconResult) } - tasks[url] = newTask + tasks[domain] = newTask return await newTask.value } + nonisolated func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + let result = FaviconsHelper.loadFaviconSync(forDomain: favorite.domain, usingCache: .fireproof, useFakeFavicon: false) + return Favicon(domain: favorite.domain, expectedSize: size, faviconResult: result) + } + nonisolated func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { let domain = favorite.domain let color = UIColor.forDomain(domain) @@ -66,9 +60,20 @@ actor FavoritesFaviconLoader: FavoritesFaviconLoading { ) if let icon { - return Favicon(image: icon, isUsingBorder: false) + return Favicon(image: icon, isUsingBorder: false, isFake: true) } else { return .empty } } } + +private extension Favicon { + init?(domain: String, expectedSize: CGFloat, faviconResult: (image: UIImage?, isFake: Bool)) { + guard let iconImage = faviconResult.image else { + return nil + } + + let useBorder = URL.isDuckDuckGo(domain: domain) || iconImage.size.width < expectedSize + self.init(image: iconImage, isUsingBorder: useBorder, isFake: faviconResult.isFake) + } +} diff --git a/DuckDuckGo/FavoritesPreviewModel.swift b/DuckDuckGo/FavoritesPreviewModel.swift index fc17ec9c60..4f2f9eb99a 100644 --- a/DuckDuckGo/FavoritesPreviewModel.swift +++ b/DuckDuckGo/FavoritesPreviewModel.swift @@ -91,6 +91,10 @@ final class FavoritesPreviewModel: FavoritesModel, FavoritesEmptyStateModel { } struct EmptyFaviconLoading: FavoritesFaviconLoading { + func existingFavicon(for favorite: Favorite, size: CGFloat) -> Favicon? { + nil + } + func fakeFavicon(for favorite: Favorite, size: CGFloat) -> Favicon { .empty } diff --git a/DuckDuckGo/FavoritesSectionHeader.swift b/DuckDuckGo/FavoritesSectionHeader.swift index 10a0779460..4a9d744e74 100644 --- a/DuckDuckGo/FavoritesSectionHeader.swift +++ b/DuckDuckGo/FavoritesSectionHeader.swift @@ -27,7 +27,7 @@ struct FavoritesSectionHeader: View { var body: some View { HStack(spacing: 16, content: { - Text("Favorites") + Text(UserText.newTabPageFavoritesSectionHeaderTitle) .font(.system(size: 15, weight: .semibold)) .foregroundColor(Color(designSystemColor: .textPrimary)) .frame(alignment: .leading) diff --git a/DuckDuckGo/FavoritesView.swift b/DuckDuckGo/FavoritesView.swift index bd2f690fe5..d5fcc55d9a 100644 --- a/DuckDuckGo/FavoritesView.swift +++ b/DuckDuckGo/FavoritesView.swift @@ -26,8 +26,10 @@ struct FavoritesView: View { @Environment(\.isLandscapeOrientation) var isLandscape @ObservedObject var model: Model + let geometry: GeometryProxy? private let selectionFeedback = UISelectionFeedbackGenerator() + private let haptics = UIImpactFeedbackGenerator() var body: some View { VStack(alignment: .center, spacing: 24) { @@ -35,7 +37,7 @@ struct FavoritesView: View { let columns = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape) let result = model.prefixedFavorites(for: columns) - NewTabPageGridView { _ in + NewTabPageGridView(geometry: geometry) { _ in ReorderableForEach(result.items) { item in Button(action: { model.favoriteSelected(item) @@ -54,11 +56,14 @@ struct FavoritesView: View { .frame(width: NewTabPageGrid.Item.edgeSize) }) .previewShape() + .transition(.opacity) } preview: { favorite in FavoriteIconView(favorite: favorite, faviconLoading: model.faviconLoader) .frame(width: NewTabPageGrid.Item.edgeSize) .previewShape() + .transition(.opacity) } onMove: { from, to in + haptics.impactOccurred() withAnimation { model.moveFavorites(from: from, to: to) } @@ -75,8 +80,13 @@ struct FavoritesView: View { .resizable() }) .buttonStyle(ToggleExpandButtonStyle()) + // Masks the content, which will otherwise shop up underneath while collapsing + .background(Color(designSystemColor: .background)) } } + // Prevent the content to leak out of bounds while collapsing + .clipped() + .padding(0) } } @@ -97,5 +107,5 @@ extension Favorite: Reorderable { } #Preview { - FavoritesView(model: FavoritesPreviewModel()) + FavoritesView(model: FavoritesPreviewModel(), geometry: nil) } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index edf27a7393..12e5ae1e7b 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -128,7 +128,8 @@ class MainViewController: UIViewController { let privacyProDataReporter: PrivacyProDataReporting private lazy var featureFlagger = AppDependencyProvider.shared.featureFlagger - + private lazy var faviconLoader: FavoritesFaviconLoading = FavoritesFaviconLoader() + lazy var menuBookmarksViewModel: MenuBookmarksInteracting = { let viewModel = MenuBookmarksViewModel(bookmarksDatabase: bookmarksDatabase, syncService: syncService) viewModel.favoritesDisplayMode = appSettings.favoritesDisplayMode @@ -633,6 +634,7 @@ class MainViewController: UIViewController { UIView.animate(withDuration: duration, delay: 0, options: animationCurve) { self.viewCoordinator.navigationBarContainer.superview?.layoutIfNeeded() + self.newTabPageViewController?.additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: max(52, keyboardHeight), right: 0) } } @@ -794,7 +796,8 @@ class MainViewController: UIViewController { privacyProDataReporting: privacyProDataReporter, variantManager: variantManager, newTabDialogFactory: newTabDaxDialogFactory, - newTabDialogTypeProvider: DaxDialogs.shared) + newTabDialogTypeProvider: DaxDialogs.shared, + faviconLoader: faviconLoader) controller.delegate = self controller.shortcutsDelegate = self diff --git a/DuckDuckGo/NewTabPageGridView.swift b/DuckDuckGo/NewTabPageGridView.swift index 9f87036355..f2b75d76b5 100644 --- a/DuckDuckGo/NewTabPageGridView.swift +++ b/DuckDuckGo/NewTabPageGridView.swift @@ -22,21 +22,52 @@ import SwiftUI struct NewTabPageGridView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.isLandscapeOrientation) var isLandscape - + + let geometry: GeometryProxy? @ViewBuilder var content: (_ columnsCount: Int) -> Content + @State private var width: CGFloat = .zero + var body: some View { let columnsCount = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape) - LazyVGrid(columns: flexibleColumns(columnsCount), spacing: 24, content: { + LazyVGrid(columns: flexibleColumns(columnsCount, width: width), spacing: 24, content: { content(columnsCount) }) + .frame(maxWidth: .infinity) + .anchorPreference(key: FramePreferenceKey.self, value: .bounds, transform: { anchor in + guard let geometry else { return FramePreferenceKey.defaultValue } + + return geometry[anchor].width + }) + .onPreferenceChange(FramePreferenceKey.self, perform: { value in + width = value + }) .padding(0) - .offset(.zero) } - private func flexibleColumns(_ count: Int) -> [GridItem] { - Array(repeating: GridItem(.flexible(minimum: NewTabPageGrid.Item.edgeSize), alignment: .top), count: count) + private func flexibleColumns(_ count: Int, width: CGFloat) -> [GridItem] { + let spacing: CGFloat? + if width != .zero { + let columnsWidth = NewTabPageGrid.Item.edgeSize * Double(count) + let spacingsCount = count - 1 + // Calculate exact spacing so that there's no leading and trailing padding. + spacing = max((width - columnsWidth) / Double(spacingsCount), 0) + } else { + spacing = nil + } + + return Array(repeating: GridItem(.flexible(), + spacing: spacing, + alignment: .top), + count: count) + } +} + +private struct FramePreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() } } diff --git a/DuckDuckGo/NewTabPageIntroMessageView.swift b/DuckDuckGo/NewTabPageIntroMessageView.swift index c5682b9f54..f140a9ee72 100644 --- a/DuckDuckGo/NewTabPageIntroMessageView.swift +++ b/DuckDuckGo/NewTabPageIntroMessageView.swift @@ -62,7 +62,7 @@ struct NewTabPageIntroMessageView: View { private enum Metrics { static let padding = 8.0 static let itemSpacing = 6.0 - static let cornerRadius = 12.0 + static let cornerRadius = 8.0 static let titlePadding = 20.0 } } diff --git a/DuckDuckGo/NewTabPageManager.swift b/DuckDuckGo/NewTabPageManager.swift index 9723dd37d3..6a7dfa2baa 100644 --- a/DuckDuckGo/NewTabPageManager.swift +++ b/DuckDuckGo/NewTabPageManager.swift @@ -35,18 +35,23 @@ final class NewTabPageManager: NewTabPageManaging, NewTabPageDebugging { var appDefaults: AppDebugSettings let featureFlagger: FeatureFlagger + let internalUserDecider: InternalUserDecider init(appDefaults: AppDebugSettings = AppDependencyProvider.shared.appSettings, - featureFlager: FeatureFlagger = AppDependencyProvider.shared.featureFlagger) { - + featureFlager: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + internalUserDecider: InternalUserDecider = AppDependencyProvider.shared.internalUserDecider) { + self.appDefaults = appDefaults self.featureFlagger = featureFlager + self.internalUserDecider = internalUserDecider } // MARK: - HomeTabManaging var isNewTabPageSectionsEnabled: Bool { - isLocalFlagEnabled && isFeatureFlagEnabled + let isLocalFlagInEffect = isLocalFlagEnabled && internalUserDecider.isInternalUser + + return isLocalFlagInEffect || isFeatureFlagEnabled } var isAvailableInPublicRelease: Bool { diff --git a/DuckDuckGo/NewTabPageSectionsDebugView.swift b/DuckDuckGo/NewTabPageSectionsDebugView.swift index fc0f5e634a..1f3a05e198 100644 --- a/DuckDuckGo/NewTabPageSectionsDebugView.swift +++ b/DuckDuckGo/NewTabPageSectionsDebugView.swift @@ -94,7 +94,7 @@ struct NewTabPageSectionsDebugView: View { HStack { VStack { - Text(verbatim: "Feature flag enabled") + Text(verbatim: "Remote feature flag enabled") } Spacer() if newTabPageDebugging.isFeatureFlagEnabled { diff --git a/DuckDuckGo/NewTabPageSettingsSectionItemView.swift b/DuckDuckGo/NewTabPageSettingsSectionItemView.swift index 1ff000b79f..793bb18a11 100644 --- a/DuckDuckGo/NewTabPageSettingsSectionItemView.swift +++ b/DuckDuckGo/NewTabPageSettingsSectionItemView.swift @@ -47,9 +47,9 @@ private extension View { @ViewBuilder func applyListRowInsets() -> some View { if #available(iOS 16, *) { - self + listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 8)) } else { - listRowInsets(EdgeInsets(top: 0, leading: -24, bottom: 0, trailing: 8)) + listRowInsets(EdgeInsets(top: 0, leading: -24, bottom: 0, trailing: 16)) } } } diff --git a/DuckDuckGo/NewTabPageSettingsView.swift b/DuckDuckGo/NewTabPageSettingsView.swift index cf3b3cec2e..c803582193 100644 --- a/DuckDuckGo/NewTabPageSettingsView.swift +++ b/DuckDuckGo/NewTabPageSettingsView.swift @@ -51,14 +51,13 @@ struct NewTabPageSettingsView: View { if sectionsSettingsModel.enabledItems.contains(.shortcuts) { GeometryReader { geometry in ScrollView { - VStack { + VStack(spacing: 0) { sectionsList(withFrameUpdates: true, geometry: geometry) .withoutScroll() .frame(height: listHeight) - EditableShortcutsView(model: shortcutsSettingsModel) + EditableShortcutsView(model: shortcutsSettingsModel, geometry: geometry) .padding(.horizontal, Metrics.horizontalPadding) - .coordinateSpace(name: Constant.scrollCoordinateSpaceName) } } } @@ -105,11 +104,11 @@ struct NewTabPageSettingsView: View { ForEach(sectionsSettingsModel.itemsSettings, id: \.item) { setting in switch setting.item { case .favorites: - NewTabPageSettingsSectionItemView(title: "Favorites", + NewTabPageSettingsSectionItemView(title: UserText.newTabPageSettingsSectionNameFavorites, iconResource: .favorite24, isEnabled: setting.isEnabled) case .shortcuts: - NewTabPageSettingsSectionItemView(title: "Shortcuts", + NewTabPageSettingsSectionItemView(title: UserText.newTabPageSettingsSectionNameShortcuts, iconResource: .shortcut24, isEnabled: setting.isEnabled) } @@ -119,16 +118,8 @@ struct NewTabPageSettingsView: View { } } -private struct Constant { - static let scrollCoordinateSpaceName = "Scroll" -} - -private extension CoordinateSpace { - static let scroll = CoordinateSpace.named(Constant.scrollCoordinateSpaceName) -} - private struct Metrics { - static let horizontalPadding = 16.0 + static let horizontalPadding = 24.0 static let initialListHeight = 5000.0 } diff --git a/DuckDuckGo/NewTabPageShortcut.swift b/DuckDuckGo/NewTabPageShortcut.swift index 2f837bec63..16fea66723 100644 --- a/DuckDuckGo/NewTabPageShortcut.swift +++ b/DuckDuckGo/NewTabPageShortcut.swift @@ -23,6 +23,10 @@ enum NewTabPageShortcut: CaseIterable, Equatable, Identifiable, Codable { var id: String { storageIdentifier } case bookmarks, aiChat, passwords, downloads, settings + + static var enabledByDefault: [NewTabPageShortcut] { + NewTabPageShortcut.allCases.filter { $0 != .aiChat } + } } extension NewTabPageShortcut { diff --git a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift index c769e31b44..7560398968 100644 --- a/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift +++ b/DuckDuckGo/NewTabPageShortcutsSettingsStorage.swift @@ -25,6 +25,6 @@ extension NewTabPageSettingsPersistentStorage { convenience init() { self.init(keyPath: \.newTabPageShortcutsSettings, defaultOrder: NewTabPageShortcut.allCases, - defaultEnabledItems: NewTabPageShortcut.allCases) + defaultEnabledItems: NewTabPageShortcut.enabledByDefault) } } diff --git a/DuckDuckGo/NewTabPageView.swift b/DuckDuckGo/NewTabPageView.swift index ccf007ff5c..ecc09bbba7 100644 --- a/DuckDuckGo/NewTabPageView.swift +++ b/DuckDuckGo/NewTabPageView.swift @@ -31,6 +31,8 @@ struct NewTabPageView= currentStackHeight + buttonVSpaceRequired + + // If there's no room, show the button inside the stack view, after sections + return !buttonHasRoomInViewport + }) + .onPreferenceChange(CustomizeButtonPrefKey.self, perform: { value in + customizeButtonShowedInline = isAnySectionEnabled ? value : false + }) + } + .withScrollKeyboardDismiss() + .safeAreaInset(edge: .bottom, alignment: .trailing) { + if !customizeButtonShowedInline { + customizeButtonView + .frame(maxWidth: .infinity) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .padding([.trailing, .bottom], Metrics.largePadding) + } + } + } + } + + @ViewBuilder + private var emptyStateView: some View { + ZStack { + NewTabPageDaxLogoView() + + VStack(spacing: Metrics.sectionSpacing) { + introMessageView + .padding(.top, Metrics.nonGridSectionTopPadding) + + messagesSectionView + .padding(.top, Metrics.nonGridSectionTopPadding) + .frame(maxHeight: .infinity, alignment: .top) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .safeAreaInset(edge: .bottom, alignment: .trailing) { + customizeButtonView + .frame(maxWidth: .infinity) + } + } + .padding(Metrics.largePadding) + } + private var messagesSectionView: some View { ForEach(messagesModel.homeMessageViewModels, id: \.messageId) { messageModel in HomeMessageView(viewModel: messageModel) - .frame(maxWidth: horizontalSizeClass == .regular ? Constant.messageMaximumWidthPad : Constant.messageMaximumWidth) - .padding(16) + .frame(maxWidth: horizontalSizeClass == .regular ? Metrics.messageMaximumWidthPad : Metrics.messageMaximumWidth) + .transition(.scale.combined(with: .opacity)) } } - private var favoritesSectionView: some View { + private func favoritesSectionView(proxy: GeometryProxy) -> some View { Group { if favoritesModel.isEmpty { - FavoritesEmptyStateView(model: favoritesModel) + FavoritesEmptyStateView(model: favoritesModel, geometry: proxy) + .padding(.top, Metrics.nonGridSectionTopPadding) } else { - FavoritesView(model: favoritesModel) + FavoritesView(model: favoritesModel, geometry: proxy) } } - .sectionPadding() } @ViewBuilder - private var shortcutsSectionView: some View { + private func shortcutsSectionView(proxy: GeometryProxy) -> some View { if isShortcutsSectionVisible { - ShortcutsView(model: shortcutsModel, shortcuts: shortcutsSettingsModel.enabledItems) - .sectionPadding() + ShortcutsView(model: shortcutsModel, shortcuts: shortcutsSettingsModel.enabledItems, proxy: proxy) + .transition(.scale.combined(with: .opacity)) } } @@ -85,8 +206,7 @@ struct NewTabPageView some View { - self.padding(Constant.sectionPadding) + @ViewBuilder + func withScrollKeyboardDismiss() -> some View { + if #available(iOS 16, *) { + scrollDismissesKeyboard(.immediately) + } else { + self } } +} -private struct Constant { - static let sectionPadding = EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24) +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 customizeButtonHeight: CGFloat = 40 static let messageMaximumWidth: CGFloat = 380 static let messageMaximumWidthPad: CGFloat = 455 } +private struct CustomizeButtonPrefKey: PreferenceKey { + static var defaultValue: Bool = true + + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() + } +} + // MARK: - Preview #Preview("Regular") { @@ -222,6 +298,21 @@ private struct Constant { ) } +#Preview("Empty state") { + NewTabPageView( + newTabPageModel: NewTabPageModel(), + messagesModel: NewTabPageMessagesModel( + homePageMessagesConfiguration: PreviewMessagesConfiguration( + homeMessages: [] + ) + ), + favoritesModel: FavoritesPreviewModel(), + shortcutsModel: ShortcutsModel(), + shortcutsSettingsModel: NewTabPageShortcutsSettingsModel(), + sectionsSettingsModel: NewTabPageSectionsSettingsModel(storage: .emptyStorage()) + ) +} + private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration { private(set) var homeMessages: [HomeMessage] @@ -241,3 +332,9 @@ private final class PreviewMessagesConfiguration: HomePageMessagesConfiguration homeMessages = homeMessages.dropLast() } } + +private extension NewTabPageSectionsSettingsStorage { + static func emptyStorage() -> Self { + Self.init(keyPath: \.newTabPageSectionsSettings, defaultOrder: [], defaultEnabledItems: []) + } +} diff --git a/DuckDuckGo/NewTabPageViewController.swift b/DuckDuckGo/NewTabPageViewController.swift index bb11fcabfc..7e154ff281 100644 --- a/DuckDuckGo/NewTabPageViewController.swift +++ b/DuckDuckGo/NewTabPageViewController.swift @@ -51,7 +51,8 @@ final class NewTabPageViewController: UIHostingController: View { + typealias ContentBuilder = (Data) -> Content + typealias PreviewBuilder = (Data) -> Preview + private let data: [Data] private let isReorderingEnabled: Bool private let id: KeyPath - private let content: (Data) -> Content - private let preview: ((Data) -> Preview)? + private let content: ContentBuilder + private let preview: PreviewBuilder? private let onMove: (_ from: IndexSet, _ to: Int) -> Void @State private var movedItem: Data? - @State private var isItemLocationChanged: Bool = false init(_ data: [Data], id: KeyPath, isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder content: @escaping ContentBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { self.data = data self.id = id @@ -54,7 +56,7 @@ struct ReorderableForEach, isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder content: @escaping ContentBuilder, @ViewBuilder preview: @escaping (Data) -> Preview, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { self.data = data @@ -95,19 +97,17 @@ struct ReorderableForEach: DropDelegate { +private struct ReorderDropDelegate: DropDelegate { let data: [Data] let item: Data let onMove: (_ from: IndexSet, _ to: Int) -> Void @Binding var movedItem: Data? - @Binding var isItemLocationChanged: Bool func dropEntered(info: DropInfo) { guard item != movedItem, @@ -116,8 +116,6 @@ private struct ReorderDropDelegate: DropDelegate { let to = data.firstIndex(of: item) else { return } - isItemLocationChanged = true - if data[to] != current { let fromIndices = IndexSet(integer: from) let toIndex = to > from ? to + 1 : to @@ -130,16 +128,15 @@ private struct ReorderDropDelegate: DropDelegate { } func performDrop(info: DropInfo) -> Bool { - isItemLocationChanged = false movedItem = nil - return true + return info.hasItemsConforming(to: [item.dropType]) } } extension ReorderableForEach where Data: Identifiable, ID == Data.ID { init(_ data: [Data], isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, + @ViewBuilder content: @escaping ContentBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) where Preview == EmptyView { self.data = data self.id = \Data.id @@ -151,8 +148,8 @@ extension ReorderableForEach where Data: Identifiable, ID == Data.ID { init(_ data: [Data], isReorderingEnabled: Bool = true, - @ViewBuilder content: @escaping (Data) -> Content, - @ViewBuilder preview: @escaping (Data) -> Preview, + @ViewBuilder content: @escaping ContentBuilder, + @ViewBuilder preview: @escaping PreviewBuilder, onMove: @escaping (_ from: IndexSet, _ to: Int) -> Void) { self.data = data self.id = \Data.id diff --git a/DuckDuckGo/ShortcutAccessoryView.swift b/DuckDuckGo/ShortcutAccessoryView.swift index a6756901ec..3a269ba930 100644 --- a/DuckDuckGo/ShortcutAccessoryView.swift +++ b/DuckDuckGo/ShortcutAccessoryView.swift @@ -18,28 +18,40 @@ // import SwiftUI +import DuckUI struct ShortcutAccessoryView: View { + @Environment(\.colorScheme) private var colorScheme + let accessoryType: ShortcutAccessoryType - let expectedSize: CGSize var body: some View { Circle() - .foregroundStyle(accessoryType.backgroundColor) + .foregroundStyle(bgColorForAccessoryType(accessoryType)) .overlay { Image(accessoryType.iconResource) + .resizable() .foregroundColor(accessoryType.foregroundColor) .aspectRatio(contentMode: .fit) + .scaleEffect(x: Constant.imageScaleRatio, y: Constant.imageScaleRatio) } .shadow(color: .shade(0.15), radius: 1, y: 1) - .frame(width: expectedSize.width, height: expectedSize.height) } -} -extension ShortcutAccessoryView { - init(accessoryType: ShortcutAccessoryType) { - self.init(accessoryType: accessoryType, expectedSize: CGSize(width: 24, height: 24)) + func bgColorForAccessoryType(_ accessoryType: ShortcutAccessoryType) -> Color { + switch accessoryType { + case .selected: + return Color(designSystemColor: .accent) + case .add: + // One-off exception for this particular case. + // See https://app.asana.com/0/72649045549333/1207988345460434/f + return colorScheme == .dark ? .gray85 : Color(designSystemColor: .surface) + } + } + + private enum Constant { + static let imageScaleRatio: CGFloat = 2.0/3.0 } } @@ -58,15 +70,6 @@ private extension ShortcutAccessoryType { } } - var backgroundColor: Color { - switch self { - case .selected: - Color(designSystemColor: .accent) - case .add: - Color(designSystemColor: .surface) - } - } - var foregroundColor: Color { switch self { case .selected: diff --git a/DuckDuckGo/ShortcutItemView.swift b/DuckDuckGo/ShortcutItemView.swift index deb085d0b0..ad551be8fd 100644 --- a/DuckDuckGo/ShortcutItemView.swift +++ b/DuckDuckGo/ShortcutItemView.swift @@ -29,8 +29,13 @@ struct ShortcutItemView: View { ShortcutIconView(shortcut: shortcut) .overlay(alignment: .topTrailing) { if let accessoryType { - ShortcutAccessoryView(accessoryType: accessoryType) - .alignedForOverlay(edgeSize: Constant.accessorySize) + Group { + let offset = Constant.accessorySize/4.0 + ShortcutAccessoryView(accessoryType: accessoryType) + .frame(width: Constant.accessorySize, height: Constant.accessorySize) + .offset(x: offset, y: -offset) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) } } @@ -52,17 +57,16 @@ struct ShortcutIconView: View { let shortcut: NewTabPageShortcut var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(Color(designSystemColor: .surface)) - .shadow(color: .shade(0.12), radius: 0.5, y: 1) - .aspectRatio(1, contentMode: .fit) - .frame(width: NewTabPageGrid.Item.edgeSize) - Image(shortcut.imageResource) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: NewTabPageGrid.Item.edgeSize * 0.5) - } + RoundedRectangle(cornerRadius: 8) + .fill(Color(designSystemColor: .surface)) + .shadow(color: .shade(0.12), radius: 0.5, y: 1) + .aspectRatio(1, contentMode: .fit) + .overlay { + Image(shortcut.imageResource) + .resizable() + .aspectRatio(contentMode: .fit) + .scaleEffect(x: 0.5, y: 0.5) + } } } @@ -98,30 +102,16 @@ private extension NewTabPageShortcut { } } -private extension ShortcutAccessoryView { - @ViewBuilder func alignedForOverlay(edgeSize: CGFloat) -> some View { - let offset = CGSize(width: edgeSize/4.0, height: -edgeSize/4.0) -// let size = CGSize(width: edgeSize, height: edgeSize) - - if #available(iOS 16, *) { - frame(width: edgeSize) - .offset(offset) - } else { - frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .offset(offset) - } - } -} - #Preview { ScrollView { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 86))], content: { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 68), spacing: 8, alignment: .top)], content: { let accessoryTypes: [ShortcutAccessoryType?] = [.none, .add, .selected] ForEach(accessoryTypes, id: \.?.hashValue) { type in Section { ForEach(NewTabPageShortcut.allCases) { shortcut in ShortcutItemView(shortcut: shortcut, accessoryType: type) + .frame(width: 64) } } footer: { diff --git a/DuckDuckGo/ShortcutsView.swift b/DuckDuckGo/ShortcutsView.swift index 9ff0c6a2b2..230d2a5393 100644 --- a/DuckDuckGo/ShortcutsView.swift +++ b/DuckDuckGo/ShortcutsView.swift @@ -23,9 +23,10 @@ import UniformTypeIdentifiers struct ShortcutsView: View { private(set) var model: ShortcutsModel let shortcuts: [NewTabPageShortcut] + let proxy: GeometryProxy? var body: some View { - NewTabPageGridView { _ in + NewTabPageGridView(geometry: proxy) { _ in ForEach(shortcuts) { shortcut in Button { model.openShortcut(shortcut) @@ -39,7 +40,7 @@ struct ShortcutsView: View { #Preview { ScrollView { - ShortcutsView(model: ShortcutsModel(), shortcuts: NewTabPageShortcut.allCases) + ShortcutsView(model: ShortcutsModel(), shortcuts: NewTabPageShortcut.allCases, proxy: nil) } .background(Color(designSystemColor: .background)) } diff --git a/DuckDuckGo/ToggleExpandButtonView.swift b/DuckDuckGo/ToggleExpandButtonStyle.swift similarity index 65% rename from DuckDuckGo/ToggleExpandButtonView.swift rename to DuckDuckGo/ToggleExpandButtonStyle.swift index eb4a9ac695..b36c9d2d33 100644 --- a/DuckDuckGo/ToggleExpandButtonView.swift +++ b/DuckDuckGo/ToggleExpandButtonStyle.swift @@ -1,5 +1,5 @@ // -// ToggleExpandButtonView.swift +// ToggleExpandButtonStyle.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -30,26 +30,31 @@ struct ToggleExpandButtonStyle: ButtonStyle { VStack { ExpandButtonDivider() } - ZStack { - Circle() - .stroke(Color(designSystemColor: .lines), lineWidth: 1) - .frame(width: 32) - .if(configuration.isPressed, transform: { - $0.background(Circle() - .fill(isDark ? Color.tint(0.12) : Color.shade(0.06))) - }) - .background( - Circle() - .fill(Color(designSystemColor: .background)) - ) - configuration.label - .foregroundColor(isDark ? .tint(0.6) : .shade(0.6)) - .frame(width: 16, height: 16) - } + + Circle() + .stroke(Color(designSystemColor: .lines), lineWidth: 1) + .frame(width: 32, height: 32) + .if(configuration.isPressed) { + $0.background(isDark ? Color.tint(0.12) : Color.shade(0.06)) + .clipShape(Circle()) + } + .background( + Circle() + .fill(Color(designSystemColor: .background)) + ) + .overlay { + configuration.label + .foregroundColor(isDark ? .tint(0.6) : .shade(0.6)) + .frame(width: 16, height: 16) + + } + VStack { ExpandButtonDivider() } } + .padding(.vertical, 0.5) // Adjust padding for drawing group, otherwise the circle stroke is clipped + .drawingGroup() } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 8410fd0612..0f6a604a29 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1289,6 +1289,8 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: - New Tab Page + public static let newTabPageFavoritesSectionHeaderTitle = NSLocalizedString("new.tab.page.favorites.setion.header.title", value: "Favorites", comment: "Header title of Favorites section") + // MARK: Shortcuts public static let newTabPageShortcutBookmarks = NSLocalizedString("new.tab.page.shortcut.bookmarks", value: "Bookmarks", comment: "Shortcut title leading to Bookmarks") public static let newTabPageShortcutAIChat = NSLocalizedString("new.tab.page.shortcut.ai.chat", value: "AI Chat", comment: "Shortcut title leading to AI Chat") @@ -1302,10 +1304,12 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: Settings - public static let newTabPageSettingsTitle = NSLocalizedString("new.tab.page.settings.title", value: "Customize New Tab", comment: "Title of New Tab Page preferences page.") + public static let newTabPageSettingsTitle = NSLocalizedString("new.tab.page.settings.title", value: "Customize New Tab Page", comment: "Title of New Tab Page preferences page.") public static let newTabPageSettingsSectionsHeaderTitle = NSLocalizedString("new.tab.page.settings.sections.header.title", value: "SECTIONS", comment: "Header title of the group allowing for setting up new tab page sections") public static let newTabPageSettingsSectionsDescription = NSLocalizedString("new.tab.page.settings.sections.description", value: "Show, hide, and reorder sections on the new tab page", comment: "Footer of the group allowing for setting up new tab page sections") public static let newTabPageSettingsShortcutsHeaderTitle = NSLocalizedString("new.tab.page.settings.shortcuts.header.title", value: "SHORTCUTS", comment: "Header title of the shortcuts in New Tab Page preferences.") + public static let newTabPageSettingsSectionNameFavorites = NSLocalizedString("new.tab.page.settings.section.name.favorites", value: "Favorites", comment: "Name of favorites section setting") + public static let newTabPageSettingsSectionNameShortcuts = NSLocalizedString("new.tab.page.settings.section.name.shortcuts", value: "Shortcuts", comment: "Name of shortcuts section setting") // MARK: Intro message diff --git a/DuckDuckGo/ViewExtension.swift b/DuckDuckGo/ViewExtension.swift index eefb1e6b66..ca1afc56cc 100644 --- a/DuckDuckGo/ViewExtension.swift +++ b/DuckDuckGo/ViewExtension.swift @@ -35,7 +35,8 @@ extension View { } extension View { - /// Disables scroll if allowed by system version + /// Disables scroll if available for current system version + @available(iOS, deprecated: 16.0, renamed: "scrollDisabled") @ViewBuilder func withoutScroll(_ isScrollDisabled: Bool = true) -> some View { if #available(iOS 16, *) { diff --git a/DuckDuckGo/bg.lproj/Localizable.strings b/DuckDuckGo/bg.lproj/Localizable.strings index bbea086b15..35bb6aa4a9 100644 --- a/DuckDuckGo/bg.lproj/Localizable.strings +++ b/DuckDuckGo/bg.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Премахване"; -/* No comment provided by engineer. */ -"Favorites" = "Любими"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Всички любими, запазени на устройството"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Само любими на мобилни устройства"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Блокиране на реклами и изскачащи прозорци"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Добре дошли в\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Настройки на VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Любими"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Персонализирайте раздела Любими и функциите за достъп. Пренаредете елементите или скрийте някои от тях за по голяма яснота."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Показване, скриване и пренареждане на секции на страницата с нов раздел"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Страницата с нов раздел е... Нова!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Любими"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Бързи Връзки"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Показване, скриване и пренареждане на секции на страницата с нов раздел"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "СЕКЦИИ"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Отваряне в друго приложение?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Търсене или въвеждане на адрес"; diff --git a/DuckDuckGo/cs.lproj/Localizable.strings b/DuckDuckGo/cs.lproj/Localizable.strings index df08ed3417..72f786bc8d 100644 --- a/DuckDuckGo/cs.lproj/Localizable.strings +++ b/DuckDuckGo/cs.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Odstranit"; -/* No comment provided by engineer. */ -"Favorites" = "Oblíbené"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Všechny oblíbené položky na zařízení"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Jenom oblíbené mobilní položky"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokování reklam a vyskakovacích oken"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Vítejte na\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Nastavení sítě VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Oblíbené"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Přizpůsob si oblíbené záložky a časté funkce. Přeskládej je nebo je skryj, ať je všechno přehledné."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Zobrazení, skrytí a změna pořadí oddílů na stránce nové karty"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Stránka nové karty je... úplně nová!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Oblíbené"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Zkratky"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Zobrazení, skrytí a změna pořadí oddílů na stránce nové karty"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ODDÍLY"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "ZKRATKY"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Přizpůsobení nové karty"; +"new.tab.page.settings.title" = "Přizpůsobení karty s novou stránkou"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otevřít v jiné aplikaci?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Vyhledejte nebo zadejte adresu"; diff --git a/DuckDuckGo/da.lproj/Localizable.strings b/DuckDuckGo/da.lproj/Localizable.strings index 89a6c47576..cf77933d37 100644 --- a/DuckDuckGo/da.lproj/Localizable.strings +++ b/DuckDuckGo/da.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Fjern"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritter"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle favoritter på enheden"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Kun favoritter på mobil"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokering af annoncer og pop op-vinduer"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Velkommen til\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-indstillinger"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritter"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Tilpas dine favoritter og go-to-funktioner. Flyt rundt på ting, eller skjul dem for at holde det enkelt."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Vis, skjul og omarranger sektioner på den nye faneside"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Din nye faneside er... Ny!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritter"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Genveje"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Vis, skjul og omarranger sektioner på den nye faneside"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEKTIONER"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "GENVEJE"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Tilpas ny fane"; +"new.tab.page.settings.title" = "Tilpas siden Ny fane"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI-chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Åbn i en anden app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Søg eller indtast adresse"; diff --git a/DuckDuckGo/de.lproj/Localizable.strings b/DuckDuckGo/de.lproj/Localizable.strings index 08eaf46ebc..6459a2bbce 100644 --- a/DuckDuckGo/de.lproj/Localizable.strings +++ b/DuckDuckGo/de.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Entfernen"; -/* No comment provided by engineer. */ -"Favorites" = "Favoriten"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle Gerätefavoriten"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Nur mobile Favoriten"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blockieren von Werbungen und Pop-ups"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Willkommen bei\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-Einstellungen"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoriten"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Passe deine Favoriten und häufig genutzten Funktionen an. Ordne die Dinge neu an oder blende sie aus, um die Übersichtlichkeit zu wahren."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Abschnitte auf der neuen Tab-Seite anzeigen, ausblenden und neu anordnen"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Deine „Neuer Tab“-Seite ist ... neu!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoriten"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Shortcuts"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Abschnitte auf der neuen Tab-Seite anzeigen, ausblenden und neu anordnen"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ABSCHNITTE"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "In einer anderen App öffnen?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Adresse suchen oder eingeben"; diff --git a/DuckDuckGo/el.lproj/Localizable.strings b/DuckDuckGo/el.lproj/Localizable.strings index aa144e9126..4d4724f848 100644 --- a/DuckDuckGo/el.lproj/Localizable.strings +++ b/DuckDuckGo/el.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Αφαίρεση"; -/* No comment provided by engineer. */ -"Favorites" = "Αγαπημένα"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Αγαπημένα όλων των συσκευών"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Αγαπημένα μόνο για κινητά"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Αποκλεισμός διαφημίσεων και αναδυόμενων παραθύρων"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Καλώς ορίσατε στο\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Ρυθμίσεις VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Αγαπημένα"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Προσαρμόστε τα Αγαπημένα και τις λειτουργίες μετάβασης. Αναδιατάξτε στοιχεία ή αποκρύψτε τα για να τη διατηρείτε καθαρή."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Εμφάνιση, απόκρυψη και αναδιάταξη ενοτήτων στη σελίδα της νέας καρτέλας"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Η σελίδα σας για τη Νέα καρτέλα είναι... Νέα!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Αγαπημένα"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Συντομευσεις"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Εμφάνιση, απόκρυψη και αναδιάταξη ενοτήτων στη σελίδα της νέας καρτέλας"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ΕΝΟΤΗΤΕΣ"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "ΣΥΝΤΟΜΕΥΣΕΙΣ"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Προσαρμογή νέας καρτέλας"; +"new.tab.page.settings.title" = "Προσαρμογή σελίδας νέας καρτέλας"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Συνομιλία με ΤΝ"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Άνοιγμα σε άλλη εφαρμογή;"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Αναζήτηση ή εισαγωγή διεύθυνσης"; diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index ec91ba69c6..1bdd754039 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1775,12 +1775,21 @@ https://duckduckgo.com/mac"; /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Settings"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favorites"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Customize your Favorites and go-to features. Reorder things or hide them to keep it clean."; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Your New Tab Page is... New!"; +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favorites"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Shortcuts"; + /* Footer of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.description" = "Show, hide, and reorder sections on the new tab page"; @@ -1791,7 +1800,7 @@ https://duckduckgo.com/mac"; "new.tab.page.settings.shortcuts.header.title" = "SHORTCUTS"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Customize New Tab"; +"new.tab.page.settings.title" = "Customize New Tab Page"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat"; diff --git a/DuckDuckGo/es.lproj/Localizable.strings b/DuckDuckGo/es.lproj/Localizable.strings index 302fa4a7b2..0751644415 100644 --- a/DuckDuckGo/es.lproj/Localizable.strings +++ b/DuckDuckGo/es.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Eliminar"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritos"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Todos los favoritos del dispositivo"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Solo favoritos móviles"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Bloquear anuncios y mensajes emergentes"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "¡Bienvenido a\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Configuración de VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritos"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personaliza tus favoritos y funciones de acceso. Reordena los elementos o escóndelos para mantener el orden."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Mostrar, ocultar y reordenar las secciones de la página nueva pestaña"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Tu nueva pestaña es... ¡Novedad!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritos"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Accesos directos"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Mostrar, ocultar y reordenar las secciones de la página nueva pestaña"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECCIONES"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Accesos directos"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personaliza la nueva pestaña"; +"new.tab.page.settings.title" = "Personalizar página Nueva pestaña"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat de IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "¿Abrir en otra aplicación?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Buscar o introducir dirección"; diff --git a/DuckDuckGo/et.lproj/Localizable.strings b/DuckDuckGo/et.lproj/Localizable.strings index 653a267f8c..8a72451e11 100644 --- a/DuckDuckGo/et.lproj/Localizable.strings +++ b/DuckDuckGo/et.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Eemaldage"; -/* No comment provided by engineer. */ -"Favorites" = "Lemmikud"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Kõik seadme lemmikud"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Ainult mobiililemmikud"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklaami ja hüpikakende blokeerimine"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Tere tulemast\nDuckDuckGo kasutajaks!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-i seaded"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Lemmikud"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Kohanda oma lemmikuid ja enim kasutatavaid funktsioone. Järjesta valikud ümber või peida need, et kõik oleks lihtsasti leitav."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Jaotiste kuvamine, peitmine ja järjestamine uuel vahelehel"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Sinu uue vahelehe leht on... Uus!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Lemmikud"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Otseteed"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Jaotiste kuvamine, peitmine ja järjestamine uuel vahelehel"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "JAOTISED"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "OTSETEED"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Uue vahekaardi kohandamine"; +"new.tab.page.settings.title" = "Uue vahekaardilehe kohandamine"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI vestlus"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Avada teises rakenduses?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Otsi või sisesta aadress"; diff --git a/DuckDuckGo/fi.lproj/Localizable.strings b/DuckDuckGo/fi.lproj/Localizable.strings index 20845d22c2..85ee101acb 100644 --- a/DuckDuckGo/fi.lproj/Localizable.strings +++ b/DuckDuckGo/fi.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Poista"; -/* No comment provided by engineer. */ -"Favorites" = "Suosikit"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Kaikki laitteen suosikit"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Vain mobiilisuosikit"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Mainosten ja ponnahdusikkunoiden esto"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Tervetuloa\nDuckDuckGo-sovellukseen!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-asetukset"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Suosikit"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Mukauta suosikkejasi ja muita usein käyttämiäsi ominaisuuksia. Luo uusi järjestys tai piilota kohteita pitääksesi selkeän ilmeen."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Näytä, piilota ja järjestä uuden välilehden osiot uudelleen"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Uusi välilehti -sivusi on... uusi!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Suosikit"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Pikavalinnat"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Näytä, piilota ja järjestä uuden välilehden osiot uudelleen"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "OSIOT"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "PIKAVALINNAT"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Mukauta uusi välilehti"; +"new.tab.page.settings.title" = "Mukauta uuden välilehden sivua"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Tekoäly-chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Avataanko toisessa sovelluksessa?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Hae tai anna osoite"; diff --git a/DuckDuckGo/fr.lproj/Localizable.strings b/DuckDuckGo/fr.lproj/Localizable.strings index 9e355d73ce..6c9382eb6a 100644 --- a/DuckDuckGo/fr.lproj/Localizable.strings +++ b/DuckDuckGo/fr.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Supprimer"; -/* No comment provided by engineer. */ -"Favorites" = "Favoris"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Tous les favoris de l'appareil"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Favoris sur mobile uniquement"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Bloquer les publicités et les fenêtres contextuelles"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Bienvenue sur\nDuckDuckGo !"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Paramètres VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoris"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personnalisez vos favoris et vos fonctionnalités préférées. Réorganisez les différents éléments ou masquez-les pour que tout reste en ordre."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Afficher, masquer et réorganiser les sections sur la nouvelle page d'onglet"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Votre nouvelle page d'onglet est… toute nouvelle !"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoris"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Raccourcis"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Afficher, masquer et réorganiser les sections sur la nouvelle page d'onglet"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECTIONS"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Raccourcis"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personnaliser le nouvel onglet"; +"new.tab.page.settings.title" = "Personnaliser la nouvelle page d'onglet"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Ouvrir dans une autre application ?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Rechercher ou saisir une adresse"; diff --git a/DuckDuckGo/hr.lproj/Localizable.strings b/DuckDuckGo/hr.lproj/Localizable.strings index 3d89765b7e..8e72639a3d 100644 --- a/DuckDuckGo/hr.lproj/Localizable.strings +++ b/DuckDuckGo/hr.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Ukloni"; -/* No comment provided by engineer. */ -"Favorites" = "Omiljeno"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Svi favoriti na uređaju"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Samo favoriti na mobilnom uređaju"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokiranje oglasa i skočnih prozora"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Dobro došao/la u\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN postavke"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Omiljeno"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Prilagodi svoje favorite i najdraže značajke. Preuredi stvari ili ih sakrij kako bi sve bilo jasno."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Prikaži, sakrij i promijeni redoslijed odjeljaka na stranici nove kartice"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Tvoja stranica nove kartice je... nova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Omiljeno"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Prečaci"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Prikaži, sakrij i promijeni redoslijed odjeljaka na stranici nove kartice"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEKCIJE"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "PREČACI"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Prilagodi novu karticu"; +"new.tab.page.settings.title" = "Prilagodi stranicu Nova kartica"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otvori u drugoj aplikaciji?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Pretraži ili unesi adresu"; diff --git a/DuckDuckGo/hu.lproj/Localizable.strings b/DuckDuckGo/hu.lproj/Localizable.strings index 941efc0b26..ea6c868cba 100644 --- a/DuckDuckGo/hu.lproj/Localizable.strings +++ b/DuckDuckGo/hu.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Eltávolítás"; -/* No comment provided by engineer. */ -"Favorites" = "Kedvencek"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Eszközön lévő minden kedvenc"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Csak mobilon lévő kedvencek"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Hirdetések és felugró ablakok letiltása"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Üdvözlünk a\nDuckDuckGo-ban!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-beállítások"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Kedvencek"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Testre szabhatja a kedvenceket és a leggyakrabban használt funkciókat. Az áttekinthetőség érdekében átrendezheti az elemeket, vagy elrejtheti őket."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Részek megjelenítése, elrejtése és átrendezése az új lap oldalon"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Az új lap oldal… Új!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Kedvencek"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Gyorsparancsok"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Részek megjelenítése, elrejtése és átrendezése az új lap oldalon"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "RÉSZEK"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Megnyitás másik alkalmazásban?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Keresés vagy cím megadása"; diff --git a/DuckDuckGo/it.lproj/Localizable.strings b/DuckDuckGo/it.lproj/Localizable.strings index f35bd757bc..4e80e35737 100644 --- a/DuckDuckGo/it.lproj/Localizable.strings +++ b/DuckDuckGo/it.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Rimuovi"; -/* No comment provided by engineer. */ -"Favorites" = "Preferiti"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Tutti i preferiti sul dispositivo"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Solo preferiti sul dispositivo mobile"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blocco di annunci e popup"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "DuckDuckGo\nti dà il benvenuto!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Impostazioni VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Preferiti"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personalizza i tuoi Preferiti e le funzionalità principali. Riorganizza gli elementi o nascondili per mantenere tutto in ordine."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Mostra, nascondi e riordina le sezioni nella nuova scheda"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "La tua pagina Nuova scheda è... Nuova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Preferiti"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Scorciatoie"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Mostra, nascondi e riordina le sezioni nella nuova scheda"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEZIONI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Scorciatoie"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personalizza la pagina Nuova scheda"; +"new.tab.page.settings.title" = "Personalizza pagina Nuova scheda"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Aprire in un'altra app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Cerca o digita l'indirizzo"; diff --git a/DuckDuckGo/lt.lproj/Localizable.strings b/DuckDuckGo/lt.lproj/Localizable.strings index 950fa90f78..193dfdf75a 100644 --- a/DuckDuckGo/lt.lproj/Localizable.strings +++ b/DuckDuckGo/lt.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Pašalinti"; -/* No comment provided by engineer. */ -"Favorites" = "Mėgstami"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Visi įrenginio mėgstamiausi"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Tik mobiliojo mėgstamiausi"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklamų ir iššokančiųjų langų blokavimas"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Sveiki atvykę į\n„DuckDuckGo“!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN nustatymai"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Mėgstami"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Tinkinkite savo mėgstamiausius ir pagrindines funkcijas. Pertvarkykite elementus arba paslėpkite juos, kad užtikrintumėte tvarką."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Rodyti, slėpti ir pertvarkyti skiltis naujame skirtuko puslapyje"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Jūsų naujo skirtuko puslapis yra... Atnaujintas!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Mėgstami"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Šaukiniai"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Rodyti, slėpti ir pertvarkyti skiltis naujame skirtuko puslapyje"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SKILTYS"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "ŠAUKINIAI"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Tinkinti naują skirtuką"; +"new.tab.page.settings.title" = "Tinkinti naujo skirtuko puslapį"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "DI pokalbis"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Atidaryti kitoje programoje?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Ieškoti arba įvesti adresą"; diff --git a/DuckDuckGo/lv.lproj/Localizable.strings b/DuckDuckGo/lv.lproj/Localizable.strings index 32f3209f50..9fde6bb665 100644 --- a/DuckDuckGo/lv.lproj/Localizable.strings +++ b/DuckDuckGo/lv.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Noņemt"; -/* No comment provided by engineer. */ -"Favorites" = "Izlase"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Visu ierīču izlase"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Tikai mobilā izlase"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklāmu un uznirstošo logu bloķēšana"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Laipni lūdzam\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN iestatījumi"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Izlase"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Pielāgo savu izlasi un bieži izmantotās funkcijas. Pārkārto elementus vai paslēp tos, lai lieki neaizņemtu vietu."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Rādīt, paslēpt un pārkārtot sadaļas jaunās cilnes lapā"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Tava jaunas cilnes lapa ir... atjaunota!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Izlase"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Saīsnes"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Rādīt, paslēpt un pārkārtot sadaļas jaunās cilnes lapā"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SADAĻAS"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SAĪSNES"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Jaunas cilnes pielāgošana"; +"new.tab.page.settings.title" = "Jaunas cilnes lapas pielāgošana"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "MI čats"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Vai atvērt citā lietotnē?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Meklē vai ievadi adresi"; diff --git a/DuckDuckGo/nb.lproj/Localizable.strings b/DuckDuckGo/nb.lproj/Localizable.strings index f525e7a8d6..320d6c30a4 100644 --- a/DuckDuckGo/nb.lproj/Localizable.strings +++ b/DuckDuckGo/nb.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Fjern"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritter"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle favoritter på enheten"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Kun favoritter på mobil"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokkering av reklame og popups"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Velkommen til\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-innstillinger"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritter"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Tilpass favorittene dine og de funksjonene du bruker mest. Endre på rekkefølgen av ting eller skjul dem for å holde orden."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Vis, skjul og endre rekkefølgen av delene på den nye fanesiden"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Den nye fanesiden din er … ny!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritter"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Snarveier"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Vis, skjul og endre rekkefølgen av delene på den nye fanesiden"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "DELER"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SNARVEIER"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Tilpass ny fane"; +"new.tab.page.settings.title" = "Tilpass ny faneside"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "KI-chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Åpne i en annen app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Søk eller skriv inn adresse"; diff --git a/DuckDuckGo/nl.lproj/Localizable.strings b/DuckDuckGo/nl.lproj/Localizable.strings index 94c0e486c7..c4e0a07bb0 100644 --- a/DuckDuckGo/nl.lproj/Localizable.strings +++ b/DuckDuckGo/nl.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Verwijderen"; -/* No comment provided by engineer. */ -"Favorites" = "Favorieten"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alle favorieten op apparaat"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Alleen mobiele favorieten"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Advertentie- en pop-upblokkering"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Welkom bij\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-instellingen"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favorieten"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Pas je favorieten en go-to-functies aan. Herschik dingen of verberg ze om de tabbladpagina overzichtelijk te maken."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Secties op het nieuwe tabblad weergeven, verbergen en opnieuw ordenen"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Je nieuwe tabbladpagina is... Nieuw!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favorieten"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Sneltoetsen"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Secties op het nieuwe tabblad weergeven, verbergen en opnieuw ordenen"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECTIES"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Openen in een andere app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Zoek of voer een adres in"; diff --git a/DuckDuckGo/pl.lproj/Localizable.strings b/DuckDuckGo/pl.lproj/Localizable.strings index 4a86e99373..aa0e54a7ef 100644 --- a/DuckDuckGo/pl.lproj/Localizable.strings +++ b/DuckDuckGo/pl.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Usuń"; -/* No comment provided by engineer. */ -"Favorites" = "Ulubione"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Ulubione na urządzeniu"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Tylko ulubione z urządzeń mobilnych"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokowanie reklam i wyskakujących okienek"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Witamy\nw DuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Ustawienia VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Ulubione"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Dostosuj ulubione elementy i podręczne funkcje. Zmień kolejność opcji lub ukryj je, aby zachować porządek."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Pokazuj, ukrywaj i zmieniaj kolejność sekcji na stronie nowej karty"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Strona nowej karty ma nowy wygląd!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Ulubione"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Skróty"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Pokazuj, ukrywaj i zmieniaj kolejność sekcji na stronie nowej karty"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SEKCJE"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Skróty"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Dostosuj nową kartę"; +"new.tab.page.settings.title" = "Dostosuj stronę nowej karty"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Czat AI"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otworzyć w innej aplikacji?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Wyszukaj lub wprowadź adres"; diff --git a/DuckDuckGo/pt.lproj/Localizable.strings b/DuckDuckGo/pt.lproj/Localizable.strings index 7694c90646..26a07512de 100644 --- a/DuckDuckGo/pt.lproj/Localizable.strings +++ b/DuckDuckGo/pt.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Remover"; -/* No comment provided by engineer. */ -"Favorites" = "Favoritos"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Todos os favoritos no dispositivo"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Apenas favoritos em dispositivos móveis"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Bloquear anúncios e pop-ups"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Damos-lhe as boas-vindas ao\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Definições da VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoritos"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personaliza os teus Favoritos e as tuas funcionalidades de preferência. Reordena as coisas ou esconde-as para manter um aspeto limpo."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Mostrar, ocultar e reordenar secções na página do novo separador"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "A tua página Novo separador é... nova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoritos"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Atalhos"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Mostrar, ocultar e reordenar secções na página do novo separador"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECÇÕES"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Atalhos"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personalizar novo separador"; +"new.tab.page.settings.title" = "Personalizar página Novo separador"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat com IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Abrir noutra aplicação?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Pesquisar ou inserir endereço"; diff --git a/DuckDuckGo/ro.lproj/Localizable.strings b/DuckDuckGo/ro.lproj/Localizable.strings index a5ed9bdb4f..a5b21cd810 100644 --- a/DuckDuckGo/ro.lproj/Localizable.strings +++ b/DuckDuckGo/ro.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Elimină"; -/* No comment provided by engineer. */ -"Favorites" = "Preferințe"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Toate favoritele de pe dispozitiv"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Doar favoritele pentru mobil"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blocarea anunțurilor și ferestrelor pop-up"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Bine ai venit la\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Setări VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Preferințe"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Personalizează-ți Favoritele și funcțiile predilecte. Reordonează lucrurile sau ascunde-le pentru a menține ordinea."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Afișează, ascunde și reordonează secțiunile din pagina noii file"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Pagina Filă nouă este... nouă!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Preferințe"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Scurtături"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Afișează, ascunde și reordonează secțiunile din pagina noii file"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "SECȚIUNI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SCURTĂTURI"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Personalizează noua filă"; +"new.tab.page.settings.title" = "Personalizează pagina Filă nouă"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Chat IA"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Deschizi în altă aplicație?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Caută sau introdu adresa"; diff --git a/DuckDuckGo/ru.lproj/Localizable.strings b/DuckDuckGo/ru.lproj/Localizable.strings index 9a18920a04..24e34238ce 100644 --- a/DuckDuckGo/ru.lproj/Localizable.strings +++ b/DuckDuckGo/ru.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Удалить"; -/* No comment provided by engineer. */ -"Favorites" = "Избранное"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Избранное со всех устройств"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Только избранное с телефона"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Блокировка рекламы и всплывающих окон"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Добро пожаловать в\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Настройки VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Избранное"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "К вашим услугам настройки «Избранного» и важнейших функций. Для поддержания порядка отдельные элементы можно переупорядочить или скрыть."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Показать, скрыть или изменить порядок разделов на странице новой вкладки"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Ваша страница новой вкладки... В новом виде!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Избранное"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Ярлыки"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Показать, скрыть или изменить порядок разделов на странице новой вкладки"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "РАЗДЕЛЫ"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "Ярлыки"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Настройки страницы новой вкладки"; +"new.tab.page.settings.title" = "Страница новой вкладки"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Чат с ИИ"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Открыть в другом приложении?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Введите поисковый запрос или адрес сайта"; diff --git a/DuckDuckGo/sk.lproj/Localizable.strings b/DuckDuckGo/sk.lproj/Localizable.strings index 8dfdde334b..ab8ce098ee 100644 --- a/DuckDuckGo/sk.lproj/Localizable.strings +++ b/DuckDuckGo/sk.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Odstrániť"; -/* No comment provided by engineer. */ -"Favorites" = "Obľúbené položky"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Obľúbené položky zo všetkých zariadení"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Iba obľúbené mobilné položky"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokovanie reklám a automaticky otváraných okien"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Vitajte v\nprehliadači DuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Nastavenia siete VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Obľúbené položky"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Prispôsobenie obľúbených položiek a funkcií. Pre prehľadnosť zmeňte usporiadanie vecí alebo ich skryte."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Zobrazenie, skrytie a zmena poradia častí na novej stránke karty"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Vaša stránka Nová karta je... Nová!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Obľúbené položky"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Skratky"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Zobrazenie, skrytie a zmena poradia častí na novej stránke karty"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "ČASTI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "SKRATKY"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Prispôsobiť novú kartu"; +"new.tab.page.settings.title" = "Prispôsobiť stránku novej karty"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI Chat"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Otvoriť v inej aplikácii?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Vyhľadajte alebo zadajte adresu"; diff --git a/DuckDuckGo/sl.lproj/Localizable.strings b/DuckDuckGo/sl.lproj/Localizable.strings index 8e47d3a97f..1606d05a14 100644 --- a/DuckDuckGo/sl.lproj/Localizable.strings +++ b/DuckDuckGo/sl.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Odstrani"; -/* No comment provided by engineer. */ -"Favorites" = "Priljubljeni"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Priljubljene iz vseh naprav"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Priljubljene samo iz mobilne naprave"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blokiranje oglasov in pojavnih oken"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Dobrodošli v aplikaciji\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "Nastavitve VPN"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Priljubljeni"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Prilagodite zavihek Priljubljene in izbrane funkcije. Preuredite stvari ali jih skrijte, da boste imeli vse urejeno."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Prikaz, skrivanje in razvrščanje razdelkov na strani novega zavihka"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Vaša stran za nov zavihek je ... nova!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Priljubljeni"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Bližnjice"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Prikaz, skrivanje in razvrščanje razdelkov na strani novega zavihka"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "RAZDELKI"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "BLIŽNJICE"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Prilagoditev strani novega zavihka"; +"new.tab.page.settings.title" = "Prilagoditev strani z novim zavihkom"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Klepet z umetno inteligenco"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Odpri v drugi aplikaciji?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Poišči ali vnesi naslov"; diff --git a/DuckDuckGo/sv.lproj/Localizable.strings b/DuckDuckGo/sv.lproj/Localizable.strings index e1bd795f64..d90d21d646 100644 --- a/DuckDuckGo/sv.lproj/Localizable.strings +++ b/DuckDuckGo/sv.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Ta bort"; -/* No comment provided by engineer. */ -"Favorites" = "Favoriter"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Alla enhetsfavoriter"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Endast mobila favoriter"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Blockering av annonser och poppuppfönster"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "Välkommen till\nDuckDuckGo!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN-inställningar"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoriter"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Anpassa dina favoriter och funktioner. Ordna om saker och ting eller göm dem för att hålla layouten minimalistisk."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Visa, dölja och ändra ordning på avsnitt på den nya fliksidan"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Din nya fliksida är... ny!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoriter"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Genvägar"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Visa, dölja och ändra ordning på avsnitt på den nya fliksidan"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "AVSNITT"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "GENVÄGAR"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Anpassa ny flik"; +"new.tab.page.settings.title" = "Anpassa sidan Ny flik"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "AI-chatt"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Öppna i annan app?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Sök eller ange adress"; diff --git a/DuckDuckGo/tr.lproj/Localizable.strings b/DuckDuckGo/tr.lproj/Localizable.strings index b6641c68e0..28cfbe8e97 100644 --- a/DuckDuckGo/tr.lproj/Localizable.strings +++ b/DuckDuckGo/tr.lproj/Localizable.strings @@ -1154,9 +1154,6 @@ /* No comment provided by engineer. */ "favorite.menu.remove" = "Kaldır"; -/* No comment provided by engineer. */ -"Favorites" = "Favoriler"; - /* Display Mode for favorites */ "favorites.settings.all-devices" = "Tüm Cihaz Sık Kullanılanları"; @@ -1169,9 +1166,6 @@ /* Display Mode for favorites */ "favorites.settings.mobile-only" = "Yalnızca Mobil Favoriler"; -/* No comment provided by engineer. */ -"Feature flag enabled" = "Feature flag enabled"; - /* No comment provided by engineer. */ "feedback.browserFeatures.ads" = "Reklam ve pop-up engelleme"; @@ -1475,9 +1469,6 @@ /* Please preserve newline character */ "launchscreenWelcomeMessage" = "DuckDuckGo'ya\nHoş Geldiniz!"; -/* No comment provided by engineer. */ -"Local setting enabled" = "Local setting enabled"; - /* No comment provided by engineer. */ "LOREM IPSUM" = "LOREM IPSUM"; @@ -1712,13 +1703,24 @@ /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Ayarları"; +/* Header title of Favorites section */ +"new.tab.page.favorites.setion.header.title" = "Favoriler"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Favorilerinizi ve düzenli olarak kullandığınız özellikleri kendinize göre ayarlayın. Temiz tutmak için öğeleri yeniden sıralayın veya gizleyin."; -/* Footer of the group allowing for setting up new tab page sections */ -"new.tab.page.settings.sections.description" = "Yeni sekme sayfasında bölümleri göster, gizle ve yeniden sırala"; /* Title of information message about New Tab Page redesign */ "new.tab.page.intro.message.title" = "Yeni Sekme Sayfanız... Yeni!"; + +/* Name of favorites section setting */ +"new.tab.page.settings.section.name.favorites" = "Favoriler"; + +/* Name of shortcuts section setting */ +"new.tab.page.settings.section.name.shortcuts" = "Kısayollar"; + +/* Footer of the group allowing for setting up new tab page sections */ +"new.tab.page.settings.sections.description" = "Yeni sekme sayfasında bölümleri göster, gizle ve yeniden sırala"; + /* Header title of the group allowing for setting up new tab page sections */ "new.tab.page.settings.sections.header.title" = "BÖLÜMLER"; @@ -1726,7 +1728,7 @@ "new.tab.page.settings.shortcuts.header.title" = "KISAYOLLAR"; /* Title of New Tab Page preferences page. */ -"new.tab.page.settings.title" = "Yeni Sekme Sayfasını Özelleştirin"; +"new.tab.page.settings.title" = "Yeni Sekme Sayfasını Özelleştir"; /* Shortcut title leading to AI Chat */ "new.tab.page.shortcut.ai.chat" = "Yapay Zeka Sohbeti"; @@ -1907,9 +1909,6 @@ /* Alert title */ "prompt.custom.url.scheme.title" = "Başka Bir Uygulamada Açılsın mı?"; -/* No comment provided by engineer. */ -"Requires internal user" = "Requires internal user"; - /* No comment provided by engineer. */ "search.hint.duckduckgo" = "Adres ara veya gir"; diff --git a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift index 8e4aff669b..77776002ae 100644 --- a/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift +++ b/DuckDuckGoTests/NewTabPageFavoritesModelTests.swift @@ -106,6 +106,7 @@ final class NewTabPageFavoritesModelTests: XCTestCase { private func createSUT() -> FavoritesDefaultModel { FavoritesDefaultModel(interactionModel: favoritesListInteracting, + faviconLoader: FavoritesFaviconLoader(), pixelFiring: PixelFiringMock.self, dailyPixelFiring: PixelFiringMock.self) } From 35b7a452d3028428033b1ff29ac878bbe0a858ea Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Mon, 16 Sep 2024 06:29:23 -0500 Subject: [PATCH 5/5] Add privacy config version to broken site reports (#3351) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/AutofillLoginListViewModel.swift | 1 + .../PrivacyDashboard/PrivacyDashboardViewController.swift | 1 + DuckDuckGoTests/BrokenSiteReportingTests.swift | 2 ++ DuckDuckGoTests/MockPrivacyConfiguration.swift | 1 + DuckDuckGoTests/PrivacyConfigurationManagerMock.swift | 1 + 7 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23688459f1..4fc50dc02a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10885,7 +10885,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 195.0.0; + version = 196.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d785772d67..1fc9b47178 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "f9134f887b1215779a1050134d09d7e824a8abc0", - "version" : "195.0.0" + "revision" : "ae3dbec01b8b72dc2ea4c510aecbc802862eab63", + "version" : "196.0.0" } }, { diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 0bf99e6fe1..c244f97752 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -362,6 +362,7 @@ final class AutofillLoginListViewModel: ObservableObject { manufacturer: "", upgradedHttps: false, tdsETag: nil, + configVersion: nil, blockedTrackerDomains: nil, installedSurrogates: nil, isGPCEnabled: true, diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift index f16e209939..1c3161a27f 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift @@ -356,6 +356,7 @@ extension PrivacyDashboardViewController { manufacturer: "Apple", upgradedHttps: breakageAdditionalInfo.httpsForced, tdsETag: ContentBlocking.shared.contentBlockingManager.currentMainRules?.etag ?? "", + configVersion: privacyConfigurationManager.privacyConfig.version, blockedTrackerDomains: blockedTrackerDomains, installedSurrogates: privacyInfo.trackerInfo.installedSurrogates.map { $0 }, isGPCEnabled: AppDependencyProvider.shared.appSettings.sendDoNotSell, diff --git a/DuckDuckGoTests/BrokenSiteReportingTests.swift b/DuckDuckGoTests/BrokenSiteReportingTests.swift index a99c8b8f1f..708c5d59d8 100644 --- a/DuckDuckGoTests/BrokenSiteReportingTests.swift +++ b/DuckDuckGoTests/BrokenSiteReportingTests.swift @@ -102,6 +102,7 @@ final class BrokenSiteReportingTests: XCTestCase { manufacturer: test.manufacturer ?? "", upgradedHttps: test.wasUpgraded, tdsETag: test.blocklistVersion, + configVersion: test.remoteConfigVersion, blockedTrackerDomains: test.blockedTrackers, installedSurrogates: test.surrogates, isGPCEnabled: test.gpcEnabled ?? false, @@ -169,6 +170,7 @@ private struct Test: Codable { let providedDescription: String? let blockedTrackers, surrogates: [String] let atb, blocklistVersion: String + let remoteConfigVersion: String? let expectReportURLPrefix: String let expectReportURLParams: [ExpectReportURLParam] let exceptPlatforms: [String] diff --git a/DuckDuckGoTests/MockPrivacyConfiguration.swift b/DuckDuckGoTests/MockPrivacyConfiguration.swift index 1053217167..3066b1c3a6 100644 --- a/DuckDuckGoTests/MockPrivacyConfiguration.swift +++ b/DuckDuckGoTests/MockPrivacyConfiguration.swift @@ -36,6 +36,7 @@ class MockPrivacyConfiguration: PrivacyConfiguration { } var identifier: String = "MockPrivacyConfiguration" + var version: String? = "123456789" var userUnprotectedDomains: [String] = [] var tempUnprotectedDomains: [String] = [] var trackerAllowlist: PrivacyConfigurationData.TrackerAllowlist = .init(entries: [:], diff --git a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift index 15c9b403ab..d50a621f91 100644 --- a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift +++ b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift @@ -24,6 +24,7 @@ import BrowserServicesKit class PrivacyConfigurationMock: PrivacyConfiguration { var identifier: String = "id" + var version: String? = "123456789" var userUnprotectedDomains: [String] = []