-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Refactor FXIOS-13499 [Trending Searches] recent searches to use A/S #29797
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
Refactor FXIOS-13499 [Trending Searches] recent searches to use A/S #29797
Conversation
8191858 to
af920fb
Compare
|
|
||
| /// A provider that manages recent search terms for a specific search engine. | ||
| /// A provider that manages recent search terms from a user's history storage. | ||
| struct DefaultRecentSearchProvider: RecentSearchProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are reviewing this file, I would say to review the Raw file since it might be easier as the whole file is basically rewritten.
If you are wondering why we are using Int64.min in this file below, this is because I am following Android's implementation that uses Long.MIN_VALUE and there's no method to fetch X number of search terms currently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the min is used in terms of the date timestamp. Does this mean we're fetching everything from all of time? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean we're fetching everything from all of time?
No it's limited to a 1000. Anyways I didn't like how we have to get x items and have to filter it out to the limit we want. I opened mozilla/application-services#7002 to add a convenience method that returns n most recent items. This is not a blocker though. We can merge this and update later when the new method is available. @cyndichin can you please add a //TODO(FXIOS-xxx)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @issammani for opening that PR, it'll be helpful for Android too! You 🪨 ! It seems requirements may have change in that we may want to fetch all recent searches (with no limits). Still TBD, but the method looks great regardless!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated here: d62599d
| case didEnterSearchTerm | ||
| // User submitted a search term to load the search request | ||
| case didSubmitSearchTerm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thatswinnie I wasn't sure why we need to make this distinction, but didEnterSearchTerm did not seem to be triggered as I thought it would be. Maybe we can add a comment to clarify the difference. We can chat live on this as well!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we are just using this action in the ToolbarMiddleware so we should make this a ToolbarMiddlewareAction instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @thatswinnie ! I believe that the naming is based on where the action is called and not the consequences.
Here's our wiki guidelines on that:
When it comes to naming convention, we dispatch general action names from the view and the middleware dispatches middleware actions.
i.e. `GeneralActionType.show` lives in the view and `GeneralMiddlewareActionType.show` lives in the middleware.
I did write these when I was discussing with @OrlaM on Redux and think I brought it to the team in the early days. Happy to sync or update this if something has changed and I'm unaware.
https://github.com/mozilla-mobile/firefox-ios/wiki/Redux-Guidelines---FAQs#faqs
💪 Quality guardian5 tests files modified. You're a champion of test coverage! 🚀 🧩 Neat PieceThis PR changes 492 lines. It's a substantial update, 💬 Description craftsmanGreat PR description! Reviewers salute you 🫡 🦊 BrowserViewController CheckWe’re tracking the size of
❌ Per-file test coverage gateThe following changed file(s) are below 35.0% coverage:
Client.app: Coverage: 37.39
libStorage.a: Coverage: 56.54
Generated by 🚫 Danger Swift against ab70f7f |
af920fb to
e59c1ea
Compare
|
Happy to chat live about this PR as well! Tagging @thatswinnie for toolbar, @issammani for A/S stuff and @ih-codes who has been reviewing the recent searches PRs. |
e0323e9 to
41cbc75
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A first pass with some thoughts, I didn't take the time to run it and will leave that to QA or other reviewers unless you'd like me to dig in more. 👌
|
|
||
| finishEditingAndSubmit(searchURL, visitType: VisitType.typed, forTab: tab) | ||
|
|
||
| guard isRecentSearchEnabled else { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit; I would like this logic to be encased in an if so someone doesn't go appending extra work at the end of this function not realizing it's barred by a feature flag guard. It's just clearer to me to wrap the conditional code in an if for stuff like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I compromised and moved into its own method, let me know if any issues?
Updated commit - 3a931ea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfecto! 👌
| ensureMainThread { [weak self] in | ||
| self?.delegate?.reloadTableView() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, you could adapt the loadRecentSearches function signature to use a @MainActor completion. Then in the loadRecentSearches method you call the completion on the main thread, and here you wouldn't need this.
I just suggest it since loading the engines somewhat suggests to me that we'd perform some UI work after some networking. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to stay away from mixing completion handlers / modern swift concurrency here and we're more explicit in what we want to ensure is on main thread. However, it does seem cleaner in theory though and can't think of a case where we wouldn't want it on the main thread.
I tried to update the function signature, but I get this error, let me know if I misunderstood what you meant. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can go either way then, now that I understand your goal!
In this case you'd need to Dispatch to the main queue to ensure the completion handler is always called on the main thread, that's expected actually. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay, I wasn't sure if there was a route without dispatching to main, lets leave this as is then. I'll need to revisit this area when I use the new method anyway.
|
|
||
| /// A provider that manages recent search terms for a specific search engine. | ||
| /// A provider that manages recent search terms from a user's history storage. | ||
| struct DefaultRecentSearchProvider: RecentSearchProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the min is used in terms of the date timestamp. Does this mean we're fetching everything from all of time? 🤔
| func test_addRecentSearch_withWhitespaces_trimsAndReturnsValidSearchTerm() { | ||
| let sut = createSubject(for: "engineA") | ||
|
|
||
| sut.addRecentSearch(" swift ", url: "https://example.com") | ||
| sut.addRecentSearch(" ", url: "https://example.com") | ||
| sut.addRecentSearch("", url: "https://example.com") | ||
|
|
||
| XCTAssertEqual(mockHistoryStorage.noteHistoryMetadataCallCount, 1) | ||
| XCTAssertEqual(mockHistoryStorage.searchTermList.reversed(), ["swift"]) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! 👌
| @@ -0,0 +1,86 @@ | |||
| // This Source Code Form is subject to the terms of the Mozilla Public | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know these aren't exactly new, but great test coverage for all the edge cases! 🚀
| func test_didSubmitSearchTerm_withoutURL_doesNotAddRecentSearchToHistoryStorage() { | ||
| let subject = createSubject(manager: toolbarManager) | ||
| let action = ToolbarAction( | ||
| searchTerm: "cookies", | ||
| windowUUID: windowUUID, | ||
| actionType: ToolbarActionType.didSubmitSearchTerm | ||
| ) | ||
|
|
||
| subject.toolbarProvider(mockStore.state, action) | ||
| XCTAssertEqual(mockRecentSearchProvider.addRecentSearchCalledCount, 0) | ||
| } | ||
|
|
||
| func test_didSubmitSearchTerm_withoutSearchTerm_doesNotAddRecentSearchToHistoryStorage() { | ||
| let subject = createSubject(manager: toolbarManager) | ||
| let action = ToolbarAction( | ||
| windowUUID: windowUUID, | ||
| actionType: ToolbarActionType.didSubmitSearchTerm | ||
| ) | ||
|
|
||
| subject.toolbarProvider(mockStore.state, action) | ||
| XCTAssertEqual(mockRecentSearchProvider.addRecentSearchCalledCount, 0) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly the type of weirdness I was describing in the meeting earlier with Redux action payload types, which action creators might solve. I have never liked how our redux actions could be paired with "bad" state.
Nothing actionable here, just pointing out one of my redux gripes. 🤣
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah 😅 I agree its the dangerous part of this, so I add tests for now lol.
I remember we used to have our actions as enums and each type had an associate value so we didn't have to have this bad state, but that had its own issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you remember those issues? Because that's exactly the direction I was thinking we might need to move for the redux actions to have correctly typed action payloads. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what I remember, it seem that it was difficult to scale with, an example is when we needed a windowUUID to be associated with each action. Matt may have more insight. Here's a ticket to describe a discussion he had with Orla - https://mozilla-hub.atlassian.net/browse/FXIOS-8188?assignee=625c9c3cd7f1b80069bcb183
Here's the slides on some updates on the changes - https://docs.google.com/presentation/d/1kO25b51gv28zQnos9jrSzyMx4vh9Z4yX4H1jCbXMDM4/edit?slide=id.g1ff410de4db_0_105#slide=id.g1ff410de4db_0_105
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ty for the links @cyndichin, much appreciated! 😄
| /// Currently only used to get the recent searches from the user's history storage. | ||
| public func getHistoryMetadataSince( | ||
| since startDate: Int64, | ||
| completion: @Sendable @escaping (Result<[HistoryMetadata], any Error>) -> Void | ||
| ) { | ||
| withReader({ connection in | ||
| return try connection.getHistoryMetadataSince(since: startDate) | ||
| }, completion: completion) | ||
| } | ||
|
|
||
| /// As part of the recent searches work, we are interesting in saving the search term in our history storage. | ||
| /// - Parameters: | ||
| /// - searchTerm: The search term used to find a page. | ||
| /// - urlString: The url of the page. | ||
| public func noteHistoryMetadata( | ||
| for searchTerm: String, | ||
| and urlString: String, | ||
| completion: @Sendable @escaping (Result<(), any Error>) -> Void | ||
| ) { | ||
| withWriter( | ||
| { connection in | ||
| return try connection.noteHistoryMetadataObservationViewTime( | ||
| key: HistoryMetadataKey( | ||
| url: urlString, | ||
| searchTerm: searchTerm, | ||
| referrerUrl: nil | ||
| ), viewTime: nil | ||
| ) | ||
| }, | ||
| completion: completion) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for using callbacks and not more Deferred. 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM ! We can follow-up on using the new method when it's available
|
@ih-codes thanks for your comments 🔥🔥 , I updated the PR and responded, ready for another review~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll approve now, but you can check my comment response RE: the @MainActor closure for the one method definition.
|
This pull request has conflicts when rebasing. Could you fix it @cyndichin? 🙏 |
3a931ea to
f4df765
Compare
|
🚀 PR merged to |
📜 Tickets
Jira ticket
Github issue
💡 Description
This PR handles adding recent search terms to our history storage as well as fetching the list.
Background
Instead of using the initial simple cache of recent searches that was implemented here, we update our
DefaultRecentSearchProviderto use the history storage viaRust Placesinstead to store our search terms (similar to what's being done on Android).Currently, Android is storing more than just search terms, but for the sake of this feature we only care about storing search terms.
ToolbarActionassociated with submitting a search term so we can triggeraddRecentSearchthat stores the search term and associated url in history storage.DefaultRecentSearchProviderWith this PR, we can now see the search terms being saved locally in
places.db, which was previously empty.Note: On Desktop, they are not using application services and each search engine has its own set of search terms, which was our the previous implementation. However, to maintain parity with Android we are using application services.
Testing
Currently, there is no UI for recent searches, but you can add a breakpoint in
retrieveRecentSearchesinSearchViewModelto confirm that you are receiving the proper search terms. This PR does not include updating the zero search logic to handle recent searches, that will be in a separate PR. I did notice the first time you write to the db, it seems to take a while to add searches, but after seems to work fine.Make sure to turn on both FF for now, you can revert this commit: ffcca39
Here's my demo:
Screen.Recording.2025-10-03.at.12.18.38.PM.mov
📝 Checklist