From 3d09008bfde3352212ab69105063150a175b8d5d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 May 2023 14:03:08 -0500 Subject: Reorganize custom feeds in the view layer --- src/view/com/feeds/CustomFeed.tsx | 153 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/view/com/feeds/CustomFeed.tsx (limited to 'src/view/com/feeds/CustomFeed.tsx') diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx new file mode 100644 index 000000000..2137081f3 --- /dev/null +++ b/src/view/com/feeds/CustomFeed.tsx @@ -0,0 +1,153 @@ +import React from 'react' +import { + StyleProp, + StyleSheet, + View, + ViewStyle, + TouchableOpacity, +} from 'react-native' +import {Text} from '../util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {colors, 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 {useFocusEffect, useNavigation} from '@react-navigation/native' +import {NavigationProp} from 'lib/routes/types' +import {useStores} from 'state/index' +import {HeartIconSolid} from 'lib/icons' +import {pluralize} from 'lib/strings/helpers' +import {AtUri} from '@atproto/api' +import {isWeb} from 'platform/detection' + +const CustomFeed = observer( + ({ + item, + style, + showBottom = true, + reloadOnFocus = false, + }: { + item: CustomFeedModel + style?: StyleProp + showBottom?: boolean + reloadOnFocus?: boolean + }) => { + const store = useStores() + const pal = usePalette('default') + const navigation = useNavigation() + + // TODO: this is pretty hacky, but it works for now + // causes issues on web + useFocusEffect(() => { + if (reloadOnFocus && !isWeb) { + item.reload() + } + }) + + return ( + { + 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`, + }) + }} + key={item.data.uri}> + + + + + + + {item.data.displayName ?? 'Feed name'} + + + {item.data.description ?? + "Explore our Feed for the latest updates and insights! Dive into a world of intriguing articles, trending news, and exciting stories that cover a wide range of topics. From technology breakthroughs to lifestyle tips, there's something here for everyone. Stay informed and get inspired with us. Join the conversation now!"} + + + + + {showBottom ? ( + + + {/* + + + + */} + + + + {item.data.likeCount && item.data.likeCount > 0 + ? `Liked by ${item.data.likeCount} ${pluralize( + item.data.likeCount, + 'other', + )}` + : 'Be the first to like this'} + + + + + + + )} + + + + + Feed + + + + + ) + }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + + return ( + + + + + ) + }), +) + +/* + + + + + + + + @{currentFeed?.data.creator.handle} + + + + {currentFeed?.data.description} + + + + + + */ + +const styles = StyleSheet.create({ + headerBtns: { + flexDirection: 'row', + gap: 8, + }, + header: { + flexDirection: 'row', + gap: 12, + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 16, + borderTopWidth: 1, + }, + headerDetails: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + fakeSelector: { + flexDirection: 'row', + paddingHorizontal: isDesktopWeb ? 16 : 6, + }, + fakeSelectorItem: { + paddingHorizontal: 12, + paddingBottom: 8, + borderBottomWidth: 3, + }, + liked: { + color: colors.red3, + }, + + /* headerContainer: { + alignItems: 'center', + justifyContent: 'center', + gap: 8, + marginBottom: 12, + }, + header: { + alignItems: 'center', + gap: 4, + }, + avatarContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + buttonsContainer: { + flexDirection: 'row', + gap: 8, + }, + saveButton: { + minWidth: 100, + alignItems: 'center', + }, + liked: { + color: colors.red3, + }, + notLiked: { + color: colors.gray3, + }, + likeButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 4, + paddingHorizontal: 8, + borderRadius: 24, + gap: 4, + },*/ +}) diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 01f27bae1..7c3ed831c 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -87,7 +87,7 @@ export const ProfileListScreen = withAuthRequired( return }, []) - const renderHeaderBtn = React.useCallback(() => { + const renderHeaderBtns = React.useCallback(() => { return ( {list?.isOwner && ( @@ -148,7 +148,7 @@ export const ProfileListScreen = withAuthRequired( pal.border, ]} testID="moderationMutelistsScreen"> - + { - navigation.navigate('CustomFeed', { + navigation.navigate('ProfileCustomFeed', { name: item.data.creator.did, rkey: new AtUri(item.data.uri).rkey, - displayName: - item.data.displayName ?? - `${item.data.creator.displayName}'s feed`, }) }} style={styles.pinnedItem}> -- 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/view/com/feeds/CustomFeed.tsx') 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/view/com/feeds/CustomFeed.tsx') 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 && ( - + + + )} + + + + + Feed + + + + + ) + }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + + return ( + + + + + ) + }), +) + +const styles = StyleSheet.create({ + headerBtns: { + flexDirection: 'row', + gap: 8, + }, + header: { + flexDirection: 'row', + gap: 12, + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 16, + borderTopWidth: 1, + }, + headerDetails: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + fakeSelector: { + flexDirection: 'row', + paddingHorizontal: isDesktopWeb ? 16 : 6, + }, + fakeSelectorItem: { + paddingHorizontal: 12, + paddingBottom: 8, + borderBottomWidth: 3, + }, + liked: { + color: colors.red3, + }, +}) diff --git a/src/view/screens/CustomFeedLikedBy.tsx b/src/view/screens/CustomFeedLikedBy.tsx new file mode 100644 index 000000000..49d0d0482 --- /dev/null +++ b/src/view/screens/CustomFeedLikedBy.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import {View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from '../com/util/ViewHeader' +import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' + +type Props = NativeStackScreenProps +export const CustomFeedLikedByScreen = withAuthRequired(({route}: Props) => { + const store = useStores() + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + }, [store]), + ) + + return ( + + + + + ) +}) diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx deleted file mode 100644 index 681798308..000000000 --- a/src/view/screens/ProfileCustomFeed.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import React, {useMemo, useRef} from 'react' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {usePalette} from 'lib/hooks/usePalette' -import {HeartIcon, HeartIconSolid} from 'lib/icons' -import {CommonNavigatorParams} from 'lib/routes/types' -import {makeRecordUri} from 'lib/strings/url-helpers' -import {colors, s} from 'lib/styles' -import {observer} from 'mobx-react-lite' -import {FlatList, StyleSheet, View} from 'react-native' -import {useStores} from 'state/index' -import {PostsFeedModel} from 'state/models/feeds/posts' -import {useCustomFeed} from 'lib/hooks/useCustomFeed' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {Feed} from 'view/com/posts/Feed' -import {pluralize} from 'lib/strings/helpers' -import {TextLink} from 'view/com/util/Link' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {Button} from 'view/com/util/forms/Button' -import {Text} from 'view/com/util/text/Text' -import * as Toast from 'view/com/util/Toast' -import {isDesktopWeb} from 'platform/detection' - -type Props = NativeStackScreenProps -export const ProfileCustomFeed = withAuthRequired( - observer(({route}: Props) => { - const store = useStores() - const pal = usePalette('default') - const {rkey, name} = route.params - const uri = useMemo( - () => makeRecordUri(name, 'app.bsky.feed.generator', rkey), - [rkey, name], - ) - const scrollElRef = useRef(null) - const currentFeed = useCustomFeed(uri) - const algoFeed: PostsFeedModel = useMemo(() => { - const feed = new PostsFeedModel(store, 'custom', { - feed: uri, - }) - feed.setup() - return feed - }, [store, uri]) - - const onToggleSaved = React.useCallback(async () => { - try { - if (currentFeed?.isSaved) { - await currentFeed?.unsave() - } else { - await currentFeed?.save() - } - } catch (err) { - Toast.show( - 'There was an an issue updating your feeds, please check your internet connection and try again.', - ) - store.log.error('Failed up update feeds', {err}) - } - }, [store, currentFeed]) - - const onToggleLiked = React.useCallback(async () => { - try { - if (currentFeed?.isLiked) { - await currentFeed?.unlike() - } else { - await currentFeed?.like() - } - } catch (err) { - Toast.show( - 'There was an an issue contacting the server, please check your internet connection and try again.', - ) - store.log.error('Failed up toggle like', {err}) - } - }, [store, currentFeed]) - - const renderHeaderBtns = React.useCallback(() => { - return ( - - - - - )} - - - - - Feed - - - - - ) - }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) - - return ( - - - - - ) - }), -) - -/* - - - - - - - - @{currentFeed?.data.creator.handle} - - - - {currentFeed?.data.description} - - - - - - */ - -const styles = StyleSheet.create({ - headerBtns: { - flexDirection: 'row', - gap: 8, - }, - header: { - flexDirection: 'row', - gap: 12, - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 16, - borderTopWidth: 1, - }, - headerDetails: { - paddingHorizontal: 16, - paddingBottom: 16, - }, - fakeSelector: { - flexDirection: 'row', - paddingHorizontal: isDesktopWeb ? 16 : 6, - }, - fakeSelectorItem: { - paddingHorizontal: 12, - paddingBottom: 8, - borderBottomWidth: 3, - }, - liked: { - color: colors.red3, - }, - - /* headerContainer: { - alignItems: 'center', - justifyContent: 'center', - gap: 8, - marginBottom: 12, - }, - header: { - alignItems: 'center', - gap: 4, - }, - avatarContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - buttonsContainer: { - flexDirection: 'row', - gap: 8, - }, - saveButton: { - minWidth: 100, - alignItems: 'center', - }, - liked: { - color: colors.red3, - }, - notLiked: { - color: colors.gray3, - }, - likeButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 4, - paddingHorizontal: 8, - borderRadius: 24, - gap: 4, - },*/ -}) -- cgit 1.4.1 From 7691fe4f481bf08c711cf92da91b2c204d121a7f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 May 2023 11:51:25 -0500 Subject: Store/sync pinned feeds on the server --- src/state/models/me.ts | 7 +- src/state/models/ui/preferences.ts | 58 +++++++++- src/state/models/ui/saved-feeds.ts | 226 +++++++++++++------------------------ src/view/com/feeds/CustomFeed.tsx | 22 +++- src/view/com/feeds/SavedFeeds.tsx | 6 +- src/view/com/pager/Pager.web.tsx | 99 ++++++++-------- src/view/screens/Home.tsx | 16 ++- src/view/screens/SavedFeeds.tsx | 84 +++++++++----- 8 files changed, 278 insertions(+), 240 deletions(-) (limited to 'src/view/com/feeds/CustomFeed.tsx') diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 9b2b96832..815044857 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -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() diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 1471420fc..05a1eb128 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -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)) + } } diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index d68664c2d..f500aef2e 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -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) } - }, - ) - - 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)) - } + runInAction(() => { + this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f)) + }) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) - 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) } } } diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index 911c33da4..d4e843b67 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -39,20 +39,30 @@ export const CustomFeed = observer( const pal = usePalette('default') const navigation = useNavigation() - const onToggleSaved = React.useCallback(() => { + const onToggleSaved = React.useCallback(async () => { 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') + onPressConfirm: async () => { + try { + await store.me.savedFeeds.unsave(item) + Toast.show('Removed from my feeds') + } catch (e) { + Toast.show('There was an issue contacting your server') + store.log.error('Failed to unsave feed', {e}) + } }, }) } else { - store.me.savedFeeds.save(item) - Toast.show('Added to my feeds') + try { + await store.me.savedFeeds.save(item) + Toast.show('Added to my feeds') + } catch (e) { + Toast.show('There was an issue contacting your server') + store.log.error('Failed to save feed', {e}) + } } }, [store, item]) diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index 7135fdf0a..1cb109a43 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -29,6 +29,10 @@ export const SavedFeeds = observer( } }, [store, isPageFocused]) + const onRefresh = useCallback(() => { + store.me.savedFeeds.refresh() + }, [store]) + const renderListEmptyComponent = useCallback(() => { return ( store.me.savedFeeds.refresh()} + onRefresh={onRefresh} tintColor={pal.colors.text} titleColor={pal.colors.text} progressViewOffset={headerOffset} diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index 107497f6f..7be2b11ec 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -1,12 +1,9 @@ import React from 'react' -import {Animated, View} from 'react-native' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {View} from 'react-native' import {s} from 'lib/styles' export interface RenderTabBarFnProps { selectedPage: number - position: Animated.Value - offset: Animated.Value onSelect?: (index: number) => void } export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element @@ -17,53 +14,51 @@ interface Props { renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void } -export const Pager = ({ - children, - tabBarPosition = 'top', - initialPage = 0, - renderTabBar, - onPageSelected, -}: React.PropsWithChildren) => { - const [selectedPage, setSelectedPage] = React.useState(initialPage) - const position = useAnimatedValue(0) - const offset = useAnimatedValue(0) +export const Pager = React.forwardRef( + ( + { + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, + }: React.PropsWithChildren, + ref, + ) => { + const [selectedPage, setSelectedPage] = React.useState(initialPage) - const onTabBarSelect = React.useCallback( - (index: number) => { - setSelectedPage(index) - onPageSelected?.(index) - Animated.timing(position, { - toValue: index, - duration: 200, - useNativeDriver: true, - }).start() - }, - [setSelectedPage, onPageSelected, position], - ) + React.useImperativeHandle(ref, () => ({ + setPage: (index: number) => setSelectedPage(index), + })) - return ( - - {tabBarPosition === 'top' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - {React.Children.map(children, (child, i) => ( - - {child} - - ))} - {tabBarPosition === 'bottom' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - - ) -} + const onTabBarSelect = React.useCallback( + (index: number) => { + setSelectedPage(index) + onPageSelected?.(index) + }, + [setSelectedPage, onPageSelected], + ) + + return ( + + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + {React.Children.map(children, (child, i) => ( + + {child} + + ))} + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + + ) + }, +) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 644182126..54cec3b31 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -4,6 +4,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' +import isEqual from 'lodash.isequal' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {PostsFeedModel} from 'state/models/feeds/posts' import {withAuthRequired} from 'view/com/auth/withAuthRequired' @@ -44,15 +45,26 @@ export const HomeScreen = withAuthRequired( }, [store]) React.useEffect(() => { + const {pinned} = store.me.savedFeeds + if ( + isEqual( + pinned.map(p => p.uri), + customFeeds.map(f => (f.params as GetCustomFeed.QueryParams).feed), + ) + ) { + // no changes + return + } + const feeds = [] - for (const feed of store.me.savedFeeds.pinned) { + for (const feed of pinned) { const model = new PostsFeedModel(store, 'custom', {feed: feed.uri}) model.setup() feeds.push(model) } pagerRef.current?.setPage(0) setCustomFeeds(feeds) - }, [store, store.me.savedFeeds.pinned, setCustomFeeds]) + }, [store, store.me.savedFeeds.pinned, customFeeds, setCustomFeeds]) React.useEffect(() => { // refresh whats hot when lang preferences change diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index c2723f694..613e42fbf 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -27,26 +27,26 @@ import DraggableFlatList, { import {CustomFeed} from 'view/com/feeds/CustomFeed' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CustomFeedModel} from 'state/models/feeds/custom-feed' +import * as Toast from 'view/com/util/Toast' type Props = NativeStackScreenProps export const SavedFeeds = withAuthRequired( observer(({}: Props) => { - // hooks for global items const pal = usePalette('default') - const rootStore = useStores() + const store = useStores() const {screen} = useAnalytics() - // hooks for local - const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore]) + const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) useFocusEffect( useCallback(() => { screen('SavedFeeds') - rootStore.shell.setMinimalShellMode(false) + store.shell.setMinimalShellMode(false) savedFeeds.refresh() - }, [screen, rootStore, savedFeeds]), + }, [screen, store, savedFeeds]), ) - const _ListEmptyComponent = () => { + + const renderListEmptyComponent = useCallback(() => { return ( - You don't have any pinned feeds. To pin a feed, go back to the Saved - Feeds screen and click the pin icon! + You don't have any saved feeds. ) - } - const _ListFooterComponent = () => { + }, [pal]) + + const renderListFooterComponent = useCallback(() => { return ( {savedFeeds.isLoading && } ) - } + }, [savedFeeds]) + + const onRefresh = useCallback(() => savedFeeds.refresh(), [savedFeeds]) + + const onDragEnd = useCallback( + async ({data}) => { + try { + await savedFeeds.reorderPinnedFeeds(data) + } catch (e) { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to save pinned feed order', {e}) + } + }, + [savedFeeds, store], + ) return ( savedFeeds.refresh()} + onRefresh={onRefresh} tintColor={pal.colors.text} titleColor={pal.colors.text} /> } renderItem={({item, drag}) => } initialNumToRender={10} - ListFooterComponent={_ListFooterComponent} - ListEmptyComponent={_ListEmptyComponent} + ListFooterComponent={renderListFooterComponent} + ListEmptyComponent={renderListEmptyComponent} extraData={savedFeeds.isLoading} - onDragEnd={({data}) => savedFeeds.reorderPinnedFeeds(data)} + onDragEnd={onDragEnd} /> ) @@ -110,13 +124,35 @@ export const SavedFeeds = withAuthRequired( const ListItem = observer( ({item, drag}: {item: CustomFeedModel; drag: () => void}) => { const pal = usePalette('default') - const rootStore = useStores() - const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore]) + const store = useStores() + const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) const isPinned = savedFeeds.isPinned(item) + const onTogglePinned = useCallback( - () => savedFeeds.togglePinnedFeed(item), - [savedFeeds, item], + () => + savedFeeds.togglePinnedFeed(item).catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to toggle pinned feed', {e}) + }), + [savedFeeds, item, store], + ) + const onPressUp = useCallback( + () => + savedFeeds.movePinnedFeed(item, 'up').catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to set pinned feed order', {e}) + }), + [store, savedFeeds, item], ) + const onPressDown = useCallback( + () => + savedFeeds.movePinnedFeed(item, 'down').catch(e => { + Toast.show('There was an issue contacting the server') + store.log.error('Failed to set pinned feed order', {e}) + }), + [store, savedFeeds, item], + ) + return ( @@ -128,9 +164,7 @@ const ListItem = observer( { - savedFeeds.movePinnedItem(item, 'up') - }}> + onPress={onPressUp}> { - savedFeeds.movePinnedItem(item, 'down') - }}> + onPress={onPressDown}> 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/view/com/feeds/CustomFeed.tsx') 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 881be748d52f0bbf19543ac8a08d81b46bb8af5d Mon Sep 17 00:00:00 2001 From: Ansh Nanda Date: Thu, 25 May 2023 13:54:43 -0700 Subject: increase save custom feed btn hitslop --- src/view/com/feeds/CustomFeed.tsx | 1 + 1 file changed, 1 insertion(+) (limited to 'src/view/com/feeds/CustomFeed.tsx') diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index 9a71eb846..748b89c02 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -98,6 +98,7 @@ export const CustomFeed = observer( } accessibilityHint="" onPress={onToggleSaved} + hitSlop={15} style={styles.btn}> {item.isSaved ? (