Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Search Suggestions Enhancement (uplift to 1.70.x) #25150

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ios/brave-ios/App/iOS/Delegates/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

// Always load YouTube in Brave for new users
Preferences.General.keepYouTubeInBrave.value = true

// Enable Search Suggestions for BraveSearch default countries
Preferences.Search.showSuggestions.value =
AppState.shared.profile.searchEngines.isBraveSearchDefaultRegion

Preferences.Search.shouldShowSuggestionsOptIn.value =
!AppState.shared.profile.searchEngines.isBraveSearchDefaultRegion
}

if Preferences.URP.referralLookupOutstanding.value == nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,16 @@ extension BrowserViewController: TopToolbarDelegate {
hideSearchController()
} else {
showSearchController()
searchController?.setSearchQuery(query: text)

Task {
await searchController?.setSearchQuery(
query: text,
showSearchSuggestions: URLBarHelper.shared.shouldShowSearchSuggestions(
using: topToolbar.locationLastReplacement
)
)
}

searchLoader?.query = text.lowercased()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,16 @@ class InitialSearchEngines {
engines.filter { !$0.id.excludedFromOnboarding(for: locale) }
}

static let braveSearchDefaultRegions = [
let braveSearchDefaultRegions = [
"US", "CA", "GB", "FR", "DE", "AD", "AT", "ES", "MX", "BR", "AR", "IN", "IT",
]
static let yandexDefaultRegions = ["AM", "AZ", "BY", "KG", "KZ", "MD", "RU", "TJ", "TM", "TZ"]
static let ecosiaEnabledRegions = [
let yandexDefaultRegions = ["AM", "AZ", "BY", "KG", "KZ", "MD", "RU", "TJ", "TM", "TZ"]
let ecosiaEnabledRegions = [
"AT", "AU", "BE", "CA", "DK", "ES", "FI", "GR", "HU", "IT",
"LU", "NO", "PT", "US", "GB", "FR", "DE", "NL", "CH", "SE", "IE",
]
static let naverDefaultRegions = ["KR"]
static let daumEnabledRegions = ["KR"]
let naverDefaultRegions = ["KR"]
let daumEnabledRegions = ["KR"]

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

public var isBraveSearchDefaultRegion: Bool {
guard let regionID = locale.region?.identifier ?? Locale.current.region?.identifier else {
return false
}

return braveSearchDefaultRegions.contains(regionID)
}

init(locale: Locale = .current) {
self.locale = locale

Expand Down Expand Up @@ -147,25 +155,25 @@ class InitialSearchEngines {
// MARK: - Locale overrides

private func regionOverrides() {
guard let region = locale.regionCode else { return }
guard let region = locale.region?.identifier else { return }

if Self.yandexDefaultRegions.contains(region) {
if yandexDefaultRegions.contains(region) {
defaultSearchEngine = .yandex
}

if Self.ecosiaEnabledRegions.contains(region) {
if ecosiaEnabledRegions.contains(region) {
replaceOrInsert(engineId: .ecosia, customId: nil)
}

if Self.braveSearchDefaultRegions.contains(region) {
if braveSearchDefaultRegions.contains(region) {
defaultSearchEngine = .braveSearch
}

if Self.naverDefaultRegions.contains(region) {
if naverDefaultRegions.contains(region) {
defaultSearchEngine = .naver
}

if Self.daumEnabledRegions.contains(region) {
if daumEnabledRegions.contains(region) {
replaceOrInsert(engineId: .daum, customId: nil)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ public class SearchEngines {
private let initialSearchEngines: InitialSearchEngines
private let locale: Locale

public var isBraveSearchDefaultRegion: Bool {
return initialSearchEngines.isBraveSearchDefaultRegion
}

public init(locale: Locale = .current) {
initialSearchEngines = InitialSearchEngines(locale: locale)

self.locale = locale
self.disabledEngineNames = getDisabledEngineNames()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,12 @@ public class SearchViewController: SiteTableViewController, LoaderListener {
layoutSuggestionsOptInPrompt()
}

func setSearchQuery(query: String) {
func setSearchQuery(query: String, showSearchSuggestions: Bool = true) {
dataSource.searchQuery = query
dataSource.querySuggestClient()
// Do not query suggestions if the text entred is suspicious
if showSearchSuggestions {
dataSource.querySuggestClient()
}
}

private func reloadSearchEngines() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class WeakTabManagerDelegate {
// TabManager must extend NSObjectProtocol in order to implement WKNavigationDelegate
class TabManager: NSObject {
fileprivate var delegates = [WeakTabManagerDelegate]()
fileprivate let tabEventHandlers: [TabEventHandler]
weak var stateDelegate: TabManagerStateDelegate?

/// Internal url to access the new tab page.
Expand Down Expand Up @@ -131,7 +130,6 @@ class TabManager: NSObject {
self.tabGeneratorAPI = tabGeneratorAPI
self.historyAPI = historyAPI
self.privateBrowsingManager = privateBrowsingManager
self.tabEventHandlers = TabEventHandlers.create(with: prefs)
super.init()

self.navDelegate.tabManager = self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ class TopToolbarView: UIView, ToolbarProtocol {
}
}

var locationLastReplacement: String {
locationTextField?.lastReplacement ?? ""
}

// MARK: Views

private var locationTextField: AutocompleteTextField?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ extension Preferences {

final public class Search {
/// Whether or not to show suggestions while the user types
static let showSuggestions = Option<Bool>(key: "search.show-suggestions", default: false)
public static let showSuggestions = Option<Bool>(key: "search.show-suggestions", default: false)
/// If the user should see the show suggetsions opt-in
static let shouldShowSuggestionsOptIn = Option<Bool>(
public static let shouldShowSuggestionsOptIn = Option<Bool>(
key: "search.show-suggestions-opt-in",
default: true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class AutocompleteTextField: UITextField, UITextFieldDelegate {
// in touchesEnd() (eg. applyCompletion() is called or not)
fileprivate var notifyTextChanged: (() -> Void)?
fileprivate var notifyTextDeleted: (() -> Void)?
private var lastReplacement: String?
public var lastReplacement: String?

var highlightColor = AutocompleteTextFieldUX.highlightColor

Expand Down
17 changes: 0 additions & 17 deletions ios/brave-ios/Sources/Brave/Helpers/TabEventHandlers.swift

This file was deleted.

184 changes: 184 additions & 0 deletions ios/brave-ios/Sources/Brave/Helpers/URLBarHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import Foundation
import UIKit

class URLBarHelper {

static let shared = URLBarHelper()

func shouldShowSearchSuggestions(using lastReplacement: String) async -> Bool {
// Check if last entry to url textfield needs to be checked as suspicious.
// The reason of checking count is bigger than 1 is the single character
// entries will always be safe and only way to achieve multi character entry is
// using paste board.
// This check also allow us to handle paste permission case
guard lastReplacement.count > 1 else {
return true
}

// Check if paste board has any text to guarantee the case
guard UIPasteboard.general.hasStrings || UIPasteboard.general.hasURLs else {
return true
}

// Perform check on pasted text
if let pasteboardContents = UIPasteboard.general.string {
let isSuspicious = await isSuspiciousQuery(pasteboardContents)
return !isSuspicious
}

return true
}

/// Whether the desired text should allow search suggestions to appear when it is copied
/// - Parameter query: Search query copied
/// - Returns: the result if it is suspicious
func isSuspiciousQuery(_ query: String) async -> Bool {
// Remove the msg if the query is too long
if query.count > 50 {
return true
}

// Remove the msg if the query contains more than 7 words
if query.components(separatedBy: " ").count > 7 {
return true
}

// Remove the msg if the query contains a number longer than 7 digits
if let _ = checkForLongNumber(query, 7) {
return true
}

// Remove if email (exact), even if not totally well formed
if checkForEmail(query) {
return true
}

// Remove if query looks like an http pass
if query.range(of: "[^:]+:[^@]+@", options: .regularExpression) != nil {
return true
}

for word in query.components(separatedBy: " ") {
if word.range(of: "[^:]+:[^@]+@", options: .regularExpression) != nil {
return true
}
}

if query.count > 12 {
let literalsPattern = "[^A-Za-z0-9]"

guard
let literalsRegex = try? NSRegularExpression(
pattern: literalsPattern,
options: .caseInsensitive
)
else {
return true
}

let range = NSRange(location: 0, length: query.utf16.count)

let cquery = literalsRegex.stringByReplacingMatches(
in: query,
options: [],
range: range,
withTemplate: ""
)

if cquery.count > 12 {
let pp = isHashProb(cquery)
// we are a bit more strict here because the query
// can have parts well formed
if pp < URLBarHelperConstants.probHashThreshold * 1.5 {
return true
}
}
}

return false
}

private func checkForLongNumber(_ str: String, _ maxNumberLength: Int) -> String? {
let controlString = str.replacingOccurrences(
of: "[^A-Za-z0-9]",
with: "",
options: .regularExpression
)

var location = 0
var maxLocation = 0
var maxLocationPosition: String.Index? = nil

for i in controlString.indices {
if controlString[i] >= "0" && controlString[i] <= "9" {
location += 1
} else {
if location > maxLocation {
maxLocation = location
maxLocationPosition = i
}

location = 0
}
}

if location > maxLocation {
maxLocation = location
maxLocationPosition = controlString.endIndex
}

if let maxLocationPosition = maxLocationPosition, maxLocation > maxNumberLength {
let start = controlString.index(maxLocationPosition, offsetBy: -maxLocation)
let end = maxLocationPosition

return String(controlString[start..<end])
} else {
return nil
}
}

private func checkForEmail(_ str: String) -> Bool {
let emailPattern = "[a-z0-9\\-_@]+(@|%40|%(25)+40)[a-z0-9\\-_]+\\.[a-z0-9\\-_]"

guard
let emailRegex = try? NSRegularExpression(pattern: emailPattern, options: .caseInsensitive)
else {
return false
}

let range = NSRange(location: 0, length: str.utf16.count)
return emailRegex.firstMatch(in: str, options: [], range: range) != nil
}

private func isHashProb(_ str: String) -> Double {
var logProb = 0.0
var transC = 0
let filteredStr = str.replacingOccurrences(
of: "[^A-Za-z0-9]",
with: "",
options: .regularExpression
)

let characters = Array(filteredStr)
for i in 0..<(characters.count - 1) {
if let pos1 = URLBarHelperConstants.probHashChars[characters[i]],
let pos2 = URLBarHelperConstants.probHashChars[characters[i + 1]]
{
logProb += URLBarHelperConstants.probHashLogM[pos1][pos2]
transC += 1
}
}

if transC > 0 {
return exp(logProb / Double(transC))
} else {
return exp(logProb)
}
}

}
Loading
Loading