Skip to content

Commit

Permalink
Merge pull request #14268 from brave/news2ui
Browse files Browse the repository at this point in the history
Brave News 2.0 Settings UI
  • Loading branch information
petemill authored Oct 15, 2022
2 parents ad98457 + fb9b2b1 commit d24fddd
Show file tree
Hide file tree
Showing 50 changed files with 3,755 additions and 2,130 deletions.
8 changes: 8 additions & 0 deletions .storybook/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
}
16 changes: 16 additions & 0 deletions .storybook/chrome-resources-mock/js/plural_string_proxy.js.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return Promise.resolve(`${key}(${count})`)
}
}
2 changes: 1 addition & 1 deletion .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
]
},
{
Expand Down
8 changes: 4 additions & 4 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ if (!is_ios) {
deps += [ ":packed_resources" ]
}
}

group("storybook") {
deps = [ "//brave/.storybook:storybook" ]
}
}

if (is_win) {
Expand Down Expand Up @@ -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" ]
Expand Down
18 changes: 18 additions & 0 deletions browser/ui/webui/brave_webui_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -208,6 +210,11 @@ void BraveNewTabMessageHandler::RegisterMessages() {
// - Stats
// - Preferences
// - PrivatePage properties
auto plural_string_handler = std::make_unique<PluralStringHandler>();
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,
Expand Down
5 changes: 4 additions & 1 deletion components/brave_new_tab_ui/api/brave_news/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
157 changes: 157 additions & 0 deletions components/brave_new_tab_ui/api/brave_news/news.ts
Original file line number Diff line number Diff line change
@@ -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()
18 changes: 18 additions & 0 deletions components/brave_new_tab_ui/components/Flex.tsx
Original file line number Diff line number Diff line change
@@ -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') <FlexProps>`
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit d24fddd

Please sign in to comment.