about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-05-17 15:52:11 -0500
committerPaul Frazee <pfrazee@gmail.com>2023-05-17 15:52:11 -0500
commitc55ce6de020d6c9df86c71158251caddf12da777 (patch)
treeaff4b8b41437a69961a28426ce79994271187d55
parent0f5b3808f8107ff4f04cda8c3da18ffcdcf6f27f (diff)
downloadvoidsky-c55ce6de020d6c9df86c71158251caddf12da777.tar.zst
Rework the UI for the custom feed view
-rw-r--r--src/Navigation.tsx4
-rw-r--r--src/lib/hooks/useCustomFeed.ts4
-rw-r--r--src/lib/routes/types.ts2
-rw-r--r--src/view/com/feeds/CustomFeed.tsx3
-rw-r--r--src/view/screens/CustomFeed.tsx164
-rw-r--r--src/view/screens/ProfileCustomFeed.tsx291
-rw-r--r--src/view/screens/ProfileList.tsx4
-rw-r--r--src/view/screens/SavedFeeds.tsx5
8 files changed, 300 insertions, 177 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 4521068f2..025020afa 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -40,6 +40,7 @@ import {SettingsScreen} from './view/screens/Settings'
 import {ProfileScreen} from './view/screens/Profile'
 import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
 import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
+import {ProfileCustomFeed} from './view/screens/ProfileCustomFeed'
 import {ProfileListScreen} from './view/screens/ProfileList'
 import {PostThreadScreen} from './view/screens/PostThread'
 import {PostLikedByScreen} from './view/screens/PostLikedBy'
@@ -56,7 +57,6 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
 import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
 import {getRoutingInstrumentation} from 'lib/sentry'
 import {SavedFeeds} from './view/screens/SavedFeeds'
-import {CustomFeed} from './view/screens/CustomFeed'
 import {PinnedFeeds} from 'view/screens/PinnedFeeds'
 import {bskyTitle} from 'lib/strings/headings'
 
@@ -127,6 +127,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
           title: title(`People followed by @${route.params.name}`),
         })}
       />
+      <Stack.Screen name="ProfileCustomFeed" component={ProfileCustomFeed} />
       <Stack.Screen
         name="ProfileList"
         component={ProfileListScreen}
@@ -189,7 +190,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
       />
       <Stack.Screen name="SavedFeeds" component={SavedFeeds} />
       <Stack.Screen name="PinnedFeeds" component={PinnedFeeds} />
-      <Stack.Screen name="CustomFeed" component={CustomFeed} />
     </>
   )
 }
diff --git a/src/lib/hooks/useCustomFeed.ts b/src/lib/hooks/useCustomFeed.ts
index ee40cf49e..d7a27050d 100644
--- a/src/lib/hooks/useCustomFeed.ts
+++ b/src/lib/hooks/useCustomFeed.ts
@@ -2,9 +2,9 @@ import {useEffect, useState} from 'react'
 import {useStores} from 'state/index'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
-export function useCustomFeed(uri: string) {
+export function useCustomFeed(uri: string): CustomFeedModel | undefined {
   const store = useStores()
-  const [item, setItem] = useState<CustomFeedModel>()
+  const [item, setItem] = useState<CustomFeedModel | undefined>()
   useEffect(() => {
     async function fetchView() {
       const res = await store.agent.app.bsky.feed.getFeedGenerator({
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 8b96aaad7..52d0e9af2 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -13,11 +13,11 @@ export type CommonNavigatorParams = {
   Profile: {name: string; hideBackButton?: boolean}
   ProfileFollowers: {name: string}
   ProfileFollows: {name: string}
+  ProfileCustomFeed: {name: string; rkey: string}
   ProfileList: {name: string; rkey: string}
   PostThread: {name: string; rkey: string}
   PostLikedBy: {name: string; rkey: string}
   PostRepostedBy: {name: string; rkey: string}
-  CustomFeed: {name: string; rkey: string; displayName?: string}
   Debug: undefined
   Log: undefined
   Support: undefined
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 8e1a78453..5a93020a0 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -40,10 +40,9 @@ export const CustomFeed = observer(
         accessibilityRole="button"
         style={[styles.container, pal.border, style]}
         onPress={() => {
-          navigation.navigate('CustomFeed', {
+          navigation.navigate('ProfileCustomFeed', {
             name: item.data.creator.did,
             rkey: new AtUri(item.data.uri).rkey,
-            displayName: item.displayName,
           })
         }}
         key={item.data.uri}>
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
deleted file mode 100644
index 76125fa5c..000000000
--- a/src/view/screens/CustomFeed.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-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 React, {useMemo, useRef} from 'react'
-import {FlatList, StyleSheet, TouchableOpacity, View} 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 {Link} from 'view/com/util/Link'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {Button} from 'view/com/util/forms/Button'
-import {Text} from 'view/com/util/text/Text'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
-export const CustomFeed = withAuthRequired(
-  observer(({route}: Props) => {
-    const rootStore = useStores()
-    const {rkey, name, displayName} = route.params
-    const uri = useMemo(
-      () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
-      [rkey, name],
-    )
-    const currentFeed = useCustomFeed(uri)
-    const scrollElRef = useRef<FlatList>(null)
-    const algoFeed: PostsFeedModel = useMemo(() => {
-      const feed = new PostsFeedModel(rootStore, 'custom', {
-        feed: uri,
-      })
-      feed.setup()
-      return feed
-    }, [rootStore, uri])
-
-    return (
-      <View style={[styles.container]}>
-        <ViewHeader
-          title={
-            displayName ?? `${currentFeed?.data.creator.displayName}'s feed`
-          }
-          showOnDesktop
-        />
-        <Feed
-          scrollElRef={scrollElRef}
-          testID={'test-feed'}
-          key="default"
-          feed={algoFeed}
-          headerOffset={12}
-          ListHeaderComponent={() => <ListHeaderComponent uri={uri} />}
-          extraData={uri}
-        />
-      </View>
-    )
-  }),
-)
-
-const ListHeaderComponent = observer(({uri}: {uri: string}) => {
-  const currentFeed = useCustomFeed(uri)
-  const pal = usePalette('default')
-  const rootStore = useStores()
-  return (
-    <View style={[styles.headerContainer]}>
-      <View style={[styles.header]}>
-        <View style={styles.avatarContainer}>
-          <UserAvatar
-            type="algo"
-            size={28}
-            avatar={currentFeed?.data.creator.avatar}
-          />
-          <Link href={`/profile/${currentFeed?.data.creator.handle}`}>
-            <Text style={[pal.textLight]}>
-              @{currentFeed?.data.creator.handle}
-            </Text>
-          </Link>
-        </View>
-        <Text style={[pal.text]}>{currentFeed?.data.description}</Text>
-      </View>
-
-      <View style={[styles.buttonsContainer]}>
-        <Button
-          type={currentFeed?.isSaved ? 'default' : 'inverted'}
-          style={[styles.saveButton]}
-          onPress={() => {
-            if (currentFeed?.data.viewer?.saved) {
-              rootStore.me.savedFeeds.unsave(currentFeed!)
-            } else {
-              rootStore.me.savedFeeds.save(currentFeed!)
-            }
-          }}
-          label={currentFeed?.data.viewer?.saved ? 'Unsave' : 'Save'}
-        />
-
-        <TouchableOpacity
-          accessibilityRole="button"
-          onPress={() => {
-            if (currentFeed?.isLiked) {
-              currentFeed?.unlike()
-            } else {
-              currentFeed?.like()
-            }
-          }}
-          style={[styles.likeButton, pal.viewLight]}>
-          <Text style={[pal.text, s.semiBold]}>
-            {currentFeed?.data.likeCount}
-          </Text>
-          {currentFeed?.isLiked ? (
-            <HeartIconSolid size={18} style={styles.liked} />
-          ) : (
-            <HeartIcon strokeWidth={3} size={18} style={styles.liked} />
-          )}
-        </TouchableOpacity>
-      </View>
-    </View>
-  )
-})
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  headerContainer: {
-    alignItems: 'center',
-    justifyContent: 'center',
-    gap: 8,
-    marginBottom: 12,
-  },
-  header: {
-    alignItems: 'center',
-    gap: 4,
-  },
-  avatarContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 8,
-  },
-  buttonsContainer: {
-    flexDirection: 'row',
-    gap: 8,
-  },
-  saveButton: {
-    minWidth: 100,
-    alignItems: 'center',
-  },
-  liked: {
-    color: colors.red3,
-  },
-  notLiked: {
-    color: colors.gray3,
-  },
-  likeButton: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    paddingVertical: 4,
-    paddingHorizontal: 8,
-    borderRadius: 24,
-    gap: 4,
-  },
-})
diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx
new file mode 100644
index 000000000..1113ebf01
--- /dev/null
+++ b/src/view/screens/ProfileCustomFeed.tsx
@@ -0,0 +1,291 @@
+import React, {useMemo, useRef} from 'react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+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} 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 {pluralize} from 'lib/strings/helpers'
+import {TextLink} from 'view/com/util/Link'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+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 {isDesktopWeb} from 'platform/detection'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileCustomFeed'>
+export const ProfileCustomFeed = withAuthRequired(
+  observer(({route}: Props) => {
+    const store = useStores()
+    const pal = usePalette('default')
+    const {rkey, name} = route.params
+    const uri = useMemo(
+      () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
+      [rkey, name],
+    )
+    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 onToggleSaved = React.useCallback(async () => {
+      try {
+        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 () => {
+      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 renderHeaderBtns = React.useCallback(() => {
+      return (
+        <View style={styles.headerBtns}>
+          <Button
+            type="default"
+            testID="toggleLikeBtn"
+            accessibilityLabel="Like this feed"
+            accessibilityHint=""
+            onPress={onToggleLiked}>
+            {currentFeed?.isLiked ? (
+              <HeartIconSolid size={18} style={styles.liked} />
+            ) : (
+              <HeartIcon strokeWidth={3} size={18} style={pal.textLight} />
+            )}
+          </Button>
+          <Button
+            type={currentFeed?.isSaved ? 'default' : 'inverted'}
+            onPress={onToggleSaved}
+            accessibilityLabel={
+              currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
+            }
+            accessibilityHint=""
+            label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
+          />
+        </View>
+      )
+    }, [
+      pal,
+      currentFeed?.isSaved,
+      currentFeed?.isLiked,
+      onToggleSaved,
+      onToggleLiked,
+    ])
+
+    const renderListHeaderComponent = React.useCallback(() => {
+      return (
+        <>
+          <View style={[styles.header, pal.border]}>
+            <View style={s.flex1}>
+              <Text
+                testID="feedName"
+                type="title-xl"
+                style={[pal.text, s.bold]}>
+                {currentFeed?.displayName}
+              </Text>
+              {currentFeed && (
+                <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+                  by{' '}
+                  {currentFeed.data.creator.did === store.me.did ? (
+                    'you'
+                  ) : (
+                    <TextLink
+                      text={`@${currentFeed.data.creator.handle}`}
+                      href={`/profile/${currentFeed.data.creator.did}`}
+                    />
+                  )}
+                </Text>
+              )}
+            </View>
+            <View>
+              <UserAvatar
+                type="algo"
+                avatar={currentFeed?.data.avatar}
+                size={64}
+              />
+            </View>
+          </View>
+          <View style={styles.headerDetails}>
+            {currentFeed?.data.description ? (
+              <Text style={[pal.text, s.mb10]} numberOfLines={6}>
+                {currentFeed.data.description}
+              </Text>
+            ) : null}
+            <Text type="md-medium" style={pal.textLight}>
+              Liked by {currentFeed?.data.likeCount}{' '}
+              {pluralize(currentFeed?.data.likeCount || 0, 'user')}
+            </Text>
+            {isDesktopWeb && (
+              <View style={styles.headerBtns}>
+                <Button
+                  type={currentFeed?.isSaved ? 'default' : 'inverted'}
+                  onPress={onToggleSaved}
+                  accessibilityLabel={
+                    currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed'
+                  }
+                  accessibilityHint=""
+                  label={currentFeed?.isSaved ? 'Unsave' : 'Save'}
+                />
+
+                <Button type="default" onPress={onToggleLiked}>
+                  {currentFeed?.isLiked ? (
+                    <HeartIconSolid size={18} style={styles.liked} />
+                  ) : (
+                    <HeartIcon strokeWidth={3} size={18} style={pal.icon} />
+                  )}
+                </Button>
+              </View>
+            )}
+          </View>
+          <View style={[styles.fakeSelector, pal.border]}>
+            <View
+              style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
+              <Text type="md-medium" style={[pal.text]}>
+                Feed
+              </Text>
+            </View>
+          </View>
+        </>
+      )
+    }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved])
+
+    return (
+      <View style={s.hContentRegion}>
+        <ViewHeader title="" renderButton={renderHeaderBtns} />
+        <Feed
+          scrollElRef={scrollElRef}
+          feed={algoFeed}
+          ListHeaderComponent={renderListHeaderComponent}
+          extraData={uri}
+        />
+      </View>
+    )
+  }),
+)
+
+/*
+
+        <View style={[styles.headerContainer]}>
+          <View style={[styles.header]}>
+            <View style={styles.avatarContainer}>
+              <UserAvatar
+                type="algo"
+                size={28}
+                avatar={currentFeed?.data.avatar}
+              />
+              <Link href={`/profile/${currentFeed?.data.creator.handle}`}>
+                <Text style={[pal.textLight]}>
+                  @{currentFeed?.data.creator.handle}
+                </Text>
+              </Link>
+            </View>
+            <Text style={[pal.text]}>{currentFeed?.data.description}</Text>
+          </View>
+
+          <View style={[styles.buttonsContainer]}>
+          </View>
+        </View>
+        */
+
+const styles = StyleSheet.create({
+  headerBtns: {
+    flexDirection: 'row',
+    gap: 8,
+  },
+  header: {
+    flexDirection: 'row',
+    gap: 12,
+    paddingHorizontal: 16,
+    paddingTop: 12,
+    paddingBottom: 16,
+    borderTopWidth: 1,
+  },
+  headerDetails: {
+    paddingHorizontal: 16,
+    paddingBottom: 16,
+  },
+  fakeSelector: {
+    flexDirection: 'row',
+    paddingHorizontal: isDesktopWeb ? 16 : 6,
+  },
+  fakeSelectorItem: {
+    paddingHorizontal: 12,
+    paddingBottom: 8,
+    borderBottomWidth: 3,
+  },
+  liked: {
+    color: colors.red3,
+  },
+
+  /*  headerContainer: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 8,
+    marginBottom: 12,
+  },
+  header: {
+    alignItems: 'center',
+    gap: 4,
+  },
+  avatarContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8,
+  },
+  buttonsContainer: {
+    flexDirection: 'row',
+    gap: 8,
+  },
+  saveButton: {
+    minWidth: 100,
+    alignItems: 'center',
+  },
+  liked: {
+    color: colors.red3,
+  },
+  notLiked: {
+    color: colors.gray3,
+  },
+  likeButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingVertical: 4,
+    paddingHorizontal: 8,
+    borderRadius: 24,
+    gap: 4,
+  },*/
+})
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 01f27bae1..7c3ed831c 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -87,7 +87,7 @@ export const ProfileListScreen = withAuthRequired(
       return <EmptyState icon="users-slash" message="This list is empty!" />
     }, [])
 
-    const renderHeaderBtn = React.useCallback(() => {
+    const renderHeaderBtns = React.useCallback(() => {
       return (
         <View style={styles.headerBtns}>
           {list?.isOwner && (
@@ -148,7 +148,7 @@ export const ProfileListScreen = withAuthRequired(
           pal.border,
         ]}
         testID="moderationMutelistsScreen">
-        <ViewHeader title="" renderButton={renderHeaderBtn} />
+        <ViewHeader title="" renderButton={renderHeaderBtns} />
         <ListItems
           list={list}
           renderEmptyState={renderEmptyState}
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 6d55649f7..c32639889 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -137,12 +137,9 @@ const ListHeaderComponent = observer(
                     key={item.data.uri}
                     accessibilityRole="button"
                     onPress={() => {
-                      navigation.navigate('CustomFeed', {
+                      navigation.navigate('ProfileCustomFeed', {
                         name: item.data.creator.did,
                         rkey: new AtUri(item.data.uri).rkey,
-                        displayName:
-                          item.data.displayName ??
-                          `${item.data.creator.displayName}'s feed`,
                       })
                     }}
                     style={styles.pinnedItem}>