Merge remote-tracking branch 'origin/main' into explore/tabs
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||||
|
|
||||||
|
export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
let opacity: number
|
||||||
|
switch (t.name) {
|
||||||
|
case 'dark':
|
||||||
|
opacity = 0.4
|
||||||
|
break
|
||||||
|
case 'dim':
|
||||||
|
opacity = 0.45
|
||||||
|
break
|
||||||
|
case 'light':
|
||||||
|
opacity = 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
a.pointer_events_none,
|
||||||
|
a.transition_opacity,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
style,
|
||||||
|
{opacity: hover ? opacity : 0},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -330,7 +330,6 @@ export type MetricEvents = {
|
|||||||
| 'suggestedFeeds'
|
| 'suggestedFeeds'
|
||||||
| 'suggestedStarterPacks'
|
| 'suggestedStarterPacks'
|
||||||
| `feed:${FeedDescriptor}`
|
| `feed:${FeedDescriptor}`
|
||||||
index: number
|
|
||||||
}
|
}
|
||||||
'explore:module:searchButtonPress': {
|
'explore:module:searchButtonPress': {
|
||||||
module: 'suggestedAccounts' | 'suggestedFeeds'
|
module: 'suggestedAccounts' | 'suggestedFeeds'
|
||||||
|
|||||||
+110
-39
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
@@ -22,9 +23,19 @@ import {
|
|||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||||
import {Nux, useNux} from '#/state/queries/nuxs'
|
import {Nux, useNux} from '#/state/queries/nuxs'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
import {
|
||||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
createGetSuggestedFeedsQueryKey,
|
||||||
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
|
useGetSuggestedFeedsQuery,
|
||||||
|
} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||||
|
import {
|
||||||
|
getSuggestedUsersQueryKeyRoot,
|
||||||
|
useGetSuggestedUsersQuery,
|
||||||
|
} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||||
|
import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
|
||||||
|
import {
|
||||||
|
createSuggestedStarterPacksQueryKey,
|
||||||
|
useSuggestedStarterPacksQuery,
|
||||||
|
} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
|
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
|
||||||
import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
|
import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
|
||||||
@@ -41,6 +52,7 @@ import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommenda
|
|||||||
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
|
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
|
||||||
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
|
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
|
||||||
import {atoms as a, native, platform, useTheme, web} from '#/alf'
|
import {atoms as a, native, platform, useTheme, web} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
|
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
|
||||||
@@ -49,9 +61,11 @@ import {type Props as IcoProps} from '#/components/icons/common'
|
|||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
|
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
|
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
|
||||||
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
|
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as ModuleHeader from './components/ModuleHeader'
|
import * as ModuleHeader from './components/ModuleHeader'
|
||||||
import {
|
import {
|
||||||
@@ -69,33 +83,26 @@ function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
|
|||||||
onPress={item.onLoadMore}
|
onPress={item.onLoadMore}
|
||||||
style={[a.relative, a.w_full]}>
|
style={[a.relative, a.w_full]}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<View
|
<>
|
||||||
style={[
|
<SubtleHover hover={hovered || pressed} />
|
||||||
a.flex_1,
|
<View
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
a.px_lg,
|
|
||||||
a.py_md,
|
|
||||||
a.gap_sm,
|
|
||||||
(hovered || pressed) && t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
style={[
|
||||||
a.leading_snug,
|
a.flex_1,
|
||||||
hovered ? t.atoms.text : t.atoms.text_contrast_medium,
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_md,
|
||||||
|
a.gap_sm,
|
||||||
]}>
|
]}>
|
||||||
{item.message}
|
<Text style={[a.leading_snug]}>{item.message}</Text>
|
||||||
</Text>
|
{item.isLoadingMore ? (
|
||||||
{item.isLoadingMore ? (
|
<Loader size="sm" />
|
||||||
<Loader size="sm" />
|
) : (
|
||||||
) : (
|
<ChevronDownIcon size="sm" style={t.atoms.text_contrast_medium} />
|
||||||
<ChevronDownIcon
|
)}
|
||||||
size="sm"
|
</View>
|
||||||
style={hovered ? t.atoms.text : t.atoms.text_contrast_medium}
|
</>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@@ -112,6 +119,7 @@ type ExploreScreenItems =
|
|||||||
title: string
|
title: string
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
iconSize?: IcoProps['size']
|
iconSize?: IcoProps['size']
|
||||||
|
bottomBorder?: boolean
|
||||||
searchButton?: {
|
searchButton?: {
|
||||||
label: string
|
label: string
|
||||||
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
|
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
|
||||||
@@ -147,6 +155,10 @@ type ExploreScreenItems =
|
|||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
recId?: number
|
recId?: number
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'profileEmpty'
|
||||||
|
key: 'profileEmpty'
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'feed'
|
type: 'feed'
|
||||||
key: string
|
key: string
|
||||||
@@ -203,11 +215,12 @@ export function Explore({
|
|||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
const guide = useProgressGuide('follow-10')
|
const guide = useProgressGuide('follow-10')
|
||||||
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
||||||
// TODO always get at least 10 back
|
// TODO always get at least 10 back TODO still
|
||||||
const {
|
const {
|
||||||
data: suggestedUsers,
|
data: suggestedUsers,
|
||||||
isLoading: suggestedUsersIsLoading,
|
isLoading: suggestedUsersIsLoading,
|
||||||
error: suggestedUsersError,
|
error: suggestedUsersError,
|
||||||
|
isRefetching: suggestedUsersIsRefetching,
|
||||||
} = useGetSuggestedUsersQuery({
|
} = useGetSuggestedUsersQuery({
|
||||||
category: selectedInterest,
|
category: selectedInterest,
|
||||||
})
|
})
|
||||||
@@ -227,6 +240,7 @@ export function Explore({
|
|||||||
data: suggestedSPs,
|
data: suggestedSPs,
|
||||||
isLoading: isLoadingSuggestedSPs,
|
isLoading: isLoadingSuggestedSPs,
|
||||||
error: suggestedSPsError,
|
error: suggestedSPsError,
|
||||||
|
isRefetching: isRefetchingSuggestedSPs,
|
||||||
} = useSuggestedStarterPacksQuery()
|
} = useSuggestedStarterPacksQuery()
|
||||||
|
|
||||||
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
|
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
|
||||||
@@ -262,6 +276,27 @@ export function Explore({
|
|||||||
},
|
},
|
||||||
} = useFeedPreviews(suggestedFeeds?.feeds ?? [])
|
} = useFeedPreviews(suggestedFeeds?.feeds ?? [])
|
||||||
|
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const [isPTR, setIsPTR] = useState(false)
|
||||||
|
const onPTR = useCallback(async () => {
|
||||||
|
setIsPTR(true)
|
||||||
|
await Promise.all([
|
||||||
|
await qc.resetQueries({
|
||||||
|
queryKey: createGetTrendsQueryKey(),
|
||||||
|
}),
|
||||||
|
await qc.resetQueries({
|
||||||
|
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||||
|
}),
|
||||||
|
await qc.resetQueries({
|
||||||
|
queryKey: [getSuggestedUsersQueryKeyRoot],
|
||||||
|
}),
|
||||||
|
await qc.resetQueries({
|
||||||
|
queryKey: createGetSuggestedFeedsQueryKey(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
setIsPTR(false)
|
||||||
|
}, [qc, setIsPTR])
|
||||||
|
|
||||||
const onLoadMoreFeedPreviews = useCallback(async () => {
|
const onLoadMoreFeedPreviews = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
isPendingFeedPreviews ||
|
isPendingFeedPreviews ||
|
||||||
@@ -305,7 +340,7 @@ export function Explore({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (suggestedUsersIsLoading) {
|
if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
|
||||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||||
} else if (suggestedUsersError) {
|
} else if (suggestedUsersError) {
|
||||||
i.push({
|
i.push({
|
||||||
@@ -333,14 +368,18 @@ export function Explore({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (profileItems.length === 0) {
|
if (profileItems.length === 0) {
|
||||||
// no items! remove the header
|
i.push({
|
||||||
i.pop()
|
type: 'profileEmpty',
|
||||||
|
key: 'profileEmpty',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
i.push(...profileItems)
|
i.push(...profileItems)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no items! remove the header
|
i.push({
|
||||||
i.pop()
|
type: 'profileEmpty',
|
||||||
|
key: 'profileEmpty',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
|
||||||
@@ -352,6 +391,7 @@ export function Explore({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
suggestedUsers,
|
suggestedUsers,
|
||||||
suggestedUsersIsLoading,
|
suggestedUsersIsLoading,
|
||||||
|
suggestedUsersIsRefetching,
|
||||||
suggestedUsersError,
|
suggestedUsersError,
|
||||||
])
|
])
|
||||||
const suggestedFeedsModule = useMemo(() => {
|
const suggestedFeedsModule = useMemo(() => {
|
||||||
@@ -466,7 +506,7 @@ export function Explore({
|
|||||||
iconSize: 'xl',
|
iconSize: 'xl',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isLoadingSuggestedSPs) {
|
if (isLoadingSuggestedSPs || isRefetchingSuggestedSPs) {
|
||||||
Array.from({length: 3}).forEach((__, index) =>
|
Array.from({length: 3}).forEach((__, index) =>
|
||||||
i.push({
|
i.push({
|
||||||
type: 'starterPackSkeleton',
|
type: 'starterPackSkeleton',
|
||||||
@@ -486,7 +526,13 @@ export function Explore({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return i
|
return i
|
||||||
}, [suggestedSPs, _, isLoadingSuggestedSPs, suggestedSPsError])
|
}, [
|
||||||
|
suggestedSPs,
|
||||||
|
_,
|
||||||
|
isLoadingSuggestedSPs,
|
||||||
|
suggestedSPsError,
|
||||||
|
isRefetchingSuggestedSPs,
|
||||||
|
])
|
||||||
const feedPreviewsModule = useMemo(() => {
|
const feedPreviewsModule = useMemo(() => {
|
||||||
const i: ExploreScreenItems[] = []
|
const i: ExploreScreenItems[] = []
|
||||||
i.push(...feedPreviewSlices)
|
i.push(...feedPreviewSlices)
|
||||||
@@ -520,6 +566,13 @@ export function Explore({
|
|||||||
if (isNewUser) {
|
if (isNewUser) {
|
||||||
i.push(...suggestedFollowsModule)
|
i.push(...suggestedFollowsModule)
|
||||||
i.push(...suggestedStarterPacksModule)
|
i.push(...suggestedStarterPacksModule)
|
||||||
|
i.push({
|
||||||
|
type: 'header',
|
||||||
|
key: 'trending-topics-header',
|
||||||
|
title: _(msg`Trending topics`),
|
||||||
|
icon: Graph,
|
||||||
|
bottomBorder: true,
|
||||||
|
})
|
||||||
i.push(trendingTopicsModule)
|
i.push(trendingTopicsModule)
|
||||||
} else {
|
} else {
|
||||||
i.push(trendingTopicsModule)
|
i.push(trendingTopicsModule)
|
||||||
@@ -533,6 +586,7 @@ export function Explore({
|
|||||||
|
|
||||||
return i
|
return i
|
||||||
}, [
|
}, [
|
||||||
|
_,
|
||||||
topBorder,
|
topBorder,
|
||||||
isNewUser,
|
isNewUser,
|
||||||
suggestedFollowsModule,
|
suggestedFollowsModule,
|
||||||
@@ -564,7 +618,7 @@ export function Explore({
|
|||||||
)
|
)
|
||||||
case 'header': {
|
case 'header': {
|
||||||
return (
|
return (
|
||||||
<ModuleHeader.Container>
|
<ModuleHeader.Container bottomBorder={item.bottomBorder}>
|
||||||
<ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
|
<ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
|
||||||
<ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
|
<ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
|
||||||
{item.searchButton && (
|
{item.searchButton && (
|
||||||
@@ -623,6 +677,15 @@ export function Explore({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case 'profileEmpty': {
|
||||||
|
return (
|
||||||
|
<View style={[a.px_lg, a.pb_lg]}>
|
||||||
|
<Admonition>
|
||||||
|
<Trans>No results for "{selectedInterest}".</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
case 'feed': {
|
case 'feed': {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -738,7 +801,13 @@ export function Explore({
|
|||||||
return (
|
return (
|
||||||
<ModuleHeader.Container
|
<ModuleHeader.Container
|
||||||
headerHeight={headerHeight}
|
headerHeight={headerHeight}
|
||||||
style={[a.pt_xs, a.border_b, t.atoms.border_contrast_low]}>
|
style={[
|
||||||
|
a.pt_xs,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
native(a.border_b),
|
||||||
|
]}>
|
||||||
|
{/* Very non-scientific way to avoid small gap on scroll */}
|
||||||
|
<View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
|
||||||
<ModuleHeader.FeedLink feed={item.feed}>
|
<ModuleHeader.FeedLink feed={item.feed}>
|
||||||
<ModuleHeader.FeedAvatar feed={item.feed} />
|
<ModuleHeader.FeedAvatar feed={item.feed} />
|
||||||
<View style={[a.flex_1, a.gap_xs]}>
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
@@ -854,7 +923,7 @@ export function Explore({
|
|||||||
}
|
}
|
||||||
if (!alreadyReportedRef.current.has(module)) {
|
if (!alreadyReportedRef.current.has(module)) {
|
||||||
alreadyReportedRef.current.set(module, module)
|
alreadyReportedRef.current.set(module, module)
|
||||||
logger.metric('explore:module:seen', {module}) //, index: index ?? -1})
|
logger.metric('explore:module:seen', {module})
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -876,6 +945,8 @@ export function Explore({
|
|||||||
windowSize={9}
|
windowSize={9}
|
||||||
maxToRenderPerBatch={platform({ios: 5, default: 1})}
|
maxToRenderPerBatch={platform({ios: 5, default: 1})}
|
||||||
updateCellsBatchingPeriod={40}
|
updateCellsBatchingPeriod={40}
|
||||||
|
refreshing={isPTR}
|
||||||
|
onRefresh={onPTR}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ export function Container({
|
|||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
|
bottomBorder,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
headerHeight?: number
|
||||||
|
bottomBorder?: boolean
|
||||||
|
} & ViewStyleProp) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -31,10 +36,9 @@ export function Container({
|
|||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
headerHeight && web({position: 'sticky', top: headerHeight}),
|
headerHeight && web({position: 'sticky', top: headerHeight}),
|
||||||
|
bottomBorder && [a.border_b, t.atoms.border_contrast_low],
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
{/* Very non-scientific way to avoid small gap on scroll */}
|
|
||||||
<View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
|
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
|
|||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
|
import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
|
||||||
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -48,75 +49,80 @@ export function StarterPackCard({
|
|||||||
.map(item => item.subject)
|
.map(item => item.subject)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<Link
|
||||||
style={[
|
to={link.to}
|
||||||
a.w_full,
|
label={link.label}
|
||||||
a.p_lg,
|
onHoverIn={link.precache}
|
||||||
a.gap_md,
|
onPress={link.precache}>
|
||||||
a.border,
|
{s => (
|
||||||
a.rounded_sm,
|
<>
|
||||||
a.overflow_hidden,
|
<SubtleHover hover={s.hovered || s.pressed} />
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
<View aria-hidden style={[a.absolute, a.inset_0, a.z_40]}>
|
|
||||||
<Link
|
|
||||||
to={link.to}
|
|
||||||
label={link.label}
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
onHoverIn={link.precache}
|
|
||||||
onPress={link.precache}>
|
|
||||||
<View />
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<AvatarStack
|
<View
|
||||||
profiles={profiles ?? []}
|
style={[
|
||||||
numPending={profileCount}
|
a.w_full,
|
||||||
total={view.list?.listItemCount}
|
a.p_lg,
|
||||||
/>
|
a.gap_md,
|
||||||
|
a.border,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<AvatarStack
|
||||||
|
profiles={profiles ?? []}
|
||||||
|
numPending={profileCount}
|
||||||
|
total={view.list?.listItemCount}
|
||||||
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_start,
|
a.align_start,
|
||||||
a.gap_lg,
|
a.gap_lg,
|
||||||
web({
|
web({
|
||||||
position: 'static',
|
position: 'static',
|
||||||
zIndex: 'unset',
|
zIndex: 'unset',
|
||||||
}),
|
}),
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<Text
|
<Text
|
||||||
emoji
|
emoji
|
||||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{view.record.name}
|
{view.record.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
emoji
|
emoji
|
||||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
|
style={[
|
||||||
numberOfLines={1}>
|
a.text_sm,
|
||||||
{view.creator?.did === currentAccount?.did
|
a.leading_snug,
|
||||||
? _(msg`By you`)
|
t.atoms.text_contrast_medium,
|
||||||
: _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
|
]}
|
||||||
</Text>
|
numberOfLines={1}>
|
||||||
</View>
|
{view.creator?.did === currentAccount?.did
|
||||||
<Link
|
? _(msg`By you`)
|
||||||
to={link.to}
|
: _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
|
||||||
label={link.label}
|
</Text>
|
||||||
onHoverIn={link.precache}
|
</View>
|
||||||
onPress={link.precache}
|
<Link
|
||||||
variant="solid"
|
to={link.to}
|
||||||
color="secondary"
|
label={link.label}
|
||||||
size="small"
|
onHoverIn={link.precache}
|
||||||
style={[a.z_50]}>
|
onPress={link.precache}
|
||||||
<ButtonText>
|
variant="solid"
|
||||||
<Trans>Open pack</Trans>
|
color="secondary"
|
||||||
</ButtonText>
|
size="small"
|
||||||
</Link>
|
style={[a.z_50]}>
|
||||||
</View>
|
<ButtonText>
|
||||||
</View>
|
<Trans>Open pack</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ export function ExploreInterestsCard() {
|
|||||||
<>
|
<>
|
||||||
<Prompt.Basic
|
<Prompt.Basic
|
||||||
control={trendingPrompt}
|
control={trendingPrompt}
|
||||||
title={_(msg`Your interests`)}
|
title={_(msg`Dismiss interests`)}
|
||||||
description={_(
|
description={_(
|
||||||
msg`You can adjust your interests at any time from your "Content and media" settings.`,
|
msg`You can adjust your interests at any time from "Content and media" settings.`,
|
||||||
)}
|
)}
|
||||||
confirmButtonCta={_(
|
confirmButtonCta={_(
|
||||||
msg({
|
msg({
|
||||||
message: `Copy that!`,
|
message: `OK`,
|
||||||
comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`,
|
comment: `Confirm button text.`,
|
||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
onConfirm={onConfirmClose}
|
onConfirm={onConfirmClose}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {atoms as a} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
|
import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
|
||||||
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -133,10 +134,7 @@ let Tab = ({
|
|||||||
a.py_sm,
|
a.py_sm,
|
||||||
a.border,
|
a.border,
|
||||||
active || hovered || pressed || focused
|
active || hovered || pressed || focused
|
||||||
? [
|
? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{borderColor: t.atoms.bg_contrast_25.backgroundColor},
|
|
||||||
]
|
|
||||||
: [t.atoms.bg, t.atoms.border_contrast_low],
|
: [t.atoms.bg, t.atoms.border_contrast_low],
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
@@ -186,47 +184,52 @@ let SuggestedProfileCard = ({
|
|||||||
{statsig: true},
|
{statsig: true},
|
||||||
)
|
)
|
||||||
}}>
|
}}>
|
||||||
<View
|
{s => (
|
||||||
style={[
|
<>
|
||||||
a.w_full,
|
<SubtleHover hover={s.hovered || s.pressed} />
|
||||||
a.py_lg,
|
<View
|
||||||
a.px_lg,
|
style={[
|
||||||
a.border_t,
|
a.flex_1,
|
||||||
t.atoms.border_contrast_low,
|
a.w_full,
|
||||||
a.flex_1,
|
a.py_lg,
|
||||||
]}>
|
a.px_lg,
|
||||||
<ProfileCard.Outer>
|
a.border_t,
|
||||||
<ProfileCard.Header>
|
t.atoms.border_contrast_low,
|
||||||
<ProfileCard.Avatar
|
]}>
|
||||||
profile={profile}
|
<ProfileCard.Outer>
|
||||||
moderationOpts={moderationOpts}
|
<ProfileCard.Header>
|
||||||
/>
|
<ProfileCard.Avatar
|
||||||
<ProfileCard.NameAndHandle
|
profile={profile}
|
||||||
profile={profile}
|
moderationOpts={moderationOpts}
|
||||||
moderationOpts={moderationOpts}
|
/>
|
||||||
/>
|
<ProfileCard.NameAndHandle
|
||||||
<ProfileCard.FollowButton
|
profile={profile}
|
||||||
profile={profile}
|
moderationOpts={moderationOpts}
|
||||||
moderationOpts={moderationOpts}
|
/>
|
||||||
withIcon={false}
|
<ProfileCard.FollowButton
|
||||||
logContext="ExploreSuggestedAccounts"
|
profile={profile}
|
||||||
onFollow={() => {
|
moderationOpts={moderationOpts}
|
||||||
logger.metric(
|
withIcon={false}
|
||||||
'suggestedUser:follow',
|
logContext="ExploreSuggestedAccounts"
|
||||||
{
|
onFollow={() => {
|
||||||
logContext: 'Explore',
|
logger.metric(
|
||||||
location: 'Card',
|
'suggestedUser:follow',
|
||||||
recId,
|
{
|
||||||
position,
|
logContext: 'Explore',
|
||||||
},
|
location: 'Card',
|
||||||
{statsig: true},
|
recId,
|
||||||
)
|
position,
|
||||||
}}
|
},
|
||||||
/>
|
{statsig: true},
|
||||||
</ProfileCard.Header>
|
)
|
||||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
}}
|
||||||
</ProfileCard.Outer>
|
/>
|
||||||
</View>
|
</ProfileCard.Header>
|
||||||
|
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||||
|
</ProfileCard.Outer>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ProfileCard.Link>
|
</ProfileCard.Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
|
|||||||
import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
|
import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
|
||||||
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
|
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const TOPIC_COUNT = 5
|
const TOPIC_COUNT = 5
|
||||||
@@ -28,10 +29,10 @@ export function ExploreTrendingTopics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Inner() {
|
function Inner() {
|
||||||
const {data: trending, error, isLoading} = useGetTrendsQuery()
|
const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
|
||||||
const noTopics = !isLoading && !error && !trending?.trends?.length
|
const noTopics = !isLoading && !error && !trending?.trends?.length
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading || isRefetching ? (
|
||||||
Array.from({length: TOPIC_COUNT}).map((__, i) => (
|
Array.from({length: TOPIC_COUNT}).map((__, i) => (
|
||||||
<TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
|
<TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
|
||||||
))
|
))
|
||||||
@@ -92,25 +93,23 @@ export function TrendRow({
|
|||||||
PressableComponent={Pressable}>
|
PressableComponent={Pressable}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<>
|
<>
|
||||||
<View
|
<SubtleHover hover={hovered || pressed} />
|
||||||
style={[
|
<View style={[gutters, a.w_full, a.py_lg, a.flex_row, a.gap_2xs]}>
|
||||||
gutters,
|
|
||||||
a.w_full,
|
|
||||||
a.py_lg,
|
|
||||||
a.flex_row,
|
|
||||||
a.gap_2xs,
|
|
||||||
(hovered || pressed) && t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_1, a.gap_xs]}>
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
<View style={[a.flex_row]}>
|
<View style={[a.flex_row]}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_md, a.font_bold, a.leading_snug, {width: 20}]}>
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
a.leading_tight,
|
||||||
|
{width: 20},
|
||||||
|
]}>
|
||||||
<Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
|
<Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
|
||||||
{rank}.
|
{rank}.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
style={[a.text_md, a.font_bold, a.leading_tight]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{trend.displayName}
|
{trend.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -34,6 +34,41 @@ const RQKEY_ROOT = 'feed-previews'
|
|||||||
const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
|
const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
|
||||||
|
|
||||||
const LIMIT = 8 // sliced to 6, overfetch to account for moderation
|
const LIMIT = 8 // sliced to 6, overfetch to account for moderation
|
||||||
|
const PINNED_POST_URIS: Record<string, boolean> = {
|
||||||
|
// 📰 News
|
||||||
|
'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b':
|
||||||
|
true,
|
||||||
|
// Gardening
|
||||||
|
'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27':
|
||||||
|
true,
|
||||||
|
// Web Development Trending
|
||||||
|
'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22':
|
||||||
|
true,
|
||||||
|
// Anime & Manga EN
|
||||||
|
'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y':
|
||||||
|
true,
|
||||||
|
// 📽️ Film
|
||||||
|
'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g':
|
||||||
|
true,
|
||||||
|
// PopSky
|
||||||
|
'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g':
|
||||||
|
true,
|
||||||
|
// Science
|
||||||
|
'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s':
|
||||||
|
true,
|
||||||
|
// Birds! 🦉
|
||||||
|
'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d':
|
||||||
|
true,
|
||||||
|
// Astronomy
|
||||||
|
'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w':
|
||||||
|
true,
|
||||||
|
// What's Cooking 🍽️
|
||||||
|
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q':
|
||||||
|
true,
|
||||||
|
// BookSky 💙📚 #booksky
|
||||||
|
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e':
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
|
||||||
export type FeedPreviewItem =
|
export type FeedPreviewItem =
|
||||||
| {
|
| {
|
||||||
@@ -181,19 +216,24 @@ export function useFeedPreviews(
|
|||||||
feedContext: item.feedContext,
|
feedContext: item.feedContext,
|
||||||
reason: item.reason,
|
reason: item.reason,
|
||||||
feedPostUri: item.feedPostUri,
|
feedPostUri: item.feedPostUri,
|
||||||
items: item.items.slice(0, 6).map((subItem, i) => {
|
items: item.items
|
||||||
const feedPostSliceItem: FeedPostSliceItem = {
|
.slice(0, 6)
|
||||||
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
|
.filter(subItem => {
|
||||||
uri: subItem.post.uri,
|
return !PINNED_POST_URIS[subItem.post.uri]
|
||||||
post: subItem.post,
|
})
|
||||||
record: subItem.record,
|
.map((subItem, i) => {
|
||||||
moderation: moderations[i],
|
const feedPostSliceItem: FeedPostSliceItem = {
|
||||||
parentAuthor: subItem.parentAuthor,
|
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
|
||||||
isParentBlocked: subItem.isParentBlocked,
|
uri: subItem.post.uri,
|
||||||
isParentNotFound: subItem.isParentNotFound,
|
post: subItem.post,
|
||||||
}
|
record: subItem.record,
|
||||||
return feedPostSliceItem
|
moderation: moderations[i],
|
||||||
}),
|
parentAuthor: subItem.parentAuthor,
|
||||||
|
isParentBlocked: subItem.isParentBlocked,
|
||||||
|
isParentNotFound: subItem.isParentNotFound,
|
||||||
|
}
|
||||||
|
return feedPostSliceItem
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
if (slice.isIncompleteThread && slice.items.length >= 3) {
|
if (slice.isIncompleteThread && slice.items.length >= 3) {
|
||||||
const beforeLast = slice.items.length - 2
|
const beforeLast = slice.items.length - 2
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ export function useGetSuggestedFeedsQuery() {
|
|||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!preferences,
|
enabled: !!preferences,
|
||||||
refetchOnWindowFocus: true,
|
staleTime: STALE.MINUTES.THREE,
|
||||||
staleTime: STALE.MINUTES.ONE,
|
|
||||||
queryKey: createGetSuggestedFeedsQueryKey(),
|
queryKey: createGetSuggestedFeedsQueryKey(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const contentLangs = getContentLanguages().join(',')
|
const contentLangs = getContentLanguages().join(',')
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
|
|||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!preferences,
|
enabled: !!preferences,
|
||||||
refetchOnWindowFocus: true,
|
staleTime: STALE.MINUTES.THREE,
|
||||||
staleTime: STALE.MINUTES.ONE,
|
|
||||||
queryKey: createGetSuggestedUsersQueryKey(props),
|
queryKey: createGetSuggestedUsersQueryKey(props),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const contentLangs = getContentLanguages().join(',')
|
const contentLangs = getContentLanguages().join(',')
|
||||||
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
|
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
|
||||||
{
|
{
|
||||||
category: props.category ?? undefined,
|
category: props.category ?? undefined,
|
||||||
|
limit: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export function useGetTrendsQuery() {
|
|||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!preferences,
|
enabled: !!preferences,
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
staleTime: STALE.MINUTES.THREE,
|
staleTime: STALE.MINUTES.THREE,
|
||||||
queryKey: createGetTrendsQueryKey(),
|
queryKey: createGetTrendsQueryKey(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ export function useSuggestedStarterPacksQuery() {
|
|||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!preferences,
|
enabled: !!preferences,
|
||||||
refetchOnWindowFocus: true,
|
staleTime: STALE.MINUTES.THREE,
|
||||||
staleTime: STALE.MINUTES.ONE,
|
|
||||||
queryKey: createSuggestedStarterPacksQueryKey(),
|
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||||
async queryFn() {
|
async queryFn() {
|
||||||
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(
|
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(
|
||||||
|
|||||||
Reference in New Issue
Block a user