Skip to content

Commit feab009

Browse files
committed
Merge pull request #24376 from brave/ios/enhancement/search-suggestions
1 parent 462b6c8 commit feab009

19 files changed

+1538
-38
lines changed

ios/brave-ios/App/iOS/Delegates/AppDelegate.swift

+7
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
245245

246246
// Always load YouTube in Brave for new users
247247
Preferences.General.keepYouTubeInBrave.value = true
248+
249+
// Enable Search Suggestions for BraveSearch default countries
250+
Preferences.Search.showSuggestions.value =
251+
AppState.shared.profile.searchEngines.isBraveSearchDefaultRegion
252+
253+
Preferences.Search.shouldShowSuggestionsOptIn.value =
254+
!AppState.shared.profile.searchEngines.isBraveSearchDefaultRegion
248255
}
249256

250257
if Preferences.URP.referralLookupOutstanding.value == nil {

ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,16 @@ extension BrowserViewController: TopToolbarDelegate {
218218
hideSearchController()
219219
} else {
220220
showSearchController()
221-
searchController?.setSearchQuery(query: text)
221+
222+
Task {
223+
await searchController?.setSearchQuery(
224+
query: text,
225+
showSearchSuggestions: URLBarHelper.shared.shouldShowSearchSuggestions(
226+
using: topToolbar.locationLastReplacement
227+
)
228+
)
229+
}
230+
222231
searchLoader?.query = text.lowercased()
223232
}
224233
}

ios/brave-ios/Sources/Brave/Frontend/Browser/Search/InitialSearchEngines.swift

+19-11
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,16 @@ class InitialSearchEngines {
8383
engines.filter { !$0.id.excludedFromOnboarding(for: locale) }
8484
}
8585

86-
static let braveSearchDefaultRegions = [
86+
let braveSearchDefaultRegions = [
8787
"US", "CA", "GB", "FR", "DE", "AD", "AT", "ES", "MX", "BR", "AR", "IN", "IT",
8888
]
89-
static let yandexDefaultRegions = ["AM", "AZ", "BY", "KG", "KZ", "MD", "RU", "TJ", "TM", "TZ"]
90-
static let ecosiaEnabledRegions = [
89+
let yandexDefaultRegions = ["AM", "AZ", "BY", "KG", "KZ", "MD", "RU", "TJ", "TM", "TZ"]
90+
let ecosiaEnabledRegions = [
9191
"AT", "AU", "BE", "CA", "DK", "ES", "FI", "GR", "HU", "IT",
9292
"LU", "NO", "PT", "US", "GB", "FR", "DE", "NL", "CH", "SE", "IE",
9393
]
94-
static let naverDefaultRegions = ["KR"]
95-
static let daumEnabledRegions = ["KR"]
94+
let naverDefaultRegions = ["KR"]
95+
let daumEnabledRegions = ["KR"]
9696

9797
/// Sets what should be the default search engine for given locale.
9898
/// If the engine does not exist in `engines` list, it is added to it.
@@ -118,6 +118,14 @@ class InitialSearchEngines {
118118
}
119119
}
120120

121+
public var isBraveSearchDefaultRegion: Bool {
122+
guard let regionID = locale.region?.identifier ?? Locale.current.region?.identifier else {
123+
return false
124+
}
125+
126+
return braveSearchDefaultRegions.contains(regionID)
127+
}
128+
121129
init(locale: Locale = .current) {
122130
self.locale = locale
123131

@@ -147,25 +155,25 @@ class InitialSearchEngines {
147155
// MARK: - Locale overrides
148156

149157
private func regionOverrides() {
150-
guard let region = locale.regionCode else { return }
158+
guard let region = locale.region?.identifier else { return }
151159

152-
if Self.yandexDefaultRegions.contains(region) {
160+
if yandexDefaultRegions.contains(region) {
153161
defaultSearchEngine = .yandex
154162
}
155163

156-
if Self.ecosiaEnabledRegions.contains(region) {
164+
if ecosiaEnabledRegions.contains(region) {
157165
replaceOrInsert(engineId: .ecosia, customId: nil)
158166
}
159167

160-
if Self.braveSearchDefaultRegions.contains(region) {
168+
if braveSearchDefaultRegions.contains(region) {
161169
defaultSearchEngine = .braveSearch
162170
}
163171

164-
if Self.naverDefaultRegions.contains(region) {
172+
if naverDefaultRegions.contains(region) {
165173
defaultSearchEngine = .naver
166174
}
167175

168-
if Self.daumEnabledRegions.contains(region) {
176+
if daumEnabledRegions.contains(region) {
169177
replaceOrInsert(engineId: .daum, customId: nil)
170178
}
171179
}

ios/brave-ios/Sources/Brave/Frontend/Browser/Search/SearchEngines.swift

+5
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,13 @@ public class SearchEngines {
5858
private let initialSearchEngines: InitialSearchEngines
5959
private let locale: Locale
6060

61+
public var isBraveSearchDefaultRegion: Bool {
62+
return initialSearchEngines.isBraveSearchDefaultRegion
63+
}
64+
6165
public init(locale: Locale = .current) {
6266
initialSearchEngines = InitialSearchEngines(locale: locale)
67+
6368
self.locale = locale
6469
self.disabledEngineNames = getDisabledEngineNames()
6570
}

ios/brave-ios/Sources/Brave/Frontend/Browser/Search/SearchViewController.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,12 @@ public class SearchViewController: SiteTableViewController, LoaderListener {
287287
layoutSuggestionsOptInPrompt()
288288
}
289289

290-
func setSearchQuery(query: String) {
290+
func setSearchQuery(query: String, showSearchSuggestions: Bool = true) {
291291
dataSource.searchQuery = query
292-
dataSource.querySuggestClient()
292+
// Do not query suggestions if the text entred is suspicious
293+
if showSearchSuggestions {
294+
dataSource.querySuggestClient()
295+
}
293296
}
294297

295298
private func reloadSearchEngines() {

ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift

-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ class WeakTabManagerDelegate {
4747
// TabManager must extend NSObjectProtocol in order to implement WKNavigationDelegate
4848
class TabManager: NSObject {
4949
fileprivate var delegates = [WeakTabManagerDelegate]()
50-
fileprivate let tabEventHandlers: [TabEventHandler]
5150
weak var stateDelegate: TabManagerStateDelegate?
5251

5352
/// Internal url to access the new tab page.
@@ -131,7 +130,6 @@ class TabManager: NSObject {
131130
self.tabGeneratorAPI = tabGeneratorAPI
132131
self.historyAPI = historyAPI
133132
self.privateBrowsingManager = privateBrowsingManager
134-
self.tabEventHandlers = TabEventHandlers.create(with: prefs)
135133
super.init()
136134

137135
self.navDelegate.tabManager = self

ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ class TopToolbarView: UIView, ToolbarProtocol {
112112
}
113113
}
114114

115+
var locationLastReplacement: String {
116+
locationTextField?.lastReplacement ?? ""
117+
}
118+
115119
// MARK: Views
116120

117121
private var locationTextField: AutocompleteTextField?

ios/brave-ios/Sources/Brave/Frontend/ClientPreferences.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ extension Preferences {
124124

125125
final public class Search {
126126
/// Whether or not to show suggestions while the user types
127-
static let showSuggestions = Option<Bool>(key: "search.show-suggestions", default: false)
127+
public static let showSuggestions = Option<Bool>(key: "search.show-suggestions", default: false)
128128
/// If the user should see the show suggetsions opt-in
129-
static let shouldShowSuggestionsOptIn = Option<Bool>(
129+
public static let shouldShowSuggestionsOptIn = Option<Bool>(
130130
key: "search.show-suggestions-opt-in",
131131
default: true
132132
)

ios/brave-ios/Sources/Brave/Frontend/Widgets/AutocompleteTextField.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class AutocompleteTextField: UITextField, UITextFieldDelegate {
5050
// in touchesEnd() (eg. applyCompletion() is called or not)
5151
fileprivate var notifyTextChanged: (() -> Void)?
5252
fileprivate var notifyTextDeleted: (() -> Void)?
53-
private var lastReplacement: String?
53+
public var lastReplacement: String?
5454

5555
var highlightColor = AutocompleteTextFieldUX.highlightColor
5656

ios/brave-ios/Sources/Brave/Helpers/TabEventHandlers.swift

-17
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2024 The Brave Authors. All rights reserved.
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
import Foundation
7+
import UIKit
8+
9+
class URLBarHelper {
10+
11+
static let shared = URLBarHelper()
12+
13+
func shouldShowSearchSuggestions(using lastReplacement: String) async -> Bool {
14+
// Check if last entry to url textfield needs to be checked as suspicious.
15+
// The reason of checking count is bigger than 1 is the single character
16+
// entries will always be safe and only way to achieve multi character entry is
17+
// using paste board.
18+
// This check also allow us to handle paste permission case
19+
guard lastReplacement.count > 1 else {
20+
return true
21+
}
22+
23+
// Check if paste board has any text to guarantee the case
24+
guard UIPasteboard.general.hasStrings || UIPasteboard.general.hasURLs else {
25+
return true
26+
}
27+
28+
// Perform check on pasted text
29+
if let pasteboardContents = UIPasteboard.general.string {
30+
let isSuspicious = await isSuspiciousQuery(pasteboardContents)
31+
return !isSuspicious
32+
}
33+
34+
return true
35+
}
36+
37+
/// Whether the desired text should allow search suggestions to appear when it is copied
38+
/// - Parameter query: Search query copied
39+
/// - Returns: the result if it is suspicious
40+
func isSuspiciousQuery(_ query: String) async -> Bool {
41+
// Remove the msg if the query is too long
42+
if query.count > 50 {
43+
return true
44+
}
45+
46+
// Remove the msg if the query contains more than 7 words
47+
if query.components(separatedBy: " ").count > 7 {
48+
return true
49+
}
50+
51+
// Remove the msg if the query contains a number longer than 7 digits
52+
if let _ = checkForLongNumber(query, 7) {
53+
return true
54+
}
55+
56+
// Remove if email (exact), even if not totally well formed
57+
if checkForEmail(query) {
58+
return true
59+
}
60+
61+
// Remove if query looks like an http pass
62+
if query.range(of: "[^:]+:[^@]+@", options: .regularExpression) != nil {
63+
return true
64+
}
65+
66+
for word in query.components(separatedBy: " ") {
67+
if word.range(of: "[^:]+:[^@]+@", options: .regularExpression) != nil {
68+
return true
69+
}
70+
}
71+
72+
if query.count > 12 {
73+
let literalsPattern = "[^A-Za-z0-9]"
74+
75+
guard
76+
let literalsRegex = try? NSRegularExpression(
77+
pattern: literalsPattern,
78+
options: .caseInsensitive
79+
)
80+
else {
81+
return true
82+
}
83+
84+
let range = NSRange(location: 0, length: query.utf16.count)
85+
86+
let cquery = literalsRegex.stringByReplacingMatches(
87+
in: query,
88+
options: [],
89+
range: range,
90+
withTemplate: ""
91+
)
92+
93+
if cquery.count > 12 {
94+
let pp = isHashProb(cquery)
95+
// we are a bit more strict here because the query
96+
// can have parts well formed
97+
if pp < URLBarHelperConstants.probHashThreshold * 1.5 {
98+
return true
99+
}
100+
}
101+
}
102+
103+
return false
104+
}
105+
106+
private func checkForLongNumber(_ str: String, _ maxNumberLength: Int) -> String? {
107+
let controlString = str.replacingOccurrences(
108+
of: "[^A-Za-z0-9]",
109+
with: "",
110+
options: .regularExpression
111+
)
112+
113+
var location = 0
114+
var maxLocation = 0
115+
var maxLocationPosition: String.Index? = nil
116+
117+
for i in controlString.indices {
118+
if controlString[i] >= "0" && controlString[i] <= "9" {
119+
location += 1
120+
} else {
121+
if location > maxLocation {
122+
maxLocation = location
123+
maxLocationPosition = i
124+
}
125+
126+
location = 0
127+
}
128+
}
129+
130+
if location > maxLocation {
131+
maxLocation = location
132+
maxLocationPosition = controlString.endIndex
133+
}
134+
135+
if let maxLocationPosition = maxLocationPosition, maxLocation > maxNumberLength {
136+
let start = controlString.index(maxLocationPosition, offsetBy: -maxLocation)
137+
let end = maxLocationPosition
138+
139+
return String(controlString[start..<end])
140+
} else {
141+
return nil
142+
}
143+
}
144+
145+
private func checkForEmail(_ str: String) -> Bool {
146+
let emailPattern = "[a-z0-9\\-_@]+(@|%40|%(25)+40)[a-z0-9\\-_]+\\.[a-z0-9\\-_]"
147+
148+
guard
149+
let emailRegex = try? NSRegularExpression(pattern: emailPattern, options: .caseInsensitive)
150+
else {
151+
return false
152+
}
153+
154+
let range = NSRange(location: 0, length: str.utf16.count)
155+
return emailRegex.firstMatch(in: str, options: [], range: range) != nil
156+
}
157+
158+
private func isHashProb(_ str: String) -> Double {
159+
var logProb = 0.0
160+
var transC = 0
161+
let filteredStr = str.replacingOccurrences(
162+
of: "[^A-Za-z0-9]",
163+
with: "",
164+
options: .regularExpression
165+
)
166+
167+
let characters = Array(filteredStr)
168+
for i in 0..<(characters.count - 1) {
169+
if let pos1 = URLBarHelperConstants.probHashChars[characters[i]],
170+
let pos2 = URLBarHelperConstants.probHashChars[characters[i + 1]]
171+
{
172+
logProb += URLBarHelperConstants.probHashLogM[pos1][pos2]
173+
transC += 1
174+
}
175+
}
176+
177+
if transC > 0 {
178+
return exp(logProb / Double(transC))
179+
} else {
180+
return exp(logProb)
181+
}
182+
}
183+
184+
}

0 commit comments

Comments
 (0)