Skip to content

Commit

Permalink
Add tabs to suggestions/autocomplete on iOS (#3371)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/392891325557410/1208036752520497/f
Tech Design URL:
CC:

**Description**:
Adds open tabs to suggestions.

I'm going to follow up with localisations and UI tests in a separate PR
as I want to get this thing merged with BSK and can update iOS internal
easily on its own.

**Steps to test this PR**:
1. Open a bunch of sites and bookmark some of them (quick way to do this
is via Hacker News Ycombinator then go through and open the first 5
sites in the background and use tab switcher to bookmark them all, then
go back and open a few more from the list)
2. Open a new tab and type until you get a suggestion for an open tab
3. Switching to the open tab should close the new tab
4. Matches that have bookmarks and tabs should show both, but history
should not be shown in these cases
5. Edit a bookmark so that its title starts with quotes and is something
different to its URL. Check that bookmark is still suggested based on
the first word in the title
6. Close all tabs (not fire) and type until you see history for sites
that have not been bookmarked
7. Use the fire button
8. Typing should not show any history but should show matching bookmarks

**Definition of Done (Internal Only)**:

* [ ] Does this PR satisfy our [Definition of
Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?

**Copy Testing**:

* [x] Use of correct apostrophes in new copy, ie `’` rather than `'`

**Orientation Testing**:

* [x] Portrait
* [ ] Landscape

**Device Testing**:

* [x] iPhone SE (1st Gen)
* [x] iPhone 8
* [x] iPhone X
* [x] iPhone 14 Pro
* [x] iPad

**OS Testing**:

* [x] iOS 15
* [x] iOS 16
* [x] iOS 17

**Theme Testing**:

* [x] Light theme
* [x] Dark theme

---
###### Internal references:
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
  • Loading branch information
brindy authored Sep 22, 2024
1 parent 92da431 commit 3845520
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 68 deletions.
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)
}

}
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
4 changes: 4 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,11 @@ extension Pixel {
case autocompleteClickFavorite
case autocompleteClickSearchHistory
case autocompleteClickSiteHistory
case autocompleteClickOpenTab
case autocompleteDisplayedLocalBookmark
case autocompleteDisplayedLocalFavorite
case autocompleteDisplayedLocalHistory
case autocompleteDisplayedOpenedTab
case autocompleteSwipeToDelete

case feedbackPositive
Expand Down Expand Up @@ -946,9 +948,11 @@ extension Pixel.Event {
case .autocompleteClickFavorite: return "m_autocomplete_click_favorite"
case .autocompleteClickSearchHistory: return "m_autocomplete_click_history_search"
case .autocompleteClickSiteHistory: return "m_autocomplete_click_history_site"
case .autocompleteClickOpenTab: return "m_autocomplete_click_switch_to_tab"
case .autocompleteDisplayedLocalBookmark: return "m_autocomplete_display_local_bookmark"
case .autocompleteDisplayedLocalFavorite: return "m_autocomplete_display_local_favorite"
case .autocompleteDisplayedLocalHistory: return "m_autocomplete_display_local_history"
case .autocompleteDisplayedOpenedTab: return "m_autocomplete_display_switch_to_tab"
case .autocompleteSwipeToDelete: return "m_autocomplete_result_deleted"

case .feedbackPositive: return "mfbs_positive_submit"
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10927,7 +10927,7 @@
repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit";
requirement = {
kind = exactVersion;
version = 197.0.0;
version = 198.0.0;
};
};
9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "40f2fcc23944e028e16798a784ceff7e24ba6683",
"version" : "197.0.0"
"revision" : "6e1520bd83bbcc269b0d561c51fc92b81fe6d93b",
"version" : "198.0.0"
}
},
{
Expand Down Expand Up @@ -138,7 +138,7 @@
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b",
"version" : "1.4.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "OpenTab-24.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions DuckDuckGo/AutocompleteView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ private struct SuggestionView: View {
title: title ?? "",
subtitle: url.formattedForSuggestion())

case .openTab(title: let title, url: let url):
SuggestionListItem(icon: Image("OpenTab-24"),
title: title,
subtitle: "\(UserText.autocompleteSwitchToTab) · \(url.formattedForSuggestion())")

case .internalPage, .unknown:
FailedAssertionView("Unknown or unsupported suggestion type")
}
Expand Down Expand Up @@ -336,6 +341,7 @@ private extension URL {
let string = absoluteString
.dropping(prefix: "https://")
.dropping(prefix: "http://")
.droppingWwwPrefix()
return pathComponents.isEmpty ? string : string.dropping(suffix: "/")
}

Expand Down
51 changes: 47 additions & 4 deletions DuckDuckGo/AutocompleteViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,33 @@ class AutocompleteViewController: UIHostingController<AutocompleteView> {
CachedBookmarks(bookmarksDatabase)
}()

private lazy var openTabs: [BrowserTab] = {
tabsModel.tabs.compactMap {
guard let url = $0.link?.url else { return nil }
return OpenTab(title: $0.link?.displayTitle ?? "", url: url)
}
}()

private var lastResults: SuggestionResult?
private var loader: SuggestionLoader?

private var historyMessageManager: HistoryMessageManager
private var tabsModel: TabsModel
private var featureFlagger: FeatureFlagger

init(historyManager: HistoryManaging,
bookmarksDatabase: CoreDataDatabase,
appSettings: AppSettings,
historyMessageManager: HistoryMessageManager = HistoryMessageManager()) {
historyMessageManager: HistoryMessageManager = HistoryMessageManager(),
tabsModel: TabsModel,
featureFlagger: FeatureFlagger) {

self.tabsModel = tabsModel
self.historyManager = historyManager
self.bookmarksDatabase = bookmarksDatabase
self.appSettings = appSettings
self.historyMessageManager = historyMessageManager
self.featureFlagger = featureFlagger

self.model = AutocompleteViewModel(isAddressBarAtBottom: appSettings.currentAddressBarPosition == .bottom,
showMessage: historyManager.isHistoryFeatureEnabled() && historyMessageManager.shouldShow())
super.init(rootView: AutocompleteView(model: model))
Expand Down Expand Up @@ -119,6 +133,7 @@ class AutocompleteViewController: UIHostingController<AutocompleteView> {
var bookmark = false
var favorite = false
var history = false
var openTab = false

lastResults?.all.forEach {
switch $0 {
Expand All @@ -132,6 +147,9 @@ class AutocompleteViewController: UIHostingController<AutocompleteView> {
case .historyEntry:
history = true

case .openTab:
openTab = true

default: break
}
}
Expand All @@ -148,6 +166,10 @@ class AutocompleteViewController: UIHostingController<AutocompleteView> {
Pixel.fire(pixel: .autocompleteDisplayedLocalHistory)
}

if openTab {
Pixel.fire(pixel: .autocompleteDisplayedOpenedTab)
}

}

private func cancelInFlightRequests() {
Expand All @@ -158,7 +180,7 @@ class AutocompleteViewController: UIHostingController<AutocompleteView> {
private func requestSuggestions(query: String) {
model.selection = nil

loader = SuggestionLoader(dataSource: self, urlFactory: { phrase in
loader = SuggestionLoader(urlFactory: { phrase in
guard let url = URL(trimmedAddressBarString: phrase),
let scheme = url.scheme,
scheme.description.hasPrefix("http"),
Expand All @@ -169,7 +191,7 @@ class AutocompleteViewController: UIHostingController<AutocompleteView> {
return url
})

loader?.getSuggestions(query: query) { [weak self] result, error in
loader?.getSuggestions(query: query, usingDataSource: self) { [weak self] result, error in
guard let self, error == nil else { return }
let updatedResults = result ?? .empty
self.lastResults = updatedResults
Expand Down Expand Up @@ -228,6 +250,9 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate {
case .website:
Pixel.fire(pixel: .autocompleteClickWebsite)

case .openTab:
Pixel.fire(pixel: .autocompleteClickOpenTab)

default:
// NO-OP
break
Expand Down Expand Up @@ -259,6 +284,10 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate {

extension AutocompleteViewController: SuggestionLoadingDataSource {

var platform: Platform {
.mobile
}

func history(for suggestionLoading: Suggestions.SuggestionLoading) -> [HistorySuggestion] {
return historyCoordinator.history ?? []
}
Expand All @@ -271,6 +300,13 @@ extension AutocompleteViewController: SuggestionLoadingDataSource {
return []
}

func openTabs(for suggestionLoading: any SuggestionLoading) -> [BrowserTab] {
if featureFlagger.isFeatureOn(.autcompleteTabs) {
return openTabs
}
return []
}

func suggestionLoading(_ suggestionLoading: Suggestions.SuggestionLoading, suggestionDataFromUrl url: URL, withParameters parameters: [String: String], completion: @escaping (Data?, Error?) -> Void) {
var queryURL = url
parameters.forEach {
Expand Down Expand Up @@ -298,3 +334,10 @@ extension HistoryEntry: HistorySuggestion {
}

}

struct OpenTab: BrowserTab {

let title: String
let url: URL

}
Loading

0 comments on commit 3845520

Please sign in to comment.