diff --git a/.storybook/BUILD.gn b/.storybook/BUILD.gn index 99a81a53497d..09924b57b12d 100644 --- a/.storybook/BUILD.gn +++ b/.storybook/BUILD.gn @@ -11,8 +11,16 @@ group("storybook") { # are disabled in a regular brave build due to build flags, # they will be generated before storybook is compiled. deps = [ + "//brave/components/brave_new_tab_ui:mojom_js", + "//brave/components/brave_shields/common:mojom_js", + "//brave/components/brave_today/common:mojom_js", "//brave/components/brave_vpn/mojom:mojom_js", "//brave/components/brave_wallet/common:mojom_js", + "//brave/components/speedreader/common:mojom_js", + "//mojo/public/js:bindings", + "//mojo/public/js:resources", "//ui/webui/resources/js:cr.m", + "//ui/webui/resources/js:modulize", + "//ui/webui/resources/js:preprocess", ] } diff --git a/.storybook/chrome-resources-mock/js/plural_string_proxy.js.ts b/.storybook/chrome-resources-mock/js/plural_string_proxy.js.ts new file mode 100644 index 000000000000..b47f1ac7c0ff --- /dev/null +++ b/.storybook/chrome-resources-mock/js/plural_string_proxy.js.ts @@ -0,0 +1,16 @@ +// 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 http://mozilla.org/MPL/2.0/. + +let instance: PluralStringProxyImpl | null = null + +export class PluralStringProxyImpl { + static getInstance() { + return instance || (instance = new PluralStringProxyImpl()) + } + + getPluralString(key: string, count: number): Promise { + return Promise.resolve(`${key}(${count})`) + } +} diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index dd596e4c27d8..5429277294a9 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -90,7 +90,7 @@ module.exports = async ({ config, mode }) => { // build, but has previously made a Component build, then an outdated // version of a module will be used. Instead, accept a cli argument // or environment variable containing which build target to use. - ...getBuildOuptutPathList('gen/ui/webui/resources') + ...getBuildOuptutPathList('gen/ui/webui/resources/preprocessed') ] }, { diff --git a/BUILD.gn b/BUILD.gn index 5372051630f6..2a49077bd45e 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -57,6 +57,10 @@ if (!is_ios) { deps += [ ":packed_resources" ] } } + + group("storybook") { + deps = [ "//brave/.storybook:storybook" ] + } } if (is_win) { @@ -542,10 +546,6 @@ group("tools") { } } -group("storybook") { - deps = [ "//brave/.storybook:storybook" ] -} - if (is_redirect_cc_build) { group("redirect_cc") { deps = [ "//brave/tools/redirect_cc" ] diff --git a/browser/ui/webui/brave_webui_source.cc b/browser/ui/webui/brave_webui_source.cc index c121ef48cd2c..bc834cdf0cce 100644 --- a/browser/ui/webui/brave_webui_source.cc +++ b/browser/ui/webui/brave_webui_source.cc @@ -206,6 +206,24 @@ void CustomizeWebUIHTMLSource(content::WebUI* web_ui, { "promoted", IDS_BRAVE_TODAY_PROMOTED }, { "ad", IDS_BRAVE_TODAY_DISPLAY_AD_LABEL }, + { "braveNewsBackToDashboard", IDS_BRAVE_NEWS_BACK_TO_DASHBOARD }, + { "braveNewsDisabledPlaceholderHeader", IDS_BRAVE_NEWS_DISABLED_PLACEHOLDER_HEADER }, // NOLINT + { "braveNewsDisabledPlaceholderSubtitle", IDS_BRAVE_NEWS_DISABLED_PLACEHOLDER_SUBTITLE }, // NOLINT + { "braveNewsDisabledPlaceholderEnableButton", IDS_BRAVE_NEWS_DISABLED_PLACEHOLDER_ENABLE_BUTTON }, // NOLINT + { "braveNewsSearchPlaceholderLabel", IDS_BRAVE_NEWS_SEARCH_PLACEHOLDER_LABEL}, // NOLINT + { "braveNewsChannelsHeader", IDS_BRAVE_NEWS_BROWSE_CHANNELS_HEADER}, // NOLINT + { "braveNewsLoadMoreCategoriesButton", IDS_BRAVE_NEWS_LOAD_MORE_CATEGORIES_BUTTON }, // NOLINT + { "braveNewsAllSourcesHeader", IDS_BRAVE_NEWS_ALL_SOURCES_HEADER}, + { "braveNewsFeedsHeading", IDS_BRAVE_NEWS_FEEDS_HEADING}, + { "braveNewsFollowButtonFollowing", IDS_BRAVE_NEWS_FOLLOW_BUTTON_FOLLOWING}, // NOLINT + { "braveNewsFollowButtonNotFollowing", IDS_BRAVE_NEWS_FOLLOW_BUTTON_NOT_FOLLOWING}, // NOLINT + { "braveNewsDirectSearchButton", IDS_BRAVE_NEWS_DIRECT_SEARCH_BUTTON}, // NOLINT + { "braveNewsDirectSearchNoResults", IDS_BRAVE_NEWS_DIRECT_SEARCH_NO_RESULTS}, // NOLINT + { "braveNewsSearchResultsNoResults", IDS_BRAVE_NEWS_SEARCH_RESULTS_NO_RESULTS}, // NOLINT + { "braveNewsSearchResultsLocalResults", IDS_BRAVE_NEWS_SEARCH_RESULTS_LOCAL_RESULTS}, // NOLINT + { "braveNewsSearchResultsDirectResults", IDS_BRAVE_NEWS_SEARCH_RESULTS_DIRECT_RESULTS}, // NOLINT + { "braveNewsSearchQueryTooShort", IDS_BRAVE_NEWS_SEARCH_QUERY_TOO_SHORT}, // NOLINT + { "addWidget", IDS_BRAVE_NEW_TAB_WIDGET_ADD }, { "hideWidget", IDS_BRAVE_NEW_TAB_WIDGET_HIDE }, { "rewardsWidgetDesc", IDS_BRAVE_NEW_TAB_REWARDS_WIDGET_DESC }, diff --git a/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.cc b/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.cc index 901548de05d1..ab3968e066e4 100644 --- a/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.cc +++ b/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.cc @@ -36,7 +36,9 @@ #include "chrome/browser/browser_process.h" #include "chrome/browser/first_run/first_run.h" #include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/webui/plural_string_handler.h" #include "chrome/common/chrome_features.h" +#include "components/grit/brave_components_strings.h" #include "components/prefs/pref_change_registrar.h" #include "components/prefs/pref_registry_simple.h" #include "components/prefs/pref_service.h" @@ -208,6 +210,11 @@ void BraveNewTabMessageHandler::RegisterMessages() { // - Stats // - Preferences // - PrivatePage properties + auto plural_string_handler = std::make_unique(); + plural_string_handler->AddLocalizedString("braveNewsSourceCount", + IDS_BRAVE_NEWS_SOURCE_COUNT); + web_ui()->AddMessageHandler(std::move(plural_string_handler)); + web_ui()->RegisterMessageCallback( "getNewTabPagePreferences", base::BindRepeating(&BraveNewTabMessageHandler::HandleGetPreferences, diff --git a/components/brave_new_tab_ui/api/brave_news/index.ts b/components/brave_new_tab_ui/api/brave_news/index.ts index 9dcdf1e445b4..18443eaeaf5b 100644 --- a/components/brave_new_tab_ui/api/brave_news/index.ts +++ b/components/brave_new_tab_ui/api/brave_news/index.ts @@ -20,7 +20,10 @@ export default function getBraveNewsController () { // doesn't try to connect, or pages which use exported types // but ultimately don't fetch any data. if (!braveNewsControllerInstance) { - braveNewsControllerInstance = BraveNews.BraveNewsController.getRemote() + // In Storybook, we have a mocked BraveNewsController because none of the + // mojo apis are available. + // @ts-expect-error + braveNewsControllerInstance = window.storybookBraveNewsController || BraveNews.BraveNewsController.getRemote() } return braveNewsControllerInstance } diff --git a/components/brave_new_tab_ui/api/brave_news/news.ts b/components/brave_new_tab_ui/api/brave_news/news.ts new file mode 100644 index 000000000000..46bc4a384db4 --- /dev/null +++ b/components/brave_new_tab_ui/api/brave_news/news.ts @@ -0,0 +1,157 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import { BraveNewsControllerRemote, Publisher, PublisherType, UserEnabled } from 'gen/brave/components/brave_today/common/brave_news.mojom.m' +import getBraveNewsController, { Channels, Publishers } from '.' + +type PublishersListener = (publishers: Publishers, oldValue: Publishers) => void +type ChannelsListener = (newValue: Channels, oldValue: Channels) => void + +export const isPublisherEnabled = (publisher: Publisher) => { + if (!publisher) return false + + // Direct Sources are enabled if they're available. + if (publisher.type === PublisherType.DIRECT_SOURCE) return true + + // Publishers enabled via channel are not shown in the sidebar. + return publisher.userEnabledStatus === UserEnabled.ENABLED +} + +export const isDirectFeed = (publisher: Publisher) => { + if (!publisher) return false + return publisher.type === PublisherType.DIRECT_SOURCE +} + +class BraveNewsApi { + controller: BraveNewsControllerRemote + + publishersListeners: PublishersListener[] = [] + lastPublishers: Publishers = {} + + channelsListeners: ChannelsListener[] = [] + lastChannels: Channels = {} + + locale: string + + constructor () { + this.controller = getBraveNewsController() + this.updateChannels() + + this.controller.getLocale().then(({ locale }) => { + this.locale = locale + this.updatePublishers() + }) + } + + getPublishers () { + return this.lastPublishers + } + + getChannels () { + return this.lastChannels + } + + async setPublisherPref (publisherId: string, status: UserEnabled) { + const newValue = { + ...this.lastPublishers, + [publisherId]: { + ...this.lastPublishers[publisherId], + userEnabledStatus: status + } + } + + // We completely remove direct feeds when setting the UserEnabled status + // to DISABLED. + const publisher = this.lastPublishers[publisherId] + if (isDirectFeed(publisher) && status === UserEnabled.DISABLED) { + delete newValue[publisherId] + } + + this.controller.setPublisherPref(publisherId, status) + this.updatePublishers(newValue) + } + + async subscribeToDirectFeed (feedUrl: string) { + const { publishers } = await this.controller.subscribeToNewDirectFeed({ url: feedUrl }) + this.updatePublishers(publishers) + } + + setPublisherSubscribed (publisherId: string, enabled: boolean) { + this.setPublisherPref(publisherId, enabled ? UserEnabled.ENABLED : UserEnabled.DISABLED) + } + + async setChannelSubscribed (channelId: string, subscribed: boolean) { + // While we're waiting for the new channels to come back, speculatively + // update them, so the UI has instant feedback. + this.updateChannels({ + ...this.lastChannels, + [channelId]: { + ...this.lastChannels[channelId], + subscribed + } + }) + + // Then, once we receive the actual update, apply it. + const { updated } = await this.controller.setChannelSubscribed(channelId, subscribed) + this.updateChannels({ + ...this.lastChannels, + [channelId]: updated + }) + } + + async updatePublishers (newPublishers?: Publishers) { + if (!newPublishers) { + ({ publishers: newPublishers } = await this.controller.getPublishers()) + } + + const oldValue = this.lastPublishers + this.lastPublishers = newPublishers! + + this.notifyPublishersListeners(newPublishers!, oldValue) + } + + async updateChannels (newChannels?: Channels) { + if (!newChannels) { + ({ channels: newChannels } = await this.controller.getChannels()) + } + + const oldValue = this.lastChannels + this.lastChannels = newChannels! + + this.notifyChannelsListeners(this.lastChannels, oldValue) + } + + addPublishersListener (listener: PublishersListener) { + this.publishersListeners.push(listener) + } + + removePublishersListener (listener: PublishersListener) { + const index = this.publishersListeners.indexOf(listener) + this.publishersListeners.splice(index, 1) + } + + addChannelsListener (listener: ChannelsListener) { + this.channelsListeners.push(listener) + } + + removeChannelsListener (listener: ChannelsListener) { + const index = this.channelsListeners.indexOf(listener) + this.channelsListeners.splice(index, 1) + } + + notifyPublishersListeners (newValue: Publishers, oldValue: Publishers) { + for (const listener of this.publishersListeners) { + listener(newValue, oldValue) + } + } + + notifyChannelsListeners (newValue: Channels, oldValue: Channels) { + for (const listener of this.channelsListeners) { + listener(newValue, oldValue) + } + } +} + +export const api = new BraveNewsApi() diff --git a/components/brave_new_tab_ui/components/Flex.tsx b/components/brave_new_tab_ui/components/Flex.tsx new file mode 100644 index 000000000000..ceecb394675d --- /dev/null +++ b/components/brave_new_tab_ui/components/Flex.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components' + +interface FlexProps { + align?: 'start' | 'end' | 'center' | 'flex-end' | 'flex-start' | 'self-start' | 'self-end' + justify?: 'start' | 'end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' | 'left' | 'right' + direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse' + gap?: number | string +} + +const Flex = styled('div') ` + display: flex; + flex-direction: ${p => p.direction}; + justify-content: ${p => p.justify}; + align-items: ${p => p.align}; + gap: ${p => typeof p.gap === 'number' ? `${p.gap}px` : p.gap}; +` + +export default Flex diff --git a/components/brave_new_tab_ui/components/default/braveToday/cards/CardImage.tsx b/components/brave_new_tab_ui/components/default/braveToday/cards/CardImage.tsx index 365774aa8ff8..5686a3bf4cd6 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/cards/CardImage.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/cards/CardImage.tsx @@ -8,54 +8,66 @@ import * as Card from '../cardSizes' import getBraveNewsController, * as BraveNews from '../../../../api/brave_news' type Props = { - imageUrl: string + imageUrl?: string list?: boolean - isUnpadded?: boolean isPromoted?: boolean onLoaded?: () => any } -function useGetUnpaddedImage (paddedUrl: string, isUnpadded: boolean, onLoaded?: () => any) { +const cache: { [url: string]: string } = {} + +export function useGetUnpaddedImage (paddedUrl: string | undefined, onLoaded?: () => any, useCache?: boolean) { const [unpaddedUrl, setUnpaddedUrl] = React.useState('') - const onReceiveUnpaddedUrl = (result: string) => { - setUnpaddedUrl(result) - window.requestAnimationFrame(() => { - if (onLoaded) { - onLoaded() - } - }) - } + React.useEffect(() => { + const onReceiveUnpaddedUrl = (result: string) => { + if (useCache) cache[paddedUrl!] = result + setUnpaddedUrl(result) + + if (onLoaded) window.requestAnimationFrame(() => onLoaded()) + } + // Storybook method // @ts-expect-error if (window.braveStorybookUnpadUrl) { // @ts-expect-error window.braveStorybookUnpadUrl(paddedUrl) - .then(onReceiveUnpaddedUrl) + .then(onReceiveUnpaddedUrl) + return + } + + if (!paddedUrl) return + + if (cache[paddedUrl]) { + onReceiveUnpaddedUrl(cache[paddedUrl]) return } let blobUrl: string getBraveNewsController().getImageData({ url: paddedUrl }) - .then(async (result) => { - if (!result.imageData) { - return + .then(async (result) => { + if (!result.imageData) { + return + } + + const blob = new Blob([new Uint8Array(result.imageData)], { type: 'image/*' }) + blobUrl = URL.createObjectURL(blob) + onReceiveUnpaddedUrl(blobUrl) + }) + .catch(err => { + console.error(`Error getting image for ${paddedUrl}.`, err) + }) + + // Only revoke the URL if we aren't using the cache. + return () => { + if (!useCache) URL.revokeObjectURL(blobUrl) } - const blob = new Blob([new Uint8Array(result.imageData)], { type: 'image/*' }) - blobUrl = URL.createObjectURL(blob) - onReceiveUnpaddedUrl(blobUrl) - }) - .catch(err => { - console.error(`Error getting image for ${paddedUrl}.`, err) - }) - - return () => URL.revokeObjectURL(blobUrl) - }, [paddedUrl, isUnpadded]) + }, [paddedUrl]) return unpaddedUrl } export default function CardImage (props: Props) { - const unpaddedUrl = useGetUnpaddedImage(props.imageUrl, !!props.isUnpadded, props.onLoaded) + const unpaddedUrl = useGetUnpaddedImage(props.imageUrl, props.onLoaded) const [isImageLoaded, setIsImageLoaded] = React.useState(false) React.useEffect(() => { if (unpaddedUrl) { diff --git a/components/brave_new_tab_ui/components/default/braveToday/cards/displayAd/index.tsx b/components/brave_new_tab_ui/components/default/braveToday/cards/displayAd/index.tsx index bf322645ff7e..38a29dbb6662 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/cards/displayAd/index.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/cards/displayAd/index.tsx @@ -60,14 +60,7 @@ export default function CardDisplayAd (props: Props) { // verbose ref type conversion due to https://stackoverflow.com/questions/61102101/cannot-assign-refobjecthtmldivelement-to-refobjecthtmlelement-instance return
} />
} - let isImageUnpadded = true - let imageUrl: string = '' - if (content.image.paddedImageUrl) { - imageUrl = content.image.paddedImageUrl.url - isImageUnpadded = false - } else if (content.image.imageUrl) { - imageUrl = content.image.imageUrl.url - } + const imageUrl = content.image.paddedImageUrl?.url || content.image.imageUrl?.url // Render ad when one is available for this unit // TODO(petemill): Avoid nested links return ( @@ -77,7 +70,6 @@ export default function CardDisplayAd (props: Props) { diff --git a/components/brave_new_tab_ui/components/default/braveToday/content.tsx b/components/brave_new_tab_ui/components/default/braveToday/content.tsx index ae105d98c308..9f337cba05fa 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/content.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/content.tsx @@ -15,6 +15,8 @@ import CardsGroup from './cardsGroup' import Customize from './options/customize' import { attributeNameCardCount, Props } from './' import Refresh from './options/refresh' +import { useBraveNews } from './customize/Context' +import { loadTimeData } from '../../../../common/loadTimeData' function getFeedHashForCache (feed?: Feed) { return feed ? feed.hash : '' @@ -26,6 +28,7 @@ export default function BraveTodayContent (props: Props) { const { feed, publishers } = props const dispatch = useDispatch() + const { setCustomizePage } = useBraveNews() const previousYAxis = React.useRef(0) const [showOptions, setShowOptions] = React.useState(false) @@ -67,7 +70,7 @@ export default function BraveTodayContent (props: Props) { // Show if target article is inside or above viewport. const isInteracting = entries.some( entry => entry.isIntersecting || - entry.boundingClientRect.top < 0 + entry.boundingClientRect.top < 0 ) console.debug('Brave News: Intersection Observer trigger show options, changing', isInteracting) setShowOptions(isInteracting) @@ -172,8 +175,8 @@ export default function BraveTodayContent (props: Props) { let runningCardCount = introCount return ( <> - {/* featured item */} - { feed.featuredItem &&
- { !isOnlyDisplayingPeekingCard && - <> + {!isOnlyDisplayingPeekingCard && <> - + +
+ + { + /* Infinitely repeating collections of content. */ + Array(displayedPageCount).fill(undefined).map((_: undefined, index: number) => { + const shouldScrollToDisplayAd = props.displayAdToScrollTo === (index + 1) + let startingDisplayIndex = runningCardCount + runningCardCount += feed.pages[index].items.length + return ( + + ) + }) + } + loadTimeData.getBoolean('featureFlagBraveNewsV2Enabled') + ? setCustomizePage('news') + : props.onCustomizeBraveToday()} show={showOptions} /> + +
-
- { - /* Infinitely repeating collections of content. */ - Array(displayedPageCount).fill(undefined).map((_: undefined, index: number) => { - const shouldScrollToDisplayAd = props.displayAdToScrollTo === (index + 1) - let startingDisplayIndex = runningCardCount - runningCardCount += feed.pages[index].items.length - return ( - - ) - }) - } - - -
- } ) diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/ChannelCard.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/ChannelCard.tsx new file mode 100644 index 000000000000..67b09c731555 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/ChannelCard.tsx @@ -0,0 +1,49 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import * as React from 'react' +import Flex from '../../../Flex' +import { getCardColor } from './colors' +import FollowButton from './FollowButton' +import { useChannelSubscribed } from './Context' + +const Container = styled(Flex) <{ backgroundColor: string }>` + height: 80px; + font-weight: 600; + font-size: 14px; + border-radius: 8px; + background: ${p => p.backgroundColor}; + padding: 16px 20px; + color: white; + position: relative; + + :hover { + opacity: 0.8; + } +` + +const SubscribeButton = styled(FollowButton)` + position: absolute; + top: 8px; + right: 8px; +` + +interface Props { + channelId: string +} + +export default function ChannelCard ({ channelId }: Props) { + const { subscribed, setSubscribed } = useChannelSubscribed(channelId) + return + setSubscribed(!subscribed)} /> + {channelId} + +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Configure.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Configure.tsx new file mode 100644 index 000000000000..3bb4a27dd616 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Configure.tsx @@ -0,0 +1,149 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import styled from 'styled-components' +import Flex from '../../../Flex' +import Discover from './Discover' +import { BackArrow, Cross } from './Icons' +import Button from '$web-components/button' +import Toggle from '$web-components/toggle' +import SourcesList from './SourcesList' +import DisabledPlaceholder from './DisabledPlaceholder' +import { useNewTabPref } from '../../../../hooks/usePref' +import { useBraveNews } from './Context' +import { getLocale } from '$web-common/locale' +import { formatMessage } from '../../../../../brave_rewards/resources/shared/lib/locale_context' + +const Grid = styled.div` + width: 100%; + height: 100%; + + display: grid; + grid-template-columns: 250px auto; + grid-template-rows: 64px 2px auto; + + grid-template-areas: + "back-button header" + "separator separator" + "sidebar content"; +` + +const Header = styled(Flex)` + grid-area: header; + padding: 24px; +` + +const HeaderText = styled.span` + font-size: 16px; + font-weight: 500; +` + +const CloseButtonContainer = styled.div` + &> button { + --inner-border-size: 0; + --outer-border-size: 0; + padding: 12px; + width: 32px; + height: 32px; + } +` + +const BackButtonContainer = styled.div` + grid-area: back-button; + align-items: center; + display: flex; + padding: 12px; + + &> button { + --inner-border-size: 0; + --outer-border-size: 0; + } +` + +const BackButtonText = styled.span` + font-size: 12px; + line-height: 1; +` + +const Hr = styled.hr` + grid-area: separator; + width: 100%; + align-self: center; + background: var(--divider1); + height: 2px; + border-width: 0; +` + +const Sidebar = styled.div` + position: relative; + overflow: auto; + grid-area: sidebar; + padding: 28px 32px; + background: var(--background2); +` + +// Overlay on top of the sidebar, shown when it is disabled. +const SidebarOverlay = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: var(--background1); + opacity: 0.7; +` + +const Content = styled.div` + grid-area: content; + overflow: auto; + padding: 20px 64px; +` + +export default function Configure () { + const [enabled, setEnabled] = useNewTabPref('isBraveTodayOptedIn') + const { setCustomizePage } = useBraveNews() + + let content: JSX.Element + if (!enabled) { + content = setEnabled(true)} /> + } else { + content = + } + + return ( + + + + +
+ + + + {enabled && + {getLocale('braveTodayTitle')} + + } +
+
+ + + {!enabled && } + + + {content} + +
+ ) +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Context.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Context.tsx new file mode 100644 index 000000000000..30685315665d --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Context.tsx @@ -0,0 +1,129 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { useCallback, useMemo, useState } from 'react' +import { Channels, Publisher, Publishers, PublisherType } from '../../../../api/brave_news' +import { api, isPublisherEnabled } from '../../../../api/brave_news/news' +import Modal from './Modal' + +// Leave possibility for more pages open. +type NewsPage = null + | 'news' + +interface BraveNewsContext { + customizePage: NewsPage + setCustomizePage: (page: NewsPage) => void + channels: Channels + // All global Publishers + publishers: Publishers + sortedPublishers: Publisher[] + // Publishers to offer the user (i.e. from current locale) + filteredPublisherIds: string[] + // Publishers the user is directly subscribed to + subscribedPublisherIds: string[] +} + +export const BraveNewsContext = React.createContext({ + customizePage: null, + setCustomizePage: () => { }, + publishers: {}, + sortedPublishers: [], + filteredPublisherIds: [], + subscribedPublisherIds: [], + channels: {} +}) + +export function BraveNewsContextProvider (props: { children: React.ReactNode }) { + const [customizePage, setCustomizePage] = useState(null) + const [channels, setChannels] = useState({}) + const [publishers, setPublishers] = useState({}) + + React.useEffect(() => { + const handler = () => setChannels(api.getChannels()) + handler() + + api.addChannelsListener(handler) + return () => api.removeChannelsListener(handler) + }, []) + + React.useEffect(() => { + const handler = () => setPublishers(api.getPublishers()) + handler() + + api.addPublishersListener(handler) + return () => api.removePublishersListener(handler) + }, []) + + const sortedPublishers = useMemo(() => + Object.values(publishers) + .sort((a, b) => a.publisherName.localeCompare(b.publisherName)), + [publishers]) + + const filteredPublisherIds = useMemo(() => + sortedPublishers + .filter(p => p.type === PublisherType.DIRECT_SOURCE || p.locales.includes(api.locale)) + .map(p => p.publisherId), + [sortedPublishers]) + + const subscribedPublisherIds = useMemo(() => + sortedPublishers.filter(isPublisherEnabled).map(p => p.publisherId), + [sortedPublishers]) + + const context = useMemo(() => ({ + customizePage, + setCustomizePage, + channels, + publishers, + sortedPublishers, + filteredPublisherIds, + subscribedPublisherIds + }), [customizePage, channels, publishers]) + + return + {props.children} + + +} + +export const useBraveNews = () => { + return React.useContext(BraveNewsContext) +} + +export const useChannels = (options: { subscribedOnly: boolean } = { subscribedOnly: false }) => { + const { channels } = useBraveNews() + return useMemo(() => Object.values(channels) + .filter(c => c.subscribed || !options.subscribedOnly), [channels, options.subscribedOnly]) +} + +export const useChannelSubscribed = (channelId: string) => { + const { channels } = useBraveNews() + const subscribed = useMemo(() => channels[channelId]?.subscribed ?? false, [channels[channelId]]) + const setSubscribed = React.useCallback((subscribed: boolean) => { + api.setChannelSubscribed(channelId, subscribed) + }, [channelId]) + + return { + subscribed, + setSubscribed + } +} + +export const usePublisher = (publisherId: string) => { + const { publishers } = useBraveNews() + return useMemo(() => publishers[publisherId], [publishers[publisherId]]) +} + +export const usePublisherSubscribed = (publisherId: string) => { + const publisher = usePublisher(publisherId) + + const subscribed = isPublisherEnabled(publisher) + const setSubscribed = useCallback((subscribed: boolean) => api.setPublisherSubscribed(publisherId, subscribed), [publisherId]) + + return { + subscribed, + setSubscribed + } +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/DisabledPlaceholder.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/DisabledPlaceholder.tsx new file mode 100644 index 000000000000..d48e9ce3f013 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/DisabledPlaceholder.tsx @@ -0,0 +1,59 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import Button from '$web-components/button' +import * as React from 'react' +import styled from 'styled-components' +import { getLocale } from '$web-common/locale' +import Flex from '../../../Flex' + +const TodayGraphic = + + + + + + + + + + +const Container = styled(Flex)` + height: 100%; +` + +const Header = styled.span` + font-size: 24px; + font-weight: 500; + line-height: 1.66; + color: var(--text2); +` + +const Subtitle = styled.span` + font-size: 14px; + font-weight: 500; + color: var(--text2); +` + +const EnableButton = styled(Button)` + margin-top: 16px; + // Move the centered content up a bit. + margin-bottom: 48px; +` + +export default function DisabledPlaceholder (props: { enableBraveNews: () => void }) { + return + {TodayGraphic} +
+ {getLocale('braveNewsDisabledPlaceholderHeader')} +
+ + {getLocale('braveNewsDisabledPlaceholderSubtitle')} + + + {getLocale('braveNewsDisabledPlaceholderEnableButton')} + +
+} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Discover.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Discover.tsx new file mode 100644 index 000000000000..2e5ffb7177bd --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Discover.tsx @@ -0,0 +1,132 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import Button from '$web-components/button' +import TextInput from '$web-components/input' +import * as React from 'react' +import { useState } from 'react' +import styled from 'styled-components' +import { getLocale } from '$web-common/locale' +import { useBraveNews, useChannels } from './Context' +import Flex from '../../../Flex' +import ChannelCard from './ChannelCard' +import DiscoverSection from './DiscoverSection' +import FeedCard, { DirectFeedCard } from './FeedCard' +import useSearch from './useSearch' + +const Header = styled.span` + font-size: 24px; + font-weight: 600; + padding: 12px 0; +` + +const SearchInput = styled(TextInput)` + margin: 16px 0; + border-radius: 4px; + --interactive8: #AEB1C2; + --focus-border: #737ADE; +` + +const LoadMoreButtonContainer = styled.div` + display: flex; + flex-direction: column; + align-items: stretch; + grid-column: 2; +` + +// The default number of category cards to show. +const DEFAULT_NUM_CATEGORIES = 3 + +export default function Discover () { + const [query, setQuery] = useState('') + + return +
Discover
+ setQuery(e.currentTarget.value)} /> + { query.length + ? + : + } +
+} + +function Home () { + const [showingAllCategories, setShowingAllCategories] = React.useState(false) + const channels = useChannels() + const { filteredPublisherIds } = useBraveNews() + + const visibleChannelIds = React.useMemo(() => channels + // If we're showing all channels, there's no end to the slice. + // Otherwise, just show the default number. + .slice(0, showingAllCategories + ? undefined + : DEFAULT_NUM_CATEGORIES) + .map(c => c.channelName), + [channels, showingAllCategories]) + + return ( + <> + + {visibleChannelIds.map(channelId => + + )} + {!showingAllCategories && + + } + + + {filteredPublisherIds.map(publisherId => + + )} + + + ) +} + +interface SearchResultsProps { + query: string +} +function SearchResults (props: SearchResultsProps) { + const search = useSearch(props.query) + const isFetchable = (search.feedUrlQuery !== null) + const showFetchPermissionButton = (isFetchable && (!search.canFetchUrl || search.loading)) + + const hasAnyChannels = search.filteredChannels.length > 0 + const hasAnySources = (search.filteredSources.publisherIds.length > 0 || search.filteredSources.direct.length > 0) + + return ( + <> + {hasAnyChannels && + + {search.filteredChannels.map(c => + + )} + + } + + {search.filteredSources.publisherIds.map(publisherId => + + )} + {showFetchPermissionButton && +
+ +
+ } + {search.filteredSources.direct.map(r => + )} + {!search.canQueryFilterSources && + getLocale('braveNewsSearchQueryTooShort') + } + {isFetchable && !hasAnySources && !showFetchPermissionButton && + getLocale('braveNewsDirectSearchNoResults').replace('$1', search.feedUrlQuery ?? '') + } +
+ + ) +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/DiscoverSection.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/DiscoverSection.tsx new file mode 100644 index 000000000000..b36a7cbb5f9b --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/DiscoverSection.tsx @@ -0,0 +1,50 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import styled from 'styled-components' +import Flex from '../../../Flex' + +interface Props { + name: string + subtitle?: React.ReactNode + children?: React.ReactNode +} + +const Container = styled(Flex)` + padding: 16px 0; + cursor: pointer; +` + +const Header = styled.span` + font-weight: 600; + font-size: 16px; + margin: 8px 0; +` + +const Subtitle = styled.span` + font-size: 12px; +` + +const ItemsContainer = styled.div` + margin: 8px 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 208px)); + gap: 16px; +` + +export default function DiscoverSection (props: Props) { + return + +
{props.name}
+
+ {props.subtitle && + {props.subtitle} + } + + {props.children} + +
+} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/FeedCard.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/FeedCard.tsx new file mode 100644 index 000000000000..00ef67bddd76 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/FeedCard.tsx @@ -0,0 +1,146 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import { useState, useEffect } from 'react' +import * as React from 'react' +import styled, { keyframes } from 'styled-components' +import Flex from '../../../Flex' +import FollowButton from './FollowButton' +import { Heart, HeartOutline } from './Icons' +import { usePublisher, usePublisherSubscribed } from './Context' +import { getCardColor } from './colors' +import { useGetUnpaddedImage } from '../cards/CardImage' +import { api } from '../../../../api/brave_news/news' + +interface CardProps { + backgroundColor?: string +} + +const Card = styled('div').attrs(props => ({ + style: { + backgroundColor: props.backgroundColor + } +}))` + position: relative; + height: 80px; + border-radius: 8px; + overflow: hidden; + box-shadow: 0px 0px 16px 0px #63696E2E; +` + +const CoverImage = styled('div') <{ backgroundImage: string }>` + position: absolute; + top: 15%; bottom: 15%; left: 15%; right: 15%; + border-radius: 8px; + background-position: center; + background-size: contain; + background-repeat: no-repeat; + background-image: url('${p => p.backgroundImage}'); +` + +const StyledFollowButton = styled(FollowButton)` + position: absolute; + right: 8px; + top: 8px; +` + +const Name = styled.span` + font-size: 14px; + font-weight: 600; +` + +const Pulse = keyframes` + 0% { + pointer-events: auto; + } + 5% { opacity: 1; } + 80% { opacity: 1; } + 99% { + pointer-events: auto; + } + 100% { + pointer-events: none; + opacity: 0; + } +` + +const HeartOverlay = styled(Flex)` + pointer-events: none; + background: white; + color: #aeb1c2; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + animation: ${Pulse} 2s ease-in-out; +` + +const HeartContainer = styled.div` + width: 32px; + height: 32px; + + > svg { + width: 100%; + height: 100%; + } +` + +export default function FeedCard (props: { + publisherId: string +}) { + const publisher = usePublisher(props.publisherId) + const { subscribed, setSubscribed } = usePublisherSubscribed(props.publisherId) + const [changeCount, setToggleCount] = useState(0) + useEffect(() => { + setToggleCount(t => t + 1) + }, [subscribed]) + + const backgroundColor = publisher.backgroundColor || getCardColor(publisher.feedSource?.url || publisher.publisherId) + const coverUrl = useGetUnpaddedImage(publisher.coverUrl?.url, undefined, /* useCache= */true) + return + + {coverUrl && } + setSubscribed(!subscribed)} /> + + {/* + Use whether or not we're following this element as the key, so + React remounts the component when we toggle following and plays + the animation. + + We don't display the overlay unless we've toggled this publisher + so we don't play the pulse animation on first load. + */} + {changeCount > 1 && + + {subscribed ? Heart : HeartOutline} + + } + + + {publisher.publisherName} + + +} + +export function DirectFeedCard (props: { + feedUrl: string + title: string +}) { + const [loading, setLoading] = useState(false) + return + + { + setLoading(true) + await api.subscribeToDirectFeed(props.feedUrl) + setLoading(false) + }} /> + + + {props.title} + + +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/FollowButton.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/FollowButton.tsx new file mode 100644 index 000000000000..dcd573ea971c --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/FollowButton.tsx @@ -0,0 +1,41 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import Button, { ButtonProps } from '$web-components/button' +import * as React from 'react' +import styled, { css } from 'styled-components' +import { getLocale } from '$web-common/locale' +import { Heart, HeartOutline } from './Icons' + +interface Props extends Omit { + className?: string + following: boolean +} + +const StyledButton = styled.div<{ following: boolean }>` + &> button { + --background: #FFF; + padding: 5px 14px; + ${p => p.following && css` + color: var(--interactive5); + --inner-border-color: var(--interactive5); + --outer-border-color: var(--interactive5); + `} + } +` + +export default function FollowButton (props: Props) { + const { following, className, ...rest } = props + return + + +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Icons.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Icons.tsx new file mode 100644 index 000000000000..19677940b16a --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Icons.tsx @@ -0,0 +1,34 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +export const Rocket = + + + +export const History = + + + +export const DoubleHeart = + + + +export const HeartOutline = + + + +export const Heart = + + + +export const BackArrow = + + + +export const Cross = + + diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Modal.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Modal.tsx new file mode 100644 index 000000000000..6e72df868547 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Modal.tsx @@ -0,0 +1,44 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import styled from 'styled-components' +import { useBraveNews } from './Context' + +const Configure = React.lazy(() => import('./Configure')) + +const Dialog = styled.dialog` + border-radius: 8px; + border: none; + width: min(100vw, 1092px); + height: min(100vh, 712px); + z-index: 1000; + background: white; + overflow: hidden; + padding: 0; + background-color: ${p => p.theme.color.contextMenuBackground}; + color: ${p => p.theme.color.contextMenuForeground}; +` + +export default function BraveNewsModal () { + const { customizePage } = useBraveNews() + const dialogRef = React.useRef void, close: () => void, open: boolean }>() + + const shouldRender = !!customizePage + + // Note: There's no attribute for open modal, so we need + // to call showModal instead. + React.useEffect(() => { + dialogRef.current?.showModal?.() + }, [customizePage, dialogRef]) + + // Only render the dialog if it should be shown, since + // it is a complex view. + return shouldRender ? + Loading...}> + + + : null +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/SourcesList.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/SourcesList.tsx new file mode 100644 index 000000000000..4d789ff5d423 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/SourcesList.tsx @@ -0,0 +1,45 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import styled from 'styled-components' +import { getLocale } from '$web-common/locale' +import { useBraveNews, useChannels } from './Context' +import Flex from '../../../Flex' +import { FeedListEntry, ChannelListEntry } from './SourcesListEntry' +import { PluralStringProxyImpl } from 'chrome://resources/js/plural_string_proxy.js' +import usePromise from '../../../../hooks/usePromise' + +const Title = styled.span` + font-size: 18px; + font-weight: 800; + line-height: 36px; +` + +const Subtitle = styled.span` + font-weight: 500; + font-size: 12px; + color: #868e96; +` + +export default function SourcesList () { + const { subscribedPublisherIds } = useBraveNews() + const channels = useChannels({ subscribedOnly: true }) + + const { result: sourcesCount } = usePromise(async () => PluralStringProxyImpl.getInstance().getPluralString('braveNewsSourceCount', subscribedPublisherIds.length + channels.length), [subscribedPublisherIds.length, channels.length]) + + return
+ + {getLocale('braveNewsFeedsHeading')} + {sourcesCount} + + + {channels.map(c => )} + {subscribedPublisherIds.map((p) => ( + + ))} + +
+} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/SourcesListEntry.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/SourcesListEntry.tsx new file mode 100644 index 000000000000..a0e58166da90 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/SourcesListEntry.tsx @@ -0,0 +1,84 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import styled from 'styled-components' +import { useChannelSubscribed, usePublisher, usePublisherSubscribed } from './Context' +import { useGetUnpaddedImage } from '../cards/CardImage' +import Flex from '../../../Flex' +import { Heart, HeartOutline } from './Icons' + +interface Props { + publisherId: string +} + +const Container = styled(Flex)` + padding: 10px 0; + cursor: pointer; + + :hover { + opacity: 0.5; + } +` + +const FavIconContainer = styled.div` + width: 24px; + height: 24px; + border-radius: 100px; + + img { + width: 100%; + height: 100%; + } +` +const ToggleButton = styled.button` + all: unset; + cursor: pointer; + color: var(--interactive5); +` + +const Text = styled.span` + font-size: 14px; + font-weight: 500; +` + +const ChannelNameText = styled.span` + font-size: 14px; + font-weight: 600; +` + +function FavIcon (props: { src?: string }) { + const url = useGetUnpaddedImage(props.src, undefined, /* useCache= */true) + const [error, setError] = React.useState(false) + return + {url && !error && setError(true)} />} + +} + +export function FeedListEntry (props: Props) { + const publisher = usePublisher(props.publisherId) + const { subscribed, setSubscribed } = usePublisherSubscribed(props.publisherId) + + return setSubscribed(!subscribed)}> + + + {publisher.publisherName} + + + {subscribed ? Heart : HeartOutline} + + +} + +export function ChannelListEntry (props: { channelId: string }) { + const { subscribed, setSubscribed } = useChannelSubscribed(props.channelId) + + return setSubscribed(!subscribed)}> + {props.channelId} + + {subscribed ? Heart : HeartOutline} + + +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/colors.ts b/components/brave_new_tab_ui/components/default/braveToday/customize/colors.ts new file mode 100644 index 000000000000..f6b34f744f98 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/colors.ts @@ -0,0 +1,25 @@ +// 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 http://mozilla.org/MPL/2.0/. + +// Used to get a random but deterministic distribution of colors. +const stringHashCode = (str: string) => { + let hash = 0 + for (let i = 0; i < str.length; ++i) hash = Math.imul(31, hash) + str.charCodeAt(i) + return (hash | 0) + 2147483647 + 1 +} + +export const cardColors = [ + '#FF9AA2', + '#FFB7B2', + '#FFDAC1', + '#E2F0CB', + '#B5EAD7', + '#C7CEEA' +] + +export const getCardColor = (key: string | number) => { + const hash = typeof key === 'string' ? stringHashCode(key) : key + return cardColors[hash % cardColors.length] +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/useSearch.ts b/components/brave_new_tab_ui/components/default/braveToday/customize/useSearch.ts new file mode 100644 index 000000000000..6bcad5528892 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/useSearch.ts @@ -0,0 +1,124 @@ +// 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 http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { api } from '../../../../api/brave_news/news' +import { FeedSearchResultItem } from '../../../../api/brave_news' +import { useBraveNews, useChannels } from './Context' + +export const QUERY_MIN_LENGTH_FILTER_SOURCES = 2 + +export default function useSearch (query: string) { + const lowerQuery = query.toLowerCase() + const channels = useChannels() + const context = useBraveNews() + const canQueryFilterSources = lowerQuery.length >= QUERY_MIN_LENGTH_FILTER_SOURCES + const [directResults, setDirectResults] = React.useState([]) + const [loading, setLoading] = React.useState(false) + // Whether user has asked us to perform a search at the query url + const [canFetchUrl, setCanFetchUrl] = React.useState(false) + + // Separate results we already have a publisher for. + type FilteredResults = { + publisherIds: string[] + direct: FeedSearchResultItem[] + } + + const filteredChannels = React.useMemo(() => + channels.filter(c => c.channelName.toLowerCase().includes(lowerQuery)), + [lowerQuery, channels]) + + const filteredSources = React.useMemo(() => { + const publishers = context.sortedPublishers + .filter(p => canQueryFilterSources && ( + p.publisherName.toLowerCase().includes(lowerQuery) || + p.categoryName.toLocaleLowerCase().includes(lowerQuery) || + p.siteUrl?.url?.toLocaleLowerCase().includes(lowerQuery) || + p.feedSource?.url?.toLocaleLowerCase().includes(lowerQuery))) + const results = { publishers, direct: [] as FeedSearchResultItem[] } + for (const result of directResults) { + const publisherMatch = publishers.find(p => p.feedSource.url === result.feedUrl.url) + if (publisherMatch && !publishers.some(p => p.publisherId === publisherMatch.publisherId)) { + results.publishers.push(publisherMatch) + } else { + results.direct.push(result) + } + } + return { + publisherIds: results.publishers.map(p => p.publisherId), + direct: results.direct + } + }, [directResults, context.sortedPublishers, lowerQuery]) + + React.useEffect(() => { + // Reset state when query changes + setCanFetchUrl(false) + setLoading(false) + setDirectResults([]) + }, [query]) + + // Contains a url as string if the query looks like a url + const feedUrlQuery = React.useMemo(() => { + // Check if we have a url-able query + let feedUrlRaw = query + // User inputting a protocol is a signal that they want to look + // for a feed. Otherwise we use the '.' that a hostname would have. + // This at least makes it possible for the user to retrieve local + // network feeds. + let queryContainsExplicitProtocol = false + if (feedUrlRaw.includes('://')) { + queryContainsExplicitProtocol = true + } else { + // Default protocol that should catch most cases. Make sure + // we check for validity after adding this prefix, and + // not before. + feedUrlRaw = 'https://' + feedUrlRaw + } + let isValidFeedUrl = false + try { + const url = new URL(feedUrlRaw) + isValidFeedUrl = ['http:', 'https:'].includes(url.protocol) && + (url.hostname.includes('.') || queryContainsExplicitProtocol) + } catch { } + if (!isValidFeedUrl) { + // Not a url-able query + return null + } + return feedUrlRaw + }, [query]) + + React.useEffect(() => { + // Do nothing if user hasn't asked to search, or input isn't + // a valid Url + if (!canFetchUrl || !feedUrlQuery) { + console.debug('News: query changed but not searching', { canFetchUrl, feedUrlQuery }) + return + } + + setLoading(true) + let cancelled = false + + api.controller.findFeeds({ url: feedUrlQuery.toString() }).then(({ results }) => { + console.debug('News: received feed results', { results, cancelled }) + if (cancelled) return + + setLoading(false) + setDirectResults(results) + }) + return () => { + cancelled = true + } + }, [canFetchUrl, feedUrlQuery]) + + return { + canFetchUrl, + canQueryFilterSources, + filteredSources, + filteredChannels, + feedUrlQuery, + loading, + setCanFetchUrl + } +} diff --git a/components/brave_new_tab_ui/components/outlineButton.tsx b/components/brave_new_tab_ui/components/outlineButton.tsx index 71ee7376a12a..402fe1d9b91e 100644 --- a/components/brave_new_tab_ui/components/outlineButton.tsx +++ b/components/brave_new_tab_ui/components/outlineButton.tsx @@ -8,11 +8,12 @@ import styled from 'styled-components' const Button = styled('button')` --outer-border-size: 0px; --inner-border-size: 1px; + --border-color: black; appearance: none; cursor: pointer; background: none; border-radius: 24px; - border: solid var(--border-size); + border: solid var(--border-size) var(--border-color); padding: 10px 22px; display: flex; flex-direction: row; diff --git a/components/brave_new_tab_ui/containers/newTab/index.tsx b/components/brave_new_tab_ui/containers/newTab/index.tsx index 5b1183aaf38c..27e1edfcfdd8 100644 --- a/components/brave_new_tab_ui/containers/newTab/index.tsx +++ b/components/brave_new_tab_ui/containers/newTab/index.tsx @@ -6,46 +6,33 @@ import * as React from 'react' // Components -import Stats from './stats' -import TopSitesGrid from './gridSites' -import FooterInfo from '../../components/default/footer/footer' -import SiteRemovalNotification from './notification' +import getNTPBrowserAPI from '../../api/background' +import { addNewTopSite, editTopSite } from '../../api/topSites' +import { brandedWallpaperLogoClicked } from '../../api/wallpaper' import { - Clock, - RewardsWidget as Rewards, - BraveTalkWidget as BraveTalk, - BinanceWidget as Binance, - GeminiWidget as Gemini, - CryptoDotComWidget as CryptoDotCom, - EditTopSite, - SearchPromotion, - EditCards, - OverrideReadabilityColor + BinanceWidget as Binance, BraveTalkWidget as BraveTalk, Clock, CryptoDotComWidget as CryptoDotCom, EditCards, EditTopSite, GeminiWidget as Gemini, OverrideReadabilityColor, RewardsWidget as Rewards, SearchPromotion } from '../../components/default' -import { FTXWidget as FTX } from '../../widgets/ftx/components' -import * as Page from '../../components/default/page' import BrandedWallpaperLogo from '../../components/default/brandedWallpaper/logo' -import { brandedWallpaperLogoClicked } from '../../api/wallpaper' -import BraveTodayHint from '../../components/default/braveToday/hint' import BraveToday, { GetDisplayAdContent } from '../../components/default/braveToday' +import FooterInfo from '../../components/default/footer/footer' +import * as Page from '../../components/default/page' import { SponsoredImageTooltip } from '../../components/default/rewards' -import { addNewTopSite, editTopSite } from '../../api/topSites' -import getNTPBrowserAPI from '../../api/background' +import { FTXWidget as FTX } from '../../widgets/ftx/components' +import TopSitesGrid from './gridSites' +import SiteRemovalNotification from './notification' +import Stats from './stats' // Helpers -import VisibilityTimer from '../../helpers/visibilityTimer' import { - fetchCryptoDotComTickerPrices, - fetchCryptoDotComLosersGainers, - fetchCryptoDotComCharts, - fetchCryptoDotComSupportedPairs + fetchCryptoDotComCharts, fetchCryptoDotComLosersGainers, fetchCryptoDotComSupportedPairs, fetchCryptoDotComTickerPrices } from '../../api/cryptoDotCom' import { generateQRData } from '../../binance-utils' import isReadableOnBackground from '../../helpers/colorUtil' +import VisibilityTimer from '../../helpers/visibilityTimer' // Types -import { GeminiAssetAddress } from '../../actions/gemini_actions' import { getLocale } from '../../../common/locale' +import { GeminiAssetAddress } from '../../actions/gemini_actions' import currencyData from '../../components/default/binance/data' import geminiData from '../../components/default/gemini/data' import { NewTabActions } from '../../constants/new_tab_types' @@ -53,8 +40,11 @@ import { BraveTodayState } from '../../reducers/today' import { FTXState } from '../../widgets/ftx/ftx_state' // NTP features -import Settings, { TabType as SettingsTabType } from './settings' import { MAX_GRID_SIZE } from '../../constants/new_tab_ui' +import Settings, { TabType as SettingsTabType } from './settings' + +import { BraveNewsContextProvider } from '../../components/default/braveToday/customize/Context' +import BraveTodayHint from '../../components/default/braveToday/hint' import GridWidget from './gridWidget' interface Props { @@ -95,7 +85,7 @@ interface State { function GetBackgroundImageSrc (props: Props) { if (!props.newTabData.showBackgroundImage && - (!props.newTabData.brandedWallpaper || props.newTabData.brandedWallpaper.isSponsored)) { + (!props.newTabData.brandedWallpaper || props.newTabData.brandedWallpaper.isSponsored)) { return undefined } if (props.newTabData.brandedWallpaper) { @@ -131,12 +121,12 @@ function GetShouldForceToHideWidget (props: Props, showSearchPromotion: boolean) function GetIsShowingBrandedWallpaper (props: Props) { const { newTabData } = props return !!((newTabData.brandedWallpaper && - newTabData.brandedWallpaper.isSponsored)) + newTabData.brandedWallpaper.isSponsored)) } function GetShouldShowBrandedWallpaperNotification (props: Props) { return GetIsShowingBrandedWallpaper(props) && - !props.newTabData.isBrandedWallpaperNotificationDismissed + !props.newTabData.isBrandedWallpaperNotificationDismissed } class NewTabPage extends React.Component { @@ -211,12 +201,12 @@ class NewTabPage extends React.Component { this.setState({ backgroundHasLoaded: false }) } if (!GetShouldShowBrandedWallpaperNotification(prevProps) && - GetShouldShowBrandedWallpaperNotification(this.props)) { + GetShouldShowBrandedWallpaperNotification(this.props)) { this.trackBrandedWallpaperNotificationAutoDismiss() } if (GetShouldShowBrandedWallpaperNotification(prevProps) && - !GetShouldShowBrandedWallpaperNotification(this.props)) { + !GetShouldShowBrandedWallpaperNotification(this.props)) { this.stopWaitingForBrandedWallpaperNotificationAutoDismiss() } } @@ -1166,6 +1156,10 @@ class NewTabPage extends React.Component { cryptoContent = null } + const BraveNewsContext = newTabData.featureFlagBraveNewsV2Enabled + ? BraveNewsContextProvider + : React.Fragment + return ( { colorForBackground={colorForBackground} data-show-news-prompt={((this.state.backgroundHasLoaded || colorForBackground) && this.state.isPromptingBraveToday) ? true : undefined}> + { { - showTopSites - ? ( + showTopSites && { textDirection={newTabData.textDirection} /> - ) : null - } - { - gridSitesData.shouldShowSiteRemovedNotification - ? ( - - - - ) : null - } + } + { + gridSitesData.shouldShowSiteRemovedNotification + ? ( + + + + ) : null + } {cryptoContent} - - - {isShowingBrandedWallpaper && newTabData.brandedWallpaper && - newTabData.brandedWallpaper.logo && - - - {this.renderBrandedWallpaperNotification()} - } - - - - {newTabData.showToday && - - - - } - + + + {isShowingBrandedWallpaper && newTabData.brandedWallpaper && + newTabData.brandedWallpaper.logo && + + + {this.renderBrandedWallpaperNotification()} + } + + + + {newTabData.showToday && + + + + } + { newTabData.showToday && newTabData.featureFlagBraveNewsEnabled && { onSave={this.saveNewTopSite} /> : null } + ) } diff --git a/components/brave_new_tab_ui/containers/newTab/settings.tsx b/components/brave_new_tab_ui/containers/newTab/settings.tsx index b84b7d4d5347..570969042886 100644 --- a/components/brave_new_tab_ui/containers/newTab/settings.tsx +++ b/components/brave_new_tab_ui/containers/newTab/settings.tsx @@ -21,6 +21,7 @@ import { import { getLocale } from '../../../common/locale' import { Publishers } from '../../api/brave_news' +import { BraveNewsContext } from '../../components/default/braveToday/customize/Context' // Icons import { CloseStrokeIcon } from 'brave-ui/components/icons' @@ -41,6 +42,7 @@ const BraveTodaySettings = React.lazy(() => import('./settings/braveToday')) // Types import { NewTabActions } from '../../constants/new_tab_types' +import { loadTimeData } from '../../../common/loadTimeData' export interface Props { newTabData: NewTab.State @@ -110,6 +112,7 @@ interface State { } export default class Settings extends React.PureComponent { + static contextType: typeof BraveNewsContext = BraveNewsContext settingsMenuRef: React.RefObject allTabTypes: TabType[] allTabTypesWithoutBackground: TabType[] @@ -139,7 +142,11 @@ export default class Settings extends React.PureComponent { if ( this.settingsMenuRef && this.settingsMenuRef.current && - !this.settingsMenuRef.current.contains(event.target) + !this.settingsMenuRef.current.contains(event.target) && + // Don't close the settings dialog for a click outside if we're in the + // Brave News modal - the user expects closing that one to bring them back + // to this one. + !this.context.page ) { this.props.onClose() } @@ -195,6 +202,11 @@ export default class Settings extends React.PureComponent { } setActiveTab (activeTab: TabType) { + if (loadTimeData.getBoolean('featureFlagBraveNewsV2Enabled') && activeTab === TabType.BraveToday) { + this.context.setCustomizePage('news') + return + } + this.setState({ activeTab }) } diff --git a/components/brave_new_tab_ui/containers/newTab/settings/braveToday/publisherPrefs.tsx b/components/brave_new_tab_ui/containers/newTab/settings/braveToday/publisherPrefs.tsx index 5cf3d48b6b1a..f2b68dff939b 100644 --- a/components/brave_new_tab_ui/containers/newTab/settings/braveToday/publisherPrefs.tsx +++ b/components/brave_new_tab_ui/containers/newTab/settings/braveToday/publisherPrefs.tsx @@ -83,9 +83,7 @@ function ListItem (props: ListItemProps) { }, [props.index, setSize, props.width]) const publisher = props.data[props.index] - const isChecked = publisher - ? isPublisherContentAllowed(publisher, props.channels) - : false + const isChecked = publisher && isPublisherContentAllowed(publisher, props.channels) const onChange = React.useCallback(() => { if (!publisher) { diff --git a/components/brave_new_tab_ui/hooks/promise.ts b/components/brave_new_tab_ui/hooks/promise.ts new file mode 100644 index 000000000000..d664f6d1eb57 --- /dev/null +++ b/components/brave_new_tab_ui/hooks/promise.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react' + +export function useResult (getPromise: () => Promise, deps: any[]) { + const [result, setResult] = useState(undefined) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(undefined) + + useEffect(() => { + let cancelled = false + setLoading(true) + + getPromise().then(result => { + if (cancelled) return + + setResult(result) + setLoading(false) + }).catch(err => { + if (cancelled) return + + setResult(undefined) + setLoading(false) + setError(err) + }) + + return () => { + cancelled = true + } + }, ...deps) + + return { + result, + loading, + error + } +} diff --git a/components/brave_new_tab_ui/hooks/useCachedValue.ts b/components/brave_new_tab_ui/hooks/useCachedValue.ts new file mode 100644 index 000000000000..7310b391095f --- /dev/null +++ b/components/brave_new_tab_ui/hooks/useCachedValue.ts @@ -0,0 +1,15 @@ +import { useCallback, useEffect, useState } from 'react' + +export const useCachedValue = (value: T, setValue: (newValue: T) => void) => { + const [cached, setCached] = useState(value) + const updateCached = useCallback((newValue: T) => { + setCached(newValue) + setValue(newValue) + }, [setValue]) + + useEffect(() => { + setCached(value) + }, [value]) + + return [cached, updateCached] as const +} diff --git a/components/brave_new_tab_ui/stories/default/data/mockBraveNewsController.ts b/components/brave_new_tab_ui/stories/default/data/mockBraveNewsController.ts new file mode 100644 index 000000000000..9819fff871c3 --- /dev/null +++ b/components/brave_new_tab_ui/stories/default/data/mockBraveNewsController.ts @@ -0,0 +1,2029 @@ +import * as BraveNews from '../../../api/brave_news' +import { BraveNewsControllerRemote } from '../../../api/brave_news' + +// Generate feed page from real data in devtools: +// let pids = [ +// "5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "d4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", +// "gd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5" +// ] +// +// copy(newState.feed.pages[0].items.map( i => ({ ...i, items: i.items.map(ii => { +// let data = ii.article?.data || ii.promotedArticle?.data || ii.deal?.data +// if (ii.article) { data = ii.article.data } +// if (ii.promotedArticle) { data = ii.promotedArticle.data } +// data.publishTime.internalValue = data.publishTime.internalValue.toString() +// data.publisherId = pids[Math.floor(Math.random() * (pids.length - 1))] +// if (!ii.article) ii.article = "undefined" +// if (!ii.promotedArticle) ii.promotedArticle = "undefined" +// if (!ii.deal) ii.deal = "undefined" +// return ii +// })}))) +export const publishers: BraveNews.Publishers = { + 'direct:https://example.com/feed': { + publisherId: 'direct:https://example.com/feed1', + publisherName: 'My Custom Feed 1', + categoryName: 'User feeds', + channels: ['User feeds'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.DIRECT_SOURCE, + isEnabled: true, + userEnabledStatus: BraveNews.UserEnabled.ENABLED + }, + 'direct:https://example2.com/feed': { + publisherId: 'direct:https://example.com/feed2', + publisherName: 'My Custom Feed 2', + categoryName: 'User feeds', + channels: ['User feeds'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.DIRECT_SOURCE, + isEnabled: true, + userEnabledStatus: BraveNews.UserEnabled.ENABLED + }, + '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 1', + categoryName: 'Tech', + channels: ['Tech'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.ENABLED + }, + '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 2', + categoryName: 'Top News', + channels: ['Top News'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED + }, + 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 3', + categoryName: 'Tech 2', + channels: ['Tech 2'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.ENABLED + }, + 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 4', + categoryName: 'Top News 1', + channels: ['Top News 1'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED + }, + 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 5 has A very very very very very very very very very very very very very very very very very very very very long publisher name', + categoryName: 'Tech 2', + channels: ['Tech 2'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.ENABLED + }, + 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 6', + categoryName: 'Top News 2', + channels: ['Top News 2'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED + }, + 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 7', + categoryName: 'Top News 3', + channels: ['Top News 3'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED + }, + 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 8', + categoryName: 'Tech 3', + channels: ['Tech 3'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.ENABLED + }, + 'gd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { + publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + publisherName: 'Test Publisher 9', + categoryName: 'Top News 4', + channels: ['Top News 4'], + feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, + siteUrl: { url: 'https://www.example.com' }, + locales: ['en_US'], + type: BraveNews.PublisherType.COMBINED_SOURCE, + isEnabled: false, + userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED + } +} + +export const feed: BraveNews.Feed = { + hash: '123abc', + featuredItem: { + promotedArticle: undefined, + deal: undefined, + article: { + data: { + categoryName: 'Top News', + description: 'Here\'s everything you need to know about the Haunted Hallows event, including how to unlock the Batmobile.', + image: { imageUrl: { url: 'https://placekitten.com/1360/912' }, paddedImageUrl: undefined }, + publishTime: { internalValue: BigInt('13278618001000000') }, + publisherId: 'd75d65f0f747650ef1ea11adb0029f9d577c629a080b5f60ec80d125b2bf205b', + publisherName: 'Newsweek', + relativeTimeDescription: '1 hour ago', + urlHash: '', + score: 14.200669212327124, + title: '\'Rocket League\' Haunted Hallows 2021: Details Revealed and How to Unlock Batmobile Cars', + url: { url: 'https://www.newsweek.com/rocket-league-haunted-hallows-halloween-2021-batmobile-price-date-1638020' } + } + } + }, + pages: [ + { + items: [ + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Sports', + 'publishTime': { + internalValue: BigInt('13278621116000000') + }, + 'title': 'The agony of the feet: Why turf toe is such a dreaded injury in the NFL', + 'description': 'A misunderstood and often dismissed condition, a big toe hyperextension can cause crippling pain with every step, creating mental anguish and fatigue that take a huge toll on an athlete. After decades of occurrences, it\'s finally being taken seriously.', + 'url': { + 'url': 'https://www.espn.com/nfl/story/_/id/32379942/why-turf-toe-such-dreaded-injury-nfl' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/944ee78cb12ebda3d46c51a4fb91db1a961d3dee13b8eb1034798ae5ca2150dc.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'ESPN', + 'score': 10.00343277763486, + 'relativeTimeDescription': '31 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Sports', + 'publishTime': { + internalValue: BigInt('13278620426000000') + }, + 'title': 'Spain didn\'t win Nations League but Oyarzabal is a star', + 'description': 'Spain\'s latest super-group of teenagers has caught the imagination this week, but 24-year-old Real Sociedad winger Mikel Oyarzabal might be the one to lead them.', + 'url': { + 'url': 'https://www.espn.com/soccer/spain-esp/story/4495432/spain-didnt-win-nations-league-but-they-have-a-gem-in-mikel-oyarzabal' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/9bd848550d4f968a1f93c46a1c4f142cb293ad8220092d8d3017b32d38b08c67.jpg.pad' + } + }, + 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'ESPN - Football', + 'score': 10.649119373004728, + 'relativeTimeDescription': '43 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Technology', + 'publishTime': { + internalValue: BigInt('13278621733000000') + }, + 'title': 'Twitter is launching a Spaces accelerator program to pay live audio creators', + 'description': '\n\nIllustration by Alex Castro / The Verge\n\nTwitter announced on Tuesday that it plans to support Twitter Spaces creators through a new three-month accelerator program called the Twitter Spaces Spark Program. Twitter’s plans follow a similar creator three-month program that Clubhouse launched in March 2021.\nThe Spark Program is designed to “discover and reward” around 150 Spaces creators with technical, financial, and marketing support, Twitter says. For anyone who applies and gets in, that inclu', + 'url': { + 'url': 'https://www.theverge.com/2021/10/13/22724450/twitter-spaces-accelerator-spark-clubhouse-creators' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/023e9e849e2a2af24c5271faa0ba5b25b15f4c90481a3583e948829ef40e881a.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The Verge', + 'score': 14.170018256553691, + 'relativeTimeDescription': '21 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Science', + 'publishTime': { + internalValue: BigInt('13278621699000000') + }, + 'title': 'Widespread masking nudges people to follow the crowd', + 'description': 'When wearing a mask to defend against the spread of COVID-19 becomes a trend, more people mask up themselves, a new study shows.', + 'url': { + 'url': 'https://www.futurity.org/covid-19-mask-viruses-2641802-2/?utm_source=rss&utm_medium=rss&utm_campaign=covid-19-mask-viruses-2641802-2' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/b735cb231cc73ce065177509ecda0ef35203e55bee5bfa798401a7bc67893182.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Futurity', + 'score': 14.226233174417557, + 'relativeTimeDescription': '22 minutes ago' + } + } + } + ] + }, + { + 'cardType': 6, + 'items': [ + { + article: undefined, + deal: undefined, + promotedArticle: { + 'data': { + 'categoryName': 'Brave Partners', + 'publishTime': { + internalValue: BigInt('13278621628000000') + }, + 'title': 'Audiovox (VOXX) Q2 2022 Earnings Call Transcript', + 'description': 'VOXX earnings call for the period ending September 30, 2021.', + 'url': { + 'url': 'https://www.fool.com/earnings/call-transcripts/2021/10/13/audiovox-voxx-q2-2022-earnings-call-transcript/?source=thebrave&utm_source=foo&utm_medium=feed&utm_campaign=article' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/5b3d8da219eee17bce800689085994a6a851545aa99b35c374874f42a93c672b.jpg.pad' + } + }, + 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The Motley Fool', + 'score': 14.338672770645763, + 'relativeTimeDescription': '23 minutes ago' + }, + 'creativeInstanceId': 'd2d506aa-5531-4069-8f85-7d9052f1b640' + } + } + ] + }, + { + 'cardType': 2, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278621613000000') + }, + 'title': 'Brexit: Most NI checks on British goods to be scrapped', + 'description': 'The proposals are a \"genuine response\" to address Brexit trade issues, says the European Commission.', + 'url': { + 'url': 'https://www.bbc.co.uk/news/uk-northern-ireland-58871221?at_medium=RSS&at_campaign=KARANGA' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/1d465c5238d91f25be576c554f691bd651be6d1346382888a9636c52258f2d67.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'BBC', + 'score': 14.361647694838718, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278621600000000') + }, + 'title': 'What needs to be done to fix the tax system?', + 'description': 'Death duties, hiking the GST and more taxes on housing are on the wish lists of the nation’s top economists.', + 'url': { + 'url': 'https://www.smh.com.au/politics/federal/what-needs-to-be-done-to-fix-the-tax-system-20211004-p58x26.html?ref=rss&utm_medium=rss&utm_source=rss_feed' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/40a3ae95c9d10d6988236d4e17e0533f2528259d67ae3ed4b44f8243b65f764e.jpg.pad' + } + }, + 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Sydney Morning Herald', + 'score': 14.381381289249747, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278621533000000') + }, + 'title': 'Woman Parades Through Airport Completely Naked, Makes Small Talk With Travelers', + 'description': '\'The woman asked travelers how they were doing and where they are from\'', + 'url': { + 'url': 'https://dailycaller.com/2021/10/13/denver-airport-naked-woman/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/e9f94baedccb0fe6aa4679dba6762ded76b6d4a412149c55b29d5090fff182c5.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Daily Caller', + 'score': 14.479957341206271, + 'relativeTimeDescription': '24 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Business', + 'publishTime': { + internalValue: BigInt('13278621684000000') + }, + 'title': 'William Shatner emotionally describes spaceflight to Jeff Bezos: \'The most profound experience\'', + 'description': 'William Shatner, after returning to Earth, recounted his experience in an emotional talk with Blue Origin founder Jeff Bezos.', + 'url': { + 'url': 'https://www.cnbc.com/2021/10/13/william-shatner-speech-to-jeff-bezos-after-blue-origin-launch.html' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/c02a9c39546df2890a411e6afdac5f8a10ceded30947c4688fcfde43010c1d84.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'CNBC', + 'score': 14.250519019150758, + 'relativeTimeDescription': '22 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Cars', + 'publishTime': { + internalValue: BigInt('13278621683000000') + }, + 'title': 'Lucid Air’s DreamDrive ADAS Suite Has LiDAR, 14 Cameras, And 32 Sensors For Future Proofing', + 'description': 'Lucid says that their overengineered tech suite will eventually be updated to include a \"hands-off, eyes-off\" driver assistance system.', + 'url': { + 'url': 'https://www.carscoops.com/2021/10/lucid-airs-dreamdrive-adas-suite-has-lidar-14-cameras-and-32-sensors-for-future-proofing/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/31b73f47c42c5323b6db05ff9907eee4b5217ca9a097e4aa810272609aebb06b.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Carscoops', + 'score': 14.252130607208834, + 'relativeTimeDescription': '22 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Home', + 'publishTime': { + internalValue: BigInt('13278621634000000') + }, + 'title': 'DMTV Milkshake: Cultivating Elegance at Home With Melissa Lee', + 'description': 'Melissa Lee, a self-titled aesthete, shares the invisible element that can change a space drastically when it comes to interior design.', + 'url': { + 'url': 'https://design-milk.com/dmtv-milkshake-cultivating-elegance-at-home-with-melissa-lee/?utm_source=feedburner&utm_campaign=Feed%3A+design-milk+%28Design+Milk%29' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/d00bc1449477f06cf7640231fd898be6c4e6fbd9f6eda99f1b845bb739419376.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Design Milk', + 'score': 14.329404416919667, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278621632000000') + }, + 'title': '‘You’ Renewed For Season 4 By Netflix', + 'description': 'Ahead of the Season 3 premiere on Friday, Netflix has handed an early fourth season renewal to its hit drama series You. Casting news for the new season will be announced at a later date. Starring Penn Badgley and Victoria Pedretti, You is developed by Sera Gamble and Greg Berlanti, and Gamble also serves as […]', + 'url': { + 'url': 'https://deadline.com/2021/10/you-renewed-season-4-netflix-1234855244/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/3b6d9b50f065b320acd02393d96199d9f45028b14399c2b971a85590da89ae33.jpg.pad' + } + }, + 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Deadline', + 'score': 14.332498680036917, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278621625000000') + }, + 'title': 'AFI Fest Full Lineup: 2021 Festival Adds Pedro Almodovar’s ‘Parallel Mothers’ and More', + 'description': 'As previously announced, the awards-facing festival will open with the premiere of Lin-Manuel Miranda\'s \"Tick Tick Boom.\"', + 'url': { + 'url': 'https://www.indiewire.com/2021/10/afi-fest-full-lineup-2021-festival-1234671528/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/d42fe1b86e568d6dca5dc3623a2bb982f3fb337ff79ae338a3544b3cc4cab1b9.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'IndieWire', + 'score': 14.343289731415563, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278621617000000') + }, + 'title': 'YOU Renewed for Season 4 at Netflix — Watch Announcement Video', + 'description': 'Netflix’s Joe Goldberg obsession continues with a Season 4 renewal of YOU, TVLine has learned. This news comes just two days before the Penn Badgley thriller is set to premiere its third season on Friday, Oct. 15. Based on Caroline Kepnes’ series of novels, YOU is developed by executive producers Greg Berlanti and Sera Gamble, […]', + 'url': { + 'url': 'https://tvline.com/2021/10/13/you-renewed-season-4-teaser-video-netflix/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/cbf6594866c44ef06f3cd74ba057559fac2e8dc659e33fa406d11a3583f4b2ed.jpg.pad' + } + }, + 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'TVLine', + 'score': 14.355544195642704, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 5, + 'items': [] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Science', + 'publishTime': { + internalValue: BigInt('13278621608000000') + }, + 'title': 'First evidence of microtubules\' mechanosensitive behavior', + 'description': 'Inside cells, microtubules not only serve as a component of the cytoskeleton (cell skeleton) but also play a role in intracellular transport. In intracellular transport, microtubules act as rails for motor proteins such as kinesin and dynein. Microtubules, the most rigid cytoskeletal component, are constantly subjected to various mechanical stresses such as compression, tension, and bending during cellular activities. It has been hypothesized that microtubules also function as mechanosensors tha', + 'url': { + 'url': 'https://phys.org/news/2021-10-evidence-microtubules-mechanosensitive-behavior.html' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/66475a510a759c80d3ed6bfac562fa8c4dd802b510326c8d0cae28eaae250444.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Phys.org', + 'score': 14.369246557105962, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Sports', + 'publishTime': { + internalValue: BigInt('13278621606000000') + }, + 'title': 'Fantasy Football Week 6 Rankings: Updated Overview for All Positions', + 'description': 'We\'ve reached a critical point in the NFL season for fantasy managers. Bye weeks are here, and the Atlanta Falcons, New Orleans Saints, New York Jets and San Francisco 49ers have the first off rotation of the year...', + 'url': { + 'url': 'https://bleacherreport.com/articles/2949345-fantasy-football-week-6-rankings-updated-overview-for-all-positions' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/191782169a17e5df634109ecd2bb76c24cb48faa48f876edb3a5b2694fa497ba.jpg.pad' + } + }, + 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Bleacher Report', + 'score': 14.372285656733615, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 3, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278621605000000') + }, + 'title': 'Alt-Rock Singer SK8 Shares How ‘Girl Next Door’ Represents The Evolution Of His Sound', + 'description': 'After first making a splash with hip-hop, SK8 has reconnected with his punk roots on \'Girls Next Door,\' and he shares how this new direction came about, what it\'s like running a label, and what\'s next.', + 'url': { + 'url': 'https://hollywoodlife.com/2021/10/13/sk8-girl-next-door-interview/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/2904d64d82725230da6b1531aab85f54ff91cc55328e61f35319bcb3e5ba5abf.jpg.pad' + } + }, + 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Hollywood Life', + 'score': 14.373802033258967, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278618574000000') + }, + 'title': 'Laverne Cox, 49, Rocks Plunging Black Swimsuit On Vacation: ‘Trans Is Beautiful’', + 'description': 'Laverne Cox made a trans-positive statement while looking fiery hot in a sexy swimsuit on a luxury vacation.', + 'url': { + 'url': 'https://hollywoodlife.com/2021/10/13/laverne-cox-plunging-swimsuit-pool-video/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/a071a47e75db68c74fc1eaf6931d9420b2345a0c12b1247cdb05da135c0d5735.jpg.pad' + } + }, + 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Hollywood Life', + 'score': 67.03023810006357, + 'relativeTimeDescription': '1 hour ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278617426000000') + }, + 'title': 'Elizabeth Warren Urges Congress To ‘Step Up’ & Protect Roe V. Wade Amidst Texas Abortion Law', + 'description': 'The Massachusetts senator also explained that the new law will be most harmful to people who don\'t have easy access to abortion while appearing on \'The View.\'', + 'url': { + 'url': 'https://hollywoodlife.com/2021/10/13/elizabeth-warren-roe-v-wade-the-view/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/d302f28efc872481f715f40ef457e419b7c35c03f514ca4a624aed099a8ec814.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Hollywood Life', + 'score': 137.80582500043627, + 'relativeTimeDescription': '2 hours ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Technology', + 'publishTime': { + internalValue: BigInt('13278621600000000') + }, + 'title': 'Here are the best resin options for your SLA/DLP 3D printer', + 'description': 'Resin printing is a little more complex than standard filament printing. Not only do you need a few must-have 3D printing accessories, but you also need to pick the right resin. When faced with multiple colors and multiple types, it\'s also easy to get turned around when choosing the right 3D printing resin. You want it to print quickly but stay strong without becoming brittle. We\'ve used as many as possible to bring you some of the best you can buy, but our favorite is Siraya Tech Fast, which pr', + 'url': { + 'url': 'https://www.windowscentral.com/best-resin-your-3d-printer?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+wmexperts+%28Windows+Central%29' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/5afe29078068e7413a7f15a16657453217fc8ad630e8291ee1c66d69c2df6f48.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Windows Central', + 'score': 14.381363717462444, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Technology', + 'publishTime': { + internalValue: BigInt('13278621600000000') + }, + 'title': 'These are the best office chairs you can buy at any budget', + 'description': 'The adage \"you get what you pay for\" definitely holds for certain categories of products, like shoes, mattresses, and yes, office chairs. The simple fact is, the more you have available to spend (up to a point), the better chair you can buy. The range of options for the best office chairs available is quite diverse and includes styles and features for just about any taste and preference. Our top pick is the AmazonBasics High-Back Leather Executive Chair. It\'s got the looks to fit in a corporate ', + 'url': { + 'url': 'https://www.androidcentral.com/best-office-chairs' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/b406b13e1fe493f9ab11140f5c33809a19125da6d5a3f1125810d299a9539da2.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Android Central', + 'score': 14.381366799582283, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Technology', + 'publishTime': { + internalValue: BigInt('13278621600000000') + }, + 'title': 'Stop Using Playlists to Look Cool and Start Using Them to Share Your Feelings', + 'description': 'Do you struggle to express yourself with words? Consider turning to the one universal language we all share: emo playlists. I believe we, as a society, waste time trying to show off “aesthetic” music tastes. Instead, we should spend more time creating emotionally-charged, hyper-specific playlists. What’s more, we need…Read more...', + 'url': { + 'url': 'https://lifehacker.com/stop-using-playlists-to-look-cool-and-start-using-them-1847855875' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/87267b91579ebbf8ead3f305759376022fd50d3273209e851727cdf24b142304.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Lifehacker', + 'score': 14.38136972344708, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 4, + 'items': [ + { + article: undefined, + promotedArticle: undefined, + deal: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13258305902000000') + }, + 'title': 'Audible Plus', + 'description': 'Listen anytime, anywhere to an unmatched selection of audiobooks, premium podcasts, and more', + 'url': { + 'url': 'https://www.amazon.com/hz/audible/mlp/mdp/discovery?ref_=assoc_tag_ph_1524216631897&_encoding=UTF8&camp=1789&creative=9325&linkCode=pf4&tag=bravesoftware-20&linkId=c6d187d14da9ca69e1a1a950348e100e' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/a29e3a601efa77ba5f2f35e58b40037b527f4e577112ea9967ced44741dcce32.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Offers', + 'score': 33.65394030598059, + 'relativeTimeDescription': '235 days ago' + }, + 'offersCategory': 'Discounts' + } + }, + { + article: undefined, + promotedArticle: undefined, + deal: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13258305902000000') + }, + 'title': 'Amazon Prime Music', + 'description': 'Listen to your favourite songs online from Brave.', + 'url': { + 'url': 'https://www.amazon.com/music/prime' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/04256e526b5cc73ddf7679ed907ac0f89e01d7d6af3a9d1c9faba288468c03ff.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Offers', + 'score': 67.3078806123441, + 'relativeTimeDescription': '235 days ago' + }, + 'offersCategory': 'Discounts' + } + }, + { + article: undefined, + promotedArticle: undefined, + deal: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13258305902000000') + }, + 'title': 'Amazon Prime', + 'description': 'Enjoy exclusive Amazon Originals as well as popular movies and TV shows.', + 'url': { + 'url': 'https://www.amazon.com/amazonprime/146-1781179-3199520?_encoding=UTF8&camp=1789&creative=9325&linkCode=pf4&linkId=a402d5b2ca72ea0a267707ef10878979&primeCampaignId=prime_assoc_ft&ref_=assoc_tag_ph_1427739975520&tag=bravesoftware-20' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/c12d5200d342e72919e8420ccea6581ee4bf8e7ab510dd58bbc24d49ef22c36f.jpg.pad' + } + }, + 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Offers', + 'score': 134.6157612254411, + 'relativeTimeDescription': '235 days ago' + }, + 'offersCategory': 'Discounts' + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278609481000000') + }, + 'title': '\'Havana Syndrome\' mystery expands with new cases at U.S. Embassy in Colombia', + 'description': 'U.S. Embassy personnel in Bogota, Colombia, have reported symptoms aligned with the mysterious “Havana Syndrome” that continues to plague U.S. spies and diplomats around the globe. U.S. officials said Tuesday that two cases were initially reported by embassy personnel in the capital city, but said several others may have been ...', + 'url': { + 'url': 'https://www.washingtontimes.com/news/2021/oct/13/havana-syndrome-mystery-expands-new-cases-us-embas/?utm_source=RSS_Feed&utm_medium=RSS' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/4caf1b03489028604884ce33a6ac6f427f117de3670f4fefb047d92dccd3706c.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The Washington Times', + 'score': 304.21157985954886, + 'relativeTimeDescription': '4 hours ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Health', + 'publishTime': { + internalValue: BigInt('13278602700000000') + }, + 'title': 'Tom Hardy Says He Was \'Really Overweight\' as Bane in \'Dark Knight Rises\'', + 'description': '\"I was just bald, slightly porky, and with pencil arms.\"', + 'url': { + 'url': 'https://www.menshealth.com/weight-loss/a37947676/tom-hardy-overweight-bane-transformation-dark-knight-rises/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/2e81e99a21735bb5f0d3b7c8ac030a368b6e35711809a3475ba2bc6bdf7e300a.jpg.pad' + } + }, + 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Men\'s Health', + 'score': 81223.05671641101, + 'relativeTimeDescription': '6 hours ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278618011000000') + }, + 'title': 'Jodi! Tina! \'The Challenge: All Stars\' Brings in Heavy Hitters for Season 2', + 'description': 'Welcome back! After the massive success of The Challenge: All Stars earlier this year, Paramount+’s reality show is back with another season and even more vets. TJ Lavin will return to host season 2, Parmount+ announced on Wednesday, October 13, with 24 cast members — some of whom haven’t competed in nearly 20 years. “With […]', + 'url': { + 'url': 'https://www.usmagazine.com/entertainment/pictures/the-challenge-all-stars-season-2-cast-includes-tina-jodi-and-more/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/c972ec42d40a1d2a5d2b180095407f119cd2c1b17ff41b5305c5b7987b7c0280.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Us Weekly', + 'score': 544.0267160210122, + 'relativeTimeDescription': '1 hour ago' + } + } + } + ] + } + ] + }, + { + items: [ + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Sports', + 'publishTime': { + internalValue: BigInt('13278621600000000') + }, + 'title': 'USMNT vs Costa Rica: TV channel, live stream, team news & preview', + 'description': 'The Stars and Stripes tasted their first defeat since May when they fell to Panama on Sunday, but can quickly get back to winning ways', + 'url': { + 'url': 'https://www.goal.com/en/news/usmnt-vs-costa-rica-tv-channel-live-stream-team-news-preview/1ou1gnhu835jx174sq8yqn34os' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/ba5b2874d167e19150f3be8bf99a09717d912699a853321939dfbdef5bb9ff87.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Goal.com', + 'score': 14.381372591543293, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Culture', + 'publishTime': { + internalValue: BigInt('13278621600000000') + }, + 'title': 'Tyga Turns Himself in to Police Following Domestic Violence Allegations From Ex-Girlfriend Camaryn Swanson', + 'description': 'Rapper Tyga turned himself in early Tuesday to the Los Angeles Police Department following a domestic violence allegation brought forth from his ex-girlfriend Camaryn Swanson.Read more...', + 'url': { + 'url': 'https://www.theroot.com/tyga-turns-himself-in-to-police-following-domestic-viol-1847854486' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/3816eb674df32251c4b9fbda44a33acfe85764931d36c26d1f5af0cd86a0a960.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The Root', + 'score': 14.381375498821068, + 'relativeTimeDescription': '23 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Business', + 'publishTime': { + internalValue: BigInt('13278621593000000') + }, + 'title': 'Fastenal Company 2021 Q3 - Results - Earnings Call Presentation', + 'description': '', + 'url': { + 'url': 'https://seekingalpha.com/article/4459715-fastenal-company-2021-q3-results-earnings-call-presentation?source=feed_all_articles' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/07fd83154e9157f70207bd1aa6dd7998ec3aa6a1b730aef93868a9ac950e912e.jpg.pad' + } + }, + 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Seeking Alpha', + 'score': 14.391950109691049, + 'relativeTimeDescription': '23 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Sports', + 'publishTime': { + internalValue: BigInt('13278621564000000') + }, + 'title': 'One-handed CB Marshon Lattimore still playing lights-out: Saints takeaways vs. Washington', + 'description': 'Marshon Lattimore and WR Deonte Harris were standouts in the Saints\' 33-22 win over Washington, and they weren\'t the only ones.', + 'url': { + 'url': 'https://theathletic.com/2885011/2021/10/13/one-handed-cb-marshon-lattimore-still-playing-lights-out-saints-takeaways-vs-washington/?source=rss' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/dbb85cea85bf1cb02a2e31827af8f4621a7fe1e3cd465739db8cda2e5c45e5db.jpg.pad' + } + }, + 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The Athletic', + 'score': 14.434964041654474, + 'relativeTimeDescription': '24 minutes ago' + } + } + } + ] + }, + { + 'cardType': 6, + 'items': [ + { + article: undefined, + deal: undefined, + promotedArticle: { + 'data': { + 'categoryName': 'Brave Partners', + 'publishTime': { + internalValue: BigInt('13278528021000000') + }, + 'title': 'The Beginner’s Guide to Account-Based Marketing (ABM)', + 'description': 'This leads to a common paradox—marketing can hit its goals by bringing in a high volume of leads, but sales can’t hit its goals because those same leads are poorly qualified. Account-based marketing (ABM) aims to fix that by tightly…Read more ›', + 'url': { + 'url': 'https://ahrefs.com/blog/account-based-marketing/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/9d2f7ab67a81520a1e281d9def23ce4f4fbcf46cac8be01707f0cd0fb24e597a.jpg.pad' + } + }, + 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Ahrefs', + 'score': 22.921395422138495, + 'relativeTimeDescription': '1 day ago' + }, + 'creativeInstanceId': '2626e169-a372-42ca-af14-b0df795d2819' + } + } + ] + }, + { + 'cardType': 2, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13276800071000000') + }, + 'title': 'Brave Launches Brave Talk for Privacy-Preserving Video Conferencing', + 'description': 'Today, Brave launched Brave Talk, a new privacy-focused video conferencing feature built directly into the Brave browser.\nThe post Brave Launches Brave Talk for Privacy-Preserving Video Conferencing appeared first on Brave Browser.', + 'url': { + 'url': 'https://brave.com/brave-talk-launch/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/8bc59275469ab8db7c8245e5269b5bfa84a8e77586de349dd1bce0435c27a6a5.jpg.pad' + } + }, + 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Blog', + 'score': 28.831838413657156, + 'relativeTimeDescription': '21 days ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13276723068000000') + }, + 'title': 'Research Paper: Privacy and Security Issues in Web 3.0', + 'description': 'We at Brave Research just published a technical report called “Privacy and Security Issues in Web 3.0” on arXiv. This blog post summarizes our findings and puts them in perspective for Brave users.\nThe post Research Paper: Privacy and Security Issues in Web 3.0 appeared first on Brave Browser.', + 'url': { + 'url': 'https://brave.com/research-paper-privacy-and-security-issues-in-web-3-0/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/260702227df254d2ada663cbdb14442933161f55da5b891d59f82335015a025d.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Blog', + 'score': 57.82917689204489, + 'relativeTimeDescription': '22 days ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13276712102000000') + }, + 'title': 'What’s Brave Done For My Privacy Lately? Episode #10: Custom Filter List Subscriptions', + 'description': 'This is the tenth in a series of blog posts on new Brave privacy features. This post describes work done by Anton Lazarev, Research Engineer. Authors: Peter Snyder and Anton Lazarev.\nThe post What’s Brave Done For My Privacy Lately? Episode #10: Custom Filter List Subscriptions appeared first on Brave Browser.', + 'url': { + 'url': 'https://brave.com/privacy-updates-10/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/2d9c785ca6d8dcdcf2d2046796d97a81cb99a002fb91895cca43a6846aeb3a2f.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Blog', + 'score': 115.70439692971131, + 'relativeTimeDescription': '22 days ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Entertainment', + 'publishTime': { + internalValue: BigInt('13278621540000000') + }, + 'title': 'We see, acknowledge, and connect with Larry David in the trailer for Curb Your Enthusiasm’s new season', + 'description': 'Larry David and his seemingly endless array of irritations are returning for another glorious season of Curb Your Enthusiasm on October 24. But you don’t have to wait until then to see how stupid things like prayers and toasts are. They’re all right here in the brand new trailer. Read more...', + 'url': { + 'url': 'https://www.avclub.com/we-see-acknowledge-and-connect-with-larry-david-in-th-1847856248' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/d299252e34751a7d534376404149e30c574809d4f2a297dfc8ef4ac97e55fa3d.jpg.pad' + } + }, + 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The A.V. Club', + 'score': 14.469881193573206, + 'relativeTimeDescription': '24 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Business', + 'publishTime': { + internalValue: BigInt('13278621540000000') + }, + 'title': 'Stocks Up Slightly After Inflation Data, Major Earnings', + 'description': 'U.S. share benchmarks ticked up as fresh consumer-price data boosted the view that the bout of elevated inflation might last longer.', + 'url': { + 'url': 'https://www.wsj.com/articles/global-stock-markets-dow-update-10-13-2021-11634110620?mod=rss_markets_main' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/9a51fc8831ae2c54f2a02293bc472e88abe2f9ded6310dc18506badcf581837a.jpg.pad' + } + }, + 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'WSJ - Markets', + 'score': 14.469883937507502, + 'relativeTimeDescription': '24 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278621508000000') + }, + 'title': 'Fox News Hosts Shout Down Liberal Panelist for Saying NYC Is No ‘Hellhole’', + 'description': 'Fox NewsFox News hosts Lisa “Kennedy” Montgomery and Julie Banderas on Wednesday took turns berating a liberal panelist for having the temerity to claim that dispute their claim that New York City is a “hellhole.”Kennedy and Banderas, in an effort to bolster their case that the city is a terrifying, crime-ridden wasteland, boasted that they “talk to cops” and read the New York Post as evidence for their claims. Besides whipping their viewers into a frenzy over vaccine mandates and critical race ', + 'url': { + 'url': 'https://www.thedailybeast.com/fox-news-hosts-kennedy-and-julie-banderas-shout-down-liberal-panelist-for-saying-nyc-is-no-hellhole?source=articles&via=rss&utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+thedailybeast%2Farticles+%28The+Daily+Beast+-+Latest+Articles%29' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/3cdc6866dee932665c37ac1ae515c360ff77b47c6a555c3f02a534a79bd49308.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'The Daily Beast', + 'score': 14.515511304097387, + 'relativeTimeDescription': '25 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Health', + 'publishTime': { + internalValue: BigInt('13278621506000000') + }, + 'title': 'Hilarie Burton Morgan on the Problem With True Crime, Life on the Farm, and Her ‘One Tree Hill’ Podcast', + 'description': 'Plus how she\'s practicing self-care.', + 'url': { + 'url': 'https://www.self.com/story/hilarie-burton-morgan-interview' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/3d1c2c118390f7d72a6502148c8733a2cbcb3a4eb3795307ab5a6c8b43671f19.jpg.pad' + } + }, + 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'SELF', + 'score': 14.518330759046432, + 'relativeTimeDescription': '25 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Business', + 'publishTime': { + internalValue: BigInt('13278621467000000') + }, + 'title': 'Plug Power jumps 13% after it partners with Airbus to study and develop hydrogen-powered air travel', + 'description': 'Airbus is working towards a goal of bringing zero-emission aircraft to market by 2035, and it thinks hydrogen could help achieve that objective.', + 'url': { + 'url': 'https://markets.businessinsider.com/news/stocks/plug-power-stock-price-airbus-partnership-hydrogen-airport-study-phillips-2021-10' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/932bcb6a3ce07dc885c0ce7b881c3dfeb9fb643e6d5ce968decf14d1983dbbdd.jpg.pad' + } + }, + 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Business Insider', + 'score': 14.572489911501611, + 'relativeTimeDescription': '25 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278621435000000') + }, + 'title': '60K Film and TV Workers May Strike Within Days as Industry Still Recovers From Pandemic', + 'description': '\"Without an end date, we could keep talking forever,\" said President of the International Alliance of Theatrical Stage Employees Matthew Loeb.', + 'url': { + 'url': 'https://www.newsweek.com/60k-film-tv-workers-may-strike-within-days-industry-still-recovers-pandemic-1638658' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/1e4526119064ccbd65d5fc372fe878ff60b64c1ba9047f3cb664b2afdeddf73f.jpg.pad' + } + }, + 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Newsweek', + 'score': 14.615857594582533, + 'relativeTimeDescription': '26 minutes ago' + } + } + } + ] + }, + { + 'cardType': 5, + 'items': [] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Technology', + 'publishTime': { + internalValue: BigInt('13278621421000000') + }, + 'title': 'Apple AirPods 3 rumored to debut alongside new MacBook Pros at October 18 Unleashed event', + 'description': ' Yesterday Apple confirmed a new “Unleashed” launch event scheduled on October 18. The stars of the show will almost certainly be Apple’s next-generation ARM-based M-series chipset dubbed M1X and all-new 14” and 16” MacBook Pro laptops. Now Weibo leaker @PandaIsBald is also throwing in the long-rumored AirPods 3 to the mix.\n\n\n\n\nAirPods 3 leaked design (images: 52audio.com)\n\nApple’s regular non-Pro AirPods 2 have been out since March 2019 and are clearly due for an update. The third generation w', + 'url': { + 'url': 'https://www.gsmarena.com/apple_airpods_3_rumored_to_debut_alongside_new_macbook_pros_at_october_18_unleashed_event-news-51408.php' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/3352539771a4ac7482f4486de7900c458f052e11b605609fafcf206bb4634075.jpg.pad' + } + }, + 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'GSMArena', + 'score': 14.634541104175572, + 'relativeTimeDescription': '26 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Business', + 'publishTime': { + internalValue: BigInt('13278621420000000') + }, + 'title': ': Starbucks and Netflix partner for series tied to Netflix’s book club', + 'description': 'Starbucks and Netflix have partnered for a series that will discuss how the streaming service\'s book club choices are transformed into movies and shows\n \n', + 'url': { + 'url': 'https://www.marketwatch.com/story/starbucks-and-netflix-partner-for-series-tied-to-netflixs-book-club-11634130338?rss=1&siteid=rss' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/98683b85d91545ed868557351adee4d1b6fa45337082c356c36cedcc9a1c0085.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'MarketWatch', + 'score': 14.635871140907735, + 'relativeTimeDescription': '26 minutes ago' + } + } + } + ] + }, + { + 'cardType': 3, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Culture', + 'publishTime': { + internalValue: BigInt('13278621400000000') + }, + 'title': 'Cheat Maker Is Not Afraid of Call of Duty’s New Kernel-Level Anti-Cheat', + 'description': 'Activision announced the launch of a kernel-level anti-cheat system called RICOCHET to fight cheaters.', + 'url': { + 'url': 'https://www.vice.com/en/article/z3xjqa/cheat-maker-is-so-far-not-afraid-of-call-of-dutys-new-kernel-level-anti-cheat' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/f2dbdd22cf97479e7c2bccae2d3becc8346edd6b603ca2350ee02c35546de347.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'VICE', + 'score': 14.662245395018518, + 'relativeTimeDescription': '27 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Culture', + 'publishTime': { + internalValue: BigInt('13278620323000000') + }, + 'title': 'Airlines Are Already Defying the Texas Ban on Vaccine Mandates', + 'description': 'Gov. Greg Abbott made companies choose between state law and federal regulations, and Southwest and American Airlines made that choice pretty fast.', + 'url': { + 'url': 'https://www.vice.com/en/article/g5gzw7/airlines-are-defying-texas-ban-on-vaccine-mandates' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/39c1559de30f9abf91d6035ad655be5e8ba09f64ab4a73201010cb8921248068.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'VICE', + 'score': 31.45969721478307, + 'relativeTimeDescription': '45 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Culture', + 'publishTime': { + internalValue: BigInt('13278620305000000') + }, + 'title': 'The Best Dog DNA Tests That Actually Work, According to the Dog-Obsessed', + 'description': 'Help your pup assume their rightful throne as king of the dog park with the best dog DNA tests, according to convinced and happy pet owners.', + 'url': { + 'url': 'https://www.vice.com/en/article/dyvad7/best-dog-dna-tests' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/c6147aad0103a239ba49c79fa7696593bbf82558ba08a5362462fbbeea0a195c.jpg.pad' + } + }, + 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'VICE', + 'score': 62.9745088197745, + 'relativeTimeDescription': '45 minutes ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Sports', + 'publishTime': { + internalValue: BigInt('13278621373000000') + }, + 'title': 'Poor IPL run not a concern, Nicholas Pooran wants to just \'refocus and go again\'', + 'description': '\"We won two World Cups without getting singles\" - West Indies vice-captain says power-hitting will still be big on the team\'s agenda', + 'url': { + 'url': 'https://www.espncricinfo.com/story/t20-world-cup-west-indies-vice-captain-nicholas-pooran-wants-to-just-refocus-and-go-again-after-poor-ipl-2021-1282815?ex_cid=OTC-RSS' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/01dde228cf27287b3ef5d3c9b5293c7b8cdaa59e269aa856f080c535016b7e01.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'ESPN - Cricket', + 'score': 14.697300142885872, + 'relativeTimeDescription': '27 minutes ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278621361000000') + }, + 'title': 'Navy engineer smuggled secrets \'a few at a time\' and wanted to meet \'foreign spies\' for drinks', + 'description': 'Written communications between Jonathan Toebbe and an undercover FBI agent posing as a foreign spy show that he collected secret data over the years. Toebbe also planned to be extracted from the US.', + 'url': { + 'url': 'https://www.dailymail.co.uk/news/article-10087987/Navy-engineer-smuggled-secrets-time-wanted-meet-foreign-spies-drinks.html?ns_mchannel=rss&ns_campaign=1490&ito=1490' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/876f15ba1c5582a0bef82b3cd6b2d567d04d9394ef208dc3e3749a3d3da46df2.jpg.pad' + } + }, + 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Daily Mail', + 'score': 14.712686451514855, + 'relativeTimeDescription': '27 minutes ago' + } + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Technology', + 'publishTime': { + internalValue: BigInt('13278621350000000') + }, + 'title': 'How Quickbase is using low-code to streamline Daifuku’s supply chain', + 'description': 'At VentureBeat\'s Low-Code/No-Code Summit, Quickbase explained how its platform helps connect and automate systems, processes, and workloads.', + 'url': { + 'url': 'https://venturebeat.com/2021/10/13/how-quickbase-is-using-low-code-to-streamline-daifukus-supply-chain/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/e1bab8826e0dc937e0c1ee4f79a3e63d3fbc8caf11773259dbea1cb3202b9a1a.jpg.pad' + } + }, + 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'VentureBeat', + 'score': 14.726687401901625, + 'relativeTimeDescription': '27 minutes ago' + } + } + } + ] + }, + { + 'cardType': 4, + 'items': [ + { + article: undefined, + promotedArticle: undefined, + deal: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13256920996000000') + }, + 'title': 'Sony 55 Inch TV', + 'description': 'BRAVIA OLED 4K Ultra HD Smart TV with HDR and Alexa Compatibility', + 'url': { + 'url': 'https://www.amazon.com/dp/B084KQFNBX?tag=bravesoftware-20&linkCode=osi&th=1&psc=1&language=en_US¤cy=USD' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/6b0de702b8c1596cb713c89f3b79b568959cfebf4af6611e63c54d39a43e0233.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Offers', + 'score': 270.2865946654164, + 'relativeTimeDescription': '251 days ago' + }, + 'offersCategory': 'Electronics' + } + }, + { + article: undefined, + promotedArticle: undefined, + deal: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13256920996000000') + }, + 'title': 'Samsung Galaxy Tab S7', + 'description': 'Go for hours on a single charge, and back to 100% with the fast-charging USB-C port.', + 'url': { + 'url': 'https://www.amazon.com/dp/B08FBN5STQ?tag=bravesoftware-20&linkCode=osi&th=1&psc=1&language=en_US¤cy=USD' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/b8b0e9f54885248f635c620d638c716164972f0b7f72c5ec79357517e2d2a171.jpg.pad' + } + }, + 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Offers', + 'score': 540.5731893340486, + 'relativeTimeDescription': '251 days ago' + }, + 'offersCategory': 'Electronics' + } + }, + { + article: undefined, + promotedArticle: undefined, + deal: { + 'data': { + 'categoryName': 'Brave', + 'publishTime': { + internalValue: BigInt('13256920996000000') + }, + 'title': 'Samsung Curved LED-Lit Monitor', + 'description': 'A stylish design featuring a Black body metallic finish and sleek curves', + 'url': { + 'url': 'https://www.amazon.com/dp/B079K3MXWF?tag=bravesoftware-20&linkCode=osi&th=1&psc=1&language=en_US¤cy=USD' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/e2bc4f0ca84cc9bc7183a92072bf41cbcdd4bd4e31da0a5d6e084a1da2c2c7fd.jpg.pad' + } + }, + 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Brave Offers', + 'score': 1081.146378673966, + 'relativeTimeDescription': '251 days ago' + }, + 'offersCategory': 'Electronics' + } + } + ] + }, + { + 'cardType': 0, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Cars', + 'publishTime': { + internalValue: BigInt('13278542934000000') + }, + 'title': 'Rivian’s first retail hub to open in Venice, CA, as ‘a space to gather’', + 'description': '\nFresh off the heels of delivering its flagship R1T electric pickup to first customers, Rivian has shared details of its first hub, centered in Venice, California. Rather than existing as solely a retail space, Rivian hopes this first of several hub locations will offer a space for public gatherings and encourages its community to visit and connect. If you happen to order a $70,000 EV while you’re there… well that’s a welcomed option as well.\n more…\nThe post Rivian’s first retail hub to open in ', + 'url': { + 'url': 'https://electrek.co/2021/10/12/rivians-first-retail-hub-to-open-in-venice-ca-as-a-space-to-gather/' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/c85762ffe3bd48886ed1ce58f868958989ad89b0723f5844a7ffe0fef57fa1c5.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Electrek', + 'score': 23121.416404237974, + 'relativeTimeDescription': '22 hours ago' + } + } + } + ] + }, + { + 'cardType': 1, + 'items': [ + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Business', + 'publishTime': { + internalValue: BigInt('13278518400000000') + }, + 'title': 'Tech’s Exponential Growth – and How to Solve the Problems It’s Created', + 'description': 'Understanding and improving the impact that Big Tech has on society\n\n \n', + 'url': { + 'url': 'https://hbr.org/podcast/2021/10/techs-exponential-growth-and-how-to-solve-the-problems-its-created?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+harvardbusiness+%28HBR.org%29' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/703dace4d9ab2590c184826c2a7b39ed2249cace04101f891c011c31021a3b9d.jpg.pad' + } + }, + 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'Harvard Business Review', + 'score': 5917.313882859709, + 'relativeTimeDescription': '1 day ago' + } + } + }, + { + promotedArticle: undefined, + deal: undefined, + article: { + 'data': { + 'categoryName': 'Top News', + 'publishTime': { + internalValue: BigInt('13278596700000000') + }, + 'title': 'Global Climate Pledges Off Track to Meet Paris Targets, IEA Says', + 'description': 'Whether lawmakers continue existing policies or make good on recent promises, rising temperatures will exceed the limit global leaders committed to in the Paris Agreement, the International Energy Agency said.', + 'url': { + 'url': 'https://www.wsj.com/articles/governments-climate-pledges-not-enough-to-meet-paris-agreement-targets-iea-says-11634097601' + }, + 'urlHash': '', + 'image': { + imageUrl: undefined, + paddedImageUrl: { + 'url': 'https://pcdn.brave.com/brave-today/cache/1261719cac8a7c5d7d5ab0d2cc3b5b6a43cf177b3fd0a103f366f59b397efd9d.jpg.pad' + } + }, + 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', + 'publisherName': 'WSJ', + 'score': 325.5895923286794, + 'relativeTimeDescription': '7 hours ago' + } + } + } + ] + } + ] + } + ] +} + +export const mockBraveNewsController: Partial = { + async getLocale () { + return { locale: 'en-US' } + }, + + async getFeed () { + return { feed } + }, + + async getPublishers () { + return { publishers } + }, + + async getChannels () { + const channelNames = Object.values(publishers).reduce((prev, next) => { + for (const channel of next.channels) prev.add(channel) + prev.add(next.categoryName) + return prev + }, new Set()) + + return { + channels: Array.from(channelNames).map(c => ({ channel: c, isSubscribed: false })) + } + }, + + async setChannelSubscribed (channelId, subscribed) { + return { updated: { ...(await mockBraveNewsController.getChannels!())[channelId], subscribed } } + }, + + async findFeeds () { + return { results: [] } + }, + + async subscribeToNewDirectFeed () { + return { isValidFeed: false, isDuplicate: false, publishers: await mockBraveNewsController.getChannels!() } + }, + + async setPublisherPref (publisherId, newStatus) { + publishers[publisherId].userEnabledStatus = newStatus + return { + newStatus + } + } +} + +// @ts-expect-error +window.storybookBraveNewsController = mockBraveNewsController diff --git a/components/brave_new_tab_ui/stories/default/data/todayStorybookState.ts b/components/brave_new_tab_ui/stories/default/data/todayStorybookState.ts index 647804399d30..58893bebeb74 100644 --- a/components/brave_new_tab_ui/stories/default/data/todayStorybookState.ts +++ b/components/brave_new_tab_ui/stories/default/data/todayStorybookState.ts @@ -1,31 +1,6 @@ import { boolean } from '@storybook/addon-knobs' -import * as BraveNews from '../../../api/brave_news' import { BraveTodayState } from '../../../reducers/today' - -// Generate feed page from real data in devtools: -// let pids = [ -// "5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "d4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5", -// "gd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5" -// ] -// -// copy(newState.feed.pages[0].items.map( i => ({ ...i, items: i.items.map(ii => { -// let data = ii.article?.data || ii.promotedArticle?.data || ii.deal?.data -// if (ii.article) { data = ii.article.data } -// if (ii.promotedArticle) { data = ii.promotedArticle.data } -// data.publishTime.internalValue = data.publishTime.internalValue.toString() -// data.publisherId = pids[Math.floor(Math.random() * (pids.length - 1))] -// if (!ii.article) ii.article = "undefined" -// if (!ii.promotedArticle) ii.promotedArticle = "undefined" -// if (!ii.deal) ii.deal = "undefined" -// return ii -// })}))) +import { feed, publishers } from './mockBraveNewsController' export default function getTodayState (): BraveTodayState { const hasDataError = boolean('Today data fetch error?', false) @@ -36,1925 +11,7 @@ export default function getTodayState (): BraveTodayState { currentPageIndex: 10, cardsViewed: 0, cardsVisited: 0, - publishers: hasDataError ? undefined : { - 'direct:https://example.com/feed': { - publisherId: 'direct:https://example.com/feed1', - publisherName: 'My Custom Feed 1', - categoryName: 'User feeds', - channels: ['User feeds'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.DIRECT_SOURCE, - isEnabled: true, - userEnabledStatus: BraveNews.UserEnabled.ENABLED - }, - 'direct:https://example2.com/feed': { - publisherId: 'direct:https://example.com/feed2', - publisherName: 'My Custom Feed 2', - categoryName: 'User feeds', - channels: ['User feeds'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.DIRECT_SOURCE, - isEnabled: true, - userEnabledStatus: BraveNews.UserEnabled.ENABLED - }, - '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 1', - categoryName: 'Tech', - channels: ['Tech'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.ENABLED - }, - '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 2', - categoryName: 'Top News', - channels: ['Top News'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED - }, - 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 3', - categoryName: 'Tech 2', - channels: ['Tech 2'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.ENABLED - }, - 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 4', - categoryName: 'Top News 1', - channels: ['Top News 1'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED - }, - 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 5 has A very very very very very very very very very very very very very very very very very very very very long publisher name', - categoryName: 'Tech 2', - channels: ['Tech 2'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.ENABLED - }, - 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 6', - categoryName: 'Top News 2', - channels: ['Top News 2'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED - }, - 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 7', - categoryName: 'Top News 3', - channels: ['Top News 3'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED - }, - 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 8', - categoryName: 'Tech 3', - channels: ['Tech 3'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.ENABLED - }, - 'gd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5': { - publisherId: '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - publisherName: 'Test Publisher 9', - categoryName: 'Top News 4', - channels: ['Top News 4'], - feedSource: { url: 'http://www.example.com/feed' }, - siteUrl: { url: 'https://www.example.com' }, - locales: ['en_US'], - type: BraveNews.PublisherType.COMBINED_SOURCE, - isEnabled: false, - userEnabledStatus: BraveNews.UserEnabled.NOT_MODIFIED - } - }, - feed: hasDataError ? undefined : { - hash: '123abc', - featuredItem: { - promotedArticle: undefined, - deal: undefined, - article: { - data: { - categoryName: 'Top News', - description: 'Here\'s everything you need to know about the Haunted Hallows event, including how to unlock the Batmobile.', - image: { imageUrl: { url: 'https://placekitten.com/1360/912' }, paddedImageUrl: undefined }, - publishTime: { internalValue: BigInt('13278618001000000') }, - publisherId: 'd75d65f0f747650ef1ea11adb0029f9d577c629a080b5f60ec80d125b2bf205b', - publisherName: 'Newsweek', - relativeTimeDescription: '1 hour ago', - urlHash: '', - score: 14.200669212327124, - title: '\'Rocket League\' Haunted Hallows 2021: Details Revealed and How to Unlock Batmobile Cars', - url: { url: 'https://www.newsweek.com/rocket-league-haunted-hallows-halloween-2021-batmobile-price-date-1638020' } - } - } - }, - pages: [ - { - items: [ - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Sports', - 'publishTime': { - internalValue: BigInt('13278621116000000') - }, - 'title': 'The agony of the feet: Why turf toe is such a dreaded injury in the NFL', - 'description': 'A misunderstood and often dismissed condition, a big toe hyperextension can cause crippling pain with every step, creating mental anguish and fatigue that take a huge toll on an athlete. After decades of occurrences, it\'s finally being taken seriously.', - 'url': { - 'url': 'https://www.espn.com/nfl/story/_/id/32379942/why-turf-toe-such-dreaded-injury-nfl' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/944ee78cb12ebda3d46c51a4fb91db1a961d3dee13b8eb1034798ae5ca2150dc.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'ESPN', - 'score': 10.00343277763486, - 'relativeTimeDescription': '31 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Sports', - 'publishTime': { - internalValue: BigInt('13278620426000000') - }, - 'title': 'Spain didn\'t win Nations League but Oyarzabal is a star', - 'description': 'Spain\'s latest super-group of teenagers has caught the imagination this week, but 24-year-old Real Sociedad winger Mikel Oyarzabal might be the one to lead them.', - 'url': { - 'url': 'https://www.espn.com/soccer/spain-esp/story/4495432/spain-didnt-win-nations-league-but-they-have-a-gem-in-mikel-oyarzabal' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/9bd848550d4f968a1f93c46a1c4f142cb293ad8220092d8d3017b32d38b08c67.jpg.pad' - } - }, - 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'ESPN - Football', - 'score': 10.649119373004728, - 'relativeTimeDescription': '43 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Technology', - 'publishTime': { - internalValue: BigInt('13278621733000000') - }, - 'title': 'Twitter is launching a Spaces accelerator program to pay live audio creators', - 'description': '\n\nIllustration by Alex Castro / The Verge\n\nTwitter announced on Tuesday that it plans to support Twitter Spaces creators through a new three-month accelerator program called the Twitter Spaces Spark Program. Twitter’s plans follow a similar creator three-month program that Clubhouse launched in March 2021.\nThe Spark Program is designed to “discover and reward” around 150 Spaces creators with technical, financial, and marketing support, Twitter says. For anyone who applies and gets in, that inclu', - 'url': { - 'url': 'https://www.theverge.com/2021/10/13/22724450/twitter-spaces-accelerator-spark-clubhouse-creators' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/023e9e849e2a2af24c5271faa0ba5b25b15f4c90481a3583e948829ef40e881a.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The Verge', - 'score': 14.170018256553691, - 'relativeTimeDescription': '21 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Science', - 'publishTime': { - internalValue: BigInt('13278621699000000') - }, - 'title': 'Widespread masking nudges people to follow the crowd', - 'description': 'When wearing a mask to defend against the spread of COVID-19 becomes a trend, more people mask up themselves, a new study shows.', - 'url': { - 'url': 'https://www.futurity.org/covid-19-mask-viruses-2641802-2/?utm_source=rss&utm_medium=rss&utm_campaign=covid-19-mask-viruses-2641802-2' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/b735cb231cc73ce065177509ecda0ef35203e55bee5bfa798401a7bc67893182.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Futurity', - 'score': 14.226233174417557, - 'relativeTimeDescription': '22 minutes ago' - } - } - } - ] - }, - { - 'cardType': 6, - 'items': [ - { - article: undefined, - deal: undefined, - promotedArticle: { - 'data': { - 'categoryName': 'Brave Partners', - 'publishTime': { - internalValue: BigInt('13278621628000000') - }, - 'title': 'Audiovox (VOXX) Q2 2022 Earnings Call Transcript', - 'description': 'VOXX earnings call for the period ending September 30, 2021.', - 'url': { - 'url': 'https://www.fool.com/earnings/call-transcripts/2021/10/13/audiovox-voxx-q2-2022-earnings-call-transcript/?source=thebrave&utm_source=foo&utm_medium=feed&utm_campaign=article' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/5b3d8da219eee17bce800689085994a6a851545aa99b35c374874f42a93c672b.jpg.pad' - } - }, - 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The Motley Fool', - 'score': 14.338672770645763, - 'relativeTimeDescription': '23 minutes ago' - }, - 'creativeInstanceId': 'd2d506aa-5531-4069-8f85-7d9052f1b640' - } - } - ] - }, - { - 'cardType': 2, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278621613000000') - }, - 'title': 'Brexit: Most NI checks on British goods to be scrapped', - 'description': 'The proposals are a \"genuine response\" to address Brexit trade issues, says the European Commission.', - 'url': { - 'url': 'https://www.bbc.co.uk/news/uk-northern-ireland-58871221?at_medium=RSS&at_campaign=KARANGA' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/1d465c5238d91f25be576c554f691bd651be6d1346382888a9636c52258f2d67.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'BBC', - 'score': 14.361647694838718, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278621600000000') - }, - 'title': 'What needs to be done to fix the tax system?', - 'description': 'Death duties, hiking the GST and more taxes on housing are on the wish lists of the nation’s top economists.', - 'url': { - 'url': 'https://www.smh.com.au/politics/federal/what-needs-to-be-done-to-fix-the-tax-system-20211004-p58x26.html?ref=rss&utm_medium=rss&utm_source=rss_feed' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/40a3ae95c9d10d6988236d4e17e0533f2528259d67ae3ed4b44f8243b65f764e.jpg.pad' - } - }, - 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Sydney Morning Herald', - 'score': 14.381381289249747, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278621533000000') - }, - 'title': 'Woman Parades Through Airport Completely Naked, Makes Small Talk With Travelers', - 'description': '\'The woman asked travelers how they were doing and where they are from\'', - 'url': { - 'url': 'https://dailycaller.com/2021/10/13/denver-airport-naked-woman/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/e9f94baedccb0fe6aa4679dba6762ded76b6d4a412149c55b29d5090fff182c5.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Daily Caller', - 'score': 14.479957341206271, - 'relativeTimeDescription': '24 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Business', - 'publishTime': { - internalValue: BigInt('13278621684000000') - }, - 'title': 'William Shatner emotionally describes spaceflight to Jeff Bezos: \'The most profound experience\'', - 'description': 'William Shatner, after returning to Earth, recounted his experience in an emotional talk with Blue Origin founder Jeff Bezos.', - 'url': { - 'url': 'https://www.cnbc.com/2021/10/13/william-shatner-speech-to-jeff-bezos-after-blue-origin-launch.html' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/c02a9c39546df2890a411e6afdac5f8a10ceded30947c4688fcfde43010c1d84.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'CNBC', - 'score': 14.250519019150758, - 'relativeTimeDescription': '22 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Cars', - 'publishTime': { - internalValue: BigInt('13278621683000000') - }, - 'title': 'Lucid Air’s DreamDrive ADAS Suite Has LiDAR, 14 Cameras, And 32 Sensors For Future Proofing', - 'description': 'Lucid says that their overengineered tech suite will eventually be updated to include a \"hands-off, eyes-off\" driver assistance system.', - 'url': { - 'url': 'https://www.carscoops.com/2021/10/lucid-airs-dreamdrive-adas-suite-has-lidar-14-cameras-and-32-sensors-for-future-proofing/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/31b73f47c42c5323b6db05ff9907eee4b5217ca9a097e4aa810272609aebb06b.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Carscoops', - 'score': 14.252130607208834, - 'relativeTimeDescription': '22 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Home', - 'publishTime': { - internalValue: BigInt('13278621634000000') - }, - 'title': 'DMTV Milkshake: Cultivating Elegance at Home With Melissa Lee', - 'description': 'Melissa Lee, a self-titled aesthete, shares the invisible element that can change a space drastically when it comes to interior design.', - 'url': { - 'url': 'https://design-milk.com/dmtv-milkshake-cultivating-elegance-at-home-with-melissa-lee/?utm_source=feedburner&utm_campaign=Feed%3A+design-milk+%28Design+Milk%29' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/d00bc1449477f06cf7640231fd898be6c4e6fbd9f6eda99f1b845bb739419376.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Design Milk', - 'score': 14.329404416919667, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278621632000000') - }, - 'title': '‘You’ Renewed For Season 4 By Netflix', - 'description': 'Ahead of the Season 3 premiere on Friday, Netflix has handed an early fourth season renewal to its hit drama series You. Casting news for the new season will be announced at a later date. Starring Penn Badgley and Victoria Pedretti, You is developed by Sera Gamble and Greg Berlanti, and Gamble also serves as […]', - 'url': { - 'url': 'https://deadline.com/2021/10/you-renewed-season-4-netflix-1234855244/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/3b6d9b50f065b320acd02393d96199d9f45028b14399c2b971a85590da89ae33.jpg.pad' - } - }, - 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Deadline', - 'score': 14.332498680036917, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278621625000000') - }, - 'title': 'AFI Fest Full Lineup: 2021 Festival Adds Pedro Almodovar’s ‘Parallel Mothers’ and More', - 'description': 'As previously announced, the awards-facing festival will open with the premiere of Lin-Manuel Miranda\'s \"Tick Tick Boom.\"', - 'url': { - 'url': 'https://www.indiewire.com/2021/10/afi-fest-full-lineup-2021-festival-1234671528/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/d42fe1b86e568d6dca5dc3623a2bb982f3fb337ff79ae338a3544b3cc4cab1b9.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'IndieWire', - 'score': 14.343289731415563, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278621617000000') - }, - 'title': 'YOU Renewed for Season 4 at Netflix — Watch Announcement Video', - 'description': 'Netflix’s Joe Goldberg obsession continues with a Season 4 renewal of YOU, TVLine has learned. This news comes just two days before the Penn Badgley thriller is set to premiere its third season on Friday, Oct. 15. Based on Caroline Kepnes’ series of novels, YOU is developed by executive producers Greg Berlanti and Sera Gamble, […]', - 'url': { - 'url': 'https://tvline.com/2021/10/13/you-renewed-season-4-teaser-video-netflix/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/cbf6594866c44ef06f3cd74ba057559fac2e8dc659e33fa406d11a3583f4b2ed.jpg.pad' - } - }, - 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'TVLine', - 'score': 14.355544195642704, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 5, - 'items': [] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Science', - 'publishTime': { - internalValue: BigInt('13278621608000000') - }, - 'title': 'First evidence of microtubules\' mechanosensitive behavior', - 'description': 'Inside cells, microtubules not only serve as a component of the cytoskeleton (cell skeleton) but also play a role in intracellular transport. In intracellular transport, microtubules act as rails for motor proteins such as kinesin and dynein. Microtubules, the most rigid cytoskeletal component, are constantly subjected to various mechanical stresses such as compression, tension, and bending during cellular activities. It has been hypothesized that microtubules also function as mechanosensors tha', - 'url': { - 'url': 'https://phys.org/news/2021-10-evidence-microtubules-mechanosensitive-behavior.html' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/66475a510a759c80d3ed6bfac562fa8c4dd802b510326c8d0cae28eaae250444.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Phys.org', - 'score': 14.369246557105962, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Sports', - 'publishTime': { - internalValue: BigInt('13278621606000000') - }, - 'title': 'Fantasy Football Week 6 Rankings: Updated Overview for All Positions', - 'description': 'We\'ve reached a critical point in the NFL season for fantasy managers. Bye weeks are here, and the Atlanta Falcons, New Orleans Saints, New York Jets and San Francisco 49ers have the first off rotation of the year...', - 'url': { - 'url': 'https://bleacherreport.com/articles/2949345-fantasy-football-week-6-rankings-updated-overview-for-all-positions' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/191782169a17e5df634109ecd2bb76c24cb48faa48f876edb3a5b2694fa497ba.jpg.pad' - } - }, - 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Bleacher Report', - 'score': 14.372285656733615, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 3, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278621605000000') - }, - 'title': 'Alt-Rock Singer SK8 Shares How ‘Girl Next Door’ Represents The Evolution Of His Sound', - 'description': 'After first making a splash with hip-hop, SK8 has reconnected with his punk roots on \'Girls Next Door,\' and he shares how this new direction came about, what it\'s like running a label, and what\'s next.', - 'url': { - 'url': 'https://hollywoodlife.com/2021/10/13/sk8-girl-next-door-interview/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/2904d64d82725230da6b1531aab85f54ff91cc55328e61f35319bcb3e5ba5abf.jpg.pad' - } - }, - 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Hollywood Life', - 'score': 14.373802033258967, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278618574000000') - }, - 'title': 'Laverne Cox, 49, Rocks Plunging Black Swimsuit On Vacation: ‘Trans Is Beautiful’', - 'description': 'Laverne Cox made a trans-positive statement while looking fiery hot in a sexy swimsuit on a luxury vacation.', - 'url': { - 'url': 'https://hollywoodlife.com/2021/10/13/laverne-cox-plunging-swimsuit-pool-video/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/a071a47e75db68c74fc1eaf6931d9420b2345a0c12b1247cdb05da135c0d5735.jpg.pad' - } - }, - 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Hollywood Life', - 'score': 67.03023810006357, - 'relativeTimeDescription': '1 hour ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278617426000000') - }, - 'title': 'Elizabeth Warren Urges Congress To ‘Step Up’ & Protect Roe V. Wade Amidst Texas Abortion Law', - 'description': 'The Massachusetts senator also explained that the new law will be most harmful to people who don\'t have easy access to abortion while appearing on \'The View.\'', - 'url': { - 'url': 'https://hollywoodlife.com/2021/10/13/elizabeth-warren-roe-v-wade-the-view/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/d302f28efc872481f715f40ef457e419b7c35c03f514ca4a624aed099a8ec814.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Hollywood Life', - 'score': 137.80582500043627, - 'relativeTimeDescription': '2 hours ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Technology', - 'publishTime': { - internalValue: BigInt('13278621600000000') - }, - 'title': 'Here are the best resin options for your SLA/DLP 3D printer', - 'description': 'Resin printing is a little more complex than standard filament printing. Not only do you need a few must-have 3D printing accessories, but you also need to pick the right resin. When faced with multiple colors and multiple types, it\'s also easy to get turned around when choosing the right 3D printing resin. You want it to print quickly but stay strong without becoming brittle. We\'ve used as many as possible to bring you some of the best you can buy, but our favorite is Siraya Tech Fast, which pr', - 'url': { - 'url': 'https://www.windowscentral.com/best-resin-your-3d-printer?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+wmexperts+%28Windows+Central%29' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/5afe29078068e7413a7f15a16657453217fc8ad630e8291ee1c66d69c2df6f48.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Windows Central', - 'score': 14.381363717462444, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Technology', - 'publishTime': { - internalValue: BigInt('13278621600000000') - }, - 'title': 'These are the best office chairs you can buy at any budget', - 'description': 'The adage \"you get what you pay for\" definitely holds for certain categories of products, like shoes, mattresses, and yes, office chairs. The simple fact is, the more you have available to spend (up to a point), the better chair you can buy. The range of options for the best office chairs available is quite diverse and includes styles and features for just about any taste and preference. Our top pick is the AmazonBasics High-Back Leather Executive Chair. It\'s got the looks to fit in a corporate ', - 'url': { - 'url': 'https://www.androidcentral.com/best-office-chairs' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/b406b13e1fe493f9ab11140f5c33809a19125da6d5a3f1125810d299a9539da2.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Android Central', - 'score': 14.381366799582283, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Technology', - 'publishTime': { - internalValue: BigInt('13278621600000000') - }, - 'title': 'Stop Using Playlists to Look Cool and Start Using Them to Share Your Feelings', - 'description': 'Do you struggle to express yourself with words? Consider turning to the one universal language we all share: emo playlists. I believe we, as a society, waste time trying to show off “aesthetic” music tastes. Instead, we should spend more time creating emotionally-charged, hyper-specific playlists. What’s more, we need…Read more...', - 'url': { - 'url': 'https://lifehacker.com/stop-using-playlists-to-look-cool-and-start-using-them-1847855875' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/87267b91579ebbf8ead3f305759376022fd50d3273209e851727cdf24b142304.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Lifehacker', - 'score': 14.38136972344708, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 4, - 'items': [ - { - article: undefined, - promotedArticle: undefined, - deal: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13258305902000000') - }, - 'title': 'Audible Plus', - 'description': 'Listen anytime, anywhere to an unmatched selection of audiobooks, premium podcasts, and more', - 'url': { - 'url': 'https://www.amazon.com/hz/audible/mlp/mdp/discovery?ref_=assoc_tag_ph_1524216631897&_encoding=UTF8&camp=1789&creative=9325&linkCode=pf4&tag=bravesoftware-20&linkId=c6d187d14da9ca69e1a1a950348e100e' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/a29e3a601efa77ba5f2f35e58b40037b527f4e577112ea9967ced44741dcce32.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Offers', - 'score': 33.65394030598059, - 'relativeTimeDescription': '235 days ago' - }, - 'offersCategory': 'Discounts' - } - }, - { - article: undefined, - promotedArticle: undefined, - deal: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13258305902000000') - }, - 'title': 'Amazon Prime Music', - 'description': 'Listen to your favourite songs online from Brave.', - 'url': { - 'url': 'https://www.amazon.com/music/prime' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/04256e526b5cc73ddf7679ed907ac0f89e01d7d6af3a9d1c9faba288468c03ff.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Offers', - 'score': 67.3078806123441, - 'relativeTimeDescription': '235 days ago' - }, - 'offersCategory': 'Discounts' - } - }, - { - article: undefined, - promotedArticle: undefined, - deal: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13258305902000000') - }, - 'title': 'Amazon Prime', - 'description': 'Enjoy exclusive Amazon Originals as well as popular movies and TV shows.', - 'url': { - 'url': 'https://www.amazon.com/amazonprime/146-1781179-3199520?_encoding=UTF8&camp=1789&creative=9325&linkCode=pf4&linkId=a402d5b2ca72ea0a267707ef10878979&primeCampaignId=prime_assoc_ft&ref_=assoc_tag_ph_1427739975520&tag=bravesoftware-20' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/c12d5200d342e72919e8420ccea6581ee4bf8e7ab510dd58bbc24d49ef22c36f.jpg.pad' - } - }, - 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Offers', - 'score': 134.6157612254411, - 'relativeTimeDescription': '235 days ago' - }, - 'offersCategory': 'Discounts' - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278609481000000') - }, - 'title': '\'Havana Syndrome\' mystery expands with new cases at U.S. Embassy in Colombia', - 'description': 'U.S. Embassy personnel in Bogota, Colombia, have reported symptoms aligned with the mysterious “Havana Syndrome” that continues to plague U.S. spies and diplomats around the globe. U.S. officials said Tuesday that two cases were initially reported by embassy personnel in the capital city, but said several others may have been ...', - 'url': { - 'url': 'https://www.washingtontimes.com/news/2021/oct/13/havana-syndrome-mystery-expands-new-cases-us-embas/?utm_source=RSS_Feed&utm_medium=RSS' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/4caf1b03489028604884ce33a6ac6f427f117de3670f4fefb047d92dccd3706c.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The Washington Times', - 'score': 304.21157985954886, - 'relativeTimeDescription': '4 hours ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Health', - 'publishTime': { - internalValue: BigInt('13278602700000000') - }, - 'title': 'Tom Hardy Says He Was \'Really Overweight\' as Bane in \'Dark Knight Rises\'', - 'description': '\"I was just bald, slightly porky, and with pencil arms.\"', - 'url': { - 'url': 'https://www.menshealth.com/weight-loss/a37947676/tom-hardy-overweight-bane-transformation-dark-knight-rises/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/2e81e99a21735bb5f0d3b7c8ac030a368b6e35711809a3475ba2bc6bdf7e300a.jpg.pad' - } - }, - 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Men\'s Health', - 'score': 81223.05671641101, - 'relativeTimeDescription': '6 hours ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278618011000000') - }, - 'title': 'Jodi! Tina! \'The Challenge: All Stars\' Brings in Heavy Hitters for Season 2', - 'description': 'Welcome back! After the massive success of The Challenge: All Stars earlier this year, Paramount+’s reality show is back with another season and even more vets. TJ Lavin will return to host season 2, Parmount+ announced on Wednesday, October 13, with 24 cast members — some of whom haven’t competed in nearly 20 years. “With […]', - 'url': { - 'url': 'https://www.usmagazine.com/entertainment/pictures/the-challenge-all-stars-season-2-cast-includes-tina-jodi-and-more/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/c972ec42d40a1d2a5d2b180095407f119cd2c1b17ff41b5305c5b7987b7c0280.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Us Weekly', - 'score': 544.0267160210122, - 'relativeTimeDescription': '1 hour ago' - } - } - } - ] - } - ] - }, - { - items: [ - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Sports', - 'publishTime': { - internalValue: BigInt('13278621600000000') - }, - 'title': 'USMNT vs Costa Rica: TV channel, live stream, team news & preview', - 'description': 'The Stars and Stripes tasted their first defeat since May when they fell to Panama on Sunday, but can quickly get back to winning ways', - 'url': { - 'url': 'https://www.goal.com/en/news/usmnt-vs-costa-rica-tv-channel-live-stream-team-news-preview/1ou1gnhu835jx174sq8yqn34os' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/ba5b2874d167e19150f3be8bf99a09717d912699a853321939dfbdef5bb9ff87.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Goal.com', - 'score': 14.381372591543293, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Culture', - 'publishTime': { - internalValue: BigInt('13278621600000000') - }, - 'title': 'Tyga Turns Himself in to Police Following Domestic Violence Allegations From Ex-Girlfriend Camaryn Swanson', - 'description': 'Rapper Tyga turned himself in early Tuesday to the Los Angeles Police Department following a domestic violence allegation brought forth from his ex-girlfriend Camaryn Swanson.Read more...', - 'url': { - 'url': 'https://www.theroot.com/tyga-turns-himself-in-to-police-following-domestic-viol-1847854486' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/3816eb674df32251c4b9fbda44a33acfe85764931d36c26d1f5af0cd86a0a960.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The Root', - 'score': 14.381375498821068, - 'relativeTimeDescription': '23 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Business', - 'publishTime': { - internalValue: BigInt('13278621593000000') - }, - 'title': 'Fastenal Company 2021 Q3 - Results - Earnings Call Presentation', - 'description': '', - 'url': { - 'url': 'https://seekingalpha.com/article/4459715-fastenal-company-2021-q3-results-earnings-call-presentation?source=feed_all_articles' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/07fd83154e9157f70207bd1aa6dd7998ec3aa6a1b730aef93868a9ac950e912e.jpg.pad' - } - }, - 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Seeking Alpha', - 'score': 14.391950109691049, - 'relativeTimeDescription': '23 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Sports', - 'publishTime': { - internalValue: BigInt('13278621564000000') - }, - 'title': 'One-handed CB Marshon Lattimore still playing lights-out: Saints takeaways vs. Washington', - 'description': 'Marshon Lattimore and WR Deonte Harris were standouts in the Saints\' 33-22 win over Washington, and they weren\'t the only ones.', - 'url': { - 'url': 'https://theathletic.com/2885011/2021/10/13/one-handed-cb-marshon-lattimore-still-playing-lights-out-saints-takeaways-vs-washington/?source=rss' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/dbb85cea85bf1cb02a2e31827af8f4621a7fe1e3cd465739db8cda2e5c45e5db.jpg.pad' - } - }, - 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The Athletic', - 'score': 14.434964041654474, - 'relativeTimeDescription': '24 minutes ago' - } - } - } - ] - }, - { - 'cardType': 6, - 'items': [ - { - article: undefined, - deal: undefined, - promotedArticle: { - 'data': { - 'categoryName': 'Brave Partners', - 'publishTime': { - internalValue: BigInt('13278528021000000') - }, - 'title': 'The Beginner’s Guide to Account-Based Marketing (ABM)', - 'description': 'This leads to a common paradox—marketing can hit its goals by bringing in a high volume of leads, but sales can’t hit its goals because those same leads are poorly qualified. Account-based marketing (ABM) aims to fix that by tightly…Read more ›', - 'url': { - 'url': 'https://ahrefs.com/blog/account-based-marketing/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/9d2f7ab67a81520a1e281d9def23ce4f4fbcf46cac8be01707f0cd0fb24e597a.jpg.pad' - } - }, - 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Ahrefs', - 'score': 22.921395422138495, - 'relativeTimeDescription': '1 day ago' - }, - 'creativeInstanceId': '2626e169-a372-42ca-af14-b0df795d2819' - } - } - ] - }, - { - 'cardType': 2, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13276800071000000') - }, - 'title': 'Brave Launches Brave Talk for Privacy-Preserving Video Conferencing', - 'description': 'Today, Brave launched Brave Talk, a new privacy-focused video conferencing feature built directly into the Brave browser.\nThe post Brave Launches Brave Talk for Privacy-Preserving Video Conferencing appeared first on Brave Browser.', - 'url': { - 'url': 'https://brave.com/brave-talk-launch/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/8bc59275469ab8db7c8245e5269b5bfa84a8e77586de349dd1bce0435c27a6a5.jpg.pad' - } - }, - 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Blog', - 'score': 28.831838413657156, - 'relativeTimeDescription': '21 days ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13276723068000000') - }, - 'title': 'Research Paper: Privacy and Security Issues in Web 3.0', - 'description': 'We at Brave Research just published a technical report called “Privacy and Security Issues in Web 3.0” on arXiv. This blog post summarizes our findings and puts them in perspective for Brave users.\nThe post Research Paper: Privacy and Security Issues in Web 3.0 appeared first on Brave Browser.', - 'url': { - 'url': 'https://brave.com/research-paper-privacy-and-security-issues-in-web-3-0/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/260702227df254d2ada663cbdb14442933161f55da5b891d59f82335015a025d.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Blog', - 'score': 57.82917689204489, - 'relativeTimeDescription': '22 days ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13276712102000000') - }, - 'title': 'What’s Brave Done For My Privacy Lately? Episode #10: Custom Filter List Subscriptions', - 'description': 'This is the tenth in a series of blog posts on new Brave privacy features. This post describes work done by Anton Lazarev, Research Engineer. Authors: Peter Snyder and Anton Lazarev.\nThe post What’s Brave Done For My Privacy Lately? Episode #10: Custom Filter List Subscriptions appeared first on Brave Browser.', - 'url': { - 'url': 'https://brave.com/privacy-updates-10/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/2d9c785ca6d8dcdcf2d2046796d97a81cb99a002fb91895cca43a6846aeb3a2f.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Blog', - 'score': 115.70439692971131, - 'relativeTimeDescription': '22 days ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Entertainment', - 'publishTime': { - internalValue: BigInt('13278621540000000') - }, - 'title': 'We see, acknowledge, and connect with Larry David in the trailer for Curb Your Enthusiasm’s new season', - 'description': 'Larry David and his seemingly endless array of irritations are returning for another glorious season of Curb Your Enthusiasm on October 24. But you don’t have to wait until then to see how stupid things like prayers and toasts are. They’re all right here in the brand new trailer. Read more...', - 'url': { - 'url': 'https://www.avclub.com/we-see-acknowledge-and-connect-with-larry-david-in-th-1847856248' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/d299252e34751a7d534376404149e30c574809d4f2a297dfc8ef4ac97e55fa3d.jpg.pad' - } - }, - 'publisherId': 'd4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The A.V. Club', - 'score': 14.469881193573206, - 'relativeTimeDescription': '24 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Business', - 'publishTime': { - internalValue: BigInt('13278621540000000') - }, - 'title': 'Stocks Up Slightly After Inflation Data, Major Earnings', - 'description': 'U.S. share benchmarks ticked up as fresh consumer-price data boosted the view that the bout of elevated inflation might last longer.', - 'url': { - 'url': 'https://www.wsj.com/articles/global-stock-markets-dow-update-10-13-2021-11634110620?mod=rss_markets_main' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/9a51fc8831ae2c54f2a02293bc472e88abe2f9ded6310dc18506badcf581837a.jpg.pad' - } - }, - 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'WSJ - Markets', - 'score': 14.469883937507502, - 'relativeTimeDescription': '24 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278621508000000') - }, - 'title': 'Fox News Hosts Shout Down Liberal Panelist for Saying NYC Is No ‘Hellhole’', - 'description': 'Fox NewsFox News hosts Lisa “Kennedy” Montgomery and Julie Banderas on Wednesday took turns berating a liberal panelist for having the temerity to claim that dispute their claim that New York City is a “hellhole.”Kennedy and Banderas, in an effort to bolster their case that the city is a terrifying, crime-ridden wasteland, boasted that they “talk to cops” and read the New York Post as evidence for their claims. Besides whipping their viewers into a frenzy over vaccine mandates and critical race ', - 'url': { - 'url': 'https://www.thedailybeast.com/fox-news-hosts-kennedy-and-julie-banderas-shout-down-liberal-panelist-for-saying-nyc-is-no-hellhole?source=articles&via=rss&utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+thedailybeast%2Farticles+%28The+Daily+Beast+-+Latest+Articles%29' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/3cdc6866dee932665c37ac1ae515c360ff77b47c6a555c3f02a534a79bd49308.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'The Daily Beast', - 'score': 14.515511304097387, - 'relativeTimeDescription': '25 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Health', - 'publishTime': { - internalValue: BigInt('13278621506000000') - }, - 'title': 'Hilarie Burton Morgan on the Problem With True Crime, Life on the Farm, and Her ‘One Tree Hill’ Podcast', - 'description': 'Plus how she\'s practicing self-care.', - 'url': { - 'url': 'https://www.self.com/story/hilarie-burton-morgan-interview' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/3d1c2c118390f7d72a6502148c8733a2cbcb3a4eb3795307ab5a6c8b43671f19.jpg.pad' - } - }, - 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'SELF', - 'score': 14.518330759046432, - 'relativeTimeDescription': '25 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Business', - 'publishTime': { - internalValue: BigInt('13278621467000000') - }, - 'title': 'Plug Power jumps 13% after it partners with Airbus to study and develop hydrogen-powered air travel', - 'description': 'Airbus is working towards a goal of bringing zero-emission aircraft to market by 2035, and it thinks hydrogen could help achieve that objective.', - 'url': { - 'url': 'https://markets.businessinsider.com/news/stocks/plug-power-stock-price-airbus-partnership-hydrogen-airport-study-phillips-2021-10' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/932bcb6a3ce07dc885c0ce7b881c3dfeb9fb643e6d5ce968decf14d1983dbbdd.jpg.pad' - } - }, - 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Business Insider', - 'score': 14.572489911501611, - 'relativeTimeDescription': '25 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278621435000000') - }, - 'title': '60K Film and TV Workers May Strike Within Days as Industry Still Recovers From Pandemic', - 'description': '\"Without an end date, we could keep talking forever,\" said President of the International Alliance of Theatrical Stage Employees Matthew Loeb.', - 'url': { - 'url': 'https://www.newsweek.com/60k-film-tv-workers-may-strike-within-days-industry-still-recovers-pandemic-1638658' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/1e4526119064ccbd65d5fc372fe878ff60b64c1ba9047f3cb664b2afdeddf73f.jpg.pad' - } - }, - 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Newsweek', - 'score': 14.615857594582533, - 'relativeTimeDescription': '26 minutes ago' - } - } - } - ] - }, - { - 'cardType': 5, - 'items': [] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Technology', - 'publishTime': { - internalValue: BigInt('13278621421000000') - }, - 'title': 'Apple AirPods 3 rumored to debut alongside new MacBook Pros at October 18 Unleashed event', - 'description': ' Yesterday Apple confirmed a new “Unleashed” launch event scheduled on October 18. The stars of the show will almost certainly be Apple’s next-generation ARM-based M-series chipset dubbed M1X and all-new 14” and 16” MacBook Pro laptops. Now Weibo leaker @PandaIsBald is also throwing in the long-rumored AirPods 3 to the mix.\n\n\n\n\nAirPods 3 leaked design (images: 52audio.com)\n\nApple’s regular non-Pro AirPods 2 have been out since March 2019 and are clearly due for an update. The third generation w', - 'url': { - 'url': 'https://www.gsmarena.com/apple_airpods_3_rumored_to_debut_alongside_new_macbook_pros_at_october_18_unleashed_event-news-51408.php' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/3352539771a4ac7482f4486de7900c458f052e11b605609fafcf206bb4634075.jpg.pad' - } - }, - 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'GSMArena', - 'score': 14.634541104175572, - 'relativeTimeDescription': '26 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Business', - 'publishTime': { - internalValue: BigInt('13278621420000000') - }, - 'title': ': Starbucks and Netflix partner for series tied to Netflix’s book club', - 'description': 'Starbucks and Netflix have partnered for a series that will discuss how the streaming service\'s book club choices are transformed into movies and shows\n \n', - 'url': { - 'url': 'https://www.marketwatch.com/story/starbucks-and-netflix-partner-for-series-tied-to-netflixs-book-club-11634130338?rss=1&siteid=rss' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/98683b85d91545ed868557351adee4d1b6fa45337082c356c36cedcc9a1c0085.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'MarketWatch', - 'score': 14.635871140907735, - 'relativeTimeDescription': '26 minutes ago' - } - } - } - ] - }, - { - 'cardType': 3, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Culture', - 'publishTime': { - internalValue: BigInt('13278621400000000') - }, - 'title': 'Cheat Maker Is Not Afraid of Call of Duty’s New Kernel-Level Anti-Cheat', - 'description': 'Activision announced the launch of a kernel-level anti-cheat system called RICOCHET to fight cheaters.', - 'url': { - 'url': 'https://www.vice.com/en/article/z3xjqa/cheat-maker-is-so-far-not-afraid-of-call-of-dutys-new-kernel-level-anti-cheat' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/f2dbdd22cf97479e7c2bccae2d3becc8346edd6b603ca2350ee02c35546de347.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'VICE', - 'score': 14.662245395018518, - 'relativeTimeDescription': '27 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Culture', - 'publishTime': { - internalValue: BigInt('13278620323000000') - }, - 'title': 'Airlines Are Already Defying the Texas Ban on Vaccine Mandates', - 'description': 'Gov. Greg Abbott made companies choose between state law and federal regulations, and Southwest and American Airlines made that choice pretty fast.', - 'url': { - 'url': 'https://www.vice.com/en/article/g5gzw7/airlines-are-defying-texas-ban-on-vaccine-mandates' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/39c1559de30f9abf91d6035ad655be5e8ba09f64ab4a73201010cb8921248068.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'VICE', - 'score': 31.45969721478307, - 'relativeTimeDescription': '45 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Culture', - 'publishTime': { - internalValue: BigInt('13278620305000000') - }, - 'title': 'The Best Dog DNA Tests That Actually Work, According to the Dog-Obsessed', - 'description': 'Help your pup assume their rightful throne as king of the dog park with the best dog DNA tests, according to convinced and happy pet owners.', - 'url': { - 'url': 'https://www.vice.com/en/article/dyvad7/best-dog-dna-tests' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/c6147aad0103a239ba49c79fa7696593bbf82558ba08a5362462fbbeea0a195c.jpg.pad' - } - }, - 'publisherId': '5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'VICE', - 'score': 62.9745088197745, - 'relativeTimeDescription': '45 minutes ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Sports', - 'publishTime': { - internalValue: BigInt('13278621373000000') - }, - 'title': 'Poor IPL run not a concern, Nicholas Pooran wants to just \'refocus and go again\'', - 'description': '\"We won two World Cups without getting singles\" - West Indies vice-captain says power-hitting will still be big on the team\'s agenda', - 'url': { - 'url': 'https://www.espncricinfo.com/story/t20-world-cup-west-indies-vice-captain-nicholas-pooran-wants-to-just-refocus-and-go-again-after-poor-ipl-2021-1282815?ex_cid=OTC-RSS' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/01dde228cf27287b3ef5d3c9b5293c7b8cdaa59e269aa856f080c535016b7e01.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'ESPN - Cricket', - 'score': 14.697300142885872, - 'relativeTimeDescription': '27 minutes ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278621361000000') - }, - 'title': 'Navy engineer smuggled secrets \'a few at a time\' and wanted to meet \'foreign spies\' for drinks', - 'description': 'Written communications between Jonathan Toebbe and an undercover FBI agent posing as a foreign spy show that he collected secret data over the years. Toebbe also planned to be extracted from the US.', - 'url': { - 'url': 'https://www.dailymail.co.uk/news/article-10087987/Navy-engineer-smuggled-secrets-time-wanted-meet-foreign-spies-drinks.html?ns_mchannel=rss&ns_campaign=1490&ito=1490' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/876f15ba1c5582a0bef82b3cd6b2d567d04d9394ef208dc3e3749a3d3da46df2.jpg.pad' - } - }, - 'publisherId': 'c5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Daily Mail', - 'score': 14.712686451514855, - 'relativeTimeDescription': '27 minutes ago' - } - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Technology', - 'publishTime': { - internalValue: BigInt('13278621350000000') - }, - 'title': 'How Quickbase is using low-code to streamline Daifuku’s supply chain', - 'description': 'At VentureBeat\'s Low-Code/No-Code Summit, Quickbase explained how its platform helps connect and automate systems, processes, and workloads.', - 'url': { - 'url': 'https://venturebeat.com/2021/10/13/how-quickbase-is-using-low-code-to-streamline-daifukus-supply-chain/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/e1bab8826e0dc937e0c1ee4f79a3e63d3fbc8caf11773259dbea1cb3202b9a1a.jpg.pad' - } - }, - 'publisherId': 'a5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'VentureBeat', - 'score': 14.726687401901625, - 'relativeTimeDescription': '27 minutes ago' - } - } - } - ] - }, - { - 'cardType': 4, - 'items': [ - { - article: undefined, - promotedArticle: undefined, - deal: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13256920996000000') - }, - 'title': 'Sony 55 Inch TV', - 'description': 'BRAVIA OLED 4K Ultra HD Smart TV with HDR and Alexa Compatibility', - 'url': { - 'url': 'https://www.amazon.com/dp/B084KQFNBX?tag=bravesoftware-20&linkCode=osi&th=1&psc=1&language=en_US¤cy=USD' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/6b0de702b8c1596cb713c89f3b79b568959cfebf4af6611e63c54d39a43e0233.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Offers', - 'score': 270.2865946654164, - 'relativeTimeDescription': '251 days ago' - }, - 'offersCategory': 'Electronics' - } - }, - { - article: undefined, - promotedArticle: undefined, - deal: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13256920996000000') - }, - 'title': 'Samsung Galaxy Tab S7', - 'description': 'Go for hours on a single charge, and back to 100% with the fast-charging USB-C port.', - 'url': { - 'url': 'https://www.amazon.com/dp/B08FBN5STQ?tag=bravesoftware-20&linkCode=osi&th=1&psc=1&language=en_US¤cy=USD' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/b8b0e9f54885248f635c620d638c716164972f0b7f72c5ec79357517e2d2a171.jpg.pad' - } - }, - 'publisherId': 'fc5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Offers', - 'score': 540.5731893340486, - 'relativeTimeDescription': '251 days ago' - }, - 'offersCategory': 'Electronics' - } - }, - { - article: undefined, - promotedArticle: undefined, - deal: { - 'data': { - 'categoryName': 'Brave', - 'publishTime': { - internalValue: BigInt('13256920996000000') - }, - 'title': 'Samsung Curved LED-Lit Monitor', - 'description': 'A stylish design featuring a Black body metallic finish and sleek curves', - 'url': { - 'url': 'https://www.amazon.com/dp/B079K3MXWF?tag=bravesoftware-20&linkCode=osi&th=1&psc=1&language=en_US¤cy=USD' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/e2bc4f0ca84cc9bc7183a92072bf41cbcdd4bd4e31da0a5d6e084a1da2c2c7fd.jpg.pad' - } - }, - 'publisherId': 'b4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Brave Offers', - 'score': 1081.146378673966, - 'relativeTimeDescription': '251 days ago' - }, - 'offersCategory': 'Electronics' - } - } - ] - }, - { - 'cardType': 0, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Cars', - 'publishTime': { - internalValue: BigInt('13278542934000000') - }, - 'title': 'Rivian’s first retail hub to open in Venice, CA, as ‘a space to gather’', - 'description': '\nFresh off the heels of delivering its flagship R1T electric pickup to first customers, Rivian has shared details of its first hub, centered in Venice, California. Rather than existing as solely a retail space, Rivian hopes this first of several hub locations will offer a space for public gatherings and encourages its community to visit and connect. If you happen to order a $70,000 EV while you’re there… well that’s a welcomed option as well.\n more…\nThe post Rivian’s first retail hub to open in ', - 'url': { - 'url': 'https://electrek.co/2021/10/12/rivians-first-retail-hub-to-open-in-venice-ca-as-a-space-to-gather/' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/c85762ffe3bd48886ed1ce58f868958989ad89b0723f5844a7ffe0fef57fa1c5.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Electrek', - 'score': 23121.416404237974, - 'relativeTimeDescription': '22 hours ago' - } - } - } - ] - }, - { - 'cardType': 1, - 'items': [ - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Business', - 'publishTime': { - internalValue: BigInt('13278518400000000') - }, - 'title': 'Tech’s Exponential Growth – and How to Solve the Problems It’s Created', - 'description': 'Understanding and improving the impact that Big Tech has on society\n\n \n', - 'url': { - 'url': 'https://hbr.org/podcast/2021/10/techs-exponential-growth-and-how-to-solve-the-problems-its-created?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+harvardbusiness+%28HBR.org%29' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/703dace4d9ab2590c184826c2a7b39ed2249cace04101f891c011c31021a3b9d.jpg.pad' - } - }, - 'publisherId': '4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'Harvard Business Review', - 'score': 5917.313882859709, - 'relativeTimeDescription': '1 day ago' - } - } - }, - { - promotedArticle: undefined, - deal: undefined, - article: { - 'data': { - 'categoryName': 'Top News', - 'publishTime': { - internalValue: BigInt('13278596700000000') - }, - 'title': 'Global Climate Pledges Off Track to Meet Paris Targets, IEA Says', - 'description': 'Whether lawmakers continue existing policies or make good on recent promises, rising temperatures will exceed the limit global leaders committed to in the Paris Agreement, the International Energy Agency said.', - 'url': { - 'url': 'https://www.wsj.com/articles/governments-climate-pledges-not-enough-to-meet-paris-agreement-targets-iea-says-11634097601' - }, - 'urlHash': '', - 'image': { - imageUrl: undefined, - paddedImageUrl: { - 'url': 'https://pcdn.brave.com/brave-today/cache/1261719cac8a7c5d7d5ab0d2cc3b5b6a43cf177b3fd0a103f366f59b397efd9d.jpg.pad' - } - }, - 'publisherId': 'eb4eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5', - 'publisherName': 'WSJ', - 'score': 325.5895923286794, - 'relativeTimeDescription': '7 hours ago' - } - } - } - ] - } - ] - } - ] - } + publishers: hasDataError ? undefined : publishers, + feed: hasDataError ? undefined : feed } } diff --git a/components/brave_new_tab_ui/stories/regular.tsx b/components/brave_new_tab_ui/stories/regular.tsx index 1b132cd9132f..d0921338cb6f 100644 --- a/components/brave_new_tab_ui/stories/regular.tsx +++ b/components/brave_new_tab_ui/stories/regular.tsx @@ -2,6 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ +// Import BraveNewsControllerMock first. +import './default/data/mockBraveNewsController' + import * as React from 'react' import { Dispatch } from 'redux' import { Provider as ReduxProvider } from 'react-redux' diff --git a/components/brave_new_tab_ui/stories/today.tsx b/components/brave_new_tab_ui/stories/today.tsx index 6d956f5b3891..b16be1ec0a38 100644 --- a/components/brave_new_tab_ui/stories/today.tsx +++ b/components/brave_new_tab_ui/stories/today.tsx @@ -52,6 +52,9 @@ export const Publisher = () => ( categoryName: 'Top News', channels: ['Top News', 'Top Sources'], feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, siteUrl: { url: 'https://www.example.com' }, locales: ['en_US'], type: BraveNews.PublisherType.COMBINED_SOURCE, @@ -67,6 +70,9 @@ export const Publisher = () => ( categoryName: 'Top News', channels: ['Top News', 'Top Sources'], feedSource: { url: 'http://www.example.com/feed' }, + backgroundColor: undefined, + coverUrl: undefined, + faviconUrl: undefined, siteUrl: { url: 'https://www.example.com' }, locales: ['en_US'], type: BraveNews.PublisherType.COMBINED_SOURCE, diff --git a/components/brave_today/browser/brave_news_controller.cc b/components/brave_today/browser/brave_news_controller.cc index 12ddf894a061..566cfc97340c 100644 --- a/components/brave_today/browser/brave_news_controller.cc +++ b/components/brave_today/browser/brave_news_controller.cc @@ -111,6 +111,10 @@ BraveNewsController::BraveNewsController( prefs::kBraveTodayOptedIn, base::BindRepeating(&BraveNewsController::ConditionallyStartOrStopTimer, base::Unretained(this))); + pref_change_registrar_.Add( + prefs::kBraveNewsChannels, + base::BindRepeating(&BraveNewsController::HandleSubscriptionsChanged, + base::Unretained(this))); if (base::FeatureList::IsEnabled( brave_today::features::kBraveNewsV2Feature)) { @@ -505,10 +509,7 @@ void BraveNewsController::Prefetch() { void BraveNewsController::ConditionallyStartOrStopTimer() { // Refresh data on an interval only if Brave News is enabled - bool should_show = prefs_->GetBoolean(prefs::kNewTabPageShowToday); - bool opted_in = prefs_->GetBoolean(prefs::kBraveTodayOptedIn); - bool is_enabled = (should_show && opted_in); - if (is_enabled) { + if (GetIsEnabled()) { VLOG(1) << "STARTING TIMERS"; if (!timer_feed_update_.IsRunning()) { timer_feed_update_.Start(FROM_HERE, base::Hours(3), this, @@ -534,4 +535,20 @@ void BraveNewsController::ConditionallyStartOrStopTimer() { } } +bool BraveNewsController::GetIsEnabled() { + bool should_show = prefs_->GetBoolean(prefs::kNewTabPageShowToday); + bool opted_in = prefs_->GetBoolean(prefs::kBraveTodayOptedIn); + bool is_enabled = (should_show && opted_in); + return is_enabled; +} + +void BraveNewsController::HandleSubscriptionsChanged() { + if (GetIsEnabled()) { + VLOG(1) << "HandleSubscriptionsChanged: Ensuring feed is updated"; + feed_controller_.EnsureFeedIsUpdating(); + } else { + VLOG(1) << "HandleSubscriptionsChanged: News not enabled, doing nothing."; + } +} + } // namespace brave_news diff --git a/components/brave_today/browser/brave_news_controller.h b/components/brave_today/browser/brave_news_controller.h index 79a16c889a9e..8851ff36a50c 100644 --- a/components/brave_today/browser/brave_news_controller.h +++ b/components/brave_today/browser/brave_news_controller.h @@ -113,6 +113,8 @@ class BraveNewsController : public KeyedService, void ConditionallyStartOrStopTimer(); void CheckForFeedsUpdate(); void CheckForPublishersUpdate(); + bool GetIsEnabled(); + void HandleSubscriptionsChanged(); void Prefetch(); raw_ptr prefs_ = nullptr; diff --git a/components/brave_today/browser/channels_controller.cc b/components/brave_today/browser/channels_controller.cc index 860a36b8437d..b9b2ad6bfcb1 100644 --- a/components/brave_today/browser/channels_controller.cc +++ b/components/brave_today/browser/channels_controller.cc @@ -93,6 +93,7 @@ mojom::ChannelPtr ChannelsController::SetChannelSubscribed( auto result = mojom::Channel::New(); result->channel_name = channel_id; result->subscribed = subscribed; + return result; } diff --git a/components/brave_today/browser/channels_controller.h b/components/brave_today/browser/channels_controller.h index fc0b7c4e5b18..d465e3c37d82 100644 --- a/components/brave_today/browser/channels_controller.h +++ b/components/brave_today/browser/channels_controller.h @@ -11,6 +11,7 @@ #include "base/callback_forward.h" #include "base/containers/flat_map.h" +#include "base/memory/raw_ptr.h" #include "brave/components/brave_today/browser/publishers_controller.h" #include "brave/components/brave_today/common/brave_news.mojom-forward.h" #include "components/prefs/pref_service.h" diff --git a/components/brave_today/browser/feed_building_unittest.cc b/components/brave_today/browser/feed_building_unittest.cc index 5974f26a0960..1fe9c5921021 100644 --- a/components/brave_today/browser/feed_building_unittest.cc +++ b/components/brave_today/browser/feed_building_unittest.cc @@ -23,6 +23,7 @@ #include "chrome/test/base/testing_profile.h" #include "content/public/test/browser_task_environment.h" #include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" #include "url/mojom/url.mojom.h" namespace brave_news { @@ -101,18 +102,21 @@ void PopulatePublishers(Publishers* publisher_list) { "111", mojom::PublisherType::COMBINED_SOURCE, "First Publisher", "Top News", std::vector{"Top News"}, true, std::vector{"en_US"}, GURL("https://www.example.com"), + absl::nullopt, absl::nullopt, absl::nullopt, GURL("https://first-publisher.com/feed.xml"), mojom::UserEnabled::NOT_MODIFIED); auto publisher2 = mojom::Publisher::New( "222", mojom::PublisherType::COMBINED_SOURCE, "Second Publisher", "Top News", std::vector{"Top News"}, true, std::vector{"en_US"}, GURL("https://www.example.com"), + absl::nullopt, absl::nullopt, absl::nullopt, GURL("https://second-publisher.com/feed.xml"), mojom::UserEnabled::NOT_MODIFIED); auto publisher3 = mojom::Publisher::New( "333", mojom::PublisherType::COMBINED_SOURCE, "Third Publisher", "Top News", std::vector{"Top News"}, true, std::vector{"en_US"}, GURL("https://www.example.com"), + absl::nullopt, absl::nullopt, absl::nullopt, GURL("https://third-publisher.com/feed.xml"), mojom::UserEnabled::NOT_MODIFIED); publisher_list->insert_or_assign(publisher1->publisher_id, diff --git a/components/brave_today/browser/publishers_controller.cc b/components/brave_today/browser/publishers_controller.cc index 91546cd0e330..04b38ecc1dca 100644 --- a/components/brave_today/browser/publishers_controller.cc +++ b/components/brave_today/browser/publishers_controller.cc @@ -142,8 +142,10 @@ void PublishersController::EnsurePublishersIsUpdating() { return; } is_update_in_progress_ = true; + std::string region_part = brave_today::GetRegionUrlPart(); GURL sources_url("https://" + brave_today::GetHostname() + "/sources." + - brave_today::GetRegionUrlPart() + "json"); + region_part + "json"); + auto onRequest = base::BindOnce( [](PublishersController* controller, api_request_helper::APIRequestResult api_request_result) { diff --git a/components/brave_today/browser/publishers_parsing.cc b/components/brave_today/browser/publishers_parsing.cc index 99d0bcd491d1..d5cb18dcf266 100644 --- a/components/brave_today/browser/publishers_parsing.cc +++ b/components/brave_today/browser/publishers_parsing.cc @@ -14,6 +14,7 @@ #include "base/values.h" #include "brave/components/brave_today/common/brave_news.mojom.h" #include "brave/components/brave_today/common/pref_names.h" +#include "third_party/abseil-cpp/absl/types/optional.h" #include "url/gurl.h" namespace brave_news { @@ -32,26 +33,28 @@ bool ParseCombinedPublisherList(const std::string& json, return false; } for (const base::Value& publisher_raw : records_v->GetList()) { + const auto& publisher_dict = publisher_raw.GetDict(); + auto publisher = brave_news::mojom::Publisher::New(); - publisher->publisher_id = *publisher_raw.FindStringKey("publisher_id"); + publisher->publisher_id = *publisher_dict.FindString("publisher_id"); publisher->type = mojom::PublisherType::COMBINED_SOURCE; - publisher->publisher_name = *publisher_raw.FindStringKey("publisher_name"); + publisher->publisher_name = *publisher_dict.FindString("publisher_name"); - publisher->category_name = *publisher_raw.FindStringKey("category"); - auto* channels_raw = publisher_raw.GetDict().FindList("channels"); + publisher->category_name = *publisher_dict.FindString("category"); + auto* channels_raw = publisher_dict.FindList("channels"); if (channels_raw) { for (const auto& channel : *channels_raw) { publisher->channels.push_back(channel.GetString()); } } - publisher->is_enabled = publisher_raw.FindBoolKey("enabled").value_or(true); - GURL feed_source(*publisher_raw.FindStringKey("feed_url")); + publisher->is_enabled = publisher_dict.FindBool("enabled").value_or(true); + GURL feed_source(*publisher_dict.FindString("feed_url")); if (feed_source.is_valid()) { publisher->feed_source = feed_source; } - auto* locales_raw = publisher_raw.GetDict().FindList("locales"); + auto* locales_raw = publisher_dict.FindList("locales"); if (locales_raw) { for (const auto& locale : *locales_raw) { if (!locale.is_string()) @@ -60,7 +63,7 @@ bool ParseCombinedPublisherList(const std::string& json, } } - std::string site_url_raw = *publisher_raw.FindStringKey("site_url"); + std::string site_url_raw = *publisher_dict.FindString("site_url"); if (!base::StartsWith(site_url_raw, "https://")) { site_url_raw = "https://" + site_url_raw; } @@ -71,6 +74,26 @@ bool ParseCombinedPublisherList(const std::string& json, continue; } publisher->site_url = site_url; + + auto* favicon_url_raw = publisher_dict.FindString("favicon_url"); + if (favicon_url_raw) { + if (GURL favicon_url(*favicon_url_raw); favicon_url.is_valid()) { + publisher->favicon_url = favicon_url; + } + } + + auto* cover_url_raw = publisher_dict.FindString("cover_url"); + if (cover_url_raw) { + if (GURL cover_url(*cover_url_raw); cover_url.is_valid()) { + publisher->cover_url = cover_url; + } + } + + auto* background_color = publisher_dict.FindString("background_color"); + if (background_color) { + publisher->background_color = *background_color; + } + // TODO(petemill): Validate publishers->insert_or_assign(publisher->publisher_id, std::move(publisher)); } diff --git a/components/brave_today/browser/urls.cc b/components/brave_today/browser/urls.cc index 17485c59e3cf..80366035072b 100644 --- a/components/brave_today/browser/urls.cc +++ b/components/brave_today/browser/urls.cc @@ -17,6 +17,12 @@ #include "brave/components/l10n/common/locale_util.h" namespace brave_today { +namespace { +// TODO(petemill): Have a remotely-updatable list of supported language +// variations. +const base::flat_set kSupportedLocales = {"en_US", "ja_JP", + "en_ES", "en_MX"}; +} // namespace std::string GetHostname() { std::string from_switch = diff --git a/components/brave_today/common/brave_news.mojom b/components/brave_today/common/brave_news.mojom index 9036f5906f99..1b7284e6eabb 100644 --- a/components/brave_today/common/brave_news.mojom +++ b/components/brave_today/common/brave_news.mojom @@ -107,6 +107,15 @@ struct Publisher { // feed. url.mojom.Url feed_source; + // The favicon url for this publisher. + url.mojom.Url? favicon_url; + + // The cover url for this publisher. + url.mojom.Url? cover_url; + + // The background color for this publisher (in hex). + string? background_color; + // The url of the site. Used to determine whether the user is subscribed to // a site when viewing a webcontents. url.mojom.Url site_url; diff --git a/components/resources/brave_components_strings.grd b/components/resources/brave_components_strings.grd index 738b63da04df..aca244521db9 100644 --- a/components/resources/brave_components_strings.grd +++ b/components/resources/brave_components_strings.grd @@ -351,6 +351,64 @@ Ad + + Back to $1Dashboard$2 + + + Turn on Brave News, and never miss a story + + + Customized news feeds, from leading sources, delivered right to your browser. All 100% private. + + + Turn on Brave News + + + Search for news, site, topic or RSS feed + + + Channels + + + Show more + + + Sources + + + Following + + + {FEED_COUNT, plural, + =1 {# source} + other {# sources} + } + + + Following + + + Follow + + + Get feeds from $1https://example.com + + + No feeds found at $1https://example.com + + + There's nothing here :'( + + + Results + + + Direct Results + + + Keep typing to search sources + + Brave Stats Clock Top Sites diff --git a/components/test/brave_new_tab_ui/components/settings_test.tsx b/components/test/brave_new_tab_ui/components/settings_test.tsx index 8860bf70b44b..e8ebecc59185 100644 --- a/components/test/brave_new_tab_ui/components/settings_test.tsx +++ b/components/test/brave_new_tab_ui/components/settings_test.tsx @@ -1,4 +1,8 @@ import * as React from 'react' + +// Note: this needs to be imported before any Settings components, to ensure the mock is set up. +import '../../../../components/brave_new_tab_ui/stories/default/data/mockBraveNewsController' + import { shallow } from 'enzyme' import { SettingsMenu } from '../../../../components/brave_new_tab_ui/components/default' import Settings, { Props } from '../../../../components/brave_new_tab_ui/containers/newTab/settings' diff --git a/components/web-components/button/index.tsx b/components/web-components/button/index.tsx index 26d3dca2d21e..d2925c3a33f4 100644 --- a/components/web-components/button/index.tsx +++ b/components/web-components/button/index.tsx @@ -6,7 +6,7 @@ import style from './button.module.scss' type Scale = 'tiny' | 'small' | 'regular' | 'large' | 'jumbo' -interface Props { +export interface ButtonProps { scale?: Scale isPrimary?: boolean isTertiary?: boolean @@ -39,7 +39,7 @@ export function ButtonIconContainer (props: React.PropsWithChildren<{}>) { ) } -export default function Button (props: Props) { +export default function Button (props: ButtonProps) { const { scale = 'regular' } = props return (