diff options
Diffstat (limited to 'src/view/screens/SavedFeeds.tsx')
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx new file mode 100644 index 000000000..103b18c70 --- /dev/null +++ b/src/view/screens/SavedFeeds.tsx @@ -0,0 +1,293 @@ +import React, {useCallback, useMemo} from 'react' +import { + RefreshControl, + StyleSheet, + View, + ActivityIndicator, + Pressable, + TouchableOpacity, +} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {useAnalytics} from 'lib/analytics' +import {usePalette} from 'lib/hooks/usePalette' +import {CommonNavigatorParams} from 'lib/routes/types' +import {observer} from 'mobx-react-lite' +import {useStores} from 'state/index' +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, isWeb} from 'platform/detection' +import {s, colors} from 'lib/styles' +import DraggableFlatList, { + ShadowDecorator, + ScaleDecorator, +} from 'react-native-draggable-flatlist' +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' +import {Haptics} from 'lib/haptics' +import {Link, TextLink} from 'view/com/util/Link' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> + +export const SavedFeeds = withAuthRequired( + observer(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const {screen} = useAnalytics() + + const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) + useFocusEffect( + useCallback(() => { + screen('SavedFeeds') + store.shell.setMinimalShellMode(false) + savedFeeds.refresh() + }, [screen, store, savedFeeds]), + ) + + const renderListEmptyComponent = useCallback(() => { + 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. + </Text> + </View> + ) + }, [pal]) + + const renderListFooterComponent = useCallback(() => { + return ( + <> + <View style={[styles.footerLinks, pal.border]}> + <Link style={styles.footerLink} href="/search/feeds"> + <FontAwesomeIcon + icon="search" + size={18} + color={pal.colors.icon} + /> + <Text type="lg-medium" style={pal.textLight}> + Discover new feeds + </Text> + </Link> + </View> + <View style={styles.footerText}> + <Text type="sm" style={pal.textLight}> + Feeds are custom algorithms that users build with a little coding + expertise.{' '} + <TextLink + type="sm" + style={pal.link} + href="https://github.com/bluesky-social/feed-generator" + text="See this guide" + />{' '} + for more information. + </Text> + </View> + {savedFeeds.isLoading && <ActivityIndicator />} + </> + ) + }, [pal, savedFeeds.isLoading]) + + 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 ( + <CenteredView + style={[ + s.hContentRegion, + pal.border, + isDesktopWeb && styles.desktopContainer, + ]}> + <ViewHeader + title="Edit My Feeds" + showOnDesktop + showBorder={!isDesktopWeb} + /> + <DraggableFlatList + containerStyle={[!isDesktopWeb && s.flex1]} + data={savedFeeds.all} + keyExtractor={item => item.data.uri} + refreshing={savedFeeds.isRefreshing} + refreshControl={ + <RefreshControl + refreshing={savedFeeds.isRefreshing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + renderItem={({item, drag}) => <ListItem item={item} drag={drag} />} + getItemLayout={(data, index) => ({ + length: 77, + offset: 77 * index, + index, + })} + initialNumToRender={10} + ListFooterComponent={renderListFooterComponent} + ListEmptyComponent={renderListEmptyComponent} + extraData={savedFeeds.isLoading} + onDragEnd={onDragEnd} + /> + </CenteredView> + ) + }), +) + +const ListItem = observer( + ({item, drag}: {item: CustomFeedModel; drag: () => void}) => { + const pal = usePalette('default') + const store = useStores() + const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) + const isPinned = savedFeeds.isPinned(item) + + const onTogglePinned = useCallback(() => { + Haptics.default() + 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 ( + <ScaleDecorator> + <ShadowDecorator> + <Pressable + accessibilityRole="button" + onLongPress={isPinned ? drag : undefined} + delayLongPress={200} + style={[styles.itemContainer, pal.border]}> + {isPinned && isWeb ? ( + <View style={styles.webArrowButtonsContainer}> + <TouchableOpacity + accessibilityRole="button" + onPress={onPressUp}> + <FontAwesomeIcon + icon="arrow-up" + size={12} + style={[pal.text, styles.webArrowUpButton]} + /> + </TouchableOpacity> + <TouchableOpacity + accessibilityRole="button" + onPress={onPressDown}> + <FontAwesomeIcon + icon="arrow-down" + size={12} + style={[pal.text]} + /> + </TouchableOpacity> + </View> + ) : isPinned ? ( + <FontAwesomeIcon + icon="bars" + size={20} + color={pal.colors.text} + style={s.ml20} + /> + ) : null} + <CustomFeed + key={item.data.uri} + item={item} + showSaveBtn + style={styles.noBorder} + /> + <TouchableOpacity + accessibilityRole="button" + hitSlop={10} + onPress={onTogglePinned}> + <FontAwesomeIcon + icon="thumb-tack" + size={20} + color={isPinned ? colors.blue3 : pal.colors.icon} + /> + </TouchableOpacity> + </Pressable> + </ShadowDecorator> + </ScaleDecorator> + ) + }, +) + +const styles = StyleSheet.create({ + desktopContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + minHeight: '100vh', + }, + empty: { + paddingHorizontal: 20, + paddingVertical: 20, + borderRadius: 16, + marginHorizontal: 24, + marginTop: 10, + }, + itemContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + borderBottomWidth: 1, + paddingRight: 16, + }, + webArrowButtonsContainer: { + paddingLeft: 16, + flexDirection: 'column', + justifyContent: 'space-around', + }, + webArrowUpButton: { + marginBottom: 10, + }, + noBorder: { + borderTopWidth: 0, + }, + footerText: { + paddingHorizontal: 26, + paddingTop: 22, + paddingBottom: 100, + }, + footerLinks: { + borderBottomWidth: 1, + borderTopWidth: 0, + }, + footerLink: { + flexDirection: 'row', + paddingHorizontal: 26, + paddingVertical: 18, + gap: 18, + }, +}) |