Skip to content

Commit

Permalink
[Brave News]: FeedV2 should provide notifications when new data is av…
Browse files Browse the repository at this point in the history
…ailable (#21323)
  • Loading branch information
fallaciousreasoning committed Dec 13, 2023
1 parent 8fa5317 commit afadcfa
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 29 deletions.
1 change: 1 addition & 0 deletions browser/ui/webui/brave_webui_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ void CustomizeWebUIHTMLSource(content::WebUI* web_ui,
{ "braveNewsOpenArticlesInNewTab", IDS_BRAVE_NEWS_OPEN_ARTICLES_IN_NEW_TAB},
{ "braveNewsOpenArticlesInCurrentTab", IDS_BRAVE_NEWS_OPEN_ARTICLES_IN_CURRENT_TAB},
{ "braveNewsCaughtUp", IDS_BRAVE_NEWS_CAUGHT_UP},
{ "braveNewsNewContentAvailable", IDS_BRAVE_NEWS_NEW_CONTENT_AVAILABLE},

// Brave News Channels
{ "braveNewsChannel-Brave", IDS_BRAVE_NEWS_CHANNEL_BRAVE},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// 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/.
import Flex from '$web-common/Flex'
import { getLocale } from '$web-common/locale'
import Button from '@brave/leo/react/button'
import { spacing } from '@brave/leo/tokens/css'
import * as React from 'react'
import styled from 'styled-components'
Expand All @@ -10,8 +13,6 @@ import FeedNavigation from '../../../../brave_news/browser/resources/FeedNavigat
import Variables from '../../../../brave_news/browser/resources/Variables'
import { useBraveNews } from '../../../../brave_news/browser/resources/shared/Context'
import { CLASSNAME_PAGE_STUCK } from '../page'
import Button from '@brave/leo/react/button'
import { getLocale } from '$web-common/locale';

const Root = styled(Variables)`
padding-top: ${spacing.xl};
Expand Down Expand Up @@ -50,8 +51,22 @@ const ButtonsContainer = styled.div`
gap: ${spacing.m};
`

const LoadNewContentButton = styled(Button)`
--leo-button-color: var(--bn-glass-10);
border-radius: 20px;
overflow: hidden;
backdrop-filter: brightness(0.8) blur(32px);
position: fixed;
z-index: 1;
top: ${spacing['3Xl']};
flex-grow: 0;
`

export default function FeedV2() {
const { feedV2, setCustomizePage, refreshFeedV2 } = useBraveNews()
const { feedV2, setCustomizePage, refreshFeedV2, feedV2UpdatesAvailable } = useBraveNews()

const ref = React.useRef<HTMLDivElement>()

Expand Down Expand Up @@ -79,7 +94,13 @@ export default function FeedV2() {
<SidebarContainer>
<FeedNavigation />
</SidebarContainer>
<Feed feed={feedV2} />
<Flex align='center' direction='column' gap={spacing.l}>
{feedV2UpdatesAvailable && <LoadNewContentButton onClick={refreshFeedV2}>
{getLocale('braveNewsNewContentAvailable')}
</LoadNewContentButton>}
<Feed feed={feedV2} />
</Flex>

<ButtonsContainer>
<Button kind='outline' onClick={() => setCustomizePage('news')}>
{getLocale('braveNewsCustomizeFeed')}
Expand Down
2 changes: 1 addition & 1 deletion components/brave_news/browser/brave_news_controller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ void BraveNewsController::IsFeedUpdateAvailable(
void BraveNewsController::AddFeedListener(
mojo::PendingRemote<mojom::FeedListener> listener) {
if (MaybeInitFeedV2()) {
feed_controller_.AddListener(std::move(listener));
feed_v2_builder_->AddListener(std::move(listener));
} else {
feed_controller_.AddListener(std::move(listener));
}
Expand Down
42 changes: 28 additions & 14 deletions components/brave_news/browser/feed_v2_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -597,13 +597,20 @@ FeedV2Builder::FeedV2Builder(
signal_calculator_(publishers_controller,
channels_controller,
prefs,
history_service) {}
history_service) {
publishers_observation_.Observe(&publishers_controller);
}

FeedV2Builder::~FeedV2Builder() = default;

void FeedV2Builder::AddListener(
mojo::PendingRemote<mojom::FeedListener> listener) {
listeners_.Add(std::move(listener));
auto id = listeners_.Add(std::move(listener));

auto* instance = listeners_.Get(id);
CHECK(instance);

instance->OnUpdateAvailable(hash_);
}

void FeedV2Builder::BuildFollowingFeed(BuildFeedCallback callback) {
Expand Down Expand Up @@ -724,6 +731,17 @@ void FeedV2Builder::GetSignals(GetSignalsCallback callback) {
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void FeedV2Builder::OnPublishersUpdated(
PublishersController* publishers_controller) {
const auto& publishers = publishers_controller_->GetLastPublishers();
auto channels =
channels_controller_->GetChannelsFromPublishers(publishers, &*prefs_);
hash_ = GetFeedHash(channels, publishers, feed_etags_);
for (const auto& listener : listeners_) {
listener->OnUpdateAvailable(hash_);
}
}

void FeedV2Builder::UpdateData(UpdateSettings settings,
base::OnceCallback<void()> callback) {
if (current_update_) {
Expand Down Expand Up @@ -861,6 +879,13 @@ void FeedV2Builder::OnGotTopics(TopicsResult topics) {
void FeedV2Builder::NotifyUpdateCompleted() {
CHECK(current_update_);

// Recalculate the hash_ - this will be used to mark the source of generated
// feeds.
const auto& publishers = publishers_controller_->GetLastPublishers();
auto channels =
channels_controller_->GetChannelsFromPublishers(publishers, &*prefs_);
hash_ = GetFeedHash(channels, publishers, feed_etags_);

// Fire all the pending callbacks.
for (auto& callback : current_update_->callbacks) {
std::move(callback).Run();
Expand All @@ -870,6 +895,7 @@ void FeedV2Builder::NotifyUpdateCompleted() {
current_update_ = std::move(next_update_);
next_update_ = absl::nullopt;

// Notify listeners of updated hash.
for (const auto& listener : listeners_) {
listener->OnUpdateAvailable(hash_);
}
Expand All @@ -895,24 +921,12 @@ void FeedV2Builder::GenerateFeed(
return;
}

const auto& publishers =
builder->publishers_controller_->GetLastPublishers();
auto channels =
builder->channels_controller_->GetChannelsFromPublishers(
publishers, &*builder->prefs_);
builder->hash_ =
GetFeedHash(channels, publishers, builder->feed_etags_);

auto feed = std::move(build_feed).Run();
feed->construct_time = base::Time::Now();
feed->type = std::move(type);
feed->source_hash = builder->hash_;

std::move(callback).Run(feed->Clone());

for (const auto& listener : builder->listeners_) {
listener->OnUpdateAvailable(builder->hash_);
}
},
weak_ptr_factory_.GetWeakPtr(), std::move(type),
std::move(build_feed), std::move(callback)));
Expand Down
11 changes: 9 additions & 2 deletions components/brave_news/browser/feed_v2_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "brave/components/brave_news/browser/channels_controller.h"
#include "brave/components/brave_news/browser/feed_fetcher.h"
#include "brave/components/brave_news/browser/publishers_controller.h"
Expand All @@ -34,7 +35,7 @@ namespace brave_news {
using BuildFeedCallback = mojom::BraveNewsController::GetFeedV2Callback;
using GetSignalsCallback = mojom::BraveNewsController::GetSignalsCallback;

class FeedV2Builder {
class FeedV2Builder : public PublishersController::Observer {
public:
FeedV2Builder(
PublishersController& publishers_controller,
Expand All @@ -45,7 +46,7 @@ class FeedV2Builder {
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
FeedV2Builder(const FeedV2Builder&) = delete;
FeedV2Builder& operator=(const FeedV2Builder&) = delete;
~FeedV2Builder();
~FeedV2Builder() override;

void AddListener(mojo::PendingRemote<mojom::FeedListener> listener);

Expand All @@ -58,6 +59,9 @@ class FeedV2Builder {

void GetSignals(GetSignalsCallback callback);

// PublishersController::Observer:
void OnPublishersUpdated(PublishersController* controller) override;

private:
using UpdateCallback = base::OnceCallback<void()>;
struct UpdateSettings {
Expand Down Expand Up @@ -135,6 +139,9 @@ class FeedV2Builder {
raw_ref<SuggestionsController> suggestions_controller_;
raw_ref<PrefService> prefs_;

base::ScopedObservation<PublishersController, PublishersController::Observer>
publishers_observation_{this};

FeedFetcher fetcher_;
TopicsFetcher topics_fetcher_;
SignalCalculator signal_calculator_;
Expand Down
20 changes: 14 additions & 6 deletions components/brave_news/browser/resources/shared/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface BraveNewsContext {
locale: string
feedView: FeedView,
feedV2?: FeedV2,
feedV2UpdatesAvailable?: boolean,
refreshFeedV2: () => void,
setFeedView: (feedType: FeedView) => void,
customizePage: NewsPage
Expand All @@ -47,7 +48,8 @@ export const BraveNewsContext = React.createContext<BraveNewsContext>({
locale: '',
feedView: 'all',
feedV2: undefined,
refreshFeedV2: () => {},
feedV2UpdatesAvailable: false,
refreshFeedV2: () => { },
setFeedView: () => { },
customizePage: null,
setCustomizePage: () => { },
Expand All @@ -62,7 +64,7 @@ export const BraveNewsContext = React.createContext<BraveNewsContext>({
isShowOnNTPPrefEnabled: undefined,
toggleBraveNewsOnNTP: (enabled: boolean) => { },
openArticlesInNewTab: true,
setOpenArticlesInNewTab: () => {}
setOpenArticlesInNewTab: () => { }
})

export const publishersCache = new PublishersCachingWrapper()
Expand All @@ -74,7 +76,12 @@ export function BraveNewsContextProvider(props: { children: React.ReactNode }) {

// Note: It's okay to fetch the FeedV2 even when the feature isn't enabled
// because the controller will just return an empty feed.
const { feedV2, feedView, setFeedView, refresh: refreshFeed } = useFeedV2()
const { feedV2,
updatesAvailable: feedV2UpdatesAvailable,
feedView,
setFeedView,
refresh: refreshFeedV2
} = useFeedV2()

const [customizePage, setCustomizePage] = useState<NewsPage>(null)
const [channels, setChannels] = useState<Channels>({})
Expand Down Expand Up @@ -143,8 +150,9 @@ export function BraveNewsContextProvider(props: { children: React.ReactNode }) {
locale,
feedView,
setFeedView,
feedV2: feedV2,
refreshFeedV2: refreshFeed,
feedV2,
feedV2UpdatesAvailable,
refreshFeedV2,
customizePage,
setCustomizePage,
channels,
Expand All @@ -159,7 +167,7 @@ export function BraveNewsContextProvider(props: { children: React.ReactNode }) {
toggleBraveNewsOnNTP,
openArticlesInNewTab: configuration.openArticlesInNewTab,
setOpenArticlesInNewTab
}), [customizePage, setFeedView, feedV2, channels, publishers, suggestedPublisherIds, updateSuggestedPublisherIds, configuration, toggleBraveNewsOnNTP])
}), [customizePage, setFeedView, feedV2, feedV2UpdatesAvailable, channels, publishers, suggestedPublisherIds, updateSuggestedPublisherIds, configuration, toggleBraveNewsOnNTP])

return <BraveNewsContext.Provider value={context}>
{props.children}
Expand Down
2 changes: 1 addition & 1 deletion components/brave_news/browser/resources/shared/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const ArrowRight = <svg width="16" height="15" viewBox="0 0 16 15" fill="

export const channelIcons = {
'default': <Icon name='news-default'/>,
'Brave': <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#6B7084" fill-rule="evenodd" d="m20.69 8.47-.628-1.675.436-.962a.324.324 0 0 0-.068-.366l-1.188-1.18a1.947 1.947 0 0 0-1.995-.454l-.332.113L15.178 2H8.794L7.079 3.97l-.323-.112a1.952 1.952 0 0 0-2.013.46l-1.21 1.2a.258.258 0 0 0-.054.292l.456.998-.625 1.674.405 1.51L5.56 16.87a3.503 3.503 0 0 0 1.382 1.963s2.24 1.549 4.45 2.956c.195.124.398.214.617.21.218.004.421-.086.615-.21 2.483-1.596 4.447-2.962 4.447-2.962a3.504 3.504 0 0 0 1.38-1.966l1.836-6.88.404-1.512Z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="m18.728 8.801-.03.09-.045.16c-.122.15-.38.437-.573.638l-1.773 1.848c-.193.201-.302.453-.192.707l.24.578c.11.254.12.674.014.956-.107.287-.291.54-.533.734l-.185.148a.9.9 0 0 1-.86.101l-.816-.38a4.234 4.234 0 0 1-.843-.55l-.773-.681a.346.346 0 0 1-.02-.505l1.883-1.245c.233-.154.357-.44.224-.683l-.67-1.194c-.132-.243-.185-.567-.117-.719.068-.152.339-.356.602-.454l2.185-.796c.264-.097.25-.198-.03-.224l-1.397-.102c-.28-.026-.486.014-.758.088l-1.056.257c-.271.074-.329.357-.278.628l.436 2.317c.051.271.076.545.056.607-.02.063-.262.164-.537.225l-.361.08a2.837 2.837 0 0 1-1 .007l-.438-.091c-.276-.058-.518-.156-.538-.219-.02-.063.004-.336.055-.607l.433-2.318c.05-.271-.007-.554-.278-.628L9.698 7.32c-.272-.074-.478-.113-.758-.087l-1.396.103c-.28.026-.294.127-.03.224l2.185.794c.264.097.535.301.603.453.068.152.015.476-.117.72l-.668 1.193c-.132.244-.008.53.225.683l1.884 1.243a.346.346 0 0 1-.018.505l-.773.682a4.246 4.246 0 0 1-.842.552l-.816.381a.902.902 0 0 1-.86-.1l-.185-.148a1.727 1.727 0 0 1-.543-.756 1.444 1.444 0 0 1 .022-.933l.24-.578c.109-.255 0-.507-.194-.708L5.882 9.696c-.193-.2-.451-.487-.573-.636l-.047-.16-.028-.09c-.003-.104.034-.433.077-.522.043-.088.207-.348.365-.577l.38-.55c.158-.23.43-.593.606-.808l.557-.684c.175-.216.325-.392.348-.39 0-.002.228.04.504.092l.844.158.678.127c.097.018.395-.037.663-.122l.607-.192c.268-.086.674-.198.903-.25l.212.003.212-.003c.23.052.636.163.904.248l.607.192c.268.085.567.14.663.122a284 284 0 0 0 .56-.106l.118-.022.843-.16c.277-.052.504-.094.52-.093.008 0 .157.174.333.39l.558.683c.176.216.45.579.607.807l.38.55c.159.229.406.644.421.739a2.3 2.3 0 0 1 .024.36Zm-6.635 5.516c.025 0 .258.086.519.192l.241.098c.26.106.679.294.93.419l.713.354c.25.125.269.359.04.52l-.608.425c-.23.16-.59.44-.8.62l-.767.655a.601.601 0 0 1-.758.002c-.206-.178-.548-.47-.76-.65a12.42 12.42 0 0 0-.802-.614l-.606-.42c-.23-.158-.214-.393.036-.52l.716-.366c.25-.127.668-.318.928-.424l.241-.098a5.18 5.18 0 0 1 .518-.193h.22Z" clip-rule="evenodd"/></svg>,
'Brave': <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#6B7084" fill-rule="evenodd" d="m20.69 8.47-.628-1.675.436-.962a.324.324 0 0 0-.068-.366l-1.188-1.18a1.947 1.947 0 0 0-1.995-.454l-.332.113L15.178 2H8.794L7.079 3.97l-.323-.112a1.952 1.952 0 0 0-2.013.46l-1.21 1.2a.258.258 0 0 0-.054.292l.456.998-.625 1.674.405 1.51L5.56 16.87a3.503 3.503 0 0 0 1.382 1.963s2.24 1.549 4.45 2.956c.195.124.398.214.617.21.218.004.421-.086.615-.21 2.483-1.596 4.447-2.962 4.447-2.962a3.504 3.504 0 0 0 1.38-1.966l1.836-6.88.404-1.512Z" clipRule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="m18.728 8.801-.03.09-.045.16c-.122.15-.38.437-.573.638l-1.773 1.848c-.193.201-.302.453-.192.707l.24.578c.11.254.12.674.014.956-.107.287-.291.54-.533.734l-.185.148a.9.9 0 0 1-.86.101l-.816-.38a4.234 4.234 0 0 1-.843-.55l-.773-.681a.346.346 0 0 1-.02-.505l1.883-1.245c.233-.154.357-.44.224-.683l-.67-1.194c-.132-.243-.185-.567-.117-.719.068-.152.339-.356.602-.454l2.185-.796c.264-.097.25-.198-.03-.224l-1.397-.102c-.28-.026-.486.014-.758.088l-1.056.257c-.271.074-.329.357-.278.628l.436 2.317c.051.271.076.545.056.607-.02.063-.262.164-.537.225l-.361.08a2.837 2.837 0 0 1-1 .007l-.438-.091c-.276-.058-.518-.156-.538-.219-.02-.063.004-.336.055-.607l.433-2.318c.05-.271-.007-.554-.278-.628L9.698 7.32c-.272-.074-.478-.113-.758-.087l-1.396.103c-.28.026-.294.127-.03.224l2.185.794c.264.097.535.301.603.453.068.152.015.476-.117.72l-.668 1.193c-.132.244-.008.53.225.683l1.884 1.243a.346.346 0 0 1-.018.505l-.773.682a4.246 4.246 0 0 1-.842.552l-.816.381a.902.902 0 0 1-.86-.1l-.185-.148a1.727 1.727 0 0 1-.543-.756 1.444 1.444 0 0 1 .022-.933l.24-.578c.109-.255 0-.507-.194-.708L5.882 9.696c-.193-.2-.451-.487-.573-.636l-.047-.16-.028-.09c-.003-.104.034-.433.077-.522.043-.088.207-.348.365-.577l.38-.55c.158-.23.43-.593.606-.808l.557-.684c.175-.216.325-.392.348-.39 0-.002.228.04.504.092l.844.158.678.127c.097.018.395-.037.663-.122l.607-.192c.268-.086.674-.198.903-.25l.212.003.212-.003c.23.052.636.163.904.248l.607.192c.268.085.567.14.663.122a284 284 0 0 0 .56-.106l.118-.022.843-.16c.277-.052.504-.094.52-.093.008 0 .157.174.333.39l.558.683c.176.216.45.579.607.807l.38.55c.159.229.406.644.421.739a2.3 2.3 0 0 1 .024.36Zm-6.635 5.516c.025 0 .258.086.519.192l.241.098c.26.106.679.294.93.419l.713.354c.25.125.269.359.04.52l-.608.425c-.23.16-.59.44-.8.62l-.767.655a.601.601 0 0 1-.758.002c-.206-.178-.548-.47-.76-.65a12.42 12.42 0 0 0-.802-.614l-.606-.42c-.23-.158-.214-.393.036-.52l.716-.366c.25-.127.668-.318.928-.424l.241-.098a5.18 5.18 0 0 1 .518-.193h.22Z" clipRule="evenodd"/></svg>,
'Business': <Icon name='news-business'/>,
'Cars': <Icon name='news-car'/>,
'Crypto': <Icon name='crypto-wallets'/>,
Expand Down
1 change: 1 addition & 0 deletions components/brave_news/browser/resources/shared/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { getLocale } from "$web-common/locale"

export const getTranslatedChannelName = (channelName: string) => {
if (!channelName) return ''
try {
return getLocale(`braveNewsChannel-${channelName}`)
} catch (err) {
Expand Down
34 changes: 33 additions & 1 deletion components/brave_news/browser/resources/shared/useFeedV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { useCallback, useEffect, useState } from "react";
import getBraveNewsController, { FeedV2, FeedV2Type } from "./api";
import { addFeedListener } from "./feedListener";

export type FeedView = 'all' | 'following' | `publishers/${string}` | `channels/${string}`

Expand Down Expand Up @@ -84,9 +85,35 @@ const fetchFeed = (feedView: FeedView) => {
})
}

// Clear out of date caches when the feed receives new data.
addFeedListener(latestHash => {
// Delete everything in the localCache which wasn't generated from the latest
// data - the last visited feed is stored in under |FEED_KEY| so clicking an
// article and coming back will still work.
for (const key in localCache) {
if (localCache[key].sourceHash === latestHash) continue
delete localCache[key]
}

// If what's in localStorage isn't from the latest data, make sure we remove
// it. Without the eslint-disable-next-line comment the below will fail on iOS
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const localStorageData = JSON.parse(localStorage.getItem(FEED_KEY)!) as FeedV2 | null
if (localStorageData?.sourceHash !== latestHash) {
localStorage.removeItem(FEED_KEY)
}
})

export const useFeedV2 = () => {
const [feedV2, setFeedV2] = useState<FeedV2 | undefined>(maybeLoadFeed())
const [feedView, setFeedView] = useState<FeedView>(feedTypeToFeedView(feedV2?.type))
const [hash, setHash] = useState<string>()

// Add a listener for the latest hash.
useEffect(() => {
// Note: A new feed listener will be notified with the latest hash.
addFeedListener(setHash)
}, [])

useEffect(() => {
const cachedFeed = maybeLoadFeed(feedView)
Expand All @@ -110,10 +137,15 @@ export const useFeedV2 = () => {
fetchFeed(feedView).then(setFeedV2)
}, [feedView])

// Updates are available if we've been told the latest hash, we have a feed
// and the hashes don't match.
const updatesAvailable = !!(hash && feedV2 && hash !== feedV2.sourceHash)
console.log("Latest hash: ", hash, "Current hash:", feedV2?.sourceHash)
return {
feedV2,
feedView,
setFeedView,
refresh
refresh,
updatesAvailable
}
}
3 changes: 3 additions & 0 deletions components/resources/brave_news_strings.grdp
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,7 @@
<message name="IDS_BRAVE_NEWS_CAUGHT_UP" desc="Text for when the user reaches the end of the feed">
You're all caught up
</message>
<message name="IDS_BRAVE_NEWS_NEW_CONTENT_AVAILABLE" desc="Indicates that new content is available, and prompts the user to load the new data">
New content available. Reload?
</message>
</grit-part>

0 comments on commit afadcfa

Please sign in to comment.