diff --git a/assets/icons/pin_filled_stroke2_corner0_rounded.svg b/assets/icons/pin_filled_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..a2e71b967c
--- /dev/null
+++ b/assets/icons/pin_filled_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index cf00215264..7443128d2c 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -55,7 +55,6 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
import {PostThreadScreen} from '#/view/screens/PostThread'
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile'
-import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
import {ProfileListScreen} from '#/view/screens/ProfileList'
import {SavedFeeds} from '#/view/screens/SavedFeeds'
@@ -75,6 +74,7 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
+import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index 0cfe09aadc..cb97a7065b 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -60,6 +60,7 @@ export function createThemes({
dim: Theme
} {
const color = {
+ like: '#ec4899',
trueBlack: '#000000',
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
@@ -124,6 +125,7 @@ export function createThemes({
const lightPalette = {
white: color.gray_0,
black: color.gray_1000,
+ like: color.like,
contrast_25: color.gray_25,
contrast_50: color.gray_50,
@@ -185,6 +187,7 @@ export function createThemes({
const darkPalette: Palette = {
white: color.gray_25,
black: color.trueBlack,
+ like: color.like,
contrast_25: color.gray_975,
contrast_50: color.gray_950,
@@ -246,6 +249,7 @@ export function createThemes({
const dimPalette: Palette = {
...darkPalette,
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
+ like: color.like,
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
diff --git a/src/alf/types.ts b/src/alf/types.ts
index 08ec593927..5bac690e2b 100644
--- a/src/alf/types.ts
+++ b/src/alf/types.ts
@@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark'
export type Palette = {
white: string
black: string
+ like: string
contrast_25: string
contrast_50: string
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index d08505fbfd..8532cbbb49 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
export type ScreenProps = React.ComponentProps & {
style?: StyleProp
+ noInsetTop?: boolean
}
/**
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps & {
*/
export const Screen = React.memo(function Screen({
style,
+ noInsetTop,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
<>
{isWeb && }
>
diff --git a/src/components/icons/Pin.tsx b/src/components/icons/Pin.tsx
index 03dbbac90e..d1c37f39a3 100644
--- a/src/components/icons/Pin.tsx
+++ b/src/components/icons/Pin.tsx
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
})
+
+export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
+ path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
+})
diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx
new file mode 100644
index 0000000000..7d48b5ac15
--- /dev/null
+++ b/src/screens/Profile/ProfileFeed/index.tsx
@@ -0,0 +1,227 @@
+import React, {useCallback, useMemo} from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useAnimatedRef} from 'react-native-reanimated'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useIsFocused, useNavigation} from '@react-navigation/native'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {ComposeIcon2} from '#/lib/icons'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {NavigationProp} from '#/lib/routes/types'
+import {makeRecordUri} from '#/lib/strings/url-helpers'
+import {s} from '#/lib/styles'
+import {isNative} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
+import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
+import {FeedDescriptor} from '#/state/queries/post-feed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {
+ usePreferencesQuery,
+ UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {truncateAndInvalidate} from '#/state/queries/util'
+import {useSession} from '#/state/session'
+import {useComposerControls} from '#/state/shell/composer'
+import {PostFeed} from '#/view/com/posts/PostFeed'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {Button} from '#/view/com/util/forms/Button'
+import {ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
+import {LoadingScreen} from '#/view/com/util/LoadingScreen'
+import {Text} from '#/view/com/util/text/Text'
+import {ProfileFeedHeader} from '#/screens/Profile/components/ProfileFeedHeader'
+import * as Layout from '#/components/Layout'
+
+type Props = NativeStackScreenProps
+export function ProfileFeedScreen(props: Props) {
+ const {rkey, name: handleOrDid} = props.route.params
+
+ const pal = usePalette('default')
+ const {_} = useLingui()
+ const navigation = useNavigation()
+
+ const uri = useMemo(
+ () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
+ [rkey, handleOrDid],
+ )
+ const {error, data: resolvedUri} = useResolveUriQuery(uri)
+
+ const onPressBack = React.useCallback(() => {
+ if (navigation.canGoBack()) {
+ navigation.goBack()
+ } else {
+ navigation.navigate('Home')
+ }
+ }, [navigation])
+
+ if (error) {
+ return (
+
+
+
+
+ Could not load feed
+
+
+ {error.toString()}
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return resolvedUri ? (
+
+
+
+ ) : (
+
+
+
+ )
+}
+
+function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
+ const {data: preferences} = usePreferencesQuery()
+ const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
+
+ if (!preferences || !info) {
+ return
+ }
+
+ return (
+
+ )
+}
+
+export function ProfileFeedScreenInner({
+ feedInfo,
+}: {
+ preferences: UsePreferencesQueryResponse
+ feedInfo: FeedSourceFeedInfo
+}) {
+ const {_} = useLingui()
+ const {hasSession} = useSession()
+ const {openComposer} = useComposerControls()
+ const isScreenFocused = useIsFocused()
+
+ useSetTitle(feedInfo?.displayName)
+
+ const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor
+
+ const [hasNew, setHasNew] = React.useState(false)
+ const [isScrolledDown, setIsScrolledDown] = React.useState(false)
+ const queryClient = useQueryClient()
+ const feedFeedback = useFeedFeedback(feed, hasSession)
+ const scrollElRef = useAnimatedRef() as ListRef
+
+ const onScrollToTop = useCallback(() => {
+ scrollElRef.current?.scrollToOffset({
+ animated: isNative,
+ offset: 0, // -headerHeight,
+ })
+ truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
+ setHasNew(false)
+ }, [scrollElRef, queryClient, feed, setHasNew])
+
+ React.useEffect(() => {
+ if (!isScreenFocused) {
+ return
+ }
+ return listenSoftReset(onScrollToTop)
+ }, [onScrollToTop, isScreenFocused])
+
+ const renderPostsEmpty = useCallback(() => {
+ return
+ }, [_])
+
+ return (
+ <>
+
+
+
+
+
+
+ {(isScrolledDown || hasNew) && (
+
+ )}
+
+ {hasSession && (
+ openComposer({})}
+ icon={
+
+ }
+ accessibilityRole="button"
+ accessibilityLabel={_(msg`New post`)}
+ accessibilityHint=""
+ />
+ )}
+ >
+ )
+}
+
+const styles = StyleSheet.create({
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ paddingVertical: 7,
+ paddingHorizontal: 14,
+ borderRadius: 50,
+ marginLeft: 6,
+ },
+ notFoundContainer: {
+ margin: 10,
+ paddingHorizontal: 18,
+ paddingVertical: 14,
+ borderRadius: 6,
+ },
+ aboutSectionContainer: {
+ paddingVertical: 4,
+ paddingHorizontal: 16,
+ gap: 12,
+ },
+})
diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx
new file mode 100644
index 0000000000..0154d535c2
--- /dev/null
+++ b/src/screens/Profile/components/ProfileFeedHeader.tsx
@@ -0,0 +1,534 @@
+import React from 'react'
+import {View} from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {AtUri} from '@atproto/api'
+import {msg, Plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useHaptics} from '#/lib/haptics'
+import {makeProfileLink} from '#/lib/routes/links'
+import {makeCustomFeedLink} from '#/lib/routes/links'
+import {shareUrl} from '#/lib/sharing'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {logger} from '#/logger'
+import {FeedSourceFeedInfo} from '#/state/queries/feed'
+import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
+import {
+ useAddSavedFeedsMutation,
+ usePreferencesQuery,
+ useRemoveFeedMutation,
+ useUpdateSavedFeedsMutation,
+} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {formatCount} from '#/view/com/util/numeric/format'
+import * as Toast from '#/view/com/util/Toast'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Divider} from '#/components/Divider'
+import {useRichText} from '#/components/hooks/useRichText'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {
+ Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
+ Heart2_Stroke2_Corner0_Rounded as Heart,
+} from '#/components/icons/Heart2'
+import {
+ Pin_Filled_Corner0_Rounded as PinFilled,
+ Pin_Stroke2_Corner0_Rounded as Pin,
+} from '#/components/icons/Pin'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import * as Layout from '#/components/Layout'
+import {InlineLinkText} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import * as Menu from '#/components/Menu'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
+
+export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
+ const t = useTheme()
+ const {_, i18n} = useLingui()
+ const {hasSession} = useSession()
+ const {gtPhone, gtMobile} = useBreakpoints()
+ const {top} = useSafeAreaInsets()
+ const infoControl = Dialog.useDialogControl()
+ const playHaptic = useHaptics()
+
+ const {data: preferences} = usePreferencesQuery()
+
+ const [likeUri, setLikeUri] = React.useState(info.likeUri || '')
+ const isLiked = !!likeUri
+ const likeCount =
+ isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0
+
+ const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
+ useAddSavedFeedsMutation()
+ const {mutateAsync: removeFeed, isPending: isRemovePending} =
+ useRemoveFeedMutation()
+ const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
+ useUpdateSavedFeedsMutation()
+
+ const isFeedStateChangePending =
+ isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
+ const savedFeedConfig = preferences?.savedFeeds?.find(
+ f => f.value === info.uri,
+ )
+ const isSaved = Boolean(savedFeedConfig)
+ const isPinned = Boolean(savedFeedConfig?.pinned)
+
+ const onToggleSaved = React.useCallback(async () => {
+ try {
+ playHaptic()
+
+ if (savedFeedConfig) {
+ await removeFeed(savedFeedConfig)
+ Toast.show(_(msg`Removed from your feeds`))
+ } else {
+ await addSavedFeeds([
+ {
+ type: 'feed',
+ value: info.uri,
+ pinned: false,
+ },
+ ])
+ Toast.show(_(msg`Saved to your feeds`))
+ }
+ } catch (err) {
+ Toast.show(
+ _(
+ msg`There was an issue updating your feeds, please check your internet connection and try again.`,
+ ),
+ 'xmark',
+ )
+ logger.error('Failed to update feeds', {message: err})
+ }
+ }, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig])
+
+ const onTogglePinned = React.useCallback(async () => {
+ try {
+ playHaptic()
+
+ if (savedFeedConfig) {
+ const pinned = !savedFeedConfig.pinned
+ await updateSavedFeeds([
+ {
+ ...savedFeedConfig,
+ pinned,
+ },
+ ])
+
+ if (pinned) {
+ Toast.show(_(msg`Pinned ${info.displayName} to Home`))
+ } else {
+ Toast.show(_(msg`Unpinned ${info.displayName} from Home`))
+ }
+ } else {
+ await addSavedFeeds([
+ {
+ type: 'feed',
+ value: info.uri,
+ pinned: true,
+ },
+ ])
+ Toast.show(_(msg`Pinned ${info.displayName} to Home`))
+ }
+ } catch (e) {
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
+ logger.error('Failed to toggle pinned feed', {message: e})
+ }
+ }, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds])
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {hasSession && (
+
+ {isPinned ? (
+
+
+ {({props}) => {
+ return (
+
+ )
+ }}
+
+
+
+
+ {_(msg`Unpin from home`)}
+
+
+
+
+ {isSaved
+ ? _(msg`Remove from my feeds`)
+ : _(msg`Save to my feeds`)}
+
+
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function DialogInner({
+ info,
+ likeUri,
+ setLikeUri,
+ likeCount,
+ isPinned,
+ onTogglePinned,
+ isFeedStateChangePending,
+}: {
+ info: FeedSourceFeedInfo
+ likeUri: string
+ setLikeUri: (uri: string) => void
+ likeCount: number
+ isPinned: boolean
+ onTogglePinned: () => void
+ isFeedStateChangePending: boolean
+}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {hasSession} = useSession()
+ const playHaptic = useHaptics()
+ const control = Dialog.useDialogContext()
+ const reportDialogControl = useReportDialogControl()
+ const [rt, loading] = useRichText(info.description.text)
+ const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
+ const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
+ useUnlikeMutation()
+
+ const isLiked = !!likeUri
+ const feedRkey = React.useMemo(() => new AtUri(info.uri).rkey, [info.uri])
+
+ const onToggleLiked = React.useCallback(async () => {
+ try {
+ playHaptic()
+
+ if (isLiked && likeUri) {
+ await unlikeFeed({uri: likeUri})
+ setLikeUri('')
+ } else {
+ const res = await likeFeed({uri: info.uri, cid: info.cid})
+ setLikeUri(res.uri)
+ }
+ } catch (err) {
+ Toast.show(
+ _(
+ msg`There was an issue contacting the server, please check your internet connection and try again.`,
+ ),
+ 'xmark',
+ )
+ logger.error('Failed to toggle like', {message: err})
+ }
+ }, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _])
+
+ const onPressShare = React.useCallback(() => {
+ playHaptic()
+ const url = toShareUrl(info.route.href)
+ shareUrl(url)
+ }, [info, playHaptic])
+
+ const onPressReport = React.useCallback(() => {
+ reportDialogControl.open()
+ }, [reportDialogControl])
+
+ return loading ? (
+
+ ) : (
+
+
+
+
+
+
+ {info.displayName}
+
+
+
+ By{' '}
+ control.close()}>
+ {sanitizeHandle(info.creatorHandle, '@')}
+
+
+
+
+
+
+
+
+
+
+
+ {typeof likeCount === 'number' && (
+ control.close()}>
+
+ Liked by
+
+
+ )}
+
+
+ {hasSession && (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Something wrong? Let us know.
+
+
+
+
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
deleted file mode 100644
index c3f98c067f..0000000000
--- a/src/view/screens/ProfileFeed.tsx
+++ /dev/null
@@ -1,621 +0,0 @@
-import React, {useCallback, useMemo} from 'react'
-import {Pressable, StyleSheet, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Plural, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useIsFocused, useNavigation} from '@react-navigation/native'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useQueryClient} from '@tanstack/react-query'
-
-import {HITSLOP_20} from '#/lib/constants'
-import {useHaptics} from '#/lib/haptics'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {ComposeIcon2} from '#/lib/icons'
-import {makeCustomFeedLink} from '#/lib/routes/links'
-import {CommonNavigatorParams} from '#/lib/routes/types'
-import {NavigationProp} from '#/lib/routes/types'
-import {shareUrl} from '#/lib/sharing'
-import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {toShareUrl} from '#/lib/strings/url-helpers'
-import {s} from '#/lib/styles'
-import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
-import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
-import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
-import {FeedDescriptor} from '#/state/queries/post-feed'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
-import {
- useAddSavedFeedsMutation,
- usePreferencesQuery,
- UsePreferencesQueryResponse,
- useRemoveFeedMutation,
- useUpdateSavedFeedsMutation,
-} from '#/state/queries/preferences'
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {truncateAndInvalidate} from '#/state/queries/util'
-import {useSession} from '#/state/session'
-import {useComposerControls} from '#/state/shell/composer'
-import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
-import {PostFeed} from '#/view/com/posts/PostFeed'
-import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
-import {EmptyState} from '#/view/com/util/EmptyState'
-import {FAB} from '#/view/com/util/fab/FAB'
-import {Button} from '#/view/com/util/forms/Button'
-import {ListRef} from '#/view/com/util/List'
-import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
-import {LoadingScreen} from '#/view/com/util/LoadingScreen'
-import {Text} from '#/view/com/util/text/Text'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
-import {Button as NewButton, ButtonText} from '#/components/Button'
-import {useRichText} from '#/components/hooks/useRichText'
-import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {
- Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
- Heart2_Stroke2_Corner0_Rounded as HeartOutline,
-} from '#/components/icons/Heart2'
-import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
-import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
-import * as Layout from '#/components/Layout'
-import {InlineLinkText} from '#/components/Link'
-import * as Menu from '#/components/Menu'
-import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
-import {RichText} from '#/components/RichText'
-
-const SECTION_TITLES = ['Posts']
-
-interface SectionRef {
- scrollToTop: () => void
-}
-
-type Props = NativeStackScreenProps
-export function ProfileFeedScreen(props: Props) {
- const {rkey, name: handleOrDid} = props.route.params
-
- const pal = usePalette('default')
- const {_} = useLingui()
- const navigation = useNavigation()
-
- const uri = useMemo(
- () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
- [rkey, handleOrDid],
- )
- const {error, data: resolvedUri} = useResolveUriQuery(uri)
-
- const onPressBack = React.useCallback(() => {
- if (navigation.canGoBack()) {
- navigation.goBack()
- } else {
- navigation.navigate('Home')
- }
- }, [navigation])
-
- if (error) {
- return (
-
-
-
-
- Could not load feed
-
-
- {error.toString()}
-
-
-
-
-
-
-
-
- )
- }
-
- return resolvedUri ? (
-
-
-
- ) : (
-
-
-
- )
-}
-
-function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
- const {data: preferences} = usePreferencesQuery()
- const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
-
- if (!preferences || !info) {
- return
- }
-
- return (
-
- )
-}
-
-export function ProfileFeedScreenInner({
- preferences,
- feedInfo,
-}: {
- preferences: UsePreferencesQueryResponse
- feedInfo: FeedSourceFeedInfo
-}) {
- const {_} = useLingui()
- const t = useTheme()
- const {hasSession, currentAccount} = useSession()
- const reportDialogControl = useReportDialogControl()
- const {openComposer} = useComposerControls()
- const playHaptic = useHaptics()
- const feedSectionRef = React.useRef(null)
- const isScreenFocused = useIsFocused()
-
- const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
- useAddSavedFeedsMutation()
- const {mutateAsync: removeFeed, isPending: isRemovePending} =
- useRemoveFeedMutation()
- const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
- useUpdateSavedFeedsMutation()
-
- const isPending =
- isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
- const savedFeedConfig = preferences.savedFeeds.find(
- f => f.value === feedInfo.uri,
- )
- const isSaved = Boolean(savedFeedConfig)
- const isPinned = Boolean(savedFeedConfig?.pinned)
-
- useSetTitle(feedInfo?.displayName)
-
- // event handlers
- //
-
- const onToggleSaved = React.useCallback(async () => {
- try {
- playHaptic()
-
- if (savedFeedConfig) {
- await removeFeed(savedFeedConfig)
- Toast.show(_(msg`Removed from your feeds`))
- } else {
- await addSavedFeeds([
- {
- type: 'feed',
- value: feedInfo.uri,
- pinned: false,
- },
- ])
- Toast.show(_(msg`Saved to your feeds`))
- }
- } catch (err) {
- Toast.show(
- _(
- msg`There was an issue updating your feeds, please check your internet connection and try again.`,
- ),
- 'xmark',
- )
- logger.error('Failed to update feeds', {message: err})
- }
- }, [_, playHaptic, feedInfo, removeFeed, addSavedFeeds, savedFeedConfig])
-
- const onTogglePinned = React.useCallback(async () => {
- try {
- playHaptic()
-
- if (savedFeedConfig) {
- await updateSavedFeeds([
- {
- ...savedFeedConfig,
- pinned: !savedFeedConfig.pinned,
- },
- ])
- } else {
- await addSavedFeeds([
- {
- type: 'feed',
- value: feedInfo.uri,
- pinned: true,
- },
- ])
- }
- } catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
- logger.error('Failed to toggle pinned feed', {message: e})
- }
- }, [
- playHaptic,
- feedInfo,
- _,
- savedFeedConfig,
- updateSavedFeeds,
- addSavedFeeds,
- ])
-
- const onPressShare = React.useCallback(() => {
- const url = toShareUrl(feedInfo.route.href)
- shareUrl(url)
- }, [feedInfo])
-
- const onPressReport = React.useCallback(() => {
- reportDialogControl.open()
- }, [reportDialogControl])
-
- const onCurrentPageSelected = React.useCallback(
- (index: number) => {
- if (index === 0) {
- feedSectionRef.current?.scrollToTop()
- }
- },
- [feedSectionRef],
- )
-
- const renderHeader = useCallback(() => {
- return (
- <>
-
-
- {feedInfo && hasSession && (
-
-
- {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
-
-
- )}
-
-
- {({props, state}) => {
- return (
-
-
-
- )
- }}
-
-
-
-
- {hasSession && (
- <>
-
-
- {isSaved
- ? _(msg`Remove from my feeds`)
- : _(msg`Save to my feeds`)}
-
-
-
-
-
- {_(msg`Report feed`)}
-
-
- >
- )}
-
-
- {_(msg`Share feed`)}
-
-
-
-
-
-
-
-
- >
- )
- }, [
- _,
- hasSession,
- feedInfo,
- isPinned,
- onTogglePinned,
- onToggleSaved,
- currentAccount?.did,
- isSaved,
- onPressReport,
- onPressShare,
- t,
- isPending,
- ])
-
- return (
- <>
-
-
- {({headerHeight, scrollElRef, isFocused}) => (
-
- )}
-
- {hasSession && (
- openComposer({})}
- icon={
-
- }
- accessibilityRole="button"
- accessibilityLabel={_(msg`New post`)}
- accessibilityHint=""
- />
- )}
- >
- )
-}
-
-interface FeedSectionProps {
- feed: FeedDescriptor
- headerHeight: number
- scrollElRef: ListRef
- isFocused: boolean
-}
-const FeedSection = React.forwardRef(
- function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
- const {_} = useLingui()
- const [hasNew, setHasNew] = React.useState(false)
- const [isScrolledDown, setIsScrolledDown] = React.useState(false)
- const queryClient = useQueryClient()
- const isScreenFocused = useIsFocused()
- const {hasSession} = useSession()
- const feedFeedback = useFeedFeedback(feed, hasSession)
-
- const onScrollToTop = useCallback(() => {
- scrollElRef.current?.scrollToOffset({
- animated: isNative,
- offset: -headerHeight,
- })
- truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
- setHasNew(false)
- }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
-
- React.useImperativeHandle(ref, () => ({
- scrollToTop: onScrollToTop,
- }))
-
- React.useEffect(() => {
- if (!isScreenFocused) {
- return
- }
- return listenSoftReset(onScrollToTop)
- }, [onScrollToTop, isScreenFocused])
-
- const renderPostsEmpty = useCallback(() => {
- return
- }, [_])
-
- return (
-
-
-
-
- {(isScrolledDown || hasNew) && (
-
- )}
-
- )
- },
-)
-
-function AboutSection({
- feedOwnerDid,
- feedRkey,
- feedInfo,
-}: {
- feedOwnerDid: string
- feedRkey: string
- feedInfo: FeedSourceFeedInfo
-}) {
- const t = useTheme()
- const pal = usePalette('default')
- const {_} = useLingui()
- const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
- const {hasSession} = useSession()
- const playHaptic = useHaptics()
- const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
- const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
- useUnlikeMutation()
- const [resolvedRT] = useRichText(feedInfo.description.text || '')
-
- const isLiked = !!likeUri
- const likeCount =
- isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount
-
- const onToggleLiked = React.useCallback(async () => {
- try {
- playHaptic()
-
- if (isLiked && likeUri) {
- await unlikeFeed({uri: likeUri})
- setLikeUri('')
- } else {
- const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid})
- setLikeUri(res.uri)
- }
- } catch (err) {
- Toast.show(
- _(
- msg`There was an issue contacting the server, please check your internet connection and try again.`,
- ),
- 'xmark',
- )
- logger.error('Failed to toggle like', {message: err})
- }
- }, [playHaptic, isLiked, likeUri, unlikeFeed, likeFeed, feedInfo, _])
-
- return (
-
-
- {feedInfo.description ? (
-
- ) : (
-
- No description
-
- )}
-
-
-
-
- {isLiked ? (
-
- ) : (
-
- )}
-
- {typeof likeCount === 'number' && (
-
-
- Liked by
-
-
- )}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- btn: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- paddingVertical: 7,
- paddingHorizontal: 14,
- borderRadius: 50,
- marginLeft: 6,
- },
- notFoundContainer: {
- margin: 10,
- paddingHorizontal: 18,
- paddingVertical: 14,
- borderRadius: 6,
- },
- aboutSectionContainer: {
- paddingVertical: 4,
- paddingHorizontal: 16,
- gap: 12,
- },
-})