Skip to content

Commit

Permalink
[Brave News]: Notify tabs when there are updates to their feed. (#16048)
Browse files Browse the repository at this point in the history
  • Loading branch information
fallaciousreasoning authored Nov 24, 2022
1 parent b1fe6e1 commit 7bee6e2
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 109 deletions.
31 changes: 31 additions & 0 deletions components/brave_new_tab_ui/api/brave_news/feedListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2022 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/.

import getBraveNewsController from '.'
import {
BraveNewsControllerRemote,
FeedListenerInterface,
FeedListenerReceiver
} from 'gen/brave/components/brave_today/common/brave_news.mojom.m'

export const addFeedListener = (listener: (feedHash: string) => void) =>
new (class implements FeedListenerInterface {
#receiver = new FeedListenerReceiver(this)
#controller: BraveNewsControllerRemote

constructor () {
this.#controller = getBraveNewsController()

if (process.env.NODE_ENV !== 'test') {
this.#controller.addFeedListener(
this.#receiver.$.bindNewPipeAndPassRemote()
)
}
}

onUpdateAvailable (feedHash: string): void {
listener(feedHash)
}
})()
282 changes: 173 additions & 109 deletions components/brave_new_tab_ui/async/today.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ import * as Actions from '../actions/today_actions'
import { ApplicationState } from '../reducers'
import { saveIsBraveTodayOptedIn } from '../api/preferences'
import getBraveNewsController, * as BraveNews from '../api/brave_news'
import store from '../store'
import { addFeedListener } from '../api/brave_news/feedListener'

addFeedListener((hash) => {
const current = store.getState().today.feed?.hash
store.dispatch(Actions.isUpdateAvailable({ isUpdateAvailable: current !== hash }))
})

function storeInHistoryState (data: Object) {
const oldHistoryState = (typeof history.state === 'object') ? history.state : {}
const oldHistoryState =
typeof history.state === 'object' ? history.state : {}
const newHistoryState = { ...oldHistoryState, ...data }
history.pushState(newHistoryState, document.title)
}
Expand All @@ -22,24 +30,27 @@ handler.on(Actions.interactionBegin.getType(), async () => {
getBraveNewsController().onInteractionSessionStarted()
})

handler.on<Actions.RefreshPayload>(Actions.refresh.getType(), async (store, payload) => {
if (payload && payload.isFirstInteraction) {
console.debug('Brave News: Marking actual interaction begin')
getBraveNewsController().onInteractionSessionStarted()
}
try {
console.debug('Brave News: Getting data...')
const [{ feed }, { publishers }] = await Promise.all([
getBraveNewsController().getFeed(),
getBraveNewsController().getPublishers()
])
console.debug('Brave News: ...data received.')
store.dispatch(Actions.dataReceived({ feed, publishers }))
} catch (e) {
console.error('error receiving feed', e)
store.dispatch(Actions.errorGettingDataFromBackground(e))
handler.on<Actions.RefreshPayload>(
Actions.refresh.getType(),
async (store, payload) => {
if (payload && payload.isFirstInteraction) {
console.debug('Brave News: Marking actual interaction begin')
getBraveNewsController().onInteractionSessionStarted()
}
try {
console.debug('Brave News: Getting data...')
const [{ feed }, { publishers }] = await Promise.all([
getBraveNewsController().getFeed(),
getBraveNewsController().getPublishers()
])
console.debug('Brave News: ...data received.')
store.dispatch(Actions.dataReceived({ feed, publishers }))
} catch (e) {
console.error('error receiving feed', e)
store.dispatch(Actions.errorGettingDataFromBackground(e))
}
}
})
)

handler.on(Actions.optIn.getType(), async () => {
saveIsBraveTodayOptedIn(true)
Expand All @@ -54,79 +65,119 @@ handler.on(Actions.ensureSettingsData.getType(), async (store) => {
store.dispatch(Actions.dataReceived({ publishers }))
})

handler.on<Actions.ReadFeedItemPayload>(Actions.readFeedItem.getType(), async (store, payload) => {
const state = store.getState() as ApplicationState
getBraveNewsController().onSessionCardVisitsCountChanged(state.today.cardsVisited)
if (payload.isPromoted) {
const promotedArticle = payload.item.promotedArticle
if (!promotedArticle) {
console.error('Brave News: readFeedItem payload with invalid promoted article', payload)
handler.on<Actions.ReadFeedItemPayload>(
Actions.readFeedItem.getType(),
async (store, payload) => {
const state = store.getState() as ApplicationState
getBraveNewsController().onSessionCardVisitsCountChanged(
state.today.cardsVisited
)
if (payload.isPromoted) {
const promotedArticle = payload.item.promotedArticle
if (!promotedArticle) {
console.error(
'Brave News: readFeedItem payload with invalid promoted article',
payload
)
return
}
if (!payload.promotedUUID) {
console.error(
'Brave News: invalid promotedUUID for readFeedItem',
payload
)
return
}
getBraveNewsController().onPromotedItemVisit(
payload.promotedUUID,
promotedArticle.creativeInstanceId
)
}
const data =
payload.item.article?.data ||
payload.item.promotedArticle?.data ||
payload.item.deal?.data
if (!data) {
console.error(
'Brave News: readFeedItem payload item not present',
payload
)
return
}
if (!payload.promotedUUID) {
console.error('Brave News: invalid promotedUUID for readFeedItem', payload)
if (!payload.openInNewTab) {
// remember article so we can scroll to it on "back" navigation
// TODO(petemill): Type this history.state data and put in an API module
// (see `reducers/today`).
storeInHistoryState({
todayArticle: data,
todayPageIndex: state.today.currentPageIndex,
todayCardsVisited: state.today.cardsVisited
})
// visit article url
window.location.href = data.url.url
} else {
window.open(data.url.url, '_blank')
}
}
)

handler.on<Actions.PromotedItemViewedPayload>(
Actions.promotedItemViewed.getType(),
async (store, payload) => {
if (!payload.item.promotedArticle) {
console.error(
'Brave News: promotedItemViewed invalid promoted article',
payload
)
return
}
getBraveNewsController().onPromotedItemVisit(payload.promotedUUID, promotedArticle.creativeInstanceId)
getBraveNewsController().onPromotedItemView(
payload.uuid,
payload.item.promotedArticle.creativeInstanceId
)
}
const data = payload.item.article?.data || payload.item.promotedArticle?.data ||
payload.item.deal?.data
if (!data) {
console.error('Brave News: readFeedItem payload item not present', payload)
return
)

handler.on<number>(
Actions.feedItemViewedCountChanged.getType(),
async (store, payload) => {
const state = store.getState() as ApplicationState
getBraveNewsController().onSessionCardViewsCountChanged(
state.today.cardsViewed
)
}
if (!payload.openInNewTab) {
// remember article so we can scroll to it on "back" navigation
// TODO(petemill): Type this history.state data and put in an API module
// (see `reducers/today`).
storeInHistoryState({
todayArticle: data,
todayPageIndex: state.today.currentPageIndex,
todayCardsVisited: state.today.cardsVisited
})
// visit article url
window.location.href = data.url.url
} else {
window.open(data.url.url, '_blank')
)

handler.on<Actions.RemoveDirectFeedPayload>(
Actions.removeDirectFeed.getType(),
async (store, payload) => {
getBraveNewsController().removeDirectFeed(payload.directFeed.publisherId)
window.setTimeout(() => {
store.dispatch(Actions.checkForUpdate())
}, 3000)
}
})

handler.on<Actions.PromotedItemViewedPayload>(Actions.promotedItemViewed.getType(), async (store, payload) => {
if (!payload.item.promotedArticle) {
console.error('Brave News: promotedItemViewed invalid promoted article', payload)
return
)

handler.on<Actions.SetPublisherPrefPayload>(
Actions.setPublisherPref.getType(),
async (store, payload) => {
const { publisherId, enabled } = payload
let userStatus =
enabled === null
? BraveNews.UserEnabled.NOT_MODIFIED
: enabled
? BraveNews.UserEnabled.ENABLED
: BraveNews.UserEnabled.DISABLED
getBraveNewsController().setPublisherPref(publisherId, userStatus)
// Refreshing of content after prefs changed is throttled, so wait
// a while before seeing if we have new content yet.
// This doesn't have to be exact since we often check for update when
// opening or scrolling through the feed.
window.setTimeout(() => {
store.dispatch(Actions.checkForUpdate())
}, 3000)
}
getBraveNewsController().onPromotedItemView(payload.uuid, payload.item.promotedArticle.creativeInstanceId)
})

handler.on<number>(Actions.feedItemViewedCountChanged.getType(), async (store, payload) => {
const state = store.getState() as ApplicationState
getBraveNewsController().onSessionCardViewsCountChanged(state.today.cardsViewed)
})

handler.on<Actions.RemoveDirectFeedPayload>(Actions.removeDirectFeed.getType(), async (store, payload) => {
getBraveNewsController().removeDirectFeed(payload.directFeed.publisherId)
window.setTimeout(() => {
store.dispatch(Actions.checkForUpdate())
}, 3000)
})

handler.on<Actions.SetPublisherPrefPayload>(Actions.setPublisherPref.getType(), async (store, payload) => {
const { publisherId, enabled } = payload
let userStatus = (enabled === null)
? BraveNews.UserEnabled.NOT_MODIFIED
: enabled
? BraveNews.UserEnabled.ENABLED
: BraveNews.UserEnabled.DISABLED
getBraveNewsController().setPublisherPref(publisherId, userStatus)
// Refreshing of content after prefs changed is throttled, so wait
// a while before seeing if we have new content yet.
// This doesn't have to be exact since we often check for update when
// opening or scrolling through the feed.
window.setTimeout(() => {
store.dispatch(Actions.checkForUpdate())
}, 3000)
})
)

handler.on(Actions.checkForUpdate.getType(), async function (store) {
const state = store.getState() as ApplicationState
Expand All @@ -137,7 +188,8 @@ handler.on(Actions.checkForUpdate.getType(), async function (store) {
return
}
const hash = state.today.feed.hash
const isUpdateAvailable: {isUpdateAvailable: boolean} = await getBraveNewsController().isFeedUpdateAvailable(hash)
const isUpdateAvailable: { isUpdateAvailable: boolean } =
await getBraveNewsController().isFeedUpdateAvailable(hash)
store.dispatch(Actions.isUpdateAvailable(isUpdateAvailable))
})

Expand All @@ -152,31 +204,43 @@ handler.on(Actions.anotherPageNeeded.getType(), async function (store) {
store.dispatch(Actions.checkForUpdate())
})

handler.on<Actions.VisitDisplayAdPayload>(Actions.visitDisplayAd.getType(), async function (store, payload) {
const state = store.getState() as ApplicationState
const todayPageIndex = state.today.currentPageIndex
getBraveNewsController().onDisplayAdVisit(payload.ad.uuid, payload.ad.creativeInstanceId)
const destinationUrl = payload.ad.targetUrl.url
if (!payload.openInNewTab) {
// Remember display ad location so we can scroll to it on "back" navigation
// We remember position and not ad ID since it can be a different ad on
// a new page load.
// TODO(petemill): Type this history.state data and put in an API module
// (see `reducers/today`).
storeInHistoryState({
todayAdPosition: todayPageIndex,
todayPageIndex,
todayCardsVisited: state.today.cardsVisited
})
// visit article url
window.location.href = destinationUrl
} else {
window.open(destinationUrl, '_blank')
handler.on<Actions.VisitDisplayAdPayload>(
Actions.visitDisplayAd.getType(),
async function (store, payload) {
const state = store.getState() as ApplicationState
const todayPageIndex = state.today.currentPageIndex
getBraveNewsController().onDisplayAdVisit(
payload.ad.uuid,
payload.ad.creativeInstanceId
)
const destinationUrl = payload.ad.targetUrl.url
if (!payload.openInNewTab) {
// Remember display ad location so we can scroll to it on "back" navigation
// We remember position and not ad ID since it can be a different ad on
// a new page load.
// TODO(petemill): Type this history.state data and put in an API module
// (see `reducers/today`).
storeInHistoryState({
todayAdPosition: todayPageIndex,
todayPageIndex,
todayCardsVisited: state.today.cardsVisited
})
// visit article url
window.location.href = destinationUrl
} else {
window.open(destinationUrl, '_blank')
}
}
})

handler.on<Actions.DisplayAdViewedPayload>(Actions.displayAdViewed.getType(), async (store, item) => {
getBraveNewsController().onDisplayAdView(item.ad.uuid, item.ad.creativeInstanceId)
})
)

handler.on<Actions.DisplayAdViewedPayload>(
Actions.displayAdViewed.getType(),
async (store, item) => {
getBraveNewsController().onDisplayAdView(
item.ad.uuid,
item.ad.creativeInstanceId
)
}
)

export default handler.middleware
5 changes: 5 additions & 0 deletions components/brave_today/browser/brave_news_controller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ void BraveNewsController::IsFeedUpdateAvailable(
std::move(callback));
}

void BraveNewsController::AddFeedListener(
mojo::PendingRemote<mojom::FeedListener> listener) {
feed_controller_.AddListener(std::move(listener));
}

void BraveNewsController::GetDisplayAd(GetDisplayAdCallback callback) {
// TODO(petemill): maybe we need to have a way to re-fetch ads_service,
// since it may have been disabled at time of service creation and enabled
Expand Down
1 change: 1 addition & 0 deletions components/brave_today/browser/brave_news_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class BraveNewsController : public KeyedService,
void ClearPrefs() override;
void IsFeedUpdateAvailable(const std::string& displayed_feed_hash,
IsFeedUpdateAvailableCallback callback) override;
void AddFeedListener(mojo::PendingRemote<mojom::FeedListener>) override;
void GetDisplayAd(GetDisplayAdCallback callback) override;
void OnInteractionSessionStarted() override;
void OnSessionCardVisitsCountChanged(
Expand Down
Loading

0 comments on commit 7bee6e2

Please sign in to comment.