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

Add favorites using autocomplete from New Tab Page #3403

Draft
wants to merge 56 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
75192d7
Bing-based favorite search PoC
dus7 Aug 21, 2024
21a3554
Handle errors and allow to define subscription id in debug
dus7 Aug 21, 2024
3f162f9
Update clear icon
dus7 Aug 22, 2024
c84dbc4
Filter results by uniquing TLDPlus1
dus7 Aug 22, 2024
9698f8e
Fetch favicons
dus7 Aug 22, 2024
7b10264
Start editing automatically
dus7 Aug 22, 2024
775a9b7
Improve mock results
dus7 Aug 22, 2024
ef61868
Do not capitalize search input
dus7 Aug 22, 2024
85054cf
Remove hostname from search results
dus7 Aug 22, 2024
1ec1498
Show one line for name
dus7 Aug 22, 2024
84d6526
Add manual entry item
dus7 Aug 22, 2024
db98f61
Add button to add custom url
dus7 Aug 22, 2024
1769e5c
Use autocomplete service for favorite suggestions
dus7 Aug 23, 2024
56b8164
Reduce debounce time
dus7 Aug 26, 2024
4b4d618
Fix manual entry validation
dus7 Aug 26, 2024
0d2ad04
Use LinkPresentation framework
dus7 Aug 26, 2024
bb7b2e8
Add a favorite autocompletion PoC button
dus7 Aug 27, 2024
eebb472
Use final url for favorite
dus7 Aug 28, 2024
3449269
Remove Bing search
dus7 Sep 9, 2024
cd6292f
Move related files to separate group
dus7 Sep 9, 2024
d95ae4c
Remove fake search service
dus7 Sep 9, 2024
235bc6c
Use URLComponents to convert search into URL
dus7 Sep 9, 2024
476d9ce
Remove debug settings transition
dus7 Sep 9, 2024
8ddf3aa
Rename view model
dus7 Sep 9, 2024
5a0b66d
Remove MockWebsiteSearch
dus7 Sep 9, 2024
1a512c6
Improve converting to URL
dus7 Sep 9, 2024
e75c458
Refactor AddFavoriteViewModel
dus7 Sep 9, 2024
56a8e57
Keep view model instance between view updates
dus7 Sep 9, 2024
737db33
Do not decorate results during search
dus7 Sep 20, 2024
62a3c66
Remove favorites header
dus7 Sep 23, 2024
277f437
Remove FavoritesEmptyStateModel and FavoritesViewModel abstraction
dus7 Sep 23, 2024
b5d46e0
Add tests for prefixing and adding add item
dus7 Sep 23, 2024
390354c
Fix tests
dus7 Sep 23, 2024
d8337fa
Remove intermediate value
dus7 Sep 24, 2024
77309af
Merge branch 'mariusz/ntp-remove-favorites-header' into mariusz/ntp-f…
dus7 Sep 24, 2024
924b714
Move NTP assets
dus7 Sep 25, 2024
2244aa6
Deprecate existing SecondaryButtonStyle
dus7 Sep 25, 2024
3099c15
Update Add Favorites pictogram
dus7 Sep 25, 2024
c0182e5
Update BSK reference
dus7 Sep 26, 2024
de7a86f
Define SecondaryButtonStyle according to StyleGuide
dus7 Sep 26, 2024
cb63042
Allow to reuse primary button colors
dus7 Sep 26, 2024
103ba1a
Add convenience initializer for Bookmarks view model
dus7 Sep 26, 2024
ad42a9e
Create result items for favorite autocompletion
dus7 Sep 26, 2024
e149eb7
Fix continuation misuse in autocomplete website search
dus7 Sep 26, 2024
7781794
Add possibility to manually create Bookmarks in existing edit controller
dus7 Sep 26, 2024
3a12069
Automatically cleanup favicon loader tasks with empty favicons
dus7 Sep 26, 2024
667122f
Add segue to favorite creation
dus7 Sep 26, 2024
d67f921
Adjust Add Favorite flow to most recent design
dus7 Sep 26, 2024
95d91d3
Merge branch 'main' into mariusz/ntp-favorites-autocomplete
dus7 Sep 26, 2024
5f89b77
Remove unused code
dus7 Sep 26, 2024
78316f8
Pre-populate title instead of URL when using manual favorite entry
dus7 Sep 27, 2024
9ae0de5
Fix favicon background in Bookmark Editor
dus7 Sep 27, 2024
2a4228d
Sanitize url input when adding custom bookmark
dus7 Sep 27, 2024
6f07607
Unify logic for adding favorite from autocomplete
dus7 Sep 27, 2024
9fdd3fc
Make addFavorite function async
dus7 Sep 27, 2024
e57dda5
Reorganize search task
dus7 Sep 27, 2024
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
28 changes: 24 additions & 4 deletions Core/BookmarksModelsErrorHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,42 @@ public extension BookmarkEditorViewModel {
convenience init(editingEntityID: NSManagedObjectID,
bookmarksDatabase: CoreDataDatabase,
favoritesDisplayMode: FavoritesDisplayMode,
syncService: DDGSyncing?) {
syncService: DDGSyncing?,
sanitization: BookmarkSanitization? = nil) {
self.init(editingEntityID: editingEntityID,
bookmarksDatabase: bookmarksDatabase,
favoritesDisplayMode: favoritesDisplayMode,
errorEvents: BookmarksModelsErrorHandling(syncService: syncService))
errorEvents: BookmarksModelsErrorHandling(syncService: syncService),
sanitization: sanitization)

}

convenience init(creatingFolderWithParentID parentFolderID: NSManagedObjectID?,
bookmarksDatabase: CoreDataDatabase,
favoritesDisplayMode: FavoritesDisplayMode,
syncService: DDGSyncing?) {
syncService: DDGSyncing?,
sanitization: BookmarkSanitization? = nil) {
self.init(creatingFolderWithParentID: parentFolderID,
bookmarksDatabase: bookmarksDatabase,
favoritesDisplayMode: favoritesDisplayMode,
errorEvents: BookmarksModelsErrorHandling(syncService: syncService))
errorEvents: BookmarksModelsErrorHandling(syncService: syncService),
sanitization: sanitization)
}

convenience init(addingBookmarkWith url: String,
title: String,
toFolderWithID folderID: NSManagedObjectID?,
bookmarksDatabase: CoreDataDatabase,
favoritesDisplayMode: FavoritesDisplayMode,
syncService: DDGSyncing?,
sanitization: BookmarkSanitization? = nil) {
self.init(addingBookmarkWith: url,
title: title,
toFolderWithID: folderID,
bookmarksDatabase: bookmarksDatabase,
favoritesDisplayMode: favoritesDisplayMode,
errorEvents: BookmarksModelsErrorHandling(syncService: syncService),
sanitization: sanitization)
}
}

Expand Down
3 changes: 1 addition & 2 deletions Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public struct UserDefaultsWrapper<T> {
// Debug keys
case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled"
case debugOnboardingHighlightsEnabledKey = "com.duckduckgo.ios.debug.onboardingHighlightsEnabled"

// Duck Player Pixel Experiment
case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed.v2"
case duckPlayerPixelExperimentCohort = "com.duckduckgo.ios.duckplayer.pixel.experiment.cohort.v2"
Expand All @@ -179,7 +179,6 @@ public struct UserDefaultsWrapper<T> {
case duckPlayerPixelExperimentLastDayPixelFired = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.day.pixel.fired.v2"
case duckPlayerPixelExperimentLastVideoIDRendered = "com.duckduckgo.ios.duckplayer.pixel.experiment.last.videoID.rendered.v2"
case duckPlayerPixelExperimentOverride = "com.duckduckgo.ios.duckplayer.pixel.experiment.override.v2"

}

private let key: Key
Expand Down
66 changes: 55 additions & 11 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

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" : "5b59c2790a7f7c69bf1f6793152bdb4ea344b1b4",
"version" : "198.1.1"
"branch" : "mariusz/ntp-favorites-autocomplete",
"revision" : "31505ed55ad8524b0225583253fd15b08e74124c"
}
},
{
Expand Down
58 changes: 58 additions & 0 deletions DuckDuckGo/AddFavoriteURLMatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// AddFavoriteURLMatcher.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
import Common
import Core

enum AddFavoriteURLMatch: Hashable {
case exactFavorite
case exactBookmark
case partialFavorite(matchedURL: URL)
case partialBookmark(matchedURL: URL)
}

protocol AddFavoriteURLMatching {
@MainActor
func favoriteMatch(for url: URL) -> AddFavoriteURLMatch?
}

struct AddFavoriteURLMatcher: AddFavoriteURLMatching {
let bookmarksSearch: BookmarksStringSearch

@MainActor
func favoriteMatch(for url: URL) -> AddFavoriteURLMatch? {
let searchTerm = url.nakedString ?? url.absoluteString

let results = bookmarksSearch.search(query: searchTerm)
guard !results.isEmpty else {
return nil
}

if let match = results.first(where: { $0.url.absoluteString == url.absoluteString }) {
return match.isFavorite ? .exactFavorite : .exactBookmark
}

if let match = results.first(where: { $0.url.absoluteString.contains(searchTerm) }) {
return match.isFavorite ? .partialFavorite(matchedURL: match.url) : .partialBookmark(matchedURL: match.url)
}

return nil
}
}
171 changes: 171 additions & 0 deletions DuckDuckGo/AddFavoriteView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//
// AddFavoriteView.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 SwiftUI
import Bookmarks
import Combine
import DuckUI
import Core

struct AddFavoriteView: View {
@Environment(\.dismiss) var dismiss

@ObservedObject private(set) var viewModel: AddFavoriteViewModel

@FocusState private var isFocused: Bool

var body: some View {
VStack(alignment: .center, spacing: 8) {

Group {
headerView
searchInputField
.padding(.top, 24)
}
.padding(.horizontal, Metrics.horizontalPadding)

if !viewModel.searchTerm.isEmpty {
searchList
.applyBackground()
.padding(24)

customEntryButton
}

Spacer()

}
.background(Color(designSystemColor: .background))
.frame(maxWidth: .infinity)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button(role: .cancel) {
dismiss()
} label: {
Text(verbatim: "Cancel")
}
}
}
.tintIfAvailable(Color(designSystemColor: .textPrimary))
.onAppear {
isFocused = true
}
.onDisappear {
viewModel.clear()
}
}

private var customEntryButton: some View {
Button {
viewModel.addCustomWebsite()
} label: {
Text(UserText.addFavoriteCustomWebsiteButtonTitle)
}
.buttonStyle(SecondaryButtonStyle(compact: true, fullWidth: false))
}

@ViewBuilder
private var searchList: some View {
LazyVStack {
if !viewModel.results.isEmpty {
ForEach(viewModel.results) { result in
Button {
Task {
await viewModel.addFavorite(for: result)
}
dismiss()
} label: {
FavoriteSearchResultItemView(result: result, isDisabled: !result.isActionable)
}
.disabled(!result.isActionable)
}
} else if viewModel.wasSearchCompleted {
FavoriteNoResultsItemView()
.disabled(true)
}
}
}

private var searchInputField: some View {
HStack(spacing: 8) {
Group {
TextField(text: $viewModel.searchTerm) {
Text(UserText.addFavoriteSearchPlaceholder)
}
.foregroundStyle(Color(designSystemColor: .textPrimary))
.daxBodyRegular()
.focused($isFocused)
.textInputAutocapitalization(.never)
.keyboardType(.URL)
.autocorrectionDisabled()
.textFieldStyle(.automatic)

if !viewModel.searchTerm.isEmpty {
Button {
viewModel.clear()
} label: {
Image(.clear16)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(width: 16)
}
}
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
}
.background {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.inset(by: 1)
.stroke(Color(designSystemColor: .accent), lineWidth: 2)
.foregroundStyle(Color(designSystemColor: .panel))
}
}

@ViewBuilder
private var headerView: some View {
Image(.favoritesAdd128)

Text(UserText.addFavoriteHeader)
.daxTitle2()
.multilineTextAlignment(.center)
.foregroundStyle(Color(designSystemColor: .textPrimary))

Text(UserText.addFavoriteSubheader)
.daxBodyRegular()
.multilineTextAlignment(.center)
.foregroundStyle(Color(designSystemColor: .textSecondary))
}

private struct Metrics {
static let horizontalPadding: CGFloat = 24
static let listHorizontalPadding: CGFloat = 8
}
}

#Preview {
AddFavoriteView(viewModel: .preview)
}

struct PreviewBookmarksSearch: BookmarksStringSearch {
let hasData: Bool = false
func search(query: String) -> [any BookmarksStringSearchResult] {
[]
}
}
Loading
Loading