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/ui/saved-feeds.ts | 249 +++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 src/state/models/ui/saved-feeds.ts (limited to 'src/state/models/ui/saved-feeds.ts') diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts new file mode 100644 index 000000000..ce0de25ec --- /dev/null +++ b/src/state/models/ui/saved-feeds.ts @@ -0,0 +1,249 @@ +import {makeAutoObservable, runInAction} from 'mobx' +import {AppBskyFeedGetSavedFeeds as GetSavedFeeds} 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 + +export class SavedFeedsModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + hasMore = true + loadMoreCursor?: string + + // data + feeds: CustomFeedModel[] = [] + pinned: CustomFeedModel[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + 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 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + get numOfFeeds() { + return this.feeds.length + } + + get listOfFeedNames() { + return this.feeds.map( + f => f.data.displayName ?? f.data.creator.displayName + "'s feed", + ) + } + + get listOfPinnedFeedNames() { + return this.pinned.map( + f => f.data.displayName ?? f.data.creator.displayName + "'s feed", + ) + } + + get savedFeedsWithoutPinned() { + return this.feeds.filter( + f => !this.pinned.find(p => p.data.uri === f.data.uri), + ) + } + + togglePinnedFeed(feed: CustomFeedModel) { + if (!this.isPinned(feed)) { + this.pinned.push(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 + } + + 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() { + return this.loadMore(true) + } + + 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) => { + if (!replace && !this.hasMore) { + return + } + this._xLoading(replace) + try { + const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({ + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + 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) { + try { + await this.rootStore.agent.app.bsky.feed.saveFeed({ + feed: algoItem.getUri, + }) + algoItem.toggleSaved = true + this.addFeed(algoItem) + } catch (e: any) { + this.rootStore.log.error('Failed to save feed', e) + } + } + + async unsave(algoItem: CustomFeedModel) { + const uri = algoItem.getUri + try { + await this.rootStore.agent.app.bsky.feed.unsaveFeed({ + feed: uri, + }) + algoItem.toggleSaved = false + this.removeFeed(uri) + this.removePinnedFeed(uri) + } catch (e: any) { + this.rootStore.log.error('Failed to unsanve feed', e) + } + } + + // state transitions + // = + + _xLoading(isRefreshing = false) { + this.isLoading = true + this.isRefreshing = isRefreshing + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + 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)) + } + } +} -- 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/ui/saved-feeds.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/ui/saved-feeds.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 f0003d193182bd70935ca6b7e67897922297deed Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 20:33:58 -0500 Subject: Add 'my feeds' tab --- src/state/models/ui/saved-feeds.ts | 44 +++++---- src/view/com/feeds/CustomFeed.tsx | 4 +- src/view/com/feeds/SavedFeeds.tsx | 163 +++++++++++++++++++++++++++++++ src/view/com/pager/FeedsTabBarMobile.tsx | 1 + src/view/screens/Home.tsx | 10 +- 5 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 src/view/com/feeds/SavedFeeds.tsx (limited to 'src/state/models/ui/saved-feeds.ts') diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index dca079b72..bae98fc84 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -139,8 +139,8 @@ export class SavedFeedsModel { // public api // = - async refresh() { - return this.loadMore(true) + async refresh(quietRefresh = false) { + return this.loadMore(true, quietRefresh) } clear() { @@ -153,26 +153,28 @@ export class SavedFeedsModel { this.feeds = [] } - loadMore = bundleAsync(async (replace: boolean = false) => { - if (!replace && !this.hasMore) { - return - } - this._xLoading(replace) - try { - const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({ - limit: PAGE_SIZE, - cursor: replace ? undefined : this.loadMoreCursor, - }) - if (replace) { - this._replaceAll(res) - } else { - this._appendAll(res) + loadMore = bundleAsync( + async (replace: boolean = false, quietRefresh = false) => { + if (!replace && !this.hasMore) { + return } - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - }) + this._xLoading(replace && !quietRefresh) + try { + const res = await this.rootStore.agent.app.bsky.feed.getSavedFeeds({ + limit: PAGE_SIZE, + cursor: replace ? undefined : this.loadMoreCursor, + }) + if (replace) { + this._replaceAll(res) + } else { + this._appendAll(res) + } + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }, + ) removeFeed(uri: string) { this.feeds = this.feeds.filter(f => f.data.uri !== uri) diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index 5a93020a0..5440a8e8f 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -24,11 +24,13 @@ export const CustomFeed = observer( item, style, showSaveBtn = false, + showDescription = false, showLikes = false, }: { item: CustomFeedModel style?: StyleProp showSaveBtn?: boolean + showDescription?: boolean showLikes?: boolean }) => { const store = useStores() @@ -75,7 +77,7 @@ export const CustomFeed = observer( )} - {item.data.description ? ( + {showDescription && item.data.description ? ( {item.data.description} diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx new file mode 100644 index 000000000..66a4efecf --- /dev/null +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -0,0 +1,163 @@ +import React, {useEffect, useCallback} from 'react' +import { + ActivityIndicator, + FlatList, + RefreshControl, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {usePalette} from 'lib/hooks/usePalette' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +import {CustomFeedModel} from 'state/models/feeds/custom-feed' +import {SavedFeedsModel} from 'state/models/ui/saved-feeds' +import {CenteredView} from 'view/com/util/Views' +import {Text} from 'view/com/util/text/Text' +import {isDesktopWeb} from 'platform/detection' +import {s, colors} from 'lib/styles' +import {Link} from 'view/com/util/Link' +import {CustomFeed} from 'view/com/feeds/CustomFeed' + +export const SavedFeeds = observer( + ({ + headerOffset = 0, + isPageFocused, + }: { + headerOffset?: number + isPageFocused: boolean + }) => { + const pal = usePalette('default') + const store = useStores() + + useEffect(() => { + if (isPageFocused) { + store.shell.setMinimalShellMode(false) + store.me.savedFeeds.refresh(true) + } + }, [store, isPageFocused]) + + const renderListEmptyComponent = useCallback(() => { + return ( + + + You don't have any saved feeds. You can find feeds by searching on + Bluesky. + + + ) + }, [pal]) + + const renderListFooterComponent = useCallback(() => { + return ( + + + + Settings + + + ) + }, [pal]) + + const renderItem = useCallback( + ({item}) => ( + + ), + [store.me.savedFeeds], + ) + + return ( + + item.data.uri} + refreshing={store.me.savedFeeds.isRefreshing} + refreshControl={ + store.me.savedFeeds.refresh()} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + progressViewOffset={headerOffset} + /> + } + renderItem={renderItem} + initialNumToRender={10} + ListFooterComponent={renderListFooterComponent} + ListEmptyComponent={renderListEmptyComponent} + extraData={store.me.savedFeeds.isLoading} + contentOffset={{x: 0, y: headerOffset * -1}} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + + ) + }, +) + +const SavedFeedItem = observer( + ({ + item, + savedFeeds, + }: { + item: CustomFeedModel + savedFeeds: SavedFeedsModel + }) => { + const isPinned = savedFeeds.isPinned(item) + const onTogglePinned = useCallback( + () => savedFeeds.togglePinnedFeed(item), + [savedFeeds, item], + ) + + return ( + + + + + + + ) + }, +) + +const styles = StyleSheet.create({ + footerLink: { + flexDirection: 'row', + borderTopWidth: 1, + borderBottomWidth: 1, + paddingHorizontal: 26, + paddingVertical: 18, + gap: 18, + }, + empty: { + paddingHorizontal: 18, + paddingVertical: 16, + borderRadius: 8, + marginHorizontal: 18, + marginTop: 10, + }, + itemContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginRight: 18, + }, +}) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index ab8f98309..c79dad4df 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -37,6 +37,7 @@ export const FeedsTabBar = observer( 'Following', "What's hot", ...store.me.savedFeeds.listOfPinnedFeedNames, + 'My feeds', ], [store.me.savedFeeds.listOfPinnedFeedNames], ) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 70816a306..9be4a4794 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -14,6 +14,7 @@ import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' import {FeedsTabBar} from '../com/pager/FeedsTabBar' import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager' import {FAB} from '../com/util/fab/FAB' +import {SavedFeeds} from 'view/com/feeds/SavedFeeds' import {useStores} from 'state/index' import {s} from 'lib/styles' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' @@ -115,14 +116,19 @@ export const HomeScreen = withAuthRequired( {store.me.savedFeeds.pinned.map((f, index) => { return ( ) })} + ) }), -- cgit 1.4.1 From 6bf8e7215784dafd04ae3639a28666eea77ed1bd Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 21:23:32 -0500 Subject: Usability improvements to feeds --- src/state/models/ui/saved-feeds.ts | 20 ++++----- src/view/com/feeds/CustomFeed.tsx | 77 ++++++++++++++++++++------------ src/view/com/feeds/SavedFeedItem.tsx | 38 ++++++++++------ src/view/com/feeds/SavedFeeds.tsx | 61 +++---------------------- src/view/com/pager/FeedsTabBar.web.tsx | 8 +--- src/view/com/pager/FeedsTabBarMobile.tsx | 4 +- src/view/screens/PinnedFeeds.tsx | 34 +++++++------- src/view/screens/ProfileCustomFeed.tsx | 18 ++++---- 8 files changed, 119 insertions(+), 141 deletions(-) (limited to 'src/state/models/ui/saved-feeds.ts') diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index bae98fc84..50bb1b871 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -61,22 +61,22 @@ export class SavedFeedsModel { return this.hasLoaded && !this.hasContent } - get numOfFeeds() { + get numFeeds() { return this.feeds.length } - get listOfFeedNames() { - return this.feeds.map(f => f.displayName) + get unpinned() { + return this.feeds.filter( + f => !this.pinned.find(p => p.data.uri === f.data.uri), + ) } - get listOfPinnedFeedNames() { - return this.pinned.map(f => f.displayName) + get feedNames() { + return this.feeds.map(f => f.displayName) } - get savedFeedsWithoutPinned() { - return this.feeds.filter( - f => !this.pinned.find(p => p.data.uri === f.data.uri), - ) + get pinnedFeedNames() { + return this.pinned.map(f => f.displayName) } togglePinnedFeed(feed: CustomFeedModel) { @@ -92,7 +92,7 @@ export class SavedFeedsModel { } reorderPinnedFeeds(temp: CustomFeedModel[]) { - this.pinned = temp + this.pinned = temp.filter(item => this.isPinned(item)) } isPinned(feed: CustomFeedModel) { diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index 5440a8e8f..5201ca848 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -1,16 +1,17 @@ import React from 'react' import { + Pressable, StyleProp, StyleSheet, View, ViewStyle, TouchableOpacity, } from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' import {UserAvatar} from '../util/UserAvatar' -import {Button} from '../util/forms/Button' import {observer} from 'mobx-react-lite' import {CustomFeedModel} from 'state/models/feeds/custom-feed' import {useNavigation} from '@react-navigation/native' @@ -18,6 +19,7 @@ import {NavigationProp} from 'lib/routes/types' import {useStores} from 'state/index' import {pluralize} from 'lib/strings/helpers' import {AtUri} from '@atproto/api' +import * as Toast from 'view/com/util/Toast' export const CustomFeed = observer( ({ @@ -37,6 +39,23 @@ export const CustomFeed = observer( const pal = usePalette('default') const navigation = useNavigation() + const onToggleSaved = React.useCallback(() => { + if (item.data.viewer?.saved) { + store.shell.openModal({ + name: 'confirm', + title: 'Remove from my feeds', + message: `Remove ${item.displayName} from my feeds?`, + onPressConfirm: () => { + store.me.savedFeeds.unsave(item) + Toast.show('Removed from my feeds') + }, + }) + } else { + store.me.savedFeeds.save(item) + Toast.show('Added to my feeds') + } + }, [store, item]) + return ( {showSaveBtn && ( - + ) @@ -275,6 +298,11 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingBottom: 16, }, + headerDetailsFooter: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, fakeSelector: { flexDirection: 'row', paddingHorizontal: isDesktopWeb ? 16 : 6, -- cgit 1.4.1 From fc9e28ca72ce498df8d0902c8e51d226affefd83 Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Tue, 23 May 2023 15:28:46 -0700 Subject: slight performance improvements --- src/state/models/log.ts | 16 ++++++++++++++++ src/state/models/ui/preferences.ts | 8 ++++++-- src/state/models/ui/saved-feeds.ts | 4 ++++ src/view/screens/SavedFeeds.tsx | 9 +++++++-- 4 files changed, 33 insertions(+), 4 deletions(-) (limited to 'src/state/models/ui/saved-feeds.ts') diff --git a/src/state/models/log.ts b/src/state/models/log.ts index d80617139..7c9c37c0d 100644 --- a/src/state/models/log.ts +++ b/src/state/models/log.ts @@ -27,6 +27,7 @@ function genId(): string { export class LogModel { entries: LogEntry[] = [] + timers = new Map() constructor() { makeAutoObservable(this) @@ -74,6 +75,21 @@ export class LogModel { ts: Date.now(), }) } + + time = (label = 'default') => { + this.timers.set(label, performance.now()) + } + + timeEnd = (label = 'default', warn = false) => { + const endTime = performance.now() + if (this.timers.has(label)) { + const elapsedTime = endTime - this.timers.get(label)! + console.log(`${label}: ${elapsedTime.toFixed(3)}ms`) + this.timers.delete(label) + } else { + warn && console.warn(`Timer with label '${label}' does not exist.`) + } + } } function detailsToStr(details?: any) { diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index c4b6da0f6..dcf6b9a7a 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -292,11 +292,15 @@ export class PreferencesModel { return res } + setFeeds(saved: string[], pinned: string[]) { + this.savedFeeds = saved + this.pinnedFeeds = pinned + } + async setSavedFeeds(saved: string[], pinned: string[]) { const oldSaved = this.savedFeeds const oldPinned = this.pinnedFeeds - this.savedFeeds = saved - this.pinnedFeeds = pinned + this.setFeeds(saved, pinned) try { await this.update((prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.find( diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 244e75898..979fddf49 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -47,6 +47,10 @@ export class SavedFeedsModel { return this.feeds.filter(f => !this.isPinned(f)) } + get all() { + return this.pinned.concat(this.unpinned) + } + get pinnedFeedNames() { return this.pinned.map(f => f.displayName) } diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 2f9165b37..e305e6305 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -99,7 +99,7 @@ export const SavedFeeds = withAuthRequired( /> item.data.uri} refreshing={savedFeeds.isRefreshing} refreshControl={ @@ -111,6 +111,11 @@ export const SavedFeeds = withAuthRequired( /> } renderItem={({item, drag}) => } + getItemLayout={(data, index) => ({ + length: 77, + offset: 77 * index, + index, + })} initialNumToRender={10} ListFooterComponent={renderListFooterComponent} ListEmptyComponent={renderListEmptyComponent} @@ -198,7 +203,7 @@ const ListItem = observer( />