Skip to content

Commit

Permalink
Add search by title to bookmark panel UI (#2969)
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira committed Jul 23, 2024
1 parent 52dfd2e commit 13e8377
Show file tree
Hide file tree
Showing 23 changed files with 498 additions and 150 deletions.
18 changes: 6 additions & 12 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2532,11 +2532,9 @@
B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; };
B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; };
B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; };
BB1597BB2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1597BA2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift */; };
BB1597BC2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1597BA2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift */; };
BB1597C02C35B666001FB9B5 /* BookmarkSearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1597BD2C35B60A001FB9B5 /* BookmarkSearchViewModelTests.swift */; };
BB1597C12C35B667001FB9B5 /* BookmarkSearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1597BD2C35B60A001FB9B5 /* BookmarkSearchViewModelTests.swift */; };
BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */; };
BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; };
BBB881892C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; };
BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; };
BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; };
BD384AC92BBC821A00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; };
Expand Down Expand Up @@ -4212,9 +4210,8 @@
B6F9BDE32B45CD1900677B33 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = "<group>"; };
B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = "<group>"; };
B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = "<group>"; };
BB1597BA2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchViewModel.swift; sourceTree = "<group>"; };
BB1597BD2C35B60A001FB9B5 /* BookmarkSearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchViewModelTests.swift; sourceTree = "<group>"; };
BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = "<group>"; };
BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerSearchDataSource.swift; sourceTree = "<group>"; };
BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = "<group>"; };
BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = "<group>"; };
BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6657,7 +6654,6 @@
9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */,
9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */,
9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */,
BB1597BD2C35B60A001FB9B5 /* BookmarkSearchViewModelTests.swift */,
);
path = ViewModels;
sourceTree = "<group>";
Expand Down Expand Up @@ -7483,7 +7479,6 @@
9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */,
9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */,
9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */,
BB1597BA2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
Expand Down Expand Up @@ -7593,6 +7588,7 @@
379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */,
B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */,
9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */,
BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -10072,6 +10068,7 @@
3706FB22293F65D500E42796 /* NSTextViewExtension.swift in Sources */,
3706FB23293F65D500E42796 /* DownloadsCellView.swift in Sources */,
3706FB25293F65D500E42796 /* PublishedAfter.swift in Sources */,
BBB881892C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */,
3706FEC1293F6EFF00E42796 /* BWCredential.swift in Sources */,
3706FEC9293F6F7500E42796 /* BWManagement.swift in Sources */,
3706FB27293F65D500E42796 /* DeviceAuthenticationService.swift in Sources */,
Expand All @@ -10095,7 +10092,6 @@
3706FB35293F65D500E42796 /* FlatButton.swift in Sources */,
3706FB36293F65D500E42796 /* PinnedTabView.swift in Sources */,
3706FB37293F65D500E42796 /* DataEncryption.swift in Sources */,
BB1597BC2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift in Sources */,
56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */,
4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */,
37197EA82942443D00394917 /* BrowserTabViewController.swift in Sources */,
Expand Down Expand Up @@ -10794,7 +10790,6 @@
4BBEE8DE2BFEDE3E00E5E111 /* SurveyRemoteMessageTests.swift in Sources */,
562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */,
3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */,
BB1597C12C35B667001FB9B5 /* BookmarkSearchViewModelTests.swift in Sources */,
3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */,
37DB56F02C3B31CD0093D4DC /* MockRemoteMessagingAvailabilityProvider.swift in Sources */,
1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */,
Expand Down Expand Up @@ -11409,7 +11404,6 @@
B6676BE12AA986A700525A21 /* AddressBarTextEditor.swift in Sources */,
B69B503B2726A12500758A2B /* Atb.swift in Sources */,
37A6A8F12AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */,
BB1597BB2C35AF00001FB9B5 /* BookmarkSearchViewModel.swift in Sources */,
7BEC20452B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift in Sources */,
B6C0BB6A29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */,
B6B1E88026D5DA9B0062C350 /* DownloadsViewController.swift in Sources */,
Expand Down Expand Up @@ -11647,6 +11641,7 @@
B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */,
AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */,
B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */,
BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */,
C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */,
B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */,
AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */,
Expand Down Expand Up @@ -12405,7 +12400,6 @@
317295D22AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */,
B6C843DA2BA1CAB6006FDEC3 /* FilePresenterTests.swift in Sources */,
B693956326F1C2A40015B914 /* FileDownloadManagerMock.swift in Sources */,
BB1597C02C35B666001FB9B5 /* BookmarkSearchViewModelTests.swift in Sources */,
B6C2C9EF276081AB005B7F0A /* DeallocationTests.swift in Sources */,
B63ED0D826AE729600A9DAD1 /* PermissionModelTests.swift in Sources */,
B69B504B2726CA2900758A2B /* MockStatisticsStore.swift in Sources */,
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Bookmarks-Search-Empty.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "search_bookmarks.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.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class BookmarkListTreeControllerDataSource: BookmarkTreeControllerDataSour
self.bookmarkManager = bookmarkManager
}

func treeController(treeController: BookmarkTreeController, childNodesFor node: BookmarkNode) -> [BookmarkNode] {
func treeController(childNodesFor node: BookmarkNode) -> [BookmarkNode] {
return node.isRoot ? childNodesForRootNode(node) : childNodes(node)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// BookmarkListTreeControllerSearchDataSource.swift
//
// 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

final class BookmarkListTreeControllerSearchDataSource: BookmarkTreeControllerSearchDataSource {
private let bookmarkManager: BookmarkManager

init(bookmarkManager: BookmarkManager) {
self.bookmarkManager = bookmarkManager
}

func nodes(for searchQuery: String) -> [BookmarkNode] {
let searchResults = bookmarkManager.search(by: searchQuery)

return rebuildChildNodes(for: searchResults)
}

private func rebuildChildNodes(for results: [BaseBookmarkEntity]) -> [BookmarkNode] {
let rootNode = BookmarkNode.genericRootNode()
let nodes = results.compactMap { (item) -> BookmarkNode in
let itemNode = rootNode.createChildNode(item)
itemNode.canHaveChildNodes = false
return itemNode
}

return nodes
}
}
2 changes: 1 addition & 1 deletion DuckDuckGo/Bookmarks/Model/BookmarkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ final class LocalBookmarkManager: BookmarkManager {
while !queue.isEmpty {
let current = queue.removeFirst()

if current.title.lowercased().contains(query) {
if current.title.lowercased().contains(query.lowercased()) {
result.append(current)
}

Expand Down
11 changes: 11 additions & 0 deletions DuckDuckGo/Bookmarks/Model/BookmarkNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ final class BookmarkNode: Hashable {
return false
}

/// Checks if two nodes represent the same base bookmark entity based only on their ID
func representedObjectHasSameId(_ otherRepresentedObject: AnyObject) -> Bool {
if let entity = otherRepresentedObject as? BaseBookmarkEntity,
let nodeEntity = self.representedObject as? BaseBookmarkEntity,
entity.id == nodeEntity.id {
return true
}

return false
}

func findOrCreateChildNode(with representedObject: AnyObject) -> BookmarkNode {
if let node = childNodeRepresenting(object: representedObject) {
return node
Expand Down
35 changes: 31 additions & 4 deletions DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
@Published var selectedFolders: [BookmarkFolder] = []

let treeController: BookmarkTreeController
private(set) var expandedNodesIDs = Set<String>()

private let contentMode: ContentMode
private(set) var expandedNodesIDs = Set<String>()
private(set) var isSearching = false

/// When a drag and drop to a folder happens while in search, we need to stor the destination folder
/// so we can expand the tree to the destination folder once the drop finishes.
private(set) var dragDestinationFolderInSearchMode: BookmarkFolder?

private let bookmarkManager: BookmarkManager
private let showMenuButtonOnHover: Bool
private let onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)?
Expand Down Expand Up @@ -62,9 +68,21 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
}

func reloadData() {
isSearching = false
dragDestinationFolderInSearchMode = nil
setFolderCount()
treeController.rebuild()
}

func reloadData(for searchQuery: String) {
isSearching = true
setFolderCount()
treeController.rebuild(for: searchQuery)
}

private func setFolderCount() {
favoritesPseudoFolder.count = bookmarkManager.list?.favoriteBookmarks.count ?? 0
bookmarksPseudoFolder.count = bookmarkManager.list?.totalBookmarks ?? 0
treeController.rebuild()
}

// MARK: - Private
Expand Down Expand Up @@ -133,7 +151,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
cell.delegate = self

if let bookmark = node.representedObject as? Bookmark {
cell.update(from: bookmark)
cell.update(from: bookmark, isSearch: isSearching)

if bookmark.favicon(.small) == nil {
presentFaviconsFetcherOnboarding?()
Expand All @@ -142,7 +160,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
}

if let folder = node.representedObject as? BookmarkFolder {
cell.update(from: folder)
cell.update(from: folder, isSearch: isSearching)
return cell
}

Expand Down Expand Up @@ -181,6 +199,15 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS
return .none
}

if isSearching {
if let destinationFolder = destinationNode.representedObject as? BookmarkFolder {
self.dragDestinationFolderInSearchMode = destinationFolder
return .move
}

return .none
}

let bookmarks = PasteboardBookmark.pasteboardBookmarks(with: info.draggingPasteboard)
let folders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation

final class BookmarkSidebarTreeController: BookmarkTreeControllerDataSource {

func treeController(treeController: BookmarkTreeController, childNodesFor node: BookmarkNode) -> [BookmarkNode] {
func treeController(childNodesFor node: BookmarkNode) -> [BookmarkNode] {
return node.isRoot ? childNodesForRootNode(node) : childNodes(for: node)
}

Expand Down
44 changes: 31 additions & 13 deletions DuckDuckGo/Bookmarks/Model/BookmarkTreeController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,42 @@ import Foundation

protocol BookmarkTreeControllerDataSource: AnyObject {

func treeController(treeController: BookmarkTreeController, childNodesFor: BookmarkNode) -> [BookmarkNode]
func treeController(childNodesFor: BookmarkNode) -> [BookmarkNode]
}

protocol BookmarkTreeControllerSearchDataSource: AnyObject {

func nodes(for searchQuery: String) -> [BookmarkNode]
}

final class BookmarkTreeController {

let rootNode: BookmarkNode

private weak var dataSource: BookmarkTreeControllerDataSource?
private weak var searchDataSource: BookmarkTreeControllerSearchDataSource?

init(dataSource: BookmarkTreeControllerDataSource, rootNode: BookmarkNode) {
init(dataSource: BookmarkTreeControllerDataSource,
searchDataSource: BookmarkTreeControllerSearchDataSource? = nil,
rootNode: BookmarkNode) {
self.dataSource = dataSource
self.searchDataSource = searchDataSource
self.rootNode = rootNode

rebuild()
}

convenience init(dataSource: BookmarkTreeControllerDataSource) {
self.init(dataSource: dataSource, rootNode: BookmarkNode.genericRootNode())
convenience init(dataSource: BookmarkTreeControllerDataSource,
searchDataSource: BookmarkTreeControllerSearchDataSource? = nil) {
self.init(dataSource: dataSource, searchDataSource: searchDataSource, rootNode: BookmarkNode.genericRootNode())
}

// MARK: - Public

func rebuild(for searchQuery: String) {
rootNode.childNodes = searchDataSource?.nodes(for: searchQuery) ?? []
}

func rebuild() {
rebuildChildNodes(node: rootNode)
}
Expand All @@ -52,21 +65,27 @@ final class BookmarkTreeController {
}

func node(representing object: AnyObject) -> BookmarkNode? {
return nodeInArrayRepresentingObject(nodes: [rootNode], representedObject: object)
return nodeInArrayRepresentingObject(nodes: [rootNode]) { $0.representedObjectEquals(object) }
}

func findNodeInSearchMode(representing object: AnyObject) -> BookmarkNode? {
return nodeInArrayRepresentingObject(nodes: [rootNode]) { $0.representedObjectHasSameId(object) }
}

// MARK: - Private

private func nodeInArrayRepresentingObject(nodes: [BookmarkNode], representedObject: AnyObject) -> BookmarkNode? {
for node in nodes {
if node.representedObjectEquals(representedObject) {
private func nodeInArrayRepresentingObject(nodes: [BookmarkNode], match: (BookmarkNode) -> Bool) -> BookmarkNode? {
var stack: [BookmarkNode] = nodes

while !stack.isEmpty {
let node = stack.removeLast()

if match(node) {
return node
}

if node.canHaveChildNodes {
if let foundNode = nodeInArrayRepresentingObject(nodes: node.childNodes, representedObject: representedObject) {
return foundNode
}
stack.append(contentsOf: node.childNodes)
}
}

Expand All @@ -84,7 +103,7 @@ final class BookmarkTreeController {
return false
}

let childNodes: [BookmarkNode] = dataSource?.treeController(treeController: self, childNodesFor: node) ?? []
let childNodes: [BookmarkNode] = dataSource?.treeController(childNodesFor: node) ?? []
var childNodesDidChange = childNodes != node.childNodes

if childNodesDidChange {
Expand All @@ -99,5 +118,4 @@ final class BookmarkTreeController {

return childNodesDidChange
}

}
Loading

0 comments on commit 13e8377

Please sign in to comment.