Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 187 lines 6.1 kB view raw
1import { 2 type JSX, 3 useCallback, 4 useEffect, 5 useMemo, 6 useRef, 7 useState, 8} from 'react' 9import {View} from 'react-native' 10import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' 11import {msg} from '@lingui/core/macro' 12import {useLingui} from '@lingui/react' 13import {type NavigationProp, useNavigation} from '@react-navigation/native' 14import {useQueryClient} from '@tanstack/react-query' 15 16import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants' 17import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 18import {ComposeIcon2} from '#/lib/icons' 19import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers' 20import {type AllNavigatorParams} from '#/lib/routes/types' 21import {s} from '#/lib/styles' 22import {listenSoftReset} from '#/state/events' 23import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 24import {useSetHomeBadge} from '#/state/home-badge' 25import {type FeedSourceInfo} from '#/state/queries/feed' 26import { 27 type FeedDescriptor, 28 type FeedParams, 29 RQKEY as FEED_RQKEY, 30} from '#/state/queries/post-feed' 31import {truncateAndInvalidate} from '#/state/queries/util' 32import {useSession} from '#/state/session' 33import {useSetMinimalShellMode} from '#/state/shell' 34import {useHeaderOffset} from '#/components/hooks/useHeaderOffset' 35import {useAnalytics} from '#/analytics' 36import {IS_NATIVE} from '#/env' 37import {PostFeed} from '../posts/PostFeed' 38import {FAB} from '../util/fab/FAB' 39import {type ListMethods} from '../util/List' 40import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' 41import {MainScrollProvider} from '../util/MainScrollProvider' 42 43const POLL_FREQ = 60e3 // 60sec 44 45export function FeedPage({ 46 testID, 47 isPageFocused, 48 isPageAdjacent, 49 feed, 50 feedParams, 51 renderEmptyState, 52 renderEndOfFeed, 53 savedFeedConfig, 54 feedInfo, 55}: { 56 testID?: string 57 feed: FeedDescriptor 58 feedParams?: FeedParams 59 isPageFocused: boolean 60 isPageAdjacent: boolean 61 renderEmptyState: () => JSX.Element 62 renderEndOfFeed?: () => JSX.Element 63 savedFeedConfig?: AppBskyActorDefs.SavedFeed 64 feedInfo: FeedSourceInfo 65}) { 66 const ax = useAnalytics() 67 const {hasSession} = useSession() 68 const {_} = useLingui() 69 const navigation = useNavigation<NavigationProp<AllNavigatorParams>>() 70 const queryClient = useQueryClient() 71 const {openComposer} = useOpenComposer() 72 const [isScrolledDown, setIsScrolledDown] = useState(false) 73 const setMinimalShellMode = useSetMinimalShellMode() 74 const headerOffset = useHeaderOffset() 75 const feedFeedback = useFeedFeedback(feedInfo, hasSession) 76 const scrollElRef = useRef<ListMethods>(null) 77 const [hasNew, setHasNew] = useState(false) 78 const setHomeBadge = useSetHomeBadge() 79 const isVideoFeed = useMemo(() => { 80 const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri) 81 const feedIsVideoMode = 82 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO 83 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode 84 return IS_NATIVE && _isVideoFeed 85 }, [feedInfo]) 86 87 useEffect(() => { 88 if (isPageFocused) { 89 setHomeBadge(hasNew) 90 } 91 }, [isPageFocused, hasNew, setHomeBadge]) 92 93 const scrollToTop = useCallback(() => { 94 scrollElRef.current?.scrollToOffset({ 95 animated: IS_NATIVE, 96 offset: -headerOffset, 97 }) 98 setMinimalShellMode(false) 99 }, [headerOffset, setMinimalShellMode]) 100 101 const onSoftReset = useCallback(() => { 102 const isScreenFocused = 103 getTabState(getRootNavigation(navigation).getState(), 'Home') === 104 TabState.InsideAtRoot 105 if (isScreenFocused && isPageFocused) { 106 scrollToTop() 107 truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) 108 setHasNew(false) 109 ax.metric('feed:refresh', { 110 feedType: feed.split('|')[0], 111 feedUrl: feed, 112 reason: 'soft-reset', 113 }) 114 } 115 }, [ax, navigation, isPageFocused, scrollToTop, queryClient, feed]) 116 117 // fires when page within screen is activated/deactivated 118 useEffect(() => { 119 if (!isPageFocused) { 120 return 121 } 122 return listenSoftReset(onSoftReset) 123 }, [onSoftReset, isPageFocused]) 124 125 const onPressCompose = useCallback(() => { 126 openComposer({logContext: 'Fab'}) 127 }, [openComposer]) 128 129 const onPressLoadLatest = useCallback(() => { 130 scrollToTop() 131 truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) 132 setHasNew(false) 133 ax.metric('feed:refresh', { 134 feedType: feed.split('|')[0], 135 feedUrl: feed, 136 reason: 'load-latest', 137 }) 138 }, [ax, scrollToTop, feed, queryClient]) 139 140 const shouldPrefetch = IS_NATIVE && isPageAdjacent 141 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI 142 return ( 143 <View 144 testID={testID} 145 // @ts-expect-error web only -sfn 146 dataSet={{nosnippet: isDiscoverFeed ? '' : undefined}}> 147 <MainScrollProvider> 148 <FeedFeedbackProvider value={feedFeedback}> 149 <PostFeed 150 testID={testID ? `${testID}-feed` : undefined} 151 enabled={isPageFocused || shouldPrefetch} 152 feed={feed} 153 feedParams={feedParams} 154 pollInterval={POLL_FREQ} 155 disablePoll={hasNew || !isPageFocused} 156 scrollElRef={scrollElRef} 157 onScrolledDownChange={setIsScrolledDown} 158 onHasNew={setHasNew} 159 renderEmptyState={renderEmptyState} 160 renderEndOfFeed={renderEndOfFeed} 161 headerOffset={headerOffset} 162 savedFeedConfig={savedFeedConfig} 163 isVideoFeed={isVideoFeed} 164 /> 165 </FeedFeedbackProvider> 166 </MainScrollProvider> 167 {(isScrolledDown || hasNew) && ( 168 <LoadLatestBtn 169 onPress={onPressLoadLatest} 170 label={_(msg`Load new posts`)} 171 showIndicator={hasNew} 172 /> 173 )} 174 175 {hasSession && ( 176 <FAB 177 testID="composeFAB" 178 onPress={onPressCompose} 179 icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 180 accessibilityRole="button" 181 accessibilityLabel={_(msg({message: `New post`, context: 'action'}))} 182 accessibilityHint="" 183 /> 184 )} 185 </View> 186 ) 187}