about summary refs log tree commit diff
path: root/src/view/screens/CustomFeed.tsx
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-11-01 16:15:40 -0700
committerGitHub <noreply@github.com>2023-11-01 16:15:40 -0700
commitf57a8cf8ba0cd10a54abf35d960d8fb90266fa6b (patch)
treea9da6032bcbd587d92fd1030e698aea2dbef9f72 /src/view/screens/CustomFeed.tsx
parentf9944b55e26fe6109bc2e7a25b88979111470ed9 (diff)
downloadvoidsky-f57a8cf8ba0cd10a54abf35d960d8fb90266fa6b.tar.zst
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 <Tabs> component with the more effective <PagerWithHeader> 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 <git@esb.lol>

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/view/screens/CustomFeed.tsx')
-rw-r--r--src/view/screens/CustomFeed.tsx495
1 files changed, 0 insertions, 495 deletions
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<CommonNavigatorParams, 'CustomFeed'>
-
-export const CustomFeedScreen = withAuthRequired(
-  observer(function CustomFeedScreenImpl(props: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const navigation = useNavigation<NavigationProp>()
-
-    const {name: handleOrDid} = props.route.params
-
-    const [feedOwnerDid, setFeedOwnerDid] = React.useState<string | undefined>()
-    const [error, setError] = React.useState<string | undefined>()
-
-    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 (
-        <CenteredView>
-          <View style={[pal.view, pal.border, styles.notFoundContainer]}>
-            <Text type="title-lg" style={[pal.text, s.mb10]}>
-              Could not load feed
-            </Text>
-            <Text type="md" style={[pal.text, s.mb20]}>
-              {error}
-            </Text>
-
-            <View style={{flexDirection: 'row'}}>
-              <Button
-                type="default"
-                accessibilityLabel="Go Back"
-                accessibilityHint="Return to previous page"
-                onPress={onPressBack}
-                style={{flexShrink: 1}}>
-                <Text type="button" style={pal.text}>
-                  Go Back
-                </Text>
-              </Button>
-            </View>
-          </View>
-        </CenteredView>
-      )
-    }
-
-    return feedOwnerDid ? (
-      <CustomFeedScreenInner {...props} feedOwnerDid={feedOwnerDid} />
-    ) : (
-      <CenteredView>
-        <View style={s.p20}>
-          <ActivityIndicator size="large" />
-        </View>
-      </CenteredView>
-    )
-  }),
-)
-
-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<NavigationProp>()
-    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<FlatList>(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 (
-        <View style={[pal.border, {borderTopWidth: 1, paddingTop: 20}]}>
-          <EmptyState icon="feed" message="This feed is empty!" />
-        </View>
-      )
-    }, [pal.border])
-
-    return (
-      <View style={s.hContentRegion}>
-        <SimpleViewHeader
-          showBackButton={isMobile}
-          style={
-            !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
-          }>
-          <Text type="title-lg" style={styles.headerText} numberOfLines={1}>
-            {currentFeed ? (
-              <TextLink
-                type="title-lg"
-                href="/"
-                style={[pal.text, {fontWeight: 'bold'}]}
-                text={currentFeed?.displayName || ''}
-                onPress={() => store.emitScreenSoftReset()}
-              />
-            ) : (
-              'Loading...'
-            )}
-          </Text>
-          {currentFeed ? (
-            <>
-              <Button
-                type="default-light"
-                testID="toggleLikeBtn"
-                accessibilityLabel="Like this feed"
-                accessibilityHint=""
-                onPress={onToggleLiked}
-                style={styles.headerBtn}>
-                {currentFeed?.isLiked ? (
-                  <HeartIconSolid size={19} style={styles.liked} />
-                ) : (
-                  <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
-                )}
-              </Button>
-              {currentFeed?.isSaved ? (
-                <Button
-                  type="default-light"
-                  accessibilityLabel={
-                    isPinned ? 'Unpin this feed' : 'Pin this feed'
-                  }
-                  accessibilityHint=""
-                  onPress={onTogglePinned}
-                  style={styles.headerBtn}>
-                  <FontAwesomeIcon
-                    icon="thumb-tack"
-                    size={17}
-                    color={isPinned ? colors.blue3 : pal.colors.textLight}
-                    style={styles.top1}
-                  />
-                </Button>
-              ) : (
-                <Button
-                  type="inverted"
-                  onPress={onToggleSaved}
-                  accessibilityLabel="Add to my feeds"
-                  accessibilityHint=""
-                  style={styles.headerAddBtn}>
-                  <FontAwesomeIcon
-                    icon="plus"
-                    color={palInverted.colors.text}
-                    size={19}
-                  />
-                  <Text type="button" style={palInverted.text}>
-                    Add{!isMobile && ' to My Feeds'}
-                  </Text>
-                </Button>
-              )}
-            </>
-          ) : null}
-          <NativeDropdown
-            testID="feedHeaderDropdownBtn"
-            items={dropdownItems}
-            accessibilityLabel="More options"
-            accessibilityHint="">
-            <View
-              style={{
-                paddingLeft: 12,
-                paddingRight: isMobile ? 12 : 0,
-              }}>
-              <FontAwesomeIcon
-                icon="ellipsis"
-                size={20}
-                color={pal.colors.textLight}
-              />
-            </View>
-          </NativeDropdown>
-        </SimpleViewHeader>
-        <Feed
-          scrollElRef={scrollElRef}
-          feed={algoFeed}
-          onScroll={onMainScroll}
-          scrollEventThrottle={100}
-          renderEmptyState={renderEmptyState}
-          extraData={[uri, isPinned]}
-          style={!isTabletOrDesktop ? {flex: 1} : undefined}
-        />
-        {isScrolledDown ? (
-          <LoadLatestBtn
-            onPress={onSoftReset}
-            label="Scroll to top"
-            showIndicator={false}
-          />
-        ) : null}
-        <FAB
-          testID="composeFAB"
-          onPress={onPressCompose}
-          icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-          accessibilityRole="button"
-          accessibilityLabel="New post"
-          accessibilityHint=""
-        />
-      </View>
-    )
-  },
-)
-
-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,
-  },
-})