From b672006f7e1c21c635eb4ec60e21910af586c00d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 13:52:16 -0500 Subject: Reorganize custom-feed state models and add the missing _reactKey attribute --- src/state/models/feeds/custom-feed.ts | 143 ++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/state/models/feeds/custom-feed.ts (limited to 'src/state/models/feeds/custom-feed.ts') diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts new file mode 100644 index 000000000..20eef49ad --- /dev/null +++ b/src/state/models/feeds/custom-feed.ts @@ -0,0 +1,143 @@ +import {AppBskyFeedDefs, AtUri} from '@atproto/api' +import {makeAutoObservable} from 'mobx' +import {RootStoreModel} from 'state/models/root-store' + +export class CustomFeedModel { + // data + _reactKey: string + data: AppBskyFeedDefs.GeneratorView + + constructor( + public rootStore: RootStoreModel, + view: AppBskyFeedDefs.GeneratorView, + ) { + this._reactKey = view.uri + this.data = view + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + // local actions + // = + set toggleSaved(value: boolean) { + if (this.data.viewer) { + this.data.viewer.saved = value + } + } + + get getUri() { + return this.data.uri + } + + get isSaved() { + return this.data.viewer?.saved + } + + get isLiked() { + return this.data.viewer?.like + } + + private toggleLiked(s?: string) { + if (this.data.viewer) { + if (this.data.viewer.like) { + this.data.viewer.like = undefined + } else { + this.data.viewer.like = s + } + } + } + + private incrementLike() { + if (this.data.likeCount) { + this.data.likeCount += 1 + } else { + this.data.likeCount = 1 + } + } + + private decrementLike() { + if (this.data.likeCount) { + this.data.likeCount -= 1 + } else { + this.data.likeCount = 0 + } + } + + private rewriteData(data: AppBskyFeedDefs.GeneratorView) { + this.data = data + } + + // public apis + // = + async like() { + try { + const res = await this.rootStore.agent.app.bsky.feed.like.create( + { + repo: this.rootStore.me.did, + }, + { + subject: { + uri: this.data.uri, + cid: this.data.cid, + }, + createdAt: new Date().toISOString(), + }, + ) + this.toggleLiked(res.uri) + this.incrementLike() + } catch (e: any) { + this.rootStore.log.error('Failed to like feed', e) + } + } + + async unlike() { + try { + await this.rootStore.agent.app.bsky.feed.like.delete({ + repo: this.rootStore.me.did, + rkey: new AtUri(this.data.viewer?.like!).rkey, + }) + this.toggleLiked() + this.decrementLike() + } catch (e: any) { + this.rootStore.log.error('Failed to unlike feed', e) + } + } + + static async getView(store: RootStoreModel, uri: string) { + const res = await store.agent.app.bsky.feed.getFeedGenerator({ + feed: uri, + }) + const view = res.data.view + return view + } + + async checkIsValid() { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ + feed: this.data.uri, + }) + return res.data.isValid + } + + async checkIsOnline() { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ + feed: this.data.uri, + }) + return res.data.isOnline + } + + async reload() { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ + feed: this.data.uri, + }) + this.rewriteData(res.data.view) + } + + serialize() { + return JSON.stringify(this.data) + } +} -- cgit 1.4.1 From 9c02fbb925290b9f147e1622f4f41f5a956dce5c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 14:38:23 -0500 Subject: Unify the display name fallback behavior --- src/state/models/feeds/custom-feed.ts | 8 ++++++++ src/state/models/ui/saved-feeds.ts | 8 ++------ src/view/com/feeds/CustomFeed.tsx | 11 ++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) (limited to 'src/state/models/feeds/custom-feed.ts') diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 20eef49ad..e60ac2740 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -1,6 +1,7 @@ import {AppBskyFeedDefs, AtUri} from '@atproto/api' import {makeAutoObservable} from 'mobx' import {RootStoreModel} from 'state/models/root-store' +import {sanitizeDisplayName} from 'lib/strings/display-names' export class CustomFeedModel { // data @@ -34,6 +35,13 @@ export class CustomFeedModel { return this.data.uri } + get displayName() { + if (this.data.displayName) { + return sanitizeDisplayName(this.data.displayName) + } + return `Feed by @${this.data.creator.handle}` + } + get isSaved() { return this.data.viewer?.saved } diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index ce0de25ec..85e77cae5 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -66,15 +66,11 @@ export class SavedFeedsModel { } get listOfFeedNames() { - return this.feeds.map( - f => f.data.displayName ?? f.data.creator.displayName + "'s feed", - ) + return this.feeds.map(f => f.displayName) } get listOfPinnedFeedNames() { - return this.pinned.map( - f => f.data.displayName ?? f.data.creator.displayName + "'s feed", - ) + return this.pinned.map(f => f.displayName) } get savedFeedsWithoutPinned() { diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index e4e0d50c2..95726be64 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -54,9 +54,7 @@ export const CustomFeed = observer( navigation.navigate('CustomFeed', { name: item.data.creator.did, rkey: new AtUri(item.data.uri).rkey, - displayName: - item.data.displayName ?? - `${item.data.creator.displayName}'s feed`, + displayName: item.displayName, }) }} key={item.data.uri}> @@ -65,8 +63,11 @@ export const CustomFeed = observer( - - {item.data.displayName ?? 'Feed name'} + + {item.displayName} + + + by @{item.data.creator.handle} {showSaveBtn && ( -- cgit 1.4.1 From 2fd3b4ca04622836ad2a65ec8992fd429ecd9fed Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 14:55:04 -0500 Subject: Clean up the custom-feed model --- src/state/models/feeds/custom-feed.ts | 115 +++++++++++--------------------- src/state/models/ui/saved-feeds.ts | 14 ++-- src/view/com/util/UserAvatar.tsx | 2 +- src/view/com/util/post-embeds/index.tsx | 1 + src/view/screens/Home.tsx | 2 +- 5 files changed, 45 insertions(+), 89 deletions(-) (limited to 'src/state/models/feeds/custom-feed.ts') diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index e60ac2740..5e550ec69 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -1,5 +1,5 @@ -import {AppBskyFeedDefs, AtUri} from '@atproto/api' -import {makeAutoObservable} from 'mobx' +import {AppBskyFeedDefs} from '@atproto/api' +import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from 'state/models/root-store' import {sanitizeDisplayName} from 'lib/strings/display-names' @@ -25,13 +25,8 @@ export class CustomFeedModel { // local actions // = - set toggleSaved(value: boolean) { - if (this.data.viewer) { - this.data.viewer.saved = value - } - } - get getUri() { + get uri() { return this.data.uri } @@ -50,99 +45,65 @@ export class CustomFeedModel { return this.data.viewer?.like } - private toggleLiked(s?: string) { - if (this.data.viewer) { - if (this.data.viewer.like) { - this.data.viewer.like = undefined - } else { - this.data.viewer.like = s - } - } - } - - private incrementLike() { - if (this.data.likeCount) { - this.data.likeCount += 1 - } else { - this.data.likeCount = 1 - } - } + // public apis + // = - private decrementLike() { - if (this.data.likeCount) { - this.data.likeCount -= 1 - } else { - this.data.likeCount = 0 - } + async save() { + await this.rootStore.agent.app.bsky.feed.saveFeed({ + feed: this.uri, + }) + runInAction(() => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.saved = true + }) } - private rewriteData(data: AppBskyFeedDefs.GeneratorView) { - this.data = data + async unsave() { + await this.rootStore.agent.app.bsky.feed.unsaveFeed({ + feed: this.uri, + }) + runInAction(() => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.saved = false + }) } - // public apis - // = async like() { try { - const res = await this.rootStore.agent.app.bsky.feed.like.create( - { - repo: this.rootStore.me.did, - }, - { - subject: { - uri: this.data.uri, - cid: this.data.cid, - }, - createdAt: new Date().toISOString(), - }, - ) - this.toggleLiked(res.uri) - this.incrementLike() + const res = await this.rootStore.agent.like(this.data.uri, this.data.cid) + runInAction(() => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = res.uri + this.data.likeCount = (this.data.likeCount || 0) + 1 + }) } catch (e: any) { this.rootStore.log.error('Failed to like feed', e) } } async unlike() { + if (!this.data.viewer.like) { + return + } try { - await this.rootStore.agent.app.bsky.feed.like.delete({ - repo: this.rootStore.me.did, - rkey: new AtUri(this.data.viewer?.like!).rkey, + await this.rootStore.agent.deleteLike(this.data.viewer.like!) + runInAction(() => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = undefined + this.data.likeCount = (this.data.likeCount || 1) - 1 }) - this.toggleLiked() - this.decrementLike() } catch (e: any) { this.rootStore.log.error('Failed to unlike feed', e) } } - static async getView(store: RootStoreModel, uri: string) { - const res = await store.agent.app.bsky.feed.getFeedGenerator({ - feed: uri, - }) - const view = res.data.view - return view - } - - async checkIsValid() { - const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ - feed: this.data.uri, - }) - return res.data.isValid - } - - async checkIsOnline() { - const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ - feed: this.data.uri, - }) - return res.data.isOnline - } - async reload() { const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ feed: this.data.uri, }) - this.rewriteData(res.data.view) + runInAction(() => { + this.data = res.data.view + }) } serialize() { diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 85e77cae5..dca079b72 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -184,10 +184,7 @@ export class SavedFeedsModel { async save(algoItem: CustomFeedModel) { try { - await this.rootStore.agent.app.bsky.feed.saveFeed({ - feed: algoItem.getUri, - }) - algoItem.toggleSaved = true + await algoItem.save() this.addFeed(algoItem) } catch (e: any) { this.rootStore.log.error('Failed to save feed', e) @@ -195,16 +192,13 @@ export class SavedFeedsModel { } async unsave(algoItem: CustomFeedModel) { - const uri = algoItem.getUri + const uri = algoItem.uri try { - await this.rootStore.agent.app.bsky.feed.unsaveFeed({ - feed: uri, - }) - algoItem.toggleSaved = false + await algoItem.unsave() this.removeFeed(uri) this.removePinnedFeed(uri) } catch (e: any) { - this.rootStore.log.error('Failed to unsanve feed', e) + this.rootStore.log.error('Failed to unsave feed', e) } } diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 10a605b25..1e3690fbd 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -188,7 +188,7 @@ export function UserAvatar({ accessibilityRole="image" /> ) : ( - + )} ) -- cgit 1.4.1 From 5537d19e555c39f5f9a0ec16735ea4c3860357c4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 14:39:04 -0500 Subject: Update saved feeds to use preferences --- src/state/models/feeds/custom-feed.ts | 20 ++-------- src/state/models/media/image.ts | 2 +- src/state/models/ui/preferences.ts | 57 ++++++++++++++++++++++------- src/state/models/ui/saved-feeds.ts | 25 +++++++------ src/state/models/ui/shell.ts | 2 +- src/view/com/feeds/CustomFeed.tsx | 2 +- src/view/com/util/ViewHeader.tsx | 2 +- src/view/com/util/moderation/ImageHider.tsx | 8 ++-- 8 files changed, 68 insertions(+), 50 deletions(-) (limited to 'src/state/models/feeds/custom-feed.ts') diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 5e550ec69..e457d2d1e 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -38,7 +38,7 @@ export class CustomFeedModel { } get isSaved() { - return this.data.viewer?.saved + return this.rootStore.preferences.savedFeeds.includes(this.uri) } get isLiked() { @@ -49,23 +49,11 @@ export class CustomFeedModel { // = async save() { - await this.rootStore.agent.app.bsky.feed.saveFeed({ - feed: this.uri, - }) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.saved = true - }) + await this.rootStore.preferences.addSavedFeed(this.uri) } async unsave() { - await this.rootStore.agent.app.bsky.feed.unsaveFeed({ - feed: this.uri, - }) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.saved = false - }) + await this.rootStore.preferences.removeSavedFeed(this.uri) } async like() { @@ -82,7 +70,7 @@ export class CustomFeedModel { } async unlike() { - if (!this.data.viewer.like) { + if (!this.data.viewer?.like) { return } try { diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index ec93bf5b6..6edf88d9d 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -135,7 +135,7 @@ export class ImageModel implements RNImage { // Only for mobile async crop() { try { - const cropped = await openCropper({ + const cropped = await openCropper(this.rootStore, { mediaType: 'photo', path: this.path, freeStyleCropEnabled: true, diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 05a1eb128..120b4adcc 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -46,6 +46,7 @@ export class PreferencesModel { contentLanguages: string[] = deviceLocales?.map?.(locale => locale.languageCode) || [] contentLabels = new LabelPreferencesModel() + savedFeeds: string[] = [] pinnedFeeds: string[] = [] constructor(public rootStore: RootStoreModel) { @@ -56,6 +57,7 @@ export class PreferencesModel { return { contentLanguages: this.contentLanguages, contentLabels: this.contentLabels, + savedFeeds: this.savedFeeds, pinnedFeeds: this.pinnedFeeds, } } @@ -75,6 +77,13 @@ export class PreferencesModel { // default to the device languages this.contentLanguages = deviceLocales.map(locale => locale.languageCode) } + if ( + hasProp(v, 'savedFeeds') && + Array.isArray(v.savedFeeds) && + typeof v.savedFeeds.every(item => typeof item === 'string') + ) { + this.savedFeeds = v.savedFeeds + } if ( hasProp(v, 'pinnedFeeds') && Array.isArray(v.pinnedFeeds) && @@ -106,10 +115,11 @@ export class PreferencesModel { pref.visibility as LabelPreference } } else if ( - AppBskyActorDefs.isPinnedFeedsPref(pref) && - AppBskyActorDefs.validatePinnedFeedsPref(pref).success + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success ) { - this.pinnedFeeds = pref.feeds + this.savedFeeds = pref.saved + this.pinnedFeeds = pref.pinned } } }) @@ -220,38 +230,57 @@ export class PreferencesModel { return res } - async setPinnedFeeds(v: string[]) { - const old = this.pinnedFeeds - this.pinnedFeeds = v + async setSavedFeeds(saved: string[], pinned: string[]) { + const oldSaved = this.savedFeeds + const oldPinned = this.pinnedFeeds + this.savedFeeds = saved + this.pinnedFeeds = pinned try { await this.update((prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.find( pref => - AppBskyActorDefs.isPinnedFeedsPref(pref) && - AppBskyActorDefs.validatePinnedFeedsPref(pref).success, + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success, ) if (existing) { - existing.feeds = v + existing.saved = saved + existing.pinned = pinned } else { prefs.push({ - $type: 'app.bsky.actor.defs#pinnedFeedsPref', - feeds: v, + $type: 'app.bsky.actor.defs#savedFeedsPref', + saved, + pinned, }) } }) } catch (e) { runInAction(() => { - this.pinnedFeeds = old + this.savedFeeds = oldSaved + this.pinnedFeeds = oldPinned }) throw e } } + async addSavedFeed(v: string) { + return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds) + } + + async removeSavedFeed(v: string) { + return this.setSavedFeeds( + this.savedFeeds.filter(uri => uri !== v), + this.pinnedFeeds.filter(uri => uri !== v), + ) + } + async addPinnedFeed(v: string) { - return this.setPinnedFeeds([...this.pinnedFeeds, v]) + return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v]) } async removePinnedFeed(v: string) { - return this.setPinnedFeeds(this.pinnedFeeds.filter(uri => uri !== v)) + return this.setSavedFeeds( + this.savedFeeds, + this.pinnedFeeds.filter(uri => uri !== v), + ) } } diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index f500aef2e..9de28e028 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -5,8 +5,6 @@ import {bundleAsync} from 'lib/async/bundle' import {cleanError} from 'lib/strings/errors' import {CustomFeedModel} from '../feeds/custom-feed' -const PAGE_SIZE = 100 - export class SavedFeedsModel { // state isLoading = false @@ -69,16 +67,15 @@ export class SavedFeedsModel { 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, + for ( + let i = 0; + i < this.rootStore.preferences.savedFeeds.length; + i += 25 + ) { + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerators({ + feeds: this.rootStore.preferences.savedFeeds.slice(i, 25), }) feeds = feeds.concat(res.data.feeds) - cursor = res.data.cursor - if (!cursor) { - break - } } runInAction(() => { this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f)) @@ -127,7 +124,8 @@ export class SavedFeedsModel { } async reorderPinnedFeeds(feeds: CustomFeedModel[]) { - return this.rootStore.preferences.setPinnedFeeds( + return this.rootStore.preferences.setSavedFeeds( + this.rootStore.preferences.savedFeeds, feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri), ) } @@ -151,7 +149,10 @@ export class SavedFeedsModel { pinned[index] = pinned[index + 1] pinned[index + 1] = temp } - await this.rootStore.preferences.setPinnedFeeds(pinned) + await this.rootStore.preferences.setSavedFeeds( + this.rootStore.preferences.savedFeeds, + pinned, + ) } // state transitions diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 9b9a176be..95b666243 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -119,7 +119,7 @@ export type Modal = // Moderation | ReportAccountModal | ReportPostModal - | CreateMuteListModal + | CreateOrEditMuteListModal | ListAddRemoveUserModal // Posts diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index d4e843b67..9a71eb846 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -40,7 +40,7 @@ export const CustomFeed = observer( const navigation = useNavigation() const onToggleSaved = React.useCallback(async () => { - if (item.data.viewer?.saved) { + if (item.isSaved) { store.shell.openModal({ name: 'confirm', title: 'Remove from my feeds', diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 7f13f1838..c17a65b14 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -121,7 +121,7 @@ const Container = observer( }: { children: React.ReactNode hideOnScroll: boolean - showBorder: boolean + showBorder?: boolean }) => { const store = useStores() const pal = usePalette('default') diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx index b42c6397d..40add5b67 100644 --- a/src/view/com/util/moderation/ImageHider.tsx +++ b/src/view/com/util/moderation/ImageHider.tsx @@ -27,6 +27,10 @@ export function ImageHider({ setOverride(false) }, [setOverride]) + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null + } + if (moderation.behavior !== ModerationBehaviorCode.WarnImages) { return ( @@ -35,10 +39,6 @@ export function ImageHider({ ) } - if (moderation.behavior === ModerationBehaviorCode.Hide) { - return null - } - return ( -- cgit 1.4.1 From 64e303d911d351a2f492a23ae97207e5c6035b6e Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Mon, 22 May 2023 16:35:37 -0700 Subject: optimistic updates for liking custom feeds --- src/lib/async/revertible.ts | 16 ++++++++++++++++ src/state/models/feeds/custom-feed.ts | 36 +++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 12 deletions(-) (limited to 'src/state/models/feeds/custom-feed.ts') diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts index 3c8e3e8f9..43383b61e 100644 --- a/src/lib/async/revertible.ts +++ b/src/lib/async/revertible.ts @@ -4,6 +4,22 @@ import set from 'lodash.set' const ongoingActions = new Set() +/** + * This is a TypeScript function that optimistically updates data on the client-side before sending a + * request to the server and rolling back changes if the request fails. + * @param {T} model - The object or record that needs to be updated optimistically. + * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It + * can be used to perform any necessary actions or updates on the model or UI before the server update + * is initiated. + * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server + * update operation. This function is called after the previous state of the model has been recorded + * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate` + * function is called with the result + * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the + * server update is successful. It takes in the response from the server update as its parameter. If + * this parameter is not provided, nothing will happen after the server update. + * @returns A Promise that resolves to `void`. + */ export const updateDataOptimistically = async < T extends Record, U, diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index e457d2d1e..9ac69ac28 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -2,6 +2,7 @@ import {AppBskyFeedDefs} from '@atproto/api' import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from 'state/models/root-store' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {updateDataOptimistically} from 'lib/async/revertible' export class CustomFeedModel { // data @@ -58,12 +59,19 @@ export class CustomFeedModel { async like() { try { - const res = await this.rootStore.agent.like(this.data.uri, this.data.cid) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.like = res.uri - this.data.likeCount = (this.data.likeCount || 0) + 1 - }) + await updateDataOptimistically( + this.data, + () => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = 'pending' + this.data.likeCount = (this.data.likeCount || 0) + 1 + }, + () => this.rootStore.agent.like(this.data.uri, this.data.cid), + res => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = res.uri + }, + ) } catch (e: any) { this.rootStore.log.error('Failed to like feed', e) } @@ -74,12 +82,16 @@ export class CustomFeedModel { return } try { - await this.rootStore.agent.deleteLike(this.data.viewer.like!) - runInAction(() => { - this.data.viewer = this.data.viewer || {} - this.data.viewer.like = undefined - this.data.likeCount = (this.data.likeCount || 1) - 1 - }) + const likeUri = this.data.viewer.like + await updateDataOptimistically( + this.data, + () => { + this.data.viewer = this.data.viewer || {} + this.data.viewer.like = undefined + this.data.likeCount = (this.data.likeCount || 1) - 1 + }, + () => this.rootStore.agent.deleteLike(likeUri), + ) } catch (e: any) { this.rootStore.log.error('Failed to unlike feed', e) } -- cgit 1.4.1 From 4cd9a4493e0477d27fbaca7ae024440460de4a8a Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Thu, 25 May 2023 18:48:40 -0700 Subject: add empty state for custom feed --- src/state/models/feeds/custom-feed.ts | 8 ++++++++ src/view/screens/CustomFeed.tsx | 6 ++++++ 2 files changed, 14 insertions(+) (limited to 'src/state/models/feeds/custom-feed.ts') diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 9ac69ac28..8fc1eb1ec 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -8,13 +8,19 @@ export class CustomFeedModel { // data _reactKey: string data: AppBskyFeedDefs.GeneratorView + isOnline: boolean + isValid: boolean constructor( public rootStore: RootStoreModel, view: AppBskyFeedDefs.GeneratorView, + isOnline?: boolean, + isValid?: boolean, ) { this._reactKey = view.uri this.data = view + this.isOnline = isOnline ?? true + this.isValid = isValid ?? true makeAutoObservable( this, { @@ -103,6 +109,8 @@ export class CustomFeedModel { }) runInAction(() => { this.data = res.data.view + this.isOnline = res.data.isOnline + this.isValid = res.data.isValid }) } diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 79b10de04..4149cd49d 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -30,6 +30,7 @@ import {FAB} from '../com/util/fab/FAB' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {EmptyState} from 'view/com/util/EmptyState' type Props = NativeStackScreenProps export const CustomFeedScreen = withAuthRequired( @@ -326,6 +327,10 @@ export const CustomFeedScreen = withAuthRequired( onTogglePinned, ]) + const renderEmptyState = React.useCallback(() => { + return + }, []) + return ( @@ -335,6 +340,7 @@ export const CustomFeedScreen = withAuthRequired( onScroll={onMainScroll} scrollEventThrottle={100} ListHeaderComponent={renderListHeaderComponent} + renderEmptyState={renderEmptyState} extraData={[uri, isPinned]} /> {isScrolledDown ? ( -- cgit 1.4.1