Feed UI update working branch [WIP] (#1420)
* Feeds navigation on right side of desktop (#1403) * Remove home feed header on desktop * Add feeds to right sidebar * Add simple non-moving header to desktop * Improve loading state of custom feed header * Remove log Co-authored-by: Eric Bailey <git@esb.lol> * Remove dead comment --------- Co-authored-by: Eric Bailey <git@esb.lol> * Redesign feeds tab (#1439) * consolidate saved feeds and discover into one screen * Add hoverStyle behavior to <Link> * More UI work on SavedFeeds * Replace satellite icon with a hashtag * Tune My Feeds mobile ui * Handle no results in my feeds * Remove old DiscoverFeeds screen * Remove multifeed * Remove DiscoverFeeds from router * Improve loading placeholders * Small fixes * Fix types * Fix overflow issue on firefox * Add icons prompting to open feeds --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com> * Merge feed prototype [WIP] (#1398) * POC WIP for the mergefeed * Add feed API wrapper and move mergefeed into it * Show feed source in mergefeed * Add lodash.random dep * Improve mergefeed sampling and reliability * Tune source ui element * Improve mergefeed edge condition handling * Remove in-place update of feeds for performance * Fix link on native * Fix bad ref * Improve variety in mergefeed sampling * Fix types * Fix rebase error * Add missing source field (got dropped in merge) * Update find more link * Simplify the right hand feeds nav * Bring back load latest button on desktop & unify impl * Add 'From' to source * Add simple headers to desktop home & notifications * Fix thread view jumping around horizontally * Add unread indicators to desktop headers * Add home feed preference for enabling the mergefeed * Add a preference for showing replies among followed users only (#1448) * Add a preference for showing replies among followed users only * Simplify the reply filter UI * Fix typo * Simplified custom feed header * Add soft reset to custom feed screen * Drop all the in-post translate links except when expanded (#1455) * Update mobile feed settings links to match desktop * Fixes to feeds screen loading states * Bolder active state of feeds tab on mobile web * Fix dark mode issue --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Ansh <anshnanda10@gmail.com>
This commit is contained in:
parent
3118e3e933
commit
ea885339cf
57 changed files with 1884 additions and 1497 deletions
|
@ -1,227 +0,0 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {CustomFeedModel} from './custom-feed'
|
||||
import {PostsFeedModel} from './posts'
|
||||
import {PostsFeedSliceModel} from './posts-slice'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
|
||||
const FEED_PAGE_SIZE = 10
|
||||
const FEEDS_PAGE_SIZE = 3
|
||||
|
||||
export type MultiFeedItem =
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'header'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'feed-header'
|
||||
avatar: string | undefined
|
||||
title: string
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'feed-slice'
|
||||
slice: PostsFeedSliceModel
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'feed-loading'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'feed-error'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'feed-footer'
|
||||
title: string
|
||||
uri: string
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'footer'
|
||||
}
|
||||
|
||||
export class PostsMultiFeedModel {
|
||||
// state
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
hasMore = true
|
||||
|
||||
// data
|
||||
feedInfos: CustomFeedModel[] = []
|
||||
feeds: PostsFeedModel[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {rootStore: false}, {autoBind: true})
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.feeds.length !== 0
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
get items() {
|
||||
const items: MultiFeedItem[] = [{_reactKey: '__header__', type: 'header'}]
|
||||
for (let i = 0; i < this.feedInfos.length; i++) {
|
||||
if (!this.feeds[i]) {
|
||||
break
|
||||
}
|
||||
const feed = this.feeds[i]
|
||||
const feedInfo = this.feedInfos[i]
|
||||
const urip = new AtUri(feedInfo.uri)
|
||||
items.push({
|
||||
_reactKey: `__feed_header_${i}__`,
|
||||
type: 'feed-header',
|
||||
avatar: feedInfo.data.avatar,
|
||||
title: feedInfo.displayName,
|
||||
})
|
||||
if (feed.isLoading) {
|
||||
items.push({
|
||||
_reactKey: `__feed_loading_${i}__`,
|
||||
type: 'feed-loading',
|
||||
})
|
||||
} else if (feed.hasError) {
|
||||
items.push({
|
||||
_reactKey: `__feed_error_${i}__`,
|
||||
type: 'feed-error',
|
||||
error: feed.error,
|
||||
})
|
||||
} else {
|
||||
for (let j = 0; j < feed.slices.length; j++) {
|
||||
items.push({
|
||||
_reactKey: `__feed_slice_${i}_${j}__`,
|
||||
type: 'feed-slice',
|
||||
slice: feed.slices[j],
|
||||
})
|
||||
}
|
||||
}
|
||||
items.push({
|
||||
_reactKey: `__feed_footer_${i}__`,
|
||||
type: 'feed-footer',
|
||||
title: feedInfo.displayName,
|
||||
uri: makeProfileLink(feedInfo.data.creator, 'feed', urip.rkey),
|
||||
})
|
||||
}
|
||||
if (!this.hasMore && this.hasContent) {
|
||||
// only show if hasContent to avoid double discover-feed links
|
||||
items.push({_reactKey: '__footer__', type: 'footer'})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
/**
|
||||
* Nuke all data
|
||||
*/
|
||||
clear() {
|
||||
this.rootStore.log.debug('MultiFeedModel:clear')
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = false
|
||||
this.hasMore = true
|
||||
this.feeds = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any event listeners. Returns a cleanup function.
|
||||
*/
|
||||
registerListeners() {
|
||||
const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this))
|
||||
return () => sub.remove()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and load
|
||||
*/
|
||||
async refresh() {
|
||||
this.feedInfos = this.rootStore.me.savedFeeds.all.slice() // capture current feeds
|
||||
await this.loadMore(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load latest in the active feeds
|
||||
*/
|
||||
loadLatest() {
|
||||
for (const feed of this.feeds) {
|
||||
/* dont await */ feed.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more posts to the end of the feed
|
||||
*/
|
||||
loadMore = bundleAsync(async (isRefreshing: boolean = false) => {
|
||||
if (!isRefreshing && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
if (isRefreshing) {
|
||||
this.isRefreshing = true // set optimistically for UI
|
||||
this.feeds = []
|
||||
}
|
||||
this._xLoading(isRefreshing)
|
||||
const start = this.feeds.length
|
||||
const newFeeds: PostsFeedModel[] = []
|
||||
for (
|
||||
let i = start;
|
||||
i < start + FEEDS_PAGE_SIZE && i < this.feedInfos.length;
|
||||
i++
|
||||
) {
|
||||
const feed = new PostsFeedModel(this.rootStore, 'custom', {
|
||||
feed: this.feedInfos[i].uri,
|
||||
})
|
||||
feed.pageSize = FEED_PAGE_SIZE
|
||||
await feed.setup()
|
||||
newFeeds.push(feed)
|
||||
}
|
||||
runInAction(() => {
|
||||
this.feeds = this.feeds.concat(newFeeds)
|
||||
this.hasMore = this.feeds.length < this.feedInfos.length
|
||||
})
|
||||
this._xIdle()
|
||||
})
|
||||
|
||||
/**
|
||||
* Attempt to load more again after a failure
|
||||
*/
|
||||
async retryLoadMore() {
|
||||
this.hasMore = true
|
||||
return this.loadMore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes posts from the feed upon deletion.
|
||||
*/
|
||||
onPostDeleted(uri: string) {
|
||||
for (const f of this.feeds) {
|
||||
f.onPostDeleted(uri)
|
||||
}
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
_xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
}
|
||||
|
||||
_xIdle() {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
}
|
|
@ -2,6 +2,7 @@ import {makeAutoObservable} from 'mobx'
|
|||
import {RootStoreModel} from '../root-store'
|
||||
import {FeedViewPostsSlice} from 'lib/api/feed-manip'
|
||||
import {PostsFeedItemModel} from './post'
|
||||
import {FeedSourceInfo} from 'lib/api/feed/types'
|
||||
|
||||
export class PostsFeedSliceModel {
|
||||
// ui state
|
||||
|
@ -9,9 +10,11 @@ export class PostsFeedSliceModel {
|
|||
|
||||
// data
|
||||
items: PostsFeedItemModel[] = []
|
||||
source: FeedSourceInfo | undefined
|
||||
|
||||
constructor(public rootStore: RootStoreModel, slice: FeedViewPostsSlice) {
|
||||
this._reactKey = slice._reactKey
|
||||
this.source = slice.source
|
||||
for (let i = 0; i < slice.items.length; i++) {
|
||||
this.items.push(
|
||||
new PostsFeedItemModel(
|
||||
|
|
|
@ -14,6 +14,13 @@ import {PostsFeedSliceModel} from './posts-slice'
|
|||
import {track} from 'lib/analytics/analytics'
|
||||
import {FeedViewPostsSlice} from 'lib/api/feed-manip'
|
||||
|
||||
import {FeedAPI, FeedAPIResponse} from 'lib/api/feed/types'
|
||||
import {FollowingFeedAPI} from 'lib/api/feed/following'
|
||||
import {AuthorFeedAPI} from 'lib/api/feed/author'
|
||||
import {LikesFeedAPI} from 'lib/api/feed/likes'
|
||||
import {CustomFeedAPI} from 'lib/api/feed/custom'
|
||||
import {MergeFeedAPI} from 'lib/api/feed/merge'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
type Options = {
|
||||
|
@ -27,6 +34,7 @@ type Options = {
|
|||
type QueryParams =
|
||||
| GetTimeline.QueryParams
|
||||
| GetAuthorFeed.QueryParams
|
||||
| GetActorLikes.QueryParams
|
||||
| GetCustomFeed.QueryParams
|
||||
|
||||
export class PostsFeedModel {
|
||||
|
@ -41,8 +49,8 @@ export class PostsFeedModel {
|
|||
loadMoreError = ''
|
||||
params: QueryParams
|
||||
hasMore = true
|
||||
loadMoreCursor: string | undefined
|
||||
pollCursor: string | undefined
|
||||
api: FeedAPI
|
||||
tuner = new FeedTuner()
|
||||
pageSize = PAGE_SIZE
|
||||
options: Options = {}
|
||||
|
@ -50,7 +58,7 @@ export class PostsFeedModel {
|
|||
// used to linearize async modifications to state
|
||||
lock = new AwaitLock()
|
||||
|
||||
// used to track if what's hot is coming up empty
|
||||
// used to track if a feed is coming up empty
|
||||
emptyFetches = 0
|
||||
|
||||
// data
|
||||
|
@ -58,7 +66,7 @@ export class PostsFeedModel {
|
|||
|
||||
constructor(
|
||||
public rootStore: RootStoreModel,
|
||||
public feedType: 'home' | 'author' | 'custom' | 'likes',
|
||||
public feedType: 'home' | 'following' | 'author' | 'custom' | 'likes',
|
||||
params: QueryParams,
|
||||
options?: Options,
|
||||
) {
|
||||
|
@ -67,12 +75,33 @@ export class PostsFeedModel {
|
|||
{
|
||||
rootStore: false,
|
||||
params: false,
|
||||
loadMoreCursor: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
this.params = params
|
||||
this.options = options || {}
|
||||
if (feedType === 'home') {
|
||||
this.api = new MergeFeedAPI(rootStore)
|
||||
} else if (feedType === 'following') {
|
||||
this.api = new FollowingFeedAPI(rootStore)
|
||||
} else if (feedType === 'author') {
|
||||
this.api = new AuthorFeedAPI(
|
||||
rootStore,
|
||||
params as GetAuthorFeed.QueryParams,
|
||||
)
|
||||
} else if (feedType === 'likes') {
|
||||
this.api = new LikesFeedAPI(
|
||||
rootStore,
|
||||
params as GetActorLikes.QueryParams,
|
||||
)
|
||||
} else if (feedType === 'custom') {
|
||||
this.api = new CustomFeedAPI(
|
||||
rootStore,
|
||||
params as GetCustomFeed.QueryParams,
|
||||
)
|
||||
} else {
|
||||
this.api = new FollowingFeedAPI(rootStore)
|
||||
}
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
|
@ -105,7 +134,6 @@ export class PostsFeedModel {
|
|||
this.hasLoaded = false
|
||||
this.error = ''
|
||||
this.hasMore = true
|
||||
this.loadMoreCursor = undefined
|
||||
this.pollCursor = undefined
|
||||
this.slices = []
|
||||
this.tuner.reset()
|
||||
|
@ -113,6 +141,8 @@ export class PostsFeedModel {
|
|||
|
||||
get feedTuners() {
|
||||
const areRepliesEnabled = this.rootStore.preferences.homeFeedRepliesEnabled
|
||||
const areRepliesByFollowedOnlyEnabled =
|
||||
this.rootStore.preferences.homeFeedRepliesByFollowedOnlyEnabled
|
||||
const repliesThreshold = this.rootStore.preferences.homeFeedRepliesThreshold
|
||||
const areRepostsEnabled = this.rootStore.preferences.homeFeedRepostsEnabled
|
||||
const areQuotePostsEnabled =
|
||||
|
@ -126,7 +156,7 @@ export class PostsFeedModel {
|
|||
),
|
||||
]
|
||||
}
|
||||
if (this.feedType === 'home') {
|
||||
if (this.feedType === 'home' || this.feedType === 'following') {
|
||||
const feedTuners = []
|
||||
|
||||
if (areRepostsEnabled) {
|
||||
|
@ -136,7 +166,13 @@ export class PostsFeedModel {
|
|||
}
|
||||
|
||||
if (areRepliesEnabled) {
|
||||
feedTuners.push(FeedTuner.likedRepliesOnly({repliesThreshold}))
|
||||
feedTuners.push(
|
||||
FeedTuner.thresholdRepliesOnly({
|
||||
userDid: this.rootStore.session.data?.did || '',
|
||||
minLikes: repliesThreshold,
|
||||
followedOnly: areRepliesByFollowedOnlyEnabled,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
feedTuners.push(FeedTuner.removeReplies)
|
||||
}
|
||||
|
@ -161,10 +197,11 @@ export class PostsFeedModel {
|
|||
await this.lock.acquireAsync()
|
||||
try {
|
||||
this.setHasNewLatest(false)
|
||||
this.api.reset()
|
||||
this.tuner.reset()
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const res = await this._getFeed({limit: this.pageSize})
|
||||
const res = await this.api.fetchNext({limit: this.pageSize})
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
|
@ -201,8 +238,7 @@ export class PostsFeedModel {
|
|||
}
|
||||
this._xLoading()
|
||||
try {
|
||||
const res = await this._getFeed({
|
||||
cursor: this.loadMoreCursor,
|
||||
const res = await this.api.fetchNext({
|
||||
limit: this.pageSize,
|
||||
})
|
||||
await this._appendAll(res)
|
||||
|
@ -230,44 +266,6 @@ export class PostsFeedModel {
|
|||
return this.loadMore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update content in-place
|
||||
*/
|
||||
update = bundleAsync(async () => {
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
if (!this.slices.length) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
let numToFetch = this.slices.length
|
||||
let cursor
|
||||
try {
|
||||
do {
|
||||
const res: GetTimeline.Response = await this._getFeed({
|
||||
cursor,
|
||||
limit: Math.min(numToFetch, 100),
|
||||
})
|
||||
if (res.data.feed.length === 0) {
|
||||
break // sanity check
|
||||
}
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.feed.length
|
||||
cursor = res.data.cursor
|
||||
} while (cursor && numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to update', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this.lock.release()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if new posts are available
|
||||
*/
|
||||
|
@ -275,9 +273,9 @@ export class PostsFeedModel {
|
|||
if (!this.hasLoaded || this.hasNewLatest || this.isLoading) {
|
||||
return
|
||||
}
|
||||
const res = await this._getFeed({limit: 1})
|
||||
if (res.data.feed[0]) {
|
||||
const slices = this.tuner.tune(res.data.feed, this.feedTuners, {
|
||||
const post = await this.api.peekLatest()
|
||||
if (post) {
|
||||
const slices = this.tuner.tune([post], this.feedTuners, {
|
||||
dryRun: true,
|
||||
})
|
||||
if (slices[0]) {
|
||||
|
@ -345,33 +343,27 @@ export class PostsFeedModel {
|
|||
// helper functions
|
||||
// =
|
||||
|
||||
async _replaceAll(
|
||||
res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response,
|
||||
) {
|
||||
this.pollCursor = res.data.feed[0]?.post.uri
|
||||
async _replaceAll(res: FeedAPIResponse) {
|
||||
this.pollCursor = res.feed[0]?.post.uri
|
||||
return this._appendAll(res, true)
|
||||
}
|
||||
|
||||
async _appendAll(
|
||||
res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response,
|
||||
replace = false,
|
||||
) {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
async _appendAll(res: FeedAPIResponse, replace = false) {
|
||||
this.hasMore = !!res.cursor
|
||||
if (replace) {
|
||||
this.emptyFetches = 0
|
||||
}
|
||||
|
||||
this.rootStore.me.follows.hydrateProfiles(
|
||||
res.data.feed.map(item => item.post.author),
|
||||
res.feed.map(item => item.post.author),
|
||||
)
|
||||
for (const item of res.data.feed) {
|
||||
for (const item of res.feed) {
|
||||
this.rootStore.posts.fromFeedItem(item)
|
||||
}
|
||||
|
||||
const slices = this.options.isSimpleFeed
|
||||
? res.data.feed.map(item => new FeedViewPostsSlice([item]))
|
||||
: this.tuner.tune(res.data.feed, this.feedTuners)
|
||||
? res.feed.map(item => new FeedViewPostsSlice([item]))
|
||||
: this.tuner.tune(res.feed, this.feedTuners)
|
||||
|
||||
const toAppend: PostsFeedSliceModel[] = []
|
||||
for (const slice of slices) {
|
||||
|
@ -401,54 +393,4 @@ export class PostsFeedModel {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
_updateAll(
|
||||
res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response,
|
||||
) {
|
||||
for (const item of res.data.feed) {
|
||||
this.rootStore.posts.fromFeedItem(item)
|
||||
const existingSlice = this.slices.find(slice =>
|
||||
slice.containsUri(item.post.uri),
|
||||
)
|
||||
if (existingSlice) {
|
||||
const existingItem = existingSlice.items.find(
|
||||
item2 => item2.post.uri === item.post.uri,
|
||||
)
|
||||
if (existingItem) {
|
||||
existingItem.copyMetrics(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async _getFeed(
|
||||
params: QueryParams,
|
||||
): Promise<
|
||||
GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response
|
||||
> {
|
||||
params = Object.assign({}, this.params, params)
|
||||
if (this.feedType === 'home') {
|
||||
return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
|
||||
} else if (this.feedType === 'custom') {
|
||||
const res = await this.rootStore.agent.app.bsky.feed.getFeed(
|
||||
params as GetCustomFeed.QueryParams,
|
||||
)
|
||||
// NOTE
|
||||
// some custom feeds fail to enforce the pagination limit
|
||||
// so we manually truncate here
|
||||
// -prf
|
||||
if (params.limit && res.data.feed.length > params.limit) {
|
||||
res.data.feed = res.data.feed.slice(0, params.limit)
|
||||
}
|
||||
return res
|
||||
} else if (this.feedType === 'author') {
|
||||
return this.rootStore.agent.getAuthorFeed(
|
||||
params as GetAuthorFeed.QueryParams,
|
||||
)
|
||||
} else {
|
||||
return this.rootStore.agent.getActorLikes(
|
||||
params as GetActorLikes.QueryParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,7 +139,7 @@ export class RootStoreModel {
|
|||
this.agent = agent
|
||||
applyDebugHeader(this.agent)
|
||||
this.me.clear()
|
||||
/* dont await */ this.preferences.sync()
|
||||
await this.preferences.sync()
|
||||
await this.me.load()
|
||||
if (!hadSession) {
|
||||
await resetNavigation()
|
||||
|
|
157
src/state/models/ui/my-feeds.ts
Normal file
157
src/state/models/ui/my-feeds.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {FeedsDiscoveryModel} from '../discovery/feeds'
|
||||
import {CustomFeedModel} from '../feeds/custom-feed'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
|
||||
export type MyFeedsItem =
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'spinner'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'discover-feeds-loading'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'error'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'saved-feeds-header'
|
||||
}
|
||||
| {
|
||||
_reactKey: string
|
||||
type: 'saved-feed'
|
||||
feed: CustomFeedModel
|
||||
}
|
||||
| {
|
||||
_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: CustomFeedModel
|
||||
}
|
||||
|
||||
export class MyFeedsUIModel {
|
||||
discovery: FeedsDiscoveryModel
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this)
|
||||
this.discovery = new FeedsDiscoveryModel(this.rootStore)
|
||||
}
|
||||
|
||||
get saved() {
|
||||
return this.rootStore.me.savedFeeds
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
items.push({
|
||||
_reactKey: '__saved_feeds_loading__',
|
||||
type: 'spinner',
|
||||
})
|
||||
} 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
|
||||
}
|
||||
}
|
|
@ -50,9 +50,11 @@ export class PreferencesModel {
|
|||
pinnedFeeds: string[] = []
|
||||
birthDate: Date | undefined = undefined
|
||||
homeFeedRepliesEnabled: boolean = true
|
||||
homeFeedRepliesThreshold: number = 2
|
||||
homeFeedRepliesByFollowedOnlyEnabled: boolean = true
|
||||
homeFeedRepliesThreshold: number = 0
|
||||
homeFeedRepostsEnabled: boolean = true
|
||||
homeFeedQuotePostsEnabled: boolean = true
|
||||
homeFeedMergeFeedEnabled: boolean = false
|
||||
requireAltTextEnabled: boolean = false
|
||||
|
||||
// used to linearize async modifications to state
|
||||
|
@ -78,9 +80,12 @@ export class PreferencesModel {
|
|||
savedFeeds: this.savedFeeds,
|
||||
pinnedFeeds: this.pinnedFeeds,
|
||||
homeFeedRepliesEnabled: this.homeFeedRepliesEnabled,
|
||||
homeFeedRepliesByFollowedOnlyEnabled:
|
||||
this.homeFeedRepliesByFollowedOnlyEnabled,
|
||||
homeFeedRepliesThreshold: this.homeFeedRepliesThreshold,
|
||||
homeFeedRepostsEnabled: this.homeFeedRepostsEnabled,
|
||||
homeFeedQuotePostsEnabled: this.homeFeedQuotePostsEnabled,
|
||||
homeFeedMergeFeedEnabled: this.homeFeedMergeFeedEnabled,
|
||||
requireAltTextEnabled: this.requireAltTextEnabled,
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +153,14 @@ export class PreferencesModel {
|
|||
) {
|
||||
this.homeFeedRepliesEnabled = v.homeFeedRepliesEnabled
|
||||
}
|
||||
// check if home feed replies "followed only" are enabled in preferences, then hydrate
|
||||
if (
|
||||
hasProp(v, 'homeFeedRepliesByFollowedOnlyEnabled') &&
|
||||
typeof v.homeFeedRepliesByFollowedOnlyEnabled === 'boolean'
|
||||
) {
|
||||
this.homeFeedRepliesByFollowedOnlyEnabled =
|
||||
v.homeFeedRepliesByFollowedOnlyEnabled
|
||||
}
|
||||
// check if home feed replies threshold is enabled in preferences, then hydrate
|
||||
if (
|
||||
hasProp(v, 'homeFeedRepliesThreshold') &&
|
||||
|
@ -169,6 +182,13 @@ export class PreferencesModel {
|
|||
) {
|
||||
this.homeFeedQuotePostsEnabled = v.homeFeedQuotePostsEnabled
|
||||
}
|
||||
// check if home feed mergefeed is enabled in preferences, then hydrate
|
||||
if (
|
||||
hasProp(v, 'homeFeedMergeFeedEnabled') &&
|
||||
typeof v.homeFeedMergeFeedEnabled === 'boolean'
|
||||
) {
|
||||
this.homeFeedMergeFeedEnabled = v.homeFeedMergeFeedEnabled
|
||||
}
|
||||
// check if requiring alt text is enabled in preferences, then hydrate
|
||||
if (
|
||||
hasProp(v, 'requireAltTextEnabled') &&
|
||||
|
@ -449,6 +469,11 @@ export class PreferencesModel {
|
|||
this.homeFeedRepliesEnabled = !this.homeFeedRepliesEnabled
|
||||
}
|
||||
|
||||
toggleHomeFeedRepliesByFollowedOnlyEnabled() {
|
||||
this.homeFeedRepliesByFollowedOnlyEnabled =
|
||||
!this.homeFeedRepliesByFollowedOnlyEnabled
|
||||
}
|
||||
|
||||
setHomeFeedRepliesThreshold(threshold: number) {
|
||||
this.homeFeedRepliesThreshold = threshold
|
||||
}
|
||||
|
@ -461,6 +486,10 @@ export class PreferencesModel {
|
|||
this.homeFeedQuotePostsEnabled = !this.homeFeedQuotePostsEnabled
|
||||
}
|
||||
|
||||
toggleHomeFeedMergeFeedEnabled() {
|
||||
this.homeFeedMergeFeedEnabled = !this.homeFeedMergeFeedEnabled
|
||||
}
|
||||
|
||||
toggleRequireAltTextEnabled() {
|
||||
this.requireAltTextEnabled = !this.requireAltTextEnabled
|
||||
}
|
||||
|
|
|
@ -240,13 +240,6 @@ export class ProfileUiModel {
|
|||
.catch(err => this.rootStore.log.error('Failed to fetch lists', err))
|
||||
}
|
||||
|
||||
async update() {
|
||||
const view = this.currentView
|
||||
if (view instanceof PostsFeedModel) {
|
||||
await view.update()
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await Promise.all([this.profile.refresh(), this.currentView.refresh()])
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue