about summary refs log tree commit diff
path: root/src/view/com/lists/ListItems.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/com/lists/ListItems.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/com/lists/ListItems.tsx')
-rw-r--r--src/view/com/lists/ListItems.tsx261
1 files changed, 63 insertions, 198 deletions
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx
index c5ea13169..855c07d14 100644
--- a/src/view/com/lists/ListItems.tsx
+++ b/src/view/com/lists/ListItems.tsx
@@ -3,34 +3,26 @@ import {
   ActivityIndicator,
   RefreshControl,
   StyleProp,
-  StyleSheet,
   View,
   ViewStyle,
-  FlatList,
 } from 'react-native'
-import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api'
+import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
+import {FlatList} from '../util/Views'
 import {observer} from 'mobx-react-lite'
 import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
 import {ProfileCard} from '../profile/ProfileCard'
 import {Button} from '../util/forms/Button'
-import {Text} from '../util/text/Text'
-import {RichText as RichTextCom} from '../util/text/RichText'
-import {UserAvatar} from '../util/UserAvatar'
-import {TextLink} from '../util/Link'
 import {ListModel} from 'state/models/content/list'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useStores} from 'state/index'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
-import {ListActions} from './ListActions'
-import {makeProfileLink} from 'lib/routes/links'
-import {sanitizeHandle} from 'lib/strings/handles'
+import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
-const HEADER_ITEM = {_reactKey: '__header__'}
 const EMPTY_ITEM = {_reactKey: '__empty__'}
 const ERROR_ITEM = {_reactKey: '__error__'}
 const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -39,36 +31,35 @@ export const ListItems = observer(function ListItemsImpl({
   list,
   style,
   scrollElRef,
+  onScroll,
   onPressTryAgain,
-  onToggleSubscribed,
-  onPressEditList,
-  onPressDeleteList,
-  onPressShareList,
-  onPressReportList,
+  renderHeader,
   renderEmptyState,
   testID,
+  scrollEventThrottle,
   headerOffset = 0,
+  desktopFixedHeightOffset,
 }: {
   list: ListModel
   style?: StyleProp<ViewStyle>
   scrollElRef?: MutableRefObject<FlatList<any> | null>
+  onScroll?: OnScrollCb
   onPressTryAgain?: () => void
-  onToggleSubscribed: () => void
-  onPressEditList: () => void
-  onPressDeleteList: () => void
-  onPressShareList: () => void
-  onPressReportList: () => void
-  renderEmptyState?: () => JSX.Element
+  renderHeader: () => JSX.Element
+  renderEmptyState: () => JSX.Element
   testID?: string
+  scrollEventThrottle?: number
   headerOffset?: number
+  desktopFixedHeightOffset?: number
 }) {
   const pal = usePalette('default')
   const store = useStores()
   const {track} = useAnalytics()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
+  const {isMobile} = useWebMediaQueries()
 
   const data = React.useMemo(() => {
-    let items: any[] = [HEADER_ITEM]
+    let items: any[] = []
     if (list.hasLoaded) {
       if (list.hasError) {
         items = items.concat([ERROR_ITEM])
@@ -124,11 +115,18 @@ export const ListItems = observer(function ListItemsImpl({
   const onPressEditMembership = React.useCallback(
     (profile: AppBskyActorDefs.ProfileViewBasic) => {
       store.shell.openModal({
-        name: 'list-add-remove-user',
+        name: 'user-add-remove-lists',
         subject: profile.did,
         displayName: profile.displayName || profile.handle,
-        onUpdate() {
-          list.refresh()
+        onAdd(listUri: string) {
+          if (listUri === list.uri) {
+            list.cacheAddMember(profile)
+          }
+        },
+        onRemove(listUri: string) {
+          if (listUri === list.uri) {
+            list.cacheRemoveMember(profile)
+          }
         },
       })
     },
@@ -145,6 +143,7 @@ export const ListItems = observer(function ListItemsImpl({
       }
       return (
         <Button
+          testID={`user-${profile.handle}-editBtn`}
           type="default"
           label="Edit"
           onPress={() => onPressEditMembership(profile)}
@@ -157,22 +156,7 @@ export const ListItems = observer(function ListItemsImpl({
   const renderItem = React.useCallback(
     ({item}: {item: any}) => {
       if (item === EMPTY_ITEM) {
-        if (renderEmptyState) {
-          return renderEmptyState()
-        }
-        return <View />
-      } else if (item === HEADER_ITEM) {
-        return list.list ? (
-          <ListHeader
-            list={list.list}
-            isOwner={list.isOwner}
-            onToggleSubscribed={onToggleSubscribed}
-            onPressEditList={onPressEditList}
-            onPressDeleteList={onPressDeleteList}
-            onPressShareList={onPressShareList}
-            onPressReportList={onPressReportList}
-          />
-        ) : null
+        return renderEmptyState()
       } else if (item === ERROR_ITEM) {
         return (
           <ErrorMessage
@@ -197,178 +181,59 @@ export const ListItems = observer(function ListItemsImpl({
           }`}
           profile={(item as AppBskyGraphDefs.ListItemView).subject}
           renderButton={renderMemberButton}
+          style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
         />
       )
     },
     [
       renderMemberButton,
       renderEmptyState,
-      list.list,
-      list.isOwner,
       list.error,
-      onToggleSubscribed,
-      onPressEditList,
-      onPressDeleteList,
-      onPressShareList,
-      onPressReportList,
       onPressTryAgain,
       onPressRetryLoadMore,
+      isMobile,
     ],
   )
 
   const Footer = React.useCallback(
-    () =>
-      list.isLoading ? (
-        <View style={styles.feedFooter}>
-          <ActivityIndicator />
-        </View>
-      ) : (
-        <View />
-      ),
-    [list],
+    () => (
+      <View style={{paddingTop: 20, paddingBottom: 200}}>
+        {list.isLoading && <ActivityIndicator />}
+      </View>
+    ),
+    [list.isLoading],
   )
 
   return (
     <View testID={testID} style={style}>
-      {data.length > 0 && (
-        <FlatList
-          testID={testID ? `${testID}-flatlist` : undefined}
-          ref={scrollElRef}
-          data={data}
-          keyExtractor={item => item._reactKey}
-          renderItem={renderItem}
-          ListFooterComponent={Footer}
-          refreshControl={
-            <RefreshControl
-              refreshing={isRefreshing}
-              onRefresh={onRefresh}
-              tintColor={pal.colors.text}
-              titleColor={pal.colors.text}
-              progressViewOffset={headerOffset}
-            />
-          }
-          contentContainerStyle={s.contentContainer}
-          style={{paddingTop: headerOffset}}
-          onEndReached={onEndReached}
-          onEndReachedThreshold={0.6}
-          removeClippedSubviews={true}
-          contentOffset={{x: 0, y: headerOffset * -1}}
-          // @ts-ignore our .web version only -prf
-          desktopFixedHeight
-        />
-      )}
+      <FlatList
+        testID={testID ? `${testID}-flatlist` : undefined}
+        ref={scrollElRef}
+        data={data}
+        keyExtractor={(item: any) => item._reactKey}
+        renderItem={renderItem}
+        ListHeaderComponent={renderHeader}
+        ListFooterComponent={Footer}
+        refreshControl={
+          <RefreshControl
+            refreshing={isRefreshing}
+            onRefresh={onRefresh}
+            tintColor={pal.colors.text}
+            titleColor={pal.colors.text}
+            progressViewOffset={headerOffset}
+          />
+        }
+        contentContainerStyle={s.contentContainer}
+        style={{paddingTop: headerOffset}}
+        onScroll={onScroll}
+        onEndReached={onEndReached}
+        onEndReachedThreshold={0.6}
+        scrollEventThrottle={scrollEventThrottle}
+        removeClippedSubviews={true}
+        contentOffset={{x: 0, y: headerOffset * -1}}
+        // @ts-ignore our .web version only -prf
+        desktopFixedHeight={desktopFixedHeightOffset || true}
+      />
     </View>
   )
 })
-
-const ListHeader = observer(function ListHeaderImpl({
-  list,
-  isOwner,
-  onToggleSubscribed,
-  onPressEditList,
-  onPressDeleteList,
-  onPressShareList,
-  onPressReportList,
-}: {
-  list: AppBskyGraphDefs.ListView
-  isOwner: boolean
-  onToggleSubscribed: () => void
-  onPressEditList: () => void
-  onPressDeleteList: () => void
-  onPressShareList: () => void
-  onPressReportList: () => void
-}) {
-  const pal = usePalette('default')
-  const store = useStores()
-  const {isDesktop} = useWebMediaQueries()
-  const descriptionRT = React.useMemo(
-    () =>
-      list?.description &&
-      new RichText({
-        text: list.description,
-        facets: (list.descriptionFacets || [])?.slice(),
-      }),
-    [list],
-  )
-  return (
-    <>
-      <View style={[styles.header, pal.border]}>
-        <View style={s.flex1}>
-          <Text testID="listName" type="title-xl" style={[pal.text, s.bold]}>
-            {list.name}
-          </Text>
-          {list && (
-            <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-              {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list '}
-              by{' '}
-              {list.creator.did === store.me.did ? (
-                'you'
-              ) : (
-                <TextLink
-                  text={sanitizeHandle(list.creator.handle, '@')}
-                  href={makeProfileLink(list.creator)}
-                  style={pal.textLight}
-                />
-              )}
-            </Text>
-          )}
-          {descriptionRT && (
-            <RichTextCom
-              testID="listDescription"
-              style={[pal.text, styles.headerDescription]}
-              richText={descriptionRT}
-            />
-          )}
-          {isDesktop && (
-            <ListActions
-              isOwner={isOwner}
-              muted={list.viewer?.muted}
-              onPressDeleteList={onPressDeleteList}
-              onPressEditList={onPressEditList}
-              onToggleSubscribed={onToggleSubscribed}
-              onPressShareList={onPressShareList}
-              onPressReportList={onPressReportList}
-            />
-          )}
-        </View>
-        <View>
-          <UserAvatar type="list" avatar={list.avatar} size={64} />
-        </View>
-      </View>
-      <View
-        style={{flexDirection: 'row', paddingHorizontal: isDesktop ? 16 : 6}}>
-        <View style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
-          <Text type="md-medium" style={[pal.text]}>
-            Muted users
-          </Text>
-        </View>
-      </View>
-    </>
-  )
-})
-
-const styles = StyleSheet.create({
-  header: {
-    flexDirection: 'row',
-    gap: 12,
-    paddingHorizontal: 16,
-    paddingTop: 12,
-    paddingBottom: 16,
-    borderTopWidth: 1,
-  },
-  headerDescription: {
-    flex: 1,
-    marginTop: 8,
-  },
-  headerBtns: {
-    flexDirection: 'row',
-    gap: 8,
-    marginTop: 12,
-  },
-  fakeSelectorItem: {
-    paddingHorizontal: 12,
-    paddingBottom: 8,
-    borderBottomWidth: 3,
-  },
-  feedFooter: {paddingTop: 20},
-})