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.

at a876aae44ea07494ebea9727350aa060b81f317b 225 lines 5.6 kB view raw
1import {type StyleProp, View, type ViewStyle} from 'react-native' 2import { 3 type $Typed, 4 AppBskyFeedDefs, 5 type AppBskyGraphDefs, 6 AtUri, 7} from '@atproto/api' 8import {msg} from '@lingui/core/macro' 9import {useLingui} from '@lingui/react' 10import {Plural, Trans} from '@lingui/react/macro' 11 12import {sanitizeHandle} from '#/lib/strings/handles' 13import { 14 type FeedSourceInfo, 15 hydrateFeedGenerator, 16 hydrateList, 17 useFeedSourceInfoQuery, 18} from '#/state/queries/feed' 19import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 20import {UserAvatar} from '#/view/com/util/UserAvatar' 21import {atoms as a, useTheme} from '#/alf' 22import {Link} from '#/components/Link' 23import {RichText} from '#/components/RichText' 24import {Text} from '#/components/Typography' 25import {MissingFeed} from './MissingFeed' 26 27type FeedSourceCardProps = { 28 feedUri: string 29 feedData?: 30 | $Typed<AppBskyFeedDefs.GeneratorView> 31 | $Typed<AppBskyGraphDefs.ListView> 32 style?: StyleProp<ViewStyle> 33 showSaveBtn?: boolean 34 showDescription?: boolean 35 showLikes?: boolean 36 pinOnSave?: boolean 37 showMinimalPlaceholder?: boolean 38 hideTopBorder?: boolean 39 link?: boolean 40} 41 42export function FeedSourceCard({ 43 feedUri, 44 feedData, 45 ...props 46}: FeedSourceCardProps) { 47 if (feedData) { 48 let feed: FeedSourceInfo 49 if (AppBskyFeedDefs.isGeneratorView(feedData)) { 50 feed = hydrateFeedGenerator(feedData) 51 } else { 52 feed = hydrateList(feedData) 53 } 54 return <FeedSourceCardLoaded feedUri={feedUri} feed={feed} {...props} /> 55 } else { 56 return <FeedSourceCardWithoutData feedUri={feedUri} {...props} /> 57 } 58} 59 60export function FeedSourceCardWithoutData({ 61 feedUri, 62 ...props 63}: Omit<FeedSourceCardProps, 'feedData'>) { 64 const {data: feed, error} = useFeedSourceInfoQuery({ 65 uri: feedUri, 66 }) 67 68 return ( 69 <FeedSourceCardLoaded 70 feedUri={feedUri} 71 feed={feed} 72 error={error} 73 {...props} 74 /> 75 ) 76} 77 78export function FeedSourceCardLoaded({ 79 feedUri, 80 feed, 81 style, 82 showDescription = false, 83 showLikes = false, 84 showMinimalPlaceholder, 85 hideTopBorder, 86 link = true, 87 error, 88}: { 89 feedUri: string 90 feed?: FeedSourceInfo 91 style?: StyleProp<ViewStyle> 92 showDescription?: boolean 93 showLikes?: boolean 94 showMinimalPlaceholder?: boolean 95 hideTopBorder?: boolean 96 link?: boolean 97 error?: unknown 98}) { 99 const t = useTheme() 100 const {_} = useLingui() 101 102 /* 103 * LOAD STATE 104 * 105 * This state also captures the scenario where a feed can't load for whatever 106 * reason. 107 */ 108 if (!feed) { 109 if (error) { 110 return ( 111 <MissingFeed 112 uri={feedUri} 113 style={style} 114 hideTopBorder={hideTopBorder} 115 error={error} 116 /> 117 ) 118 } 119 120 return ( 121 <FeedLoadingPlaceholder 122 style={[ 123 t.atoms.border_contrast_low, 124 !(showMinimalPlaceholder || hideTopBorder) && a.border_t, 125 a.flex_1, 126 style, 127 ]} 128 showTopBorder={false} 129 showLowerPlaceholder={!showMinimalPlaceholder} 130 /> 131 ) 132 } 133 134 const inner = ( 135 <> 136 <View style={[a.flex_row, a.align_center]}> 137 <View style={[a.mr_md]}> 138 <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 139 </View> 140 <View style={[a.flex_1]}> 141 <Text 142 emoji 143 style={[a.text_sm, a.font_semi_bold, a.leading_snug]} 144 numberOfLines={1}> 145 {feed.displayName} 146 </Text> 147 <Text 148 style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]} 149 numberOfLines={1}> 150 {feed.type === 'feed' ? ( 151 <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 152 ) : ( 153 <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 154 )} 155 </Text> 156 </View> 157 </View> 158 {showDescription && feed.description ? ( 159 <RichText 160 style={[t.atoms.text_contrast_high, a.flex_1, a.flex_wrap]} 161 value={feed.description} 162 numberOfLines={3} 163 /> 164 ) : null} 165 {showLikes && feed.type === 'feed' ? ( 166 <Text 167 style={[ 168 a.text_sm, 169 a.font_semi_bold, 170 t.atoms.text_contrast_medium, 171 a.leading_snug, 172 ]}> 173 <Trans> 174 Liked by{' '} 175 <Plural value={feed.likeCount || 0} one="# user" other="# users" /> 176 </Trans> 177 </Text> 178 ) : null} 179 </> 180 ) 181 182 if (link) { 183 return ( 184 <Link 185 testID={`feed-${feed.displayName}`} 186 label={ 187 feed.type === 'feed' 188 ? _( 189 msg`${feed.displayName}, a feed by ${sanitizeHandle(feed.creatorHandle, '@')}, liked by ${feed.likeCount || 0}`, 190 ) 191 : _( 192 msg`${feed.displayName}, a list by ${sanitizeHandle(feed.creatorHandle, '@')}`, 193 ) 194 } 195 to={{ 196 screen: feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList', 197 params: {name: feed.creatorDid, rkey: new AtUri(feed.uri).rkey}, 198 }} 199 style={[ 200 a.flex_1, 201 a.p_lg, 202 a.gap_md, 203 !hideTopBorder && !a.border_t, 204 t.atoms.border_contrast_low, 205 style, 206 ]}> 207 {inner} 208 </Link> 209 ) 210 } else { 211 return ( 212 <View 213 style={[ 214 a.flex_1, 215 a.p_lg, 216 a.gap_md, 217 !hideTopBorder && !a.border_t, 218 t.atoms.border_contrast_low, 219 style, 220 ]}> 221 {inner} 222 </View> 223 ) 224 } 225}