From f57a8cf8ba0cd10a54abf35d960d8fb90266fa6b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 1 Nov 2023 16:15:40 -0700 Subject: Lists updates: curate lists and blocklists (#1689) * Add lists screen * Update Lists screen and List create/edit modal to support curate lists * Rework the ProfileList screen and add curatelist support * More ProfileList progress * Update list modals * Rename mutelists to modlists * Layout updates/fixes * More layout fixes * Modal fixes * List list screen updates * Update feed page to give more info * Layout fixes to ListAddUser modal * Layout fixes to FlatList and Feed on desktop * Layout fix to LoadLatestBtn on Web * Handle did resolution before showing the ProfileList screen * Rename the CustomFeed routes to ProfileFeed for consistency * Fix layout issues with the pager and feeds * Factor out some common code * Fix UIs for mobile * Fix user list rendering * Fix: dont bubble custom feed errors in the merge feed * Refactor feed models to reduce usage of the SavedFeeds model * Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists * Add the ability to pin lists * Add pinned lists to mobile * Remove dead code * Rework the ProfileScreenHeader to create more real-estate for action buttons * Improve layout behavior on web mobile breakpoints * Refactor feed & list pages to use new Tabs layout component * Refactor to ProfileSubpageHeader * Implement modlist block and mute * Switch to new api and just modify state on modlist actions * Fix some UI overflows * Fix: dont show edit buttons on lists you dont own * Fix alignment issue on long titles * Improve loading and error states for feeds & lists * Update list dropdown icons for ios * Fetch feed display names in the mergefeed * Improve rendering off offline feeds in the feed-listing page * Update Feeds listing UI to react to changes in saved/pinned state * Refresh list and feed on posts tab press * Fix pinned feed ordering UI * Fixes to list pinning * Remove view=simple qp * Add list to feed tuners * Render richtext * Add list href * Add 'view avatar' * Remove unused import * Fix missing import * Correctly reflect block by list state * Replace the component with the more effective component * Improve the responsiveness of the PagerWithHeader * Fix visual jank in the feed loading state * Improve performance of the PagerWithHeader * Fix a case that would cause the header to animate too aggressively * Add the ability to scroll to top by tapping the selected tab * Fix unit test runner * Update modlists test * Add curatelist tests * Fix: remove link behavior in ListAddUser modal * Fix some layout jank in the PagerWithHeader on iOS * Simplify ListItems header rendering * Wait for the appview to recognize the list before proceeding with list creation * Fix glitch in the onPageSelecting index of the Pager * Fix until() * Copy fix Co-authored-by: Eric Bailey --------- Co-authored-by: Eric Bailey --- src/view/screens/ProfileFeed.tsx | 535 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 src/view/screens/ProfileFeed.tsx (limited to 'src/view/screens/ProfileFeed.tsx') diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx new file mode 100644 index 000000000..70e52bf7a --- /dev/null +++ b/src/view/screens/ProfileFeed.tsx @@ -0,0 +1,535 @@ +import React, {useMemo, useCallback} from 'react' +import {FlatList, StyleSheet, View, ActivityIndicator} from 'react-native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {useNavigation} from '@react-navigation/native' +import {usePalette} from 'lib/hooks/usePalette' +import {HeartIcon, HeartIconSolid} from 'lib/icons' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {CommonNavigatorParams} from 'lib/routes/types' +import {makeRecordUri} from 'lib/strings/url-helpers' +import {colors, s} from 'lib/styles' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {FeedSourceModel} from 'state/models/content/feed-source' +import {PostsFeedModel} from 'state/models/feeds/posts' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' +import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' +import {Feed} from 'view/com/posts/Feed' +import {TextLink} from 'view/com/util/Link' +import {Button} from 'view/com/util/forms/Button' +import {Text} from 'view/com/util/text/Text' +import {RichText} from 'view/com/util/text/RichText' +import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {FAB} from 'view/com/util/fab/FAB' +import {EmptyState} from 'view/com/util/EmptyState' +import * as Toast from 'view/com/util/Toast' +import {useSetTitle} from 'lib/hooks/useSetTitle' +import {useCustomFeed} from 'lib/hooks/useCustomFeed' +import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {shareUrl} from 'lib/sharing' +import {toShareUrl} from 'lib/strings/url-helpers' +import {Haptics} from 'lib/haptics' +import {useAnalytics} from 'lib/analytics/analytics' +import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' +import {resolveName} from 'lib/api' +import {makeCustomFeedLink} from 'lib/routes/links' +import {pluralize} from 'lib/strings/helpers' +import {CenteredView, ScrollView} from 'view/com/util/Views' +import {NavigationProp} from 'lib/routes/types' +import {sanitizeHandle} from 'lib/strings/handles' +import {makeProfileLink} from 'lib/routes/links' +import {ComposeIcon2} from 'lib/icons' + +const SECTION_TITLES = ['Posts', 'About'] + +interface SectionRef { + scrollToTop: () => void +} + +type Props = NativeStackScreenProps +export const ProfileFeedScreen = withAuthRequired( + observer(function ProfileFeedScreenImpl(props: Props) { + const pal = usePalette('default') + const store = useStores() + const navigation = useNavigation() + + const {name: handleOrDid} = props.route.params + + const [feedOwnerDid, setFeedOwnerDid] = React.useState() + const [error, setError] = React.useState() + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + React.useEffect(() => { + /* + * We must resolve the DID of the feed owner before we can fetch the feed. + */ + async function fetchDid() { + try { + const did = await resolveName(store, handleOrDid) + setFeedOwnerDid(did) + } catch (e) { + setError( + `We're sorry, but we were unable to resolve this feed. If this persists, please contact the feed creator, @${handleOrDid}.`, + ) + } + } + + fetchDid() + }, [store, handleOrDid, setFeedOwnerDid]) + + if (error) { + return ( + + + + Could not load feed + + + {error} + + + + + + + + ) + } + + return feedOwnerDid ? ( + + ) : ( + + + + + + ) + }), +) + +export const ProfileFeedScreenInner = observer( + function ProfileFeedScreenInnerImpl({ + route, + feedOwnerDid, + }: Props & {feedOwnerDid: string}) { + const pal = usePalette('default') + const store = useStores() + const {track} = useAnalytics() + const feedSectionRef = React.useRef(null) + const {rkey, name: handleOrDid} = route.params + const uri = useMemo( + () => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey), + [rkey, feedOwnerDid], + ) + const feedInfo = useCustomFeed(uri) + const feed: PostsFeedModel = useMemo(() => { + const model = new PostsFeedModel(store, 'custom', { + feed: uri, + }) + model.setup() + return model + }, [store, uri]) + const isPinned = store.preferences.isPinnedFeed(uri) + useSetTitle(feedInfo?.displayName) + + // events + // = + + const onToggleSaved = React.useCallback(async () => { + try { + Haptics.default() + if (feedInfo?.isSaved) { + await feedInfo?.unsave() + } else { + await feedInfo?.save() + } + } catch (err) { + Toast.show( + 'There was an an issue updating your feeds, please check your internet connection and try again.', + ) + store.log.error('Failed up update feeds', {err}) + } + }, [store, feedInfo]) + + const onToggleLiked = React.useCallback(async () => { + Haptics.default() + try { + if (feedInfo?.isLiked) { + await feedInfo?.unlike() + } else { + await feedInfo?.like() + } + } catch (err) { + Toast.show( + 'There was an an issue contacting the server, please check your internet connection and try again.', + ) + store.log.error('Failed up toggle like', {err}) + } + }, [store, feedInfo]) + + const onTogglePinned = React.useCallback(async () => { + Haptics.default() + if (feedInfo) { + feedInfo.togglePin().catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to toggle pinned feed', {e}) + }) + } + }, [store, feedInfo]) + + const onPressShare = React.useCallback(() => { + const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`) + shareUrl(url) + track('CustomFeed:Share') + }, [handleOrDid, rkey, track]) + + const onPressReport = React.useCallback(() => { + if (!feedInfo) return + store.shell.openModal({ + name: 'report', + uri: feedInfo.uri, + cid: feedInfo.cid, + }) + }, [store, feedInfo]) + + const onCurrentPageSelected = React.useCallback( + (index: number) => { + if (index === 0) { + feedSectionRef.current?.scrollToTop() + } + }, + [feedSectionRef], + ) + + // render + // = + + const dropdownItems: DropdownItem[] = React.useMemo(() => { + return [ + { + testID: 'feedHeaderDropdownToggleSavedBtn', + label: feedInfo?.isSaved ? 'Remove from my feeds' : 'Add to my feeds', + onPress: onToggleSaved, + icon: feedInfo?.isSaved + ? { + ios: { + name: 'trash', + }, + android: 'ic_delete', + web: ['far', 'trash-can'], + } + : { + ios: { + name: 'plus', + }, + android: '', + web: 'plus', + }, + }, + { + testID: 'feedHeaderDropdownReportBtn', + label: 'Report feed', + onPress: onPressReport, + icon: { + ios: { + name: 'exclamationmark.triangle', + }, + android: 'ic_menu_report_image', + web: 'circle-exclamation', + }, + }, + { + testID: 'feedHeaderDropdownShareBtn', + label: 'Share link', + onPress: onPressShare, + icon: { + ios: { + name: 'square.and.arrow.up', + }, + android: 'ic_menu_share', + web: 'share', + }, + }, + ] as DropdownItem[] + }, [feedInfo, onToggleSaved, onPressReport, onPressShare]) + + const renderHeader = useCallback(() => { + return ( + + {feedInfo && ( + <> + + {typeof feedInfo.likeCount === 'number' && ( + + )} + + + Created by{' '} + {feedInfo.isOwner ? ( + 'you' + ) : ( + + )} + + + ) +}) + +const styles = StyleSheet.create({ + btn: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingVertical: 7, + paddingHorizontal: 14, + borderRadius: 50, + marginLeft: 6, + }, + liked: { + color: colors.red3, + }, + notFoundContainer: { + margin: 10, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 6, + }, +}) -- cgit 1.4.1