Compare commits

...

34 Commits

Author SHA1 Message Date
Eric Bailey b963ce8b8a Simple feed-end component with trends on mobile
(cherry picked from commit ccfea4b9461111701586f39e8e3e1bfad187708c)
2024-12-17 17:12:44 -06:00
Eric Bailey 30207577b7 Remove stray flex_1 2024-12-17 17:11:48 -06:00
Eric Bailey 511a13ec2f Remove comment 2024-12-17 17:11:48 -06:00
Eric Bailey d0b6000d22 Remove old file 2024-12-17 17:11:48 -06:00
Eric Bailey b3f253dbba Up config query to 5 min 2024-12-17 17:11:48 -06:00
Eric Bailey 90ed779cc3 Fix no-show logic 2024-12-17 17:11:48 -06:00
Eric Bailey eda3c35155 Hook up prod agent 2024-12-17 17:11:48 -06:00
Eric Bailey 7cb9694971 Straight passthrough links 2024-12-17 17:11:48 -06:00
Eric Bailey 87e88c31e8 Tweak config 2024-12-17 17:11:48 -06:00
Eric Bailey 3bf151d9b6 Hook up real data 2024-12-17 17:11:47 -06:00
Eric Bailey b269b1979d Only english during beta period 2024-12-17 17:11:47 -06:00
Eric Bailey 4a8fb8255d Add feature gate 2024-12-17 17:11:47 -06:00
Eric Bailey cd52e1bcda Update package, rm langs 2024-12-17 17:11:47 -06:00
Eric Bailey 57ff6f184d Gate settings too 2024-12-17 17:11:47 -06:00
Eric Bailey 305a9e6346 Remove titleChild 2024-12-17 17:11:47 -06:00
Eric Bailey 369373d2aa Remove broadcast hack 2024-12-17 17:11:47 -06:00
Eric Bailey 82a069fdaf Cache enabled value for reloads, use real data for service config 2024-12-17 17:11:47 -06:00
Eric Bailey e03a52b6c4 Stale time to 30m 2024-12-17 17:11:47 -06:00
Eric Bailey 2f57c24e5b Centralize mute word handling 2024-12-17 17:11:47 -06:00
Eric Bailey 2097b67ff0 Handle service config, enablement, load states, update lex contract 2024-12-17 17:11:47 -06:00
Eric Bailey 9d7ebe3ee7 Some skeleton UI 2024-12-17 17:11:47 -06:00
Eric Bailey 111ca28fb9 Align components 2024-12-17 17:11:47 -06:00
Eric Bailey c8a6e3b323 Wire up interstitial and Explore page 2024-12-17 17:11:47 -06:00
Eric Bailey e92095810d First hack at mute words 2024-12-17 17:11:47 -06:00
Eric Bailey 72cf85d38b Exploration 2024-12-17 17:11:47 -06:00
Eric Bailey b03a85ad53 Add new interstitial to Discover 2024-12-17 17:11:47 -06:00
Eric Bailey be3c36fcf6 Clean up persisted code, add new setting 2024-12-17 17:11:47 -06:00
Eric Bailey 99db939beb Fix up hide/show toggle state, WITH broadcast hacK 2024-12-17 17:11:47 -06:00
Eric Bailey e3d110207e Add to settings, abstract state, not updating in tab 2024-12-17 17:11:47 -06:00
Eric Bailey 55b573a35a Add persisted option to hide trending from sidebar 2024-12-17 17:11:47 -06:00
Eric Bailey 0ff6151ab8 Add tiny option in RightNav 2024-12-17 17:11:47 -06:00
Eric Bailey 6287bf6c44 Improve explore treatment a bit, add some polish to cards 2024-12-17 17:11:47 -06:00
Eric Bailey 352302a1a4 Disable SPs for now 2024-12-17 17:11:47 -06:00
Paul Frazee 7dd6f6c3dc Add WIP UIs for trending topics and suggested starterpacks 2024-12-17 17:11:47 -06:00
25 changed files with 1061 additions and 93 deletions
+1 -1
View File
@@ -54,7 +54,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.13.20",
"@atproto/api": "^0.13.21",
"@bitdrift/react-native": "0.4.0",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+9 -6
View File
@@ -57,6 +57,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
@@ -143,12 +144,14 @@ function InnerApp() {
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
<TrendingConfigProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</TrendingConfigProvider>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
+5 -2
View File
@@ -47,6 +47,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
@@ -127,8 +128,10 @@ function InnerApp() {
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
<TrendingConfigProvider>
<Shell />
<NuxDialogs />
</TrendingConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
+129
View File
@@ -0,0 +1,129 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
import {isNative} from '#/platform/detection'
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, useBreakpoints,useGutters, useTheme, web} from '#/alf'
import {Button, ButtonIcon,ButtonText} from '#/components/Button'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
const TRENDS_COUNT = 9
export function FeedEnd() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters(['wide'])
const {enabled} = useTrendingConfig()
const navigation = useNavigationDeduped()
const {gtTablet} = useBreakpoints()
const onPressExplore = React.useCallback(() => {
if (isNative) {
navigation.navigate('SearchTab')
navigation.popToTop()
} else {
navigation.navigate('Search', {})
}
}, [navigation])
return (
<View
style={[
a.align_center,
gutters,
a.py_5xl,
a.border_t,
t.atoms.border_contrast_low,
]}>
<View style={[a.gap_sm, a.pb_2xl]}>
<Text
style={[
a.text_sm,
a.leading_snug,
a.text_center,
{
maxWidth: 200,
},
web({
maxWidth: '12rem',
}),
]}>
<Trans>That's it! You've reached the end of this feed.</Trans>
</Text>
<Button
label={_(msg`Explore Bluesky`)}
onPress={onPressExplore}
size="small"
variant="solid"
color="secondary"
style={[a.justify_center]}>
<ButtonIcon icon={Search} position="left" />
<ButtonText>
<Trans>Explore Bluesky</Trans>
</ButtonText>
</Button>
</View>
{enabled && !gtTablet ? <Trending /> : null}
</View>
)
}
function Trending() {
const t = useTheme()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
const {gtMobile} = useBreakpoints()
return error || noTopics ? null : (
<View style={[a.gap_sm, gtMobile && a.px_2xl]}>
<View style={[a.flex_row, a.align_center, a.justify_center, a.gap_xs]}>
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>Check out what's trending</Trans>
</Text>
<Graph size="sm" />
</View>
<View style={[a.flex_row, a.flex_wrap, a.justify_center, a.gap_sm]}>
{isLoading ? (
Array(TRENDS_COUNT)
.fill(0)
.map((_n, i) => (
<TrendingTopicSkeleton key={i} size="small" index={i} />
))
) : !trending?.topics ? null : (
<>
{trending.topics.slice(0, TRENDS_COUNT).map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
size="small"
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
</View>
)
}
+5 -3
View File
@@ -1,11 +1,13 @@
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, tokens} from '#/alf'
import {atoms as a, tokens, ViewStyleProp} from '#/alf'
export function GradientFill({
gradient,
}: {
style,
}: ViewStyleProp & {
gradient:
| typeof tokens.gradients.primary
| typeof tokens.gradients.sky
| typeof tokens.gradients.midnight
| typeof tokens.gradients.sunrise
@@ -26,7 +28,7 @@ export function GradientFill({
}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
style={[a.absolute, a.inset_0, style]}
/>
)
}
+223
View File
@@ -0,0 +1,223 @@
import React from 'react'
import {View} from 'react-native'
import {AtUri} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
// import {makeProfileLink} from '#/lib/routes/links'
// import {feedUriToHref} from '#/lib/strings/url-helpers'
// import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
// import {CloseQuote_Filled_Stroke2_Corner0_Rounded as Quote} from '#/components/icons/Quote'
// import {UserAvatar} from '#/view/com/util/UserAvatar'
import type {TrendingTopic} from '#/state/queries/trending/useTrendingTopics'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
export function TrendingTopic({
topic: raw,
size,
style,
}: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) {
const t = useTheme()
const topic = useTopic(raw)
const isSmall = size === 'small'
// const hasAvi = topic.type === 'feed' || topic.type === 'profile'
// const aviSize = isSmall ? 16 : 20
// const iconSize = isSmall ? 16 : 20
return (
<View
style={[
a.flex_row,
a.align_center,
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg,
isSmall
? [
{
paddingVertical: 5,
paddingHorizontal: 10,
},
]
: [a.py_sm, a.px_md],
style,
/*
{
padding: 6,
gap: hasAvi ? 4 : 2,
},
a.pr_md,
*/
]}>
{/*
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
a.overflow_hidden,
{
width: aviSize,
height: aviSize,
},
]}>
{topic.type === 'tag' ? (
<Hashtag width={iconSize} />
) : topic.type === 'topic' ? (
<Quote width={iconSize - 2} />
) : topic.type === 'feed' ? (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
) : (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
)}
</View>
*/}
<Text
style={[
a.font_bold,
a.leading_tight,
isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}],
]}
numberOfLines={1}>
{topic.displayName}
</Text>
</View>
)
}
export function TrendingTopicSkeleton({
size = 'large',
index = 0,
}: {
size?: 'large' | 'small'
index?: number
}) {
const t = useTheme()
const isSmall = size === 'small'
return (
<View
style={[
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg_contrast_25,
isSmall
? {
width: index % 2 === 0 ? 75 : 90,
height: 27,
}
: {
width: index % 2 === 0 ? 90 : 110,
height: 36,
},
]}
/>
)
}
export function TrendingTopicLink({
topic: raw,
children,
...rest
}: {
topic: TrendingTopic
} & Omit<LinkProps, 'to' | 'label'>) {
const topic = useTopic(raw)
return (
<InternalLink label={topic.label} to={topic.url} {...rest}>
{children}
</InternalLink>
)
}
type ParsedTrendingTopic =
| {
type: 'topic' | 'tag' | 'unknown'
label: string
displayName: string
url: string
uri: undefined
}
| {
type: 'profile' | 'feed'
label: string
displayName: string
url: string
uri: AtUri
}
export function useTopic(raw: TrendingTopic): ParsedTrendingTopic {
const {_} = useLingui()
return React.useMemo(() => {
const {topic: displayName, link} = raw
if (link.startsWith('/search')) {
return {
type: 'topic',
label: _(msg`Browse posts about ${displayName}`),
displayName,
uri: undefined,
url: link,
}
} else if (link.startsWith('/hashtag')) {
return {
type: 'tag',
label: _(msg`Browse posts tagged with ${displayName}`),
displayName,
// displayName: displayName.replace(/^#/, ''),
uri: undefined,
url: link,
}
}
/*
if (!link.startsWith('at://')) {
// above logic
} else {
const urip = new AtUri(link)
switch (urip.collection) {
case 'app.bsky.actor.profile': {
return {
type: 'profile',
label: _(msg`View ${displayName}'s profile`),
displayName,
uri: urip,
url: makeProfileLink({did: urip.host, handle: urip.host}),
}
}
case 'app.bsky.feed.generator': {
return {
type: 'feed',
label: _(msg`Browse the ${displayName} feed`),
displayName,
uri: urip,
url: feedUriToHref(link),
}
}
}
}
*/
return {
type: 'unknown',
label: _(msg`Browse topic ${displayName}`),
displayName,
uri: undefined,
url: link,
}
}, [_, raw])
}
+113
View File
@@ -0,0 +1,113 @@
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {
DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {GradientFill} from '#/components/GradientFill'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import * as Prompt from '#/components/Prompt'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
export function TrendingInterstitial() {
const {enabled} = useTrendingConfig()
const {trendingDiscoverHidden} = useTrendingSettings()
return !enabled ? null : trendingDiscoverHidden ? null : <Inner />
}
export function Inner() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters(['base'])
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingDiscoverHidden} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
return error || noTopics ? null : (
<View
style={[
gutters,
a.gap_lg,
a.border_t,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<Graph size="lg" />
<Text style={[a.text_lg, a.font_heavy]}>
<Trans>Trending</Trans>
</Text>
<View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
<GradientFill gradient={tokens.gradients.primary} />
<Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
<Trans>BETA</Trans>
</Text>
</View>
</View>
<Button
label={_(msg`Hide trending topics from your feed`)}
size="tiny"
variant="outline"
color="secondary"
shape="round"
onPress={() => trendingPrompt.open()}>
<ButtonIcon icon={X} />
</Button>
</View>
<View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
.fill(0)
.map((_n, i) => <TrendingTopicSkeleton key={i} index={i} />)
) : !trending?.topics ? null : (
<>
{trending.topics.map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Hide trending topics in your feed?`)}
description={_(
msg`This is a device setting, and will apply to all accounts on this device. You can update this later from your settings.`,
)}
confirmButtonCta={_(msg`Hide`)}
onConfirm={() => setTrendingDiscoverHidden(true)}
/>
</View>
)
}
+1
View File
@@ -4,3 +4,4 @@ export type Gate =
| 'debug_subscriptions'
| 'new_postonboarding'
| 'remove_show_latest_button'
| 'trending_topics_beta'
@@ -0,0 +1,94 @@
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {isWeb} from '#/platform/detection'
import {
DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
import {GradientFill} from '#/components/GradientFill'
import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
export function ExploreTrendingTopics() {
const {enabled} = useTrendingConfig()
return enabled ? <Inner /> : null
}
function Inner() {
const t = useTheme()
const gutters = useGutters([0, 'compact'])
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
return error || noTopics ? null : (
<>
<View
style={[
isWeb
? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
: [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md],
a.border_b,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_sm]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Trending
size="lg"
fill={t.palette.primary_500}
style={{marginLeft: -2}}
/>
<Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
<Trans>Trending</Trans>
</Text>
<View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
<GradientFill gradient={tokens.gradients.primary} />
<Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
<Trans>BETA</Trans>
</Text>
</View>
</View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>What people are posting about now</Trans>
</Text>
</View>
</View>
<View style={[a.pt_md, a.pb_lg]}>
<View
style={[a.flex_row, a.justify_start, a.flex_wrap, a.gap_sm, gutters]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
.fill(0)
.map((_, i) => <TrendingTopicSkeleton key={i} index={i} />)
) : !trending?.topics ? null : (
<>
{trending.topics.map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
</View>
</>
)
}
@@ -9,6 +9,11 @@ import {
useInAppBrowser,
useSetInAppBrowser,
} from '#/state/preferences/in-app-browser'
import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {useTrendingConfig} from '#/state/trending-config'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import * as Toggle from '#/components/forms/Toggle'
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
@@ -16,6 +21,7 @@ import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
import * as Layout from '#/components/Layout'
@@ -29,6 +35,10 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const setAutoplayDisabledPref = useSetAutoplayDisabled()
const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser()
const {enabled: trendingEnabled} = useTrendingConfig()
const {trendingSidebarHidden, trendingDiscoverHidden} = useTrendingSettings()
const {setTrendingSidebarHidden, setTrendingDiscoverHidden} =
useTrendingSettingsApi()
return (
<Layout.Screen>
@@ -104,6 +114,39 @@ export function ContentAndMediaSettingsScreen({}: Props) {
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
{trendingEnabled && (
<>
<SettingsList.Divider />
{!isNative && (
<Toggle.Item
name="show_trending_topics_sidebar"
label={_(msg`Show trending topics in your sidebar`)}
value={!trendingSidebarHidden}
onChange={value => setTrendingSidebarHidden(!value)}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={Graph} />
<SettingsList.ItemText>
<Trans>Show trending topics in your sidebar</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
)}
<Toggle.Item
name="show_trending_topics_discover"
label={_(msg`Show trending topics in your feed`)}
value={!trendingDiscoverHidden}
onChange={value => setTrendingDiscoverHidden(!value)}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={Graph} />
<SettingsList.ItemText>
<Trans>Show trending topics in your feed</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
</>
)}
</SettingsList.Container>
</Layout.Content>
</Layout.Screen>
+12
View File
@@ -125,6 +125,12 @@ const schema = z.object({
subtitlesEnabled: z.boolean().optional(),
/** @deprecated */
mutedThreads: z.array(z.string()),
/*
* Trending
*/
trendingSidebarHidden: z.boolean().optional(),
trendingDiscoverHidden: z.boolean().optional(),
})
export type Schema = z.infer<typeof schema>
@@ -170,6 +176,12 @@ export const defaults: Schema = {
kawaii: false,
hasCheckedForStarterPack: false,
subtitlesEnabled: true,
/*
* Trending
*/
trendingSidebarHidden: false,
trendingDiscoverHidden: false,
}
export function tryParse(rawData: string): Schema | undefined {
+4 -1
View File
@@ -10,6 +10,7 @@ import {Provider as KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages'
import {Provider as LargeAltBadgeProvider} from './large-alt-badge'
import {Provider as SubtitlesProvider} from './subtitles'
import {Provider as TrendingSettingsProvider} from './trending'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
export {
@@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
<AutoplayProvider>
<UsedStarterPacksProvider>
<SubtitlesProvider>
<KawaiiProvider>{children}</KawaiiProvider>
<TrendingSettingsProvider>
<KawaiiProvider>{children}</KawaiiProvider>
</TrendingSettingsProvider>
</SubtitlesProvider>
</UsedStarterPacksProvider>
</AutoplayProvider>
+86
View File
@@ -0,0 +1,86 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext = {
trendingSidebarHidden: Exclude<
persisted.Schema['trendingSidebarHidden'],
undefined
>
trendingDiscoverHidden: Exclude<
persisted.Schema['trendingDiscoverHidden'],
undefined
>
}
type ApiContext = {
setTrendingSidebarHidden(
hidden: Exclude<persisted.Schema['trendingSidebarHidden'], undefined>,
): void
setTrendingDiscoverHidden(
hidden: Exclude<persisted.Schema['trendingDiscoverHidden'], undefined>,
): void
}
const StateContext = React.createContext<StateContext>({
trendingSidebarHidden: Boolean(persisted.defaults.trendingSidebarHidden),
trendingDiscoverHidden: Boolean(persisted.defaults.trendingDiscoverHidden),
})
const ApiContext = React.createContext<ApiContext>({
setTrendingSidebarHidden() {},
setTrendingDiscoverHidden() {},
})
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
const [value, _set] = React.useState(() => {
return Boolean(persisted.get(key))
})
const set = React.useCallback<
(value: Exclude<persisted.Schema[T], undefined>) => void
>(
hidden => {
_set(Boolean(hidden))
persisted.write(key, hidden)
},
[key, _set],
)
React.useEffect(() => {
return persisted.onUpdate(key, hidden => {
_set(Boolean(hidden))
})
}, [key, _set])
return [value, set] as const
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [trendingSidebarHidden, setTrendingSidebarHidden] =
usePersistedBooleanValue('trendingSidebarHidden')
const [trendingDiscoverHidden, setTrendingDiscoverHidden] =
usePersistedBooleanValue('trendingDiscoverHidden')
/*
* Context
*/
const state = React.useMemo(
() => ({trendingSidebarHidden, trendingDiscoverHidden}),
[trendingSidebarHidden, trendingDiscoverHidden],
)
const api = React.useMemo(
() => ({setTrendingSidebarHidden, setTrendingDiscoverHidden}),
[setTrendingSidebarHidden, setTrendingDiscoverHidden],
)
return (
<StateContext.Provider value={state}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
</StateContext.Provider>
)
}
export function useTrendingSettings() {
return React.useContext(StateContext)
}
export function useTrendingSettingsApi() {
return React.useContext(ApiContext)
}
+1
View File
@@ -6,6 +6,7 @@ export const STALE = {
MINUTES: {
ONE: 1e3 * 60,
FIVE: 1e3 * 60 * 5,
THIRTY: 1e3 * 60 * 30,
},
HOURS: {
ONE: 1e3 * 60 * 60,
+32
View File
@@ -0,0 +1,32 @@
import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
type ServiceConfig = {
checkEmailConfirmed: boolean
topicsEnabled: boolean
}
export function useServiceConfigQuery() {
const agent = useAgent()
return useQuery<ServiceConfig>({
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.FIVE,
queryKey: ['service-config'],
queryFn: async () => {
try {
const {data} = await agent.api.app.bsky.unspecced.getConfig()
return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
topicsEnabled: Boolean(data.topicsEnabled),
}
} catch (e) {
return {
checkEmailConfirmed: false,
topicsEnabled: false,
}
}
},
})
}
@@ -0,0 +1,49 @@
import React from 'react'
import {AppBskyUnspeccedDefs} from '@atproto/api'
import {hasMutedWord} from '@atproto/api/dist/moderation/mutewords'
import {useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
export type TrendingTopic = AppBskyUnspeccedDefs.TrendingTopic
export const DEFAULT_LIMIT = 14
export const trendingTopicsQueryKey = ['trending-topics']
export function useTrendingTopics() {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
const mutedWords = React.useMemo(() => {
return preferences?.moderationPrefs?.mutedWords || []
}, [preferences?.moderationPrefs])
return useQuery({
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.THIRTY,
queryKey: trendingTopicsQueryKey,
async queryFn() {
const {data} = await agent.api.app.bsky.unspecced.getTrendingTopics({
limit: DEFAULT_LIMIT,
})
const {topics, suggested} = data
return {
topics: topics.filter(t => {
return !hasMutedWord({
mutedWords,
text: t.topic + ' ' + t.displayName + ' ' + t.description,
})
}),
suggested: suggested.filter(t => {
return !hasMutedWord({
mutedWords,
text: t.topic + ' ' + t.displayName + ' ' + t.description,
})
}),
}
},
})
}
+70
View File
@@ -0,0 +1,70 @@
import React from 'react'
import {useGate} from '#/lib/statsig/statsig'
import {useLanguagePrefs} from '#/state/preferences/languages'
import {useServiceConfigQuery} from '#/state/queries/service-config'
import {device} from '#/storage'
type Context = {
enabled: boolean
}
const Context = React.createContext<Context>({
enabled: false,
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const gate = useGate()
const langPrefs = useLanguagePrefs()
const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery()
const ctx = React.useMemo<Context>(() => {
if (__DEV__) {
return {enabled: true}
}
/*
* Only English during beta period
*/
if (
!!langPrefs.contentLanguages.length &&
!langPrefs.contentLanguages.includes('en')
) {
return {enabled: false}
}
/*
* While loading, use cached value
*/
const cachedEnabled = device.get(['trendingBetaEnabled'])
if (isInitialLoad) {
return {enabled: Boolean(cachedEnabled)}
}
/*
* Doing an extra check here to reduce hits to statsig. If it's disabled on
* the server, we can exit early.
*/
const enabled = Boolean(config?.topicsEnabled)
if (!enabled) {
// cache for next reload
device.set(['trendingBetaEnabled'], enabled)
return {enabled: false}
}
/*
* Service is enabled, but also check statsig in case we're rolling back.
*/
const gateEnabled = gate('trending_topics_beta')
const _enabled = enabled && gateEnabled
// update cache
device.set(['trendingBetaEnabled'], _enabled)
return {enabled: _enabled}
}, [isInitialLoad, config, gate, langPrefs.contentLanguages])
return <Context.Provider value={ctx}>{children}</Context.Provider>
}
export function useTrendingConfig() {
return React.useContext(Context)
}
+1
View File
@@ -8,4 +8,5 @@ export type Device = {
geolocation?: {
countryCode: string | undefined
}
trendingBetaEnabled: boolean
}
+17 -1
View File
@@ -23,6 +23,7 @@ import {logger} from '#/logger'
import {isIOS, isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useTrendingSettings} from '#/state/preferences/trending'
import {STALE} from '#/state/queries'
import {
FeedDescriptor,
@@ -35,6 +36,7 @@ import {
import {useSession} from '#/state/session'
import {useProgressGuide} from '#/state/shell/progress-guide'
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
import {TrendingInterstitial} from '#/components/interstitials/Trending'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -90,6 +92,10 @@ type FeedRow =
type: 'interstitialProgressGuide'
key: string
}
| {
type: 'interstitialTrending'
key: string
}
export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
if (feedRow.type === 'sliceItem') {
@@ -259,6 +265,8 @@ let PostFeed = ({
const showProgressIntersitial =
(followProgressGuide || followAndLikeProgressGuide) && !isDesktop
const {trendingDiscoverHidden} = useTrendingSettings()
const feedItems: FeedRow[] = React.useMemo(() => {
let feedKind: 'following' | 'discover' | 'profile' | undefined
if (feedType === 'following') {
@@ -304,7 +312,12 @@ let PostFeed = ({
type: 'interstitialProgressGuide',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
} else if (sliceIndex === 20) {
} else if (sliceIndex === 15 && !trendingDiscoverHidden) {
arr.push({
type: 'interstitialTrending',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
} else if (sliceIndex === 30) {
arr.push({
type: 'interstitialFollows',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
@@ -390,6 +403,7 @@ let PostFeed = ({
feedTab,
hasSession,
showProgressIntersitial,
trendingDiscoverHidden,
])
// events
@@ -476,6 +490,8 @@ let PostFeed = ({
return <SuggestedFollows feed={feed} />
} else if (row.type === 'interstitialProgressGuide') {
return <ProgressGuide />
} else if (row.type === 'interstitialTrending') {
return <TrendingInterstitial />
} else if (row.type === 'sliceItem') {
const slice = row.slice
if (slice.isFallbackMarker) {
+6 -15
View File
@@ -25,10 +25,8 @@ import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {FeedPage} from '#/view/com/feeds/FeedPage'
import {HomeHeader} from '#/view/com/home/HomeHeader'
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
import {FeedEnd} from '#/components/FeedEnd'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
@@ -192,14 +190,6 @@ function HomeScreenReady({
[onPressSelected, pinnedFeedInfos],
)
const renderFollowingEmptyState = React.useCallback(() => {
return <FollowingEmptyState />
}, [])
const renderCustomFeedEmptyState = React.useCallback(() => {
return <CustomFeedEmptyState />
}, [])
const homeFeedParams = React.useMemo<FeedParams>(() => {
return {
mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
@@ -232,8 +222,8 @@ function HomeScreenReady({
isPageAdjacent={Math.abs(selectedIndex - index) === 1}
feed={feed}
feedParams={homeFeedParams}
renderEmptyState={renderFollowingEmptyState}
renderEndOfFeed={FollowingEndOfFeed}
renderEmptyState={FeedEnd}
renderEndOfFeed={FeedEnd}
/>
)
}
@@ -245,8 +235,9 @@ function HomeScreenReady({
isPageFocused={selectedFeed === feed}
isPageAdjacent={Math.abs(selectedIndex - index) === 1}
feed={feed}
renderEmptyState={renderCustomFeedEmptyState}
renderEmptyState={FeedEnd}
savedFeedConfig={savedFeedConfig}
renderEndOfFeed={FeedEnd}
/>
)
})
@@ -265,7 +256,7 @@ function HomeScreenReady({
isPageFocused
isPageAdjacent={false}
feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
renderEmptyState={renderCustomFeedEmptyState}
renderEmptyState={FeedEnd}
/>
</Pager>
)
+24 -11
View File
@@ -24,6 +24,7 @@ import {
ProfileCardFeedLoadingPlaceholder,
} from '#/view/com/util/LoadingPlaceholder'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
@@ -239,6 +240,10 @@ type ExploreScreenItems =
style?: ViewStyleProp['style']
icon: React.ComponentType<SVGIconProps>
}
| {
type: 'trendingTopics'
key: string
}
| {
type: 'profile'
key: string
@@ -325,17 +330,22 @@ export function Explore() {
])
const items = React.useMemo<ExploreScreenItems[]>(() => {
const i: ExploreScreenItems[] = [
{
type: 'header',
key: 'suggested-follows-header',
title: _(msg`Suggested accounts`),
description: _(
msg`Follow more accounts to get connected to your interests and build your network.`,
),
icon: Person,
},
]
const i: ExploreScreenItems[] = []
i.push({
type: 'trendingTopics',
key: `trending-topics`,
})
i.push({
type: 'header',
key: 'suggested-follows-header',
title: _(msg`Suggested accounts`),
description: _(
msg`Follow more accounts to get connected to your interests and build your network.`,
),
icon: Person,
})
if (profiles) {
// Currently the responses contain duplicate items.
@@ -490,6 +500,9 @@ export function Explore() {
/>
)
}
case 'trendingTopics': {
return <ExploreTrendingTopics />
}
case 'profile': {
return (
<View style={[a.border_b, t.atoms.border_contrast_low]}>
-1
View File
@@ -32,7 +32,6 @@ export function DesktopFeeds() {
return (
<View
style={[
a.flex_1,
web({
gap: 10,
/*
+11 -10
View File
@@ -8,7 +8,9 @@ import {useKawaiiMode} from '#/state/preferences/kawaii'
import {useSession} from '#/state/session'
import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
import {DesktopSearch} from '#/view/shell/desktop/Search'
import {SidebarTrendingTopics} from '#/view/shell/desktop/SidebarTrendingTopics'
import {atoms as a, useGutters, useTheme, web} from '#/alf'
import {Divider} from '#/components/Divider'
import {InlineLinkText} from '#/components/Link'
import {ProgressGuideList} from '#/components/ProgressGuide/List'
import {Text} from '#/components/Typography'
@@ -19,6 +21,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
const {hasSession, currentAccount} = useSession()
const kawaii = useKawaiiMode()
const gutters = useGutters(['base', 0, 'base', 'wide'])
const isSearchScreen = routeName === 'Search'
const {isTablet} = useWebMediaQueries()
if (isTablet) {
@@ -29,6 +32,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
<View
style={[
gutters,
a.gap_lg,
web({
position: 'fixed',
left: '50%',
@@ -43,21 +47,18 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
overflowY: 'auto',
}),
]}>
{routeName !== 'Search' && (
<View style={[a.pb_lg]}>
<DesktopSearch />
</View>
)}
{!isSearchScreen && <DesktopSearch />}
{hasSession && (
<>
<ProgressGuideList style={[a.pb_xl]} />
<View
style={[a.pb_lg, a.mb_lg, a.border_b, t.atoms.border_contrast_low]}>
<DesktopFeeds />
</View>
<ProgressGuideList />
<DesktopFeeds />
<Divider />
</>
)}
{!isSearchScreen && <SidebarTrendingTopics />}
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
{hasSession && (
<>
@@ -0,0 +1,107 @@
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {
DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import * as Prompt from '#/components/Prompt'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
export function SidebarTrendingTopics() {
const {enabled} = useTrendingConfig()
const {trendingSidebarHidden} = useTrendingSettings()
return !enabled ? null : trendingSidebarHidden ? null : <Inner />
}
function Inner() {
const t = useTheme()
const {_} = useLingui()
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingSidebarHidden} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
return error || noTopics ? null : (
<>
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Graph size="sm" />
<Text
style={[
a.flex_1,
a.text_sm,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
<Trans>Trending</Trans>
</Text>
<Button
label={_(msg`Hide trending topics from your sidebar`)}
size="tiny"
variant="ghost"
color="secondary"
shape="round"
onPress={() => trendingPrompt.open()}>
<ButtonIcon icon={X} />
</Button>
</View>
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
.fill(0)
.map((_n, i) => (
<TrendingTopicSkeleton key={i} size="small" index={i} />
))
) : !trending?.topics ? null : (
<>
{trending.topics.map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
size="small"
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
</View>
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Hide trending topics?`)}
description={_(
msg`This is a device setting, and will apply to all accounts on this device. You can update this later from your settings.`,
)}
confirmButtonCta={_(msg`Hide`)}
onConfirm={() => setTrendingSidebarHidden(true)}
/>
<Divider />
</>
)
}
+18 -42
View File
@@ -72,6 +72,20 @@
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/api@^0.13.21":
version "0.13.21"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.21.tgz#8ee27a07e5a024b5bf32408d9bd623dd598ad1cc"
integrity sha512-iOxSj2YS3Fx9IPz1NivKrSsdYPNbBgpnUH7+WhKYAMvDFDUe2PZe7taau8wsUjJAu/H3S0Mk2TDh5e/7tCRwHA==
dependencies:
"@atproto/common-web" "^0.3.1"
"@atproto/lexicon" "^0.4.4"
"@atproto/syntax" "^0.3.1"
"@atproto/xrpc" "^0.6.5"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/aws@^0.2.10":
version "0.2.10"
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4"
@@ -3247,7 +3261,7 @@
"@babel/parser" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
@@ -3308,19 +3322,6 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
dependencies:
"@babel/code-frame" "^7.25.9"
"@babel/generator" "^7.25.9"
"@babel/parser" "^7.25.9"
"@babel/template" "^7.25.9"
"@babel/types" "^7.25.9"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
@@ -17456,16 +17457,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -17565,7 +17557,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17579,13 +17571,6 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -18860,7 +18845,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -18878,15 +18863,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"