bsky-app/src/view/screens/SavedFeeds.tsx
Paul Frazee ea885339cf
Feed UI update working branch [WIP] (#1420)
* Feeds navigation on right side of desktop (#1403)

* Remove home feed header on desktop

* Add feeds to right sidebar

* Add simple non-moving header to desktop

* Improve loading state of custom feed header

* Remove log

Co-authored-by: Eric Bailey <git@esb.lol>

* Remove dead comment

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Redesign feeds tab (#1439)

* consolidate saved feeds and discover into one screen

* Add hoverStyle behavior to <Link>

* More UI work on SavedFeeds

* Replace satellite icon with a hashtag

* Tune My Feeds mobile ui

* Handle no results in my feeds

* Remove old DiscoverFeeds screen

* Remove multifeed

* Remove DiscoverFeeds from router

* Improve loading placeholders

* Small fixes

* Fix types

* Fix overflow issue on firefox

* Add icons prompting to open feeds

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Merge feed prototype [WIP] (#1398)

* POC WIP for the mergefeed

* Add feed API wrapper and move mergefeed into it

* Show feed source in mergefeed

* Add lodash.random dep

* Improve mergefeed sampling and reliability

* Tune source ui element

* Improve mergefeed edge condition handling

* Remove in-place update of feeds for performance

* Fix link on native

* Fix bad ref

* Improve variety in mergefeed sampling

* Fix types

* Fix rebase error

* Add missing source field (got dropped in merge)

* Update find more link

* Simplify the right hand feeds nav

* Bring back load latest button on desktop & unify impl

* Add 'From' to source

* Add simple headers to desktop home & notifications

* Fix thread view jumping around horizontally

* Add unread indicators to desktop headers

* Add home feed preference for enabling the mergefeed

* Add a preference for showing replies among followed users only (#1448)

* Add a preference for showing replies among followed users only

* Simplify the reply filter UI

* Fix typo

* Simplified custom feed header

* Add soft reset to custom feed screen

* Drop all the in-post translate links except when expanded (#1455)

* Update mobile feed settings links to match desktop

* Fixes to feeds screen loading states

* Bolder active state of feeds tab on mobile web

* Fix dark mode issue

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Ansh <anshnanda10@gmail.com>
2023-09-18 11:44:29 -07:00

294 lines
8.6 KiB
TypeScript

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/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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
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 {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(function SavedFeedsImpl({}: Props) {
const pal = usePalette('default')
const store = useStores()
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
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,
isMobile && s.flex1,
pal.viewLight,
styles.empty,
]}>
<Text type="lg" style={[pal.text]}>
You don't have any saved feeds.
</Text>
</View>
)
}, [pal, isMobile])
const renderListFooterComponent = useCallback(() => {
return (
<>
<View style={[styles.footerLinks, pal.border]}>
<Link style={styles.footerLink} href="/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}: {data: CustomFeedModel[]}) => {
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,
isTabletOrDesktop && styles.desktopContainer,
]}>
<ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
<DraggableFlatList
containerStyle={[isTabletOrDesktop ? s.hContentRegion : 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(function ListItemImpl({
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,
// @ts-ignore only rendered on web
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,
},
})