Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

FeedCard & ListCard cleanups (#4644)

* Extract ListCard from FeedCard

* Export FeedCard.Action and optionally include in ListCard

* Remove list dual usage from most of FeedCard

* Update usages of FeedCard and ListCard

* Add back list purpose logic

* Make Action comp easier to use, clarify list purpose

* Rename Action to SaveButton

authored by

Eric Bailey and committed by
GitHub
1a037d35 58a97db5

+198 -97
+30 -70
src/components/FeedCard.tsx
··· 18 18 useRemoveFeedMutation, 19 19 } from '#/state/queries/preferences' 20 20 import {sanitizeHandle} from 'lib/strings/handles' 21 - import {precacheFeedFromGeneratorView, precacheList} from 'state/queries/feed' 21 + import {precacheFeedFromGeneratorView} from 'state/queries/feed' 22 22 import {useSession} from 'state/session' 23 23 import {UserAvatar} from '#/view/com/util/UserAvatar' 24 24 import * as Toast from 'view/com/util/Toast' ··· 33 33 import {RichText} from '#/components/RichText' 34 34 import {Text} from '#/components/Typography' 35 35 36 - type Props = 37 - | { 38 - type: 'feed' 39 - view: AppBskyFeedDefs.GeneratorView 40 - } 41 - | { 42 - type: 'list' 43 - view: AppBskyGraphDefs.ListView 44 - } 36 + type Props = { 37 + view: AppBskyFeedDefs.GeneratorView 38 + } 45 39 46 40 export function Default(props: Props) { 47 - const {type, view} = props 48 - const displayName = type === 'feed' ? view.displayName : view.name 49 - const purpose = type === 'list' ? view.purpose : undefined 41 + const {view} = props 50 42 return ( 51 - <Link label={displayName} {...props}> 43 + <Link label={view.displayName} {...props}> 52 44 <Outer> 53 45 <Header> 54 46 <Avatar src={view.avatar} /> 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} /> 47 + <TitleAndByline title={view.displayName} creator={view.creator} /> 48 + <SaveButton view={view} pin /> 62 49 </Header> 63 50 <Description description={view.description} /> 64 - {type === 'feed' && <Likes count={view.likeCount || 0} />} 51 + <Likes count={view.likeCount || 0} /> 65 52 </Outer> 66 53 </Link> 67 54 ) 68 55 } 69 56 70 57 export function Link({ 71 - type, 72 58 view, 73 - label, 74 59 children, 60 + ...props 75 61 }: Props & Omit<LinkProps, 'to'>) { 76 62 const queryClient = useQueryClient() 77 63 ··· 79 65 return createProfileFeedHref({feed: view}) 80 66 }, [view]) 81 67 68 + React.useEffect(() => { 69 + precacheFeedFromGeneratorView(queryClient, view) 70 + }, [view, queryClient]) 71 + 82 72 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 - }}> 73 + <InternalLink to={href} {...props}> 93 74 {children} 94 75 </InternalLink> 95 76 ) ··· 132 113 export function TitleAndByline({ 133 114 title, 134 115 creator, 135 - type, 136 - purpose, 137 116 }: { 138 117 title: string 139 118 creator?: AppBskyActorDefs.ProfileViewBasic 140 - type: 'feed' | 'list' 141 - purpose?: AppBskyGraphDefs.ListView['purpose'] 142 119 }) { 143 120 const t = useTheme() 144 121 ··· 151 128 <Text 152 129 style={[a.leading_snug, t.atoms.text_contrast_medium]} 153 130 numberOfLines={1}> 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 - )} 131 + <Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans> 163 132 </Text> 164 133 )} 165 134 </View> ··· 221 190 ) 222 191 } 223 192 224 - export function Action({ 225 - uri, 193 + export function SaveButton({ 194 + view, 226 195 pin, 227 - type, 228 - purpose, 229 196 }: { 230 - uri: string 197 + view: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView 231 198 pin?: boolean 232 - type: 'feed' | 'list' 233 - purpose?: AppBskyGraphDefs.ListView['purpose'] 234 199 }) { 235 200 const {hasSession} = useSession() 236 - if ( 237 - !hasSession || 238 - (type === 'list' && purpose !== 'app.bsky.graph.defs#curatelist') 239 - ) 240 - return null 241 - return <ActionInner uri={uri} pin={pin} type={type} /> 201 + if (!hasSession) return null 202 + return <SaveButtonInner view={view} pin={pin} /> 242 203 } 243 204 244 - function ActionInner({ 245 - uri, 205 + function SaveButtonInner({ 206 + view, 246 207 pin, 247 - type, 248 208 }: { 249 - uri: string 209 + view: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView 250 210 pin?: boolean 251 - type: 'feed' | 'list' 252 211 }) { 253 212 const {_} = useLingui() 254 213 const {data: preferences} = usePreferencesQuery() ··· 256 215 useAddSavedFeedsMutation() 257 216 const {isPending: isRemovePending, mutateAsync: removeFeed} = 258 217 useRemoveFeedMutation() 218 + 219 + const uri = view.uri 220 + const type = view.uri.includes('app.bsky.feed.generator') ? 'feed' : 'list' 221 + 259 222 const savedFeedConfig = React.useMemo(() => { 260 223 return preferences?.savedFeeds?.find(feed => feed.value === uri) 261 224 }, [preferences?.savedFeeds, uri]) ··· 332 295 export function createProfileFeedHref({ 333 296 feed, 334 297 }: { 335 - feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView 298 + feed: AppBskyFeedDefs.GeneratorView 336 299 }) { 337 300 const urip = new AtUri(feed.uri) 338 - const type = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'list' 339 301 const handleOrDid = feed.creator.handle || feed.creator.did 340 - return `/profile/${handleOrDid}/${type === 'feed' ? 'feed' : 'lists'}/${ 341 - urip.rkey 342 - }` 302 + return `/profile/${handleOrDid}/feed/${urip.rkey}` 343 303 }
+129
src/components/ListCard.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api' 4 + import {Trans} from '@lingui/macro' 5 + import {useQueryClient} from '@tanstack/react-query' 6 + 7 + import {sanitizeHandle} from 'lib/strings/handles' 8 + import {precacheList} from 'state/queries/feed' 9 + import {useTheme} from '#/alf' 10 + import {atoms as a} from '#/alf' 11 + import { 12 + Avatar, 13 + Description, 14 + Header, 15 + Outer, 16 + SaveButton, 17 + } from '#/components/FeedCard' 18 + import {Link as InternalLink, LinkProps} from '#/components/Link' 19 + import {Text} from '#/components/Typography' 20 + 21 + /* 22 + * This component is based on `FeedCard` and is tightly coupled with that 23 + * component. Please refer to `FeedCard` for more context. 24 + */ 25 + 26 + export { 27 + Avatar, 28 + AvatarPlaceholder, 29 + Description, 30 + Header, 31 + Outer, 32 + SaveButton, 33 + TitleAndBylinePlaceholder, 34 + } from '#/components/FeedCard' 35 + 36 + const CURATELIST = 'app.bsky.graph.defs#curatelist' 37 + const MODLIST = 'app.bsky.graph.defs#modlist' 38 + 39 + type Props = { 40 + view: AppBskyGraphDefs.ListView 41 + showPinButton?: boolean 42 + } 43 + 44 + export function Default(props: Props) { 45 + const {view, showPinButton} = props 46 + return ( 47 + <Link label={view.name} {...props}> 48 + <Outer> 49 + <Header> 50 + <Avatar src={view.avatar} /> 51 + <TitleAndByline 52 + title={view.name} 53 + creator={view.creator} 54 + purpose={view.purpose} 55 + /> 56 + {showPinButton && view.purpose === CURATELIST && ( 57 + <SaveButton view={view} pin /> 58 + )} 59 + </Header> 60 + <Description description={view.description} /> 61 + </Outer> 62 + </Link> 63 + ) 64 + } 65 + 66 + export function Link({ 67 + view, 68 + children, 69 + ...props 70 + }: Props & Omit<LinkProps, 'to'>) { 71 + const queryClient = useQueryClient() 72 + 73 + const href = React.useMemo(() => { 74 + return createProfileListHref({list: view}) 75 + }, [view]) 76 + 77 + React.useEffect(() => { 78 + precacheList(queryClient, view) 79 + }, [view, queryClient]) 80 + 81 + return ( 82 + <InternalLink to={href} {...props}> 83 + {children} 84 + </InternalLink> 85 + ) 86 + } 87 + 88 + export function TitleAndByline({ 89 + title, 90 + creator, 91 + purpose = CURATELIST, 92 + }: { 93 + title: string 94 + creator?: AppBskyActorDefs.ProfileViewBasic 95 + purpose?: AppBskyGraphDefs.ListView['purpose'] 96 + }) { 97 + const t = useTheme() 98 + 99 + return ( 100 + <View style={[a.flex_1]}> 101 + <Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> 102 + {title} 103 + </Text> 104 + {creator && ( 105 + <Text 106 + style={[a.leading_snug, t.atoms.text_contrast_medium]} 107 + numberOfLines={1}> 108 + {purpose === MODLIST ? ( 109 + <Trans> 110 + Moderation list by {sanitizeHandle(creator.handle, '@')} 111 + </Trans> 112 + ) : ( 113 + <Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans> 114 + )} 115 + </Text> 116 + )} 117 + </View> 118 + ) 119 + } 120 + 121 + export function createProfileListHref({ 122 + list, 123 + }: { 124 + list: AppBskyGraphDefs.ListView 125 + }) { 126 + const urip = new AtUri(list.uri) 127 + const handleOrDid = list.creator.handle || list.creator.did 128 + return `/profile/${handleOrDid}/lists/${urip.rkey}` 129 + }
+1 -1
src/components/StarterPack/Main/FeedsList.tsx
··· 45 45 (isWeb || index !== 0) && a.border_t, 46 46 t.atoms.border_contrast_low, 47 47 ]}> 48 - <FeedCard.Default type="feed" view={item} /> 48 + <FeedCard.Default view={item} /> 49 49 </View> 50 50 ) 51 51 }
+1 -1
src/screens/StarterPack/StarterPackLandingScreen.tsx
··· 316 316 t.atoms.border_contrast_low, 317 317 ]} 318 318 key={feed.uri}> 319 - <FeedCard.Default type="feed" view={feed} /> 319 + <FeedCard.Default view={feed} /> 320 320 </View> 321 321 ))} 322 322 </View>
+1 -1
src/view/com/feeds/ProfileFeedgens.tsx
··· 163 163 a.px_lg, 164 164 a.py_lg, 165 165 ]}> 166 - <FeedCard.Default type="feed" view={item} /> 166 + <FeedCard.Default view={item} /> 167 167 </View> 168 168 ) 169 169 }
+2 -2
src/view/com/lists/ProfileLists.tsx
··· 18 18 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 19 19 import {EmptyState} from 'view/com/util/EmptyState' 20 20 import {atoms as a, useTheme} from '#/alf' 21 - import * as FeedCard from '#/components/FeedCard' 21 + import * as ListCard from '#/components/ListCard' 22 22 import {ErrorMessage} from '../util/error/ErrorMessage' 23 23 import {List, ListRef} from '../util/List' 24 24 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' ··· 172 172 a.px_lg, 173 173 a.py_lg, 174 174 ]}> 175 - <FeedCard.Default type="list" view={item} /> 175 + <ListCard.Default view={item} /> 176 176 </View> 177 177 ) 178 178 },
+32 -20
src/view/screens/Feeds.tsx
··· 41 41 import {Divider} from '#/components/Divider' 42 42 import * as FeedCard from '#/components/FeedCard' 43 43 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 44 + import * as ListCard from '#/components/ListCard' 44 45 45 46 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'> 46 47 ··· 495 496 } else if (item.type === 'popularFeed') { 496 497 return ( 497 498 <View style={[a.px_lg, a.pt_lg, a.gap_lg]}> 498 - <FeedCard.Default type="feed" view={item.feed} /> 499 + <FeedCard.Default view={item.feed} /> 499 500 <Divider /> 500 501 </View> 501 502 ) ··· 627 628 fill={t.palette.white} 628 629 /> 629 630 </View> 630 - <FeedCard.TitleAndByline title={_(msg`Following`)} type="feed" /> 631 + <FeedCard.TitleAndByline title={_(msg`Following`)} /> 631 632 </FeedCard.Header> 632 633 </View> 633 634 ) ··· 639 640 savedFeed: SavedFeedItem & {type: 'feed' | 'list'} 640 641 }) { 641 642 const t = useTheme() 642 - const {view: feed} = savedFeed 643 - const displayName = 644 - savedFeed.type === 'feed' ? savedFeed.view.displayName : savedFeed.view.name 643 + 644 + const commonStyle = [ 645 + a.flex_1, 646 + a.px_lg, 647 + a.py_md, 648 + a.border_b, 649 + t.atoms.border_contrast_low, 650 + ] 645 651 646 - return ( 647 - <FeedCard.Link testID={`saved-feed-${feed.displayName}`} {...savedFeed}> 652 + return savedFeed.type === 'feed' ? ( 653 + <FeedCard.Link 654 + testID={`saved-feed-${savedFeed.view.displayName}`} 655 + {...savedFeed}> 648 656 {({hovered, pressed}) => ( 649 657 <View 650 - style={[ 651 - a.flex_1, 652 - a.px_lg, 653 - a.py_md, 654 - a.border_b, 655 - t.atoms.border_contrast_low, 656 - (hovered || pressed) && t.atoms.bg_contrast_25, 657 - ]}> 658 + style={[commonStyle, (hovered || pressed) && t.atoms.bg_contrast_25]}> 658 659 <FeedCard.Header> 659 - <FeedCard.Avatar src={feed.avatar} size={28} /> 660 - <FeedCard.TitleAndByline 661 - title={displayName} 662 - type={savedFeed.type} 663 - /> 660 + <FeedCard.Avatar src={savedFeed.view.avatar} size={28} /> 661 + <FeedCard.TitleAndByline title={savedFeed.view.displayName} /> 664 662 665 663 <ChevronRight size="sm" fill={t.atoms.text_contrast_low.color} /> 666 664 </FeedCard.Header> 667 665 </View> 668 666 )} 669 667 </FeedCard.Link> 668 + ) : ( 669 + <ListCard.Link testID={`saved-feed-${savedFeed.view.name}`} {...savedFeed}> 670 + {({hovered, pressed}) => ( 671 + <View 672 + style={[commonStyle, (hovered || pressed) && t.atoms.bg_contrast_25]}> 673 + <ListCard.Header> 674 + <ListCard.Avatar src={savedFeed.view.avatar} size={28} /> 675 + <ListCard.TitleAndByline title={savedFeed.view.name} /> 676 + 677 + <ChevronRight size="sm" fill={t.atoms.text_contrast_low.color} /> 678 + </ListCard.Header> 679 + </View> 680 + )} 681 + </ListCard.Link> 670 682 ) 671 683 } 672 684
+1 -1
src/view/screens/Search/Explore.tsx
··· 505 505 a.px_lg, 506 506 a.py_lg, 507 507 ]}> 508 - <FeedCard.Default type="feed" view={item.feed} /> 508 + <FeedCard.Default view={item.feed} /> 509 509 </View> 510 510 ) 511 511 }
+1 -1
src/view/screens/Search/Search.tsx
··· 306 306 a.px_lg, 307 307 a.py_lg, 308 308 ]}> 309 - <FeedCard.Default type="feed" view={item} /> 309 + <FeedCard.Default view={item} /> 310 310 </View> 311 311 )} 312 312 keyExtractor={item => item.uri}