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:
Paul Frazee 2023-09-18 11:44:29 -07:00 committed by GitHub
parent 3118e3e933
commit ea885339cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1884 additions and 1497 deletions

View file

@ -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
// =
}

View file

@ -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(

View file

@ -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,
)
}
}
}

View file

@ -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()

View 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
}
}

View file

@ -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
}

View file

@ -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()])
}