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/CustomFeed.tsx | 495 ---------------------------------------- 1 file changed, 495 deletions(-) delete mode 100644 src/view/screens/CustomFeed.tsx (limited to 'src/view/screens/CustomFeed.tsx') diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx deleted file mode 100644 index f9383639c..000000000 --- a/src/view/screens/CustomFeed.tsx +++ /dev/null @@ -1,495 +0,0 @@ -import React, {useMemo, useRef} from 'react' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useNavigation, useIsFocused} from '@react-navigation/native' -import {usePalette} from 'lib/hooks/usePalette' -import {HeartIcon, HeartIconSolid} from 'lib/icons' -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 {FlatList, StyleSheet, View, ActivityIndicator} from 'react-native' -import {useStores} from 'state/index' -import {PostsFeedModel} from 'state/models/feeds/posts' -import {useCustomFeed} from 'lib/hooks/useCustomFeed' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {Feed} from 'view/com/posts/Feed' -import {TextLink} from 'view/com/util/Link' -import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' -import {Button} from 'view/com/util/forms/Button' -import {Text} from 'view/com/util/text/Text' -import * as Toast from 'view/com/util/Toast' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {useSetTitle} from 'lib/hooks/useSetTitle' -import {shareUrl} from 'lib/sharing' -import {toShareUrl} from 'lib/strings/url-helpers' -import {Haptics} from 'lib/haptics' -import {ComposeIcon2} from 'lib/icons' -import {FAB} from '../com/util/fab/FAB' -import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' -import {EmptyState} from 'view/com/util/EmptyState' -import {useAnalytics} from 'lib/analytics/analytics' -import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' -import {resolveName} from 'lib/api' -import {CenteredView} from 'view/com/util/Views' -import {NavigationProp} from 'lib/routes/types' - -type Props = NativeStackScreenProps - -export const CustomFeedScreen = withAuthRequired( - observer(function CustomFeedScreenImpl(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 CustomFeedScreenInner = observer( - function CustomFeedScreenInnerImpl({ - route, - feedOwnerDid, - }: Props & {feedOwnerDid: string}) { - const store = useStores() - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const navigation = useNavigation() - const isScreenFocused = useIsFocused() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() - const {track} = useAnalytics() - const {rkey, name: handleOrDid} = route.params - const uri = useMemo( - () => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey), - [rkey, feedOwnerDid], - ) - const scrollElRef = useRef(null) - const currentFeed = useCustomFeed(uri) - const algoFeed: PostsFeedModel = useMemo(() => { - const feed = new PostsFeedModel(store, 'custom', { - feed: uri, - }) - feed.setup() - return feed - }, [store, uri]) - const isPinned = store.me.savedFeeds.isPinned(uri) - const [onMainScroll, isScrolledDown, resetMainScroll] = - useOnMainScroll(store) - useSetTitle(currentFeed?.displayName) - - const onToggleSaved = React.useCallback(async () => { - try { - Haptics.default() - if (currentFeed?.isSaved) { - await currentFeed?.unsave() - } else { - await currentFeed?.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, currentFeed]) - - const onToggleLiked = React.useCallback(async () => { - Haptics.default() - try { - if (currentFeed?.isLiked) { - await currentFeed?.unlike() - } else { - await currentFeed?.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, currentFeed]) - - const onTogglePinned = React.useCallback(async () => { - Haptics.default() - store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => { - Toast.show('There was an issue contacting the server') - store.log.error('Failed to toggle pinned feed', {e}) - }) - }, [store, currentFeed]) - - const onPressAbout = React.useCallback(() => { - store.shell.openModal({ - name: 'confirm', - title: currentFeed?.displayName || '', - message: - currentFeed?.data.description || 'This feed has no description.', - confirmBtnText: 'Close', - onPressConfirm() {}, - }) - }, [store, currentFeed]) - - const onPressViewAuthor = React.useCallback(() => { - navigation.navigate('Profile', {name: handleOrDid}) - }, [handleOrDid, navigation]) - - const onPressShare = React.useCallback(() => { - const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`) - shareUrl(url) - track('CustomFeed:Share') - }, [handleOrDid, rkey, track]) - - const onPressReport = React.useCallback(() => { - if (!currentFeed) return - store.shell.openModal({ - name: 'report', - uri: currentFeed.uri, - cid: currentFeed.data.cid, - }) - }, [store, currentFeed]) - - const onScrollToTop = React.useCallback(() => { - scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) - resetMainScroll() - }, [scrollElRef, resetMainScroll]) - - const onPressCompose = React.useCallback(() => { - store.shell.openComposer({}) - }, [store]) - - const onSoftReset = React.useCallback(() => { - if (isScreenFocused) { - onScrollToTop() - algoFeed.refresh() - } - }, [isScreenFocused, onScrollToTop, algoFeed]) - - // fires when page within screen is activated/deactivated - React.useEffect(() => { - if (!isScreenFocused) { - return - } - - const softResetSub = store.onScreenSoftReset(onSoftReset) - return () => { - softResetSub.remove() - } - }, [store, onSoftReset, isScreenFocused]) - - const dropdownItems: DropdownItem[] = React.useMemo(() => { - return [ - currentFeed - ? { - testID: 'feedHeaderDropdownAboutBtn', - label: 'About this feed', - onPress: onPressAbout, - icon: { - ios: { - name: 'info.circle', - }, - android: '', - web: 'info', - }, - } - : undefined, - { - testID: 'feedHeaderDropdownViewAuthorBtn', - label: 'View author', - onPress: onPressViewAuthor, - icon: { - ios: { - name: 'person', - }, - android: '', - web: ['far', 'user'], - }, - }, - { - testID: 'feedHeaderDropdownToggleSavedBtn', - label: currentFeed?.isSaved - ? 'Remove from my feeds' - : 'Add to my feeds', - onPress: onToggleSaved, - icon: currentFeed?.isSaved - ? { - ios: { - name: 'trash', - }, - android: 'ic_delete', - web: 'trash', - } - : { - 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', - }, - }, - ].filter(Boolean) as DropdownItem[] - }, [ - currentFeed, - onPressAbout, - onToggleSaved, - onPressReport, - onPressShare, - onPressViewAuthor, - ]) - - const renderEmptyState = React.useCallback(() => { - return ( - - - - ) - }, [pal.border]) - - return ( - - - - {currentFeed ? ( - store.emitScreenSoftReset()} - /> - ) : ( - 'Loading...' - )} - - {currentFeed ? ( - <> - - {currentFeed?.isSaved ? ( - - ) : ( - - )} - - ) : null} - - - - - - - - {isScrolledDown ? ( - - ) : null} - } - accessibilityRole="button" - accessibilityLabel="New post" - accessibilityHint="" - /> - - ) - }, -) - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - gap: 12, - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 16, - borderTopWidth: 1, - }, - headerText: { - flex: 1, - fontWeight: 'bold', - }, - headerBtn: { - paddingVertical: 0, - }, - headerAddBtn: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - paddingVertical: 4, - paddingLeft: 10, - }, - liked: { - color: colors.red3, - }, - top1: { - position: 'relative', - top: 1, - }, - top2: { - position: 'relative', - top: 2, - }, - notFoundContainer: { - margin: 10, - paddingHorizontal: 18, - paddingVertical: 14, - borderRadius: 6, - }, -}) -- cgit 1.4.1