Skip to content

Commit

Permalink
Merge branch 'main' into sam/persistent-pixels
Browse files Browse the repository at this point in the history
# By Daniel Bernal (12) and others
# Via Daniel Bernal (5) and others
* main: (46 commits)
  Release 7.139.0-4 (#3411)
  Onboarding highlights experiment updates (#3406)
  fix suggestions performance (#3405)
  For third party requests differentiate if they are affiliated with first party (#3386)
  Bump BSK to pull in C-S-S 6.19.0 (#3396)
  Release 7.139.0-3 (#3399)
  Onboarding Highlights Ship Review  (#3380)
  add assertions for tabs in suggestions (#3394)
  Release 7.139.0-2 (#3398)
  Bump BSK to Include C.S.S 6.17 (#3397)
  Bump BSK which includes C.S.S 6.17 (#3395)
  Rever BSK branch
  Revert "Bump C.S.S"
  Bump C.S.S
  translations for Switch to Tab (#3391)
  Remove Favorites section header from NTP (#3381)
  Release 7.139.0-1 (#3389)
  Add origin to /apps URL (#3378)
  chery pick returning user fix
  Release 7.138.1-0 (#3388)
  ...

# Conflicts:
#	DuckDuckGo/AppDependencyProvider.swift
  • Loading branch information
samsymons committed Sep 28, 2024
2 parents d27681b + 6694595 commit c772efa
Show file tree
Hide file tree
Showing 184 changed files with 6,941 additions and 1,846 deletions.
1 change: 0 additions & 1 deletion .github/workflows/end-to-end.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ jobs:

steps:
- name: Create Asana task when workflow failed
if: ${{ failure() }}
run: |
curl -s "https://app.asana.com/api/1.0/tasks" \
--header "Accept: application/json" \
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/pr-task-url.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Asana PR Task URL

on:
pull_request:
types: [opened, edited, closed, synchronize, review_requested]
types: [opened, edited, closed, synchronize, review_requested, ready_for_review]

jobs:

Expand All @@ -14,6 +14,8 @@ jobs:

runs-on: ubuntu-latest

if: ${{ !github.event.pull_request.draft }}

outputs:
task_id: ${{ steps.get-task-id.outputs.task_id }}
task_in_project: ${{ steps.check-board-membership.outputs.task_in_project }}
Expand Down Expand Up @@ -47,7 +49,7 @@ jobs:
- name: Add Task to the App Board Project
id: add-task-to-project
if: ${{ github.event.action == 'opened' && steps.check-board-membership.outputs.task_in_project == '0' }}
if: ${{ (github.event.action == 'opened' || github.event.action == 'ready_for_review') && steps.check-board-membership.outputs.task_in_project == '0' }}
env:
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
ASANA_PROJECT_ID: ${{ vars.IOS_APP_BOARD_ASANA_PROJECT_ID }}
Expand Down
44 changes: 44 additions & 0 deletions .maestro/release_tests/tabs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ tags:
- assertVisible: ".*Privacy Test Pages.*"
- tapOn: "Refresh Page"

# Suggestions
- assertVisible:
id: "searchEntry"

- tapOn:
id: "searchEntry"
- inputText: "ad click"
- assertVisible: "Switch to Tab.*search-company.site"
- tapOn: "Switch to Tab.*search-company.site"
- assertVisible: ".*Ad Click Flow.*"

- tapOn:
id: "searchEntry"
- inputText: "privacy"
- assertVisible: "Switch to Tab.*privacy-test-pages.site"
- tapOn: "Switch to Tab.*privacy-test-pages.site"
- assertVisible: ".*Privacy Test Pages.*"

# Needed or else test can't see the Tab Switcher button for some reason
- tapOn: "Refresh Page"

# Close Tab
- assertVisible: Tab Switcher
- tapOn: Tab Switcher
Expand All @@ -57,3 +78,26 @@ tags:
- assertNotVisible: ".*Ad Click Flow.*"
- assertVisible: "1 Private Tab"
- tapOn: "Done"

# Switch tabs from new tab
- tapOn: "Refresh Page"
- assertVisible: Tab Switcher
- tapOn: Tab Switcher
- assertVisible: ".*Privacy Test Pages.*"
- assertVisible:
id: "Add"
- tapOn:
id: "Add"
- assertVisible:
id: "searchEntry"
- tapOn:
id: "searchEntry"
- inputText: "privacy"
- assertVisible: "Switch to Tab.*privacy-test-pages.site"
- tapOn: "Switch to Tab.*privacy-test-pages.site"
- assertVisible: ".*Privacy Test Pages.*"
- tapOn: "Refresh Page"
- assertVisible: Tab Switcher
- tapOn: Tab Switcher
- assertVisible: "1 Private Tab"

10 changes: 10 additions & 0 deletions .maestro/shared/onboarding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ appId: com.duckduckgo.mobile.ios
# - assertVisible: "Make DuckDuckGo your default browser."
- tapOn:
text: "Skip"

- runFlow:
when:
visible: "Which color looks best on me?"
commands:
- assertVisible: "Next"
- tapOn: "Next"
- assertVisible: "Where should I put your address bar?"
- assertVisible: "Next"
- tapOn: "Next"
2 changes: 1 addition & 1 deletion Configuration/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MARKETING_VERSION = 7.137.0
MARKETING_VERSION = 7.139.0
4 changes: 2 additions & 2 deletions Core/AppPrivacyConfigurationDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import BrowserServicesKit
final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider {

public struct Constants {
public static let embeddedDataETag = "\"9087766799743533c0741b03cea431d1\""
public static let embeddedDataSHA = "9e9fcfd329fc587ba732cf9cb7e71d81f7af7717c3f804f28b9c8603599ee8d8"
public static let embeddedDataETag = "\"aa6acaab3804053c652b64a3568cee2b\""
public static let embeddedDataSHA = "d56a1b7ff72713333d2d17e6825a6ab8f14d3f87b4b77c2406d74840d393960b"
}

public var embeddedDataEtag: String {
Expand Down
3 changes: 2 additions & 1 deletion Core/AppURLs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ public extension URL {
static let emailProtectionSupportLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/email/settings/support"))!
static let emailProtectionHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/email-protection/what-is-duckduckgo-email-protection/"))!
static let aboutLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/about"))!
static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps"))!
static let apps = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/apps?origin=funnel_app_ios"))!
static let searchSettings = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/settings"))!
static let autofillHelpPageLink = URL(string: AppDeepLinkSchemes.quickLink.appending("\(ddg.host!)/duckduckgo-help-pages/sync-and-backup/password-manager-security/"))!

static let surrogates = URL(string: "\(staticBase)/surrogates.txt")!

Expand Down
99 changes: 55 additions & 44 deletions Core/BookmarksCachingSearch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch {
self.title = title
self.url = url
self.isFavorite = isFavorite

if isFavorite {
score = 0
} else {
score = -1
}
self.score = 0
}

init?(bookmark: [String: Any]) {
Expand Down Expand Up @@ -191,7 +186,6 @@ public class BookmarksCachingSearch: BookmarksStringSearch {
return cachedBookmarksAndFavorites
}

// swiftlint:disable cyclomatic_complexity
private func score(query: String, input: [ScoredBookmark]) -> [ScoredBookmark] {
let query = query.lowercased()
let tokens = query.split(separator: " ").filter { !$0.isEmpty }.map { String($0).lowercased() }
Expand All @@ -201,55 +195,63 @@ public class BookmarksCachingSearch: BookmarksStringSearch {

for index in 0..<input.count {
let entry = input[index]
let title = entry.title.lowercased()

// Exact matches - full query
if title.starts(with: query) { // High score for exact match from the beginning of the title
input[index].score += 200
} else if title.contains(" \(query)") { // Exact match from the beginning of the word within string.
input[index].score += 100
// Add the new score to the existing score defined by them being a favorite
input[index].score = score(query, entry, tokens)
if input[index].score > 0 {
result.append(input[index])
}
}
return result
}

let domain = entry.url.host?.droppingWwwPrefix() ?? ""
private func score(_ query: String, _ bookmark: ScoredBookmark, _ tokens: [String]) -> Int {
let title = bookmark.title.lowercased()
let domain = bookmark.url.host?.droppingWwwPrefix() ?? ""
var score = bookmark.isFavorite ? 0 : -1

// Tokenized matches
// Exact matches - full query
if title.leadingBoundaryStartsWith(query) { // High score for exact match from the beginning of the title
score += 200
} else if title.contains(" \(query)") { // Exact match from the beginning of the word within string.
score += 100
}

if tokens.count > 1 {
var matchesAllTokens = true
for token in tokens {
// Match only from the beginning of the word to avoid unintuitive matches.
if !title.starts(with: token) && !title.contains(" \(token)") && !domain.starts(with: token) {
matchesAllTokens = false
break
}
// Tokenized matches

if tokens.count > 1 {
var matchesAllTokens = true
for token in tokens {
// Match only from the beginning of the word to avoid unintuitive matches.
if !title.leadingBoundaryStartsWith(token) &&
!title.contains(" \(token)")
&& !domain.starts(with: token) {
matchesAllTokens = false
break
}
}

if matchesAllTokens {
// Score tokenized matches
input[index].score += 10

// Boost score if first token matches:
if let firstToken = tokens.first { // domain - high score boost
if domain.starts(with: firstToken) {
input[index].score += 300
} else if title.starts(with: firstToken) { // beginning of the title - moderate score boost
input[index].score += 50
}
if matchesAllTokens {
// Score tokenized matches
score += 10

// Boost score if first token matches:
if let firstToken = tokens.first { // domain - high score boost
if domain.starts(with: firstToken) {
score += 300
} else if title.leadingBoundaryStartsWith(firstToken) { // beginning of the title - moderate score boost
score += 50
}
}
} else {
// High score for matching domain in the URL
if let firstToken = tokens.first, domain.starts(with: firstToken) {
input[index].score += 300
}
}
if input[index].score > 0 {
result.append(input[index])
} else {
// High score for matching domain in the URL
if let firstToken = tokens.first, domain.starts(with: firstToken) {
score += 300
}
}
return result

return score
}
// swiftlint:enable cyclomatic_complexity

public func search(query: String) -> [BookmarksStringSearchResult] {
guard hasData else {
Expand All @@ -265,3 +267,12 @@ public class BookmarksCachingSearch: BookmarksStringSearch {
return finalResult
}
}

private extension String {

/// e.g. "Cats and Dogs" would match `Cats` or `"Cats`
func leadingBoundaryStartsWith(_ s: String) -> Bool {
return starts(with: s) || trimmingCharacters(in: .alphanumerics.inverted).starts(with: s)
}

}
1 change: 1 addition & 0 deletions Core/ContentBlockerStoreConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ import Foundation
public struct ContentBlockerStoreConstants {

public static let groupName = "\(Global.groupIdPrefix).contentblocker"
public static let configurationGroupName = "\(Global.groupIdPrefix).app-configuration"

}
2 changes: 1 addition & 1 deletion Core/DailyPixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public final class DailyPixel {

}

private static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)!
public static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)!

/// Sends a given Pixel once per day.
/// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected.
Expand Down
66 changes: 66 additions & 0 deletions Core/Debouncer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// Debouncer.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 class that provides a debouncing mechanism.
public final class Debouncer {
private let runLoop: RunLoop
private let mode: RunLoop.Mode
private var timer: Timer?

/// Initializes a new instance of `Debouncer`.
///
/// - Parameters:
/// - runLoop: The `RunLoop` on which the debounced actions will be scheduled. Defaults to the current run loop.
///
/// - mode: The `RunLoop.Mode` in which the debounced actions will be scheduled. Defaults to `.default`.
///
/// Use `RunLoop.main` for UI-related actions to ensure they run on the main thread.
public init(runLoop: RunLoop = .current, mode: RunLoop.Mode = .default) {
self.runLoop = runLoop
self.mode = mode
}

/// Debounces the provided block of code, executing it after a specified time interval elapses.
/// - Parameters:
/// - dueTime: The time interval (in seconds) to wait before executing the block.
/// - block: The closure to execute after the due time has passed.
///
/// If `dueTime` is less than or equal to zero, the block is executed immediately.
public func debounce(for dueTime: TimeInterval, block: @escaping () -> Void) {
timer?.invalidate()

guard dueTime > 0 else { return block() }

let timer = Timer(timeInterval: dueTime, repeats: false, block: { timer in
guard timer.isValid else { return }
block()
})

runLoop.add(timer, forMode: mode)
self.timer = timer
}

/// Cancels any pending execution of the debounced block.
public func cancel() {
timer?.invalidate()
timer = nil
}
}
7 changes: 5 additions & 2 deletions Core/DefaultVariantManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ extension FeatureName {
// public static let experimentalFeature = FeatureName(rawValue: "experimentalFeature")

public static let newOnboardingIntro = FeatureName(rawValue: "newOnboardingIntro")
public static let newOnboardingIntroHighlights = FeatureName(rawValue: "newOnboardingIntroHighlights")
public static let contextualDaxDialogs = FeatureName(rawValue: "contextualDaxDialogs")
}

public struct VariantIOS: Variant {
Expand Down Expand Up @@ -56,8 +58,9 @@ public struct VariantIOS: Variant {
VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []),
VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []),

VariantIOS(name: "ma", weight: 1, isIncluded: When.always, features: []),
VariantIOS(name: "mb", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]),
VariantIOS(name: "ms", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro]),
VariantIOS(name: "mu", weight: 1, isIncluded: When.always, features: [.newOnboardingIntro, .contextualDaxDialogs]),
VariantIOS(name: "mx", weight: 1, isIncluded: When.always, features: [.newOnboardingIntroHighlights, .contextualDaxDialogs]),

returningUser
]
Expand Down
2 changes: 1 addition & 1 deletion Core/EtagStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public protocol BlockerListETagStorage {

public struct UserDefaultsETagStorage: BlockerListETagStorage {

private let defaults = UserDefaults(suiteName: "com.duckduckgo.blocker-list.etags")
private let defaults = UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration")

public init() { }

Expand Down
3 changes: 3 additions & 0 deletions Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum FeatureFlag: String {
case syncPromotionPasswords
case onboardingHighlights
case autofillSurveys
case autcompleteTabs
}

extension FeatureFlag: FeatureFlagSourceProviding {
Expand Down Expand Up @@ -89,6 +90,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .internalOnly
case .autofillSurveys:
return .remoteReleasable(.feature(.autofillSurveys))
case .autcompleteTabs:
return .remoteReleasable(.feature(.autocompleteTabs))
}
}
}
Expand Down
Loading

0 comments on commit c772efa

Please sign in to comment.