From 5e89cd84a348907a235774093cdcaf7283fcc0b3 Mon Sep 17 00:00:00 2001 From: Pete Miller Date: Fri, 4 Nov 2022 12:41:41 -0700 Subject: [PATCH] Merge pull request #15755 from brave/bn-carousels [Brave News]: Update Discover section to match new design spec. --- browser/ui/webui/brave_webui_source.cc | 3 +- .../default/braveToday/customize/Carousel.tsx | 153 ++++++++++++++++++ .../braveToday/customize/Configure.tsx | 8 +- .../default/braveToday/customize/Context.tsx | 2 + .../braveToday/customize/CustomizeLink.tsx | 17 ++ .../braveToday/customize/CustomizePage.tsx | 58 +++++++ .../default/braveToday/customize/Discover.tsx | 43 +---- .../braveToday/customize/DiscoverSection.tsx | 6 +- .../default/braveToday/customize/Icons.tsx | 22 +-- .../default/braveToday/customize/Modal.tsx | 13 +- .../default/braveToday/customize/Popular.tsx | 62 ++++--- .../braveToday/customize/Suggestions.tsx | 61 +++---- .../containers/newTab/settings.tsx | 2 +- components/resources/brave_news_strings.grdp | 7 +- 14 files changed, 328 insertions(+), 129 deletions(-) create mode 100644 components/brave_new_tab_ui/components/default/braveToday/customize/Carousel.tsx create mode 100644 components/brave_new_tab_ui/components/default/braveToday/customize/CustomizeLink.tsx create mode 100644 components/brave_new_tab_ui/components/default/braveToday/customize/CustomizePage.tsx diff --git a/browser/ui/webui/brave_webui_source.cc b/browser/ui/webui/brave_webui_source.cc index a723def29ccf..cff2e825a97f 100644 --- a/browser/ui/webui/brave_webui_source.cc +++ b/browser/ui/webui/brave_webui_source.cc @@ -207,12 +207,13 @@ void CustomizeWebUIHTMLSource(content::WebUI* web_ui, { "ad", IDS_BRAVE_TODAY_DISPLAY_AD_LABEL }, { "braveNewsBackToDashboard", IDS_BRAVE_NEWS_BACK_TO_DASHBOARD }, + { "braveNewsBackButton", IDS_BRAVE_NEWS_BACK_BUTTON }, { "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 - { "braveNewsShowMoreButton", IDS_BRAVE_NEWS_SHOW_MORE_BUTTON}, + { "braveNewsViewAllButton", IDS_BRAVE_NEWS_VIEW_ALL_BUTTON}, { "braveNewsAllSourcesHeader", IDS_BRAVE_NEWS_ALL_SOURCES_HEADER}, { "braveNewsFeedsHeading", IDS_BRAVE_NEWS_FEEDS_HEADING}, { "braveNewsFollowButtonFollowing", IDS_BRAVE_NEWS_FOLLOW_BUTTON_FOLLOWING}, // NOLINT diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Carousel.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Carousel.tsx new file mode 100644 index 000000000000..31578b054aa2 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Carousel.tsx @@ -0,0 +1,153 @@ +// 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, { css } from 'styled-components' +import Flex from '../../../Flex' +import FeedCard from './FeedCard' +import { ArrowRight } from './Icons' + +const CARD_SIZE = 208 +const CARD_SIZE_PX = `${CARD_SIZE}px` +const CARD_GAP = '16px' + +const ScrollButton = styled.button<{ hidden: boolean }>` + all: unset; + position: absolute; + width: 32px; + height: 32px; + top: 32px; + background: white; + border-radius: 32px; + box-shadow: 0px 1px 4px rgba(63, 76, 99, 0.35); + display: flex; + align-items: center; + justify-content: center; + color: var(--text2); + cursor: pointer; + + :hover { + box-shadow: 0px 1px 4px rgba(63, 76, 99, 0.5); + color: var(--interactive4); + } + + ${p => p.hidden && css`opacity: 0;`} + + transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out; +` + +const ScrollButtonLeft = styled(ScrollButton)` + left: -16px; + transform: rotate(180deg); +` + +const ScrollButtonRight = styled(ScrollButton)` + right: -16px; +` + +const Container = styled(Flex)` + padding: 16px 0; + max-width: calc(${CARD_SIZE_PX} * 3 + ${CARD_GAP} * 2); + container-name: carousel; + container-type: inline-size; + &:not(:hover, :has(:focus-visible)) ${ScrollButton} { + opacity: 0; + } +` + +const Header = styled.div` + width: 100%; + font-weight: 600; + font-size: 16px; + margin: 8px 0; +` + +const Subtitle = styled.span` + font-size: 12px; +` + +const CarouselContainer = styled.div` + position: relative; +` + +const ItemsContainer = styled(Flex)` + margin: 8px 0; + overflow-x: auto; + scroll-snap-type: x mandatory; + + &::-webkit-scrollbar { + display: none; + width: 0; + } +` + +const FeedCardContainer = styled.div` + min-width: calc((100cqi - ${CARD_GAP} * 2) / 3); + max-width: ${CARD_SIZE_PX}; + scroll-snap-align: start; +` + +interface Props { + title: string | JSX.Element + subtitle?: React.ReactNode + publisherIds: string[] +} + +export default function Carousel (props: Props) { + const scrollContainerRef = React.useRef() + const [availableDirections, setAvailableDirections] = React.useState<'none' | 'left' | 'right' | 'both'>('right') + const updateAvailableDirections = React.useCallback(() => { + if (!scrollContainerRef.current) return + + const end = scrollContainerRef.current.scrollWidth - scrollContainerRef.current.clientWidth + const scrollPos = scrollContainerRef.current.scrollLeft + if (end <= 0) { + setAvailableDirections('none') + } else if (end > scrollPos && scrollPos > 0) { + setAvailableDirections('both') + } else if (end > scrollPos) { + setAvailableDirections('right') + } else { + setAvailableDirections('left') + } + }, []) + + const scroll = React.useCallback((dir: 'left' | 'right') => { + if (!scrollContainerRef.current) return + + scrollContainerRef.current.scrollBy({ + behavior: 'smooth', + left: CARD_SIZE * (dir === 'left' ? -1 : 1) + }) + }, []) + + if (!props.publisherIds.length) { + return null + } + + return ( + + +
{props.title}
+
+ {props.subtitle && + {props.subtitle} + } + + + {props.publisherIds.map(p => + + )} + + scroll('left')} hidden={availableDirections === 'right' || availableDirections === 'none'}> + {ArrowRight} + + scroll('right')} hidden={availableDirections === 'left' || availableDirections === 'none'}> + {ArrowRight} + + +
+ ) +} 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 index 08cb38664701..3742ae6c3bdc 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Configure.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Configure.tsx @@ -16,6 +16,8 @@ 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' +import { SuggestionsPage } from './Suggestions' +import { PopularPage } from './Popular' const Grid = styled.div` width: 100%; @@ -104,11 +106,15 @@ const Content = styled.div` export default function Configure () { const [enabled, setEnabled] = useNewTabPref('isBraveTodayOptedIn') - const { setCustomizePage } = useBraveNews() + const { setCustomizePage, customizePage } = useBraveNews() let content: JSX.Element if (!enabled) { content = setEnabled(true)} /> + } else if (customizePage === 'suggestions') { + content = + } else if (customizePage === 'popular') { + content = } else { 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 index 5b843e7d2833..cfbf2cbcd11e 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Context.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Context.tsx @@ -12,6 +12,8 @@ import Modal from './Modal' // Leave possibility for more pages open. type NewsPage = null | 'news' + | 'suggestions' + | 'popular' interface BraveNewsContext { customizePage: NewsPage diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/CustomizeLink.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/CustomizeLink.tsx new file mode 100644 index 000000000000..d3e824163534 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/CustomizeLink.tsx @@ -0,0 +1,17 @@ +// 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' + +const CustomizeLink = styled.button` + all: unset; + font-weight: 600; + font-size: 12px; + line-height: 18px; + color: var(--interactive5); + cursor: pointer; + +` +export default CustomizeLink diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/CustomizePage.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/CustomizePage.tsx new file mode 100644 index 000000000000..feda25f884c3 --- /dev/null +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/CustomizePage.tsx @@ -0,0 +1,58 @@ +// 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 '../../../../../common/locale' +import Button from '../../../../../web-components/button' +import Flex from '../../../Flex' +import { useBraveNews } from './Context' +import { BackArrow } from './Icons' + +const BackButtonContainer = styled.div` + all: unset; + flex: 1; + + &> button { + --inner-border-size: 0; + --outer-border-size: 0; + padding: 0; + + &:hover { + --inner-border-size: 0; + --outer-border-size: 0; + } + } +` + +const Header = styled.span` + font-weight: 500; + font-size: 16px; + color: var(--text01); + flex: 5; + text-align: center; +` + +const Spacer = styled.div`flex: 1;` + +export default function CustomizePage (props: { + title: string + children: React.ReactNode +}) { + const { setCustomizePage } = useBraveNews() + return + + + + +
{props.title}
+ +
+ {props.children} +
+} 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 index 08b09b7d11f1..203241bd9d6b 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Discover.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Discover.tsx @@ -15,8 +15,8 @@ import ChannelCard from './ChannelCard' import DiscoverSection from './DiscoverSection' import FeedCard, { DirectFeedCard } from './FeedCard' import useSearch from './useSearch' -import Suggestions from './Suggestions' -import Popular from './Popular' +import { SuggestionsCarousel } from './Suggestions' +import { PopularCarousel } from './Popular' const Header = styled.span` font-size: 24px; @@ -31,16 +31,6 @@ const SearchInput = styled(TextInput)` --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('') @@ -55,40 +45,23 @@ export default function Discover () { } function Home () { - const [showingAllCategories, setShowingAllCategories] = React.useState(false) const channels = useChannels() - const { filteredPublisherIds, updateSuggestedPublisherIds } = useBraveNews() + const { updateSuggestedPublisherIds } = 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]) + const channelNames = React.useMemo(() => channels.map(c => c.channelName), + [channels]) // When we mount this component, update the suggested publisher ids. React.useEffect(() => { updateSuggestedPublisherIds() }, []) return ( <> - - + + - {visibleChannelIds.map(channelName => + {channelNames.map(channelName => )} - {!showingAllCategories && - - } - - - {filteredPublisherIds.map(publisherId => - - )} ) 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 index a8bd41cd27a9..0db114b8a231 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/DiscoverSection.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/DiscoverSection.tsx @@ -8,7 +8,7 @@ import styled from 'styled-components' import Flex from '../../../Flex' interface Props { - name: string + name?: string subtitle?: React.ReactNode children?: React.ReactNode } @@ -36,9 +36,9 @@ const ItemsContainer = styled.div` export default function DiscoverSection (props: Props) { return - + {props.name &&
{props.name}
-
+
} {props.subtitle && {props.subtitle} } 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 index 13a3828972ec..37817a14aae9 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Icons.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Icons.tsx @@ -21,6 +21,10 @@ export const Cross = +export const ArrowRight = + + + export const channels = { 'default': @@ -39,7 +43,7 @@ export const channels = { , 'Culture': - + @@ -49,7 +53,7 @@ export const channels = { , 'Entertainment': - + @@ -61,7 +65,7 @@ export const channels = { , 'Fashion': - + @@ -71,7 +75,7 @@ export const channels = { , 'Food': - + @@ -81,7 +85,7 @@ export const channels = { , 'Fun': - + @@ -99,7 +103,7 @@ export const channels = { , 'Health': - + @@ -118,7 +122,7 @@ export const channels = { , 'Sports': - + @@ -128,7 +132,7 @@ export const channels = { , 'Travel': - + @@ -149,7 +153,7 @@ export const channels = { , 'Top News': - + 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 index 6e72df868547..c168a633a0dc 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Modal.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Modal.tsx @@ -5,6 +5,7 @@ import * as React from 'react' import styled from 'styled-components' +import LoadingComponent from '../../../loading' import { useBraveNews } from './Context' const Configure = React.lazy(() => import('./Configure')) @@ -12,7 +13,7 @@ const Configure = React.lazy(() => import('./Configure')) const Dialog = styled.dialog` border-radius: 8px; border: none; - width: min(100vw, 1092px); + width: min(100vw, 1049px); height: min(100vh, 712px); z-index: 1000; background: white; @@ -31,13 +32,17 @@ export default function BraveNewsModal () { // Note: There's no attribute for open modal, so we need // to call showModal instead. React.useEffect(() => { - dialogRef.current?.showModal?.() - }, [customizePage, dialogRef]) + if (shouldRender && !dialogRef.current?.open) { + dialogRef.current?.showModal?.() + } else if (!shouldRender && dialogRef.current?.open) { + dialogRef.current?.close?.() + } + }, [shouldRender, 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/Popular.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Popular.tsx index 7ce43714a558..355e942b8472 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Popular.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Popular.tsx @@ -3,30 +3,22 @@ // 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 { getLocale } from '$web-common/locale' import * as React from 'react' -import styled from 'styled-components' -import Button from '$web-components/button' +import { api } from '../../../../api/brave_news/news' +import Flex from '../../../Flex' +import Carousel from './Carousel' import { useBraveNews } from './Context' +import CustomizeLink from './CustomizeLink' +import CustomizePage from './CustomizePage' import DiscoverSection from './DiscoverSection' import FeedCard from './FeedCard' -import { getLocale } from '$web-common/locale' -import { api } from '../../../../api/brave_news/news' -const LoadMoreButtonContainer = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; - grid-column: 2; -` - -const DEFAULT_SUGGESTIONS_COUNT = 3 - -export default function Suggestions () { +const usePopularPublisherIds = () => { const { filteredPublisherIds, publishers } = useBraveNews() - const [showAll, setShowAll] = React.useState(false) - const popularPublisherIds = React.useMemo(() => filteredPublisherIds.map(id => publishers[id]) + return React.useMemo(() => filteredPublisherIds.map(id => publishers[id]) .map(p => [p, p.locales.find(l => l.locale === api.locale)?.rank] as const) - // Filter out publishers which aren't in the current locale. + // Filter out publishers which aren't in the current locale. .filter(([p, pRank]) => pRank !== undefined) .sort(([a, aRank], [b, bRank]) => { // Neither source has a rank, sort alphabetically @@ -39,24 +31,26 @@ export default function Suggestions () { return (aRank || Number.MAX_SAFE_INTEGER) - (bRank || Number.MAX_SAFE_INTEGER) }) .map(([p]) => p.publisherId), [filteredPublisherIds, publishers]) +} - const popularPublishersTruncated = React.useMemo(() => showAll - ? popularPublisherIds - : popularPublisherIds.slice(0, DEFAULT_SUGGESTIONS_COUNT), [popularPublisherIds, showAll]) - - if (!popularPublisherIds.length) { - return null - } - +export function PopularCarousel () { + const { setCustomizePage } = useBraveNews() + const popularPublisherIds = usePopularPublisherIds() return ( - - {popularPublishersTruncated.map(s => )} - {!showAll && popularPublisherIds.length > DEFAULT_SUGGESTIONS_COUNT && - - - } - + + {getLocale('braveNewsPopularTitle')} + setCustomizePage('popular')}> + {getLocale('braveNewsViewAllButton')} + + } publisherIds={popularPublisherIds} /> ) } + +export function PopularPage () { + const popularPublisherIds = usePopularPublisherIds() + return + + {popularPublisherIds.map(p => )} + + +} diff --git a/components/brave_new_tab_ui/components/default/braveToday/customize/Suggestions.tsx b/components/brave_new_tab_ui/components/default/braveToday/customize/Suggestions.tsx index 0c842de21989..a0319a7261c6 100644 --- a/components/brave_new_tab_ui/components/default/braveToday/customize/Suggestions.tsx +++ b/components/brave_new_tab_ui/components/default/braveToday/customize/Suggestions.tsx @@ -3,52 +3,35 @@ // 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 { getLocale } from '$web-common/locale' import * as React from 'react' -import styled from 'styled-components' -import Button from '$web-components/button' +import Flex from '../../../Flex' +import Carousel from './Carousel' import { useBraveNews } from './Context' +import CustomizeLink from './CustomizeLink' +import CustomizePage from './CustomizePage' import DiscoverSection from './DiscoverSection' import FeedCard from './FeedCard' -import { getLocale } from '$web-common/locale' - -const LoadMoreButtonContainer = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; - grid-column: 2; -` -const Subtitle = styled.span` - font-weight: 400; - font-size: 12px; - color: var(--text2); -` +export function SuggestionsCarousel () { + const { suggestedPublisherIds, setCustomizePage } = useBraveNews() -const DEFAULT_SUGGESTIONS_COUNT = 3 + return + {getLocale('braveNewsSuggestionsTitle')} + setCustomizePage('suggestions')}> + {getLocale('braveNewsViewAllButton')} + + } + subtitle={getLocale('braveNewsSuggestionsSubtitle')} + publisherIds={suggestedPublisherIds}/> +} -export default function Suggestions () { +export function SuggestionsPage () { const { suggestedPublisherIds } = useBraveNews() - const [showAll, setShowAll] = React.useState(false) - const filteredSuggestions = React.useMemo(() => suggestedPublisherIds - .slice(0, showAll ? undefined : DEFAULT_SUGGESTIONS_COUNT), [suggestedPublisherIds, showAll]) - - if (!filteredSuggestions.length) { - return null - } - - return ( - {getLocale('braveNewsSuggestionsSubtitle')} - }> - {filteredSuggestions.map(s => - ) - } - {!showAll && suggestedPublisherIds.length > DEFAULT_SUGGESTIONS_COUNT && - - - } + return + + {suggestedPublisherIds.map(p => )} - ) + } diff --git a/components/brave_new_tab_ui/containers/newTab/settings.tsx b/components/brave_new_tab_ui/containers/newTab/settings.tsx index 570969042886..ba94bcda17fb 100644 --- a/components/brave_new_tab_ui/containers/newTab/settings.tsx +++ b/components/brave_new_tab_ui/containers/newTab/settings.tsx @@ -146,7 +146,7 @@ export default class Settings extends React.PureComponent { // 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.context.customizePage ) { this.props.onClose() } diff --git a/components/resources/brave_news_strings.grdp b/components/resources/brave_news_strings.grdp index 9a5649efc5b5..9d4d059123f7 100644 --- a/components/resources/brave_news_strings.grdp +++ b/components/resources/brave_news_strings.grdp @@ -3,6 +3,9 @@ Back to $1Dashboard$2 + + Back + Turn on Brave News, and never miss a story @@ -18,8 +21,8 @@ Channels - - Show more + + View All Sources