Factor our feed source model (#1887)
* Refactor first onboarding step * Replace old FeedSourceCard * Clean up CustomFeedEmbed * Remove discover feeds model * Refactor ProfileFeed screen * Remove useCustomFeed * Delete some unused models * Rip out more prefs * Factor out treeView from thread comp * Improve last commit
This commit is contained in:
parent
a01463788d
commit
06eb8b9a4c
21 changed files with 526 additions and 1356 deletions
|
@ -1,231 +0,0 @@
|
|||
import {AtUri, RichText, AppBskyFeedDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {RootStoreModel} from 'state/models/root-store'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export class FeedSourceModel {
|
||||
// state
|
||||
_reactKey: string
|
||||
hasLoaded = false
|
||||
error: string | undefined
|
||||
|
||||
// data
|
||||
uri: string
|
||||
cid: string = ''
|
||||
type: 'feed-generator' | 'list' | 'unsupported' = 'unsupported'
|
||||
avatar: string | undefined = ''
|
||||
displayName: string = ''
|
||||
descriptionRT: RichText | null = null
|
||||
creatorDid: string = ''
|
||||
creatorHandle: string = ''
|
||||
likeCount: number | undefined = 0
|
||||
likeUri: string | undefined = ''
|
||||
|
||||
constructor(public rootStore: RootStoreModel, uri: string) {
|
||||
this._reactKey = uri
|
||||
this.uri = uri
|
||||
|
||||
try {
|
||||
const urip = new AtUri(uri)
|
||||
if (urip.collection === 'app.bsky.feed.generator') {
|
||||
this.type = 'feed-generator'
|
||||
} else if (urip.collection === 'app.bsky.graph.list') {
|
||||
this.type = 'list'
|
||||
}
|
||||
} catch {}
|
||||
this.displayName = uri.split('/').pop() || ''
|
||||
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
get href() {
|
||||
const urip = new AtUri(this.uri)
|
||||
const collection =
|
||||
urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists'
|
||||
return `/profile/${urip.hostname}/${collection}/${urip.rkey}`
|
||||
}
|
||||
|
||||
get isSaved() {
|
||||
return this.rootStore.preferences.savedFeeds.includes(this.uri)
|
||||
}
|
||||
|
||||
get isPinned() {
|
||||
return false
|
||||
}
|
||||
|
||||
get isLiked() {
|
||||
return !!this.likeUri
|
||||
}
|
||||
|
||||
get isOwner() {
|
||||
return this.creatorDid === this.rootStore.me.did
|
||||
}
|
||||
|
||||
setup = bundleAsync(async () => {
|
||||
try {
|
||||
if (this.type === 'feed-generator') {
|
||||
const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
|
||||
feed: this.uri,
|
||||
})
|
||||
this.hydrateFeedGenerator(res.data.view)
|
||||
} else if (this.type === 'list') {
|
||||
const res = await this.rootStore.agent.app.bsky.graph.getList({
|
||||
list: this.uri,
|
||||
limit: 1,
|
||||
})
|
||||
this.hydrateList(res.data.list)
|
||||
}
|
||||
} catch (e) {
|
||||
runInAction(() => {
|
||||
this.error = cleanError(e)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
hydrateFeedGenerator(view: AppBskyFeedDefs.GeneratorView) {
|
||||
this.uri = view.uri
|
||||
this.cid = view.cid
|
||||
this.avatar = view.avatar
|
||||
this.displayName = view.displayName
|
||||
? sanitizeDisplayName(view.displayName)
|
||||
: `Feed by ${sanitizeHandle(view.creator.handle, '@')}`
|
||||
this.descriptionRT = new RichText({
|
||||
text: view.description || '',
|
||||
facets: (view.descriptionFacets || [])?.slice(),
|
||||
})
|
||||
this.creatorDid = view.creator.did
|
||||
this.creatorHandle = view.creator.handle
|
||||
this.likeCount = view.likeCount
|
||||
this.likeUri = view.viewer?.like
|
||||
this.hasLoaded = true
|
||||
}
|
||||
|
||||
hydrateList(view: AppBskyGraphDefs.ListView) {
|
||||
this.uri = view.uri
|
||||
this.cid = view.cid
|
||||
this.avatar = view.avatar
|
||||
this.displayName = view.name
|
||||
? sanitizeDisplayName(view.name)
|
||||
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`
|
||||
this.descriptionRT = new RichText({
|
||||
text: view.description || '',
|
||||
facets: (view.descriptionFacets || [])?.slice(),
|
||||
})
|
||||
this.creatorDid = view.creator.did
|
||||
this.creatorHandle = view.creator.handle
|
||||
this.likeCount = undefined
|
||||
this.hasLoaded = true
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (this.type !== 'feed-generator') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.rootStore.preferences.addSavedFeed(this.uri)
|
||||
} catch (error) {
|
||||
logger.error('Failed to save feed', {error})
|
||||
} finally {
|
||||
track('CustomFeed:Save')
|
||||
}
|
||||
}
|
||||
|
||||
async unsave() {
|
||||
// TODO TEMPORARY — see PRF's comment in content/list.ts togglePin
|
||||
if (this.type !== 'feed-generator' && this.type !== 'list') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.rootStore.preferences.removeSavedFeed(this.uri)
|
||||
} catch (error) {
|
||||
logger.error('Failed to unsave feed', {error})
|
||||
} finally {
|
||||
track('CustomFeed:Unsave')
|
||||
}
|
||||
}
|
||||
|
||||
async pin() {
|
||||
try {
|
||||
await this.rootStore.preferences.addPinnedFeed(this.uri)
|
||||
} catch (error) {
|
||||
logger.error('Failed to pin feed', {error})
|
||||
} finally {
|
||||
track('CustomFeed:Pin', {
|
||||
name: this.displayName,
|
||||
uri: this.uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async togglePin() {
|
||||
if (!this.isPinned) {
|
||||
track('CustomFeed:Pin', {
|
||||
name: this.displayName,
|
||||
uri: this.uri,
|
||||
})
|
||||
return this.rootStore.preferences.addPinnedFeed(this.uri)
|
||||
} else {
|
||||
track('CustomFeed:Unpin', {
|
||||
name: this.displayName,
|
||||
uri: this.uri,
|
||||
})
|
||||
|
||||
if (this.type === 'list') {
|
||||
// TODO TEMPORARY — see PRF's comment in content/list.ts togglePin
|
||||
return this.unsave()
|
||||
} else {
|
||||
return this.rootStore.preferences.removePinnedFeed(this.uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async like() {
|
||||
if (this.type !== 'feed-generator') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.likeUri = 'pending'
|
||||
this.likeCount = (this.likeCount || 0) + 1
|
||||
const res = await this.rootStore.agent.like(this.uri, this.cid)
|
||||
this.likeUri = res.uri
|
||||
} catch (e: any) {
|
||||
this.likeUri = undefined
|
||||
this.likeCount = (this.likeCount || 1) - 1
|
||||
logger.error('Failed to like feed', {error: e})
|
||||
} finally {
|
||||
track('CustomFeed:Like')
|
||||
}
|
||||
}
|
||||
|
||||
async unlike() {
|
||||
if (this.type !== 'feed-generator') {
|
||||
return
|
||||
}
|
||||
if (!this.likeUri) {
|
||||
return
|
||||
}
|
||||
const uri = this.likeUri
|
||||
try {
|
||||
this.likeUri = undefined
|
||||
this.likeCount = (this.likeCount || 1) - 1
|
||||
await this.rootStore.agent.deleteLike(uri!)
|
||||
} catch (e: any) {
|
||||
this.likeUri = uri
|
||||
this.likeCount = (this.likeCount || 0) + 1
|
||||
logger.error('Failed to unlike feed', {error: e})
|
||||
} finally {
|
||||
track('CustomFeed:Unlike')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {AppBskyUnspeccedGetPopularFeedGenerators} from '@atproto/api'
|
||||
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'
|
||||
|
||||
const DEFAULT_LIMIT = 50
|
||||
|
||||
export class FeedsDiscoveryModel {
|
||||
// state
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
error = ''
|
||||
loadMoreCursor: string | undefined = undefined
|
||||
|
||||
// data
|
||||
feeds: FeedSourceModel[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
get hasMore() {
|
||||
if (this.loadMoreCursor) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.feeds.length > 0
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
return this.error !== ''
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
refresh = bundleAsync(async () => {
|
||||
this._xLoading()
|
||||
try {
|
||||
const res =
|
||||
await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: DEFAULT_LIMIT,
|
||||
})
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
})
|
||||
|
||||
loadMore = bundleAsync(async () => {
|
||||
if (!this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
try {
|
||||
const res =
|
||||
await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: DEFAULT_LIMIT,
|
||||
cursor: this.loadMoreCursor,
|
||||
})
|
||||
this._append(res)
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
this._xIdle()
|
||||
})
|
||||
|
||||
search = async (query: string) => {
|
||||
this._xLoading(false)
|
||||
try {
|
||||
const results =
|
||||
await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({
|
||||
limit: DEFAULT_LIMIT,
|
||||
query: query,
|
||||
})
|
||||
this._replaceAll(results)
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
this._xIdle()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = false
|
||||
this.error = ''
|
||||
this.feeds = []
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
_xLoading(isRefreshing = true) {
|
||||
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 popular feeds', {error: err})
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
_replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) {
|
||||
// 1. set feeds data to empty array
|
||||
this.feeds = []
|
||||
// 2. call this._append()
|
||||
this._append(res)
|
||||
}
|
||||
|
||||
_append(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) {
|
||||
// 1. push data into feeds array
|
||||
for (const f of res.data.feeds) {
|
||||
const model = new FeedSourceModel(this.rootStore, f.uri)
|
||||
model.hydrateFeedGenerator(f)
|
||||
this.feeds.push(model)
|
||||
}
|
||||
// 2. set loadMoreCursor
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
}
|
||||
}
|
|
@ -1,123 +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 {FeedSourceModel} from '../content/feed-source'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export class ActorFeedsModel {
|
||||
// state
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
error = ''
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
|
||||
// data
|
||||
feeds: FeedSourceModel[] = []
|
||||
|
||||
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) {
|
||||
logger.error('Failed to fetch user followers', {error: 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) {
|
||||
const model = new FeedSourceModel(this.rootStore, f.uri)
|
||||
model.hydrateFeedGenerator(f)
|
||||
this.feeds.push(model)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -126,33 +126,6 @@ export class PreferencesModel {
|
|||
],
|
||||
}
|
||||
}
|
||||
|
||||
// feeds
|
||||
// =
|
||||
|
||||
isPinnedFeed(uri: string) {
|
||||
return this.pinnedFeeds.includes(uri)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `useAddSavedFeedMutation` from `#/state/queries/preferences` instead
|
||||
*/
|
||||
async addSavedFeed(_v: string) {}
|
||||
|
||||
/**
|
||||
* @deprecated use `useRemoveSavedFeedMutation` from `#/state/queries/preferences` instead
|
||||
*/
|
||||
async removeSavedFeed(_v: string) {}
|
||||
|
||||
/**
|
||||
* @deprecated use `usePinFeedMutation` from `#/state/queries/preferences` instead
|
||||
*/
|
||||
async addPinnedFeed(_v: string) {}
|
||||
|
||||
/**
|
||||
* @deprecated use `useUnpinFeedMutation` from `#/state/queries/preferences` instead
|
||||
*/
|
||||
async removePinnedFeed(_v: string) {}
|
||||
}
|
||||
|
||||
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {ProfileModel} from '../content/profile'
|
||||
import {ActorFeedsModel} from '../lists/actor-feeds'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export enum Sections {
|
||||
PostsNoReplies = 'Posts',
|
||||
PostsWithReplies = 'Posts & replies',
|
||||
PostsWithMedia = 'Media',
|
||||
Likes = 'Likes',
|
||||
CustomAlgorithms = 'Feeds',
|
||||
Lists = 'Lists',
|
||||
}
|
||||
|
||||
export interface ProfileUiParams {
|
||||
user: string
|
||||
}
|
||||
|
||||
export class ProfileUiModel {
|
||||
static LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
static END_ITEM = {_reactKey: '__end__'}
|
||||
static EMPTY_ITEM = {_reactKey: '__empty__'}
|
||||
|
||||
isAuthenticatedUser = false
|
||||
|
||||
// data
|
||||
profile: ProfileModel
|
||||
feed: PostsFeedModel
|
||||
algos: ActorFeedsModel
|
||||
lists: ListsListModel
|
||||
|
||||
// ui state
|
||||
selectedViewIndex = 0
|
||||
|
||||
constructor(
|
||||
public rootStore: RootStoreModel,
|
||||
public params: ProfileUiParams,
|
||||
) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
params: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
this.profile = new ProfileModel(rootStore, {actor: params.user})
|
||||
this.feed = new PostsFeedModel(rootStore, 'author', {
|
||||
actor: params.user,
|
||||
limit: 10,
|
||||
filter: 'posts_no_replies',
|
||||
})
|
||||
this.algos = new ActorFeedsModel(rootStore, {actor: params.user})
|
||||
this.lists = new ListsListModel(rootStore, params.user)
|
||||
}
|
||||
|
||||
get currentView(): PostsFeedModel | ActorFeedsModel | ListsListModel {
|
||||
if (
|
||||
this.selectedView === Sections.PostsNoReplies ||
|
||||
this.selectedView === Sections.PostsWithReplies ||
|
||||
this.selectedView === Sections.PostsWithMedia ||
|
||||
this.selectedView === Sections.Likes
|
||||
) {
|
||||
return this.feed
|
||||
} else if (this.selectedView === Sections.Lists) {
|
||||
return this.lists
|
||||
}
|
||||
if (this.selectedView === Sections.CustomAlgorithms) {
|
||||
return this.algos
|
||||
}
|
||||
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
|
||||
}
|
||||
|
||||
get isInitialLoading() {
|
||||
const view = this.currentView
|
||||
return view.isLoading && !view.isRefreshing && !view.hasContent
|
||||
}
|
||||
|
||||
get isRefreshing() {
|
||||
return this.profile.isRefreshing || this.currentView.isRefreshing
|
||||
}
|
||||
|
||||
get selectorItems() {
|
||||
const items = [
|
||||
Sections.PostsNoReplies,
|
||||
Sections.PostsWithReplies,
|
||||
Sections.PostsWithMedia,
|
||||
this.isAuthenticatedUser && Sections.Likes,
|
||||
].filter(Boolean) as string[]
|
||||
if (this.algos.hasLoaded && !this.algos.isEmpty) {
|
||||
items.push(Sections.CustomAlgorithms)
|
||||
}
|
||||
if (this.lists.hasLoaded && !this.lists.isEmpty) {
|
||||
items.push(Sections.Lists)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
get selectedView() {
|
||||
// If, for whatever reason, the selected view index is not available, default back to posts
|
||||
// This can happen when the user was focused on a view but performed an action that caused
|
||||
// the view to disappear (e.g. deleting the last list in their list of lists https://imgflip.com/i/7txu1y)
|
||||
return this.selectorItems[this.selectedViewIndex] || Sections.PostsNoReplies
|
||||
}
|
||||
|
||||
get uiItems() {
|
||||
let arr: any[] = []
|
||||
// if loading, return loading item to show loading spinner
|
||||
if (this.isInitialLoading) {
|
||||
arr = arr.concat([ProfileUiModel.LOADING_ITEM])
|
||||
} else if (this.currentView.hasError) {
|
||||
// if error, return error item to show error message
|
||||
arr = arr.concat([
|
||||
{
|
||||
_reactKey: '__error__',
|
||||
error: this.currentView.error,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
if (
|
||||
this.selectedView === Sections.PostsNoReplies ||
|
||||
this.selectedView === Sections.PostsWithReplies ||
|
||||
this.selectedView === Sections.PostsWithMedia ||
|
||||
this.selectedView === Sections.Likes
|
||||
) {
|
||||
if (this.feed.hasContent) {
|
||||
arr = this.feed.slices.slice()
|
||||
if (!this.feed.hasMore) {
|
||||
arr = arr.concat([ProfileUiModel.END_ITEM])
|
||||
}
|
||||
} else if (this.feed.isEmpty) {
|
||||
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
|
||||
}
|
||||
} else if (this.selectedView === Sections.CustomAlgorithms) {
|
||||
if (this.algos.hasContent) {
|
||||
arr = this.algos.feeds
|
||||
} else if (this.algos.isEmpty) {
|
||||
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
|
||||
}
|
||||
} else if (this.selectedView === Sections.Lists) {
|
||||
if (this.lists.hasContent) {
|
||||
arr = this.lists.lists
|
||||
} else if (this.lists.isEmpty) {
|
||||
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
|
||||
}
|
||||
} else {
|
||||
// fallback, add empty item, to show empty message
|
||||
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
get showLoadingMoreFooter() {
|
||||
if (
|
||||
this.selectedView === Sections.PostsNoReplies ||
|
||||
this.selectedView === Sections.PostsWithReplies ||
|
||||
this.selectedView === Sections.PostsWithMedia ||
|
||||
this.selectedView === Sections.Likes
|
||||
) {
|
||||
return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading
|
||||
} else if (this.selectedView === Sections.Lists) {
|
||||
return this.lists.hasContent && this.lists.hasMore && this.lists.isLoading
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
setSelectedViewIndex(index: number) {
|
||||
// ViewSelector fires onSelectView on mount
|
||||
if (index === this.selectedViewIndex) return
|
||||
|
||||
this.selectedViewIndex = index
|
||||
|
||||
if (
|
||||
this.selectedView === Sections.PostsNoReplies ||
|
||||
this.selectedView === Sections.PostsWithReplies ||
|
||||
this.selectedView === Sections.PostsWithMedia
|
||||
) {
|
||||
let filter = 'posts_no_replies'
|
||||
if (this.selectedView === Sections.PostsWithReplies) {
|
||||
filter = 'posts_with_replies'
|
||||
} else if (this.selectedView === Sections.PostsWithMedia) {
|
||||
filter = 'posts_with_media'
|
||||
}
|
||||
|
||||
this.feed = new PostsFeedModel(
|
||||
this.rootStore,
|
||||
'author',
|
||||
{
|
||||
actor: this.params.user,
|
||||
limit: 10,
|
||||
filter,
|
||||
},
|
||||
{
|
||||
isSimpleFeed: ['posts_with_media'].includes(filter),
|
||||
},
|
||||
)
|
||||
|
||||
this.feed.setup()
|
||||
} else if (this.selectedView === Sections.Likes) {
|
||||
this.feed = new PostsFeedModel(
|
||||
this.rootStore,
|
||||
'likes',
|
||||
{
|
||||
actor: this.params.user,
|
||||
limit: 10,
|
||||
},
|
||||
{
|
||||
isSimpleFeed: true,
|
||||
},
|
||||
)
|
||||
|
||||
this.feed.setup()
|
||||
}
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await Promise.all([
|
||||
this.profile
|
||||
.setup()
|
||||
.catch(err => logger.error('Failed to fetch profile', {error: err})),
|
||||
this.feed
|
||||
.setup()
|
||||
.catch(err => logger.error('Failed to fetch feed', {error: err})),
|
||||
])
|
||||
runInAction(() => {
|
||||
this.isAuthenticatedUser =
|
||||
this.profile.did === this.rootStore.session.currentSession?.did
|
||||
})
|
||||
this.algos.refresh()
|
||||
// HACK: need to use the DID as a param, not the username -prf
|
||||
this.lists.source = this.profile.did
|
||||
this.lists
|
||||
.loadMore()
|
||||
.catch(err => logger.error('Failed to fetch lists', {error: err}))
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await Promise.all([this.profile.refresh(), this.currentView.refresh()])
|
||||
}
|
||||
|
||||
async loadMore() {
|
||||
if (
|
||||
!this.currentView.isLoading &&
|
||||
!this.currentView.hasError &&
|
||||
!this.currentView.isEmpty
|
||||
) {
|
||||
await this.currentView.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,39 +21,41 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
|||
import {useSession} from '#/state/session'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
|
||||
export type FeedSourceInfo =
|
||||
| {
|
||||
type: 'feed'
|
||||
uri: string
|
||||
route: {
|
||||
href: string
|
||||
name: string
|
||||
params: Record<string, string>
|
||||
}
|
||||
cid: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
likeCount: number | undefined
|
||||
likeUri: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'list'
|
||||
uri: string
|
||||
route: {
|
||||
href: string
|
||||
name: string
|
||||
params: Record<string, string>
|
||||
}
|
||||
cid: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
}
|
||||
export type FeedSourceFeedInfo = {
|
||||
type: 'feed'
|
||||
uri: string
|
||||
route: {
|
||||
href: string
|
||||
name: string
|
||||
params: Record<string, string>
|
||||
}
|
||||
cid: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
likeCount: number | undefined
|
||||
likeUri: string | undefined
|
||||
}
|
||||
|
||||
export type FeedSourceListInfo = {
|
||||
type: 'list'
|
||||
uri: string
|
||||
route: {
|
||||
href: string
|
||||
name: string
|
||||
params: Record<string, string>
|
||||
}
|
||||
cid: string
|
||||
avatar: string | undefined
|
||||
displayName: string
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
}
|
||||
|
||||
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
|
||||
|
||||
export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
|
||||
'getFeedSourceInfo',
|
||||
|
|
24
src/state/queries/like.ts
Normal file
24
src/state/queries/like.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
export function useLikeMutation() {
|
||||
const {agent} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
|
||||
const res = await agent.like(uri, cid)
|
||||
return {uri: res.uri}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUnlikeMutation() {
|
||||
const {agent} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({uri}: {uri: string}) => {
|
||||
await agent.deleteLike(uri)
|
||||
},
|
||||
})
|
||||
}
|
29
src/state/queries/suggested-feeds.ts
Normal file
29
src/state/queries/suggested-feeds.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
|
||||
import {AppBskyFeedGetSuggestedFeeds} from '@atproto/api'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
export const suggestedFeedsQueryKey = ['suggestedFeeds']
|
||||
|
||||
export function useSuggestedFeedsQuery() {
|
||||
const {agent} = useSession()
|
||||
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetSuggestedFeeds.OutputSchema,
|
||||
Error,
|
||||
InfiniteData<AppBskyFeedGetSuggestedFeeds.OutputSchema>,
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
queryKey: suggestedFeedsQueryKey,
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.feed.getSuggestedFeeds({
|
||||
limit: 10,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue