Store/sync pinned feeds on the server
This commit is contained in:
parent
d88c27a419
commit
7691fe4f48
8 changed files with 278 additions and 240 deletions
|
@ -69,7 +69,6 @@ export class MeModel {
|
|||
displayName: this.displayName,
|
||||
description: this.description,
|
||||
avatar: this.avatar,
|
||||
savedFeeds: this.savedFeeds.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,9 +90,6 @@ export class MeModel {
|
|||
if (hasProp(v, 'avatar') && typeof v.avatar === 'string') {
|
||||
avatar = v.avatar
|
||||
}
|
||||
if (hasProp(v, 'savedFeeds') && isObj(v.savedFeeds)) {
|
||||
this.savedFeeds.hydrate(v.savedFeeds)
|
||||
}
|
||||
if (did && handle) {
|
||||
this.did = did
|
||||
this.handle = handle
|
||||
|
@ -118,7 +114,7 @@ export class MeModel {
|
|||
/* dont await */ this.notifications.setup().catch(e => {
|
||||
this.rootStore.log.error('Failed to setup notifications model', e)
|
||||
})
|
||||
/* dont await */ this.savedFeeds.refresh()
|
||||
/* dont await */ this.savedFeeds.refresh(true)
|
||||
this.rootStore.emitSessionLoaded()
|
||||
await this.fetchInviteCodes()
|
||||
await this.fetchAppPasswords()
|
||||
|
@ -128,6 +124,7 @@ export class MeModel {
|
|||
}
|
||||
|
||||
async updateIfNeeded() {
|
||||
/* dont await */ this.savedFeeds.refresh(true)
|
||||
if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) {
|
||||
this.rootStore.log.debug('Updating me profile information')
|
||||
this.lastProfileStateUpdate = Date.now()
|
||||
|
|
|
@ -25,6 +25,7 @@ const LABEL_GROUPS = [
|
|||
'spam',
|
||||
'impersonation',
|
||||
]
|
||||
const VISIBILITY_VALUES = ['show', 'warn', 'hide']
|
||||
|
||||
export class LabelPreferencesModel {
|
||||
nsfw: LabelPreference = 'hide'
|
||||
|
@ -45,6 +46,7 @@ export class PreferencesModel {
|
|||
contentLanguages: string[] =
|
||||
deviceLocales?.map?.(locale => locale.languageCode) || []
|
||||
contentLabels = new LabelPreferencesModel()
|
||||
pinnedFeeds: string[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {}, {autoBind: true})
|
||||
|
@ -54,6 +56,7 @@ export class PreferencesModel {
|
|||
return {
|
||||
contentLanguages: this.contentLanguages,
|
||||
contentLabels: this.contentLabels,
|
||||
pinnedFeeds: this.pinnedFeeds,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +75,13 @@ export class PreferencesModel {
|
|||
// default to the device languages
|
||||
this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
|
||||
}
|
||||
if (
|
||||
hasProp(v, 'pinnedFeeds') &&
|
||||
Array.isArray(v.pinnedFeeds) &&
|
||||
typeof v.pinnedFeeds.every(item => typeof item === 'string')
|
||||
) {
|
||||
this.pinnedFeeds = v.pinnedFeeds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,9 +98,18 @@ export class PreferencesModel {
|
|||
AppBskyActorDefs.isContentLabelPref(pref) &&
|
||||
AppBskyActorDefs.validateAdultContentPref(pref).success
|
||||
) {
|
||||
if (LABEL_GROUPS.includes(pref.label)) {
|
||||
this.contentLabels[pref.label] = pref.visibility
|
||||
if (
|
||||
LABEL_GROUPS.includes(pref.label) &&
|
||||
VISIBILITY_VALUES.includes(pref.visibility)
|
||||
) {
|
||||
this.contentLabels[pref.label as keyof LabelPreferencesModel] =
|
||||
pref.visibility as LabelPreference
|
||||
}
|
||||
} else if (
|
||||
AppBskyActorDefs.isPinnedFeedsPref(pref) &&
|
||||
AppBskyActorDefs.validatePinnedFeedsPref(pref).success
|
||||
) {
|
||||
this.pinnedFeeds = pref.feeds
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -200,4 +219,39 @@ export class PreferencesModel {
|
|||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async setPinnedFeeds(v: string[]) {
|
||||
const old = this.pinnedFeeds
|
||||
this.pinnedFeeds = v
|
||||
try {
|
||||
await this.update((prefs: AppBskyActorDefs.Preferences) => {
|
||||
const existing = prefs.find(
|
||||
pref =>
|
||||
AppBskyActorDefs.isPinnedFeedsPref(pref) &&
|
||||
AppBskyActorDefs.validatePinnedFeedsPref(pref).success,
|
||||
)
|
||||
if (existing) {
|
||||
existing.feeds = v
|
||||
} else {
|
||||
prefs.push({
|
||||
$type: 'app.bsky.actor.defs#pinnedFeedsPref',
|
||||
feeds: v,
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
runInAction(() => {
|
||||
this.pinnedFeeds = old
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async addPinnedFeed(v: string) {
|
||||
return this.setPinnedFeeds([...this.pinnedFeeds, v])
|
||||
}
|
||||
|
||||
async removePinnedFeed(v: string) {
|
||||
return this.setPinnedFeeds(this.pinnedFeeds.filter(uri => uri !== v))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} from '@atproto/api'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {CustomFeedModel} from '../feeds/custom-feed'
|
||||
import {hasProp, isObj} from 'lib/type-guards'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
const PAGE_SIZE = 100
|
||||
|
||||
export class SavedFeedsModel {
|
||||
// state
|
||||
|
@ -14,12 +13,9 @@ export class SavedFeedsModel {
|
|||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
error = ''
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
|
||||
// data
|
||||
feeds: CustomFeedModel[] = []
|
||||
pinned: CustomFeedModel[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(
|
||||
|
@ -31,24 +27,6 @@ export class SavedFeedsModel {
|
|||
)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
pinned: this.pinned.map(f => f.serialize()),
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'pinned')) {
|
||||
const pinnedSerialized = (v as any).pinned as string[]
|
||||
const pinnedDeserialized = pinnedSerialized.map(
|
||||
(s: string) => new CustomFeedModel(this.rootStore, JSON.parse(s)),
|
||||
)
|
||||
this.pinned = pinnedDeserialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.feeds.length > 0
|
||||
}
|
||||
|
@ -61,149 +39,121 @@ export class SavedFeedsModel {
|
|||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
get numFeeds() {
|
||||
return this.feeds.length
|
||||
get pinned() {
|
||||
return this.rootStore.preferences.pinnedFeeds
|
||||
.map(uri => this.feeds.find(f => f.uri === uri) as CustomFeedModel)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
get unpinned() {
|
||||
return this.feeds.filter(
|
||||
f => !this.pinned.find(p => p.data.uri === f.data.uri),
|
||||
)
|
||||
}
|
||||
|
||||
get feedNames() {
|
||||
return this.feeds.map(f => f.displayName)
|
||||
return this.feeds.filter(f => !this.isPinned(f))
|
||||
}
|
||||
|
||||
get pinnedFeedNames() {
|
||||
return this.pinned.map(f => f.displayName)
|
||||
}
|
||||
|
||||
togglePinnedFeed(feed: CustomFeedModel) {
|
||||
if (!this.isPinned(feed)) {
|
||||
this.pinned = [...this.pinned, feed]
|
||||
} else {
|
||||
this.removePinnedFeed(feed.data.uri)
|
||||
}
|
||||
}
|
||||
|
||||
removePinnedFeed(uri: string) {
|
||||
this.pinned = this.pinned.filter(f => f.data.uri !== uri)
|
||||
}
|
||||
|
||||
reorderPinnedFeeds(temp: CustomFeedModel[]) {
|
||||
this.pinned = temp.filter(item => this.isPinned(item))
|
||||
}
|
||||
|
||||
isPinned(feed: CustomFeedModel) {
|
||||
return this.pinned.find(f => f.data.uri === feed.data.uri) ? true : false
|
||||
}
|
||||
|
||||
movePinnedItem(item: CustomFeedModel, direction: 'up' | 'down') {
|
||||
if (this.pinned.length < 2) {
|
||||
throw new Error('Array must have at least 2 items')
|
||||
}
|
||||
const index = this.pinned.indexOf(item)
|
||||
if (index === -1) {
|
||||
throw new Error('Item not found in array')
|
||||
}
|
||||
|
||||
const len = this.pinned.length
|
||||
|
||||
runInAction(() => {
|
||||
if (direction === 'up') {
|
||||
if (index === 0) {
|
||||
// Remove the item from the first place and put it at the end
|
||||
this.pinned.push(this.pinned.shift()!)
|
||||
} else {
|
||||
// Swap the item with the one before it
|
||||
const temp = this.pinned[index]
|
||||
this.pinned[index] = this.pinned[index - 1]
|
||||
this.pinned[index - 1] = temp
|
||||
}
|
||||
} else if (direction === 'down') {
|
||||
if (index === len - 1) {
|
||||
// Remove the item from the last place and put it at the start
|
||||
this.pinned.unshift(this.pinned.pop()!)
|
||||
} else {
|
||||
// Swap the item with the one after it
|
||||
const temp = this.pinned[index]
|
||||
this.pinned[index] = this.pinned[index + 1]
|
||||
this.pinned[index + 1] = temp
|
||||
}
|
||||
}
|
||||
// this.pinned = [...this.pinned]
|
||||
})
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
async refresh(quietRefresh = false) {
|
||||
return this.loadMore(true, quietRefresh)
|
||||
}
|
||||
|
||||
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, quietRefresh = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading(replace && !quietRefresh)
|
||||
try {
|
||||
refresh = bundleAsync(async (quietRefresh = false) => {
|
||||
this._xLoading(!quietRefresh)
|
||||
try {
|
||||
let feeds: AppBskyFeedDefs.GeneratorView[] = []
|
||||
let cursor
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({
|
||||
limit: PAGE_SIZE,
|
||||
cursor: replace ? undefined : this.loadMoreCursor,
|
||||
cursor,
|
||||
})
|
||||
if (replace) {
|
||||
this._replaceAll(res)
|
||||
} else {
|
||||
this._appendAll(res)
|
||||
feeds = feeds.concat(res.data.feeds)
|
||||
cursor = res.data.cursor
|
||||
if (!cursor) {
|
||||
break
|
||||
}
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
},
|
||||
)
|
||||
runInAction(() => {
|
||||
this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f))
|
||||
})
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
})
|
||||
|
||||
removeFeed(uri: string) {
|
||||
this.feeds = this.feeds.filter(f => f.data.uri !== uri)
|
||||
}
|
||||
|
||||
addFeed(algoItem: CustomFeedModel) {
|
||||
this.feeds.push(new CustomFeedModel(this.rootStore, algoItem.data))
|
||||
}
|
||||
|
||||
async save(algoItem: CustomFeedModel) {
|
||||
async save(feed: CustomFeedModel) {
|
||||
try {
|
||||
await algoItem.save()
|
||||
this.addFeed(algoItem)
|
||||
await feed.save()
|
||||
runInAction(() => {
|
||||
this.feeds = [
|
||||
...this.feeds,
|
||||
new CustomFeedModel(this.rootStore, feed.data),
|
||||
]
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.rootStore.log.error('Failed to save feed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async unsave(algoItem: CustomFeedModel) {
|
||||
const uri = algoItem.uri
|
||||
async unsave(feed: CustomFeedModel) {
|
||||
const uri = feed.uri
|
||||
try {
|
||||
await algoItem.unsave()
|
||||
this.removeFeed(uri)
|
||||
this.removePinnedFeed(uri)
|
||||
if (this.isPinned(feed)) {
|
||||
await this.rootStore.preferences.removePinnedFeed(uri)
|
||||
}
|
||||
await feed.unsave()
|
||||
runInAction(() => {
|
||||
this.feeds = this.feeds.filter(f => f.data.uri !== uri)
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.rootStore.log.error('Failed to unsave feed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async togglePinnedFeed(feed: CustomFeedModel) {
|
||||
if (!this.isPinned(feed)) {
|
||||
return this.rootStore.preferences.addPinnedFeed(feed.uri)
|
||||
} else {
|
||||
return this.rootStore.preferences.removePinnedFeed(feed.uri)
|
||||
}
|
||||
}
|
||||
|
||||
async reorderPinnedFeeds(feeds: CustomFeedModel[]) {
|
||||
return this.rootStore.preferences.setPinnedFeeds(
|
||||
feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri),
|
||||
)
|
||||
}
|
||||
|
||||
isPinned(feed: CustomFeedModel) {
|
||||
return this.rootStore.preferences.pinnedFeeds.includes(feed.uri)
|
||||
}
|
||||
|
||||
async movePinnedFeed(item: CustomFeedModel, direction: 'up' | 'down') {
|
||||
const pinned = this.rootStore.preferences.pinnedFeeds.slice()
|
||||
const index = pinned.indexOf(item.uri)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
if (direction === 'up' && index !== 0) {
|
||||
const temp = pinned[index]
|
||||
pinned[index] = pinned[index - 1]
|
||||
pinned[index - 1] = temp
|
||||
} else if (direction === 'down' && index < pinned.length - 1) {
|
||||
const temp = pinned[index]
|
||||
pinned[index] = pinned[index + 1]
|
||||
pinned[index + 1] = temp
|
||||
}
|
||||
await this.rootStore.preferences.setPinnedFeeds(pinned)
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
|
@ -219,23 +169,7 @@ export class SavedFeedsModel {
|
|||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch user followers', err)
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
_replaceAll(res: GetSavedFeeds.Response) {
|
||||
this.feeds = []
|
||||
this._appendAll(res)
|
||||
}
|
||||
|
||||
_appendAll(res: GetSavedFeeds.Response) {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
for (const f of res.data.feeds) {
|
||||
this.feeds.push(new CustomFeedModel(this.rootStore, f))
|
||||
this.rootStore.log.error('Failed to fetch user feeds', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue