diff --git a/components/brave_extension/extension/brave_extension/background/today/feedToData.ts b/components/brave_extension/extension/brave_extension/background/today/feedToData.ts index 74bbc9e5a0e0..bdd1e17e2349 100644 --- a/components/brave_extension/extension/brave_extension/background/today/feedToData.ts +++ b/components/brave_extension/extension/brave_extension/background/today/feedToData.ts @@ -37,7 +37,7 @@ export default async function getBraveTodayData ( enabledPublishers: BraveToday.Publishers ): Promise { // No sponsor content yet, wait until we have content spec - // const sponsors: (BraveToday.Article)[] = [] + const promotedArticles: (BraveToday.PromotedArticle)[] = [] const deals: (BraveToday.Deal)[] = [] let articles: (BraveToday.Article)[] = [] @@ -55,9 +55,9 @@ export default async function getBraveTodayData ( for (let feedItem of feedContent) { feedItem = convertFeedItem(feedItem) switch (feedItem.content_type) { - // case 'offer': - // sponsors.push(feedItem as BraveToday.Article) - // break + case 'brave_partner': + promotedArticles.push(feedItem) + break case 'product': deals.push(feedItem) break @@ -103,9 +103,6 @@ export default async function getBraveTodayData ( const dealsCategoriesByPriority = Array.from(dealsCategoryCounts.keys()) .sort((a, b) => dealsCategoryCounts[a] - dealsCategoryCounts[b]) - // TODO(petemill): Sponsor - // const firstSponsors = sponsors.splice(0, 1) // Featured sponsor is the first sponsor - const firstHeadlines = take(articles, 1, isArticleTopNews) const firstDeals = deals.splice(0, 3) @@ -119,7 +116,7 @@ export default async function getBraveTodayData ( while (canGenerateAnotherPage && curPage++ < maxPages) { const category = categoriesByPriority.shift() const dealsCategory = dealsCategoriesByPriority.shift() - const nextPage = generateNextPage(articles, deals, category, dealsCategory) + const nextPage = generateNextPage(articles, deals, promotedArticles, category, dealsCategory) if (!nextPage) { canGenerateAnotherPage = false continue @@ -128,7 +125,6 @@ export default async function getBraveTodayData ( } return { - // featuredSponsor: firstSponsors.length ? firstSponsors[0] : undefined, hash, featuredArticle: firstHeadlines.length ? firstHeadlines[0] : undefined, featuredDeals: firstDeals, @@ -153,6 +149,7 @@ function isArticleWithin48Hours (article: BraveToday.Article) { function generateNextPage ( articles: BraveToday.Article[], allDeals: BraveToday.Deal[], + promotedArticles: BraveToday.PromotedArticle[], featuredCategory?: string, dealsCategory?: string): BraveToday.Page | null { @@ -177,6 +174,8 @@ function generateNextPage ( deals = deals.concat(allDeals.splice(0, 3 - deals.length)) } + const pagePromotedArticles = take(promotedArticles, 1) + const publisherInfo = generateArticleSourceGroup(articles) const randomArticles = take(articles, 4, isArticleWithin48Hours, true) @@ -185,6 +184,7 @@ function generateNextPage ( articles: headlines, randomArticles, deals, + promotedArticle: pagePromotedArticles.length ? pagePromotedArticles[0] : undefined, itemsByCategory: (featuredCategory && articlesWithCategory.length) ? { categoryName: featuredCategory, items: articlesWithCategory } : undefined, itemsByPublisher: publisherInfo ? { name: publisherInfo[0], items: publisherInfo[1] } : undefined } diff --git a/components/brave_new_tab_ui/components/default/braveToday/cardSizes.ts b/components/brave_new_tab_ui/components/default/braveToday/cardSizes.ts index 62b2d0077d61..778cfa6c8423 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/cardSizes.ts +++ b/components/brave_new_tab_ui/components/default/braveToday/cardSizes.ts @@ -7,7 +7,6 @@ import styled from 'brave-ui/theme' import { Block as StandardBlock, Heading as StandardHeading, - Image as StandardImage, Text as StandardText, Time as StandardTime } from './default' @@ -58,7 +57,11 @@ export const ListImageFrame = styled(ImageFrame)` padding-top: 0; ` -export const Image = styled(StandardImage)` +type ImageProps = { + isPromoted?: boolean +} +export const Image = styled('img')` + box-sizing: border-box; display: block; position: absolute; border: none; @@ -69,7 +72,7 @@ export const Image = styled(StandardImage)` right: 0; width: 100%; height: 100%; - object-fit: cover; + object-fit: ${p => p.isPromoted ? 'contain' : 'cover'}; background-color: rgba(188,188,188,0.2); ` @@ -105,14 +108,43 @@ export const Time = styled(StandardTime)` } ` +export const Source = styled('div')` + margin: 10px 0 0 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +` + export const Publisher = styled('div')` box-sizing: border-box; - margin: 10px 0 0 0; - padding: 0; font: 500 14px ${p => p.theme.fontFamily.heading}; color: #fff; ` +export const PromotedLabel = styled('a')` + border-radius: 4px; + background: #495057; + color: rgba(255, 255, 255, .8); + padding: 5px; + font: 400 12px ${p => p.theme.fontFamily.heading}; + display: flex; + flex-direction: row; + gap: 5px; +` + +export const PromotedIcon = styled('div')` + width: 16px; + height: 9px; + color: inherit; + svg { + width: 100%; + height: 100%; + color: inherit; + fill: currentColor; + } +` + export const ContainerForTwo = styled<{}, 'div'>('div')` width: 680px; display: grid; 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 713be7640ac6..d02dd0c09ee7 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 @@ -10,6 +10,7 @@ import * as Background from '../../../../../common/Background' type Props = { imageUrl: string list?: boolean + isPromoted?: boolean onLoaded?: () => any } @@ -61,7 +62,7 @@ export default function CardImage (props: Props) { const Frame = props.list ? Card.ListImageFrame : Card.ImageFrame return ( - + ) } diff --git a/components/brave_new_tab_ui/components/default/braveToday/cards/_articles/cardArticleLarge.tsx b/components/brave_new_tab_ui/components/default/braveToday/cards/_articles/cardArticleLarge.tsx index 2a2e213e3dff..37c51c1cbfb9 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/cards/_articles/cardArticleLarge.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/cards/_articles/cardArticleLarge.tsx @@ -13,31 +13,49 @@ import { OnReadFeedItem, OnSetPublisherPref } from '../../' // TODO(petemill): Large and Medium article should be combined to 1 component. interface Props { - content: (BraveToday.Article | undefined)[] + content: (BraveToday.Article | BraveToday.PromotedArticle | undefined)[] publishers: BraveToday.Publishers articleToScrollTo?: BraveToday.FeedItem onReadFeedItem: OnReadFeedItem onSetPublisherPref: OnSetPublisherPref + isPromoted?: boolean } type ArticleProps = { - item: BraveToday.Article + item: BraveToday.Article | BraveToday.PromotedArticle publisher?: BraveToday.Publisher shouldScrollIntoView?: boolean onReadFeedItem: OnReadFeedItem onSetPublisherPref: OnSetPublisherPref + isPromoted?: boolean +} + +const promotedInfoUrl = 'https://brave.com/brave-today' + +function onClickPromoted (e: React.MouseEvent) { + const openInNewTab = e.ctrlKey || e.metaKey + if (openInNewTab) { + document.open(promotedInfoUrl, '__blank') + } else { + window.location.href = promotedInfoUrl + } + e.preventDefault() } const LargeArticle = React.forwardRef(function (props: ArticleProps, ref) { const { publisher, item } = props const [cardRef] = useScrollIntoView(props.shouldScrollIntoView || false) + const onClick = useReadArticleClickHandler(props.onReadFeedItem, item) + + // TODO(petemill): Avoid nested links // `ref as any` due to https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884 return ( @@ -46,12 +64,29 @@ const LargeArticle = React.forwardRef(function (props {item.relative_time} { publisher && + + {props.isPromoted && + + + + + + + Promoted + + } + } @@ -86,6 +121,7 @@ const CardSingleArticleLarge = React.forwardRef(function (pr shouldScrollIntoView={shouldScrollIntoView} onReadFeedItem={props.onReadFeedItem} onSetPublisherPref={props.onSetPublisherPref} + isPromoted={props.isPromoted} /> ) })} diff --git a/components/brave_new_tab_ui/components/default/braveToday/cardsGroup.tsx b/components/brave_new_tab_ui/components/default/braveToday/cardsGroup.tsx index b8cef731bb45..f4d8cd9d7b14 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/cardsGroup.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/cardsGroup.tsx @@ -22,6 +22,7 @@ enum CardType { HeadlinePaired, CategoryGroup, Deals, + PromotedArticle, PublisherGroup } @@ -30,6 +31,7 @@ const PageContentOrder = [ CardType.Headline, CardType.HeadlinePaired, CardType.HeadlinePaired, + CardType.PromotedArticle, CardType.CategoryGroup, CardType.Headline, CardType.Deals, @@ -80,6 +82,18 @@ function Card (props: CardProps) { onReadFeedItem={props.onReadFeedItem} onSetPublisherPref={props.onSetPublisherPref} /> + case CardType.PromotedArticle: + if (!props.content.promotedArticle) { + return null + } + return case CardType.CategoryGroup: if (!props.content.itemsByCategory) { return null 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 16ece7c070c5..fd19dc2776e5 100644 --- a/components/brave_new_tab_ui/stories/default/data/todayStorybookState.ts +++ b/components/brave_new_tab_ui/stories/default/data/todayStorybookState.ts @@ -95,6 +95,22 @@ export default function getTodayState (): BraveTodayState { ], pages: [ { + promotedArticle: { + category: `Brave Partners`, + publish_time: `2020-10-21 21:46:00 UTC`, + url: `https://www.autoblog.com/2020/10/21/tesla-quarterly-earnings-net-profit/`, + img: `https://pcdn.brave.software/brave-today/cache/bc35efae36f1a6e590f68a30858c27c7e0adfb5f508bc1267798a2fe000eaea6.jpg.pad`, + title: `Tesla posts net profit for fifth straight quarter`, + description: `Filed under:\n\t\t\t\t\t Earnings/Financials,Green,Tesla,Electric\n\t\t\t\t\t Tesla charged through a summertime auto industry sales slump in the U.S. to post stronger-than-expected net earnings for the third quarter. The electric car and solar panel maker says Wednesday that it made $331 million, or 27 cents per share, for its fifth-straight profitable quarter. Excluding special items such as stock-based compensation, Tesla made 76 cents per share, beating Wall Street estimates of 57 cents.Continue`, + content_type: `brave_partner`, + publisher_id: `5eece347713f329f156cd0204cf9b12629f1dc8f4ea3c1b67984cfbfd66cdca5`, + publisher_name: `Autoblog`, + url_hash: `963e1dbf6eccd9cdf0d273a89f3bc25c727f871104bf9e91d98db796fd09b696`, + padded_img: `https://pcdn.brave.software/brave-today/cache/bc35efae36f1a6e590f68a30858c27c7e0adfb5f508bc1267798a2fe000eaea6.jpg.pad`, + score: 19.716764507900656, + points: 14.716764507900656, + relative_time: `about 6 hours ago` + }, articles: [ { category: `Cars`, diff --git a/components/definitions/today.d.ts b/components/definitions/today.d.ts index ba3e5a9658b2..f0af6c1b4399 100644 --- a/components/definitions/today.d.ts +++ b/components/definitions/today.d.ts @@ -40,7 +40,7 @@ declare namespace BraveToday { } } - export type FeedItem = (Article | Deal) + export type FeedItem = (Article | Deal | PromotedArticle) export interface Feed { hash: string @@ -62,7 +62,7 @@ declare namespace BraveToday { items: Article[] // 3 } deals: Deal[] // 3 - + promotedArticle?: PromotedArticle } export interface ScrollingList { @@ -72,7 +72,7 @@ declare namespace BraveToday { } type BaseFeedItem = { - content_type: 'article' | 'product' + content_type: 'article' | 'product' | 'brave_partner' category: string // 'Tech', 'Business', 'Top News', 'Crypto', 'Cars', 'Culture', 'Fashion', 'Sports', 'Entertainment' publish_time: string // UTC "2020-04-17 19:21:10" title: string // "14 Truly Incredible Catfish Makeup Transformations From TikTok" @@ -93,6 +93,10 @@ declare namespace BraveToday { content_type: 'article' } + export type PromotedArticle = BaseFeedItem & { + content_type: 'brave_partner' + } + export type Deal = BaseFeedItem & { content_type: 'product' offers_category: string // 'Companion Products