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 push-notifications 437 lines 12 kB view raw
1import React from 'react' 2import {observer} from 'mobx-react-lite' 3import { 4 Animated, 5 TouchableOpacity, 6 TouchableWithoutFeedback, 7 StyleSheet, 8 View, 9} from 'react-native' 10import {AppBskyEmbedImages} from '@atproto/api' 11import {AtUri} from '../../../third-party/uri' 12import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome' 13import {NotificationsViewItemModel} from '../../../state/models/notifications-view' 14import {PostThreadViewModel} from '../../../state/models/post-thread-view' 15import {s, colors} from '../../lib/styles' 16import {ago, pluralize} from '../../../lib/strings' 17import {HeartIconSolid} from '../../lib/icons' 18import {Text} from '../util/text/Text' 19import {UserAvatar} from '../util/UserAvatar' 20import {ImageHorzList} from '../util/images/ImageHorzList' 21import {ErrorMessage} from '../util/error/ErrorMessage' 22import {Post} from '../post/Post' 23import {Link} from '../util/Link' 24import {usePalette} from '../../lib/hooks/usePalette' 25import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue' 26 27const MAX_AUTHORS = 5 28 29const EXPANDED_AUTHOR_EL_HEIGHT = 35 30 31interface Author { 32 href: string 33 handle: string 34 displayName?: string 35 avatar?: string 36} 37 38export const FeedItem = observer(function FeedItem({ 39 item, 40}: { 41 item: NotificationsViewItemModel 42}) { 43 const pal = usePalette('default') 44 const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false) 45 const itemHref = React.useMemo(() => { 46 if (item.isUpvote || item.isRepost) { 47 const urip = new AtUri(item.subjectUri) 48 return `/profile/${urip.host}/post/${urip.rkey}` 49 } else if (item.isFollow || item.isAssertion) { 50 return `/profile/${item.author.handle}` 51 } else if (item.isReply) { 52 const urip = new AtUri(item.uri) 53 return `/profile/${urip.host}/post/${urip.rkey}` 54 } 55 return '' 56 }, [item]) 57 const itemTitle = React.useMemo(() => { 58 if (item.isUpvote || item.isRepost) { 59 return 'Post' 60 } else if (item.isFollow || item.isAssertion) { 61 return item.author.handle 62 } else if (item.isReply) { 63 return 'Post' 64 } 65 }, [item]) 66 67 const onToggleAuthorsExpanded = () => { 68 setAuthorsExpanded(!isAuthorsExpanded) 69 } 70 71 if (item.additionalPost?.notFound) { 72 // don't render anything if the target post was deleted or unfindable 73 return <View /> 74 } 75 76 if (item.isReply || item.isMention) { 77 return ( 78 <Link href={itemHref} title={itemTitle} noFeedback> 79 <Post 80 uri={item.uri} 81 initView={item.additionalPost} 82 style={ 83 item.isRead 84 ? undefined 85 : [ 86 styles.outerUnread, 87 {backgroundColor: pal.colors.unreadNotifBg}, 88 ] 89 } 90 /> 91 </Link> 92 ) 93 } 94 95 let action = '' 96 let icon: Props['icon'] | 'HeartIconSolid' 97 let iconStyle: Props['style'] = [] 98 if (item.isUpvote) { 99 action = 'liked your post' 100 icon = 'HeartIconSolid' 101 iconStyle = [s.red3, {position: 'relative', top: -4}] 102 } else if (item.isRepost) { 103 action = 'reposted your post' 104 icon = 'retweet' 105 iconStyle = [s.green3] 106 } else if (item.isReply) { 107 action = 'replied to your post' 108 icon = ['far', 'comment'] 109 } else if (item.isFollow) { 110 action = 'followed you' 111 icon = 'user-plus' 112 iconStyle = [s.blue3] 113 } else { 114 return <></> 115 } 116 117 let authors: Author[] = [ 118 { 119 href: `/profile/${item.author.handle}`, 120 handle: item.author.handle, 121 displayName: item.author.displayName, 122 avatar: item.author.avatar, 123 }, 124 ] 125 if (item.additional?.length) { 126 authors = authors.concat( 127 item.additional.map(item2 => ({ 128 href: `/profile/${item2.author.handle}`, 129 handle: item2.author.handle, 130 displayName: item2.author.displayName, 131 avatar: item2.author.avatar, 132 })), 133 ) 134 } 135 136 return ( 137 <Link 138 style={[ 139 styles.outer, 140 pal.view, 141 pal.border, 142 item.isRead 143 ? undefined 144 : [styles.outerUnread, {backgroundColor: pal.colors.unreadNotifBg}], 145 ]} 146 href={itemHref} 147 title={itemTitle} 148 noFeedback> 149 <View style={styles.layout}> 150 <View style={styles.layoutIcon}> 151 {icon === 'HeartIconSolid' ? ( 152 <HeartIconSolid size={28} style={[styles.icon, ...iconStyle]} /> 153 ) : ( 154 <FontAwesomeIcon 155 icon={icon} 156 size={24} 157 style={[styles.icon, ...iconStyle]} 158 /> 159 )} 160 </View> 161 <View style={styles.layoutContent}> 162 <TouchableWithoutFeedback 163 onPress={authors.length > 1 ? onToggleAuthorsExpanded : () => {}}> 164 <View> 165 <CondensedAuthorsList 166 visible={!isAuthorsExpanded} 167 authors={authors} 168 onToggleAuthorsExpanded={onToggleAuthorsExpanded} 169 /> 170 <ExpandedAuthorsList 171 visible={isAuthorsExpanded} 172 authors={authors} 173 /> 174 <View style={styles.meta}> 175 <Link 176 key={authors[0].href} 177 style={styles.metaItem} 178 href={authors[0].href} 179 title={`@${authors[0].handle}`}> 180 <Text style={[pal.text, s.bold]}> 181 {authors[0].displayName || authors[0].handle} 182 </Text> 183 </Link> 184 {authors.length > 1 ? ( 185 <> 186 <Text style={[styles.metaItem, pal.text]}>and</Text> 187 <Text style={[styles.metaItem, pal.text, s.bold]}> 188 {authors.length - 1}{' '} 189 {pluralize(authors.length - 1, 'other')} 190 </Text> 191 </> 192 ) : undefined} 193 <Text style={[styles.metaItem, pal.text]}>{action}</Text> 194 <Text style={[styles.metaItem, pal.textLight]}> 195 {ago(item.indexedAt)} 196 </Text> 197 </View> 198 </View> 199 </TouchableWithoutFeedback> 200 {item.isUpvote || item.isRepost ? ( 201 <AdditionalPostText additionalPost={item.additionalPost} /> 202 ) : ( 203 <></> 204 )} 205 </View> 206 </View> 207 </Link> 208 ) 209}) 210 211function CondensedAuthorsList({ 212 visible, 213 authors, 214 onToggleAuthorsExpanded, 215}: { 216 visible: boolean 217 authors: Author[] 218 onToggleAuthorsExpanded: () => void 219}) { 220 const pal = usePalette('default') 221 if (!visible) { 222 return ( 223 <View style={styles.avis}> 224 <TouchableOpacity 225 style={styles.expandedAuthorsCloseBtn} 226 onPress={onToggleAuthorsExpanded}> 227 <FontAwesomeIcon 228 icon="angle-up" 229 size={18} 230 style={[styles.expandedAuthorsCloseBtnIcon, pal.text]} 231 /> 232 <Text type="sm-medium" style={pal.text}> 233 Hide 234 </Text> 235 </TouchableOpacity> 236 </View> 237 ) 238 } 239 if (authors.length === 1) { 240 return ( 241 <View style={styles.avis}> 242 <Link 243 style={s.mr5} 244 href={authors[0].href} 245 title={`@${authors[0].handle}`}> 246 <UserAvatar 247 size={35} 248 displayName={authors[0].displayName} 249 handle={authors[0].handle} 250 avatar={authors[0].avatar} 251 /> 252 </Link> 253 </View> 254 ) 255 } 256 return ( 257 <View style={styles.avis}> 258 {authors.slice(0, MAX_AUTHORS).map(author => ( 259 <View key={author.href} style={s.mr5}> 260 <UserAvatar 261 size={35} 262 displayName={author.displayName} 263 handle={author.handle} 264 avatar={author.avatar} 265 /> 266 </View> 267 ))} 268 {authors.length > MAX_AUTHORS ? ( 269 <Text style={[styles.aviExtraCount, pal.textLight]}> 270 +{authors.length - MAX_AUTHORS} 271 </Text> 272 ) : undefined} 273 <FontAwesomeIcon 274 icon="angle-down" 275 size={18} 276 style={[styles.expandedAuthorsCloseBtnIcon, pal.textLight]} 277 /> 278 </View> 279 ) 280} 281 282function ExpandedAuthorsList({ 283 visible, 284 authors, 285}: { 286 visible: boolean 287 authors: Author[] 288}) { 289 const pal = usePalette('default') 290 const heightInterp = useAnimatedValue(visible ? 1 : 0) 291 const targetHeight = 292 authors.length * (EXPANDED_AUTHOR_EL_HEIGHT + 10) /*10=margin*/ 293 const heightStyle = { 294 height: Animated.multiply(heightInterp, targetHeight), 295 overflow: 'hidden', 296 } 297 React.useEffect(() => { 298 Animated.timing(heightInterp, { 299 toValue: visible ? 1 : 0, 300 duration: 200, 301 useNativeDriver: false, 302 }).start() 303 }, [heightInterp, visible]) 304 return ( 305 <Animated.View style={[heightStyle, visible ? s.mb10 : undefined]}> 306 {authors.map(author => ( 307 <Link 308 key={author.href} 309 href={author.href} 310 title={author.displayName || author.handle} 311 style={styles.expandedAuthor}> 312 <View style={styles.expandedAuthorAvi}> 313 <UserAvatar 314 size={35} 315 displayName={author.displayName} 316 handle={author.handle} 317 avatar={author.avatar} 318 /> 319 </View> 320 <View style={s.flex1}> 321 <Text type="lg-bold" numberOfLines={1} style={pal.text}> 322 {author.displayName || author.handle} 323 &nbsp; 324 <Text style={[pal.textLight]}>{author.handle}</Text> 325 </Text> 326 </View> 327 </Link> 328 ))} 329 </Animated.View> 330 ) 331} 332 333function AdditionalPostText({ 334 additionalPost, 335}: { 336 additionalPost?: PostThreadViewModel 337}) { 338 const pal = usePalette('default') 339 if (!additionalPost || !additionalPost.thread?.postRecord) { 340 return <View /> 341 } 342 if (additionalPost.error) { 343 return <ErrorMessage message={additionalPost.error} /> 344 } 345 const text = additionalPost.thread?.postRecord.text 346 const images = ( 347 additionalPost.thread.post.embed as AppBskyEmbedImages.Presented 348 )?.images 349 return ( 350 <> 351 {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>} 352 {images && images?.length > 0 && ( 353 <ImageHorzList 354 uris={images?.map(img => img.thumb)} 355 style={styles.additionalPostImages} 356 /> 357 )} 358 </> 359 ) 360} 361 362const styles = StyleSheet.create({ 363 outer: { 364 padding: 10, 365 paddingRight: 15, 366 borderTopWidth: 1, 367 }, 368 outerUnread: { 369 borderColor: colors.blue1, 370 }, 371 layout: { 372 flexDirection: 'row', 373 }, 374 layoutIcon: { 375 width: 70, 376 alignItems: 'flex-end', 377 paddingTop: 2, 378 }, 379 icon: { 380 marginRight: 10, 381 marginTop: 4, 382 }, 383 avis: { 384 flexDirection: 'row', 385 alignItems: 'center', 386 }, 387 aviExtraCount: { 388 fontWeight: 'bold', 389 paddingLeft: 6, 390 }, 391 layoutContent: { 392 flex: 1, 393 }, 394 meta: { 395 flexDirection: 'row', 396 flexWrap: 'wrap', 397 paddingTop: 6, 398 paddingBottom: 2, 399 }, 400 metaItem: { 401 paddingRight: 3, 402 }, 403 postText: { 404 paddingBottom: 5, 405 color: colors.black, 406 }, 407 additionalPostImages: { 408 marginTop: 5, 409 marginLeft: 2, 410 opacity: 0.8, 411 }, 412 413 addedContainer: { 414 paddingTop: 4, 415 paddingLeft: 36, 416 }, 417 418 expandedAuthorsCloseBtn: { 419 flexDirection: 'row', 420 alignItems: 'center', 421 paddingTop: 10, 422 paddingBottom: 6, 423 }, 424 expandedAuthorsCloseBtnIcon: { 425 marginLeft: 4, 426 marginRight: 4, 427 }, 428 expandedAuthor: { 429 flexDirection: 'row', 430 alignItems: 'center', 431 marginTop: 10, 432 height: EXPANDED_AUTHOR_EL_HEIGHT, 433 }, 434 expandedAuthorAvi: { 435 marginRight: 5, 436 }, 437})