diff options
-rw-r--r-- | src/state/models/feeds/algo/algo-item.ts | 4 | ||||
-rw-r--r-- | src/state/models/feeds/algo/saved.ts | 42 | ||||
-rw-r--r-- | src/state/models/me.ts | 4 | ||||
-rw-r--r-- | src/view/com/algos/AlgoItem.tsx | 67 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | src/view/screens/CustomFeed.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 107 |
7 files changed, 170 insertions, 58 deletions
diff --git a/src/state/models/feeds/algo/algo-item.ts b/src/state/models/feeds/algo/algo-item.ts index 0eaeebd39..39bc760ac 100644 --- a/src/state/models/feeds/algo/algo-item.ts +++ b/src/state/models/feeds/algo/algo-item.ts @@ -153,4 +153,8 @@ export class AlgoItemModel { }) this.data = res.data.view } + + serialize() { + return JSON.stringify(this.data) + } } diff --git a/src/state/models/feeds/algo/saved.ts b/src/state/models/feeds/algo/saved.ts index 5d2f854dc..15859fe0c 100644 --- a/src/state/models/feeds/algo/saved.ts +++ b/src/state/models/feeds/algo/saved.ts @@ -4,6 +4,7 @@ import {RootStoreModel} from '../../root-store' import {bundleAsync} from 'lib/async/bundle' import {cleanError} from 'lib/strings/errors' import {AlgoItemModel} from './algo-item' +import {hasProp, isObj} from 'lib/type-guards' const PAGE_SIZE = 30 @@ -18,6 +19,7 @@ export class SavedFeedsModel { // data feeds: AlgoItemModel[] = [] + pinned: AlgoItemModel[] = [] constructor(public rootStore: RootStoreModel) { makeAutoObservable( @@ -29,6 +31,24 @@ 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 AlgoItemModel(this.rootStore, JSON.parse(s)), + ) + this.pinned = pinnedDeserialized + } + } + } + get hasContent() { return this.feeds.length > 0 } @@ -51,6 +71,28 @@ export class SavedFeedsModel { ) } + get savedFeedsWithoutPinned() { + return this.feeds.filter( + f => !this.pinned.find(p => p.data.uri === f.data.uri), + ) + } + + togglePinnedFeed(feed: AlgoItemModel) { + if (!this.isPinned(feed)) { + this.pinned.push(feed) + } else { + this.pinned = this.pinned.filter(f => f.data.uri !== feed.data.uri) + } + } + + reorderPinnedFeeds(temp: AlgoItemModel[]) { + this.pinned = temp + } + + isPinned(feed: AlgoItemModel) { + return this.pinned.find(f => f.data.uri === feed.data.uri) ? true : false + } + // public api // = diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 314e76b9c..68c89ac9b 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -69,6 +69,7 @@ export class MeModel { displayName: this.displayName, description: this.description, avatar: this.avatar, + savedFeeds: this.savedFeeds.serialize(), } } @@ -90,6 +91,9 @@ 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 diff --git a/src/view/com/algos/AlgoItem.tsx b/src/view/com/algos/AlgoItem.tsx index 4377e3583..f7d320530 100644 --- a/src/view/com/algos/AlgoItem.tsx +++ b/src/view/com/algos/AlgoItem.tsx @@ -19,7 +19,17 @@ import {useStores} from 'state/index' import {HeartIconSolid} from 'lib/icons' const AlgoItem = observer( - ({item, style}: {item: AlgoItemModel; style?: StyleProp<ViewStyle>}) => { + ({ + item, + style, + showBottom = true, + onLongPress, + }: { + item: AlgoItemModel + style?: StyleProp<ViewStyle> + showBottom?: boolean + onLongPress?: () => void + }) => { const store = useStores() const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() @@ -34,10 +44,11 @@ const AlgoItem = observer( rkey: item.data.uri, }) }} + onLongPress={onLongPress} key={item.data.uri}> <View style={[styles.headerContainer]}> <View style={[s.mr10]}> - <UserAvatar size={36} avatar={item.data.avatar} s /> + <UserAvatar size={36} avatar={item.data.avatar} /> </View> <View style={[styles.headerTextContainer]}> <Text style={[pal.text, s.bold]}> @@ -49,37 +60,39 @@ const AlgoItem = observer( </View> </View> - <View style={styles.bottomContainer}> - <View style={styles.likedByContainer}> - {/* <View style={styles.likedByAvatars}> + {showBottom ? ( + <View style={styles.bottomContainer}> + <View style={styles.likedByContainer}> + {/* <View style={styles.likedByAvatars}> <UserAvatar size={24} avatar={item.data.avatar} /> <UserAvatar size={24} avatar={item.data.avatar} /> <UserAvatar size={24} avatar={item.data.avatar} /> </View> */} - <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} /> - <Text style={[pal.text, pal.textLight]}> - {item.data.likeCount && item.data.likeCount > 1 - ? `Liked by ${item.data.likeCount} others` - : 'Be the first to like this'} - </Text> - </View> - <View> - <Button - type="inverted" - onPress={() => { - if (item.data.viewer?.saved) { - item.unsave() - store.me.savedFeeds.removeFeed(item.data.uri) - } else { - item.save() - store.me.savedFeeds.addFeed(item) - } - }} - label={item.data.viewer?.saved ? 'Unsave' : 'Save'} - /> + <HeartIconSolid size={16} style={[s.mr2, {color: colors.red3}]} /> + <Text style={[pal.text, pal.textLight]}> + {item.data.likeCount && item.data.likeCount > 1 + ? `Liked by ${item.data.likeCount} others` + : 'Be the first to like this'} + </Text> + </View> + <View> + <Button + type="inverted" + onPress={() => { + if (item.data.viewer?.saved) { + item.unsave() + store.me.savedFeeds.removeFeed(item.data.uri) + } else { + item.save() + store.me.savedFeeds.addFeed(item) + } + }} + label={item.data.viewer?.saved ? 'Unsave' : 'Save'} + /> + </View> </View> - </View> + ) : null} </TouchableOpacity> ) }, diff --git a/src/view/index.ts b/src/view/index.ts index dd8a585d6..253735e81 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -75,6 +75,7 @@ import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' +import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' export function setup() { library.add( @@ -149,6 +150,7 @@ export function setup() { faUserXmark, faTicket, faTrashCan, + faThumbtack, faX, faXmark, faPlay, diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 05c8d38f1..97dd1cf81 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -19,7 +19,7 @@ import {Text} from 'view/com/util/text/Text' type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'> export const CustomFeed = withAuthRequired( - observer(({route, navigation}: Props) => { + observer(({route}: Props) => { const rootStore = useStores() const {rkey, name} = route.params const currentFeed = useCustomFeed(rkey) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 7b04a6474..65ffdb233 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -3,8 +3,9 @@ import { RefreshControl, StyleSheet, View, - FlatList, ActivityIndicator, + FlatList, + TouchableOpacity, } from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' @@ -13,28 +14,28 @@ import {usePalette} from 'lib/hooks/usePalette' import {CommonNavigatorParams} from 'lib/routes/types' import {observer} from 'mobx-react-lite' import {useStores} from 'state/index' -import {SavedFeedsModel} from 'state/models/feeds/algo/saved' import AlgoItem from 'view/com/algos/AlgoItem' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from 'view/com/util/ViewHeader' import {CenteredView} from 'view/com/util/Views' import {Text} from 'view/com/util/text/Text' import {isDesktopWeb} from 'platform/detection' -import {s} from 'lib/styles' +import {colors, s} from 'lib/styles' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {AlgoItemModel} from 'state/models/feeds/algo/algo-item' +import {SavedFeedsModel} from 'state/models/feeds/algo/saved' type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> export const SavedFeeds = withAuthRequired( observer(({}: Props) => { + // hooks for global items const pal = usePalette('default') const rootStore = useStores() const {screen} = useAnalytics() - const savedFeeds = useMemo( - () => new SavedFeedsModel(rootStore), - [rootStore], - ) - + // hooks for local + const savedFeeds = useMemo(() => rootStore.me.savedFeeds, [rootStore]) useFocusEffect( useCallback(() => { screen('SavedFeeds') @@ -42,14 +43,38 @@ export const SavedFeeds = withAuthRequired( savedFeeds.refresh() }, [screen, rootStore, savedFeeds]), ) + const _ListEmptyComponent = () => { + return ( + <View + style={[ + pal.border, + !isDesktopWeb && s.flex1, + pal.viewLight, + styles.empty, + ]}> + <Text type="lg" style={[pal.text]}> + You don't have any saved feeds. To save a feed, click the save + button when a custom feed or algorithm shows up. + </Text> + </View> + ) + } + const _ListFooterComponent = () => { + return ( + <View style={styles.footer}> + {savedFeeds.isLoading && <ActivityIndicator />} + </View> + ) + } return ( <CenteredView style={[s.flex1]}> - <ViewHeader title="Custom Algorithms" showOnDesktop /> + <ViewHeader title="Saved Feeds" showOnDesktop /> <FlatList style={[!isDesktopWeb && s.flex1]} data={savedFeeds.feeds} keyExtractor={item => item.data.uri} + refreshing={savedFeeds.isRefreshing} refreshControl={ <RefreshControl refreshing={savedFeeds.isRefreshing} @@ -58,28 +83,12 @@ export const SavedFeeds = withAuthRequired( titleColor={pal.colors.text} /> } - onEndReached={() => savedFeeds.loadMore()} - renderItem={({item}) => <AlgoItem key={item.data.uri} item={item} />} - initialNumToRender={15} - ListFooterComponent={() => ( - <View style={styles.footer}> - {savedFeeds.isLoading && <ActivityIndicator />} - </View> - )} - ListEmptyComponent={() => ( - <View - style={[ - pal.border, - !isDesktopWeb && s.flex1, - pal.viewLight, - styles.empty, - ]}> - <Text type="lg" style={[pal.text]}> - You don't have any saved feeds. To save a feed, click the save - button when a custom feed or algorithm shows up. - </Text> - </View> + renderItem={({item}) => ( + <SavedFeedItem item={item} savedFeeds={savedFeeds} /> )} + initialNumToRender={10} + ListFooterComponent={_ListFooterComponent} + ListEmptyComponent={_ListEmptyComponent} extraData={savedFeeds.isLoading} // @ts-ignore our .web version only -prf desktopFixedHeight @@ -89,6 +98,36 @@ export const SavedFeeds = withAuthRequired( }), ) +const SavedFeedItem = observer( + ({item, savedFeeds}: {item: AlgoItemModel; savedFeeds: SavedFeedsModel}) => { + const isPinned = savedFeeds.isPinned(item) + + return ( + <View style={styles.itemContainer}> + <AlgoItem + key={item.data.uri} + item={item} + showBottom={false} + style={styles.item} + /> + <TouchableOpacity + accessibilityRole="button" + onPress={() => { + savedFeeds.togglePinnedFeed(item) + console.log('pinned', savedFeeds.pinned) + console.log('isPinned', savedFeeds.isPinned(item)) + }}> + <FontAwesomeIcon + icon="thumb-tack" + size={20} + color={isPinned ? colors.blue3 : colors.gray3} + /> + </TouchableOpacity> + </View> + ) + }, +) + const styles = StyleSheet.create({ footer: { paddingVertical: 20, @@ -100,4 +139,12 @@ const styles = StyleSheet.create({ marginHorizontal: 24, marginTop: 10, }, + itemContainer: { + flexDirection: 'row', + alignItems: 'center', + marginRight: 18, + }, + item: { + borderTopWidth: 0, + }, }) |