Refactor My Feeds (#1877)
* Refactor My Feeds screen * Remove unused feed UI models * Add back PTR
This commit is contained in:
parent
d9e0a927c1
commit
c584a3378d
5 changed files with 532 additions and 542 deletions
|
@ -5,7 +5,6 @@ import {
|
|||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {NotificationsFeedModel} from './feeds/notifications'
|
||||
import {MyFeedsUIModel} from './ui/my-feeds'
|
||||
import {MyFollowsCache} from './cache/my-follows'
|
||||
import {isObj, hasProp} from 'lib/type-guards'
|
||||
import {logger} from '#/logger'
|
||||
|
@ -22,7 +21,6 @@ export class MeModel {
|
|||
followsCount: number | undefined
|
||||
followersCount: number | undefined
|
||||
notifications: NotificationsFeedModel
|
||||
myFeeds: MyFeedsUIModel
|
||||
follows: MyFollowsCache
|
||||
invites: ComAtprotoServerDefs.InviteCode[] = []
|
||||
appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = []
|
||||
|
@ -40,13 +38,11 @@ export class MeModel {
|
|||
{autoBind: true},
|
||||
)
|
||||
this.notifications = new NotificationsFeedModel(this.rootStore)
|
||||
this.myFeeds = new MyFeedsUIModel(this.rootStore)
|
||||
this.follows = new MyFollowsCache(this.rootStore)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.notifications.clear()
|
||||
this.myFeeds.clear()
|
||||
this.follows.clear()
|
||||
this.rootStore.profiles.cache.clear()
|
||||
this.rootStore.posts.cache.clear()
|
||||
|
@ -113,8 +109,6 @@ export class MeModel {
|
|||
error: e,
|
||||
})
|
||||
})
|
||||
this.myFeeds.clear()
|
||||
/* dont await */ this.myFeeds.saved.refresh()
|
||||
this.rootStore.emitSessionLoaded()
|
||||
await this.fetchInviteCodes()
|
||||
await this.fetchAppPasswords()
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
import {makeAutoObservable, reaction} from 'mobx'
|
||||
import {SavedFeedsModel} from './saved-feeds'
|
||||
import {FeedsDiscoveryModel} from '../discovery/feeds'
|
||||
import {FeedSourceModel} from '../content/feed-source'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
|
||||
export type MyFeedsItem =
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'spinner'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'saved-feeds-loading'
|
||||
numItems: number
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'discover-feeds-loading'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'error'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'saved-feeds-header'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'saved-feed'
|
||||
feed: FeedSourceModel
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'saved-feeds-load-more'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'discover-feeds-header'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'discover-feeds-no-results'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'discover-feed'
|
||||
feed: FeedSourceModel
|
||||
}
|
||||
|
||||
export class MyFeedsUIModel {
|
||||
saved: SavedFeedsModel
|
||||
discovery: FeedsDiscoveryModel
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this)
|
||||
this.saved = new SavedFeedsModel(this.rootStore)
|
||||
this.discovery = new FeedsDiscoveryModel(this.rootStore)
|
||||
}
|
||||
|
||||
get isRefreshing() {
|
||||
return !this.saved.isLoading && this.saved.isRefreshing
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.saved.isLoading || this.discovery.isLoading
|
||||
}
|
||||
|
||||
async setup() {
|
||||
if (!this.saved.hasLoaded) {
|
||||
await this.saved.refresh()
|
||||
}
|
||||
if (!this.discovery.hasLoaded) {
|
||||
await this.discovery.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.saved.clear()
|
||||
this.discovery.clear()
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
const dispose1 = reaction(
|
||||
() => this.rootStore.preferences.savedFeeds,
|
||||
() => this.saved.refresh(),
|
||||
)
|
||||
const dispose2 = reaction(
|
||||
() => this.rootStore.preferences.pinnedFeeds,
|
||||
() => this.saved.refresh(),
|
||||
)
|
||||
return () => {
|
||||
dispose1()
|
||||
dispose2()
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
return Promise.all([this.saved.refresh(), this.discovery.refresh()])
|
||||
}
|
||||
|
||||
async loadMore() {
|
||||
return this.discovery.loadMore()
|
||||
}
|
||||
|
||||
get items() {
|
||||
let items: MyFeedsItem[] = []
|
||||
|
||||
items.push({
|
||||
_reactKey: '__saved_feeds_header__',
|
||||
type: 'saved-feeds-header',
|
||||
})
|
||||
if (this.saved.isLoading && !this.saved.hasContent) {
|
||||
items.push({
|
||||
_reactKey: '__saved_feeds_loading__',
|
||||
type: 'saved-feeds-loading',
|
||||
numItems: this.rootStore.preferences.savedFeeds.length || 3,
|
||||
})
|
||||
} else if (this.saved.hasError) {
|
||||
items.push({
|
||||
_reactKey: '__saved_feeds_error__',
|
||||
type: 'error',
|
||||
error: this.saved.error,
|
||||
})
|
||||
} else {
|
||||
const savedSorted = this.saved.all
|
||||
.slice()
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
||||
items = items.concat(
|
||||
savedSorted.map(feed => ({
|
||||
_reactKey: `saved-${feed.uri}`,
|
||||
type: 'saved-feed',
|
||||
feed,
|
||||
})),
|
||||
)
|
||||
items.push({
|
||||
_reactKey: '__saved_feeds_load_more__',
|
||||
type: 'saved-feeds-load-more',
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
_reactKey: '__discover_feeds_header__',
|
||||
type: 'discover-feeds-header',
|
||||
})
|
||||
if (this.discovery.isLoading && !this.discovery.hasContent) {
|
||||
items.push({
|
||||
_reactKey: '__discover_feeds_loading__',
|
||||
type: 'discover-feeds-loading',
|
||||
})
|
||||
} else if (this.discovery.hasError) {
|
||||
items.push({
|
||||
_reactKey: '__discover_feeds_error__',
|
||||
type: 'error',
|
||||
error: this.discovery.error,
|
||||
})
|
||||
} else if (this.discovery.isEmpty) {
|
||||
items.push({
|
||||
_reactKey: '__discover_feeds_no_results__',
|
||||
type: 'discover-feeds-no-results',
|
||||
})
|
||||
} else {
|
||||
items = items.concat(
|
||||
this.discovery.feeds.map(feed => ({
|
||||
_reactKey: `discover-${feed.uri}`,
|
||||
type: 'discover-feed',
|
||||
feed,
|
||||
})),
|
||||
)
|
||||
if (this.discovery.isLoading) {
|
||||
items.push({
|
||||
_reactKey: '__discover_feeds_loading_more__',
|
||||
type: 'spinner',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {FeedSourceModel} from '../content/feed-source'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export class SavedFeedsModel {
|
||||
// state
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
error = ''
|
||||
|
||||
// data
|
||||
all: FeedSourceModel[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.all.length > 0
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
return this.error !== ''
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
get pinned(): FeedSourceModel[] {
|
||||
return this.rootStore.preferences.savedFeeds
|
||||
.filter(feed => this.rootStore.preferences.isPinnedFeed(feed))
|
||||
.map(uri => this.all.find(f => f.uri === uri))
|
||||
.filter(Boolean) as FeedSourceModel[]
|
||||
}
|
||||
|
||||
get unpinned(): FeedSourceModel[] {
|
||||
return this.rootStore.preferences.savedFeeds
|
||||
.filter(feed => !this.rootStore.preferences.isPinnedFeed(feed))
|
||||
.map(uri => this.all.find(f => f.uri === uri))
|
||||
.filter(Boolean) as FeedSourceModel[]
|
||||
}
|
||||
|
||||
get pinnedFeedNames() {
|
||||
return this.pinned.map(f => f.displayName)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
clear() {
|
||||
this.all = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the preferences then reload all feed infos
|
||||
*/
|
||||
refresh = bundleAsync(async () => {
|
||||
this._xLoading(true)
|
||||
try {
|
||||
const uris = dedup(
|
||||
this.rootStore.preferences.pinnedFeeds.concat(
|
||||
this.rootStore.preferences.savedFeeds,
|
||||
),
|
||||
)
|
||||
const feeds = uris.map(uri => new FeedSourceModel(this.rootStore, uri))
|
||||
await Promise.all(feeds.map(f => f.setup()))
|
||||
runInAction(() => {
|
||||
this.all = feeds
|
||||
this._updatePinSortOrder()
|
||||
})
|
||||
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) {
|
||||
logger.error('Failed to fetch user feeds', {err})
|
||||
}
|
||||
}
|
||||
|
||||
// helpers
|
||||
// =
|
||||
|
||||
_updatePinSortOrder(order?: string[]) {
|
||||
order ??= this.rootStore.preferences.pinnedFeeds.concat(
|
||||
this.rootStore.preferences.savedFeeds,
|
||||
)
|
||||
this.all.sort((a, b) => {
|
||||
return order!.indexOf(a.uri) - order!.indexOf(b.uri)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function dedup(strings: string[]): string[] {
|
||||
return Array.from(new Set(strings))
|
||||
}
|
|
@ -1,5 +1,17 @@
|
|||
import {useQuery} from '@tanstack/react-query'
|
||||
import {AtUri, RichText, AppBskyFeedDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||
import {
|
||||
useQuery,
|
||||
useInfiniteQuery,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
useMutation,
|
||||
} from '@tanstack/react-query'
|
||||
import {
|
||||
AtUri,
|
||||
RichText,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyUnspeccedGetPopularFeedGenerators,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
|
@ -10,6 +22,7 @@ type FeedSourceInfo =
|
|||
type: 'feed'
|
||||
uri: string
|
||||
cid: string
|
||||
href: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
|
@ -22,6 +35,7 @@ type FeedSourceInfo =
|
|||
type: 'list'
|
||||
uri: string
|
||||
cid: string
|
||||
href: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
|
@ -42,10 +56,16 @@ const feedSourceNSIDs = {
|
|||
function hydrateFeedGenerator(
|
||||
view: AppBskyFeedDefs.GeneratorView,
|
||||
): FeedSourceInfo {
|
||||
const urip = new AtUri(view.uri)
|
||||
const collection =
|
||||
urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists'
|
||||
const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}`
|
||||
|
||||
return {
|
||||
type: 'feed',
|
||||
uri: view.uri,
|
||||
cid: view.cid,
|
||||
href,
|
||||
avatar: view.avatar,
|
||||
displayName: view.displayName
|
||||
? sanitizeDisplayName(view.displayName)
|
||||
|
@ -62,10 +82,16 @@ function hydrateFeedGenerator(
|
|||
}
|
||||
|
||||
function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
|
||||
const urip = new AtUri(view.uri)
|
||||
const collection =
|
||||
urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists'
|
||||
const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}`
|
||||
|
||||
return {
|
||||
type: 'list',
|
||||
uri: view.uri,
|
||||
cid: view.cid,
|
||||
href,
|
||||
avatar: view.avatar,
|
||||
description: new RichText({
|
||||
text: view.description || '',
|
||||
|
@ -104,3 +130,43 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
|
||||
|
||||
export function useGetPopularFeedsQuery() {
|
||||
const {agent} = useSession()
|
||||
|
||||
return useInfiniteQuery<
|
||||
AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema,
|
||||
Error,
|
||||
InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>,
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
queryKey: useGetPopularFeedsQueryKey,
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: 10,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSearchPopularFeedsMutation() {
|
||||
const {agent} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (query: string) => {
|
||||
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: 10,
|
||||
query: query,
|
||||
})
|
||||
|
||||
return res.data.feeds
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue