Store/sync pinned feeds on the server

This commit is contained in:
Paul Frazee 2023-05-18 11:51:25 -05:00
parent d88c27a419
commit 7691fe4f48
8 changed files with 278 additions and 240 deletions

View file

@ -39,20 +39,30 @@ export const CustomFeed = observer(
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
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])

View file

@ -29,6 +29,10 @@ export const SavedFeeds = observer(
}
}, [store, isPageFocused])
const onRefresh = useCallback(() => {
store.me.savedFeeds.refresh()
}, [store])
const renderListEmptyComponent = useCallback(() => {
return (
<View
@ -73,7 +77,7 @@ export const SavedFeeds = observer(
refreshControl={
<RefreshControl
refreshing={store.me.savedFeeds.isRefreshing}
onRefresh={() => store.me.savedFeeds.refresh()}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}

View file

@ -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<Props>) => {
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<Props>,
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 (
<View>
{tabBarPosition === 'top' &&
renderTabBar({
selectedPage,
position,
offset,
onSelect: onTabBarSelect,
})}
{React.Children.map(children, (child, i) => (
<View
style={selectedPage === i ? undefined : s.hidden}
key={`page-${i}`}>
{child}
</View>
))}
{tabBarPosition === 'bottom' &&
renderTabBar({
selectedPage,
position,
offset,
onSelect: onTabBarSelect,
})}
</View>
)
}
const onTabBarSelect = React.useCallback(
(index: number) => {
setSelectedPage(index)
onPageSelected?.(index)
},
[setSelectedPage, onPageSelected],
)
return (
<View>
{tabBarPosition === 'top' &&
renderTabBar({
selectedPage,
onSelect: onTabBarSelect,
})}
{React.Children.map(children, (child, i) => (
<View
style={selectedPage === i ? undefined : s.hidden}
key={`page-${i}`}>
{child}
</View>
))}
{tabBarPosition === 'bottom' &&
renderTabBar({
selectedPage,
onSelect: onTabBarSelect,
})}
</View>
)
},
)

View file

@ -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

View file

@ -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<CommonNavigatorParams, 'SavedFeeds'>
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 (
<View
style={[
@ -56,19 +56,33 @@ export const SavedFeeds = withAuthRequired(
styles.empty,
]}>
<Text type="lg" style={[pal.text]}>
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.
</Text>
</View>
)
}
const _ListFooterComponent = () => {
}, [pal])
const renderListFooterComponent = useCallback(() => {
return (
<View style={styles.footer}>
{savedFeeds.isLoading && <ActivityIndicator />}
</View>
)
}
}, [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 (
<CenteredView
@ -90,17 +104,17 @@ export const SavedFeeds = withAuthRequired(
refreshControl={
<RefreshControl
refreshing={savedFeeds.isRefreshing}
onRefresh={() => savedFeeds.refresh()}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
renderItem={({item, drag}) => <ListItem item={item} drag={drag} />}
initialNumToRender={10}
ListFooterComponent={_ListFooterComponent}
ListEmptyComponent={_ListEmptyComponent}
ListFooterComponent={renderListFooterComponent}
ListEmptyComponent={renderListEmptyComponent}
extraData={savedFeeds.isLoading}
onDragEnd={({data}) => savedFeeds.reorderPinnedFeeds(data)}
onDragEnd={onDragEnd}
/>
</CenteredView>
)
@ -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 (
<ScaleDecorator>
<ShadowDecorator>
@ -128,9 +164,7 @@ const ListItem = observer(
<View style={styles.webArrowButtonsContainer}>
<TouchableOpacity
accessibilityRole="button"
onPress={() => {
savedFeeds.movePinnedItem(item, 'up')
}}>
onPress={onPressUp}>
<FontAwesomeIcon
icon="arrow-up"
size={12}
@ -139,9 +173,7 @@ const ListItem = observer(
</TouchableOpacity>
<TouchableOpacity
accessibilityRole="button"
onPress={() => {
savedFeeds.movePinnedItem(item, 'down')
}}>
onPress={onPressDown}>
<FontAwesomeIcon
icon="arrow-down"
size={12}