Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 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}