Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

Tweak feed card to prevent spinnerz when pushing to screen (#4600)

authored by

Hailey and committed by
GitHub
35f64535 1715afd8

+153 -105
+77 -25
src/components/FeedCard.tsx
··· 8 8 } from '@atproto/api' 9 9 import {msg, plural, Trans} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' 11 + import {useQueryClient} from '@tanstack/react-query' 11 12 12 13 import {logger} from '#/logger' 13 14 import { ··· 16 17 useRemoveFeedMutation, 17 18 } from '#/state/queries/preferences' 18 19 import {sanitizeHandle} from 'lib/strings/handles' 20 + import {precacheFeedFromGeneratorView, precacheList} from 'state/queries/feed' 19 21 import {useSession} from 'state/session' 20 22 import {UserAvatar} from '#/view/com/util/UserAvatar' 21 23 import * as Toast from 'view/com/util/Toast' ··· 31 33 import {RichText} from '#/components/RichText' 32 34 import {Text} from '#/components/Typography' 33 35 34 - export function Default({ 35 - type, 36 - view, 37 - }: 36 + type Props = 38 37 | { 39 38 type: 'feed' 40 39 view: AppBskyFeedDefs.GeneratorView ··· 42 41 | { 43 42 type: 'list' 44 43 view: AppBskyGraphDefs.ListView 45 - }) { 44 + } 45 + 46 + export function Default(props: Props) { 47 + const {type, view} = props 46 48 const displayName = type === 'feed' ? view.displayName : view.name 49 + const purpose = type === 'list' ? view.purpose : undefined 47 50 return ( 48 - <Link feed={view}> 51 + <Link label={displayName} {...props}> 49 52 <Outer> 50 53 <Header> 51 54 <Avatar src={view.avatar} /> 52 - <TitleAndByline title={displayName} creator={view.creator} /> 53 - <Action uri={view.uri} pin /> 55 + <TitleAndByline 56 + title={displayName} 57 + creator={view.creator} 58 + type={type} 59 + purpose={purpose} 60 + /> 61 + <Action uri={view.uri} pin type={type} purpose={purpose} /> 54 62 </Header> 55 63 <Description description={view.description} /> 56 64 {type === 'feed' && <Likes count={view.likeCount || 0} />} ··· 60 68 } 61 69 62 70 export function Link({ 71 + type, 72 + view, 73 + label, 63 74 children, 64 - feed, 65 - }: { 66 - feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView 67 - } & Omit<LinkProps, 'to'>) { 75 + }: Props & Omit<LinkProps, 'to'>) { 76 + const queryClient = useQueryClient() 77 + 68 78 const href = React.useMemo(() => { 69 - return createProfileFeedHref({feed}) 70 - }, [feed]) 71 - return <InternalLink to={href}>{children}</InternalLink> 79 + return createProfileFeedHref({feed: view}) 80 + }, [view]) 81 + 82 + return ( 83 + <InternalLink 84 + to={href} 85 + label={label} 86 + onPress={() => { 87 + if (type === 'feed') { 88 + precacheFeedFromGeneratorView(queryClient, view) 89 + } else { 90 + precacheList(queryClient, view) 91 + } 92 + }}> 93 + {children} 94 + </InternalLink> 95 + ) 72 96 } 73 97 74 98 export function Outer({children}: {children: React.ReactNode}) { ··· 108 132 export function TitleAndByline({ 109 133 title, 110 134 creator, 135 + type, 136 + purpose, 111 137 }: { 112 138 title: string 113 139 creator?: AppBskyActorDefs.ProfileViewBasic 140 + type: 'feed' | 'list' 141 + purpose?: AppBskyGraphDefs.ListView['purpose'] 114 142 }) { 115 143 const t = useTheme() 116 144 ··· 123 151 <Text 124 152 style={[a.leading_snug, t.atoms.text_contrast_medium]} 125 153 numberOfLines={1}> 126 - <Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans> 154 + {type === 'list' && purpose === 'app.bsky.graph.defs#curatelist' ? ( 155 + <Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans> 156 + ) : type === 'list' && purpose === 'app.bsky.graph.defs#modlist' ? ( 157 + <Trans> 158 + Moderation list by {sanitizeHandle(creator.handle, '@')} 159 + </Trans> 160 + ) : ( 161 + <Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans> 162 + )} 127 163 </Text> 128 164 )} 129 165 </View> ··· 184 220 ) 185 221 } 186 222 187 - export function Action({uri, pin}: {uri: string; pin?: boolean}) { 223 + export function Action({ 224 + uri, 225 + pin, 226 + type, 227 + purpose, 228 + }: { 229 + uri: string 230 + pin?: boolean 231 + type: 'feed' | 'list' 232 + purpose?: AppBskyGraphDefs.ListView['purpose'] 233 + }) { 188 234 const {hasSession} = useSession() 189 - if (!hasSession) return null 190 - return <ActionInner uri={uri} pin={pin} /> 235 + if (!hasSession || purpose !== 'app.bsky.graph.defs#curatelist') return null 236 + return <ActionInner uri={uri} pin={pin} type={type} /> 191 237 } 192 238 193 - function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { 239 + function ActionInner({ 240 + uri, 241 + pin, 242 + type, 243 + }: { 244 + uri: string 245 + pin?: boolean 246 + type: 'feed' | 'list' 247 + }) { 194 248 const {_} = useLingui() 195 249 const {data: preferences} = usePreferencesQuery() 196 250 const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} = ··· 198 252 const {isPending: isRemovePending, mutateAsync: removeFeed} = 199 253 useRemoveFeedMutation() 200 254 const savedFeedConfig = React.useMemo(() => { 201 - return preferences?.savedFeeds?.find( 202 - feed => feed.type === 'feed' && feed.value === uri, 203 - ) 255 + return preferences?.savedFeeds?.find(feed => feed.value === uri) 204 256 }, [preferences?.savedFeeds, uri]) 205 257 const removePromptControl = Prompt.usePromptControl() 206 258 const isPending = isAddSavedFeedPending || isRemovePending ··· 216 268 } else { 217 269 await saveFeeds([ 218 270 { 219 - type: 'feed', 271 + type, 220 272 value: uri, 221 273 pinned: pin || false, 222 274 }, ··· 228 280 Toast.show(_(msg`Failed to update feeds`)) 229 281 } 230 282 }, 231 - [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig], 283 + [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type], 232 284 ) 233 285 234 286 const onPrompRemoveFeed = React.useCallback(
+9 -1
src/state/queries/feed.ts
··· 578 578 ) 579 579 } 580 580 581 - function precacheList( 581 + export function precacheList( 582 582 queryClient: QueryClient, 583 583 list: AppBskyGraphDefs.ListView, 584 584 ) { ··· 588 588 list, 589 589 ) 590 590 } 591 + 592 + export function precacheFeedFromGeneratorView( 593 + queryClient: QueryClient, 594 + view: AppBskyFeedDefs.GeneratorView, 595 + ) { 596 + const hydratedFeed = hydrateFeedGenerator(view) 597 + precacheFeed(queryClient, hydratedFeed) 598 + }
+47 -58
src/view/com/feeds/ProfileFeedgens.tsx
··· 3 3 findNodeHandle, 4 4 ListRenderItemInfo, 5 5 StyleProp, 6 - StyleSheet, 7 6 View, 8 7 ViewStyle, 9 8 } from 'react-native' ··· 12 11 import {useQueryClient} from '@tanstack/react-query' 13 12 14 13 import {cleanError} from '#/lib/strings/errors' 15 - import {useTheme} from '#/lib/ThemeContext' 16 14 import {logger} from '#/logger' 17 15 import {isNative, isWeb} from '#/platform/detection' 18 - import {hydrateFeedGenerator} from '#/state/queries/feed' 19 16 import {usePreferencesQuery} from '#/state/queries/preferences' 20 17 import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 21 18 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 22 19 import {EmptyState} from 'view/com/util/EmptyState' 20 + import {atoms as a, useTheme} from '#/alf' 21 + import * as FeedCard from '#/components/FeedCard' 23 22 import {ErrorMessage} from '../util/error/ErrorMessage' 24 23 import {List, ListRef} from '../util/List' 25 24 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 26 - import {FeedSourceCardLoaded} from './FeedSourceCard' 27 25 28 26 const LOADING = {_reactKey: '__loading__'} 29 27 const EMPTY = {_reactKey: '__empty__'} ··· 52 50 ref, 53 51 ) { 54 52 const {_} = useLingui() 55 - const theme = useTheme() 53 + const t = useTheme() 56 54 const [isPTRing, setIsPTRing] = React.useState(false) 57 55 const opts = React.useMemo(() => ({enabled}), [enabled]) 58 56 const { ··· 79 77 items = items.concat([EMPTY]) 80 78 } else if (data?.pages) { 81 79 for (const page of data?.pages) { 82 - items = items.concat(page.feeds.map(feed => hydrateFeedGenerator(feed))) 80 + items = items.concat(page.feeds) 83 81 } 84 - } 85 - if (isError && !isEmpty) { 82 + } else if (isError && !isEmpty) { 86 83 items = items.concat([LOAD_MORE_ERROR_ITEM]) 87 84 } 88 85 return items ··· 132 129 // rendering 133 130 // = 134 131 135 - const renderItemInner = React.useCallback( 136 - ({item, index}: ListRenderItemInfo<any>) => { 137 - if (item === EMPTY) { 138 - return ( 139 - <EmptyState 140 - icon="hashtag" 141 - message={_(msg`You have no feeds.`)} 142 - testID="listsEmpty" 143 - /> 144 - ) 145 - } else if (item === ERROR_ITEM) { 146 - return ( 147 - <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 148 - ) 149 - } else if (item === LOAD_MORE_ERROR_ITEM) { 150 - return ( 151 - <LoadMoreRetryBtn 152 - label={_( 153 - msg`There was an issue fetching your lists. Tap here to try again.`, 154 - )} 155 - onPress={onPressRetryLoadMore} 156 - /> 157 - ) 158 - } else if (item === LOADING) { 159 - return <FeedLoadingPlaceholder /> 160 - } 161 - if (preferences) { 162 - return ( 163 - <FeedSourceCardLoaded 164 - feedUri={item.uri} 165 - feed={item} 166 - preferences={preferences} 167 - style={styles.item} 168 - showLikes 169 - hideTopBorder={index === 0 && !isWeb} 170 - /> 171 - ) 172 - } 173 - return null 174 - }, 175 - [error, refetch, onPressRetryLoadMore, preferences, _], 176 - ) 132 + const renderItem = ({item, index}: ListRenderItemInfo<any>) => { 133 + if (item === EMPTY) { 134 + return ( 135 + <EmptyState 136 + icon="hashtag" 137 + message={_(msg`You have no feeds.`)} 138 + testID="listsEmpty" 139 + /> 140 + ) 141 + } else if (item === ERROR_ITEM) { 142 + return ( 143 + <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 144 + ) 145 + } else if (item === LOAD_MORE_ERROR_ITEM) { 146 + return ( 147 + <LoadMoreRetryBtn 148 + label={_( 149 + msg`There was an issue fetching your lists. Tap here to try again.`, 150 + )} 151 + onPress={onPressRetryLoadMore} 152 + /> 153 + ) 154 + } else if (item === LOADING) { 155 + return <FeedLoadingPlaceholder /> 156 + } 157 + if (preferences) { 158 + return ( 159 + <View 160 + style={[ 161 + (index !== 0 || isWeb) && a.border_t, 162 + t.atoms.border_contrast_low, 163 + a.px_lg, 164 + a.py_lg, 165 + ]}> 166 + <FeedCard.Default type="feed" view={item} /> 167 + </View> 168 + ) 169 + } 170 + return null 171 + } 177 172 178 173 React.useEffect(() => { 179 174 if (enabled && scrollElRef.current) { ··· 189 184 ref={scrollElRef} 190 185 data={items} 191 186 keyExtractor={(item: any) => item._reactKey || item.uri} 192 - renderItem={renderItemInner} 187 + renderItem={renderItem} 193 188 refreshing={isPTRing} 194 189 onRefresh={onRefresh} 195 190 headerOffset={headerOffset} 196 191 contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}} 197 - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 192 + indicatorStyle={t.name === 'light' ? 'black' : 'white'} 198 193 removeClippedSubviews={true} 199 194 // @ts-ignore our .web version only -prf 200 195 desktopFixedHeight ··· 203 198 </View> 204 199 ) 205 200 }) 206 - 207 - const styles = StyleSheet.create({ 208 - item: { 209 - paddingHorizontal: 18, 210 - }, 211 - })
+14 -18
src/view/com/lists/ProfileLists.tsx
··· 3 3 findNodeHandle, 4 4 ListRenderItemInfo, 5 5 StyleProp, 6 - StyleSheet, 7 6 View, 8 7 ViewStyle, 9 8 } from 'react-native' ··· 12 11 import {useQueryClient} from '@tanstack/react-query' 13 12 14 13 import {cleanError} from '#/lib/strings/errors' 15 - import {useTheme} from '#/lib/ThemeContext' 16 14 import {logger} from '#/logger' 17 15 import {isNative, isWeb} from '#/platform/detection' 18 16 import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' 19 17 import {useAnalytics} from 'lib/analytics/analytics' 20 18 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 21 19 import {EmptyState} from 'view/com/util/EmptyState' 20 + import {atoms as a, useTheme} from '#/alf' 21 + import * as FeedCard from '#/components/FeedCard' 22 22 import {ErrorMessage} from '../util/error/ErrorMessage' 23 23 import {List, ListRef} from '../util/List' 24 24 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' 25 - import {ListCard} from './ListCard' 26 25 27 26 const LOADING = {_reactKey: '__loading__'} 28 27 const EMPTY = {_reactKey: '__empty__'} ··· 48 47 {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, 49 48 ref, 50 49 ) { 51 - const theme = useTheme() 50 + const t = useTheme() 52 51 const {track} = useAnalytics() 53 52 const {_} = useLingui() 54 53 const [isPTRing, setIsPTRing] = React.useState(false) ··· 166 165 return <FeedLoadingPlaceholder /> 167 166 } 168 167 return ( 169 - <ListCard 170 - list={item} 171 - testID={`list-${item.name}`} 172 - style={styles.item} 173 - noBorder={index === 0 && !isWeb} 174 - /> 168 + <View 169 + style={[ 170 + (index !== 0 || isWeb) && a.border_t, 171 + t.atoms.border_contrast_low, 172 + a.px_lg, 173 + a.py_lg, 174 + ]}> 175 + <FeedCard.Default type="list" view={item} /> 176 + </View> 175 177 ) 176 178 }, 177 - [error, refetch, onPressRetryLoadMore, _], 179 + [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], 178 180 ) 179 181 180 182 React.useEffect(() => { ··· 198 200 contentContainerStyle={ 199 201 isNative && {paddingBottom: headerOffset + 100} 200 202 } 201 - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 203 + indicatorStyle={t.name === 'light' ? 'black' : 'white'} 202 204 removeClippedSubviews={true} 203 205 // @ts-ignore our .web version only -prf 204 206 desktopFixedHeight ··· 208 210 ) 209 211 }, 210 212 ) 211 - 212 - const styles = StyleSheet.create({ 213 - item: { 214 - paddingHorizontal: 18, 215 - }, 216 - })
+6 -3
src/view/screens/Feeds.tsx
··· 627 627 fill={t.palette.white} 628 628 /> 629 629 </View> 630 - <FeedCard.TitleAndByline title={_(msg`Following`)} /> 630 + <FeedCard.TitleAndByline title={_(msg`Following`)} type="feed" /> 631 631 </FeedCard.Header> 632 632 </View> 633 633 ) ··· 644 644 savedFeed.type === 'feed' ? savedFeed.view.displayName : savedFeed.view.name 645 645 646 646 return ( 647 - <FeedCard.Link testID={`saved-feed-${feed.displayName}`} feed={feed}> 647 + <FeedCard.Link testID={`saved-feed-${feed.displayName}`} {...savedFeed}> 648 648 {({hovered, pressed}) => ( 649 649 <View 650 650 style={[ ··· 657 657 ]}> 658 658 <FeedCard.Header> 659 659 <FeedCard.Avatar src={feed.avatar} size={28} /> 660 - <FeedCard.TitleAndByline title={displayName} /> 660 + <FeedCard.TitleAndByline 661 + title={displayName} 662 + type={savedFeed.type} 663 + /> 661 664 662 665 <ChevronRight size="sm" fill={t.atoms.text_contrast_low.color} /> 663 666 </FeedCard.Header>