diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 203 |
1 files changed, 114 insertions, 89 deletions
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 66bbd9b8a..2334abb5d 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -1,22 +1,23 @@ import React from 'react' import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' +import Animated, {LinearTransition} from 'react-native-reanimated' 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 {useNavigation} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useHaptics} from '#/lib/haptics' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {colors, s} from '#/lib/styles' import {logger} from '#/logger' import { useOverwriteSavedFeedsMutation, usePreferencesQuery, - useUpdateSavedFeedsMutation, } from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSetMinimalShellMode} from '#/state/shell' @@ -29,43 +30,40 @@ 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 {Button, ButtonIcon, ButtonText} from '#/components/Button' 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, -} +import {Loader} from '#/components/Loader' type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> export function SavedFeeds({}: Props) { + const {data: preferences} = usePreferencesQuery() + if (!preferences) { + return <View /> + } + return <SavedFeedsInner preferences={preferences} /> +} + +function SavedFeedsInner({ + preferences, +}: { + preferences: UsePreferencesQueryResponse +}) { const pal = usePalette('default') const {_} = useLingui() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries() const setMinimalShellMode = useSetMinimalShellMode() - const {data: preferences} = usePreferencesQuery() - const { - mutateAsync: overwriteSavedFeeds, - variables: optimisticSavedFeedsResponse, - reset: resetSaveFeedsMutationState, - error: savedFeedsError, - } = useOverwriteSavedFeedsMutation() + const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} = + useOverwriteSavedFeedsMutation() + const navigation = useNavigation<NavigationProp>() /* * Use optimistic data if exists and no error, otherwise fallback to remote * data */ - const currentFeeds = - optimisticSavedFeedsResponse && !savedFeedsError - ? optimisticSavedFeedsResponse - : preferences?.savedFeeds || [] + const [currentFeeds, setCurrentFeeds] = React.useState( + () => preferences.savedFeeds || [], + ) + const hasUnsavedChanges = currentFeeds !== preferences.savedFeeds const pinnedFeeds = currentFeeds.filter(f => f.pinned) const unpinnedFeeds = currentFeeds.filter(f => !f.pinned) const noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0 @@ -78,6 +76,35 @@ export function SavedFeeds({}: Props) { }, [setMinimalShellMode]), ) + const onSaveChanges = React.useCallback(async () => { + try { + await overwriteSavedFeeds(currentFeeds) + Toast.show(_(msg`Feeds updated!`)) + navigation.navigate('Feeds') + } catch (e) { + Toast.show(_(msg`There was an issue contacting the server`), 'xmark') + logger.error('Failed to toggle pinned feed', {message: e}) + } + }, [_, overwriteSavedFeeds, currentFeeds, navigation]) + + const renderHeaderBtn = React.useCallback(() => { + return ( + <Button + size="small" + variant={hasUnsavedChanges ? 'solid' : 'solid'} + color={hasUnsavedChanges ? 'primary' : 'secondary'} + onPress={onSaveChanges} + label={_(msg`Save changes`)} + disabled={isOverwritePending || !hasUnsavedChanges} + style={[isDesktop && a.mt_sm]}> + <ButtonText style={[isDesktop && a.text_md]}> + {isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>} + </ButtonText> + {isOverwritePending && <ButtonIcon icon={Loader} />} + </Button> + ) + }, [_, isDesktop, onSaveChanges, hasUnsavedChanges, isOverwritePending]) + return ( <CenteredView style={[ @@ -85,7 +112,12 @@ export function SavedFeeds({}: Props) { pal.border, isTabletOrDesktop && styles.desktopContainer, ]}> - <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder /> + <ViewHeader + title={_(msg`Edit My Feeds`)} + showOnDesktop + showBorder + renderButton={renderHeaderBtn} + /> <ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}> {noSavedFeedsOfAnyType && ( <View @@ -119,9 +151,8 @@ export function SavedFeeds({}: Props) { key={f.id} feed={f} isPinned - overwriteSavedFeeds={overwriteSavedFeeds} - resetSaveFeedsMutationState={resetSaveFeedsMutationState} currentFeeds={currentFeeds} + setCurrentFeeds={setCurrentFeeds} preferences={preferences} /> )) @@ -161,9 +192,8 @@ export function SavedFeeds({}: Props) { key={f.id} feed={f} isPinned={false} - overwriteSavedFeeds={overwriteSavedFeeds} - resetSaveFeedsMutationState={resetSaveFeedsMutationState} currentFeeds={currentFeeds} + setCurrentFeeds={setCurrentFeeds} preferences={preferences} /> )) @@ -197,44 +227,27 @@ function ListItem({ feed, isPinned, currentFeeds, - overwriteSavedFeeds, - resetSaveFeedsMutationState, + setCurrentFeeds, }: { feed: AppBskyActorDefs.SavedFeed isPinned: boolean currentFeeds: AppBskyActorDefs.SavedFeed[] - overwriteSavedFeeds: ReturnType< - typeof useOverwriteSavedFeedsMutation - >['mutateAsync'] - resetSaveFeedsMutationState: ReturnType< - typeof useOverwriteSavedFeedsMutation - >['reset'] + setCurrentFeeds: React.Dispatch<AppBskyActorDefs.SavedFeed[]> preferences: UsePreferencesQueryResponse }) { - const pal = usePalette('default') const {_} = useLingui() + const pal = usePalette('default') 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`), 'xmark') - logger.error('Failed to toggle pinned feed', {message: e}) - } - }, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState]) + setCurrentFeeds( + currentFeeds.map(f => + f.id === feed.id ? {...feed, pinned: !feed.pinned} : f, + ), + ) + }, [playHaptic, feed, currentFeeds, setCurrentFeeds]) const onPressUp = React.useCallback(async () => { if (!isPinned) return @@ -250,13 +263,8 @@ function ListItem({ nextFeeds[index], ] - try { - await overwriteSavedFeeds(nextFeeds) - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to set pinned feed order', {message: e}) - } - }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) + setCurrentFeeds(nextFeeds) + }, [feed, isPinned, setCurrentFeeds, currentFeeds]) const onPressDown = React.useCallback(async () => { if (!isPinned) return @@ -266,22 +274,25 @@ function ListItem({ const index = ids.indexOf(feed.id) const nextIndex = index + 1 - if (index === -1 || index >= nextFeeds.length - 1) return + if (index === -1 || index >= nextFeeds.filter(f => f.pinned).length - 1) + return ;[nextFeeds[index], nextFeeds[nextIndex]] = [ nextFeeds[nextIndex], nextFeeds[index], ] - try { - await overwriteSavedFeeds(nextFeeds) - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to set pinned feed order', {message: e}) - } - }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) + setCurrentFeeds(nextFeeds) + }, [feed, isPinned, setCurrentFeeds, currentFeeds]) + + const onPressRemove = React.useCallback(async () => { + playHaptic() + setCurrentFeeds(currentFeeds.filter(f => f.id !== feed.id)) + }, [playHaptic, feed, currentFeeds, setCurrentFeeds]) return ( - <View style={[styles.itemContainer, pal.border]}> + <Animated.View + style={[styles.itemContainer, pal.border]} + layout={LinearTransition.duration(100)}> {feed.type === 'timeline' ? ( <FollowingFeedCard /> ) : ( @@ -290,25 +301,22 @@ function ListItem({ feedUri={feedUri} style={[isPinned && {paddingRight: 8}]} showMinimalPlaceholder - showSaveBtn={!isPinned} hideTopBorder={true} /> )} {isPinned ? ( <> <Pressable - disabled={isUpdatePending} accessibilityRole="button" onPress={onPressUp} - hitSlop={HITSLOP_TOP} + hitSlop={5} style={state => ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, marginRight: 8, - opacity: - state.hovered || state.pressed || isUpdatePending ? 0.5 : 1, + opacity: state.hovered || state.pressed ? 0.5 : 1, })}> <FontAwesomeIcon icon="arrow-up" @@ -317,18 +325,16 @@ function ListItem({ /> </Pressable> <Pressable - disabled={isUpdatePending} accessibilityRole="button" onPress={onPressDown} - hitSlop={HITSLOP_BOTTOM} + hitSlop={5} style={state => ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, marginRight: 8, - opacity: - state.hovered || state.pressed || isUpdatePending ? 0.5 : 1, + opacity: state.hovered || state.pressed ? 0.5 : 1, })}> <FontAwesomeIcon icon="arrow-down" @@ -337,20 +343,39 @@ function ListItem({ /> </Pressable> </> - ) : null} + ) : ( + <Pressable + testID={`feed-${feedUri}-toggleSave`} + accessibilityRole="button" + accessibilityLabel={_(msg`Remove from my feeds`)} + accessibilityHint="" + onPress={onPressRemove} + hitSlop={5} + style={state => ({ + marginRight: 8, + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 4, + opacity: state.hovered || state.focused ? 0.5 : 1, + })}> + <FontAwesomeIcon + icon={['far', 'trash-can']} + size={19} + color={pal.colors.icon} + /> + </Pressable> + )} <View style={{paddingRight: 16}}> <Pressable - disabled={isUpdatePending} accessibilityRole="button" - hitSlop={10} + hitSlop={5} onPress={onTogglePinned} style={state => ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, - opacity: - state.hovered || state.focused || isUpdatePending ? 0.5 : 1, + opacity: state.hovered || state.focused ? 0.5 : 1, })}> <FontAwesomeIcon icon="thumb-tack" @@ -359,7 +384,7 @@ function ListItem({ /> </Pressable> </View> - </View> + </Animated.View> ) } |