* install and setup lingui * setup dynamic locale activation and async loading * first pass of automated replacement of text messages * add some more documentaton * fix nits * add `es` and `hi`locales for testing purposes * make accessibilityLabel localized * compile and extract new messages * fix merge conflicts * fix eslint warning * change instructions from sending email to opening PR * fix comments
372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
import React from 'react'
|
|
import {ActivityIndicator, StyleSheet, RefreshControl, View} from 'react-native'
|
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
|
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
|
import {ViewHeader} from 'view/com/util/ViewHeader'
|
|
import {FAB} from 'view/com/util/fab/FAB'
|
|
import {Link} from 'view/com/util/Link'
|
|
import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
|
|
import {observer} from 'mobx-react-lite'
|
|
import {usePalette} from 'lib/hooks/usePalette'
|
|
import {useStores} from 'state/index'
|
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
import {ComposeIcon2, CogIcon} from 'lib/icons'
|
|
import {s} from 'lib/styles'
|
|
import {SearchInput} from 'view/com/util/forms/SearchInput'
|
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
|
import {
|
|
LoadingPlaceholder,
|
|
FeedFeedLoadingPlaceholder,
|
|
} from 'view/com/util/LoadingPlaceholder'
|
|
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
|
import debounce from 'lodash.debounce'
|
|
import {Text} from 'view/com/util/text/Text'
|
|
import {MyFeedsItem} from 'state/models/ui/my-feeds'
|
|
import {FeedSourceModel} from 'state/models/content/feed-source'
|
|
import {FlatList} from 'view/com/util/Views'
|
|
import {useFocusEffect} from '@react-navigation/native'
|
|
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
|
import {Trans, msg} from '@lingui/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
import {useSetMinimalShellMode} from '#/state/shell'
|
|
|
|
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
|
|
export const FeedsScreen = withAuthRequired(
|
|
observer<Props>(function FeedsScreenImpl({}: Props) {
|
|
const pal = usePalette('default')
|
|
const store = useStores()
|
|
const {_} = useLingui()
|
|
const setMinimalShellMode = useSetMinimalShellMode()
|
|
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
|
const myFeeds = store.me.myFeeds
|
|
const [query, setQuery] = React.useState<string>('')
|
|
const debouncedSearchFeeds = React.useMemo(
|
|
() => debounce(q => myFeeds.discovery.search(q), 500), // debounce for 500ms
|
|
[myFeeds],
|
|
)
|
|
|
|
useFocusEffect(
|
|
React.useCallback(() => {
|
|
setMinimalShellMode(false)
|
|
myFeeds.setup()
|
|
|
|
const softResetSub = store.onScreenSoftReset(() => myFeeds.refresh())
|
|
return () => {
|
|
softResetSub.remove()
|
|
}
|
|
}, [store, myFeeds, setMinimalShellMode]),
|
|
)
|
|
React.useEffect(() => {
|
|
// watch for changes to saved/pinned feeds
|
|
return myFeeds.registerListeners()
|
|
}, [myFeeds])
|
|
|
|
const onPressCompose = React.useCallback(() => {
|
|
store.shell.openComposer({})
|
|
}, [store])
|
|
const onChangeQuery = React.useCallback(
|
|
(text: string) => {
|
|
setQuery(text)
|
|
if (text.length > 1) {
|
|
debouncedSearchFeeds(text)
|
|
} else {
|
|
myFeeds.discovery.refresh()
|
|
}
|
|
},
|
|
[debouncedSearchFeeds, myFeeds.discovery],
|
|
)
|
|
const onPressCancelSearch = React.useCallback(() => {
|
|
setQuery('')
|
|
myFeeds.discovery.refresh()
|
|
}, [myFeeds])
|
|
const onSubmitQuery = React.useCallback(() => {
|
|
debouncedSearchFeeds(query)
|
|
debouncedSearchFeeds.flush()
|
|
}, [debouncedSearchFeeds, query])
|
|
|
|
const renderHeaderBtn = React.useCallback(() => {
|
|
return (
|
|
<Link
|
|
href="/settings/saved-feeds"
|
|
hitSlop={10}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Edit Saved Feeds`)}
|
|
accessibilityHint="Opens screen to edit Saved Feeds">
|
|
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
|
</Link>
|
|
)
|
|
}, [pal, _])
|
|
|
|
const onRefresh = React.useCallback(() => {
|
|
myFeeds.refresh()
|
|
}, [myFeeds])
|
|
|
|
const renderItem = React.useCallback(
|
|
({item}: {item: MyFeedsItem}) => {
|
|
if (item.type === 'discover-feeds-loading') {
|
|
return <FeedFeedLoadingPlaceholder />
|
|
} else if (item.type === 'spinner') {
|
|
return (
|
|
<View style={s.p10}>
|
|
<ActivityIndicator />
|
|
</View>
|
|
)
|
|
} else if (item.type === 'error') {
|
|
return <ErrorMessage message={item.error} />
|
|
} else if (item.type === 'saved-feeds-header') {
|
|
if (!isMobile) {
|
|
return (
|
|
<View
|
|
style={[
|
|
pal.view,
|
|
styles.header,
|
|
pal.border,
|
|
{
|
|
borderBottomWidth: 1,
|
|
},
|
|
]}>
|
|
<Text type="title-lg" style={[pal.text, s.bold]}>
|
|
<Trans>My Feeds</Trans>
|
|
</Text>
|
|
<Link
|
|
href="/settings/saved-feeds"
|
|
accessibilityLabel={_(msg`Edit My Feeds`)}
|
|
accessibilityHint="">
|
|
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
|
|
</Link>
|
|
</View>
|
|
)
|
|
}
|
|
return <View />
|
|
} else if (item.type === 'saved-feeds-loading') {
|
|
return (
|
|
<>
|
|
{Array.from(Array(item.numItems)).map((_i, i) => (
|
|
<SavedFeedLoadingPlaceholder key={`placeholder-${i}`} />
|
|
))}
|
|
</>
|
|
)
|
|
} else if (item.type === 'saved-feed') {
|
|
return <SavedFeed feed={item.feed} />
|
|
} else if (item.type === 'discover-feeds-header') {
|
|
return (
|
|
<>
|
|
<View
|
|
style={[
|
|
pal.view,
|
|
styles.header,
|
|
{
|
|
marginTop: 16,
|
|
paddingLeft: isMobile ? 12 : undefined,
|
|
paddingRight: 10,
|
|
paddingBottom: isMobile ? 6 : undefined,
|
|
},
|
|
]}>
|
|
<Text type="title-lg" style={[pal.text, s.bold]}>
|
|
<Trans>Discover new feeds</Trans>
|
|
</Text>
|
|
{!isMobile && (
|
|
<SearchInput
|
|
query={query}
|
|
onChangeQuery={onChangeQuery}
|
|
onPressCancelSearch={onPressCancelSearch}
|
|
onSubmitQuery={onSubmitQuery}
|
|
style={{flex: 1, maxWidth: 250}}
|
|
/>
|
|
)}
|
|
</View>
|
|
{isMobile && (
|
|
<View style={{paddingHorizontal: 8, paddingBottom: 10}}>
|
|
<SearchInput
|
|
query={query}
|
|
onChangeQuery={onChangeQuery}
|
|
onPressCancelSearch={onPressCancelSearch}
|
|
onSubmitQuery={onSubmitQuery}
|
|
/>
|
|
</View>
|
|
)}
|
|
</>
|
|
)
|
|
} else if (item.type === 'discover-feed') {
|
|
return (
|
|
<FeedSourceCard
|
|
item={item.feed}
|
|
showSaveBtn
|
|
showDescription
|
|
showLikes
|
|
/>
|
|
)
|
|
} else if (item.type === 'discover-feeds-no-results') {
|
|
return (
|
|
<View
|
|
style={{
|
|
paddingHorizontal: 16,
|
|
paddingTop: 10,
|
|
paddingBottom: '150%',
|
|
}}>
|
|
<Text type="lg" style={pal.textLight}>
|
|
<Trans>No results found for "{query}"</Trans>
|
|
</Text>
|
|
</View>
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
[
|
|
isMobile,
|
|
pal,
|
|
query,
|
|
onChangeQuery,
|
|
onPressCancelSearch,
|
|
onSubmitQuery,
|
|
_,
|
|
],
|
|
)
|
|
|
|
return (
|
|
<View style={[pal.view, styles.container]}>
|
|
{isMobile && (
|
|
<ViewHeader
|
|
title="Feeds"
|
|
canGoBack={false}
|
|
renderButton={renderHeaderBtn}
|
|
showBorder
|
|
/>
|
|
)}
|
|
|
|
<FlatList
|
|
style={[!isTabletOrDesktop && s.flex1, styles.list]}
|
|
data={myFeeds.items}
|
|
keyExtractor={item => item._reactKey}
|
|
contentContainerStyle={styles.contentContainer}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={myFeeds.isRefreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={pal.colors.text}
|
|
titleColor={pal.colors.text}
|
|
/>
|
|
}
|
|
renderItem={renderItem}
|
|
initialNumToRender={10}
|
|
onEndReached={() => myFeeds.loadMore()}
|
|
extraData={myFeeds.isLoading}
|
|
// @ts-ignore our .web version only -prf
|
|
desktopFixedHeight
|
|
/>
|
|
<FAB
|
|
testID="composeFAB"
|
|
onPress={onPressCompose}
|
|
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`New post`)}
|
|
accessibilityHint=""
|
|
/>
|
|
</View>
|
|
)
|
|
}),
|
|
)
|
|
|
|
function SavedFeed({feed}: {feed: FeedSourceModel}) {
|
|
const pal = usePalette('default')
|
|
const {isMobile} = useWebMediaQueries()
|
|
return (
|
|
<Link
|
|
testID={`saved-feed-${feed.displayName}`}
|
|
href={feed.href}
|
|
style={[pal.border, styles.savedFeed, isMobile && styles.savedFeedMobile]}
|
|
hoverStyle={pal.viewLight}
|
|
accessibilityLabel={feed.displayName}
|
|
accessibilityHint=""
|
|
asAnchor
|
|
anchorNoUnderline>
|
|
{feed.error ? (
|
|
<View
|
|
style={{width: 28, flexDirection: 'row', justifyContent: 'center'}}>
|
|
<FontAwesomeIcon
|
|
icon="exclamation-circle"
|
|
color={pal.colors.textLight}
|
|
/>
|
|
</View>
|
|
) : (
|
|
<UserAvatar type="algo" size={28} avatar={feed.avatar} />
|
|
)}
|
|
<View
|
|
style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
|
|
<Text type="lg-medium" style={pal.text} numberOfLines={1}>
|
|
{feed.displayName}
|
|
</Text>
|
|
{feed.error ? (
|
|
<View style={[styles.offlineSlug, pal.borderDark]}>
|
|
<Text type="xs" style={pal.textLight}>
|
|
<Trans>Feed offline</Trans>
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
{isMobile && (
|
|
<FontAwesomeIcon
|
|
icon="chevron-right"
|
|
size={14}
|
|
style={pal.textLight as FontAwesomeIconStyle}
|
|
/>
|
|
)}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
function SavedFeedLoadingPlaceholder() {
|
|
const pal = usePalette('default')
|
|
const {isMobile} = useWebMediaQueries()
|
|
return (
|
|
<View
|
|
style={[
|
|
pal.border,
|
|
styles.savedFeed,
|
|
isMobile && styles.savedFeedMobile,
|
|
]}>
|
|
<LoadingPlaceholder width={28} height={28} style={{borderRadius: 4}} />
|
|
<LoadingPlaceholder width={140} height={12} />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
list: {
|
|
height: '100%',
|
|
},
|
|
contentContainer: {
|
|
paddingBottom: 100,
|
|
},
|
|
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 16,
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
},
|
|
|
|
savedFeed: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 14,
|
|
gap: 12,
|
|
borderBottomWidth: 1,
|
|
},
|
|
savedFeedMobile: {
|
|
paddingVertical: 10,
|
|
},
|
|
offlineSlug: {
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
paddingHorizontal: 4,
|
|
paddingVertical: 2,
|
|
},
|
|
})
|