mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at rn-stack-repro 313 lines 9.3 kB view raw
1import React from 'react' 2import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4import {Text} from '../util/text/Text' 5import {RichText} from '#/components/RichText' 6import {usePalette} from 'lib/hooks/usePalette' 7import {s} from 'lib/styles' 8import {UserAvatar} from '../util/UserAvatar' 9import {useNavigation} from '@react-navigation/native' 10import {NavigationProp} from 'lib/routes/types' 11import {pluralize} from 'lib/strings/helpers' 12import {AtUri} from '@atproto/api' 13import * as Toast from 'view/com/util/Toast' 14import {sanitizeHandle} from 'lib/strings/handles' 15import {logger} from '#/logger' 16import {useModalControls} from '#/state/modals' 17import {Trans, msg} from '@lingui/macro' 18import {useLingui} from '@lingui/react' 19import { 20 usePinFeedMutation, 21 UsePreferencesQueryResponse, 22 usePreferencesQuery, 23 useSaveFeedMutation, 24 useRemoveFeedMutation, 25} from '#/state/queries/preferences' 26import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' 27import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 28import {useTheme} from '#/alf' 29 30export function FeedSourceCard({ 31 feedUri, 32 style, 33 showSaveBtn = false, 34 showDescription = false, 35 showLikes = false, 36 pinOnSave = false, 37 showMinimalPlaceholder, 38}: { 39 feedUri: string 40 style?: StyleProp<ViewStyle> 41 showSaveBtn?: boolean 42 showDescription?: boolean 43 showLikes?: boolean 44 pinOnSave?: boolean 45 showMinimalPlaceholder?: boolean 46}) { 47 const {data: preferences} = usePreferencesQuery() 48 const {data: feed} = useFeedSourceInfoQuery({uri: feedUri}) 49 50 return ( 51 <FeedSourceCardLoaded 52 feedUri={feedUri} 53 feed={feed} 54 preferences={preferences} 55 style={style} 56 showSaveBtn={showSaveBtn} 57 showDescription={showDescription} 58 showLikes={showLikes} 59 pinOnSave={pinOnSave} 60 showMinimalPlaceholder={showMinimalPlaceholder} 61 /> 62 ) 63} 64 65export function FeedSourceCardLoaded({ 66 feedUri, 67 feed, 68 preferences, 69 style, 70 showSaveBtn = false, 71 showDescription = false, 72 showLikes = false, 73 pinOnSave = false, 74 showMinimalPlaceholder, 75}: { 76 feedUri: string 77 feed?: FeedSourceInfo 78 preferences?: UsePreferencesQueryResponse 79 style?: StyleProp<ViewStyle> 80 showSaveBtn?: boolean 81 showDescription?: boolean 82 showLikes?: boolean 83 pinOnSave?: boolean 84 showMinimalPlaceholder?: boolean 85}) { 86 const t = useTheme() 87 const pal = usePalette('default') 88 const {_} = useLingui() 89 const navigation = useNavigation<NavigationProp>() 90 const {openModal} = useModalControls() 91 92 const {isPending: isSavePending, mutateAsync: saveFeed} = 93 useSaveFeedMutation() 94 const {isPending: isRemovePending, mutateAsync: removeFeed} = 95 useRemoveFeedMutation() 96 const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation() 97 98 const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || '')) 99 100 const onToggleSaved = React.useCallback(async () => { 101 // Only feeds can be un/saved, lists are handled elsewhere 102 if (feed?.type !== 'feed') return 103 104 if (isSaved) { 105 openModal({ 106 name: 'confirm', 107 title: _(msg`Remove from my feeds`), 108 message: _(msg`Remove ${feed?.displayName} from my feeds?`), 109 onPressConfirm: async () => { 110 try { 111 await removeFeed({uri: feed.uri}) 112 // await item.unsave() 113 Toast.show(_(msg`Removed from my feeds`)) 114 } catch (e) { 115 Toast.show(_(msg`There was an issue contacting your server`)) 116 logger.error('Failed to unsave feed', {message: e}) 117 } 118 }, 119 }) 120 } else { 121 try { 122 if (pinOnSave) { 123 await pinFeed({uri: feed.uri}) 124 } else { 125 await saveFeed({uri: feed.uri}) 126 } 127 Toast.show(_(msg`Added to my feeds`)) 128 } catch (e) { 129 Toast.show(_(msg`There was an issue contacting your server`)) 130 logger.error('Failed to save feed', {message: e}) 131 } 132 } 133 }, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed]) 134 135 /* 136 * LOAD STATE 137 * 138 * This state also captures the scenario where a feed can't load for whatever 139 * reason. 140 */ 141 if (!feed || !preferences) 142 return ( 143 <View 144 style={[ 145 pal.border, 146 { 147 borderTopWidth: showMinimalPlaceholder ? 0 : 1, 148 flexDirection: 'row', 149 alignItems: 'center', 150 flex: 1, 151 paddingRight: 18, 152 }, 153 ]}> 154 {showMinimalPlaceholder ? ( 155 <FeedLoadingPlaceholder 156 style={{flex: 1}} 157 showTopBorder={false} 158 showLowerPlaceholder={false} 159 /> 160 ) : ( 161 <FeedLoadingPlaceholder style={{flex: 1}} showTopBorder={false} /> 162 )} 163 164 {showSaveBtn && ( 165 <Pressable 166 testID={`feed-${feedUri}-toggleSave`} 167 disabled={isRemovePending} 168 accessibilityRole="button" 169 accessibilityLabel={_(msg`Remove from my feeds`)} 170 accessibilityHint="" 171 onPress={() => { 172 openModal({ 173 name: 'confirm', 174 title: _(msg`Remove from my feeds`), 175 message: _(msg`Remove this feed from my feeds?`), 176 onPressConfirm: async () => { 177 try { 178 await removeFeed({uri: feedUri}) 179 // await item.unsave() 180 Toast.show(_(msg`Removed from my feeds`)) 181 } catch (e) { 182 Toast.show( 183 _(msg`There was an issue contacting your server`), 184 ) 185 logger.error('Failed to unsave feed', {message: e}) 186 } 187 }, 188 }) 189 }} 190 hitSlop={15} 191 style={styles.btn}> 192 <FontAwesomeIcon 193 icon={['far', 'trash-can']} 194 size={19} 195 color={pal.colors.icon} 196 /> 197 </Pressable> 198 )} 199 </View> 200 ) 201 202 return ( 203 <Pressable 204 testID={`feed-${feed.displayName}`} 205 accessibilityRole="button" 206 style={[styles.container, pal.border, style]} 207 onPress={() => { 208 if (feed.type === 'feed') { 209 navigation.push('ProfileFeed', { 210 name: feed.creatorDid, 211 rkey: new AtUri(feed.uri).rkey, 212 }) 213 } else if (feed.type === 'list') { 214 navigation.push('ProfileList', { 215 name: feed.creatorDid, 216 rkey: new AtUri(feed.uri).rkey, 217 }) 218 } 219 }} 220 key={feed.uri}> 221 <View style={[styles.headerContainer]}> 222 <View style={[s.mr10]}> 223 <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 224 </View> 225 <View style={[styles.headerTextContainer]}> 226 <Text style={[pal.text, s.bold]} numberOfLines={3}> 227 {feed.displayName} 228 </Text> 229 <Text style={[pal.textLight]} numberOfLines={3}> 230 {feed.type === 'feed' ? ( 231 <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 232 ) : ( 233 <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 234 )} 235 </Text> 236 </View> 237 238 {showSaveBtn && feed.type === 'feed' && ( 239 <View style={[s.justifyCenter]}> 240 <Pressable 241 testID={`feed-${feed.displayName}-toggleSave`} 242 disabled={isSavePending || isPinPending || isRemovePending} 243 accessibilityRole="button" 244 accessibilityLabel={ 245 isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`) 246 } 247 accessibilityHint="" 248 onPress={onToggleSaved} 249 hitSlop={15} 250 style={styles.btn}> 251 {isSaved ? ( 252 <FontAwesomeIcon 253 icon={['far', 'trash-can']} 254 size={19} 255 color={pal.colors.icon} 256 /> 257 ) : ( 258 <FontAwesomeIcon 259 icon="plus" 260 size={18} 261 color={pal.colors.link} 262 /> 263 )} 264 </Pressable> 265 </View> 266 )} 267 </View> 268 269 {showDescription && feed.description ? ( 270 <RichText 271 style={[t.atoms.text_contrast_high, styles.description]} 272 value={feed.description} 273 numberOfLines={3} 274 /> 275 ) : null} 276 277 {showLikes && feed.type === 'feed' ? ( 278 <Text type="sm-medium" style={[pal.text, pal.textLight]}> 279 <Trans> 280 Liked by {feed.likeCount || 0}{' '} 281 {pluralize(feed.likeCount || 0, 'user')} 282 </Trans> 283 </Text> 284 ) : null} 285 </Pressable> 286 ) 287} 288 289const styles = StyleSheet.create({ 290 container: { 291 paddingHorizontal: 18, 292 paddingVertical: 20, 293 flexDirection: 'column', 294 flex: 1, 295 borderTopWidth: 1, 296 gap: 14, 297 }, 298 headerContainer: { 299 flexDirection: 'row', 300 }, 301 headerTextContainer: { 302 flexDirection: 'column', 303 columnGap: 4, 304 flex: 1, 305 }, 306 description: { 307 flex: 1, 308 flexWrap: 'wrap', 309 }, 310 btn: { 311 paddingVertical: 6, 312 }, 313})