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

Allow automated fetching of synced bookmarks' favicons #564

Merged
merged 105 commits into from
Nov 30, 2023

Conversation

ayoy
Copy link
Contributor

@ayoy ayoy commented Nov 15, 2023

Please review the release process for BrowserServicesKit here.

Required:

Task/Issue URL: https://app.asana.com/0/0/1205949780297088/f
iOS PR: duckduckgo/iOS#2163
macOS PR: duckduckgo/macos-browser#1857
What kind of version bump will this require?: Major

Tech Design URL: https://app.asana.com/0/481882893211075/1204986998781220/f

Description:
Add BookmarksFaviconFetcher that is used to fetch favicons for bookmarks received by Sync.
Fetcher is opt-in, controlled by a setting inside Sync settings (with an additional in-context onboarding
popup presented from client apps). Fetcher uses LinkPresentation framework to obtain a favicon
for a given domain, and in case of failure it falls back to checking hardcoded favicon URLs.
Fetcher keeps a state internally, by saving list of bookmarks IDs that need processing to a file on disk.
Fetcher plugs into clients' implementation of favicon storage by exposing FaviconStoring protocol.
Fetcher performs fetching on a serial operation queue. Each fetcher invocation cancels previously scheduled
operation and schedules a new one. Updating fetcher state is also scheduled on the operation queue -
state updates don't support cancelling and always finish before next operation is started.

Steps to test this PR:

  1. Run the macOS app from Xcode, enable Sync logging in Debug -> Logging -> Sync.
  2. Import some bookmarks (you can use a HTML file provided in the test builds task - it adds a bunch of popular websites as favorites).
  3. Start sync, run the iOS app and connect to the Sync account – enable sharing favorites so that iOS gets the same set as macOS.

On iOS:

  1. On iOS, go back from Sync settings to the home screen. Drag the favorites view or open a new tab. Verify that you're prompted to enable favicons fetching.
  2. Enable favicons fetching and observe that favicons are being fetched (you need to drag the view to trigger reloading, or open new tab page to reload the full view. Some of the favicons in the test set won't be fetched because bookmarks URLs are not found (e.g. https://www.dropboxusercontent.com)
  3. Kill the app while fetching and restart. Verify that fetching is resumed.
  4. Disable Sync. Verify that fetching is interrupted.
  5. Re-enable Sync. Verify that fetching is disabled.
  6. Go to Debug menu -> Sync -> Reset favicons fetcher onboarding.
  7. Go to bookmarks view and scroll to a bookmark without favicon. Verify that you're prompted to enable fetching favicons.
  8. Verify that after all favicons are fetched, fetcher doesn't try to fetch any more favicons after every sync operation (the fetching operation should start and immediately finish).

On macOS:

  1. After enabling Sync on iOS, go back to new tab page. Verify that you're prompted to enable favicons fetching. Note that the popup UI is not final.
  2. Enable favicons fetching and observe that favicons are being fetched (you need to drag the view or switch tabs back and forth to trigger reloading).
  3. Kill the app while fetching and restart. Verify that fetching is resumed.
  4. Disable Sync. Verify that fetching is interrupted.
  5. Re-enable Sync. Verify that fetching is disabled.
  6. Go to Debug menu -> Sync -> Reset favicons fetcher onboarding.
  7. Open Bookmarks Management view. Verify that you're prompted to enable fetching favicons.
  8. Verify that after all favicons are fetched, fetcher doesn't try to fetch any more favicons after every sync operation (the fetching operation should start and immediately finish).

OS Testing:

  • iOS 14
  • iOS 15
  • iOS 16
  • macOS 10.15
  • macOS 11
  • macOS 12

Internal references:

Software Engineering Expectations
Technical Design Template

ayoy added 30 commits September 21, 2023 15:58
@ayoy ayoy self-assigned this Nov 15, 2023
@ayoy ayoy marked this pull request as ready for review November 16, 2023 09:03
@ayoy ayoy requested a review from bwaresiak November 16, 2023 09:06
@ayoy ayoy assigned bwaresiak and unassigned ayoy Nov 16, 2023

context.performAndWait {
let bookmarks = BookmarkUtils.fetchAllBookmarks(in: context)
ids = bookmarks.compactMap(\.uuid)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be more efficient to use dictionary result type query to get one property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea :) let me rework it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's done! All other comments are addressed now so ready for you to take another look.


// MARK: - Overrides

override var isAsynchronous: Bool { true }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? You seem to be always starting operation through the queue?

Copy link
Contributor Author

@ayoy ayoy Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need this in order for the operation's main() function to not report finished state after exiting, but wait until isFinished is set by an async Task that's run there. It's the same for SyncOperation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it hurts, but I belive this value is ignored by OperationQueue - as it always starts the operation on a queue. Nonetheless we can keep it. :)

// guard let favoritesFolder = BookmarkUtils.fetchFavoritesFolder(context) else {
// // Error - unable to process favorites
// return
// }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's with this? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a remainder from pre-form-factor-specific favorites code which I forgot to remove :)

Copy link
Contributor

@SabrinaTardio SabrinaTardio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great and works perfectly! Great job
I left a couple of minor comments

Also general question… are we using different icons in the iOS prompt and macOS prompt

}

/**
* This class manages fetching favicons for bookmarks updated by Sync.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we keep this generic and not relate it to sync specifically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual API technically doesn't prevent this fetcher to be used outside of Sync. I think it's okay to be specific about it in the docs at the moment, and update the docs whenever we apply the fetcher to another domain (which may require revisiting its API anyway).


private func initStorage() throws {
if !FileManager.default.fileExists(atPath: dataDirectoryURL.path) {
try FileManager.default.createDirectory(at: dataDirectoryURL, withIntermediateDirectories: true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t want an error like BookmarksFaviconsFetcherError.failedToCreateDirectory(error)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need it because this class is not initialized in BSK, but in client apps. The sole reason for having BookmarksFaviconsFetcherError is to be able to pass various errors to client apps via EventMapping< BookmarksFaviconsFetcherError>. For this function we don't need EventMapping because it's initialized directly in client apps that are expected to handle the error with a pixel as appropriate.


public func storeBookmarkIDs(_ ids: Set<String>) throws {
do {
try ids.joined(separator: ",").data(using: .utf8)?.write(to: missingIDsFileURL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we want to differentiate failed to store from failed to convert?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't worry about conversion to Data because UUIDs is something we should have control over, and converting to data would only return nil when a string can't be represented in UTF-8. We're even force unwrapping these calls in a bunch of places across the BSK codebase.

await runAfterOperationsFinished {
let ids = (try? self.stateStore.getBookmarkIDs()) ?? []
XCTAssertTrue(ids.isEmpty)
XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me think… should it be an error if empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that there are no bookmarks to handle which is an expected scenario for users that don't have bookmarks :)

@ayoy
Copy link
Contributor Author

ayoy commented Nov 29, 2023

Thanks for the review @SabrinaTardio! For the prompt icons, the onboarding dialog uses the same icons. The settings option used different icons, but from what I can see, the newest designs get rid of the icons in settings altogether.

@ayoy ayoy merged commit f1ae021 into main Nov 30, 2023
5 checks passed
@ayoy ayoy deleted the dominik/sync-favicons-fetching branch November 30, 2023 07:19
samsymons added a commit that referenced this pull request Nov 30, 2023
* main:
  bump privacy-dashboard to 3.0.0 (#549)
  Allow automated fetching of synced bookmarks' favicons (#564)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants