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

Brave search conversion promotion in omnibox #12846

Merged
merged 4 commits into from
Jun 16, 2022
Merged

Conversation

simonhong
Copy link
Member

@simonhong simonhong commented Mar 31, 2022

Resolves brave/brave-browser#22616

This PR is for showing brave search promotion match in omnibox popup.
To do this, PromotionProvider adds promotion matches and BraveOmniboxResultView
renders promotion UI. There are two types of conversion. One is button type and the other is banner.
Button type match is located as a second match and Bannter type is located as a last match.
There position is re-arragned after upstreams match sorting.

First commit is PromotionProvider(non UI) part and second commit is BraveOmniboxResultView (UI) part.

Button type
Screen Shot 2022-06-15 at 1 17 58 PM
Screen Shot 2022-06-15 at 1 24 33 PM
Screen Shot 2022-06-15 at 1 18 41 PM
Screen Shot 2022-06-15 at 1 25 00 PM

Banner type
Screen Shot 2022-06-15 at 1 20 24 PM
Screen Shot 2022-06-15 at 1 22 01 PM
Screen Shot 2022-06-15 at 1 20 07 PM
Screen Shot 2022-06-15 at 1 22 52 PM

Submitter Checklist:

  • I confirm that no security/privacy review is needed, or that I have requested one
  • There is a ticket for my issue
  • Used Github auto-closing keywords in the PR description above
  • Wrote a good PR/commit description
  • Squashed any review feedback or "fixup" commits before merge, so that history is a record of what happened in the repo, not your PR
  • Added appropriate labels (QA/Yes or QA/No; release-notes/include or release-notes/exclude; OS/...) to the associated issue
  • Checked the PR locally: npm run test -- brave_browser_tests, npm run test -- brave_unit_tests, npm run lint, npm run gn_check, npm run tslint
  • Ran git rebase master (if needed)

Reviewer Checklist:

  • A security review is not needed, or a link to one is included in the PR description
  • New files have MPL-2.0 license header
  • Adequate test coverage exists to prevent regressions
  • Major classes, functions and non-trivial code blocks are well-commented
  • Changes in component dependencies are properly reflected in gn
  • Code follows the style guide
  • Test plan is specified in PR before merging

After-merge Checklist:

Test Plan:

Button type

  1. Launch browser with --enable-features=BraveSearchOmniboxButton command line switch
  2. Set any search engine except brave search as a default search provider
  3. Type any keyword and check button type autocomplete is visible as a second item

Banner type

  1. Launch browser with --enable-features=BraveSearchOmniboxBanner command line switch
  2. Set any search engine except brave search as a default search provider
  3. Type any keyword and check button type autocomplete is visible as a last item

Note:

  • When first item is not keyword match from default provider, promotion match will not be added because first item is highly recommended match
  • When dismissed button or x button clicked, autocomplete match list are refreshed with same input data and promotion match will not be shown anymore.
  • When user types an input that was tried by promotion match, promotion match will not be shown again. Instead promotion url could be visible as a history match.
  • This conversion promotion is only enabled for below 5 countries
    • US, CA, DE, FR, GB
  • Only normal profile window give promotion. Private(or tor) window don't show promotion match.

@simonhong simonhong self-assigned this Mar 31, 2022
@simonhong simonhong force-pushed the brave_search_conversion branch 3 times, most recently from a0da9e3 to 6659cc3 Compare May 12, 2022 07:54
@simonhong
Copy link
Member Author

simonhong commented May 13, 2022

We want to locate conversion ask entry at second or last in omnibox popup.
In omnibox framework, match is located by some factors such as type or relavance.
It seems difficult to set conversion entry visible always as a last entry because omnibox popup has a maximum number of visible entries at once. So, if more higher priority entries are added more, conversion entry could not be visible. Need to find concrete way to locate it as a last entry always.

One more thing - when user select promotion entry, that promotion url could be visibile again in omnibox popup later like other history entries. In this case, history entry should have more priority?

@simonhong simonhong force-pushed the brave_search_conversion branch 14 times, most recently from 6c92777 to 648b01d Compare May 20, 2022 02:54
@simonhong simonhong force-pushed the brave_search_conversion branch 10 times, most recently from 7f1bd18 to a54829b Compare May 26, 2022 07:19
@simonhong simonhong changed the title WIP: Brave search conversion Brave search conversion May 26, 2022
Copy link
Member Author

@simonhong simonhong left a comment

Choose a reason for hiding this comment

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

@fallaciousreasoning Thanks for review! I think all of your comments are addressed or commented.

app/brave_generated_resources.grd Outdated Show resolved Hide resolved
app/brave_generated_resources.grd Outdated Show resolved Hide resolved
app/brave_generated_resources.grd Outdated Show resolved Hide resolved
app/brave_generated_resources.grd Outdated Show resolved Hide resolved
app/brave_generated_resources.grd Outdated Show resolved Hide resolved
components/omnibox/browser/promotion_utils.cc Outdated Show resolved Hide resolved

// If first match is not from search query with default provider,
// it means there is more proper match from other providers.
// In this case, remove promotion match from |result|.
Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

components/omnibox/browser/promotion_utils.cc Outdated Show resolved Hide resolved
components/omnibox/browser/promotion_utils.h Outdated Show resolved Hide resolved
components/omnibox/browser/promotion_utils.h Outdated Show resolved Hide resolved
@simonhong simonhong force-pushed the brave_search_conversion branch from f22922e to e984d9a Compare May 31, 2022 11:26
Copy link
Contributor

@fallaciousreasoning fallaciousreasoning left a comment

Choose a reason for hiding this comment

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

LGTM but still worth having someone with a bit more experience in the brave codebase take a look.

@simonhong
Copy link
Member Author

Kindly ping :)

Copy link
Member

@petemill petemill left a comment

Choose a reason for hiding this comment

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

In addition to the minor feedback below, I noticed a couple of UI issues:

  • There is an extra space character (" ") between the search term and the "- Brave Search" string.
  • The result row is not highlighting on keyboard arrow navigation, only on mouse hover, but the other chromium result rows are
    • with mouse: image
    • with keyboard: image

Similarly, I feel like with the banner option, the UX with the keyboard is a little strange. Using the keyboard I can press the down button and I know the banner is selected because the url changes to search.brave.com/xyz in the address bar, but there is no indication in the UI that it is the selected item. It does not have the left "selected indicator" nor does the background change via keyboard or mouse. I'm not sure if this is a design desicion so maybe we should ask @aguscruiz but I feel this banner row needs to either have a selection state, or not be selectable via keyboard.
Example here is where the banner is "selected" but it's not obvious in the UI:
image


ResetChildrenVisibility();

if (type_ == ConversionType::kButton) {
Copy link
Member

@petemill petemill Jun 3, 2022

Choose a reason for hiding this comment

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

Would it be simpler and easier to read to have 2 separate views - one for the "button" type and another for the "banner" type? Either as children for this view or as replacement for this view.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, this view has two childrens for each types.
button_type_container_ and banner_type_container_ for button and banner type.
Based on type, only one is visibile in this promotion view. Added comment about this - 68d8c81

// Use 14px font size for input text.
views::Label::CustomFont emphasized_font = {
GetFont(14, gfx::Font::Weight::SEMIBOLD)};
button_type_contents_input_ = input_container->AddChildView(
Copy link
Member

@petemill petemill Jun 3, 2022

Choose a reason for hiding this comment

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

In the screenshot this looks like it's the same or very similar text format as the regular search result row. Perhaps you could explain here, or leave a comment in the code as to why we're not using OmniboxMatchCellView or even at the least OmniboxTextView for the "button" version? I'm not very familiar with the omnibox views, so sorry if it's obvious why we can't use them. But I would have hoped we could do less custom views, and extend the existing ones. I'm sure there's a good reason for you to do it like this and I guess it's maybe because we have the button on the right and not underneath, so maybe leave a code comment, perhaps at the beginning of the ConfigureForButtonType method.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it seems very similar but has slightly different for our purpose such as layout (you mentioned - buttons posiiton), bg, text colors or font. So, I decided to implement whole UI. In this case, I could set our view to whole area easily by only controlling upstream's view and our view's visibilty w/o touching upstream views.

Copy link
Member Author

@simonhong simonhong left a comment

Choose a reason for hiding this comment

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

@petemill On selection, selection indicator is only used and background color is not changed based on design spec. and only banner type spec(no indicator) is design decision.
cc @aguscruiz


ResetChildrenVisibility();

if (type_ == ConversionType::kButton) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, this view has two childrens for each types.
button_type_container_ and banner_type_container_ for button and banner type.
Based on type, only one is visibile in this promotion view. Added comment about this - 68d8c81

// Use 14px font size for input text.
views::Label::CustomFont emphasized_font = {
GetFont(14, gfx::Font::Weight::SEMIBOLD)};
button_type_contents_input_ = input_container->AddChildView(
Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it seems very similar but has slightly different for our purpose such as layout (you mentioned - buttons posiiton), bg, text colors or font. So, I decided to implement whole UI. In this case, I could set our view to whole area easily by only controlling upstream's view and our view's visibilty w/o touching upstream views.

@aguscruiz
Copy link
Collaborator

aguscruiz commented Jun 6, 2022

@petemill On selection, selection indicator is only used and background color is not changed based on design spec. and only banner type spec(no indicator) is design decision. cc @aguscruiz

For option 3 (the small one) I think we can use the hover state as a focused state too, what do you think?

As for option 2, I've mocked it up here, sorry, didn't think of that state but absolutely, needs a focused state. It's a thicker stroke (2px instead of 1) and in a slightly darker tone

https://www.figma.com/file/8wncTFNBvKN7CS6LTXAMYs/Search-Conversion?node-id=770%3A38037
image

https://www.figma.com/file/8wncTFNBvKN7CS6LTXAMYs/Search-Conversion?node-id=770%3A38321
image

@simonhong simonhong force-pushed the brave_search_conversion branch from 1fb90bf to c6bdf33 Compare June 7, 2022 04:03
@simonhong simonhong requested a review from petemill June 7, 2022 04:04
@simonhong
Copy link
Member Author

Kindly ping to @brave/deps-reviewers @brave/patch-reviewers @brave/chromium-src-reviewers :)

@simonhong simonhong force-pushed the brave_search_conversion branch from c6bdf33 to 840832b Compare June 12, 2022 23:58
AddChildView(std::make_unique<OmniboxRowView>(
i, edit_model_,
- std::make_unique<OmniboxResultView>(this, edit_model_, i),
+ std::make_unique<BraveOmniboxResultView>(this, edit_model_, i),
Copy link
Member

Choose a reason for hiding this comment

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

maybe we can replace OmniboxResultView with BraveOmniboxResultView in header/cc files?

using brave_search_conversion::ConversionType;

void SortBraveSearchPromotionMatch(
AutocompleteResult* result,
Copy link
Member

Choose a reason for hiding this comment

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

mutable args should go last.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

AutocompleteResult* result,
const AutocompleteInput& input,
brave_search_conversion::ConversionType type) {
if (result->size() == 0)
Copy link
Member

Choose a reason for hiding this comment

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

DCHECK(result);

Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

void SetUp() override {
SetMockLocale("en-US");
SetMockClient();
provider_ = new PromotionProvider(client_mock_.get());
Copy link
Member

Choose a reason for hiding this comment

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

base::MakeRefCounted

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

namespace brave_search_conversion {

bool IsPromotionEnabledCountry(const std::string& country_code) {
static const base::flat_set<std::string> supported_country{"US", "CA", "DE",
Copy link
Member

Choose a reason for hiding this comment

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

base::NoDestructor or just a simple array: const base::StringPiece kSupportedCountries[].

Copy link
Member Author

Choose a reason for hiding this comment

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

Done with simple array.

// Move match at |from| to |to|.
void AutocompleteResult::MoveMatch(size_t from, size_t to) {
DCHECK_LE(from, matches_.size());
DCHECK_LE(to, matches_.size());
Copy link
Member

Choose a reason for hiding this comment

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

should be DCHECK_LT, but actually we should do proper runtime if checks. currently this looks like a crash after some upstream change.

RemoveMatch(matches_.begin() + from);

if (from < to)
to--;
Copy link
Member

Choose a reason for hiding this comment

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

if from == 0 and to == 1, will this work as expected?

if (from < to)
to--;

matches_.insert(matches_.begin() + to, promotion);
Copy link
Member

Choose a reason for hiding this comment

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

let's rewrite this function via std::rotate with iterators, should be much safer. some examples:
https://chromium-review.googlesource.com/c/chromium/src/+/1526299
https://stackoverflow.com/a/57399634

Copy link
Member Author

Choose a reason for hiding this comment

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

Done with rotate. It works great! 👍🏼

#define SetMatch \
virtual SetMatch(const AutocompleteMatch& match); \
friend class BraveOmniboxResultView; \
void SetMatchUnUsed
Copy link
Member

Choose a reason for hiding this comment

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

this way you don't need to copy-paste args and keep track of it on rebase:

#define SetMatch                                    \
  NotUsed(); \
  friend class BraveOmniboxResultView;              \
  virtual void SetMatch

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

if (prefs && prefs->GetBoolean(prefs::kDismissed))
return ConversionType::kNone;

if (service) {
Copy link
Member

Choose a reason for hiding this comment

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

We should force prefs and service with DCHECK(prefs); and DCHECK(service);, otherwise this function can be misused.

We can split it into two and have the second one generate ConversionType just by using static info (locale/country, features).

Copy link
Member Author

Choose a reason for hiding this comment

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

Added GetConversionType().

Copy link
Member Author

@simonhong simonhong left a comment

Choose a reason for hiding this comment

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

@goodov Thanks for review. I think I address all comments except BraveOmniboxResultView patching. It's still difficult for me.

#define SetMatch \
virtual SetMatch(const AutocompleteMatch& match); \
friend class BraveOmniboxResultView; \
void SetMatchUnUsed
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

if (from < to)
to--;

matches_.insert(matches_.begin() + to, promotion);
Copy link
Member Author

Choose a reason for hiding this comment

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

Done with rotate. It works great! 👍🏼

namespace brave_search_conversion {

bool IsPromotionEnabledCountry(const std::string& country_code) {
static const base::flat_set<std::string> supported_country{"US", "CA", "DE",
Copy link
Member Author

Choose a reason for hiding this comment

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

Done with simple array.

void SetUp() override {
SetMockLocale("en-US");
SetMockClient();
provider_ = new PromotionProvider(client_mock_.get());
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

using brave_search_conversion::ConversionType;

void SortBraveSearchPromotionMatch(
AutocompleteResult* result,
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

AutocompleteResult* result,
const AutocompleteInput& input,
brave_search_conversion::ConversionType type) {
if (result->size() == 0)
Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

if (prefs && prefs->GetBoolean(prefs::kDismissed))
return ConversionType::kNone;

if (service) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Added GetConversionType().

// Move match to |index|.
void AutocompleteResult::ReorderMatch(const ACMatches::iterator& it,
size_t index) {
const bool move_to_end = (index < 0) || (index >= size());
Copy link
Member

Choose a reason for hiding this comment

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

I believe index < 0 is always false. move to end works because (size_t)-1 creates max value which is always >= size(). maybe make it as a flag or pass an iterator instead? currently index doesn't do what it's declaring, this is confusing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, I fixed making index as int type.

namespace features {

// Brave search promotion match located at last low in omnibox popup.
// This type seems more ads banner.
Copy link
Member

Choose a reason for hiding this comment

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

more like ads banner?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

bool IsPromotionEnabledCountry(const std::string& country_code) {
constexpr base::StringPiece kSupportedCountries[] = {"US", "CA", "DE", "FR",
"GB"};
return std::find(std::begin(kSupportedCountries),
Copy link
Member

Choose a reason for hiding this comment

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

base::Contains

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

components/brave_search_conversion/utils.cc Outdated Show resolved Hide resolved
components/omnibox/browser/promotion_utils.cc Outdated Show resolved Hide resolved
Copy link
Member Author

@simonhong simonhong left a comment

Choose a reason for hiding this comment

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

@goodov With match's additional info, code becomes much simpler.
Thanks and PTAL again!

namespace features {

// Brave search promotion match located at last low in omnibox popup.
// This type seems more ads banner.
Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

bool IsPromotionEnabledCountry(const std::string& country_code) {
constexpr base::StringPiece kSupportedCountries[] = {"US", "CA", "DE", "FR",
"GB"};
return std::find(std::begin(kSupportedCountries),
Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

components/brave_search_conversion/utils.cc Outdated Show resolved Hide resolved
// Move match to |index|.
void AutocompleteResult::ReorderMatch(const ACMatches::iterator& it,
size_t index) {
const bool move_to_end = (index < 0) || (index >= size());
Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, I fixed making index as int type.

components/omnibox/browser/promotion_utils.cc Outdated Show resolved Hide resolved
@simonhong
Copy link
Member Author

@petemill @aguscruiz Updated screenshots that shows normal/hover or selected state.

@simonhong simonhong force-pushed the brave_search_conversion branch from 21e34c9 to 30dc2c9 Compare June 15, 2022 05:40
enum class ConversionType {
kButton,
kBanner,
kNone,
Copy link
Member

Choose a reason for hiding this comment

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

nit: usually this goes as a first item (kNone == 0) in enums.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

providers_.push_back(new TopSitesProvider(provider_client_.get())); \
providers_.push_back(new SuggestedSitesProvider(provider_client_.get())); \
if (!provider_client_->IsOffTheRecord()) \
providers_.push_back(new PromotionProvider(provider_client_.get()));
Copy link
Member

Choose a reason for hiding this comment

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

@simonhong I think one more thing that we can add into this PR is to skip this and other Init-like places when both features are turned off to make things safe in case we missed something.
you can make a bool function like bool IsBraveSearchConversionEnabled() and insert it into some places to protect ourselves.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok. Added two places. here and UI side (https://github.com/brave/brave-core/pull/12846/files#diff-18e8763975abf287e0ef660d283f3485f5672dd3262390d753b738b132d58262R43).
I thinks double checking at both places (data provider and consumer) would be sufficient.

PromotionProvider Provides the autocomplete match for conversion promotion.
And AutocompleteController adjust that match position based on conversion type.
Implement BraveOmniboxResultView by subclassing OmniboxResultView to provide
promotion specfic views. BraveOmniboxResultView controls visibility upstream children and
promotion children based on match. When promotion match is set, upstream children are all
hidden and promotion children get visible.
Deleted chrome/test dependency from brave/components/search_engines.
As //brave/components/search_engines target is included by
/components/omnibox/browser due to brave search promotion, this deps
caused error.
@simonhong simonhong force-pushed the brave_search_conversion branch from 30dc2c9 to 0eb41e9 Compare June 15, 2022 08:35
Copy link
Member

@petemill petemill left a comment

Choose a reason for hiding this comment

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

Nice work. UI looks really nice and polished.

One very minor issue I'm seeing that might be a general issue with the omnibox: Sometimes when I move the mouse from outside the item (for the Button version) to inside and then to outside again, the hover "sticks"

output

Another non-related issue, but worth noting is that the "set as default" button on brave search is always showing, yet it won't always work since the relevant browser API is limited to a certain amount of calls per day. The website needs to call brave.getCanSetDefaultSearchProvider first to know whether to show the button or not.

Removed extra space between search term and Brave Search string.
Added some comments about promotion view's children.
Updated search promotion url
Added more UI interaction for hover/selected state
Updated test for checking promotion is not added in private profile
@simonhong simonhong force-pushed the brave_search_conversion branch from 0eb41e9 to 983694e Compare June 16, 2022 03:57
@simonhong
Copy link
Member Author

Squashed into 4 commits and merged.

@simonhong simonhong merged commit f279bb5 into master Jun 16, 2022
@simonhong simonhong deleted the brave_search_conversion branch June 16, 2022 03:58
@github-actions github-actions bot added this to the 1.41.x - Nightly milestone Jun 16, 2022
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.

Implement search conversion promo's omnibox option
7 participants