[Brave News]: Improve peeking card algorithm (#25355)

* [Brave News]: Update peeking card algorithm

* Algorithm tweaks

* Algorithm tweaks

* [Peeking Card]: Boost top stories

* Fix test compile

* Begin work on tests

* Changes for PR

* Add score cutoff

* Limit by publishers

* Reduce cutoff

* Changes for PR (1/2)

* comment max_candidates
This commit is contained in:
Jay Harris
2024-10-01 00:41:46 +02:00
committed by GitHub
parent e4cc12fb21
commit fbaded98e7
12 changed files with 497 additions and 35 deletions
+2
View File
@@ -46,6 +46,8 @@ static_library("browser") {
"initialization_promise.h",
"network.cc",
"network.h",
"peeking_card.cc",
"peeking_card.h",
"publishers_controller.cc",
"publishers_controller.h",
"publishers_parsing.cc",
@@ -20,6 +20,7 @@
#include "brave/components/brave_news/browser/signal_calculator.h"
#include "brave/components/brave_news/common/brave_news.mojom.h"
#include "brave/components/brave_news/common/features.h"
#include "brave/components/brave_news/common/subscriptions_snapshot.h"
namespace brave_news {
@@ -172,6 +173,7 @@ ArticleInfos GetArticleInfos(const std::string& locale,
} // namespace
FeedGenerationInfo::FeedGenerationInfo(
const SubscriptionsSnapshot& subscriptions,
const std::string& locale,
const FeedItems& feed_items,
const Publishers& publishers,
@@ -179,7 +181,8 @@ FeedGenerationInfo::FeedGenerationInfo(
const Signals& signals,
const std::vector<std::string>& suggested_publisher_ids,
const TopicsResult& topics)
: locale_(locale),
: subscriptions_(subscriptions),
locale_(locale),
channels_(std::move(channels)),
suggested_publisher_ids_(suggested_publisher_ids),
suggested_publisher_ids_span_(base::make_span(suggested_publisher_ids_)) {
@@ -18,12 +18,14 @@
#include "brave/components/brave_news/browser/signal_calculator.h"
#include "brave/components/brave_news/browser/topics_fetcher.h"
#include "brave/components/brave_news/common/brave_news.mojom-forward.h"
#include "brave/components/brave_news/common/subscriptions_snapshot.h"
namespace brave_news {
class FeedGenerationInfo {
public:
FeedGenerationInfo(const std::string& locale,
FeedGenerationInfo(const SubscriptionsSnapshot& subscriptions,
const std::string& locale,
const FeedItems& feed_items,
const Publishers& publishers,
std::vector<std::string> channels,
@@ -51,6 +53,7 @@ class FeedGenerationInfo {
// maintain the list of content groups.
mojom::FeedItemMetadataPtr PickAndConsume(PickArticles picker);
const SubscriptionsSnapshot& subscriptions() { return subscriptions_; }
const std::string locale() { return locale_; }
const Publishers& publishers() { return publishers_; }
@@ -72,6 +75,8 @@ class FeedGenerationInfo {
void ReduceCounts(const mojom::FeedItemMetadataPtr& article,
const ArticleMetadata& meta);
SubscriptionsSnapshot subscriptions_;
std::string locale_;
std::vector<std::string> channels_;
Publishers publishers_;
@@ -115,8 +115,9 @@ TEST_F(BraveNewsFeedGenerationInfoTest, CanCreateFeedGenerationInfo) {
feed_items.push_back(MakeArticleItem(kPublisher1));
feed_items.push_back(MakeArticleItem(kPublisher2));
feed_items.push_back(MakeArticleItem(kPublisherDisabled));
FeedGenerationInfo info("en_NZ", feed_items, publishers,
{kTopNewsChannel, kFooChannel}, signals, {}, topics);
FeedGenerationInfo info(SubscriptionsSnapshot(), "en_NZ", feed_items,
publishers, {kTopNewsChannel, kFooChannel}, signals,
{}, topics);
// ContentGroups and ArticleInfos should be lazily created.
EXPECT_FALSE(HasCreatedArticleInfos(info));
@@ -148,8 +149,9 @@ TEST_F(BraveNewsFeedGenerationInfoTest,
feed_items.push_back(MakeArticleItem(kPublisher1));
feed_items.push_back(MakeArticleItem(kPublisher2));
feed_items.push_back(MakeArticleItem(kPublisherDisabled));
FeedGenerationInfo info("en_NZ", feed_items, publishers,
{kTopNewsChannel, kFooChannel}, signals, {}, topics);
FeedGenerationInfo info(SubscriptionsSnapshot(), "en_NZ", feed_items,
publishers, {kTopNewsChannel, kFooChannel}, signals,
{}, topics);
EXPECT_EQ(3u, info.GetEligibleContentGroups().size());
@@ -174,8 +176,9 @@ TEST_F(BraveNewsFeedGenerationInfoTest,
feed_items.push_back(MakeArticleItem(kPublisher1));
feed_items.push_back(MakeArticleItem(kPublisher2));
feed_items.push_back(MakeArticleItem(kPublisherDisabled));
FeedGenerationInfo info("en_NZ", feed_items, publishers,
{kTopNewsChannel, kFooChannel}, signals, {}, topics);
FeedGenerationInfo info(SubscriptionsSnapshot(), "en_NZ", feed_items,
publishers, {kTopNewsChannel, kFooChannel}, signals,
{}, topics);
EXPECT_EQ(3u, info.GetEligibleContentGroups().size());
EXPECT_EQ(2u, info.EligibleChannels().size());
@@ -18,6 +18,7 @@
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
@@ -31,6 +32,7 @@
#include "brave/components/brave_news/browser/feed_fetcher.h"
#include "brave/components/brave_news/browser/feed_generation_info.h"
#include "brave/components/brave_news/browser/feed_sampling.h"
#include "brave/components/brave_news/browser/peeking_card.h"
#include "brave/components/brave_news/browser/publishers_controller.h"
#include "brave/components/brave_news/browser/signal_calculator.h"
#include "brave/components/brave_news/browser/topics_fetcher.h"
@@ -158,13 +160,15 @@ std::vector<mojom::FeedItemV2Ptr> GenerateBlock(FeedGenerationInfo& info,
// 2. 1 - 5 Inline Articles (a percentage of which might be discover cards).
std::vector<mojom::FeedItemV2Ptr> GenerateBlockFromContentGroups(
FeedGenerationInfo& info,
// Ratio of inline articles to discovery articles.
// discover ratio % of the time, we should do a discover card here instead
// of a roulette card.
// https://docs.google.com/document/d/1bSVHunwmcHwyQTpa3ab4KRbGbgNQ3ym_GHvONnrBypg/edit#heading=h.4rkb0vecgekl
double inline_discovery_ratio =
features::kBraveNewsInlineDiscoveryRatio.Get()) {
PickArticles pick_hero = base::NullCallback()) {
DVLOG(1) << __FUNCTION__;
// Ratio of inline articles to discovery articles.
// discover ratio % of the time, we should do a discover card here instead
// of a roulette card.
// https://docs.google.com/document/d/1bSVHunwmcHwyQTpa3ab4KRbGbgNQ3ym_GHvONnrBypg/edit#heading=h.4rkb0vecgekl
const double inline_discovery_ratio =
features::kBraveNewsInlineDiscoveryRatio.Get();
auto eligible_content_groups = info.GetEligibleContentGroups();
std::vector<mojom::FeedItemV2Ptr> result;
if (info.GetArticleInfos().empty() || eligible_content_groups.empty()) {
@@ -225,11 +229,13 @@ std::vector<mojom::FeedItemV2Ptr> GenerateBlockFromContentGroups(
info.locale(), std::move(eligible_content_groups),
std::move(publisher_id_to_channels));
PickArticles pick_hero = base::BindRepeating(
[](GetWeighting weighting, const ArticleInfos& articles) {
return PickRouletteWithWeighting(articles, std::move(weighting));
},
get_weighting.Run(true));
if (pick_hero.is_null()) {
pick_hero = base::BindRepeating(
[](GetWeighting weighting, const ArticleInfos& articles) {
return PickRouletteWithWeighting(articles, std::move(weighting));
},
get_weighting.Run(true));
}
PickArticles pick_article = base::BindRepeating(
[](base::RepeatingCallback<GetWeighting(bool)> gen_weighting,
@@ -465,15 +471,17 @@ void FeedV2Builder::UpdateRequest::AlsoUpdate(
// static
mojom::FeedV2Ptr FeedV2Builder::GenerateBasicFeed(FeedGenerationInfo info,
PickArticles pick_hero,
PickArticles pick_article) {
PickArticles pick_article,
PickArticles pick_peeking) {
DVLOG(1) << __FUNCTION__;
auto feed = mojom::FeedV2::New();
constexpr size_t kIterationsPerAd = 2;
size_t blocks = 0;
while (!info.GetArticleInfos().empty()) {
auto items = GenerateBlock(info, pick_hero, pick_article,
/*inline_discovery_ratio=*/0);
auto items = GenerateBlock(
info, feed->items.empty() ? pick_peeking : pick_hero, pick_article,
/*inline_discovery_ratio=*/0);
if (items.empty()) {
break;
}
@@ -514,10 +522,14 @@ mojom::FeedV2Ptr FeedV2Builder::GenerateAllFeed(FeedGenerationInfo info) {
return feed;
}
// Step 1: Generate a block
// Step 1: Generate the initial block. We have a special algorithm for the
// first hero card
// (https://docs.google.com/document/d/1HH7pohTPp-8uqdccwK4phOrwqK0-NRo-kkBxTDuWUsM)
// https://docs.google.com/document/d/1bSVHunwmcHwyQTpa3ab4KRbGbgNQ3ym_GHvONnrBypg/edit#heading=h.rkq699fwps0
std::vector<mojom::FeedItemV2Ptr> initial_block =
GenerateBlockFromContentGroups(info);
GenerateBlockFromContentGroups(
info, base::BindRepeating(&PickPeekingCard, info.subscriptions(),
GetTopStoryUrls(info.topics())));
DVLOG(1) << "Step 1: Standard Block (" << initial_block.size()
<< " articles)";
add_items(initial_block);
@@ -629,9 +641,13 @@ void FeedV2Builder::BuildFollowingFeed(
subscriptions, {.signals = true},
mojom::FeedV2Type::NewFollowing(mojom::FeedV2FollowingType::New()),
base::BindOnce([](FeedGenerationInfo info) {
return GenerateBasicFeed(std::move(info),
base::BindRepeating(&PickRoulette),
base::BindRepeating(&PickRoulette));
auto subscriptions = info.subscriptions();
auto top_stories = GetTopStoryUrls(info.topics());
return GenerateBasicFeed(
std::move(info), base::BindRepeating(&PickRoulette),
base::BindRepeating(&PickRoulette),
base::BindRepeating(&PickPeekingCard, std::move(subscriptions),
std::move(top_stories)));
}),
std::move(callback));
}
@@ -678,10 +694,13 @@ void FeedV2Builder::BuildChannelFeed(const SubscriptionsSnapshot& subscriptions,
}
info.raw_feed_items() = std::move(feed_items);
return GenerateBasicFeed(std::move(info),
base::BindRepeating(&PickRoulette),
base::BindRepeating(&PickRoulette));
auto subscriptions = info.subscriptions();
auto top_stories = GetTopStoryUrls(info.topics());
return GenerateBasicFeed(
std::move(info), base::BindRepeating(&PickRoulette),
base::BindRepeating(&PickRoulette),
base::BindRepeating(&PickPeekingCard, std::move(subscriptions),
std::move(top_stories)));
},
channel),
std::move(callback));
@@ -720,6 +739,7 @@ void FeedV2Builder::BuildPublisherFeed(
info.raw_feed_items() = std::move(items);
return GenerateBasicFeed(std::move(info),
base::BindRepeating(&PickFirstIndex),
base::BindRepeating(&PickFirstIndex),
base::BindRepeating(&PickFirstIndex));
},
@@ -970,6 +990,7 @@ void FeedV2Builder::GenerateFeed(const SubscriptionsSnapshot& subscriptions,
subscriptions, std::move(settings),
base::BindOnce(
[](const base::WeakPtr<FeedV2Builder> builder,
const SubscriptionsSnapshot& subscriptions,
mojom::FeedV2TypePtr type, FeedGenerator generate,
BuildFeedCallback callback) {
if (!builder) {
@@ -991,7 +1012,7 @@ void FeedV2Builder::GenerateFeed(const SubscriptionsSnapshot& subscriptions,
}
FeedGenerationInfo info(
locale, builder->raw_feed_items_, publishers,
subscriptions, locale, builder->raw_feed_items_, publishers,
std::move(channels), builder->signals_,
builder->suggested_publisher_ids_, builder->topics_);
@@ -1020,8 +1041,8 @@ void FeedV2Builder::GenerateFeed(const SubscriptionsSnapshot& subscriptions,
std::move(callback).Run(std::move(feed));
},
weak_ptr_factory_.GetWeakPtr(), std::move(type), std::move(generator),
std::move(callback)));
weak_ptr_factory_.GetWeakPtr(), subscriptions, std::move(type),
std::move(generator), std::move(callback)));
}
} // namespace brave_news
@@ -125,7 +125,8 @@ class FeedV2Builder {
static mojom::FeedV2Ptr GenerateBasicFeed(FeedGenerationInfo info,
PickArticles pick_hero,
PickArticles pick_article);
PickArticles pick_article,
PickArticles pick_peeking);
static mojom::FeedV2Ptr GenerateAllFeed(FeedGenerationInfo info);
void UpdateData(const SubscriptionsSnapshot& subscriptions,
@@ -0,0 +1,227 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
#include "brave/components/brave_news/browser/peeking_card.h"
#include <algorithm>
#include <iterator>
#include <map>
#include <optional>
#include <set>
#include <tuple>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/time/time.h"
#include "brave/components/brave_news/browser/channels_controller.h"
#include "brave/components/brave_news/browser/feed_sampling.h"
#include "brave/components/brave_news/browser/topics_fetcher.h"
#include "brave/components/brave_news/common/brave_news.mojom.h"
#include "url/gurl.h"
namespace brave_news {
namespace {
using ItemScore = std::tuple</*index*/ size_t, /*score*/ double>;
constexpr size_t kMaxPeekingCardCandidates = 10;
// The percentage of the final candidates that are allowed to come from the same
// publisher.
constexpr double kMaxPublisherPercentOfCandidates = 0.2;
constexpr double kDirectBoost = 15;
constexpr double kPublisherBoost = 10;
constexpr double kChannelBoost = 5;
constexpr double kTopStoryMultiplier = 1.2;
constexpr double kOneHourMultiplier = 1.5;
constexpr double kThreeHoursMultiplier = 1.3;
constexpr double kSixHoursMultiplier = 1.2;
constexpr double kOneDayMultiplier = 1.1;
constexpr double kMorningNewsBoost = 3;
constexpr double kEveningEntertainmentBoost = 3;
constexpr double kMaxCandidatesScorePercentCutoff = 0.7;
constexpr char kEntertainmentChannel[] = "Entertainment";
} // namespace
base::flat_set<std::string> GetTopStoryUrls(
const base::span<TopicAndArticles>& topics) {
std::vector<std::string> urls;
for (auto& [topic, articles] : topics) {
base::ranges::transform(articles, std::back_inserter(urls),
[](const auto& article) { return article.url; });
}
return base::flat_set<std::string>(urls);
}
std::optional<size_t> PickPeekingCardWithMax(
SubscriptionsSnapshot subscriptions,
base::flat_set<std::string> top_story_urls,
const ArticleInfos& articles,
size_t max_candidates) {
// Store now, so it's consistent for everything.
auto now = base::Time::Now();
auto get_article =
[&articles](size_t index) -> const mojom::FeedItemMetadataPtr& {
return std::get<0>(articles[index]);
};
// Create sets for looking up whether articles are subscribed.
base::flat_set<std::string> subscribed_channels(
subscriptions.GetChannelsFromAllLocales());
std::set<std::string> direct_feed_publishers;
for (const auto& direct : subscriptions.direct_feeds()) {
direct_feed_publishers.insert(direct.id);
}
size_t following_count = subscriptions.enabled_publishers().size() +
subscribed_channels.size() +
subscriptions.direct_feeds().size();
if (following_count == 0) {
return std::nullopt;
}
std::vector<ItemScore> candidates;
for (size_t i = 0; i < articles.size(); ++i) {
auto& [article, metadata] = articles[i];
// Ignore disabled publishers completely - they should never be picked for
// the peeking card.
if (subscriptions.disabled_publishers().contains(article->publisher_id)) {
continue;
}
double score = 0;
// Boost direct feeds.
if (direct_feed_publishers.contains(article->publisher_id)) {
score += kDirectBoost;
// Boost enabled publishers
} else if (subscriptions.enabled_publishers().contains(
article->publisher_id)) {
score += kPublisherBoost;
// Boost enabled channels.
} else if (std::ranges::any_of(article->channels, [&subscribed_channels](
const auto& channel) {
return base::Contains(subscribed_channels, channel);
})) {
score += kChannelBoost;
}
// Article has no score, so we can't do anything with it.
if (score == 0) {
continue;
}
if (base::Contains(top_story_urls, article->url.spec())) {
score *= kTopStoryMultiplier;
}
// Apply a boost to recent articles.
auto elapsed = now - article->publish_time;
if (elapsed <= base::Hours(1)) {
score *= kOneHourMultiplier;
} else if (elapsed <= base::Hours(3)) {
score *= kThreeHoursMultiplier;
} else if (elapsed <= base::Hours(6)) {
score *= kSixHoursMultiplier;
} else if (elapsed <= base::Days(1)) {
score *= kOneDayMultiplier;
} else {
// Decay for a week - after a week there probably isn't much difference.
// (a half life of 1.1 days does nicely here)
score *= std::max(0.1, pow(0.5, elapsed.InDays() / 1.1));
}
candidates.emplace_back(i, score);
}
base::Time::Exploded exploded;
now.LocalExplode(&exploded);
for (auto& [index, score] : candidates) {
const auto& article = get_article(index);
// In the morning, weight news higher
if (6 <= exploded.hour && exploded.hour < 10 &&
base::Contains(article->channels, kTopNewsChannel)) {
score += kMorningNewsBoost;
// In the evening, weight entertainment higher
} else if (17 <= exploded.hour && exploded.hour <= 22 &&
base::Contains(article->channels, kEntertainmentChannel)) {
score += kEveningEntertainmentBoost;
}
}
base::ranges::sort(candidates, [get_article](const auto& a, const auto& b) {
const auto& [a_index, a_score] = a;
const auto& [b_index, b_score] = b;
if (a_score != b_score) {
return a_score > b_score;
}
const auto& a_article = get_article(a_index);
const auto& b_article = get_article(b_index);
return a_article->publish_time > b_article->publish_time;
});
if (candidates.empty()) {
return std::nullopt;
}
std::vector<ItemScore> final_candidates;
std::map<std::string, size_t> seen_publishers;
// Limit each publisher to a percentage of the final candidates (i.e. no more
// than 20% of the candidates should come from one source).
const auto publisher_limit = static_cast<int>(
kMaxPeekingCardCandidates * kMaxPublisherPercentOfCandidates);
// This is the minimum score that we'll consider candidates at;
const auto min_score =
kMaxCandidatesScorePercentCutoff * std::get<1>(candidates.front());
for (auto& [index, score] : candidates) {
if (final_candidates.size() >= max_candidates || score < min_score) {
break;
}
const auto& article = get_article(index);
if (seen_publishers[article->publisher_id] >= publisher_limit) {
continue;
}
seen_publishers[article->publisher_id]++;
final_candidates.emplace_back(index, score);
}
if (final_candidates.empty()) {
return std::nullopt;
}
auto [index, score] = PickRandom(final_candidates);
return index;
}
std::optional<size_t> PickPeekingCard(
SubscriptionsSnapshot subscriptions,
base::flat_set<std::string> top_story_urls,
const ArticleInfos& articles) {
return PickPeekingCardWithMax(std::move(subscriptions),
std::move(top_story_urls), articles,
kMaxPeekingCardCandidates);
}
} // namespace brave_news
@@ -0,0 +1,35 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
#ifndef BRAVE_COMPONENTS_BRAVE_NEWS_BROWSER_PEEKING_CARD_H_
#define BRAVE_COMPONENTS_BRAVE_NEWS_BROWSER_PEEKING_CARD_H_
#include <string>
#include "base/containers/flat_set.h"
#include "brave/components/brave_news/browser/feed_fetcher.h"
#include "brave/components/brave_news/browser/feed_sampling.h"
#include "brave/components/brave_news/browser/topics_fetcher.h"
#include "brave/components/brave_news/common/subscriptions_snapshot.h"
namespace brave_news {
base::flat_set<std::string> GetTopStoryUrls(
const base::span<TopicAndArticles>& topics);
std::optional<size_t> PickPeekingCardWithMax(
SubscriptionsSnapshot subscriptions,
base::flat_set<std::string> top_story_urls,
const ArticleInfos& articles,
size_t max_candidates);
std::optional<size_t> PickPeekingCard(
SubscriptionsSnapshot subscriptions,
base::flat_set<std::string> top_story_urls,
const ArticleInfos& articles);
} // namespace brave_news
#endif // BRAVE_COMPONENTS_BRAVE_NEWS_BROWSER_PEEKING_CARD_H_
@@ -0,0 +1,149 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
#include "brave/components/brave_news/browser/peeking_card.h"
#include <tuple>
#include <utility>
#include <vector>
#include "base/containers/flat_map.h"
#include "brave/components/brave_news/browser/feed_sampling.h"
#include "brave/components/brave_news/common/brave_news.mojom.h"
#include "brave/components/brave_news/common/subscriptions_snapshot.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace brave_news {
namespace {
ArticleInfo FakeInfo(std::string id = "",
std::vector<std::string> channels = {},
GURL url = GURL()) {
static int next_id = 0;
if (id.empty()) {
id = base::NumberToString(++next_id);
}
auto item = mojom::FeedItemMetadata::New();
item->publisher_id = id;
item->channels = channels;
item->url = url;
return std::make_tuple(std::move(item), ArticleMetadata());
}
} // namespace
TEST(BraveNewsPeekingCard, NoArticles) {
EXPECT_EQ(std::nullopt, PickPeekingCard(SubscriptionsSnapshot(), {}, {}));
}
TEST(BraveNewsPeekingCard, NoSubscribedArticles) {
ArticleInfos articles;
articles.push_back(FakeInfo());
articles.push_back(FakeInfo());
articles.push_back(FakeInfo());
EXPECT_EQ(std::nullopt,
PickPeekingCard(SubscriptionsSnapshot(), {}, articles));
}
TEST(BraveNewsPeekingCard, PublishersAreIncluded) {
ArticleInfos articles;
articles.push_back(FakeInfo());
articles.push_back(FakeInfo());
articles.push_back(FakeInfo());
size_t enabled_index = 1;
SubscriptionsSnapshot subscriptions(
{std::get<0>(articles[enabled_index])->publisher_id}, {}, {}, {});
EXPECT_EQ(enabled_index, PickPeekingCardWithMax(subscriptions, {}, articles,
/*max_candidates=*/1));
}
TEST(BraveNewsPeekingCard, ChannelsAreIncluded) {
ArticleInfos articles;
articles.push_back(FakeInfo());
articles.push_back(FakeInfo("foo", {"one"}));
articles.push_back(FakeInfo());
base::flat_map<std::string, std::vector<std::string>> channels;
channels["en_NZ"] = {"one"};
SubscriptionsSnapshot subscriptions({}, {}, {}, channels);
EXPECT_EQ(1, PickPeekingCardWithMax(subscriptions, {}, articles,
/*max_candidates=*/1));
}
TEST(BraveNewsPeekingCard, DirectFeedsAreIncluded) {
ArticleInfos articles;
articles.push_back(FakeInfo());
articles.push_back(FakeInfo("foo"));
articles.push_back(FakeInfo());
std::vector<DirectFeed> feeds;
feeds.push_back({"foo", GURL(), ""});
SubscriptionsSnapshot subscriptions({}, {}, feeds, {});
EXPECT_EQ(1, PickPeekingCardWithMax(subscriptions, {}, articles,
/*max_candidates=*/1));
}
TEST(BraveNewsPeekingCard, DisabledPublishersExcluded) {
ArticleInfos articles;
articles.push_back(FakeInfo("bar"));
articles.push_back(FakeInfo("foo"));
articles.push_back(FakeInfo("frob"));
SubscriptionsSnapshot subscriptions({"foo"}, {"foo", "bar", "frob"}, {}, {});
EXPECT_EQ(std::nullopt, PickPeekingCardWithMax(subscriptions, {}, articles,
/*max_candidates=*/1));
}
TEST(BraveNewsPeekingCard, DirectFeedsAreHigherThanPublishers) {
ArticleInfos articles;
articles.push_back(FakeInfo("combined"));
articles.push_back(FakeInfo("direct"));
articles.push_back(FakeInfo("other"));
std::vector<DirectFeed> feeds;
feeds.push_back({"direct", GURL(), ""});
SubscriptionsSnapshot subscriptions({"other", "combined"}, {}, feeds, {});
EXPECT_EQ(1, PickPeekingCardWithMax(subscriptions, {}, articles,
/*max_candidates=*/1));
}
TEST(BraveNewsPeekingCard, PublishersAreHigherThanChannels) {
ArticleInfos articles;
articles.push_back(FakeInfo("", {"one", "two"}));
articles.push_back(FakeInfo("combined"));
articles.push_back(FakeInfo("", {"two"}));
base::flat_map<std::string, std::vector<std::string>> channels;
channels["en_NZ"] = {"one", "two"};
SubscriptionsSnapshot subscriptions({"other", "combined"}, {}, {}, channels);
EXPECT_EQ(1, PickPeekingCardWithMax(subscriptions, {}, articles,
/*max_candidates=*/1));
}
TEST(BraveNewsPeekingCard, TopNewsBoost) {
ArticleInfos articles;
articles.push_back(FakeInfo("one", {}, GURL("https://one.com/1")));
articles.push_back(FakeInfo("one", {}, GURL("https://one.com/2")));
articles.push_back(FakeInfo("one", {}, GURL("https://one.com/3")));
SubscriptionsSnapshot subscriptions({"one"}, {}, {}, {});
EXPECT_EQ(1, PickPeekingCardWithMax(subscriptions, {"https://one.com/2"},
articles, /*max_candidates=*/1));
}
} // namespace brave_news
@@ -21,6 +21,7 @@ source_set("brave_news_unit_tests") {
"//brave/components/brave_news/browser/feed_sampling_unittest.cc",
"//brave/components/brave_news/browser/html_parsing_unittest.cc",
"//brave/components/brave_news/browser/initialization_promise_unittest.cc",
"//brave/components/brave_news/browser/peeking_card_unittest.cc",
"//brave/components/brave_news/browser/publishers_controller_unittest.cc",
"//brave/components/brave_news/browser/publishers_parsing_unittest.cc",
"//brave/components/brave_news/browser/suggestions_controller_unittest.cc",
@@ -5,7 +5,11 @@
#include "brave/components/brave_news/common/subscriptions_snapshot.h"
#include <algorithm>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
@@ -77,6 +81,15 @@ bool SubscriptionsSnapshot::GetChannelSubscribed(
return false;
}
std::vector<std::string> SubscriptionsSnapshot::GetChannelsFromAllLocales()
const {
std::vector<std::string> result;
for (auto& [locale, channel] : channels_) {
base::ranges::copy(channel, std::back_inserter(result));
}
return result;
}
SubscriptionsDiff SubscriptionsSnapshot::DiffPublishers(
const SubscriptionsSnapshot& old) const {
SubscriptionsDiff result;
@@ -67,6 +67,8 @@ class SubscriptionsSnapshot {
bool GetChannelSubscribed(const std::string& locale,
const std::string& channel) const;
std::vector<std::string> GetChannelsFromAllLocales() const;
// Get the changes to the publisher subscriptions between two snapshots.
// Useful for notifying the front end of publisher changes.
SubscriptionsDiff DiffPublishers(const SubscriptionsSnapshot& old) const;