diff --git a/components/brave_new_tab_ui/components/default/braveNews/FeedV2.tsx b/components/brave_new_tab_ui/components/default/braveNews/FeedV2.tsx index ae2676465366..8b8eed6041d7 100644 --- a/components/brave_new_tab_ui/components/default/braveNews/FeedV2.tsx +++ b/components/brave_new_tab_ui/components/default/braveNews/FeedV2.tsx @@ -5,7 +5,8 @@ import Flex from '$web-common/Flex' import { getLocale } from '$web-common/locale' import Button from '@brave/leo/react/button' -import { spacing } from '@brave/leo/tokens/css' +import Icon from '@brave/leo/react/icon' +import { radius, spacing } from '@brave/leo/tokens/css' import * as React from 'react' import styled from 'styled-components' import Feed from '../../../../brave_news/browser/resources/Feed' @@ -38,8 +39,9 @@ const ButtonsContainer = styled.div` visibility: hidden; position: fixed; - bottom: ${spacing.xl}; - right: ${spacing.xl}; + bottom: ${spacing['5Xl']}; + right: ${spacing['5Xl']}; + border-radius: ${radius.m}; opacity: calc((var(--ntp-scroll-percent) - 0.5) / 0.5); @@ -49,6 +51,15 @@ const ButtonsContainer = styled.div` display: flex; gap: ${spacing.m}; + padding: ${spacing.m}; + + background: var(--bn-glass-container); +` + +const NewsButton = styled(Button)` + --leo-button-color: var(--bn-glass-50); + --leo-button-radius: ${radius.s}; + --leo-button-padding: ${spacing.m}; ` const LoadNewContentButton = styled(Button)` @@ -102,12 +113,12 @@ export default function FeedV2() { - - + }}> } diff --git a/components/brave_news/browser/feed_v2_builder.cc b/components/brave_news/browser/feed_v2_builder.cc index 0b29bcb5c917..a2096e6ce173 100644 --- a/components/brave_news/browser/feed_v2_builder.cc +++ b/components/brave_news/browser/feed_v2_builder.cc @@ -445,7 +445,7 @@ std::vector GenerateTopicBlock( result->type = mojom::ClusterType::TOPIC; auto& [topic, articles] = topics.front(); - result->id = topic.title; + result->id = topic.claude_title_short; uint64_t max_articles = features::kBraveNewsMaxBlockCards.Get(); for (const auto& article : articles) { diff --git a/components/brave_news/browser/resources/FeedNavigation.tsx b/components/brave_news/browser/resources/FeedNavigation.tsx index ca5d675f8e47..fba9f5d71eab 100644 --- a/components/brave_news/browser/resources/FeedNavigation.tsx +++ b/components/brave_news/browser/resources/FeedNavigation.tsx @@ -27,6 +27,9 @@ const Container = styled(Card)` max-height: calc(100vh - ${spacing.xl} * 2); overflow-y: auto; + + scrollbar-width: thin; + scrollbar-color: var(--bn-glass-10) var(--bn-glass-10); ` const Heading = styled.h3` diff --git a/components/brave_news/browser/resources/feed/Ad.tsx b/components/brave_news/browser/resources/feed/Ad.tsx index 5839b642dbab..0a3b55556d35 100644 --- a/components/brave_news/browser/resources/feed/Ad.tsx +++ b/components/brave_news/browser/resources/feed/Ad.tsx @@ -39,6 +39,7 @@ const BatAdLabel = styled.a` ` const CtaButton = styled(Button)` + --leo-button-color: var(--bn-glass-container); align-self: flex-start; ` @@ -121,6 +122,6 @@ export default function Advert(props: Props) { {advert.title} - {advert.ctaText} + {advert.ctaText} } diff --git a/components/brave_news/browser/resources/feed/Article.tsx b/components/brave_news/browser/resources/feed/Article.tsx index 3af97e2c91ab..8b4fbc5773db 100644 --- a/components/brave_news/browser/resources/feed/Article.tsx +++ b/components/brave_news/browser/resources/feed/Article.tsx @@ -19,16 +19,20 @@ interface Props { const Container = styled(Card)` display: flex; flex-direction: column; + gap: ${spacing.s}; padding-top: ${spacing.l}; ` export default function Article({ info, hideChannel }: Props) { - const { url: imageUrl, setElementRef } = useLazyUnpaddedImageUrl(info.data.image.paddedImageUrl?.url ?? info.data.image.imageUrl?.url, { useCache: true }) + const { url: imageUrl, setElementRef } = useLazyUnpaddedImageUrl(info.data.image.paddedImageUrl?.url ?? info.data.image.imageUrl?.url, { + useCache: true, + rootMargin: '500px 0px' + }) const url = info.data.url.url; return - + <BraveNewsLink href={url}>{info.data.title}</BraveNewsLink> diff --git a/components/brave_news/browser/resources/feed/ArticleMetaRow.tsx b/components/brave_news/browser/resources/feed/ArticleMetaRow.tsx index 038cb92fec94..8817a4192af7 100644 --- a/components/brave_news/browser/resources/feed/ArticleMetaRow.tsx +++ b/components/brave_news/browser/resources/feed/ArticleMetaRow.tsx @@ -37,7 +37,7 @@ export const MetaInfoContainer = styled.h4` const publisherDescription = (article: FeedItemMetadata) => { if (article.publisherName) return article.publisherName const url = new URL(article.url.url) - return url.origin + return url.hostname } export function MetaInfo(props: { article: FeedItemMetadata, hideChannel?: boolean }) { diff --git a/components/brave_news/browser/resources/feed/Cluster.tsx b/components/brave_news/browser/resources/feed/Cluster.tsx index 9532feef0c28..6752b72e7a68 100644 --- a/components/brave_news/browser/resources/feed/Cluster.tsx +++ b/components/brave_news/browser/resources/feed/Cluster.tsx @@ -4,12 +4,11 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import * as React from 'react'; import { Cluster as Info, ClusterType } from 'gen/brave/components/brave_news/common/brave_news.mojom.m'; -import Card from './Card'; +import Card, { Title } from './Card'; import Article from './Article'; import styled from 'styled-components'; -import { spacing } from '@brave/leo/tokens/css'; +import { icon, radius, spacing } from '@brave/leo/tokens/css'; import { channelIcons } from '../shared/Icons'; -import { MetaInfoContainer } from './ArticleMetaRow'; import { getTranslatedChannelName } from '../shared/channel'; interface Props { @@ -19,8 +18,21 @@ interface Props { const Container = styled(Card)` display: flex; flex-direction: column; - gap: ${spacing['2Xl']}; - padding-top: ${spacing['2Xl']}; + gap: ${spacing.l}; + + & > ${Title} { + --leo-icon-color: currentColor; + --leo-icon-size: ${icon.s}; + + gap: ${spacing.m}; + align-items: center; + + margin: ${spacing.m} 0; + } + + & > ${Card} { + border-radius: ${radius.m}; + } ` export default function Cluster({ info }: Props) { @@ -28,9 +40,9 @@ export default function Cluster({ info }: Props) { ? getTranslatedChannelName(info.id) : info.id return - + {channelIcons[info.id] ?? channelIcons.default} {groupName} - </MetaInfoContainer> + {info.articles.map((a, i) => { const info: any = a.article || a.hero return
diff --git a/components/brave_news/browser/resources/feed/Hero.tsx b/components/brave_news/browser/resources/feed/Hero.tsx index 0d5f28f4442c..7a208617840e 100644 --- a/components/brave_news/browser/resources/feed/Hero.tsx +++ b/components/brave_news/browser/resources/feed/Hero.tsx @@ -7,22 +7,34 @@ import * as React from 'react'; import { useLazyUnpaddedImageUrl } from '../shared/useUnpaddedImageUrl'; import ArticleMetaRow from './ArticleMetaRow'; import Card, { BraveNewsLink, LargeImage, Title, braveNewsCardClickHandler } from './Card'; +import styled from 'styled-components'; +import { spacing } from '@brave/leo/tokens/css'; interface Props { info: Info } +const Container = styled(Card)` + display: flex; + flex-direction: column; + gap: ${spacing.s}; + + & > ${LargeImage} { + margin-bottom: ${spacing.l}; + } +` + export default function HeroArticle({ info }: Props) { const { url, setElementRef } = useLazyUnpaddedImageUrl(info.data.image.paddedImageUrl?.url ?? info.data.image.imageUrl?.url, { useCache: true, rootElement: document.body, - rootMargin: '0px 0px 200px 0px' + rootMargin: '500px 0px' }) - return + return <BraveNewsLink href={info.data.url.url}>{info.data.title}</BraveNewsLink> - + } diff --git a/components/brave_news/browser/resources/feed/NoArticles.tsx b/components/brave_news/browser/resources/feed/NoArticles.tsx index 0878a41cccd0..499e5e222ed1 100644 --- a/components/brave_news/browser/resources/feed/NoArticles.tsx +++ b/components/brave_news/browser/resources/feed/NoArticles.tsx @@ -3,29 +3,30 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. import { getLocale } from '$web-common/locale'; -import Button from '@brave/leo/react/button'; import { spacing } from '@brave/leo/tokens/css'; import * as React from 'react'; import styled from 'styled-components'; -import Flex from '../../../../common/Flex'; -import { useBraveNews } from '../shared/Context'; +import Flex from '$web-common/Flex'; import { Title } from './Card'; const Container = styled(Flex)` - position: sticky; - top: ${spacing.xl}; + text-align: center; - padding: ${spacing.xl}; + padding: ${spacing['3Xl']}; gap: ${spacing.m}; color: var(--bn-glass-70); + & > svg { + margin-bottom: ${spacing['2Xl']}; + fill: none; + } + & > leo-button { flex: 0; } ` export default function NoArticles() { - const { refreshFeedV2 } = useBraveNews() return @@ -60,6 +61,5 @@ export default function NoArticles() {
{getLocale('braveNewsNoArticlesMessage')}
-
} diff --git a/components/brave_news/browser/resources/shared/Context.tsx b/components/brave_news/browser/resources/shared/Context.tsx index 11d24d53a0f0..239a67f80b64 100644 --- a/components/brave_news/browser/resources/shared/Context.tsx +++ b/components/brave_news/browser/resources/shared/Context.tsx @@ -73,6 +73,7 @@ export const configurationCache = new ConfigurationCachingWrapper() export function BraveNewsContextProvider(props: { children: React.ReactNode }) { const [locale, setLocale] = useState('') + const [configuration, setConfiguration] = useState(configurationCache.value) // Note: It's okay to fetch the FeedV2 even when the feature isn't enabled // because the controller will just return an empty feed. @@ -81,13 +82,12 @@ export function BraveNewsContextProvider(props: { children: React.ReactNode }) { feedView, setFeedView, refresh: refreshFeedV2 - } = useFeedV2() + } = useFeedV2(configuration.isOptedIn && configuration.showOnNTP) const [customizePage, setCustomizePage] = useState(null) const [channels, setChannels] = useState({}) const [publishers, setPublishers] = useState({}) const [suggestedPublisherIds, setSuggestedPublisherIds] = useState([]) - const [configuration, setConfiguration] = useState(configurationCache.value) // Get the default locale on load. useEffect(() => { diff --git a/components/brave_news/browser/resources/shared/useFeedV2.ts b/components/brave_news/browser/resources/shared/useFeedV2.ts index 86b1344c2791..4e9fdaa41e0c 100644 --- a/components/brave_news/browser/resources/shared/useFeedV2.ts +++ b/components/brave_news/browser/resources/shared/useFeedV2.ts @@ -21,7 +21,11 @@ const feedTypeToFeedView = (type: FeedV2Type | undefined): FeedView => { const FEED_KEY = 'feedV2' const localCache: { [feedView: string]: FeedV2 } = {} const saveFeed = (feed?: FeedV2) => { - if (!feed) return + if (!feed || !feed.items.length) { + sessionStorage.removeItem(FEED_KEY) + localStorage.removeItem(FEED_KEY) + return + } localCache[feedTypeToFeedView(feed.type)] = feed @@ -29,8 +33,18 @@ const saveFeed = (feed?: FeedV2) => { const data = JSON.stringify(feed, (_, value) => typeof value === "bigint" ? value.toString() : value); - sessionStorage.setItem(FEED_KEY, data) - localStorage.setItem(FEED_KEY, data) + + try { + sessionStorage.setItem(FEED_KEY, data) + } catch (err) { + console.log(err) + } + + try { + localStorage.setItem(FEED_KEY, data) + } catch (err) { + console.log(err) + } } const maybeLoadFeed = (view?: FeedView) => { @@ -104,18 +118,32 @@ addFeedListener(latestHash => { } }) -export const useFeedV2 = () => { +export const useFeedV2 = (enabled: boolean) => { const [feedV2, setFeedV2] = useState(maybeLoadFeed()) const [feedView, setFeedView] = useState(feedTypeToFeedView(feedV2?.type)) const [hash, setHash] = useState() - // Add a listener for the latest hash. + // Add a listener for the latest hash if Brave News is enabled. Note: We need + // to re-add the listener when the enabled state changes because the backing + // FeedV2Builder is created/destroyed. useEffect(() => { + if (!enabled) return + + let cancelled = false // Note: A new feed listener will be notified with the latest hash. - addFeedListener(setHash) - }, []) + addFeedListener(newHash => { + if (cancelled) return + setHash(newHash) + }) + + return () => { cancelled = true } + }, [enabled]) useEffect(() => { + if (!enabled) return + + setFeedV2(undefined) + const cachedFeed = maybeLoadFeed(feedView) if (cachedFeed) { setFeedV2(cachedFeed) @@ -128,7 +156,7 @@ export const useFeedV2 = () => { setFeedV2(feed) }) return () => { cancelled = true } - }, [feedView]) + }, [feedView, enabled]) const refresh = useCallback(() => { // Set the feed to undefined - this will trigger the loading indicator. @@ -140,7 +168,6 @@ export const useFeedV2 = () => { // Updates are available if we've been told the latest hash, we have a feed // and the hashes don't match. const updatesAvailable = !!(hash && feedV2 && hash !== feedV2.sourceHash) - console.log("Latest hash: ", hash, "Current hash:", feedV2?.sourceHash) return { feedV2, feedView,