Skip to content

Commit

Permalink
Merge pull request #24376 from brave/ios/enhancement/search-suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
soner-yuksel authored Aug 14, 2024
2 parents fa1e937 + 65c4e09 commit 3ce97f8
Show file tree
Hide file tree
Showing 19 changed files with 1,538 additions and 38 deletions.
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
2 changes: 0 additions & 2 deletions ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift
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
4 changes: 2 additions & 2 deletions ios/brave-ios/Sources/Brave/Frontend/ClientPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,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

0 comments on commit 3ce97f8

Please sign in to comment.