diff --git a/src/components/HomeAlgoNoticeDialog.tsx b/src/components/HomeAlgoNoticeDialog.tsx new file mode 100644 index 0000000000..d84aa1250a --- /dev/null +++ b/src/components/HomeAlgoNoticeDialog.tsx @@ -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 HomeAlgoNoticeDialog({ + control, +}: { + control: DialogOuterProps['control'] +}) { + const {_} = useLingui() + return ( + + Your home algorithm + + This feed is set as your home algorithm, which is used as your home + screen when you open the app. + + + + + + ) +} diff --git a/src/components/icons/Home.tsx b/src/components/icons/Home.tsx new file mode 100644 index 0000000000..e150b7b813 --- /dev/null +++ b/src/components/icons/Home.tsx @@ -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', +}) diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 85e3f9a25d..c1a383672a 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -3,6 +3,7 @@ import { AppBskyActorDefs, BSKY_LABELER_DID, BskyFeedViewPreference, + BskyHomeAlgoPreference, LabelPreference, ModerationOpts, } from '@atproto/api' @@ -345,3 +346,17 @@ export function useRemoveMutedWordMutation() { }, }) } + +export function useSetHomeAlgoMutation() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (pref: BskyHomeAlgoPreference) => { + await getAgent().setHomeAlgoPref(pref) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 2e769f8d8a..d0c45bfc4d 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -25,6 +25,7 @@ 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 {HomeAlgoNoticeDialog} from '#/components/HomeAlgoNoticeDialog' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -312,16 +313,7 @@ export function FeedSourceCardLoaded({ confirmButtonColor="negative" /> - - Your home algorithm - - This feed is set as your home algorithm, which is used as your home - screen when you open the app. - - - - - + ) } diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 814c1e8558..8ae949b477 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -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, + useSetHomeAlgoMutation, useUnpinFeedMutation, } from '#/state/queries/preferences' import {useResolveUriQuery} from '#/state/queries/resolve-uri' @@ -52,14 +54,18 @@ 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 {HomeAlgoNoticeDialog} from '#/components/HomeAlgoNoticeDialog' 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' @@ -162,6 +168,10 @@ export function ProfileFeedScreenInner({ const playHaptic = useHaptics() const feedSectionRef = React.useRef(null) const isScreenFocused = useIsFocused() + const isHomeAlgoExperimentEnabled = useGate( + 'reduced_onboarding_and_home_algo', + ) + const homeAlgoDialogControl = useDialogControl() const { mutateAsync: saveFeed, @@ -187,6 +197,11 @@ export function ProfileFeedScreenInner({ reset: resetUnpinFeed, isPending: isUnpinPending, } = useUnpinFeedMutation() + const { + mutateAsync: setHomeAlgo, + variables: homeAlgoVariables, + isPending: isSetHomeAlgoPending, + } = useSetHomeAlgoMutation() const isSaved = !removedFeed && @@ -194,6 +209,11 @@ export function ProfileFeedScreenInner({ const isPinned = !unpinnedFeed && (!!pinnedFeed || preferences.feeds.pinned.includes(feedInfo.uri)) + const isHomeAlgo = + (preferences.homeAlgo?.enabled && + preferences.homeAlgo?.uri && + preferences.homeAlgo.uri === feedInfo.uri) || + (homeAlgoVariables?.enabled && homeAlgoVariables.uri === feedInfo.uri) useSetTitle(feedInfo?.displayName) @@ -258,6 +278,16 @@ export function ProfileFeedScreenInner({ _, ]) + const onSetHomeAlgo = React.useCallback(async () => { + try { + playHaptic() + await setHomeAlgo({enabled: true, uri: feedInfo.uri}) + } catch (e: any) { + Toast.show(_(msg`There was an issue contacting the server`)) + logger.error('ProfileFeed: failed to set home algo', {message: e.message}) + } + }, [setHomeAlgo, feedInfo, _, playHaptic]) + const onPressShare = React.useCallback(() => { const url = toShareUrl(feedInfo.route.href) shareUrl(url) @@ -293,20 +323,38 @@ export function ProfileFeedScreenInner({ } avatarType="algo"> - {feedInfo && hasSession && ( - - - {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)} - - - )} + {feedInfo && + hasSession && + (isHomeAlgoExperimentEnabled && isHomeAlgo ? ( + { + homeAlgoDialogControl.open() + }}> + + Home Algo + + ) : ( + + + {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)} + + + ))} {({props, state}) => { @@ -338,25 +386,40 @@ export function ProfileFeedScreenInner({ {hasSession && ( <> - - - {isSaved - ? _(msg`Remove from my feeds`) - : _(msg`Save to my feeds`)} - - - + {!isHomeAlgo && ( + <> + + + {isSaved + ? _(msg`Remove from my feeds`) + : _(msg`Save to my feeds`)} + + + + + + + {_(msg`Set as home algorithm`)} + + + + + )} + + ) }, [ @@ -403,6 +468,11 @@ export function ProfileFeedScreenInner({ onPressReport, onPressShare, t, + isHomeAlgoExperimentEnabled, + isHomeAlgo, + homeAlgoDialogControl, + onSetHomeAlgo, + isSetHomeAlgoPending, ]) return (