import React from 'react' import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {track} from '#/lib/analytics/analytics' import {logger} from '#/logger' import { useOverwriteSavedFeedsMutation, usePreferencesQuery, useUpdateSavedFeedsMutation, } from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSetMinimalShellMode} from '#/state/shell' import {useAnalytics} from 'lib/analytics/analytics' import {useHaptics} from 'lib/haptics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {CommonNavigatorParams} from 'lib/routes/types' import {colors, s} from 'lib/styles' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {TextLink} from 'view/com/util/Link' import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' import {ViewHeader} from 'view/com/util/ViewHeader' import {CenteredView, ScrollView} from 'view/com/util/Views' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {atoms as a, useTheme} from '#/alf' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' const HITSLOP_TOP = { top: 20, left: 20, bottom: 5, right: 20, } const HITSLOP_BOTTOM = { top: 5, left: 20, bottom: 20, right: 20, } type Props = NativeStackScreenProps export function SavedFeeds({}: Props) { const pal = usePalette('default') const {_} = useLingui() const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() const setMinimalShellMode = useSetMinimalShellMode() const {data: preferences} = usePreferencesQuery() const { mutateAsync: overwriteSavedFeeds, variables: optimisticSavedFeedsResponse, reset: resetSaveFeedsMutationState, error: savedFeedsError, } = useOverwriteSavedFeedsMutation() /* * Use optimistic data if exists and no error, otherwise fallback to remote * data */ const currentFeeds = optimisticSavedFeedsResponse && !savedFeedsError ? optimisticSavedFeedsResponse : preferences?.savedFeeds || [] const pinnedFeeds = currentFeeds.filter(f => f.pinned) const unpinnedFeeds = currentFeeds.filter(f => !f.pinned) const noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0 const noFollowingFeed = currentFeeds.every(f => f.type !== 'timeline') && !noSavedFeedsOfAnyType useFocusEffect( React.useCallback(() => { screen('SavedFeeds') setMinimalShellMode(false) }, [screen, setMinimalShellMode]), ) return ( {noSavedFeedsOfAnyType && ( )} Pinned Feeds {preferences ? ( !pinnedFeeds.length ? ( You don't have any pinned feeds. ) : ( pinnedFeeds.map(f => ( )) ) ) : ( )} {noFollowingFeed && ( )} Saved Feeds {preferences ? ( !unpinnedFeeds.length ? ( You don't have any saved feeds. ) : ( unpinnedFeeds.map(f => ( )) ) ) : ( )} Feeds are custom algorithms that users build with a little coding expertise.{' '} {' '} for more information. ) } function ListItem({ feed, isPinned, currentFeeds, overwriteSavedFeeds, resetSaveFeedsMutationState, }: { feed: AppBskyActorDefs.SavedFeed isPinned: boolean currentFeeds: AppBskyActorDefs.SavedFeed[] overwriteSavedFeeds: ReturnType< typeof useOverwriteSavedFeedsMutation >['mutateAsync'] resetSaveFeedsMutationState: ReturnType< typeof useOverwriteSavedFeedsMutation >['reset'] preferences: UsePreferencesQueryResponse }) { const pal = usePalette('default') const {_} = useLingui() const playHaptic = useHaptics() const {isPending: isUpdatePending, mutateAsync: updateSavedFeeds} = useUpdateSavedFeedsMutation() const feedUri = feed.value const onTogglePinned = React.useCallback(async () => { playHaptic() try { resetSaveFeedsMutationState() await updateSavedFeeds([ { ...feed, pinned: !feed.pinned, }, ]) } catch (e) { Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {message: e}) } }, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState]) const onPressUp = React.useCallback(async () => { if (!isPinned) return const nextFeeds = currentFeeds.slice() const ids = currentFeeds.map(f => f.id) const index = ids.indexOf(feed.id) const nextIndex = index - 1 if (index === -1 || index === 0) return ;[nextFeeds[index], nextFeeds[nextIndex]] = [ nextFeeds[nextIndex], nextFeeds[index], ] try { await overwriteSavedFeeds(nextFeeds) track('CustomFeed:Reorder', { uri: feed.value, index: nextIndex, }) } catch (e) { Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to set pinned feed order', {message: e}) } }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) const onPressDown = React.useCallback(async () => { if (!isPinned) return const nextFeeds = currentFeeds.slice() const ids = currentFeeds.map(f => f.id) const index = ids.indexOf(feed.id) const nextIndex = index + 1 if (index === -1 || index >= nextFeeds.length - 1) return ;[nextFeeds[index], nextFeeds[nextIndex]] = [ nextFeeds[nextIndex], nextFeeds[index], ] try { await overwriteSavedFeeds(nextFeeds) track('CustomFeed:Reorder', { uri: feed.value, index: nextIndex, }) } catch (e) { Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to set pinned feed order', {message: e}) } }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) return ( {feed.type === 'timeline' ? ( ) : ( )} {isPinned ? ( <> ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, marginRight: 8, opacity: state.hovered || state.pressed || isUpdatePending ? 0.5 : 1, })}> ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, marginRight: 8, opacity: state.hovered || state.pressed || isUpdatePending ? 0.5 : 1, })}> ) : null} ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, opacity: state.hovered || state.focused || isUpdatePending ? 0.5 : 1, })}> ) } function FollowingFeedCard() { const t = useTheme() return ( Following ) } const styles = StyleSheet.create({ desktopContainer: { borderLeftWidth: 1, borderRightWidth: 1, // @ts-ignore only rendered on web minHeight: '100vh', }, empty: { paddingHorizontal: 20, paddingVertical: 20, borderRadius: 8, marginHorizontal: 10, marginTop: 10, }, title: { paddingHorizontal: 14, paddingTop: 20, paddingBottom: 10, borderBottomWidth: 1, }, itemContainer: { flexDirection: 'row', alignItems: 'center', borderBottomWidth: 1, }, noTopBorder: { borderTopWidth: 0, }, footerText: { paddingHorizontal: 26, paddingTop: 22, paddingBottom: 100, }, noBorder: { borderBottomWidth: 0, borderRightWidth: 0, borderLeftWidth: 0, borderTopWidth: 0, }, })