From c24389df87cc8a079c447aad6a0e0b025def84f5 Mon Sep 17 00:00:00 2001
From: Ansh Nanda <anshnanda10@gmail.com>
Date: Fri, 12 May 2023 19:46:50 -0700
Subject: custom feed embed

---
 src/view/com/util/post-embeds/index.tsx | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 2dda9069e..72158af42 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -13,6 +13,7 @@ import {
   AppBskyEmbedRecord,
   AppBskyEmbedRecordWithMedia,
   AppBskyFeedPost,
+  AppBskyFeedDefs,
 } from '@atproto/api'
 import {Link} from '../Link'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -24,6 +25,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
+import AlgoItem from 'view/com/algos/AlgoItem'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -42,6 +44,8 @@ export function PostEmbeds({
   const pal = usePalette('default')
   const store = useStores()
 
+  // quote post with media
+  // =
   if (
     AppBskyEmbedRecordWithMedia.isView(embed) &&
     AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
@@ -65,6 +69,8 @@ export function PostEmbeds({
     )
   }
 
+  // quote post
+  // =
   if (AppBskyEmbedRecord.isView(embed)) {
     if (
       AppBskyEmbedRecord.isViewRecord(embed.record) &&
@@ -87,6 +93,8 @@ export function PostEmbeds({
     }
   }
 
+  // image embed
+  // =
   if (AppBskyEmbedImages.isView(embed)) {
     const {images} = embed
 
@@ -132,10 +140,11 @@ export function PostEmbeds({
           />
         </View>
       )
-      // }
     }
   }
 
+  // external link embed
+  // =
   if (AppBskyEmbedExternal.isView(embed)) {
     const link = embed.external
     const youtubeVideoId = getYoutubeVideoId(link.uri)
@@ -153,6 +162,21 @@ export function PostEmbeds({
       </Link>
     )
   }
+
+  // custom feed embed (i.e. generator view)
+  // =
+  if (
+    AppBskyEmbedRecord.isView(embed) &&
+    AppBskyFeedDefs.isGeneratorView(embed.record)
+  ) {
+    return (
+      <AlgoItem
+        item={embed.record}
+        style={[pal.view, pal.border, styles.extOuter]}
+      />
+    )
+  }
+
   return <View />
 }
 
-- 
cgit 1.4.1


From 047024a5ac96a5292b6a2122835673b5a034f4c3 Mon Sep 17 00:00:00 2001
From: Ansh Nanda <anshnanda10@gmail.com>
Date: Sat, 13 May 2023 11:59:08 -0700
Subject: create algo-item model and redefine data models

---
 src/state/models/feeds/actor.ts          | 121 ----------------------------
 src/state/models/feeds/algo/actor.ts     | 121 ++++++++++++++++++++++++++++
 src/state/models/feeds/algo/algo-item.ts |  56 +++++++++++++
 src/state/models/feeds/algo/saved.ts     | 116 ++++++++++++++++++++++++++
 src/state/models/feeds/bookmarked.ts     | 134 -------------------------------
 src/state/models/ui/profile.ts           |   2 +-
 src/view/com/algos/AlgoItem.tsx          |  93 ++++++++++++++-------
 src/view/com/util/post-embeds/index.tsx  |   3 +-
 src/view/screens/Profile.tsx             |   8 +-
 9 files changed, 363 insertions(+), 291 deletions(-)
 delete mode 100644 src/state/models/feeds/actor.ts
 create mode 100644 src/state/models/feeds/algo/actor.ts
 create mode 100644 src/state/models/feeds/algo/algo-item.ts
 create mode 100644 src/state/models/feeds/algo/saved.ts
 delete mode 100644 src/state/models/feeds/bookmarked.ts

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/state/models/feeds/actor.ts b/src/state/models/feeds/actor.ts
deleted file mode 100644
index 08b7c2a74..000000000
--- a/src/state/models/feeds/actor.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  AppBskyFeedDefs as FeedDefs,
-  AppBskyFeedGetActorFeeds as GetActorFeeds,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-import {cleanError} from 'lib/strings/errors'
-
-const PAGE_SIZE = 30
-
-export class ActorFeedsModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  feeds: FeedDefs.GeneratorView[] = []
-
-  constructor(
-    public rootStore: RootStoreModel,
-    public params: GetActorFeeds.QueryParams,
-  ) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasContent() {
-    return this.feeds.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  clear() {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = false
-    this.error = ''
-    this.hasMore = true
-    this.loadMoreCursor = undefined
-    this.feeds = []
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({
-        actor: this.params.actor,
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      console.log('res', res.data.feeds)
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      this.rootStore.log.error('Failed to fetch user followers', err)
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetActorFeeds.Response) {
-    this.feeds = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetActorFeeds.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    this.feeds = this.feeds.concat(res.data.feeds)
-  }
-}
diff --git a/src/state/models/feeds/algo/actor.ts b/src/state/models/feeds/algo/actor.ts
new file mode 100644
index 000000000..e42df8495
--- /dev/null
+++ b/src/state/models/feeds/algo/actor.ts
@@ -0,0 +1,121 @@
+import {makeAutoObservable} from 'mobx'
+import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api'
+import {RootStoreModel} from '../../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {AlgoItemModel} from './algo-item'
+
+const PAGE_SIZE = 30
+
+export class ActorFeedsModel {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  hasMore = true
+  loadMoreCursor?: string
+
+  // data
+  feeds: AlgoItemModel[] = []
+
+  constructor(
+    public rootStore: RootStoreModel,
+    public params: GetActorFeeds.QueryParams,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  get hasContent() {
+    return this.feeds.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async refresh() {
+    return this.loadMore(true)
+  }
+
+  clear() {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = false
+    this.error = ''
+    this.hasMore = true
+    this.loadMoreCursor = undefined
+    this.feeds = []
+  }
+
+  loadMore = bundleAsync(async (replace: boolean = false) => {
+    if (!replace && !this.hasMore) {
+      return
+    }
+    this._xLoading(replace)
+    try {
+      const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({
+        actor: this.params.actor,
+        limit: PAGE_SIZE,
+        cursor: replace ? undefined : this.loadMoreCursor,
+      })
+      console.log('res', res.data.feeds)
+      if (replace) {
+        this._replaceAll(res)
+      } else {
+        this._appendAll(res)
+      }
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(e)
+    }
+  })
+
+  // state transitions
+  // =
+
+  _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  _xIdle(err?: any) {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = cleanError(err)
+    if (err) {
+      this.rootStore.log.error('Failed to fetch user followers', err)
+    }
+  }
+
+  // helper functions
+  // =
+
+  _replaceAll(res: GetActorFeeds.Response) {
+    this.feeds = []
+    this._appendAll(res)
+  }
+
+  _appendAll(res: GetActorFeeds.Response) {
+    this.loadMoreCursor = res.data.cursor
+    this.hasMore = !!this.loadMoreCursor
+    for (const f of res.data.feeds) {
+      this.feeds.push(new AlgoItemModel(this.rootStore, f))
+    }
+  }
+}
diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts
new file mode 100644
index 000000000..555d1d56d
--- /dev/null
+++ b/src/state/models/feeds/algo/algo-item.ts
@@ -0,0 +1,56 @@
+import {AppBskyFeedDefs} from '@atproto/api'
+import {makeAutoObservable, makeObservable} from 'mobx'
+import {RootStoreModel} from 'state/models/root-store'
+
+// algoitemmodel implemented in mobx
+export class AlgoItemModel {
+  // data
+  data: AppBskyFeedDefs.GeneratorView
+
+  constructor(
+    public rootStore: RootStoreModel,
+    view: AppBskyFeedDefs.GeneratorView,
+  ) {
+    this.data = view
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  set toggleSaved(value: boolean) {
+    console.log('toggleSaved', this.data.viewer)
+    if (this.data.viewer) {
+      this.data.viewer.saved = value
+    }
+  }
+
+  async save() {
+    try {
+      // runInAction(() => {
+      this.toggleSaved = true
+      // })
+      const res = await this.rootStore.agent.app.bsky.feed.saveFeed({
+        feed: this.data.uri,
+      })
+    } catch (e: any) {
+      this.rootStore.log.error('Failed to save feed', e)
+    }
+  }
+
+  async unsave() {
+    try {
+      // runInAction(() => {
+      this.toggleSaved = false
+      // })
+      const res = await this.rootStore.agent.app.bsky.feed.unsaveFeed({
+        feed: this.data.uri,
+      })
+    } catch (e: any) {
+      this.rootStore.log.error('Failed to unsanve feed', e)
+    }
+  }
+}
diff --git a/src/state/models/feeds/algo/saved.ts b/src/state/models/feeds/algo/saved.ts
new file mode 100644
index 000000000..fabb75ae0
--- /dev/null
+++ b/src/state/models/feeds/algo/saved.ts
@@ -0,0 +1,116 @@
+import {makeAutoObservable} from 'mobx'
+import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} from '@atproto/api'
+import {RootStoreModel} from '../../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {AlgoItemModel} from './algo-item'
+
+const PAGE_SIZE = 30
+
+export class SavedFeedsModel {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  hasMore = true
+  loadMoreCursor?: string
+
+  // data
+  feeds: AlgoItemModel[] = []
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  get hasContent() {
+    return this.feeds.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async refresh() {
+    return this.loadMore(true)
+  }
+
+  clear() {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = false
+    this.error = ''
+    this.hasMore = true
+    this.loadMoreCursor = undefined
+    this.feeds = []
+  }
+
+  loadMore = bundleAsync(async (replace: boolean = false) => {
+    if (!replace && !this.hasMore) {
+      return
+    }
+    this._xLoading(replace)
+    try {
+      const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
+        limit: PAGE_SIZE,
+        cursor: replace ? undefined : this.loadMoreCursor,
+      })
+      if (replace) {
+        this._replaceAll(res)
+      } else {
+        this._appendAll(res)
+      }
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(e)
+    }
+  })
+
+  // state transitions
+  // =
+
+  _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  _xIdle(err?: any) {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = cleanError(err)
+    if (err) {
+      this.rootStore.log.error('Failed to fetch user followers', err)
+    }
+  }
+
+  // helper functions
+  // =
+
+  _replaceAll(res: GetSavedFeeds.Response) {
+    this.feeds = []
+    this._appendAll(res)
+  }
+
+  _appendAll(res: GetSavedFeeds.Response) {
+    this.loadMoreCursor = res.data.cursor
+    this.hasMore = !!this.loadMoreCursor
+    for (const f of res.data.feeds) {
+      this.feeds.push(new AlgoItemModel(f))
+    }
+  }
+}
diff --git a/src/state/models/feeds/bookmarked.ts b/src/state/models/feeds/bookmarked.ts
deleted file mode 100644
index d472f0480..000000000
--- a/src/state/models/feeds/bookmarked.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  AppBskyFeedGetBookmarkedFeeds as GetBookmarkedFeeds,
-  // AppBskyFeedBookmarkFeed as bookmarkedFeed,
-  // AppBskyFeedUnbookmarkFeed as unbookmarkFeed,
-  AppBskyFeedDefs as FeedDefs,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-import {cleanError} from 'lib/strings/errors'
-
-const PAGE_SIZE = 30
-
-export class BookmarkedFeedsModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  feeds: FeedDefs.GeneratorView[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasContent() {
-    return this.feeds.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  clear() {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = false
-    this.error = ''
-    this.hasMore = true
-    this.loadMoreCursor = undefined
-    this.feeds = []
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.feed.getBookmarkedFeeds({
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  async bookmark(feed: FeedDefs.GeneratorView) {
-    try {
-      await this.rootStore.agent.app.bsky.feed.bookmarkFeed({feed: feed.uri})
-    } catch (e: any) {
-      this.rootStore.log.error('Failed to bookmark feed', e)
-    }
-  }
-
-  async unbookmark(feed: FeedDefs.GeneratorView) {
-    try {
-      await this.rootStore.agent.app.bsky.feed.unbookmarkFeed({feed: feed.uri})
-    } catch (e: any) {
-      this.rootStore.log.error('Failed to unbookmark feed', e)
-    }
-  }
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      this.rootStore.log.error('Failed to fetch user followers', err)
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetBookmarkedFeeds.Response) {
-    this.feeds = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetBookmarkedFeeds.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    this.feeds = this.feeds.concat(res.data.feeds)
-  }
-}
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index 86199108e..855955d12 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from '../root-store'
 import {ProfileModel} from '../content/profile'
 import {PostsFeedModel} from '../feeds/posts'
-import {ActorFeedsModel} from '../feeds/actor'
+import {ActorFeedsModel} from '../feeds/algo/actor'
 import {AppBskyFeedDefs} from '@atproto/api'
 
 export enum Sections {
diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx
index 979518f1d..987bfd68d 100644
--- a/src/view/com/algos/AlgoItem.tsx
+++ b/src/view/com/algos/AlgoItem.tsx
@@ -1,41 +1,62 @@
 import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {Text} from '../util/text/Text'
-import {AppBskyFeedDefs} from '@atproto/api'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
+import {Button} from '../util/forms/Button'
+import {observer} from 'mobx-react-lite'
+import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
 
-const AlgoItem = ({
-  item,
-  style,
-}: {
-  item: AppBskyFeedDefs.GeneratorView
-  style?: StyleProp<ViewStyle>
-}) => {
-  const pal = usePalette('default')
-  return (
-    <View style={[styles.container, style]} key={item.uri}>
-      <View style={[styles.headerContainer]}>
-        <View style={[s.mr20]}>
-          <UserAvatar size={56} avatar={item.avatar} />
+const AlgoItem = observer(
+  ({item, style}: {item: AlgoItemModel; style?: StyleProp<ViewStyle>}) => {
+    const pal = usePalette('default')
+    return (
+      <View style={[styles.container, style]} key={item.data.uri}>
+        <View style={[styles.headerContainer]}>
+          <View style={[s.mr10]}>
+            <UserAvatar size={36} avatar={item.data.avatar} />
+          </View>
+          <View style={[styles.headerTextContainer]}>
+            <Text style={[pal.text, s.bold]}>
+              {item.data.displayName ?? 'Feed name'}
+            </Text>
+            <Text style={[pal.textLight, styles.description]}>
+              {item.data.description ??
+                'THIS IS A FEED DESCRIPTION, IT WILL TELL YOU WHAT THE FEED IS ABOUT. THIS IS A COOL FEED ABOUT COOL PEOPLE.'}
+            </Text>
+          </View>
         </View>
-        <View style={[styles.headerTextContainer]}>
-          <Text style={[pal.text, s.bold]}>
-            {item.displayName ?? 'Feed name'}
-          </Text>
-          <Text style={[pal.textLight, styles.description]}>
-            {item.description ??
-              'THIS IS A FEED DESCRIPTION, IT WILL TELL YOU WHAT THE FEED IS ABOUT. THIS IS A COOL FEED ABOUT COOL PEOPLE.'}
-          </Text>
-        </View>
-      </View>
 
-      {/* TODO: this feed is like by *3* people UserAvatars and others */}
-    </View>
-  )
-}
+        {/* TODO: this feed is like by *3* people UserAvatars and others */}
+        <View style={styles.bottomContainer}>
+          <View style={styles.likedByContainer}>
+            <View style={styles.likedByAvatars}>
+              <UserAvatar size={24} avatar={item.data.avatar} />
+              <UserAvatar size={24} avatar={item.data.avatar} />
+              <UserAvatar size={24} avatar={item.data.avatar} />
+            </View>
 
+            <Text style={[pal.text, pal.textLight]}>Liked by 3 others</Text>
+          </View>
+          <View>
+            <Button
+              type="inverted"
+              onPress={() => {
+                if (item.data.viewer?.saved) {
+                  item.unsave()
+                } else {
+                  item.save()
+                }
+              }}
+              label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+            />
+          </View>
+        </View>
+      </View>
+    )
+  },
+)
 export default AlgoItem
 
 const styles = StyleSheet.create({
@@ -43,10 +64,10 @@ const styles = StyleSheet.create({
     paddingHorizontal: 18,
     paddingVertical: 20,
     flexDirection: 'column',
-    columnGap: 36,
     flex: 1,
     borderTopWidth: 1,
     borderTopColor: '#E5E5E5',
+    gap: 18,
   },
   headerContainer: {
     flexDirection: 'row',
@@ -60,4 +81,18 @@ const styles = StyleSheet.create({
     flex: 1,
     flexWrap: 'wrap',
   },
+  bottomContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  likedByContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 2,
+  },
+  likedByAvatars: {
+    flexDirection: 'row',
+    gap: -12,
+  },
 })
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 72158af42..3eb2720c0 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -26,6 +26,7 @@ import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
 import AlgoItem from 'view/com/algos/AlgoItem'
+import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -171,7 +172,7 @@ export function PostEmbeds({
   ) {
     return (
       <AlgoItem
-        item={embed.record}
+        item={new AlgoItemModel(store, embed.record)}
         style={[pal.view, pal.border, styles.extOuter]}
       />
     )
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index b88caf1f8..68aa97b66 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -21,8 +21,8 @@ import {FAB} from '../com/util/fab/FAB'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 import {ComposeIcon2} from 'lib/icons'
-import {AppBskyFeedDefs} from '@atproto/api'
 import AlgoItem from 'view/com/algos/AlgoItem'
+import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
 export const ProfileScreen = withAuthRequired(
@@ -154,10 +154,8 @@ export const ProfileScreen = withAuthRequired(
           )
         } else if (item instanceof PostsFeedSliceModel) {
           return <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
-        } else if (item.creator) {
-          // TODO: this is a hack to see if it is a custom feed. fix it to something more robust
-          const typedItem = item as AppBskyFeedDefs.GeneratorView
-          return <AlgoItem item={typedItem} />
+        } else if (item instanceof AlgoItemModel) {
+          return <AlgoItem item={item} />
         }
         return <View />
       },
-- 
cgit 1.4.1


From 1e54125f7f046742e08b7ea24ce7c546b5e5f83c Mon Sep 17 00:00:00 2001
From: Ansh Nanda <anshnanda10@gmail.com>
Date: Tue, 16 May 2023 16:27:38 -0700
Subject: reload AlgoItem on focus via prop

---
 src/view/com/algos/AlgoItem.tsx         | 6 +++++-
 src/view/com/util/post-embeds/index.tsx | 1 +
 2 files changed, 6 insertions(+), 1 deletion(-)

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx
index b52a7e00d..ffb8791a8 100644
--- a/src/view/com/algos/AlgoItem.tsx
+++ b/src/view/com/algos/AlgoItem.tsx
@@ -24,11 +24,13 @@ const AlgoItem = observer(
     style,
     showBottom = true,
     onLongPress,
+    reloadOnFocus = false,
   }: {
     item: AlgoItemModel
     style?: StyleProp<ViewStyle>
     showBottom?: boolean
     onLongPress?: () => void
+    reloadOnFocus?: boolean
   }) => {
     const store = useStores()
     const pal = usePalette('default')
@@ -36,7 +38,9 @@ const AlgoItem = observer(
 
     // TODO: this is pretty hacky, but it works for now
     useFocusEffect(() => {
-      item.reload()
+      if (reloadOnFocus) {
+        item.reload()
+      }
     })
 
     return (
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 3eb2720c0..621bd7c0f 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -174,6 +174,7 @@ export function PostEmbeds({
       <AlgoItem
         item={new AlgoItemModel(store, embed.record)}
         style={[pal.view, pal.border, styles.extOuter]}
+        reloadOnFocus={true}
       />
     )
   }
-- 
cgit 1.4.1


From b672006f7e1c21c635eb4ec60e21910af586c00d Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 13:52:16 -0500
Subject: Reorganize custom-feed state models and add the missing _reactKey
 attribute

---
 src/state/models/feeds/algo/actor.ts     | 121 ---------------
 src/state/models/feeds/algo/algo-item.ts | 142 ------------------
 src/state/models/feeds/algo/saved.ts     | 249 -------------------------------
 src/state/models/feeds/custom-feed.ts    | 143 ++++++++++++++++++
 src/state/models/lists/actor-feeds.ts    | 120 +++++++++++++++
 src/state/models/me.ts                   |   2 +-
 src/state/models/ui/profile.ts           |   2 +-
 src/state/models/ui/saved-feeds.ts       | 249 +++++++++++++++++++++++++++++++
 src/view/com/algos/AlgoItem.tsx          |   4 +-
 src/view/com/algos/SavedFeedItem.tsx     |  12 +-
 src/view/com/algos/useCustomFeed.ts      |   6 +-
 src/view/com/util/post-embeds/index.tsx  |   4 +-
 src/view/screens/PinnedFeeds.tsx         |   4 +-
 src/view/screens/Profile.tsx             |   4 +-
 src/view/screens/SavedFeeds.tsx          |   2 +-
 15 files changed, 535 insertions(+), 529 deletions(-)
 delete mode 100644 src/state/models/feeds/algo/actor.ts
 delete mode 100644 src/state/models/feeds/algo/algo-item.ts
 delete mode 100644 src/state/models/feeds/algo/saved.ts
 create mode 100644 src/state/models/feeds/custom-feed.ts
 create mode 100644 src/state/models/lists/actor-feeds.ts
 create mode 100644 src/state/models/ui/saved-feeds.ts

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/state/models/feeds/algo/actor.ts b/src/state/models/feeds/algo/actor.ts
deleted file mode 100644
index e42df8495..000000000
--- a/src/state/models/feeds/algo/actor.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api'
-import {RootStoreModel} from '../../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-import {cleanError} from 'lib/strings/errors'
-import {AlgoItemModel} from './algo-item'
-
-const PAGE_SIZE = 30
-
-export class ActorFeedsModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  feeds: AlgoItemModel[] = []
-
-  constructor(
-    public rootStore: RootStoreModel,
-    public params: GetActorFeeds.QueryParams,
-  ) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasContent() {
-    return this.feeds.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  clear() {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = false
-    this.error = ''
-    this.hasMore = true
-    this.loadMoreCursor = undefined
-    this.feeds = []
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({
-        actor: this.params.actor,
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      console.log('res', res.data.feeds)
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      this.rootStore.log.error('Failed to fetch user followers', err)
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetActorFeeds.Response) {
-    this.feeds = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetActorFeeds.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    for (const f of res.data.feeds) {
-      this.feeds.push(new AlgoItemModel(this.rootStore, f))
-    }
-  }
-}
diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts
deleted file mode 100644
index bd4ea4fd6..000000000
--- a/src/state/models/feeds/algo/algo-item.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import {AppBskyFeedDefs, AtUri} from '@atproto/api'
-import {makeAutoObservable} from 'mobx'
-import {RootStoreModel} from 'state/models/root-store'
-
-export class AlgoItemModel {
-  // data
-  data: AppBskyFeedDefs.GeneratorView
-
-  constructor(
-    public rootStore: RootStoreModel,
-    view: AppBskyFeedDefs.GeneratorView,
-  ) {
-    this.data = view
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  // local actions
-  // =
-  set toggleSaved(value: boolean) {
-    console.log('toggleSaved', this.data.viewer)
-    if (this.data.viewer) {
-      this.data.viewer.saved = value
-    }
-  }
-
-  get getUri() {
-    return this.data.uri
-  }
-
-  get isSaved() {
-    return this.data.viewer?.saved
-  }
-
-  get isLiked() {
-    return this.data.viewer?.like
-  }
-
-  private toggleLiked(s?: string) {
-    if (this.data.viewer) {
-      if (this.data.viewer.like) {
-        this.data.viewer.like = undefined
-      } else {
-        this.data.viewer.like = s
-      }
-    }
-  }
-
-  private incrementLike() {
-    if (this.data.likeCount) {
-      this.data.likeCount += 1
-    } else {
-      this.data.likeCount = 1
-    }
-  }
-
-  private decrementLike() {
-    if (this.data.likeCount) {
-      this.data.likeCount -= 1
-    } else {
-      this.data.likeCount = 0
-    }
-  }
-
-  private rewriteData(data: AppBskyFeedDefs.GeneratorView) {
-    this.data = data
-  }
-
-  // public apis
-  // =
-  async like() {
-    try {
-      const res = await this.rootStore.agent.app.bsky.feed.like.create(
-        {
-          repo: this.rootStore.me.did,
-        },
-        {
-          subject: {
-            uri: this.data.uri,
-            cid: this.data.cid,
-          },
-          createdAt: new Date().toISOString(),
-        },
-      )
-      this.toggleLiked(res.uri)
-      this.incrementLike()
-    } catch (e: any) {
-      this.rootStore.log.error('Failed to like feed', e)
-    }
-  }
-
-  async unlike() {
-    try {
-      await this.rootStore.agent.app.bsky.feed.like.delete({
-        repo: this.rootStore.me.did,
-        rkey: new AtUri(this.data.viewer?.like!).rkey,
-      })
-      this.toggleLiked()
-      this.decrementLike()
-    } catch (e: any) {
-      this.rootStore.log.error('Failed to unlike feed', e)
-    }
-  }
-
-  static async getView(store: RootStoreModel, uri: string) {
-    const res = await store.agent.app.bsky.feed.getFeedGenerator({
-      feed: uri,
-    })
-    const view = res.data.view
-    return view
-  }
-
-  async checkIsValid() {
-    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
-      feed: this.data.uri,
-    })
-    return res.data.isValid
-  }
-
-  async checkIsOnline() {
-    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
-      feed: this.data.uri,
-    })
-    return res.data.isOnline
-  }
-
-  async reload() {
-    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
-      feed: this.data.uri,
-    })
-    this.rewriteData(res.data.view)
-  }
-
-  serialize() {
-    return JSON.stringify(this.data)
-  }
-}
diff --git a/src/state/models/feeds/algo/saved.ts b/src/state/models/feeds/algo/saved.ts
deleted file mode 100644
index cb2015ccb..000000000
--- a/src/state/models/feeds/algo/saved.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} from '@atproto/api'
-import {RootStoreModel} from '../../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-import {cleanError} from 'lib/strings/errors'
-import {AlgoItemModel} from './algo-item'
-import {hasProp, isObj} from 'lib/type-guards'
-
-const PAGE_SIZE = 30
-
-export class SavedFeedsModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  feeds: AlgoItemModel[] = []
-  pinned: AlgoItemModel[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  serialize() {
-    return {
-      pinned: this.pinned.map(f => f.serialize()),
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      if (hasProp(v, 'pinned')) {
-        const pinnedSerialized = (v as any).pinned as string[]
-        const pinnedDeserialized = pinnedSerialized.map(
-          (s: string) => new AlgoItemModel(this.rootStore, JSON.parse(s)),
-        )
-        this.pinned = pinnedDeserialized
-      }
-    }
-  }
-
-  get hasContent() {
-    return this.feeds.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  get numOfFeeds() {
-    return this.feeds.length
-  }
-
-  get listOfFeedNames() {
-    return this.feeds.map(
-      f => f.data.displayName ?? f.data.creator.displayName + "'s feed",
-    )
-  }
-
-  get listOfPinnedFeedNames() {
-    return this.pinned.map(
-      f => f.data.displayName ?? f.data.creator.displayName + "'s feed",
-    )
-  }
-
-  get savedFeedsWithoutPinned() {
-    return this.feeds.filter(
-      f => !this.pinned.find(p => p.data.uri === f.data.uri),
-    )
-  }
-
-  togglePinnedFeed(feed: AlgoItemModel) {
-    if (!this.isPinned(feed)) {
-      this.pinned.push(feed)
-    } else {
-      this.removePinnedFeed(feed.data.uri)
-    }
-  }
-
-  removePinnedFeed(uri: string) {
-    this.pinned = this.pinned.filter(f => f.data.uri !== uri)
-  }
-
-  reorderPinnedFeeds(temp: AlgoItemModel[]) {
-    this.pinned = temp
-  }
-
-  isPinned(feed: AlgoItemModel) {
-    return this.pinned.find(f => f.data.uri === feed.data.uri) ? true : false
-  }
-
-  movePinnedItem(item: AlgoItemModel, direction: 'up' | 'down') {
-    if (this.pinned.length < 2) {
-      throw new Error('Array must have at least 2 items')
-    }
-    const index = this.pinned.indexOf(item)
-    if (index === -1) {
-      throw new Error('Item not found in array')
-    }
-
-    const len = this.pinned.length
-
-    runInAction(() => {
-      if (direction === 'up') {
-        if (index === 0) {
-          // Remove the item from the first place and put it at the end
-          this.pinned.push(this.pinned.shift()!)
-        } else {
-          // Swap the item with the one before it
-          const temp = this.pinned[index]
-          this.pinned[index] = this.pinned[index - 1]
-          this.pinned[index - 1] = temp
-        }
-      } else if (direction === 'down') {
-        if (index === len - 1) {
-          // Remove the item from the last place and put it at the start
-          this.pinned.unshift(this.pinned.pop()!)
-        } else {
-          // Swap the item with the one after it
-          const temp = this.pinned[index]
-          this.pinned[index] = this.pinned[index + 1]
-          this.pinned[index + 1] = temp
-        }
-      }
-      // this.pinned = [...this.pinned]
-    })
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  clear() {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = false
-    this.error = ''
-    this.hasMore = true
-    this.loadMoreCursor = undefined
-    this.feeds = []
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  removeFeed(uri: string) {
-    this.feeds = this.feeds.filter(f => f.data.uri !== uri)
-  }
-
-  addFeed(algoItem: AlgoItemModel) {
-    this.feeds.push(new AlgoItemModel(this.rootStore, algoItem.data))
-  }
-
-  async save(algoItem: AlgoItemModel) {
-    try {
-      await this.rootStore.agent.app.bsky.feed.saveFeed({
-        feed: algoItem.getUri,
-      })
-      algoItem.toggleSaved = true
-      this.addFeed(algoItem)
-    } catch (e: any) {
-      this.rootStore.log.error('Failed to save feed', e)
-    }
-  }
-
-  async unsave(algoItem: AlgoItemModel) {
-    const uri = algoItem.getUri
-    try {
-      await this.rootStore.agent.app.bsky.feed.unsaveFeed({
-        feed: uri,
-      })
-      algoItem.toggleSaved = false
-      this.removeFeed(uri)
-      this.removePinnedFeed(uri)
-    } catch (e: any) {
-      this.rootStore.log.error('Failed to unsanve feed', e)
-    }
-  }
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      this.rootStore.log.error('Failed to fetch user followers', err)
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetSavedFeeds.Response) {
-    this.feeds = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetSavedFeeds.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    for (const f of res.data.feeds) {
-      this.feeds.push(new AlgoItemModel(this.rootStore, f))
-    }
-  }
-}
diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts
new file mode 100644
index 000000000..20eef49ad
--- /dev/null
+++ b/src/state/models/feeds/custom-feed.ts
@@ -0,0 +1,143 @@
+import {AppBskyFeedDefs, AtUri} from '@atproto/api'
+import {makeAutoObservable} from 'mobx'
+import {RootStoreModel} from 'state/models/root-store'
+
+export class CustomFeedModel {
+  // data
+  _reactKey: string
+  data: AppBskyFeedDefs.GeneratorView
+
+  constructor(
+    public rootStore: RootStoreModel,
+    view: AppBskyFeedDefs.GeneratorView,
+  ) {
+    this._reactKey = view.uri
+    this.data = view
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  // local actions
+  // =
+  set toggleSaved(value: boolean) {
+    if (this.data.viewer) {
+      this.data.viewer.saved = value
+    }
+  }
+
+  get getUri() {
+    return this.data.uri
+  }
+
+  get isSaved() {
+    return this.data.viewer?.saved
+  }
+
+  get isLiked() {
+    return this.data.viewer?.like
+  }
+
+  private toggleLiked(s?: string) {
+    if (this.data.viewer) {
+      if (this.data.viewer.like) {
+        this.data.viewer.like = undefined
+      } else {
+        this.data.viewer.like = s
+      }
+    }
+  }
+
+  private incrementLike() {
+    if (this.data.likeCount) {
+      this.data.likeCount += 1
+    } else {
+      this.data.likeCount = 1
+    }
+  }
+
+  private decrementLike() {
+    if (this.data.likeCount) {
+      this.data.likeCount -= 1
+    } else {
+      this.data.likeCount = 0
+    }
+  }
+
+  private rewriteData(data: AppBskyFeedDefs.GeneratorView) {
+    this.data = data
+  }
+
+  // public apis
+  // =
+  async like() {
+    try {
+      const res = await this.rootStore.agent.app.bsky.feed.like.create(
+        {
+          repo: this.rootStore.me.did,
+        },
+        {
+          subject: {
+            uri: this.data.uri,
+            cid: this.data.cid,
+          },
+          createdAt: new Date().toISOString(),
+        },
+      )
+      this.toggleLiked(res.uri)
+      this.incrementLike()
+    } catch (e: any) {
+      this.rootStore.log.error('Failed to like feed', e)
+    }
+  }
+
+  async unlike() {
+    try {
+      await this.rootStore.agent.app.bsky.feed.like.delete({
+        repo: this.rootStore.me.did,
+        rkey: new AtUri(this.data.viewer?.like!).rkey,
+      })
+      this.toggleLiked()
+      this.decrementLike()
+    } catch (e: any) {
+      this.rootStore.log.error('Failed to unlike feed', e)
+    }
+  }
+
+  static async getView(store: RootStoreModel, uri: string) {
+    const res = await store.agent.app.bsky.feed.getFeedGenerator({
+      feed: uri,
+    })
+    const view = res.data.view
+    return view
+  }
+
+  async checkIsValid() {
+    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
+      feed: this.data.uri,
+    })
+    return res.data.isValid
+  }
+
+  async checkIsOnline() {
+    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
+      feed: this.data.uri,
+    })
+    return res.data.isOnline
+  }
+
+  async reload() {
+    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
+      feed: this.data.uri,
+    })
+    this.rewriteData(res.data.view)
+  }
+
+  serialize() {
+    return JSON.stringify(this.data)
+  }
+}
diff --git a/src/state/models/lists/actor-feeds.ts b/src/state/models/lists/actor-feeds.ts
new file mode 100644
index 000000000..0f2060581
--- /dev/null
+++ b/src/state/models/lists/actor-feeds.ts
@@ -0,0 +1,120 @@
+import {makeAutoObservable} from 'mobx'
+import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {CustomFeedModel} from '../feeds/custom-feed'
+
+const PAGE_SIZE = 30
+
+export class ActorFeedsModel {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  hasMore = true
+  loadMoreCursor?: string
+
+  // data
+  feeds: CustomFeedModel[] = []
+
+  constructor(
+    public rootStore: RootStoreModel,
+    public params: GetActorFeeds.QueryParams,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  get hasContent() {
+    return this.feeds.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async refresh() {
+    return this.loadMore(true)
+  }
+
+  clear() {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = false
+    this.error = ''
+    this.hasMore = true
+    this.loadMoreCursor = undefined
+    this.feeds = []
+  }
+
+  loadMore = bundleAsync(async (replace: boolean = false) => {
+    if (!replace && !this.hasMore) {
+      return
+    }
+    this._xLoading(replace)
+    try {
+      const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({
+        actor: this.params.actor,
+        limit: PAGE_SIZE,
+        cursor: replace ? undefined : this.loadMoreCursor,
+      })
+      if (replace) {
+        this._replaceAll(res)
+      } else {
+        this._appendAll(res)
+      }
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(e)
+    }
+  })
+
+  // state transitions
+  // =
+
+  _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  _xIdle(err?: any) {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = cleanError(err)
+    if (err) {
+      this.rootStore.log.error('Failed to fetch user followers', err)
+    }
+  }
+
+  // helper functions
+  // =
+
+  _replaceAll(res: GetActorFeeds.Response) {
+    this.feeds = []
+    this._appendAll(res)
+  }
+
+  _appendAll(res: GetActorFeeds.Response) {
+    this.loadMoreCursor = res.data.cursor
+    this.hasMore = !!this.loadMoreCursor
+    for (const f of res.data.feeds) {
+      this.feeds.push(new CustomFeedModel(this.rootStore, f))
+    }
+  }
+}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 68c89ac9b..9b2b96832 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -8,7 +8,7 @@ import {PostsFeedModel} from './feeds/posts'
 import {NotificationsFeedModel} from './feeds/notifications'
 import {MyFollowsCache} from './cache/my-follows'
 import {isObj, hasProp} from 'lib/type-guards'
-import {SavedFeedsModel} from './feeds/algo/saved'
+import {SavedFeedsModel} from './ui/saved-feeds'
 
 const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min
 const NOTIFS_UPDATE_INTERVAL = 30 * 1e3 // 30sec
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index 4f604bfc0..2189e8e97 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -3,7 +3,7 @@ import {AppBskyFeedDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {ProfileModel} from '../content/profile'
 import {PostsFeedModel} from '../feeds/posts'
-import {ActorFeedsModel} from '../feeds/algo/actor'
+import {ActorFeedsModel} from '../lists/actor-feeds'
 import {ListsListModel} from '../lists/lists-list'
 
 export enum Sections {
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
new file mode 100644
index 000000000..ce0de25ec
--- /dev/null
+++ b/src/state/models/ui/saved-feeds.ts
@@ -0,0 +1,249 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {CustomFeedModel} from '../feeds/custom-feed'
+import {hasProp, isObj} from 'lib/type-guards'
+
+const PAGE_SIZE = 30
+
+export class SavedFeedsModel {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  hasMore = true
+  loadMoreCursor?: string
+
+  // data
+  feeds: CustomFeedModel[] = []
+  pinned: CustomFeedModel[] = []
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  serialize() {
+    return {
+      pinned: this.pinned.map(f => f.serialize()),
+    }
+  }
+
+  hydrate(v: unknown) {
+    if (isObj(v)) {
+      if (hasProp(v, 'pinned')) {
+        const pinnedSerialized = (v as any).pinned as string[]
+        const pinnedDeserialized = pinnedSerialized.map(
+          (s: string) => new CustomFeedModel(this.rootStore, JSON.parse(s)),
+        )
+        this.pinned = pinnedDeserialized
+      }
+    }
+  }
+
+  get hasContent() {
+    return this.feeds.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  get numOfFeeds() {
+    return this.feeds.length
+  }
+
+  get listOfFeedNames() {
+    return this.feeds.map(
+      f => f.data.displayName ?? f.data.creator.displayName + "'s feed",
+    )
+  }
+
+  get listOfPinnedFeedNames() {
+    return this.pinned.map(
+      f => f.data.displayName ?? f.data.creator.displayName + "'s feed",
+    )
+  }
+
+  get savedFeedsWithoutPinned() {
+    return this.feeds.filter(
+      f => !this.pinned.find(p => p.data.uri === f.data.uri),
+    )
+  }
+
+  togglePinnedFeed(feed: CustomFeedModel) {
+    if (!this.isPinned(feed)) {
+      this.pinned.push(feed)
+    } else {
+      this.removePinnedFeed(feed.data.uri)
+    }
+  }
+
+  removePinnedFeed(uri: string) {
+    this.pinned = this.pinned.filter(f => f.data.uri !== uri)
+  }
+
+  reorderPinnedFeeds(temp: CustomFeedModel[]) {
+    this.pinned = temp
+  }
+
+  isPinned(feed: CustomFeedModel) {
+    return this.pinned.find(f => f.data.uri === feed.data.uri) ? true : false
+  }
+
+  movePinnedItem(item: CustomFeedModel, direction: 'up' | 'down') {
+    if (this.pinned.length < 2) {
+      throw new Error('Array must have at least 2 items')
+    }
+    const index = this.pinned.indexOf(item)
+    if (index === -1) {
+      throw new Error('Item not found in array')
+    }
+
+    const len = this.pinned.length
+
+    runInAction(() => {
+      if (direction === 'up') {
+        if (index === 0) {
+          // Remove the item from the first place and put it at the end
+          this.pinned.push(this.pinned.shift()!)
+        } else {
+          // Swap the item with the one before it
+          const temp = this.pinned[index]
+          this.pinned[index] = this.pinned[index - 1]
+          this.pinned[index - 1] = temp
+        }
+      } else if (direction === 'down') {
+        if (index === len - 1) {
+          // Remove the item from the last place and put it at the start
+          this.pinned.unshift(this.pinned.pop()!)
+        } else {
+          // Swap the item with the one after it
+          const temp = this.pinned[index]
+          this.pinned[index] = this.pinned[index + 1]
+          this.pinned[index + 1] = temp
+        }
+      }
+      // this.pinned = [...this.pinned]
+    })
+  }
+
+  // public api
+  // =
+
+  async refresh() {
+    return this.loadMore(true)
+  }
+
+  clear() {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = false
+    this.error = ''
+    this.hasMore = true
+    this.loadMoreCursor = undefined
+    this.feeds = []
+  }
+
+  loadMore = bundleAsync(async (replace: boolean = false) => {
+    if (!replace && !this.hasMore) {
+      return
+    }
+    this._xLoading(replace)
+    try {
+      const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
+        limit: PAGE_SIZE,
+        cursor: replace ? undefined : this.loadMoreCursor,
+      })
+      if (replace) {
+        this._replaceAll(res)
+      } else {
+        this._appendAll(res)
+      }
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(e)
+    }
+  })
+
+  removeFeed(uri: string) {
+    this.feeds = this.feeds.filter(f => f.data.uri !== uri)
+  }
+
+  addFeed(algoItem: CustomFeedModel) {
+    this.feeds.push(new CustomFeedModel(this.rootStore, algoItem.data))
+  }
+
+  async save(algoItem: CustomFeedModel) {
+    try {
+      await this.rootStore.agent.app.bsky.feed.saveFeed({
+        feed: algoItem.getUri,
+      })
+      algoItem.toggleSaved = true
+      this.addFeed(algoItem)
+    } catch (e: any) {
+      this.rootStore.log.error('Failed to save feed', e)
+    }
+  }
+
+  async unsave(algoItem: CustomFeedModel) {
+    const uri = algoItem.getUri
+    try {
+      await this.rootStore.agent.app.bsky.feed.unsaveFeed({
+        feed: uri,
+      })
+      algoItem.toggleSaved = false
+      this.removeFeed(uri)
+      this.removePinnedFeed(uri)
+    } catch (e: any) {
+      this.rootStore.log.error('Failed to unsanve feed', e)
+    }
+  }
+
+  // state transitions
+  // =
+
+  _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  _xIdle(err?: any) {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = cleanError(err)
+    if (err) {
+      this.rootStore.log.error('Failed to fetch user followers', err)
+    }
+  }
+
+  // helper functions
+  // =
+
+  _replaceAll(res: GetSavedFeeds.Response) {
+    this.feeds = []
+    this._appendAll(res)
+  }
+
+  _appendAll(res: GetSavedFeeds.Response) {
+    this.loadMoreCursor = res.data.cursor
+    this.hasMore = !!this.loadMoreCursor
+    for (const f of res.data.feeds) {
+      this.feeds.push(new CustomFeedModel(this.rootStore, f))
+    }
+  }
+}
diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx
index 45526309f..ea45005d3 100644
--- a/src/view/com/algos/AlgoItem.tsx
+++ b/src/view/com/algos/AlgoItem.tsx
@@ -12,7 +12,7 @@ import {colors, s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
 import {Button} from '../util/forms/Button'
 import {observer} from 'mobx-react-lite'
-import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
@@ -28,7 +28,7 @@ const AlgoItem = observer(
     showBottom = true,
     reloadOnFocus = false,
   }: {
-    item: AlgoItemModel
+    item: CustomFeedModel
     style?: StyleProp<ViewStyle>
     showBottom?: boolean
     reloadOnFocus?: boolean
diff --git a/src/view/com/algos/SavedFeedItem.tsx b/src/view/com/algos/SavedFeedItem.tsx
index bb4ec10b3..e8aeae726 100644
--- a/src/view/com/algos/SavedFeedItem.tsx
+++ b/src/view/com/algos/SavedFeedItem.tsx
@@ -3,12 +3,18 @@ import {View, TouchableOpacity, StyleSheet} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {colors} from 'lib/styles'
 import {observer} from 'mobx-react-lite'
-import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
-import {SavedFeedsModel} from 'state/models/feeds/algo/saved'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import AlgoItem from './AlgoItem'
 
 export const SavedFeedItem = observer(
-  ({item, savedFeeds}: {item: AlgoItemModel; savedFeeds: SavedFeedsModel}) => {
+  ({
+    item,
+    savedFeeds,
+  }: {
+    item: CustomFeedModel
+    savedFeeds: SavedFeedsModel
+  }) => {
     const isPinned = savedFeeds.isPinned(item)
 
     return (
diff --git a/src/view/com/algos/useCustomFeed.ts b/src/view/com/algos/useCustomFeed.ts
index cea9c1cea..ee40cf49e 100644
--- a/src/view/com/algos/useCustomFeed.ts
+++ b/src/view/com/algos/useCustomFeed.ts
@@ -1,10 +1,10 @@
 import {useEffect, useState} from 'react'
 import {useStores} from 'state/index'
-import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 export function useCustomFeed(uri: string) {
   const store = useStores()
-  const [item, setItem] = useState<AlgoItemModel>()
+  const [item, setItem] = useState<CustomFeedModel>()
   useEffect(() => {
     async function fetchView() {
       const res = await store.agent.app.bsky.feed.getFeedGenerator({
@@ -16,7 +16,7 @@ export function useCustomFeed(uri: string) {
     async function buildFeedItem() {
       const view = await fetchView()
       if (view) {
-        const temp = new AlgoItemModel(store, view)
+        const temp = new CustomFeedModel(store, view)
         setItem(temp)
       }
     }
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 328b9305b..727c56e9e 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -26,7 +26,7 @@ import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
 import AlgoItem from 'view/com/algos/AlgoItem'
-import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -172,7 +172,7 @@ export function PostEmbeds({
   ) {
     return (
       <AlgoItem
-        item={new AlgoItemModel(store, embed.record)}
+        item={new CustomFeedModel(store, embed.record)}
         style={[pal.view, pal.border, styles.extOuter]}
         reloadOnFocus={true}
       />
diff --git a/src/view/screens/PinnedFeeds.tsx b/src/view/screens/PinnedFeeds.tsx
index ac901ba71..dc4e22a71 100644
--- a/src/view/screens/PinnedFeeds.tsx
+++ b/src/view/screens/PinnedFeeds.tsx
@@ -26,7 +26,7 @@ import DraggableFlatList, {
 } from 'react-native-draggable-flatlist'
 import {SavedFeedItem} from 'view/com/algos/SavedFeedItem'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PinnedFeeds'>
 
@@ -101,7 +101,7 @@ export const PinnedFeeds = withAuthRequired(
 )
 
 const PinnedItem = observer(
-  ({item, drag}: {item: AlgoItemModel; drag: () => void}) => {
+  ({item, drag}: {item: CustomFeedModel; drag: () => void}) => {
     const pal = usePalette('default')
     const rootStore = useStores()
     const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore])
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 9c8dd458c..9c204f14f 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -26,7 +26,7 @@ import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 import {ComposeIcon2} from 'lib/icons'
 import AlgoItem from 'view/com/algos/AlgoItem'
-import {AlgoItemModel} from 'state/models/feeds/algo/algo-item'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
 
@@ -188,7 +188,7 @@ export const ProfileScreen = withAuthRequired(
             return (
               <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
             )
-          } else if (item instanceof AlgoItemModel) {
+          } else if (item instanceof CustomFeedModel) {
             return <AlgoItem item={item} />
           }
         }
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 4f0287035..c02d8ce36 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -21,7 +21,7 @@ import {CenteredView} from 'view/com/util/Views'
 import {Text} from 'view/com/util/text/Text'
 import {isDesktopWeb, isWeb} from 'platform/detection'
 import {s} from 'lib/styles'
-import {SavedFeedsModel} from 'state/models/feeds/algo/saved'
+import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {Link} from 'view/com/util/Link'
 import {UserAvatar} from 'view/com/util/UserAvatar'
 import {SavedFeedItem} from 'view/com/algos/SavedFeedItem'
-- 
cgit 1.4.1


From 3d09008bfde3352212ab69105063150a175b8d5d Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:03:08 -0500
Subject: Reorganize custom feeds in the view layer

---
 src/lib/hooks/useCustomFeed.ts          |  27 ++++++
 src/state/models/ui/profile.ts          |   4 -
 src/view/com/algos/AlgoItem.tsx         | 153 --------------------------------
 src/view/com/algos/SavedFeedItem.tsx    |  56 ------------
 src/view/com/algos/useCustomFeed.ts     |  27 ------
 src/view/com/feeds/CustomFeed.tsx       | 153 ++++++++++++++++++++++++++++++++
 src/view/com/feeds/SavedFeedItem.tsx    |  56 ++++++++++++
 src/view/com/util/post-embeds/index.tsx |   4 +-
 src/view/screens/CustomFeed.tsx         |   2 +-
 src/view/screens/PinnedFeeds.tsx        |   2 +-
 src/view/screens/Profile.tsx            |   4 +-
 src/view/screens/SavedFeeds.tsx         |   2 +-
 12 files changed, 243 insertions(+), 247 deletions(-)
 create mode 100644 src/lib/hooks/useCustomFeed.ts
 delete mode 100644 src/view/com/algos/AlgoItem.tsx
 delete mode 100644 src/view/com/algos/SavedFeedItem.tsx
 delete mode 100644 src/view/com/algos/useCustomFeed.ts
 create mode 100644 src/view/com/feeds/CustomFeed.tsx
 create mode 100644 src/view/com/feeds/SavedFeedItem.tsx

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/lib/hooks/useCustomFeed.ts b/src/lib/hooks/useCustomFeed.ts
new file mode 100644
index 000000000..ee40cf49e
--- /dev/null
+++ b/src/lib/hooks/useCustomFeed.ts
@@ -0,0 +1,27 @@
+import {useEffect, useState} from 'react'
+import {useStores} from 'state/index'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+
+export function useCustomFeed(uri: string) {
+  const store = useStores()
+  const [item, setItem] = useState<CustomFeedModel>()
+  useEffect(() => {
+    async function fetchView() {
+      const res = await store.agent.app.bsky.feed.getFeedGenerator({
+        feed: uri,
+      })
+      const view = res.data.view
+      return view
+    }
+    async function buildFeedItem() {
+      const view = await fetchView()
+      if (view) {
+        const temp = new CustomFeedModel(store, view)
+        setItem(temp)
+      }
+    }
+    buildFeedItem()
+  }, [store, uri])
+
+  return item
+}
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index 2189e8e97..54ee461b0 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -1,5 +1,4 @@
 import {makeAutoObservable} from 'mobx'
-import {AppBskyFeedDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {ProfileModel} from '../content/profile'
 import {PostsFeedModel} from '../feeds/posts'
@@ -90,9 +89,6 @@ export class ProfileUiModel {
   get selectedView() {
     return this.selectorItems[this.selectedViewIndex]
   }
-  isGeneratorView(v: any) {
-    return AppBskyFeedDefs.isGeneratorView(v)
-  }
 
   get uiItems() {
     let arr: any[] = []
diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx
deleted file mode 100644
index ea45005d3..000000000
--- a/src/view/com/algos/AlgoItem.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import React from 'react'
-import {
-  StyleProp,
-  StyleSheet,
-  View,
-  ViewStyle,
-  TouchableOpacity,
-} from 'react-native'
-import {Text} from '../util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {colors, s} from 'lib/styles'
-import {UserAvatar} from '../util/UserAvatar'
-import {Button} from '../util/forms/Button'
-import {observer} from 'mobx-react-lite'
-import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-import {useFocusEffect, useNavigation} from '@react-navigation/native'
-import {NavigationProp} from 'lib/routes/types'
-import {useStores} from 'state/index'
-import {HeartIconSolid} from 'lib/icons'
-import {pluralize} from 'lib/strings/helpers'
-import {AtUri} from '@atproto/api'
-import {isWeb} from 'platform/detection'
-
-const AlgoItem = observer(
-  ({
-    item,
-    style,
-    showBottom = true,
-    reloadOnFocus = false,
-  }: {
-    item: CustomFeedModel
-    style?: StyleProp<ViewStyle>
-    showBottom?: boolean
-    reloadOnFocus?: boolean
-  }) => {
-    const store = useStores()
-    const pal = usePalette('default')
-    const navigation = useNavigation<NavigationProp>()
-
-    // TODO: this is pretty hacky, but it works for now
-    // causes issues on web
-    useFocusEffect(() => {
-      if (reloadOnFocus && !isWeb) {
-        item.reload()
-      }
-    })
-
-    return (
-      <TouchableOpacity
-        accessibilityRole="button"
-        style={[styles.container, style]}
-        onPress={() => {
-          navigation.navigate('CustomFeed', {
-            name: item.data.creator.did,
-            rkey: new AtUri(item.data.uri).rkey,
-            displayName:
-              item.data.displayName ??
-              `${item.data.creator.displayName}'s feed`,
-          })
-        }}
-        key={item.data.uri}>
-        <View style={[styles.headerContainer]}>
-          <View style={[s.mr10]}>
-            <UserAvatar type="algo" size={36} avatar={item.data.avatar} />
-          </View>
-          <View style={[styles.headerTextContainer]}>
-            <Text style={[pal.text, s.bold]}>
-              {item.data.displayName ?? 'Feed name'}
-            </Text>
-            <Text style={[pal.textLight, styles.description]} numberOfLines={5}>
-              {item.data.description ??
-                "Explore our Feed for the latest updates and insights! Dive into a world of intriguing articles, trending news, and exciting stories that cover a wide range of topics. From technology breakthroughs to lifestyle tips, there's something here for everyone. Stay informed and get inspired with us. Join the conversation now!"}
-            </Text>
-          </View>
-        </View>
-
-        {showBottom ? (
-          <View style={styles.bottomContainer}>
-            <View style={styles.likedByContainer}>
-              {/* <View style={styles.likedByAvatars}>
-              <UserAvatar size={24} avatar={item.data.avatar} />
-              <UserAvatar size={24} avatar={item.data.avatar} />
-              <UserAvatar size={24} avatar={item.data.avatar} />
-            </View> */}
-
-              <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} />
-              <Text style={[pal.text, pal.textLight]}>
-                {item.data.likeCount && item.data.likeCount > 0
-                  ? `Liked by ${item.data.likeCount} ${pluralize(
-                      item.data.likeCount,
-                      'other',
-                    )}`
-                  : 'Be the first to like this'}
-              </Text>
-            </View>
-            <View>
-              <Button
-                type={item.isSaved ? 'default' : 'inverted'}
-                onPress={() => {
-                  if (item.data.viewer?.saved) {
-                    store.me.savedFeeds.unsave(item)
-                  } else {
-                    store.me.savedFeeds.save(item)
-                  }
-                }}
-                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
-              />
-            </View>
-          </View>
-        ) : null}
-      </TouchableOpacity>
-    )
-  },
-)
-export default AlgoItem
-
-const styles = StyleSheet.create({
-  container: {
-    paddingHorizontal: 18,
-    paddingVertical: 20,
-    flexDirection: 'column',
-    flex: 1,
-    borderTopWidth: 1,
-    borderTopColor: '#E5E5E5',
-    gap: 18,
-  },
-  headerContainer: {
-    flexDirection: 'row',
-  },
-  headerTextContainer: {
-    flexDirection: 'column',
-    columnGap: 4,
-    flex: 1,
-  },
-  description: {
-    flex: 1,
-    flexWrap: 'wrap',
-  },
-  bottomContainer: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-  },
-  likedByContainer: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 2,
-  },
-  likedByAvatars: {
-    flexDirection: 'row',
-    gap: -12,
-  },
-})
diff --git a/src/view/com/algos/SavedFeedItem.tsx b/src/view/com/algos/SavedFeedItem.tsx
deleted file mode 100644
index e8aeae726..000000000
--- a/src/view/com/algos/SavedFeedItem.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react'
-import {View, TouchableOpacity, StyleSheet} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {colors} from 'lib/styles'
-import {observer} from 'mobx-react-lite'
-import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
-import AlgoItem from './AlgoItem'
-
-export const SavedFeedItem = observer(
-  ({
-    item,
-    savedFeeds,
-  }: {
-    item: CustomFeedModel
-    savedFeeds: SavedFeedsModel
-  }) => {
-    const isPinned = savedFeeds.isPinned(item)
-
-    return (
-      <View style={styles.itemContainer}>
-        <AlgoItem
-          key={item.data.uri}
-          item={item}
-          showBottom={false}
-          style={styles.item}
-        />
-        <TouchableOpacity
-          accessibilityRole="button"
-          onPress={() => {
-            savedFeeds.togglePinnedFeed(item)
-            console.log('pinned', savedFeeds.pinned)
-            console.log('isPinned', savedFeeds.isPinned(item))
-          }}>
-          <FontAwesomeIcon
-            icon="thumb-tack"
-            size={20}
-            color={isPinned ? colors.blue3 : colors.gray3}
-          />
-        </TouchableOpacity>
-      </View>
-    )
-  },
-)
-
-const styles = StyleSheet.create({
-  itemContainer: {
-    flex: 1,
-    flexDirection: 'row',
-    alignItems: 'center',
-    marginRight: 18,
-  },
-  item: {
-    borderTopWidth: 0,
-  },
-})
diff --git a/src/view/com/algos/useCustomFeed.ts b/src/view/com/algos/useCustomFeed.ts
deleted file mode 100644
index ee40cf49e..000000000
--- a/src/view/com/algos/useCustomFeed.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import {useEffect, useState} from 'react'
-import {useStores} from 'state/index'
-import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-
-export function useCustomFeed(uri: string) {
-  const store = useStores()
-  const [item, setItem] = useState<CustomFeedModel>()
-  useEffect(() => {
-    async function fetchView() {
-      const res = await store.agent.app.bsky.feed.getFeedGenerator({
-        feed: uri,
-      })
-      const view = res.data.view
-      return view
-    }
-    async function buildFeedItem() {
-      const view = await fetchView()
-      if (view) {
-        const temp = new CustomFeedModel(store, view)
-        setItem(temp)
-      }
-    }
-    buildFeedItem()
-  }, [store, uri])
-
-  return item
-}
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
new file mode 100644
index 000000000..2137081f3
--- /dev/null
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -0,0 +1,153 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  View,
+  ViewStyle,
+  TouchableOpacity,
+} from 'react-native'
+import {Text} from '../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {colors, s} from 'lib/styles'
+import {UserAvatar} from '../util/UserAvatar'
+import {Button} from '../util/forms/Button'
+import {observer} from 'mobx-react-lite'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
+import {useStores} from 'state/index'
+import {HeartIconSolid} from 'lib/icons'
+import {pluralize} from 'lib/strings/helpers'
+import {AtUri} from '@atproto/api'
+import {isWeb} from 'platform/detection'
+
+const CustomFeed = observer(
+  ({
+    item,
+    style,
+    showBottom = true,
+    reloadOnFocus = false,
+  }: {
+    item: CustomFeedModel
+    style?: StyleProp<ViewStyle>
+    showBottom?: boolean
+    reloadOnFocus?: boolean
+  }) => {
+    const store = useStores()
+    const pal = usePalette('default')
+    const navigation = useNavigation<NavigationProp>()
+
+    // TODO: this is pretty hacky, but it works for now
+    // causes issues on web
+    useFocusEffect(() => {
+      if (reloadOnFocus && !isWeb) {
+        item.reload()
+      }
+    })
+
+    return (
+      <TouchableOpacity
+        accessibilityRole="button"
+        style={[styles.container, style]}
+        onPress={() => {
+          navigation.navigate('CustomFeed', {
+            name: item.data.creator.did,
+            rkey: new AtUri(item.data.uri).rkey,
+            displayName:
+              item.data.displayName ??
+              `${item.data.creator.displayName}'s feed`,
+          })
+        }}
+        key={item.data.uri}>
+        <View style={[styles.headerContainer]}>
+          <View style={[s.mr10]}>
+            <UserAvatar type="algo" size={36} avatar={item.data.avatar} />
+          </View>
+          <View style={[styles.headerTextContainer]}>
+            <Text style={[pal.text, s.bold]}>
+              {item.data.displayName ?? 'Feed name'}
+            </Text>
+            <Text style={[pal.textLight, styles.description]} numberOfLines={5}>
+              {item.data.description ??
+                "Explore our Feed for the latest updates and insights! Dive into a world of intriguing articles, trending news, and exciting stories that cover a wide range of topics. From technology breakthroughs to lifestyle tips, there's something here for everyone. Stay informed and get inspired with us. Join the conversation now!"}
+            </Text>
+          </View>
+        </View>
+
+        {showBottom ? (
+          <View style={styles.bottomContainer}>
+            <View style={styles.likedByContainer}>
+              {/* <View style={styles.likedByAvatars}>
+              <UserAvatar size={24} avatar={item.data.avatar} />
+              <UserAvatar size={24} avatar={item.data.avatar} />
+              <UserAvatar size={24} avatar={item.data.avatar} />
+            </View> */}
+
+              <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} />
+              <Text style={[pal.text, pal.textLight]}>
+                {item.data.likeCount && item.data.likeCount > 0
+                  ? `Liked by ${item.data.likeCount} ${pluralize(
+                      item.data.likeCount,
+                      'other',
+                    )}`
+                  : 'Be the first to like this'}
+              </Text>
+            </View>
+            <View>
+              <Button
+                type={item.isSaved ? 'default' : 'inverted'}
+                onPress={() => {
+                  if (item.data.viewer?.saved) {
+                    store.me.savedFeeds.unsave(item)
+                  } else {
+                    store.me.savedFeeds.save(item)
+                  }
+                }}
+                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+              />
+            </View>
+          </View>
+        ) : null}
+      </TouchableOpacity>
+    )
+  },
+)
+export default CustomFeed
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 18,
+    paddingVertical: 20,
+    flexDirection: 'column',
+    flex: 1,
+    borderTopWidth: 1,
+    borderTopColor: '#E5E5E5',
+    gap: 18,
+  },
+  headerContainer: {
+    flexDirection: 'row',
+  },
+  headerTextContainer: {
+    flexDirection: 'column',
+    columnGap: 4,
+    flex: 1,
+  },
+  description: {
+    flex: 1,
+    flexWrap: 'wrap',
+  },
+  bottomContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  likedByContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 2,
+  },
+  likedByAvatars: {
+    flexDirection: 'row',
+    gap: -12,
+  },
+})
diff --git a/src/view/com/feeds/SavedFeedItem.tsx b/src/view/com/feeds/SavedFeedItem.tsx
new file mode 100644
index 000000000..b93cebe1e
--- /dev/null
+++ b/src/view/com/feeds/SavedFeedItem.tsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import {View, TouchableOpacity, StyleSheet} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {colors} from 'lib/styles'
+import {observer} from 'mobx-react-lite'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
+import CustomFeed from './CustomFeed'
+
+export const SavedFeedItem = observer(
+  ({
+    item,
+    savedFeeds,
+  }: {
+    item: CustomFeedModel
+    savedFeeds: SavedFeedsModel
+  }) => {
+    const isPinned = savedFeeds.isPinned(item)
+
+    return (
+      <View style={styles.itemContainer}>
+        <CustomFeed
+          key={item.data.uri}
+          item={item}
+          showBottom={false}
+          style={styles.item}
+        />
+        <TouchableOpacity
+          accessibilityRole="button"
+          onPress={() => {
+            savedFeeds.togglePinnedFeed(item)
+            console.log('pinned', savedFeeds.pinned)
+            console.log('isPinned', savedFeeds.isPinned(item))
+          }}>
+          <FontAwesomeIcon
+            icon="thumb-tack"
+            size={20}
+            color={isPinned ? colors.blue3 : colors.gray3}
+          />
+        </TouchableOpacity>
+      </View>
+    )
+  },
+)
+
+const styles = StyleSheet.create({
+  itemContainer: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginRight: 18,
+  },
+  item: {
+    borderTopWidth: 0,
+  },
+})
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 727c56e9e..1f8e48810 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -25,7 +25,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
-import AlgoItem from 'view/com/algos/AlgoItem'
+import CustomFeed from 'view/com/feeds/CustomFeed'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 type Embed =
@@ -171,7 +171,7 @@ export function PostEmbeds({
     AppBskyFeedDefs.isGeneratorView(embed.record)
   ) {
     return (
-      <AlgoItem
+      <CustomFeed
         item={new CustomFeedModel(store, embed.record)}
         style={[pal.view, pal.border, styles.extOuter]}
         reloadOnFocus={true}
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
index a50916a9d..76125fa5c 100644
--- a/src/view/screens/CustomFeed.tsx
+++ b/src/view/screens/CustomFeed.tsx
@@ -9,7 +9,7 @@ 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 'view/com/algos/useCustomFeed'
+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'
diff --git a/src/view/screens/PinnedFeeds.tsx b/src/view/screens/PinnedFeeds.tsx
index dc4e22a71..85f737749 100644
--- a/src/view/screens/PinnedFeeds.tsx
+++ b/src/view/screens/PinnedFeeds.tsx
@@ -24,7 +24,7 @@ import DraggableFlatList, {
   ShadowDecorator,
   ScaleDecorator,
 } from 'react-native-draggable-flatlist'
-import {SavedFeedItem} from 'view/com/algos/SavedFeedItem'
+import {SavedFeedItem} from 'view/com/feeds/SavedFeedItem'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 9c204f14f..a80e36237 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -25,7 +25,7 @@ import {FAB} from '../com/util/fab/FAB'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 import {ComposeIcon2} from 'lib/icons'
-import AlgoItem from 'view/com/algos/AlgoItem'
+import CustomFeed from 'view/com/feeds/CustomFeed'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
@@ -189,7 +189,7 @@ export const ProfileScreen = withAuthRequired(
               <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
             )
           } else if (item instanceof CustomFeedModel) {
-            return <AlgoItem item={item} />
+            return <CustomFeed item={item} />
           }
         }
         return <View />
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index c02d8ce36..6d55649f7 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -24,7 +24,7 @@ import {s} from 'lib/styles'
 import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {Link} from 'view/com/util/Link'
 import {UserAvatar} from 'view/com/util/UserAvatar'
-import {SavedFeedItem} from 'view/com/algos/SavedFeedItem'
+import {SavedFeedItem} from 'view/com/feeds/SavedFeedItem'
 import {AtUri} from '@atproto/api'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
-- 
cgit 1.4.1


From a4c6dd6b1ead07e5606146408e3d78a24dc5977a Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:24:27 -0500
Subject: Tune up the custom feed card rendering

---
 src/view/com/feeds/CustomFeed.tsx       | 71 ++++++++++++++-------------------
 src/view/com/feeds/SavedFeedItem.tsx    |  9 +----
 src/view/com/util/post-embeds/index.tsx | 14 +++++--
 src/view/screens/Profile.tsx            |  4 +-
 4 files changed, 44 insertions(+), 54 deletions(-)

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 2137081f3..edadada9c 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -16,21 +16,21 @@ import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
-import {HeartIconSolid} from 'lib/icons'
+import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import {isWeb} from 'platform/detection'
 
-const CustomFeed = observer(
+export const CustomFeed = observer(
   ({
     item,
     style,
-    showBottom = true,
+    showLikes = false,
     reloadOnFocus = false,
   }: {
     item: CustomFeedModel
     style?: StyleProp<ViewStyle>
-    showBottom?: boolean
+    showLikes?: boolean
     reloadOnFocus?: boolean
   }) => {
     const store = useStores()
@@ -67,52 +67,43 @@ const CustomFeed = observer(
             <Text style={[pal.text, s.bold]}>
               {item.data.displayName ?? 'Feed name'}
             </Text>
-            <Text style={[pal.textLight, styles.description]} numberOfLines={5}>
-              {item.data.description ??
-                "Explore our Feed for the latest updates and insights! Dive into a world of intriguing articles, trending news, and exciting stories that cover a wide range of topics. From technology breakthroughs to lifestyle tips, there's something here for everyone. Stay informed and get inspired with us. Join the conversation now!"}
-            </Text>
+            <Text style={[pal.textLight]}>by @{item.data.creator.handle}</Text>
+          </View>
+          <View>
+            <Button
+              type={item.isSaved ? 'default' : 'inverted'}
+              onPress={() => {
+                if (item.data.viewer?.saved) {
+                  store.me.savedFeeds.unsave(item)
+                } else {
+                  store.me.savedFeeds.save(item)
+                }
+              }}
+              label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
+            />
           </View>
         </View>
 
-        {showBottom ? (
+        {item.data.description ? (
+          <Text style={[pal.textLight, styles.description]} numberOfLines={3}>
+            {item.data.description}
+          </Text>
+        ) : null}
+
+        {showLikes ? (
           <View style={styles.bottomContainer}>
             <View style={styles.likedByContainer}>
-              {/* <View style={styles.likedByAvatars}>
-              <UserAvatar size={24} avatar={item.data.avatar} />
-              <UserAvatar size={24} avatar={item.data.avatar} />
-              <UserAvatar size={24} avatar={item.data.avatar} />
-            </View> */}
-
-              <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} />
-              <Text style={[pal.text, pal.textLight]}>
-                {item.data.likeCount && item.data.likeCount > 0
-                  ? `Liked by ${item.data.likeCount} ${pluralize(
-                      item.data.likeCount,
-                      'other',
-                    )}`
-                  : 'Be the first to like this'}
+              <Text type="sm-medium" style={[pal.text, pal.textLight]}>
+                Liked by {item.data.likeCount || 0}{' '}
+                {pluralize(item.data.likeCount || 0, 'user')}
               </Text>
             </View>
-            <View>
-              <Button
-                type={item.isSaved ? 'default' : 'inverted'}
-                onPress={() => {
-                  if (item.data.viewer?.saved) {
-                    store.me.savedFeeds.unsave(item)
-                  } else {
-                    store.me.savedFeeds.save(item)
-                  }
-                }}
-                label={item.data.viewer?.saved ? 'Unsave' : 'Save'}
-              />
-            </View>
           </View>
         ) : null}
       </TouchableOpacity>
     )
   },
 )
-export default CustomFeed
 
 const styles = StyleSheet.create({
   container: {
@@ -122,7 +113,7 @@ const styles = StyleSheet.create({
     flex: 1,
     borderTopWidth: 1,
     borderTopColor: '#E5E5E5',
-    gap: 18,
+    gap: 14,
   },
   headerContainer: {
     flexDirection: 'row',
@@ -146,8 +137,4 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     gap: 2,
   },
-  likedByAvatars: {
-    flexDirection: 'row',
-    gap: -12,
-  },
 })
diff --git a/src/view/com/feeds/SavedFeedItem.tsx b/src/view/com/feeds/SavedFeedItem.tsx
index b93cebe1e..9ebededc4 100644
--- a/src/view/com/feeds/SavedFeedItem.tsx
+++ b/src/view/com/feeds/SavedFeedItem.tsx
@@ -5,7 +5,7 @@ import {colors} from 'lib/styles'
 import {observer} from 'mobx-react-lite'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
-import CustomFeed from './CustomFeed'
+import {CustomFeed} from './CustomFeed'
 
 export const SavedFeedItem = observer(
   ({
@@ -19,12 +19,7 @@ export const SavedFeedItem = observer(
 
     return (
       <View style={styles.itemContainer}>
-        <CustomFeed
-          key={item.data.uri}
-          item={item}
-          showBottom={false}
-          style={styles.item}
-        />
+        <CustomFeed key={item.data.uri} item={item} style={styles.item} />
         <TouchableOpacity
           accessibilityRole="button"
           onPress={() => {
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 1f8e48810..ed2f8feba 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -25,7 +25,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
-import CustomFeed from 'view/com/feeds/CustomFeed'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 
 type Embed =
@@ -173,8 +173,9 @@ export function PostEmbeds({
     return (
       <CustomFeed
         item={new CustomFeedModel(store, embed.record)}
-        style={[pal.view, pal.border, styles.extOuter]}
-        reloadOnFocus={true}
+        style={[pal.view, pal.border, styles.customFeedOuter]}
+        reloadOnFocus
+        showLikes
       />
     )
   }
@@ -198,6 +199,13 @@ const styles = StyleSheet.create({
     borderRadius: 8,
     marginTop: 4,
   },
+  customFeedOuter: {
+    borderWidth: 1,
+    borderRadius: 8,
+    marginTop: 4,
+    paddingHorizontal: 12,
+    paddingVertical: 12,
+  },
   alt: {
     backgroundColor: 'rgba(0, 0, 0, 0.75)',
     borderRadius: 6,
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index a80e36237..9224a5967 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -25,7 +25,7 @@ import {FAB} from '../com/util/fab/FAB'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 import {ComposeIcon2} from 'lib/icons'
-import CustomFeed from 'view/com/feeds/CustomFeed'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
@@ -189,7 +189,7 @@ export const ProfileScreen = withAuthRequired(
               <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
             )
           } else if (item instanceof CustomFeedModel) {
-            return <CustomFeed item={item} />
+            return <CustomFeed item={item} showLikes />
           }
         }
         return <View />
-- 
cgit 1.4.1


From 2fd3b4ca04622836ad2a65ec8992fd429ecd9fed Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 14:55:04 -0500
Subject: Clean up the custom-feed model

---
 src/state/models/feeds/custom-feed.ts   | 115 +++++++++++---------------------
 src/state/models/ui/saved-feeds.ts      |  14 ++--
 src/view/com/util/UserAvatar.tsx        |   2 +-
 src/view/com/util/post-embeds/index.tsx |   1 +
 src/view/screens/Home.tsx               |   2 +-
 5 files changed, 45 insertions(+), 89 deletions(-)

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts
index e60ac2740..5e550ec69 100644
--- a/src/state/models/feeds/custom-feed.ts
+++ b/src/state/models/feeds/custom-feed.ts
@@ -1,5 +1,5 @@
-import {AppBskyFeedDefs, AtUri} from '@atproto/api'
-import {makeAutoObservable} from 'mobx'
+import {AppBskyFeedDefs} from '@atproto/api'
+import {makeAutoObservable, runInAction} from 'mobx'
 import {RootStoreModel} from 'state/models/root-store'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 
@@ -25,13 +25,8 @@ export class CustomFeedModel {
 
   // local actions
   // =
-  set toggleSaved(value: boolean) {
-    if (this.data.viewer) {
-      this.data.viewer.saved = value
-    }
-  }
 
-  get getUri() {
+  get uri() {
     return this.data.uri
   }
 
@@ -50,99 +45,65 @@ export class CustomFeedModel {
     return this.data.viewer?.like
   }
 
-  private toggleLiked(s?: string) {
-    if (this.data.viewer) {
-      if (this.data.viewer.like) {
-        this.data.viewer.like = undefined
-      } else {
-        this.data.viewer.like = s
-      }
-    }
-  }
-
-  private incrementLike() {
-    if (this.data.likeCount) {
-      this.data.likeCount += 1
-    } else {
-      this.data.likeCount = 1
-    }
-  }
+  // public apis
+  // =
 
-  private decrementLike() {
-    if (this.data.likeCount) {
-      this.data.likeCount -= 1
-    } else {
-      this.data.likeCount = 0
-    }
+  async save() {
+    await this.rootStore.agent.app.bsky.feed.saveFeed({
+      feed: this.uri,
+    })
+    runInAction(() => {
+      this.data.viewer = this.data.viewer || {}
+      this.data.viewer.saved = true
+    })
   }
 
-  private rewriteData(data: AppBskyFeedDefs.GeneratorView) {
-    this.data = data
+  async unsave() {
+    await this.rootStore.agent.app.bsky.feed.unsaveFeed({
+      feed: this.uri,
+    })
+    runInAction(() => {
+      this.data.viewer = this.data.viewer || {}
+      this.data.viewer.saved = false
+    })
   }
 
-  // public apis
-  // =
   async like() {
     try {
-      const res = await this.rootStore.agent.app.bsky.feed.like.create(
-        {
-          repo: this.rootStore.me.did,
-        },
-        {
-          subject: {
-            uri: this.data.uri,
-            cid: this.data.cid,
-          },
-          createdAt: new Date().toISOString(),
-        },
-      )
-      this.toggleLiked(res.uri)
-      this.incrementLike()
+      const res = await this.rootStore.agent.like(this.data.uri, this.data.cid)
+      runInAction(() => {
+        this.data.viewer = this.data.viewer || {}
+        this.data.viewer.like = res.uri
+        this.data.likeCount = (this.data.likeCount || 0) + 1
+      })
     } catch (e: any) {
       this.rootStore.log.error('Failed to like feed', e)
     }
   }
 
   async unlike() {
+    if (!this.data.viewer.like) {
+      return
+    }
     try {
-      await this.rootStore.agent.app.bsky.feed.like.delete({
-        repo: this.rootStore.me.did,
-        rkey: new AtUri(this.data.viewer?.like!).rkey,
+      await this.rootStore.agent.deleteLike(this.data.viewer.like!)
+      runInAction(() => {
+        this.data.viewer = this.data.viewer || {}
+        this.data.viewer.like = undefined
+        this.data.likeCount = (this.data.likeCount || 1) - 1
       })
-      this.toggleLiked()
-      this.decrementLike()
     } catch (e: any) {
       this.rootStore.log.error('Failed to unlike feed', e)
     }
   }
 
-  static async getView(store: RootStoreModel, uri: string) {
-    const res = await store.agent.app.bsky.feed.getFeedGenerator({
-      feed: uri,
-    })
-    const view = res.data.view
-    return view
-  }
-
-  async checkIsValid() {
-    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
-      feed: this.data.uri,
-    })
-    return res.data.isValid
-  }
-
-  async checkIsOnline() {
-    const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
-      feed: this.data.uri,
-    })
-    return res.data.isOnline
-  }
-
   async reload() {
     const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
       feed: this.data.uri,
     })
-    this.rewriteData(res.data.view)
+    runInAction(() => {
+      this.data = res.data.view
+    })
   }
 
   serialize() {
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index 85e77cae5..dca079b72 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -184,10 +184,7 @@ export class SavedFeedsModel {
 
   async save(algoItem: CustomFeedModel) {
     try {
-      await this.rootStore.agent.app.bsky.feed.saveFeed({
-        feed: algoItem.getUri,
-      })
-      algoItem.toggleSaved = true
+      await algoItem.save()
       this.addFeed(algoItem)
     } catch (e: any) {
       this.rootStore.log.error('Failed to save feed', e)
@@ -195,16 +192,13 @@ export class SavedFeedsModel {
   }
 
   async unsave(algoItem: CustomFeedModel) {
-    const uri = algoItem.getUri
+    const uri = algoItem.uri
     try {
-      await this.rootStore.agent.app.bsky.feed.unsaveFeed({
-        feed: uri,
-      })
-      algoItem.toggleSaved = false
+      await algoItem.unsave()
       this.removeFeed(uri)
       this.removePinnedFeed(uri)
     } catch (e: any) {
-      this.rootStore.log.error('Failed to unsanve feed', e)
+      this.rootStore.log.error('Failed to unsave feed', e)
     }
   }
 
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 10a605b25..1e3690fbd 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -188,7 +188,7 @@ export function UserAvatar({
           accessibilityRole="image"
         />
       ) : (
-        <DefaultAvatar size={size} />
+        <DefaultAvatar type={type} size={size} />
       )}
       <View style={[styles.editButtonContainer, pal.btn]}>
         <FontAwesomeIcon
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index ed2f8feba..01d26db91 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -170,6 +170,7 @@ export function PostEmbeds({
     AppBskyEmbedRecord.isView(embed) &&
     AppBskyFeedDefs.isGeneratorView(embed.record)
   ) {
+    // TODO memoize this?
     return (
       <CustomFeed
         item={new CustomFeedModel(store, embed.record)}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 1457478d5..70816a306 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -118,7 +118,7 @@ export const HomeScreen = withAuthRequired(
               key={String(2 + index + 1)}
               testID="customFeed"
               isPageFocused={selectedPage === 2 + index}
-              feed={new PostsFeedModel(store, 'custom', {feed: f.getUri})}
+              feed={new PostsFeedModel(store, 'custom', {feed: f.uri})}
               renderEmptyState={renderFollowingEmptyState}
             />
           )
-- 
cgit 1.4.1


From 0f5b3808f8107ff4f04cda8c3da18ffcdcf6f27f Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 17 May 2023 15:03:19 -0500
Subject: Remove a focus-effect that was triggering a rerender loop

---
 src/view/com/feeds/CustomFeed.tsx       | 13 +------------
 src/view/com/util/post-embeds/index.tsx | 26 +++++++++++++++++---------
 2 files changed, 18 insertions(+), 21 deletions(-)

(limited to 'src/view/com/util/post-embeds/index.tsx')

diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
index 95726be64..8e1a78453 100644
--- a/src/view/com/feeds/CustomFeed.tsx
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -13,12 +13,11 @@ import {UserAvatar} from '../util/UserAvatar'
 import {Button} from '../util/forms/Button'
 import {observer} from 'mobx-react-lite'
 import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
-import {isWeb} from 'platform/detection'
 
 export const CustomFeed = observer(
   ({
@@ -26,26 +25,16 @@ export const CustomFeed = observer(
     style,
     showSaveBtn = false,
     showLikes = false,
-    reloadOnFocus = false,
   }: {
     item: CustomFeedModel
     style?: StyleProp<ViewStyle>
     showSaveBtn?: boolean
     showLikes?: boolean
-    reloadOnFocus?: boolean
   }) => {
     const store = useStores()
     const pal = usePalette('default')
     const navigation = useNavigation<NavigationProp>()
 
-    // TODO: this is pretty hacky, but it works for now
-    // causes issues on web
-    useFocusEffect(() => {
-      if (reloadOnFocus && !isWeb) {
-        item.reload()
-      }
-    })
-
     return (
       <TouchableOpacity
         accessibilityRole="button"
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 01d26db91..9eb140c2e 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -170,20 +170,28 @@ export function PostEmbeds({
     AppBskyEmbedRecord.isView(embed) &&
     AppBskyFeedDefs.isGeneratorView(embed.record)
   ) {
-    // TODO memoize this?
-    return (
-      <CustomFeed
-        item={new CustomFeedModel(store, embed.record)}
-        style={[pal.view, pal.border, styles.customFeedOuter]}
-        reloadOnFocus
-        showLikes
-      />
-    )
+    return <CustomFeedEmbed record={embed.record} />
   }
 
   return <View />
 }
 
+function CustomFeedEmbed({record}: {record: AppBskyFeedDefs.GeneratorView}) {
+  const pal = usePalette('default')
+  const store = useStores()
+  const item = React.useMemo(
+    () => new CustomFeedModel(store, record),
+    [store, record],
+  )
+  return (
+    <CustomFeed
+      item={item}
+      style={[pal.view, pal.border, styles.customFeedOuter]}
+      showLikes
+    />
+  )
+}
+
 const styles = StyleSheet.create({
   stackContainer: {
     gap: 6,
-- 
cgit 1.4.1

