Compare commits

...

27 Commits

Author SHA1 Message Date
Eric Bailey ecf275b7f4 WIP suggested follows section for feeds 2024-04-18 15:22:57 -05:00
Eric Bailey 42c8a07551 Revert testing change 2024-04-18 12:06:36 -05:00
Eric Bailey a6d2921fa6 Hide primary feed setting outside exp 2024-04-18 12:03:44 -05:00
Eric Bailey c66529a6ef Merge remote-tracking branch 'origin/main' into halo/base
* origin/main:
  [Statsig] Slightly block the UI on gates (#3608)
  [Statsig] Prefetch configs for other accounts (#3607)
2024-04-18 12:02:02 -05:00
Eric Bailey 98658a313f Missing following handling 2024-04-18 11:44:48 -05:00
Eric Bailey a26c08f95e Simplify logic 2024-04-18 11:34:35 -05:00
Eric Bailey 0521488d05 Filter dupe from Feeds screen 2024-04-18 11:11:36 -05:00
Eric Bailey abc9b48eef Filter out primary algo from feeds page 2024-04-18 11:08:56 -05:00
Eric Bailey b5e57c8768 Move gate call down 2024-04-18 10:59:40 -05:00
Eric Bailey df501d870c Restore Feeds sparkle, fix line height 2024-04-18 10:46:49 -05:00
Eric Bailey aa1d1c23a5 Handle saved feed screen edge case 2024-04-18 10:34:51 -05:00
Eric Bailey 6a8b670ae3 Better comment 2024-04-18 10:21:55 -05:00
Eric Bailey e02ea9ee26 Clarify primary algo usage 2024-04-18 10:19:57 -05:00
Eric Bailey 9351281fd9 Better formatting 2024-04-18 10:14:02 -05:00
Eric Bailey 699ddc8de0 Support following feed as well 2024-04-18 10:09:20 -05:00
Eric Bailey a255de4775 Revert unneeded changes 2024-04-18 10:07:36 -05:00
Eric Bailey bb53548541 Update statsig API 2024-04-18 10:03:22 -05:00
Eric Bailey 9075b123d5 Improve perf of pinned feeds with primary algo 2024-04-18 09:56:58 -05:00
Eric Bailey d976684ddf Fix pinned feeds key 2024-04-18 09:56:58 -05:00
Eric Bailey 6f35e50dbf Rename 2024-04-18 09:56:56 -05:00
Eric Bailey 38ce57acf1 Handle home algo on ProfileFeed screen 2024-04-18 09:56:52 -05:00
Eric Bailey 11cc7c7d96 Fix handling of pinned feed if home algo is disabled 2024-04-18 09:56:52 -05:00
Eric Bailey b81632ef5e Handle home algo in FeedSourceCard 2024-04-18 09:56:52 -05:00
Eric Bailey 16cc338c86 Handle edge case 2024-04-18 09:56:52 -05:00
Eric Bailey 620f958f72 Simplify filter logic 2024-04-18 09:56:44 -05:00
Eric Bailey f6ec6ed207 Remove todo, fix pwi view 2024-04-18 09:56:44 -05:00
Eric Bailey e3a4fcc152 Handle home algo with backwards compat 2024-04-18 09:56:42 -05:00
19 changed files with 666 additions and 169 deletions
@@ -0,0 +1,156 @@
import React from 'react'
import {View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/queries/preferences'
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
import {isJustAMute} from 'lib/moderation'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {useFollowMethods} from '#/components/hooks/useFollowMethods'
import {useRichText} from '#/components/hooks/useRichText'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
export function SuggestedFollowCard({
profile: profileUnshadowed,
}: {
profile: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
const {_} = useLingui()
const profile = useProfileShadow(profileUnshadowed)
const moderationOpts = useModerationOpts()
const {follow, unfollow} = useFollowMethods({
profile,
// @ts-ignore TODO
logContext: 'FeedSuggestedFollowsCard',
})
// @ts-ignore TODO
const [descriptionRT] = useRichText(profileUnshadowed?.description ?? '')
if (!moderationOpts) return null
const moderation = moderateProfile(profile, moderationOpts)
const modui = moderation.ui('profileList')
if (modui.filter && !isJustAMute(modui)) return null
return (
<View
style={[
a.p_lg,
a.rounded_md,
a.gap_sm,
t.atoms.bg,
{
width: 300,
},
]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<PreviewableUserAvatar
size={40}
did={profile.did}
handle={profile.handle}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
/>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.flex_1,
]}>
<View style={[a.gap_2xs, a.flex_1]}>
<Text
style={[a.text_md, a.font_bold, a.leading_tight, a.flex_1]}
numberOfLines={1}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
</Text>
<Text
style={[t.atoms.text_contrast_medium, a.flex_1]}
numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</View>
<Button
label={
profile.viewer?.following ? _(msg`Following`) : _(msg`Follow`)
}
size="small"
shape="round"
variant="solid"
color="secondary"
onPress={profile.viewer?.following ? unfollow : follow}>
{profile.viewer?.following ? (
<ButtonIcon icon={Check} />
) : (
<ButtonIcon icon={Plus} />
)}
</Button>
</View>
</View>
<Text style={[a.flex_1]} numberOfLines={2}>
<RichText value={descriptionRT} style={[t.atoms.text_contrast_high]} />
</Text>
</View>
)
}
export function FeedSuggestedFollows() {
const t = useTheme()
const {isLoading, data: suggestions, error} = useSuggestedFollowsQuery()
const profiles: AppBskyActorDefs.ProfileViewBasic[] = []
if (suggestions) {
// Currently the responses contain duplicate items.
// Needs to be fixed on backend, but let's dedupe to be safe.
let seen = new Set()
for (const page of suggestions.pages) {
for (const actor of page.actors) {
if (!seen.has(actor.did)) {
seen.add(actor.did)
profiles.push(actor)
}
}
}
}
return (
<View
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
<View style={[a.pt_xl, a.px_lg, a.flex_row, a.gap_md]}>
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
Suggested for you
</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
{isLoading
? null
: error || !profiles.length
? null
: profiles.map((profile, i) => (
<SuggestedFollowCard key={i} profile={profile} />
))}
</View>
</ScrollView>
</View>
)
}
@@ -0,0 +1,26 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DialogOuterProps} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
export function PrimaryAlgoNoticeDialog({
control,
}: {
control: DialogOuterProps['control']
}) {
const {_} = useLingui()
return (
<Prompt.Outer control={control}>
<Prompt.TitleText>Your primary algorithm</Prompt.TitleText>
<Prompt.DescriptionText>
This feed is set as your primary algorithm, which is used as your home
screen when you open the app.
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Cancel cta={_(msg`Close`)} />
</Prompt.Actions>
</Prompt.Outer>
)
}
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Home_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M11.46 1.362a2 2 0 0 1 1.08 0c.249.07.448.188.611.301.146.102.306.232.467.363l6.421 5.218.046.036c.169.137.38.308.54.53a2 2 0 0 1 .304.64c.073.264.072.536.071.753v9.229c0 .252 0 .498-.017.706a2.023 2.023 0 0 1-.201.77 2 2 0 0 1-.874.874 2.02 2.02 0 0 1-.77.201c-.208.017-.454.017-.706.017H5.568c-.252 0-.498 0-.706-.017a2.02 2.02 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C3 18.93 3 18.684 3 18.432V9.203c0-.217-.002-.49.07-.754a2 2 0 0 1 .304-.638c.16-.223.372-.394.541-.53l.045-.037 6.422-5.218c.161-.13.321-.26.467-.362.163-.114.362-.232.612-.302Zm.532 1.943c-.077.054-.18.136-.37.29l-6.4 5.2a6.315 6.315 0 0 0-.215.18c-.002 0-.003.002-.004.003v.004C5 9.036 5 9.112 5 9.262V18.4a8.18 8.18 0 0 0 .011.588l.014.002c.116.01.278.01.575.01H8v-5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v5h2.4a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V9.262c0-.15 0-.226-.003-.28v-.004l-.003-.003a6.448 6.448 0 0 0-.216-.18l-6.4-5.2a7.373 7.373 0 0 0-.37-.29L12 3.299l-.008.006ZM14 19v-5h-4v5h4Z',
})
export const Home_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M13.261 1.736a2 2 0 0 0-2.522 0l-7 5.687A2 2 0 0 0 3 8.976V19a2 2 0 0 0 2 2h3v-8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8h3a2 2 0 0 0 2-2V8.976a2 2 0 0 0-.739-1.553l-7-5.687ZM14 21h-4v-7h4v7Z',
})
+3
View File
@@ -89,3 +89,6 @@ export const BSKY_FEED_OWNER_DIDS = [
'did:plc:vpkhqolt662uhesyj6nxm7ys',
'did:plc:q6gjnaw2blty4crticxkmujt',
]
export const DISCOVER_FEED_URI =
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
+1 -1
View File
@@ -8,5 +8,5 @@ export type Gate =
| 'new_search'
| 'receive_updates'
| 'show_follow_back_label'
| 'start_session_with_following'
| 'use_new_suggestions_endpoint'
| 'reduced_onboarding_and_home_algo'
+62 -17
View File
@@ -13,6 +13,8 @@ import {
useQuery,
} from '@tanstack/react-query'
import {DISCOVER_FEED_URI} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {STALE} from '#/state/queries'
@@ -21,6 +23,7 @@ import {getAgent, useSession} from '#/state/session'
import {router} from '#/routes'
export type FeedSourceFeedInfo = {
isPrimaryAlgorithm: boolean
type: 'feed'
uri: string
route: {
@@ -39,6 +42,7 @@ export type FeedSourceFeedInfo = {
}
export type FeedSourceListInfo = {
isPrimaryAlgorithm: boolean
type: 'list'
uri: string
route: {
@@ -69,6 +73,7 @@ const feedSourceNSIDs = {
export function hydrateFeedGenerator(
view: AppBskyFeedDefs.GeneratorView,
options?: Pick<FeedSourceFeedInfo, 'isPrimaryAlgorithm'>,
): FeedSourceInfo {
const urip = new AtUri(view.uri)
const collection =
@@ -77,6 +82,7 @@ export function hydrateFeedGenerator(
const route = router.matchPath(href)
return {
isPrimaryAlgorithm: options?.isPrimaryAlgorithm ?? false,
type: 'feed',
uri: view.uri,
cid: view.cid,
@@ -108,6 +114,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
const route = router.matchPath(href)
return {
isPrimaryAlgorithm: false,
type: 'list',
uri: view.uri,
route: {
@@ -199,10 +206,14 @@ export function useSearchPopularFeedsMutation() {
})
}
const FOLLOWING_FEED_STUB: FeedSourceInfo = {
/**
* The following feed, with fallbacks to Discover
*/
const HOME_FEED_STUB: FeedSourceInfo = {
isPrimaryAlgorithm: false,
type: 'feed',
displayName: 'Following',
uri: '',
uri: 'home',
route: {
href: '/',
name: 'Home',
@@ -216,10 +227,11 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeCount: 0,
likeUri: '',
}
const DISCOVER_FEED_STUB: FeedSourceInfo = {
const PWI_DISCOVER_FEED_STUB: FeedSourceInfo = {
isPrimaryAlgorithm: true,
type: 'feed',
displayName: 'Discover',
uri: '',
uri: DISCOVER_FEED_URI,
route: {
href: '/',
name: 'Home',
@@ -239,22 +251,38 @@ const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const gate = useGate()
const isPrimaryAlgoExperimentEnabled = gate(
'reduced_onboarding_and_home_algo',
)
const primaryAlgo = preferences?.primaryAlgorithm
const pinnedUris = preferences?.feeds?.pinned ?? []
const feedUris = pinnedUris.filter(uri => getFeedTypeFromUri(uri) === 'feed')
const listUris = pinnedUris.filter(uri => getFeedTypeFromUri(uri) === 'list')
if (
isPrimaryAlgoExperimentEnabled &&
hasSession &&
primaryAlgo?.enabled &&
primaryAlgo?.uri
) {
feedUris.unshift(primaryAlgo.uri)
}
// used for query key
const allUris = feedUris.concat(listUris)
return useQuery({
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryKey: [
pinnedFeedInfosQueryKeyRoot,
(hasSession ? 'authed:' : 'unauthed:') + pinnedUris.join(','),
(hasSession ? 'authed:' : 'unauthed:') + allUris.join(','),
],
queryFn: async () => {
let resolved = new Map()
let resolved = new Map<string, FeedSourceInfo>()
// Get all feeds. We can do this in a batch.
const feedUris = pinnedUris.filter(
uri => getFeedTypeFromUri(uri) === 'feed',
)
let feedsPromise = Promise.resolve()
if (feedUris.length > 0) {
feedsPromise = getAgent()
@@ -269,9 +297,6 @@ export function usePinnedFeedsInfos() {
}
// Get all lists. This currently has to be done individually.
const listUris = pinnedUris.filter(
uri => getFeedTypeFromUri(uri) === 'list',
)
const listsPromises = listUris.map(listUri =>
getAgent()
.app.bsky.graph.getList({
@@ -284,14 +309,34 @@ export function usePinnedFeedsInfos() {
}),
)
// The returned result will have the original order.
const result = [hasSession ? FOLLOWING_FEED_STUB : DISCOVER_FEED_STUB]
const result = [hasSession ? HOME_FEED_STUB : PWI_DISCOVER_FEED_STUB]
await Promise.allSettled([feedsPromise, ...listsPromises])
for (let pinnedUri of pinnedUris) {
if (resolved.has(pinnedUri)) {
result.push(resolved.get(pinnedUri))
// if primary algo is enabled and was fetched, add it to the front of the list
if (primaryAlgo?.enabled && primaryAlgo?.uri) {
const feedInfo = resolved.get(primaryAlgo.uri)
if (feedInfo) {
feedInfo.isPrimaryAlgorithm = true
result.unshift(feedInfo)
}
}
const pinnedUrisSansPrimary = pinnedUris.filter(uri => {
if (primaryAlgo?.enabled) {
return uri !== primaryAlgo?.uri
}
return true
})
// order the feeds/lists in the order they were pinned
for (let pinnedUri of pinnedUrisSansPrimary) {
const feedInfo = resolved.get(pinnedUri)
if (feedInfo) {
result.push(feedInfo)
}
}
return result
},
})
+5
View File
@@ -43,6 +43,11 @@ type AuthorFilter =
| 'posts_with_media'
type FeedUri = string
type ListUri = string
/**
* Represents a "not found" state, that results in the left-most tab being
* selected on the Home screen.
*/
export const DEFAULT_FEED_DESCRIPTOR = '__default__'
export type FeedDescriptor =
| 'home'
| 'following'
+5 -4
View File
@@ -1,8 +1,8 @@
import {
UsePreferencesQueryResponse,
ThreadViewPreferences,
} from '#/state/queries/preferences/types'
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
import {
ThreadViewPreferences,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
{
@@ -45,4 +45,5 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
userAge: 13, // TODO(pwi)
interests: {tags: []},
primaryAlgorithm: {enabled: undefined},
}
+14
View File
@@ -345,3 +345,17 @@ export function useRemoveMutedWordMutation() {
},
})
}
export function useSetPrimaryAlgorithmMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (pref: AppBskyActorDefs.PrimaryAlgoPref) => {
await getAgent().setPrimaryAlgorithm(pref)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
})
}
+24 -9
View File
@@ -4,6 +4,7 @@ import {Gate} from '#/lib/statsig/gates'
import {useGate} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
import {DEFAULT_FEED_DESCRIPTOR} from '#/state/queries/post-feed'
type StateContext = string
type SetContext = (v: string) => void
@@ -12,29 +13,43 @@ const stateContext = React.createContext<StateContext>('home')
const setContext = React.createContext<SetContext>((_: string) => {})
function getInitialFeed(gate: (gateName: Gate) => boolean) {
const isPrimaryAlgoExperimentEnabled = gate(
'reduced_onboarding_and_home_algo',
)
let feed = DEFAULT_FEED_DESCRIPTOR
if (isWeb) {
if (window.location.pathname === '/') {
const params = new URLSearchParams(window.location.search)
const feedFromUrl = params.get('feed')
if (feedFromUrl) {
// If explicitly booted from a link like /?feed=..., prefer that.
// basically a link to a specific tab, use that every time if present
return feedFromUrl
}
}
const feedFromSession = sessionStorage.getItem('lastSelectedHomeFeed')
if (feedFromSession) {
// Fall back to a previously chosen feed for this browser tab.
return feedFromSession
feed = feedFromSession
}
}
if (!gate('start_session_with_following')) {
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
}
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
feed = feedFromPersisted
}
return 'home'
if (isPrimaryAlgoExperimentEnabled) {
// following feed
if (feed === 'home') {
return 'home'
}
// or left-most tab
return DEFAULT_FEED_DESCRIPTOR
}
return DEFAULT_FEED_DESCRIPTOR
}
export function Provider({children}: React.PropsWithChildren<{}>) {
+81 -47
View File
@@ -1,30 +1,36 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {RichText} from '#/components/RichText'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {UserAvatar} from '../util/UserAvatar'
import {pluralize} from 'lib/strings/helpers'
import {AtUri} from '@atproto/api'
import * as Toast from 'view/com/util/Toast'
import {sanitizeHandle} from 'lib/strings/handles'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {
usePinFeedMutation,
UsePreferencesQueryResponse,
usePreferencesQuery,
useSaveFeedMutation,
UsePreferencesQueryResponse,
useRemoveFeedMutation,
useSaveFeedMutation,
} from '#/state/queries/preferences'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {useTheme} from '#/alf'
import * as Prompt from '#/components/Prompt'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeHandle} from 'lib/strings/handles'
import {pluralize} from 'lib/strings/helpers'
import {s} from 'lib/styles'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import * as Toast from 'view/com/util/Toast'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PrimaryAlgoNoticeDialog} from '#/components/PrimaryAlgoNoticeDialog'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
export function FeedSourceCard({
feedUri,
@@ -87,6 +93,8 @@ export function FeedSourceCardLoaded({
const {_} = useLingui()
const removePromptControl = Prompt.usePromptControl()
const navigation = useNavigationDeduped()
const gate = useGate()
const primaryAlgoDialogControl = Prompt.usePromptControl()
const {isPending: isSavePending, mutateAsync: saveFeed} =
useSaveFeedMutation()
@@ -185,6 +193,10 @@ export function FeedSourceCardLoaded({
</View>
)
const primaryAlgo = preferences?.primaryAlgorithm
const isPrimaryAlgo =
primaryAlgo?.enabled && primaryAlgo?.uri && primaryAlgo.uri === feed.uri
return (
<>
<Pressable
@@ -205,7 +217,7 @@ export function FeedSourceCardLoaded({
}
}}
key={feed.uri}>
<View style={[styles.headerContainer]}>
<View style={[styles.headerContainer, a.align_start]}>
<View style={[s.mr10]}>
<UserAvatar type="algo" size={36} avatar={feed.avatar} />
</View>
@@ -223,35 +235,55 @@ export function FeedSourceCardLoaded({
</View>
{showSaveBtn && feed.type === 'feed' && (
<View style={[s.justifyCenter]}>
<Pressable
testID={`feed-${feed.displayName}-toggleSave`}
disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button"
accessibilityLabel={
isSaved
? _(msg`Remove from my feeds`)
: _(msg`Add to my feeds`)
}
accessibilityHint=""
onPress={onToggleSaved}
hitSlop={15}
style={styles.btn}>
{isSaved ? (
<FontAwesomeIcon
icon={['far', 'trash-can']}
size={19}
color={pal.colors.icon}
/>
) : (
<FontAwesomeIcon
icon="plus"
size={18}
color={pal.colors.link}
/>
)}
</Pressable>
</View>
<>
{gate('reduced_onboarding_and_home_algo') && isPrimaryAlgo ? (
<Button
variant="solid"
color="secondary"
size="small"
label={_(
msg`This feed is already set as your primary algorithm.`,
)}
onPress={() => {
primaryAlgoDialogControl.open()
}}>
<ButtonIcon icon={Check} position="left" />
<ButtonText>
<Trans>Primary Algorithm</Trans>
</ButtonText>
</Button>
) : (
<View style={[s.justifyCenter]}>
<Pressable
testID={`feed-${feed.displayName}-toggleSave`}
disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button"
accessibilityLabel={
isSaved
? _(msg`Remove from my feeds`)
: _(msg`Add to my feeds`)
}
accessibilityHint=""
onPress={onToggleSaved}
hitSlop={15}
style={styles.btn}>
{isSaved ? (
<FontAwesomeIcon
icon={['far', 'trash-can']}
size={19}
color={pal.colors.icon}
/>
) : (
<FontAwesomeIcon
icon="plus"
size={18}
color={pal.colors.link}
/>
)}
</Pressable>
</View>
)}
</>
)}
</View>
@@ -283,6 +315,8 @@ export function FeedSourceCardLoaded({
confirmButtonCta={_(msg`Remove`)}
confirmButtonColor="negative"
/>
<PrimaryAlgoNoticeDialog control={primaryAlgoDialogControl} />
</>
)
}
+15 -6
View File
@@ -1,12 +1,14 @@
import React from 'react'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {HomeHeaderLayout} from './HomeHeaderLayout'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useNavigation} from '@react-navigation/native'
import {usePalette} from '#/lib/hooks/usePalette'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useSession} from '#/state/session'
import {NavigationProp} from 'lib/routes/types'
import {isWeb} from 'platform/detection'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from '../pager/TabBar'
import {usePalette} from '#/lib/hooks/usePalette'
import {HomeHeaderLayout} from './HomeHeaderLayout'
export function HomeHeader(
props: RenderTabBarFnProps & {
@@ -16,12 +18,19 @@ export function HomeHeader(
},
) {
const {feeds} = props
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const pal = usePalette('default')
const hasPinnedCustom = React.useMemo<boolean>(() => {
return feeds.some(tab => tab.uri !== '')
}, [feeds])
if (!hasSession) return false
return feeds.some(tab => {
const isFollowing = ['home', 'following'].includes(tab.uri)
const isPrimaryAlgo = tab.isPrimaryAlgorithm
const isCustom = !isFollowing && !isPrimaryAlgo
return isCustom
})
}, [feeds, hasSession])
const items = React.useMemo(() => {
const pinnedNames = feeds.map(f => f.displayName)
+10 -6
View File
@@ -1,11 +1,12 @@
import React, {useRef, useMemo, useEffect, useState, useCallback} from 'react'
import {StyleSheet, View, ScrollView, LayoutChangeEvent} from 'react-native'
import {Text} from '../util/text/Text'
import {PressableWithHover} from '../util/PressableWithHover'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {LayoutChangeEvent, ScrollView, StyleSheet, View} from 'react-native'
import {isNative} from '#/platform/detection'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {PressableWithHover} from '../util/PressableWithHover'
import {Text} from '../util/text/Text'
import {DraggableScrollView} from './DraggableScrollView'
import {isNative} from '#/platform/detection'
export interface TabBarProps {
testID?: string
@@ -139,7 +140,10 @@ export function TabBar({
<Text
type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
testID={testID ? `${testID}-${item}` : undefined}
style={selected ? pal.text : pal.textLight}>
style={[
selected ? pal.text : pal.textLight,
{lineHeight: 20},
]}>
{item}
</Text>
</View>
+5
View File
@@ -29,6 +29,7 @@ import {useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useTheme} from 'lib/ThemeContext'
import {FeedSuggestedFollows} from '#/components/FeedSuggestedFollows'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -40,6 +41,7 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const SUGGESTED_FOLLOWS_ITEM = {_reactKey: '__suggested_follows__'}
// DISABLED need to check if this is causing random feed refreshes -prf
// const REFRESH_AFTER = STALE.HOURS.ONE
@@ -198,6 +200,7 @@ let Feed = ({
} else if (isEmpty) {
arr = arr.concat([EMPTY_FEED_ITEM])
} else if (data) {
arr = arr.concat(SUGGESTED_FOLLOWS_ITEM)
for (const page of data?.pages) {
arr = arr.concat(page.slices)
}
@@ -297,6 +300,8 @@ let Feed = ({
// see home.ts (feed api) for more info
// -prf
return <DiscoverFallbackHeader />
} else if (item === SUGGESTED_FOLLOWS_ITEM) {
return <FeedSuggestedFollows />
}
return <FeedSlice slice={item} />
},
+72 -8
View File
@@ -13,6 +13,7 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import debounce from 'lodash.debounce'
import {useGate} from '#/lib/statsig/statsig'
import {isNative, isWeb} from '#/platform/detection'
import {
getAvatarTypeFromUri,
@@ -150,6 +151,7 @@ export function FeedsScreen(_props: Props) {
const {hasSession} = useSession()
const listRef = React.useRef<FlatList>(null)
const searchInputRef = React.useRef<SearchInputRef>(null)
const gate = useGate()
/**
* A search query is present. We may not have search results yet.
@@ -236,15 +238,41 @@ export function FeedsScreen(_props: Props) {
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
})
} else {
const isPrimaryAlgoExperimentEnabled = gate(
'reduced_onboarding_and_home_algo',
)
const primaryAlgo = preferences.primaryAlgorithm
if (
isPrimaryAlgoExperimentEnabled &&
primaryAlgo?.enabled &&
primaryAlgo?.uri
) {
slices = slices.concat({
key: `savedFeed:${primaryAlgo?.uri}`,
type: 'savedFeed',
feedUri: primaryAlgo?.uri,
})
}
if (preferences?.feeds?.saved.length !== 0) {
const {saved, pinned} = preferences.feeds
slices = slices.concat(
pinned.map(uri => ({
key: `savedFeed:${uri}`,
type: 'savedFeed',
feedUri: uri,
})),
pinned
.filter(uri => {
return !(
isPrimaryAlgoExperimentEnabled &&
primaryAlgo?.enabled &&
primaryAlgo?.uri &&
uri === primaryAlgo?.uri
)
})
.map(uri => ({
key: `savedFeed:${uri}`,
type: 'savedFeed',
feedUri: uri,
})),
)
slices = slices.concat(
@@ -323,7 +351,17 @@ export function FeedsScreen(_props: Props) {
) {
return false
}
return !preferences?.feeds?.saved.includes(feed.uri)
const isPrimaryAlgoExperimentEnabled = gate(
'reduced_onboarding_and_home_algo',
)
const isPrimaryAlgo =
isPrimaryAlgoExperimentEnabled &&
preferences?.primaryAlgorithm?.enabled &&
preferences?.primaryAlgorithm?.uri === feed.uri
return (
!preferences?.feeds?.saved.includes(feed.uri) &&
!isPrimaryAlgo
)
})
.map(feed => ({
key: `popularFeed:${feed.uri}`,
@@ -358,6 +396,7 @@ export function FeedsScreen(_props: Props) {
isSearchPending,
searchError,
isUserSearching,
gate,
])
const renderHeaderBtn = React.useCallback(() => {
@@ -479,7 +518,18 @@ export function FeedsScreen(_props: Props) {
</View>
)
} else if (item.type === 'savedFeed') {
return <SavedFeed feedUri={item.feedUri} />
const isPrimaryAlgoExperimentEnabled = gate(
'reduced_onboarding_and_home_algo',
)
const primaryAlgo = preferences?.primaryAlgorithm
const isPrimaryAlgo =
isPrimaryAlgoExperimentEnabled &&
primaryAlgo?.enabled &&
primaryAlgo?.uri === item.feedUri
return (
<SavedFeed feedUri={item.feedUri} isPrimaryAlgo={isPrimaryAlgo} />
)
} else if (item.type === 'popularFeedsHeader') {
return (
<>
@@ -532,6 +582,7 @@ export function FeedsScreen(_props: Props) {
pal.icon,
pal.textLight,
_,
preferences?.primaryAlgorithm,
preferences?.feeds?.saved?.length,
query,
onChangeQuery,
@@ -539,6 +590,7 @@ export function FeedsScreen(_props: Props) {
onSubmitQuery,
onChangeSearchFocus,
hasSession,
gate,
],
)
@@ -585,7 +637,13 @@ export function FeedsScreen(_props: Props) {
)
}
function SavedFeed({feedUri}: {feedUri: string}) {
function SavedFeed({
feedUri,
isPrimaryAlgo,
}: {
feedUri: string
isPrimaryAlgo?: boolean
}) {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const {data: info, error} = useFeedSourceInfoQuery({uri: feedUri})
@@ -632,6 +690,12 @@ function SavedFeed({feedUri}: {feedUri: string}) {
</View>
) : null}
</View>
{isPrimaryAlgo && (
<Text type="xs" style={[pal.textLight, s.semiBold]}>
<Trans>Primary Algorithm</Trans>
</Text>
)}
{isMobile && (
<FontAwesomeIcon
icon="chevron-right"
+24 -13
View File
@@ -53,15 +53,19 @@ function HomeScreenReady({
pinnedFeedInfos: FeedSourceInfo[]
}) {
useOTAUpdates()
const gate = useGate()
const allFeeds = React.useMemo(() => {
const feeds: FeedDescriptor[] = []
feeds.push('home')
for (const {uri} of pinnedFeedInfos) {
if (uri.includes('app.bsky.feed.generator')) {
feeds.push(`feedgen|${uri}`)
} else if (uri.includes('app.bsky.graph.list')) {
feeds.push(`list|${uri}`)
} else if (uri === 'home') {
feeds.push('home')
} else if (uri === 'following') {
feeds.push('following')
}
}
return feeds
@@ -70,6 +74,10 @@ function HomeScreenReady({
const rawSelectedFeed = useSelectedFeed()
const setSelectedFeed = useSetSelectedFeed()
const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed as FeedDescriptor)
/*
* N.B. if `rawSelectedFeed` returns `DEFAULT_FEED_DESCRIPTOR`,
* `maybeFoundIndex` will be -1 and we'll fall back to left-most tab
*/
const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex]
@@ -111,7 +119,6 @@ function HomeScreenReady({
}),
)
const gate = useGate()
React.useEffect(() => {
const listener = AppState.addEventListener('change', nextAppState => {
if (nextAppState === 'active') {
@@ -186,7 +193,6 @@ function HomeScreenReady({
return <CustomFeedEmptyState />
}, [])
const [homeFeed, ...customFeeds] = allFeeds
const homeFeedParams = React.useMemo<FeedParams>(() => {
return {
mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
@@ -206,16 +212,21 @@ function HomeScreenReady({
onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged}
renderTabBar={renderTabBar}>
<FeedPage
key={homeFeed}
testID="followingFeedPage"
isPageFocused={selectedFeed === homeFeed}
feed={homeFeed}
feedParams={homeFeedParams}
renderEmptyState={renderFollowingEmptyState}
renderEndOfFeed={FollowingEndOfFeed}
/>
{customFeeds.map(feed => {
{allFeeds.map(feed => {
if (feed === 'home' || feed === 'following') {
return (
<FeedPage
key={feed}
testID="followingFeedPage"
isPageFocused={selectedFeed === feed}
feed={feed}
feedParams={homeFeedParams}
renderEmptyState={renderFollowingEmptyState}
renderEndOfFeed={FollowingEndOfFeed}
/>
)
}
return (
<FeedPage
key={feed}
+109 -32
View File
@@ -7,6 +7,7 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
import {HITSLOP_20} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
@@ -20,6 +21,7 @@ import {
UsePreferencesQueryResponse,
useRemoveFeedMutation,
useSaveFeedMutation,
useSetPrimaryAlgorithmMutation,
useUnpinFeedMutation,
} from '#/state/queries/preferences'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
@@ -52,18 +54,22 @@ import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {Button as NewButton, ButtonText} from '#/components/Button'
import {Button as NewButton, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
Heart2_Stroke2_Corner0_Rounded as HeartOutline,
} from '#/components/icons/Heart2'
import {Home_Stroke2_Corner0_Rounded as Home} from '#/components/icons/Home'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {InlineLinkText} from '#/components/Link'
import * as Menu from '#/components/Menu'
import {PrimaryAlgoNoticeDialog} from '#/components/PrimaryAlgoNoticeDialog'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
@@ -162,6 +168,9 @@ export function ProfileFeedScreenInner({
const playHaptic = useHaptics()
const feedSectionRef = React.useRef<SectionRef>(null)
const isScreenFocused = useIsFocused()
const gate = useGate()
const primaryAlgoDialogControl = useDialogControl()
const primaryAlgo = preferences.primaryAlgorithm
const {
mutateAsync: saveFeed,
@@ -187,6 +196,11 @@ export function ProfileFeedScreenInner({
reset: resetUnpinFeed,
isPending: isUnpinPending,
} = useUnpinFeedMutation()
const {
mutateAsync: setPrimaryAlgo,
variables: primaryAlgoVariables,
isPending: isSetPrimaryAlgoPending,
} = useSetPrimaryAlgorithmMutation()
const isSaved =
!removedFeed &&
@@ -194,6 +208,11 @@ export function ProfileFeedScreenInner({
const isPinned =
!unpinnedFeed &&
(!!pinnedFeed || preferences.feeds.pinned.includes(feedInfo.uri))
const isPrimaryAlgo =
(primaryAlgo?.enabled &&
primaryAlgo?.uri &&
primaryAlgo.uri === feedInfo.uri) ||
(primaryAlgoVariables?.enabled && primaryAlgoVariables.uri === feedInfo.uri)
useSetTitle(feedInfo?.displayName)
@@ -258,6 +277,18 @@ export function ProfileFeedScreenInner({
_,
])
const onSetPrimaryAlgo = React.useCallback(async () => {
try {
playHaptic()
await setPrimaryAlgo({enabled: true, uri: feedInfo.uri})
} catch (e: any) {
Toast.show(_(msg`There was an issue contacting the server`))
logger.error('ProfileFeed: failed to set primary algo', {
message: e.message,
})
}
}, [setPrimaryAlgo, feedInfo, _, playHaptic])
const onPressShare = React.useCallback(() => {
const url = toShareUrl(feedInfo.route.href)
shareUrl(url)
@@ -294,18 +325,40 @@ export function ProfileFeedScreenInner({
avatarType="algo">
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
{feedInfo && hasSession && (
<NewButton
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
size="small"
variant="solid"
color={isPinned ? 'secondary' : 'primary'}
label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
onPress={onTogglePinned}>
<ButtonText>
{isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
</ButtonText>
</NewButton>
<>
{gate('reduced_onboarding_and_home_algo') && isPrimaryAlgo ? (
<NewButton
variant="solid"
color="secondary"
size="small"
label={_(
msg`This feed is already set as your primary algorithm.`,
)}
onPress={() => {
primaryAlgoDialogControl.open()
}}>
<ButtonIcon icon={Check} position="left" />
<ButtonText>
<Trans>Primary Algorithm</Trans>
</ButtonText>
</NewButton>
) : (
<NewButton
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
size="small"
variant="solid"
color={isPinned ? 'secondary' : 'primary'}
label={
isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)
}
onPress={onTogglePinned}>
<ButtonText>
{isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
</ButtonText>
</NewButton>
)}
</>
)}
<Menu.Root>
<Menu.Trigger label={_(msg`Open feed options menu`)}>
@@ -338,25 +391,42 @@ export function ProfileFeedScreenInner({
<Menu.Group>
{hasSession && (
<>
<Menu.Item
disabled={isSavePending || isRemovePending}
testID="feedHeaderDropdownToggleSavedBtn"
label={
isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)
}
onPress={onToggleSaved}>
<Menu.ItemText>
{isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)}
</Menu.ItemText>
<Menu.ItemIcon
icon={isSaved ? Trash : Plus}
position="right"
/>
</Menu.Item>
{!isPrimaryAlgo && (
<>
<Menu.Item
disabled={isSavePending || isRemovePending}
testID="feedHeaderDropdownToggleSavedBtn"
label={
isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)
}
onPress={onToggleSaved}>
<Menu.ItemText>
{isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)}
</Menu.ItemText>
<Menu.ItemIcon
icon={isSaved ? Trash : Plus}
position="right"
/>
</Menu.Item>
{gate('reduced_onboarding_and_home_algo') && (
<Menu.Item
disabled={isSetPrimaryAlgoPending}
testID="feedHeaderDropdownSetPrimaryAlgoBtn"
label={_(msg`Set as primary algorithm`)}
onPress={onSetPrimaryAlgo}>
<Menu.ItemText>
{_(msg`Set as primary algorithm`)}
</Menu.ItemText>
<Menu.ItemIcon icon={Home} position="right" />
</Menu.Item>
)}
</>
)}
<Menu.Item
testID="feedHeaderDropdownReportBtn"
@@ -385,6 +455,8 @@ export function ProfileFeedScreenInner({
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
/>
<PrimaryAlgoNoticeDialog control={primaryAlgoDialogControl} />
</>
)
}, [
@@ -403,6 +475,11 @@ export function ProfileFeedScreenInner({
onPressReport,
onPressShare,
t,
gate,
isPrimaryAlgo,
primaryAlgoDialogControl,
onSetPrimaryAlgo,
isSetPrimaryAlgoPending,
])
return (
+31 -15
View File
@@ -7,6 +7,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {track} from '#/lib/analytics/analytics'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {
usePinFeedMutation,
@@ -14,6 +15,7 @@ import {
useSetSaveFeedsMutation,
useUnpinFeedMutation,
} from '#/state/queries/preferences'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useSetMinimalShellMode} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics'
import {useHaptics} from 'lib/haptics'
@@ -112,6 +114,7 @@ export function SavedFeeds({}: Props) {
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
preferences={preferences}
/>
))
)
@@ -145,6 +148,7 @@ export function SavedFeeds({}: Props) {
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
preferences={preferences}
/>
))
)
@@ -179,6 +183,7 @@ function ListItem({
currentFeeds,
setSavedFeeds,
resetSaveFeedsMutationState,
preferences,
}: {
feedUri: string // uri
isPinned: boolean
@@ -187,6 +192,7 @@ function ListItem({
resetSaveFeedsMutationState: ReturnType<
typeof useSetSaveFeedsMutation
>['reset']
preferences: UsePreferencesQueryResponse
}) {
const pal = usePalette('default')
const {_} = useLingui()
@@ -195,6 +201,13 @@ function ListItem({
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
useUnpinFeedMutation()
const isPending = isPinPending || isUnpinPending
const gate = useGate()
const primaryAlgo = preferences.primaryAlgorithm
const isPrimaryAlgoExperimentEnabled = gate(
'reduced_onboarding_and_home_algo',
)
const isPrimaryAlgo = primaryAlgo?.enabled && primaryAlgo?.uri === feedUri
const showPinButton = !(isPrimaryAlgoExperimentEnabled && isPrimaryAlgo)
const onTogglePinned = React.useCallback(async () => {
playHaptic()
@@ -303,20 +316,24 @@ function ListItem({
showSaveBtn
showMinimalPlaceholder
/>
<Pressable
disabled={isPending}
accessibilityRole="button"
hitSlop={10}
onPress={onTogglePinned}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="thumb-tack"
size={20}
color={isPinned ? colors.blue3 : pal.colors.icon}
/>
</Pressable>
{showPinButton && (
<View style={{paddingRight: 16}}>
<Pressable
disabled={isPending}
accessibilityRole="button"
hitSlop={10}
onPress={onTogglePinned}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="thumb-tack"
size={20}
color={isPinned ? colors.blue3 : pal.colors.icon}
/>
</Pressable>
</View>
)}
</Pressable>
)
}
@@ -345,7 +362,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
paddingRight: 16,
},
webArrowButtonsContainer: {
paddingLeft: 16,
+14 -11
View File
@@ -1,16 +1,17 @@
import React from 'react'
import {View, StyleSheet} from 'react-native'
import {useNavigationState, useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {TextLink} from 'view/com/util/Link'
import {getCurrentRoute} from 'lib/routes/helpers'
import {useLingui} from '@lingui/react'
import {StyleSheet, View} from 'react-native'
import {msg} from '@lingui/macro'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {NavigationProp} from 'lib/routes/types'
import {useLingui} from '@lingui/react'
import {useNavigation, useNavigationState} from '@react-navigation/native'
import {emitSoftReset} from '#/state/events'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {usePalette} from 'lib/hooks/usePalette'
import {getCurrentRoute} from 'lib/routes/helpers'
import {NavigationProp} from 'lib/routes/types'
import {TextLink} from 'view/com/util/Link'
export function DesktopFeeds() {
const pal = usePalette('default')
@@ -33,8 +34,10 @@ export function DesktopFeeds() {
{pinnedFeedInfos.map(feedInfo => {
const uri = feedInfo.uri
let feed: FeedDescriptor
if (!uri) {
if (uri === 'home') {
feed = 'home'
} else if (uri === 'following') {
feed = 'following'
} else if (uri.includes('app.bsky.feed.generator')) {
feed = `feedgen|${uri}`
} else if (uri.includes('app.bsky.graph.list')) {