ea885339cf
* 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>
257 lines
7.2 KiB
TypeScript
257 lines
7.2 KiB
TypeScript
import {makeAutoObservable, runInAction} from 'mobx'
|
|
import {RootStoreModel} from '../root-store'
|
|
import {ProfileModel} from '../content/profile'
|
|
import {PostsFeedModel} from '../feeds/posts'
|
|
import {ActorFeedsModel} from '../lists/actor-feeds'
|
|
import {ListsListModel} from '../lists/lists-list'
|
|
|
|
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 => this.rootStore.log.error('Failed to fetch profile', err)),
|
|
this.feed
|
|
.setup()
|
|
.catch(err => this.rootStore.log.error('Failed to fetch feed', 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 => this.rootStore.log.error('Failed to fetch lists', 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()
|
|
}
|
|
}
|
|
}
|