Trending (Beta) (#7144)
* Add WIP UIs for trending topics and suggested starterpacks
* Disable SPs for now
* Improve explore treatment a bit, add some polish to cards
* Add tiny option in RightNav
* Add persisted option to hide trending from sidebar
* Add to settings, abstract state, not updating in tab
* Fix up hide/show toggle state, WITH broadcast hacK
* Clean up persisted code, add new setting
* Add new interstitial to Discover
* Exploration
* First hack at mute words
* Wire up interstitial and Explore page
* Align components
* Some skeleton UI
* Handle service config, enablement, load states, update lex contract
* Centralize mute word handling
* Stale time to 30m
* Cache enabled value for reloads, use real data for service config
* Remove broadcast hack
* Remove titleChild
* Gate settings too
* Update package, rm langs
* Add feature gate
* Only english during beta period
* Hook up real data
* Tweak config
* Straight passthrough links
* Hook up prod agent
* Fix no-show logic
* Up config query to 5 min
* Remove old file
* Remove comment
* Remove stray flex_1
* Make trending setting global
* Quick placeholder state
* Limit # in sidebar, tweak spacing
* Tweak gaps
* Handle hide/show of sidebar
* Simplify messages
* Remove interstitial
* Revert "Remove interstitial"
This reverts commit 1358ad47fd.
* Only show interstitial on mobile
* Fix gap
* Add explore page recommendations
* [topics] add topic screen (#7149)
* add topic screen
* decode
* fix search query
* decode
* add server route
* Fix potential bad destructure (undefined)
---------
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
@@ -235,6 +235,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
|
|
||||||
// generic routes
|
// generic routes
|
||||||
e.GET("/hashtag/:tag", server.WebGeneric)
|
e.GET("/hashtag/:tag", server.WebGeneric)
|
||||||
|
e.GET("/topic/:topic", server.WebGeneric)
|
||||||
e.GET("/search", server.WebGeneric)
|
e.GET("/search", server.WebGeneric)
|
||||||
e.GET("/feeds", server.WebGeneric)
|
e.GET("/feeds", server.WebGeneric)
|
||||||
e.GET("/notifications", server.WebGeneric)
|
e.GET("/notifications", server.WebGeneric)
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.13.20",
|
"@atproto/api": "^0.13.21",
|
||||||
"@bitdrift/react-native": "0.4.0",
|
"@bitdrift/react-native": "0.4.0",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
|
|||||||
+9
-6
@@ -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 SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
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 {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -143,12 +144,14 @@ function InnerApp() {
|
|||||||
<BackgroundNotificationPreferencesProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<ProgressGuideProvider>
|
<ProgressGuideProvider>
|
||||||
<GestureHandlerRootView
|
<TrendingConfigProvider>
|
||||||
style={s.h100pct}>
|
<GestureHandlerRootView
|
||||||
<TestCtrls />
|
style={s.h100pct}>
|
||||||
<Shell />
|
<TestCtrls />
|
||||||
<NuxDialogs />
|
<Shell />
|
||||||
</GestureHandlerRootView>
|
<NuxDialogs />
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
</TrendingConfigProvider>
|
||||||
</ProgressGuideProvider>
|
</ProgressGuideProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
|
|||||||
+5
-2
@@ -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 SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
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 ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -127,8 +128,10 @@ function InnerApp() {
|
|||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<ProgressGuideProvider>
|
<ProgressGuideProvider>
|
||||||
<Shell />
|
<TrendingConfigProvider>
|
||||||
<NuxDialogs />
|
<Shell />
|
||||||
|
<NuxDialogs />
|
||||||
|
</TrendingConfigProvider>
|
||||||
</ProgressGuideProvider>
|
</ProgressGuideProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings'
|
|||||||
import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
|
import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
|
||||||
import {SettingsScreen} from './screens/Settings/Settings'
|
import {SettingsScreen} from './screens/Settings/Settings'
|
||||||
import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences'
|
import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences'
|
||||||
|
import TopicScreen from './screens/Topic'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
@@ -376,6 +377,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => HashtagScreen}
|
getComponent={() => HashtagScreen}
|
||||||
options={{title: title(msg`Hashtag`)}}
|
options={{title: title(msg`Hashtag`)}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="Topic"
|
||||||
|
getComponent={() => TopicScreen}
|
||||||
|
options={{title: title(msg`Topic`)}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="MessagesConversation"
|
name="MessagesConversation"
|
||||||
getComponent={() => MessagesConversationScreen}
|
getComponent={() => MessagesConversationScreen}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
|
||||||
import {atoms as a, tokens} from '#/alf'
|
import {atoms as a, tokens, ViewStyleProp} from '#/alf'
|
||||||
|
|
||||||
export function GradientFill({
|
export function GradientFill({
|
||||||
gradient,
|
gradient,
|
||||||
}: {
|
style,
|
||||||
|
}: ViewStyleProp & {
|
||||||
gradient:
|
gradient:
|
||||||
|
| typeof tokens.gradients.primary
|
||||||
| typeof tokens.gradients.sky
|
| typeof tokens.gradients.sky
|
||||||
| typeof tokens.gradients.midnight
|
| typeof tokens.gradients.midnight
|
||||||
| typeof tokens.gradients.sunrise
|
| typeof tokens.gradients.sunrise
|
||||||
@@ -26,7 +28,7 @@ export function GradientFill({
|
|||||||
}
|
}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
end={{x: 1, y: 1}}
|
end={{x: 1, y: 1}}
|
||||||
style={[a.absolute, a.inset_0]}
|
style={[a.absolute, a.inset_0, style]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
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 {trendingDisabled} = useTrendingSettings()
|
||||||
|
return enabled && !trendingDisabled ? <Inner /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Inner() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const gutters = useGutters(['wide', 'base'])
|
||||||
|
const trendingPrompt = Prompt.usePromptControl()
|
||||||
|
const {setTrendingDisabled} = 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`)}
|
||||||
|
size="tiny"
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
onPress={() => trendingPrompt.open()}>
|
||||||
|
<ButtonIcon icon={X} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.flex_wrap, {rowGap: 8, columnGap: 6}]}>
|
||||||
|
{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?`)}
|
||||||
|
description={_(msg`You can update this later from your settings.`)}
|
||||||
|
confirmButtonCta={_(msg`Hide`)}
|
||||||
|
onConfirm={() => setTrendingDisabled(true)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ export type CommonNavigatorParams = {
|
|||||||
AppIconSettings: undefined
|
AppIconSettings: undefined
|
||||||
Search: {q?: string}
|
Search: {q?: string}
|
||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
|
Topic: {topic: string}
|
||||||
MessagesConversation: {conversation: string; embed?: string}
|
MessagesConversation: {conversation: string; embed?: string}
|
||||||
MessagesSettings: undefined
|
MessagesSettings: undefined
|
||||||
NotificationSettings: undefined
|
NotificationSettings: undefined
|
||||||
@@ -92,6 +93,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
|||||||
Feeds: undefined
|
Feeds: undefined
|
||||||
Notifications: undefined
|
Notifications: undefined
|
||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
|
Topic: {topic: string}
|
||||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +107,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||||||
Notifications: undefined
|
Notifications: undefined
|
||||||
MyProfileTab: undefined
|
MyProfileTab: undefined
|
||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
|
Topic: {topic: string}
|
||||||
MessagesTab: undefined
|
MessagesTab: undefined
|
||||||
Messages: {animation?: 'push' | 'pop'}
|
Messages: {animation?: 'push' | 'pop'}
|
||||||
Start: {name: string; rkey: string}
|
Start: {name: string; rkey: string}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export type Gate =
|
|||||||
| 'debug_subscriptions'
|
| 'debug_subscriptions'
|
||||||
| 'new_postonboarding'
|
| 'new_postonboarding'
|
||||||
| 'remove_show_latest_button'
|
| 'remove_show_latest_button'
|
||||||
|
| 'trending_topics_beta'
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const router = new Router({
|
|||||||
CopyrightPolicy: '/support/copyright',
|
CopyrightPolicy: '/support/copyright',
|
||||||
// hashtags
|
// hashtags
|
||||||
Hashtag: '/hashtag/:tag',
|
Hashtag: '/hashtag/:tag',
|
||||||
|
Topic: '/topic/:topic',
|
||||||
// DMs
|
// DMs
|
||||||
Messages: '/messages',
|
Messages: '/messages',
|
||||||
MessagesSettings: '/messages/settings',
|
MessagesSettings: '/messages/settings',
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||||
|
import {
|
||||||
|
DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
|
||||||
|
useTrendingTopics,
|
||||||
|
} from '#/state/queries/trending/useTrendingTopics'
|
||||||
|
import {useTrendingConfig} from '#/state/trending-config'
|
||||||
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
|
import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag'
|
||||||
|
import {
|
||||||
|
TrendingTopic,
|
||||||
|
TrendingTopicLink,
|
||||||
|
TrendingTopicSkeleton,
|
||||||
|
} from '#/components/TrendingTopics'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function ExploreRecommendations() {
|
||||||
|
const {enabled} = useTrendingConfig()
|
||||||
|
const {trendingDisabled} = useTrendingSettings()
|
||||||
|
return enabled && !trendingDisabled ? <Inner /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inner() {
|
||||||
|
const t = useTheme()
|
||||||
|
const gutters = useGutters([0, 'compact'])
|
||||||
|
const {data: trending, error, isLoading} = useTrendingTopics()
|
||||||
|
const noRecs = !isLoading && !error && !trending?.suggested?.length
|
||||||
|
|
||||||
|
return error || noRecs ? 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]}>
|
||||||
|
<Hashtag_Stroke2_Corner0_Rounded
|
||||||
|
size="lg"
|
||||||
|
fill={t.palette.primary_500}
|
||||||
|
style={{marginLeft: -2}}
|
||||||
|
/>
|
||||||
|
<Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
|
||||||
|
<Trans>Recommended</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||||
|
<Trans>Feeds we think you might like.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pt_md, a.pb_lg]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_start,
|
||||||
|
a.flex_wrap,
|
||||||
|
{rowGap: 8, columnGap: 6},
|
||||||
|
gutters,
|
||||||
|
]}>
|
||||||
|
{isLoading ? (
|
||||||
|
Array(RECOMMENDATIONS_COUNT)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => <TrendingTopicSkeleton key={i} index={i} />)
|
||||||
|
) : !trending?.suggested ? null : (
|
||||||
|
<>
|
||||||
|
{trending.suggested.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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useTrendingSettings} 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 {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()
|
||||||
|
const {trendingDisabled} = useTrendingSettings()
|
||||||
|
return enabled && !trendingDisabled ? <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.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pt_md, a.pb_lg]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_start,
|
||||||
|
a.flex_wrap,
|
||||||
|
{rowGap: 8, columnGap: 6},
|
||||||
|
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,
|
useInAppBrowser,
|
||||||
useSetInAppBrowser,
|
useSetInAppBrowser,
|
||||||
} from '#/state/preferences/in-app-browser'
|
} 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 SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
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 {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
|
||||||
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
||||||
import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
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 {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
@@ -29,6 +35,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
|||||||
const setAutoplayDisabledPref = useSetAutoplayDisabled()
|
const setAutoplayDisabledPref = useSetAutoplayDisabled()
|
||||||
const inAppBrowserPref = useInAppBrowser()
|
const inAppBrowserPref = useInAppBrowser()
|
||||||
const setUseInAppBrowser = useSetInAppBrowser()
|
const setUseInAppBrowser = useSetInAppBrowser()
|
||||||
|
const {enabled: trendingEnabled} = useTrendingConfig()
|
||||||
|
const {trendingDisabled} = useTrendingSettings()
|
||||||
|
const {setTrendingDisabled} = useTrendingSettingsApi()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -104,6 +113,24 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
|||||||
<Toggle.Platform />
|
<Toggle.Platform />
|
||||||
</SettingsList.Item>
|
</SettingsList.Item>
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
|
{trendingEnabled && (
|
||||||
|
<>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<Toggle.Item
|
||||||
|
name="show_trending_topics"
|
||||||
|
label={_(msg`Enable trending topics`)}
|
||||||
|
value={!trendingDisabled}
|
||||||
|
onChange={value => setTrendingDisabled(!value)}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<SettingsList.ItemIcon icon={Graph} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Enable trending topics</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Toggle.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SettingsList.Container>
|
</SettingsList.Container>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {ListRenderItemInfo, View} from 'react-native'
|
||||||
|
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
|
import {shareUrl} from '#/lib/sharing'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {enforceLen} from '#/lib/strings/helpers'
|
||||||
|
import {useSearchPostsQuery} from '#/state/queries/search-posts'
|
||||||
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
import {Pager} from '#/view/com/pager/Pager'
|
||||||
|
import {TabBar} from '#/view/com/pager/TabBar'
|
||||||
|
import {Post} from '#/view/com/post/Post'
|
||||||
|
import {List} from '#/view/com/util/List'
|
||||||
|
import {atoms as a, web} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||||
|
|
||||||
|
const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
|
||||||
|
return <Post post={item} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyExtractor = (item: PostView, index: number) => {
|
||||||
|
return `${item.uri}-${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TopicScreen({
|
||||||
|
route,
|
||||||
|
}: NativeStackScreenProps<CommonNavigatorParams, 'Topic'>) {
|
||||||
|
const {topic} = route.params
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const headerTitle = React.useMemo(() => {
|
||||||
|
return enforceLen(decodeURIComponent(topic), 24, true, 'middle')
|
||||||
|
}, [topic])
|
||||||
|
|
||||||
|
const onShare = React.useCallback(() => {
|
||||||
|
const url = new URL('https://bsky.app')
|
||||||
|
url.pathname = `/topic/${topic}`
|
||||||
|
shareUrl(url.toString())
|
||||||
|
}, [topic])
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = React.useState(0)
|
||||||
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
React.useCallback(() => {
|
||||||
|
setMinimalShellMode(false)
|
||||||
|
}, [setMinimalShellMode]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPageSelected = React.useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
setMinimalShellMode(false)
|
||||||
|
setActiveTab(index)
|
||||||
|
},
|
||||||
|
[setMinimalShellMode],
|
||||||
|
)
|
||||||
|
|
||||||
|
const sections = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: _(msg`Top`),
|
||||||
|
component: (
|
||||||
|
<TopicScreenTab topic={topic} sort="top" active={activeTab === 0} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: _(msg`Latest`),
|
||||||
|
component: (
|
||||||
|
<TopicScreenTab
|
||||||
|
topic={topic}
|
||||||
|
sort="latest"
|
||||||
|
active={activeTab === 1}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [_, topic, activeTab])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen>
|
||||||
|
<Layout.Header.Outer noBottomBorder>
|
||||||
|
<Layout.Header.BackButton />
|
||||||
|
<Layout.Header.Content>
|
||||||
|
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
|
||||||
|
</Layout.Header.Content>
|
||||||
|
<Layout.Header.Slot>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Share`)}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="primary"
|
||||||
|
shape="round"
|
||||||
|
onPress={onShare}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
style={[{right: -3}]}>
|
||||||
|
<ButtonIcon icon={Share} size="md" />
|
||||||
|
</Button>
|
||||||
|
</Layout.Header.Slot>
|
||||||
|
</Layout.Header.Outer>
|
||||||
|
<Pager
|
||||||
|
onPageSelected={onPageSelected}
|
||||||
|
renderTabBar={props => (
|
||||||
|
<Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}>
|
||||||
|
<TabBar items={sections.map(section => section.title)} {...props} />
|
||||||
|
</Layout.Center>
|
||||||
|
)}
|
||||||
|
initialPage={0}>
|
||||||
|
{sections.map((section, i) => (
|
||||||
|
<View key={i}>{section.component}</View>
|
||||||
|
))}
|
||||||
|
</Pager>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopicScreenTab({
|
||||||
|
topic,
|
||||||
|
sort,
|
||||||
|
active,
|
||||||
|
}: {
|
||||||
|
topic: string
|
||||||
|
sort: 'top' | 'latest'
|
||||||
|
active: boolean
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const initialNumToRender = useInitialNumToRender()
|
||||||
|
const [isPTR, setIsPTR] = React.useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isFetched,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
} = useSearchPostsQuery({
|
||||||
|
query: decodeURIComponent(topic),
|
||||||
|
sort,
|
||||||
|
enabled: active,
|
||||||
|
})
|
||||||
|
|
||||||
|
const posts = React.useMemo(() => {
|
||||||
|
return data?.pages.flatMap(page => page.posts) || []
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
setIsPTR(true)
|
||||||
|
await refetch()
|
||||||
|
setIsPTR(false)
|
||||||
|
}, [refetch])
|
||||||
|
|
||||||
|
const onEndReached = React.useCallback(() => {
|
||||||
|
if (isFetchingNextPage || !hasNextPage || error) return
|
||||||
|
fetchNextPage()
|
||||||
|
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{posts.length < 1 ? (
|
||||||
|
<ListMaybePlaceholder
|
||||||
|
isLoading={isLoading || !isFetched}
|
||||||
|
isError={isError}
|
||||||
|
onRetry={refetch}
|
||||||
|
emptyType="results"
|
||||||
|
emptyMessage={_(msg`We couldn't find any results for that topic.`)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<List
|
||||||
|
data={posts}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
refreshing={isPTR}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
onEndReachedThreshold={4}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
desktopFixedHeight
|
||||||
|
ListFooterComponent={
|
||||||
|
<ListFooter
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
error={cleanError(error)}
|
||||||
|
onRetry={fetchNextPage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
initialNumToRender={initialNumToRender}
|
||||||
|
windowSize={11}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -125,6 +125,7 @@ const schema = z.object({
|
|||||||
subtitlesEnabled: z.boolean().optional(),
|
subtitlesEnabled: z.boolean().optional(),
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
mutedThreads: z.array(z.string()),
|
mutedThreads: z.array(z.string()),
|
||||||
|
trendingDisabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
export type Schema = z.infer<typeof schema>
|
export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
@@ -170,6 +171,7 @@ export const defaults: Schema = {
|
|||||||
kawaii: false,
|
kawaii: false,
|
||||||
hasCheckedForStarterPack: false,
|
hasCheckedForStarterPack: false,
|
||||||
subtitlesEnabled: true,
|
subtitlesEnabled: true,
|
||||||
|
trendingDisabled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryParse(rawData: string): Schema | undefined {
|
export function tryParse(rawData: string): Schema | undefined {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {Provider as KawaiiProvider} from './kawaii'
|
|||||||
import {Provider as LanguagesProvider} from './languages'
|
import {Provider as LanguagesProvider} from './languages'
|
||||||
import {Provider as LargeAltBadgeProvider} from './large-alt-badge'
|
import {Provider as LargeAltBadgeProvider} from './large-alt-badge'
|
||||||
import {Provider as SubtitlesProvider} from './subtitles'
|
import {Provider as SubtitlesProvider} from './subtitles'
|
||||||
|
import {Provider as TrendingSettingsProvider} from './trending'
|
||||||
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
|
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
<AutoplayProvider>
|
<AutoplayProvider>
|
||||||
<UsedStarterPacksProvider>
|
<UsedStarterPacksProvider>
|
||||||
<SubtitlesProvider>
|
<SubtitlesProvider>
|
||||||
<KawaiiProvider>{children}</KawaiiProvider>
|
<TrendingSettingsProvider>
|
||||||
|
<KawaiiProvider>{children}</KawaiiProvider>
|
||||||
|
</TrendingSettingsProvider>
|
||||||
</SubtitlesProvider>
|
</SubtitlesProvider>
|
||||||
</UsedStarterPacksProvider>
|
</UsedStarterPacksProvider>
|
||||||
</AutoplayProvider>
|
</AutoplayProvider>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
|
type StateContext = {
|
||||||
|
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
|
||||||
|
}
|
||||||
|
type ApiContext = {
|
||||||
|
setTrendingDisabled(
|
||||||
|
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
|
||||||
|
): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const StateContext = React.createContext<StateContext>({
|
||||||
|
trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
|
||||||
|
})
|
||||||
|
const ApiContext = React.createContext<ApiContext>({
|
||||||
|
setTrendingDisabled() {},
|
||||||
|
})
|
||||||
|
|
||||||
|
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 [trendingDisabled, setTrendingDisabled] =
|
||||||
|
usePersistedBooleanValue('trendingDisabled')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Context
|
||||||
|
*/
|
||||||
|
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled])
|
||||||
|
const api = React.useMemo(
|
||||||
|
() => ({setTrendingDisabled}),
|
||||||
|
[setTrendingDisabled],
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export const STALE = {
|
|||||||
MINUTES: {
|
MINUTES: {
|
||||||
ONE: 1e3 * 60,
|
ONE: 1e3 * 60,
|
||||||
FIVE: 1e3 * 60 * 5,
|
FIVE: 1e3 * 60 * 5,
|
||||||
|
THIRTY: 1e3 * 60 * 30,
|
||||||
},
|
},
|
||||||
HOURS: {
|
HOURS: {
|
||||||
ONE: 1e3 * 60 * 60,
|
ONE: 1e3 * 60 * 60,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ export type Device = {
|
|||||||
geolocation?: {
|
geolocation?: {
|
||||||
countryCode: string | undefined
|
countryCode: string | undefined
|
||||||
}
|
}
|
||||||
|
trendingBetaEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {logger} from '#/logger'
|
|||||||
import {isIOS, isWeb} from '#/platform/detection'
|
import {isIOS, isWeb} from '#/platform/detection'
|
||||||
import {listenPostCreated} from '#/state/events'
|
import {listenPostCreated} from '#/state/events'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
|
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {
|
import {
|
||||||
FeedDescriptor,
|
FeedDescriptor,
|
||||||
@@ -34,7 +35,9 @@ import {
|
|||||||
} from '#/state/queries/post-feed'
|
} from '#/state/queries/post-feed'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
|
import {useBreakpoints} from '#/alf'
|
||||||
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
|
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
|
||||||
|
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||||
import {List, ListRef} from '../util/List'
|
import {List, ListRef} from '../util/List'
|
||||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
@@ -90,6 +93,10 @@ type FeedRow =
|
|||||||
type: 'interstitialProgressGuide'
|
type: 'interstitialProgressGuide'
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'interstitialTrending'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
|
export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
|
||||||
if (feedRow.type === 'sliceItem') {
|
if (feedRow.type === 'sliceItem') {
|
||||||
@@ -156,6 +163,7 @@ let PostFeed = ({
|
|||||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||||
const lastFetchRef = React.useRef<number>(Date.now())
|
const lastFetchRef = React.useRef<number>(Date.now())
|
||||||
const [feedType, feedUri, feedTab] = feed.split('|')
|
const [feedType, feedUri, feedTab] = feed.split('|')
|
||||||
|
const {gtTablet} = useBreakpoints()
|
||||||
|
|
||||||
const opts = React.useMemo(
|
const opts = React.useMemo(
|
||||||
() => ({enabled, ignoreFilterFor}),
|
() => ({enabled, ignoreFilterFor}),
|
||||||
@@ -259,6 +267,8 @@ let PostFeed = ({
|
|||||||
const showProgressIntersitial =
|
const showProgressIntersitial =
|
||||||
(followProgressGuide || followAndLikeProgressGuide) && !isDesktop
|
(followProgressGuide || followAndLikeProgressGuide) && !isDesktop
|
||||||
|
|
||||||
|
const {trendingDisabled} = useTrendingSettings()
|
||||||
|
|
||||||
const feedItems: FeedRow[] = React.useMemo(() => {
|
const feedItems: FeedRow[] = React.useMemo(() => {
|
||||||
let feedKind: 'following' | 'discover' | 'profile' | undefined
|
let feedKind: 'following' | 'discover' | 'profile' | undefined
|
||||||
if (feedType === 'following') {
|
if (feedType === 'following') {
|
||||||
@@ -304,7 +314,16 @@ let PostFeed = ({
|
|||||||
type: 'interstitialProgressGuide',
|
type: 'interstitialProgressGuide',
|
||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
})
|
})
|
||||||
} else if (sliceIndex === 20) {
|
} else if (
|
||||||
|
sliceIndex === 15 &&
|
||||||
|
!gtTablet &&
|
||||||
|
!trendingDisabled
|
||||||
|
) {
|
||||||
|
arr.push({
|
||||||
|
type: 'interstitialTrending',
|
||||||
|
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
|
})
|
||||||
|
} else if (sliceIndex === 30) {
|
||||||
arr.push({
|
arr.push({
|
||||||
type: 'interstitialFollows',
|
type: 'interstitialFollows',
|
||||||
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
|
||||||
@@ -390,6 +409,8 @@ let PostFeed = ({
|
|||||||
feedTab,
|
feedTab,
|
||||||
hasSession,
|
hasSession,
|
||||||
showProgressIntersitial,
|
showProgressIntersitial,
|
||||||
|
trendingDisabled,
|
||||||
|
gtTablet,
|
||||||
])
|
])
|
||||||
|
|
||||||
// events
|
// events
|
||||||
@@ -476,6 +497,8 @@ let PostFeed = ({
|
|||||||
return <SuggestedFollows feed={feed} />
|
return <SuggestedFollows feed={feed} />
|
||||||
} else if (row.type === 'interstitialProgressGuide') {
|
} else if (row.type === 'interstitialProgressGuide') {
|
||||||
return <ProgressGuide />
|
return <ProgressGuide />
|
||||||
|
} else if (row.type === 'interstitialTrending') {
|
||||||
|
return <TrendingInterstitial />
|
||||||
} else if (row.type === 'sliceItem') {
|
} else if (row.type === 'sliceItem') {
|
||||||
const slice = row.slice
|
const slice = row.slice
|
||||||
if (slice.isFallbackMarker) {
|
if (slice.isFallbackMarker) {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import {
|
|||||||
ProfileCardFeedLoadingPlaceholder,
|
ProfileCardFeedLoadingPlaceholder,
|
||||||
} from '#/view/com/util/LoadingPlaceholder'
|
} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
|
||||||
|
import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
|
||||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
@@ -239,6 +241,14 @@ type ExploreScreenItems =
|
|||||||
style?: ViewStyleProp['style']
|
style?: ViewStyleProp['style']
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'trendingTopics'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'recommendations'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'profile'
|
type: 'profile'
|
||||||
key: string
|
key: string
|
||||||
@@ -325,17 +335,27 @@ export function Explore() {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const items = React.useMemo<ExploreScreenItems[]>(() => {
|
const items = React.useMemo<ExploreScreenItems[]>(() => {
|
||||||
const i: ExploreScreenItems[] = [
|
const i: ExploreScreenItems[] = []
|
||||||
{
|
|
||||||
type: 'header',
|
i.push({
|
||||||
key: 'suggested-follows-header',
|
type: 'trendingTopics',
|
||||||
title: _(msg`Suggested accounts`),
|
key: `trending-topics`,
|
||||||
description: _(
|
})
|
||||||
msg`Follow more accounts to get connected to your interests and build your network.`,
|
|
||||||
),
|
i.push({
|
||||||
icon: Person,
|
type: 'recommendations',
|
||||||
},
|
key: `recommendations`,
|
||||||
]
|
})
|
||||||
|
|
||||||
|
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) {
|
if (profiles) {
|
||||||
// Currently the responses contain duplicate items.
|
// Currently the responses contain duplicate items.
|
||||||
@@ -490,6 +510,12 @@ export function Explore() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case 'trendingTopics': {
|
||||||
|
return <ExploreTrendingTopics />
|
||||||
|
}
|
||||||
|
case 'recommendations': {
|
||||||
|
return <ExploreRecommendations />
|
||||||
|
}
|
||||||
case 'profile': {
|
case 'profile': {
|
||||||
return (
|
return (
|
||||||
<View style={[a.border_b, t.atoms.border_contrast_low]}>
|
<View style={[a.border_b, t.atoms.border_contrast_low]}>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {createStaticClick, InlineLinkText} from '#/components/Link'
|
|||||||
export function DesktopFeeds() {
|
export function DesktopFeeds() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
|
const {data: pinnedFeedInfos, error, isLoading} = usePinnedFeedsInfos()
|
||||||
const selectedFeed = useSelectedFeed()
|
const selectedFeed = useSelectedFeed()
|
||||||
const setSelectedFeed = useSetSelectedFeed()
|
const setSelectedFeed = useSetSelectedFeed()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
@@ -25,14 +25,40 @@ export function DesktopFeeds() {
|
|||||||
return getCurrentRoute(state)
|
return getCurrentRoute(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!pinnedFeedInfos) {
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{Array(5)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{
|
||||||
|
height: 16,
|
||||||
|
width: i % 2 === 0 ? '60%' : '80%',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !pinnedFeedInfos) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
|
||||||
web({
|
web({
|
||||||
gap: 10,
|
gap: 10,
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useNavigation} from '@react-navigation/core'
|
||||||
|
|
||||||
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
|
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
@@ -8,17 +10,41 @@ import {useKawaiiMode} from '#/state/preferences/kawaii'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
|
import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
|
||||||
import {DesktopSearch} from '#/view/shell/desktop/Search'
|
import {DesktopSearch} from '#/view/shell/desktop/Search'
|
||||||
|
import {SidebarTrendingTopics} from '#/view/shell/desktop/SidebarTrendingTopics'
|
||||||
import {atoms as a, useGutters, useTheme, web} from '#/alf'
|
import {atoms as a, useGutters, useTheme, web} from '#/alf'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
function useWebQueryParams() {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const [params, setParams] = React.useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return navigation.addListener('state', e => {
|
||||||
|
try {
|
||||||
|
const {state} = e.data
|
||||||
|
const lastRoute = state.routes[state.routes.length - 1]
|
||||||
|
const {params} = lastRoute
|
||||||
|
setParams(params)
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
}, [navigation, setParams])
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const kawaii = useKawaiiMode()
|
const kawaii = useKawaiiMode()
|
||||||
const gutters = useGutters(['base', 0, 'base', 'wide'])
|
const gutters = useGutters(['base', 0, 'base', 'wide'])
|
||||||
|
const isSearchScreen = routeName === 'Search'
|
||||||
|
const webqueryParams = useWebQueryParams()
|
||||||
|
const searchQuery = webqueryParams?.q
|
||||||
|
const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery)
|
||||||
|
|
||||||
const {isTablet} = useWebMediaQueries()
|
const {isTablet} = useWebMediaQueries()
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
@@ -29,6 +55,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
gutters,
|
gutters,
|
||||||
|
a.gap_lg,
|
||||||
web({
|
web({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
@@ -43,21 +70,18 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}),
|
}),
|
||||||
]}>
|
]}>
|
||||||
{routeName !== 'Search' && (
|
{!isSearchScreen && <DesktopSearch />}
|
||||||
<View style={[a.pb_lg]}>
|
|
||||||
<DesktopSearch />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
<>
|
<>
|
||||||
<ProgressGuideList style={[a.pb_xl]} />
|
<ProgressGuideList />
|
||||||
<View
|
<DesktopFeeds />
|
||||||
style={[a.pb_lg, a.mb_lg, a.border_b, t.atoms.border_contrast_low]}>
|
<Divider />
|
||||||
<DesktopFeeds />
|
|
||||||
</View>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showTrending && <SidebarTrendingTopics />}
|
||||||
|
|
||||||
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
|
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useTrendingSettings,
|
||||||
|
useTrendingSettingsApi,
|
||||||
|
} from '#/state/preferences/trending'
|
||||||
|
import {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'
|
||||||
|
|
||||||
|
const TRENDING_LIMIT = 6
|
||||||
|
|
||||||
|
export function SidebarTrendingTopics() {
|
||||||
|
const {enabled} = useTrendingConfig()
|
||||||
|
const {trendingDisabled} = useTrendingSettings()
|
||||||
|
return !enabled ? null : trendingDisabled ? null : <Inner />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inner() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const trendingPrompt = Prompt.usePromptControl()
|
||||||
|
const {setTrendingDisabled} = useTrendingSettingsApi()
|
||||||
|
const {data: trending, error, isLoading} = useTrendingTopics()
|
||||||
|
const noTopics = !isLoading && !error && !trending?.topics?.length
|
||||||
|
|
||||||
|
return error || noTopics ? null : (
|
||||||
|
<>
|
||||||
|
<View style={[a.gap_sm, {paddingBottom: 2}]}>
|
||||||
|
<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`)}
|
||||||
|
size="tiny"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
onPress={() => trendingPrompt.open()}>
|
||||||
|
<ButtonIcon icon={X} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.flex_wrap, {gap: '6px 4px'}]}>
|
||||||
|
{isLoading ? (
|
||||||
|
Array(TRENDING_LIMIT)
|
||||||
|
.fill(0)
|
||||||
|
.map((_n, i) => (
|
||||||
|
<TrendingTopicSkeleton key={i} size="small" index={i} />
|
||||||
|
))
|
||||||
|
) : !trending?.topics ? null : (
|
||||||
|
<>
|
||||||
|
{trending.topics.slice(0, TRENDING_LIMIT).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`You can update this later from your settings.`)}
|
||||||
|
confirmButtonCta={_(msg`Hide`)}
|
||||||
|
onConfirm={() => setTrendingDisabled(true)}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -72,6 +72,20 @@
|
|||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
zod "^3.23.8"
|
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":
|
"@atproto/aws@^0.2.10":
|
||||||
version "0.2.10"
|
version "0.2.10"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4"
|
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4"
|
||||||
@@ -3247,7 +3261,7 @@
|
|||||||
"@babel/parser" "^7.25.9"
|
"@babel/parser" "^7.25.9"
|
||||||
"@babel/types" "^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"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
||||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
||||||
@@ -3308,19 +3322,6 @@
|
|||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
globals "^11.1.0"
|
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":
|
"@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"
|
version "7.22.10"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
|
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"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"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==
|
|
||||||
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:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -17565,7 +17557,7 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
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"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@@ -17579,13 +17571,6 @@ strip-ansi@^5.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^4.1.0"
|
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:
|
strip-ansi@^7.0.1:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
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"
|
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
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"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@@ -18878,15 +18863,6 @@ wrap-ansi@^6.2.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.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:
|
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|||||||
Reference in New Issue
Block a user