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

Sync form factor specific favorites #511

Merged
merged 61 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
df433e7
Add to-many relationship for favorites folders
ayoy Sep 8, 2023
214da43
Update addToFavorites API usage
ayoy Sep 8, 2023
7d9e0ed
Update BookmarkTree and fix unit tests to support form factor specifi…
ayoy Sep 8, 2023
4f4404e
Make FavoritesConfiguration conform to Equatable and make its api public
ayoy Sep 8, 2023
1c45a40
Fix favorites syncable representation
ayoy Sep 8, 2023
0e83557
Fix FavoritesConfiguration.folderUUIDs
ayoy Sep 9, 2023
0e46410
Fix favorites folders in MenuBookmarksViewModel
ayoy Sep 9, 2023
4ea3455
Fix populating favorites in Sync response
ayoy Sep 9, 2023
2400000
Rename FavoritesConfiguration to FavoritesDisplayMode
ayoy Sep 9, 2023
d873593
Fix adding to favorites on iOS
ayoy Sep 10, 2023
f33013c
Fix handling favorites in sync response handler
ayoy Sep 10, 2023
bad2850
Make favorites display mode public and modifiable in FavoriteListView…
ayoy Sep 10, 2023
bb08514
Make favorites display mode public and modifiable in MenuBookmarksVie…
ayoy Sep 10, 2023
547da01
Remove favoritesDisplayMode from initializers in FavoritesListViewMod…
ayoy Sep 10, 2023
161088d
Add favoritesDisplayMode to FavoritesListInteracting and MenuBookmark…
ayoy Sep 10, 2023
bb12fb7
Set renaming identifier for favoriteFolders
ayoy Sep 11, 2023
b4d5db3
Remove renamingIdentifier from BookmarkEntity
ayoy Sep 11, 2023
21029fc
Add a helper function to migrate to form factor specific favorites
ayoy Sep 11, 2023
e7db707
Add UserDefaultsSyncHandler for syncing settings stored in UserDefaults
ayoy Sep 14, 2023
4643781
Add SettingSyncHandler class as a base class for all handlers for syn…
ayoy Sep 15, 2023
08b1419
Add FavoritesDisplayMode.isDisplayAll
ayoy Sep 15, 2023
b5ed45b
Add BookmarkUtils.fetchFavoritesFoldersForUnfavoriting
ayoy Sep 15, 2023
d27a0ff
Move unfavoriting folders logic to BookmarkEntity
ayoy Sep 15, 2023
06f52b4
Add BookmarkUtils.favoritesFoldersForUnfavoriting and keep BookmarkEn…
ayoy Sep 18, 2023
20ac7bd
Rename FavoritesPlatform with FavoritesFolderID and remove favorites …
ayoy Sep 18, 2023
8f4f67f
Add display mode related API to BookmarkEntity extension
ayoy Sep 18, 2023
cb8ef49
Remove unneeded code
ayoy Sep 18, 2023
6bd32a3
Rename FavoritesFolderID all to unified
ayoy Sep 18, 2023
ee5b87d
Remove FavoriteListViewModel.favoritesFolder
ayoy Sep 19, 2023
1c38686
Restore favoriteFolder in FavoriteListViewModel
ayoy Sep 19, 2023
669537f
Update SettingsProviderTests with generic settings test cases
ayoy Sep 19, 2023
8cddf22
Update SettingsInitialSyncResponseHandlerTests with generic settings …
ayoy Sep 19, 2023
2de2631
Update SettingsRegularSyncResponseHandlerTests with generic settings …
ayoy Sep 19, 2023
e15e04f
Update Bookmarks Sync Data Provider tests
ayoy Sep 19, 2023
36987ed
Add tests to verify that invalid form factor favorites are stored 1:1…
ayoy Sep 19, 2023
8804d65
Fix removing a favorite
ayoy Sep 20, 2023
3769cd9
Add a helper function that cleans up favorites after disabling sync
ayoy Sep 20, 2023
151c635
Update favorites display mode definition in view models
ayoy Sep 20, 2023
ade3c2a
Add unit tests for form factor specific favorites to BookmarkListView…
ayoy Sep 20, 2023
2b5f202
Add unit tests for form factor specific favorites to FavoriteListView…
ayoy Sep 20, 2023
6d3a8e5
Add unit tests in BookmarkUtils related to form factor specific favor…
ayoy Sep 20, 2023
b9a242c
Add documentation to FavoritesDisplayMode
ayoy Sep 21, 2023
9fb6381
Rename displayAll as displayUnified
ayoy Sep 21, 2023
5b00317
Rename native/displayedPlatform -> native/displayedFolder
ayoy Sep 21, 2023
6fba8ab
Rename SettingsProvider initializers
ayoy Sep 21, 2023
eb24eeb
Add prepareLegacyFoldersStructure to BookmarkUtils and change copyFav…
ayoy Sep 21, 2023
3ac191b
Fix tests compilation
ayoy Sep 21, 2023
9860874
Add FavoritesDisplayModeSyncHandlerBase
ayoy Sep 21, 2023
d367904
Make EmailProtectionSyncHandler public and remove EmailManager from S…
ayoy Sep 21, 2023
54d524b
Make emailManager public in EmailProtectionSyncHandler
ayoy Sep 21, 2023
4df0990
Revert "Make emailManager public in EmailProtectionSyncHandler"
ayoy Sep 21, 2023
5e7521f
Make BookmarkEditorViewModel.favoritesDisplayMode public
ayoy Sep 21, 2023
ed95274
Fix BookmarkListInteracting.createBookmark for FFS favorites
ayoy Sep 21, 2023
0325036
Make BookmarkUtils.fetchLegacyFavoritesFolder public
ayoy Sep 21, 2023
82c828a
Merge branch 'main' into dominik/sync-ffs-favorites
ayoy Oct 11, 2023
95a3c21
Make Swiftlint happy
ayoy Oct 11, 2023
c9e4e96
Merge branch 'main' into dominik/sync-ffs-favorites
ayoy Oct 13, 2023
e2480a2
Merge branch 'main' into dominik/sync-ffs-favorites
ayoy Oct 29, 2023
9068189
Fix syncing empty favorites folders
ayoy Oct 31, 2023
fc59aff
Merge branch 'main' into dominik/sync-ffs-favorites
ayoy Nov 7, 2023
3e912b7
Merge branch 'main' into dominik/sync-ffs-favorites
ayoy Nov 10, 2023
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
18 changes: 13 additions & 5 deletions Sources/Bookmarks/BookmarkEditorViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ public class BookmarkEditorViewModel: ObservableObject {
}

let context: NSManagedObjectContext
public let favoritesDisplayMode: FavoritesDisplayMode

@Published public var bookmark: BookmarkEntity
@Published public var locations = [Location]()

lazy var favoritesFolder: BookmarkEntity! = BookmarkUtils.fetchFavoritesFolder(context)
lazy var favoritesFolder: BookmarkEntity! = BookmarkUtils.fetchFavoritesFolder(
withUUID: favoritesDisplayMode.displayedFolder.rawValue,
in: context
)

private var observer: NSObjectProtocol?
private let subject = PassthroughSubject<Void, Never>()
Expand All @@ -59,11 +63,13 @@ public class BookmarkEditorViewModel: ObservableObject {

public init(editingEntityID: NSManagedObjectID,
bookmarksDatabase: CoreDataDatabase,
favoritesDisplayMode: FavoritesDisplayMode,
errorEvents: EventMapping<BookmarksModelError>?) {

externalUpdates = subject.eraseToAnyPublisher()
self.errorEvents = errorEvents
self.context = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType)
self.favoritesDisplayMode = favoritesDisplayMode

guard let entity = context.object(with: editingEntityID) as? BookmarkEntity else {
// For sync, this is valid scenario in case of a timing issue
Expand All @@ -84,11 +90,13 @@ public class BookmarkEditorViewModel: ObservableObject {

public init(creatingFolderWithParentID parentFolderID: NSManagedObjectID?,
bookmarksDatabase: CoreDataDatabase,
favoritesDisplayMode: FavoritesDisplayMode,
errorEvents: EventMapping<BookmarksModelError>?) {

externalUpdates = subject.eraseToAnyPublisher()
self.errorEvents = errorEvents
self.context = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType)
self.favoritesDisplayMode = favoritesDisplayMode

let parent: BookmarkEntity?
if let parentFolderID = parentFolderID {
Expand Down Expand Up @@ -173,13 +181,13 @@ public class BookmarkEditorViewModel: ObservableObject {
}

public func removeFromFavorites() {
assert(bookmark.isFavorite)
bookmark.removeFromFavorites()
assert(bookmark.isFavorite(on: favoritesDisplayMode.displayedFolder))
bookmark.removeFromFavorites(with: favoritesDisplayMode)
}

public func addToFavorites() {
assert(!bookmark.isFavorite)
bookmark.addToFavorites(favoritesRoot: favoritesFolder)
assert(!bookmark.isFavorite(on: favoritesDisplayMode.displayedFolder))
bookmark.addToFavorites(with: favoritesDisplayMode, in: context)
}

public func setParentWithID(_ parentID: NSManagedObjectID) {
Expand Down
81 changes: 63 additions & 18 deletions Sources/Bookmarks/BookmarkEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,28 @@
import Foundation
import CoreData

/**
* This enum defines available favorites folders with their UUIDs as raw value.
*/
public enum FavoritesFolderID: String, CaseIterable {
/// Mobile form factor favorites folder
case mobile = "mobile_favorites_root"
/// Desktop form factor favorites folder
case desktop = "desktop_favorites_root"
/// Unified (mobile + desktop) favorites folder
case unified = "favorites_root"
}

@objc(BookmarkEntity)
public class BookmarkEntity: NSManagedObject {

public enum Constants {
public static let rootFolderID = "bookmarks_root"
public static let favoritesFolderID = "favorites_root"
public static let favoriteFoldersIDs: Set<String> = Set(FavoritesFolderID.allCases.map(\.rawValue))
}

public static func isValidFavoritesFolderID(_ value: String) -> Bool {
FavoritesFolderID.allCases.contains { $0.rawValue == value }
}

public enum Error: Swift.Error {
Expand All @@ -49,7 +65,7 @@ public class BookmarkEntity: NSManagedObject {
@NSManaged public var url: String?
@NSManaged public var uuid: String?
@NSManaged public var children: NSOrderedSet?
@NSManaged fileprivate(set) public var favoriteFolder: BookmarkEntity?
@NSManaged public fileprivate(set) var favoriteFolders: NSSet?
@NSManaged public fileprivate(set) var favorites: NSOrderedSet?
@NSManaged public var parent: BookmarkEntity?

Expand All @@ -58,8 +74,12 @@ public class BookmarkEntity: NSManagedObject {
/// In-memory flag. When set to `false`, disables adjusting `modifiedAt` on `willSave()`. It's reset to `true` on `didSave()`.
public var shouldManageModifiedAt: Bool = true

public var isFavorite: Bool {
favoriteFolder != nil
public func isFavorite(on platform: FavoritesFolderID) -> Bool {
favoriteFoldersSet.contains { $0.uuid == platform.rawValue }
}

public var favoritedOn: [FavoritesFolderID] {
favoriteFoldersSet.compactMap(\.uuid).compactMap(FavoritesFolderID.init)
}

public convenience init(context moc: NSManagedObjectContext) {
Expand All @@ -81,7 +101,7 @@ public class BookmarkEntity: NSManagedObject {
guard !changedKeys.isEmpty, !changedKeys.contains(NSStringFromSelector(#selector(getter: modifiedAt))) else {
return
}
if isInserted && (uuid == Constants.rootFolderID || uuid == Constants.favoritesFolderID) {
if isInserted, let uuid, uuid == Constants.rootFolderID || Self.isValidFavoritesFolderID(uuid) {
return
}
modifiedAt = Date()
Expand Down Expand Up @@ -123,6 +143,10 @@ public class BookmarkEntity: NSManagedObject {
return children.filter { $0.isPendingDeletion == false }
}

public var favoriteFoldersSet: Set<BookmarkEntity> {
return favoriteFolders.flatMap(Set<BookmarkEntity>.init) ?? []
}

public static func makeFolder(title: String,
parent: BookmarkEntity,
insertAtBeginning: Bool = false,
Expand Down Expand Up @@ -168,9 +192,21 @@ public class BookmarkEntity: NSManagedObject {
root.addToFavorites(self)
}
}

public func removeFromFavorites() {
favoriteFolder = nil

public func addToFavorites(folders: [BookmarkEntity]) {
for root in folders {
root.addToFavorites(self)
}
}

public func removeFromFavorites(folders: [BookmarkEntity]) {
for root in folders {
root.removeFromFavorites(self)
}
}

public func removeFromFavorites(favoritesRoot: BookmarkEntity) {
favoritesRoot.removeFromFavorites(self)
}

public func markPendingDeletion() {
Expand Down Expand Up @@ -200,20 +236,12 @@ extension BookmarkEntity {
func validate() throws {
try validateThatFoldersDoNotHaveURLs()
try validateThatFolderHierarchyHasNoCycles()
try validateFavoritesStatus()
try validateFavoritesFolder()
}

func validateFavoritesStatus() throws {
let isInFavoriteCollection = favoriteFolder != nil
if isFavorite != isInFavoriteCollection {
throw Error.invalidFavoritesStatus
}
}

func validateFavoritesFolder() throws {
if let favoritesFolderID = favoriteFolder?.uuid,
favoritesFolderID != Constants.favoritesFolderID {
let uuids = Set(favoriteFoldersSet.compactMap(\.uuid))
guard uuids.isSubset(of: Constants.favoriteFoldersIDs) else {
throw Error.invalidFavoritesFolder
}
}
Expand Down Expand Up @@ -296,6 +324,23 @@ extension BookmarkEntity {

}

// MARK: Generated accessors for favoriteFolders
extension BookmarkEntity {

@objc(addFavoriteFoldersObject:)
@NSManaged private func addToFavoriteFolders(_ value: BookmarkEntity)

@objc(removeFavoriteFoldersObject:)
@NSManaged private func removeFromFavoriteFolders(_ value: BookmarkEntity)

@objc(addFavoriteFolders:)
@NSManaged private func addToFavoriteFolders(_ values: NSSet)

@objc(removeFavoriteFolders:)
@NSManaged private func removeFromFavoriteFolders(_ values: NSSet)

}

extension BookmarkEntity: Identifiable {

}
33 changes: 20 additions & 13 deletions Sources/Bookmarks/BookmarkListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject {
public let currentFolder: BookmarkEntity?

let context: NSManagedObjectContext
public var favoritesDisplayMode: FavoritesDisplayMode {
didSet {
reloadData()
}
}

public var bookmarks = [BookmarkEntity]()

Expand All @@ -40,11 +45,13 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject {

public init(bookmarksDatabase: CoreDataDatabase,
parentID: NSManagedObjectID?,
favoritesDisplayMode: FavoritesDisplayMode,
errorEvents: EventMapping<BookmarksModelError>?) {
self.externalUpdates = self.subject.eraseToAnyPublisher()
self.localUpdates = self.localSubject.eraseToAnyPublisher()
self.errorEvents = errorEvents
self.context = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType)
self.favoritesDisplayMode = favoritesDisplayMode

if let parentID = parentID {
if let bookmark = (try? context.existingObject(with: parentID)) as? BookmarkEntity {
Expand Down Expand Up @@ -75,19 +82,19 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject {
}
}

// swiftlint:disable:next function_parameter_count
public func createBookmark(title: String,
url: String,
folder: BookmarkEntity,
folderIndex: Int,
favoritesFolder: BookmarkEntity?,
favoritesIndex: Int?) {
public func createBookmark(
title: String,
url: String,
folder: BookmarkEntity,
folderIndex: Int,
favoritesFoldersAndIndexes: [BookmarkEntity: Int]
) {
let bookmark = BookmarkEntity.makeBookmark(title: title, url: url, parent: folder, context: context)
if let addedIndex = folder.childrenArray.firstIndex(of: bookmark) {
moveBookmark(bookmark, fromIndex: addedIndex, toIndex: folderIndex)
}
if let favoritesFolder, let favoritesIndex {
bookmark.addToFavorites(insertAt: favoritesIndex, favoritesRoot: favoritesFolder)
for (favoritesFolder, index) in favoritesFoldersAndIndexes {
bookmark.addToFavorites(insertAt: index, favoritesRoot: favoritesFolder)
}
save()
}
Expand Down Expand Up @@ -115,10 +122,10 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject {
}

public func toggleFavorite(_ bookmark: BookmarkEntity) {
if bookmark.isFavorite {
bookmark.removeFromFavorites()
} else if let folder = BookmarkUtils.fetchFavoritesFolder(context) {
bookmark.addToFavorites(favoritesRoot: folder)
if bookmark.isFavorite(on: favoritesDisplayMode.displayedFolder) {
bookmark.removeFromFavorites(with: favoritesDisplayMode)
} else {
bookmark.addToFavorites(with: favoritesDisplayMode, in: context)
}
save()
}
Expand Down
Loading