Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74ec8401f4 | |||
| b84e160f92 | |||
| 93f87f8ced | |||
| 1ad9eb6330 | |||
| 2d7a562239 |
@@ -7,11 +7,16 @@ export function useCustomFeed(uri: string): CustomFeedModel | undefined {
|
||||
const [item, setItem] = useState<CustomFeedModel | undefined>()
|
||||
useEffect(() => {
|
||||
async function fetchView() {
|
||||
const res = await store.agent.app.bsky.feed.getFeedGenerator({
|
||||
feed: uri,
|
||||
})
|
||||
const view = res.data.view
|
||||
return view
|
||||
try {
|
||||
const res = await store.agent.app.bsky.feed.getFeedGenerator({
|
||||
feed: uri,
|
||||
})
|
||||
const view = res.data.view
|
||||
return view
|
||||
} catch (e) {
|
||||
store.log.error('useCustomFeed failed to getFeedGenerator', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
async function buildFeedItem() {
|
||||
const view = await fetchView()
|
||||
|
||||
@@ -22,6 +22,7 @@ export function isNetworkError(e: unknown) {
|
||||
return (
|
||||
str.includes('Abort') ||
|
||||
str.includes('Network request failed') ||
|
||||
str.includes('Failed to fetch')
|
||||
str.includes('Failed to fetch') ||
|
||||
str.includes('NetworkError when attempting to fetch resource')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export type PostsFeedModelError = {
|
||||
type: 'network' | 'upstream'
|
||||
message: string
|
||||
}
|
||||
|
||||
type Options = {
|
||||
/**
|
||||
* Formats the feed in a flat array with no threading of replies, just
|
||||
@@ -47,6 +52,7 @@ export class PostsFeedModel {
|
||||
isBlockedBy = false
|
||||
error = ''
|
||||
loadMoreError = ''
|
||||
cleanError?: PostsFeedModelError = undefined
|
||||
params: QueryParams
|
||||
hasMore = true
|
||||
pollCursor: string | undefined
|
||||
@@ -137,6 +143,7 @@ export class PostsFeedModel {
|
||||
this.hasNewLatest = false
|
||||
this.hasLoaded = false
|
||||
this.error = ''
|
||||
this.cleanError = undefined
|
||||
this.hasMore = true
|
||||
this.pollCursor = undefined
|
||||
this.slices = []
|
||||
@@ -281,6 +288,7 @@ export class PostsFeedModel {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
this.cleanError = undefined
|
||||
}
|
||||
|
||||
_xIdle(error?: any, loadMoreError?: any) {
|
||||
@@ -291,6 +299,7 @@ export class PostsFeedModel {
|
||||
this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError
|
||||
this.error = cleanError(error)
|
||||
this.loadMoreError = cleanError(loadMoreError)
|
||||
this.cleanError = this.error ? this._cleanError(this.error) : undefined
|
||||
if (error) {
|
||||
this.rootStore.log.error('Posts feed request failed', error)
|
||||
}
|
||||
@@ -358,4 +367,21 @@ export class PostsFeedModel {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_cleanError(error: string) {
|
||||
const e: PostsFeedModelError = {
|
||||
type: 'network',
|
||||
message: `Hmmm, the feed appears to be offline. Please check your internet connection and try again.`,
|
||||
}
|
||||
|
||||
if (
|
||||
error.includes('could not find feed') ||
|
||||
error.includes('server appears to be experiencing issues')
|
||||
) {
|
||||
e.type = 'upstream'
|
||||
e.message = `Hmmm, we're having trouble finding this feed. It may have been deleted.`
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import React from 'react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
|
||||
import {Haptics} from 'lib/haptics'
|
||||
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
|
||||
import {CustomFeedModel} from 'state/models/feeds/custom-feed'
|
||||
import {useStores} from 'state/index'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
|
||||
export function CustomFeedContextMenu({
|
||||
children,
|
||||
feed,
|
||||
...rest
|
||||
}: React.PropsWithChildren<
|
||||
{feed: CustomFeedModel} & Omit<
|
||||
React.ComponentProps<typeof NativeDropdown>,
|
||||
'items'
|
||||
>
|
||||
>) {
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const handleOrDid = feed?.data?.creator?.handle || feed?.data?.creator?.did
|
||||
const {track} = useAnalytics()
|
||||
|
||||
const onPressAbout = React.useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: feed?.displayName || '',
|
||||
message: feed?.data.description || 'This feed has no description.',
|
||||
confirmBtnText: 'Close',
|
||||
onPressConfirm() {},
|
||||
})
|
||||
}, [store, feed])
|
||||
|
||||
const onPressViewAuthor = React.useCallback(() => {
|
||||
navigation.navigate('Profile', {name: handleOrDid})
|
||||
}, [handleOrDid, navigation])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
const {rkey} = new AtUri(feed.uri)
|
||||
const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`)
|
||||
shareUrl(url)
|
||||
track('CustomFeed:Share')
|
||||
}, [feed.uri, handleOrDid, track])
|
||||
|
||||
const onPressReport = React.useCallback(() => {
|
||||
if (!feed) return
|
||||
store.shell.openModal({
|
||||
name: 'report',
|
||||
uri: feed.uri,
|
||||
cid: feed.data.cid,
|
||||
})
|
||||
}, [store, feed])
|
||||
|
||||
const onToggleSaved = React.useCallback(async () => {
|
||||
try {
|
||||
Haptics.default()
|
||||
if (feed?.isSaved) {
|
||||
await feed?.unsave()
|
||||
} else {
|
||||
await feed?.save()
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
'There was an an issue updating your feeds, please check your internet connection and try again.',
|
||||
)
|
||||
store.log.error('Failed up update feeds', {err})
|
||||
}
|
||||
}, [store, feed])
|
||||
|
||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||
return [
|
||||
feed
|
||||
? {
|
||||
testID: 'feedHeaderDropdownAboutBtn',
|
||||
label: 'About this feed',
|
||||
onPress: onPressAbout,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'info.circle',
|
||||
},
|
||||
android: '',
|
||||
web: 'info',
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
testID: 'feedHeaderDropdownViewAuthorBtn',
|
||||
label: 'View author',
|
||||
onPress: onPressViewAuthor,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person',
|
||||
},
|
||||
android: '',
|
||||
web: ['far', 'user'],
|
||||
},
|
||||
},
|
||||
{
|
||||
testID: 'feedHeaderDropdownToggleSavedBtn',
|
||||
label: feed?.isSaved ? 'Remove from my feeds' : 'Add to my feeds',
|
||||
onPress: onToggleSaved,
|
||||
icon: feed?.isSaved
|
||||
? {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
},
|
||||
android: 'ic_delete',
|
||||
web: 'trash',
|
||||
}
|
||||
: {
|
||||
ios: {
|
||||
name: 'plus',
|
||||
},
|
||||
android: '',
|
||||
web: 'plus',
|
||||
},
|
||||
},
|
||||
{
|
||||
testID: 'feedHeaderDropdownReportBtn',
|
||||
label: 'Report feed',
|
||||
onPress: onPressReport,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
},
|
||||
{
|
||||
testID: 'feedHeaderDropdownShareBtn',
|
||||
label: 'Share link',
|
||||
onPress: onPressShare,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
].filter(Boolean) as DropdownItem[]
|
||||
}, [
|
||||
feed,
|
||||
onPressAbout,
|
||||
onToggleSaved,
|
||||
onPressReport,
|
||||
onPressShare,
|
||||
onPressViewAuthor,
|
||||
])
|
||||
|
||||
return (
|
||||
<NativeDropdown
|
||||
testID="feedHeaderDropdownBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="More options"
|
||||
accessibilityHint=""
|
||||
{...rest}>
|
||||
{children}
|
||||
</NativeDropdown>
|
||||
)
|
||||
}
|
||||
+213
-11
@@ -8,10 +8,17 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AtUri, AppBskyFeedGetFeed} from '@atproto/api'
|
||||
|
||||
import {useStores} from 'state/index'
|
||||
import {Haptics} from 'lib/haptics'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useCustomFeed} from 'lib/hooks/useCustomFeed'
|
||||
import {FlatList} from '../util/Views'
|
||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {PostsFeedModel} from 'state/models/feeds/posts'
|
||||
import {PostsFeedModel, PostsFeedModelError} from 'state/models/feeds/posts'
|
||||
import {FeedSlice} from './FeedSlice'
|
||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||
@@ -19,11 +26,19 @@ import {s} from 'lib/styles'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {CustomFeedContextMenu} from 'view/com/feeds/CustomFeedContextMenu'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
const FEED_INNER_HEADER = {_reactKey: '__feedInnerHeader__'}
|
||||
const FEED_INNER_HEADER_LOADING = {_reactKey: '__feedInnerHeaderLoading__'}
|
||||
|
||||
export const Feed = observer(function Feed({
|
||||
feed,
|
||||
@@ -38,6 +53,7 @@ export const Feed = observer(function Feed({
|
||||
headerOffset = 0,
|
||||
ListHeaderComponent,
|
||||
extraData,
|
||||
showFeedHeaderContextMenu,
|
||||
}: {
|
||||
feed: PostsFeedModel
|
||||
style?: StyleProp<ViewStyle>
|
||||
@@ -51,16 +67,23 @@ export const Feed = observer(function Feed({
|
||||
headerOffset?: number
|
||||
ListHeaderComponent?: () => JSX.Element
|
||||
extraData?: any
|
||||
showFeedHeaderContextMenu?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||
const params = feed.params as AppBskyFeedGetFeed.QueryParams
|
||||
const isCustomFeed = Boolean(params.feed)
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
let feedItems: any[] = []
|
||||
if (feed.hasLoaded) {
|
||||
if (feed.hasError) {
|
||||
if (isCustomFeed) {
|
||||
feedItems.push(FEED_INNER_HEADER)
|
||||
}
|
||||
if (feed.hasError && !isCustomFeed) {
|
||||
// applies to our internal algo feeds only
|
||||
feedItems = feedItems.concat([ERROR_ITEM])
|
||||
}
|
||||
if (feed.isEmpty) {
|
||||
@@ -79,6 +102,7 @@ export const Feed = observer(function Feed({
|
||||
feed.isEmpty,
|
||||
feed.slices,
|
||||
feed.loadMoreError,
|
||||
isCustomFeed,
|
||||
])
|
||||
|
||||
// events
|
||||
@@ -115,15 +139,20 @@ export const Feed = observer(function Feed({
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({item}: {item: any}) => {
|
||||
if (item === EMPTY_FEED_ITEM) {
|
||||
return renderEmptyState()
|
||||
} else if (item === ERROR_ITEM) {
|
||||
if (item === FEED_INNER_HEADER_LOADING) {
|
||||
return <FeedHeaderLoading />
|
||||
} else if (item === FEED_INNER_HEADER) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={feed.error}
|
||||
onPressTryAgain={onPressTryAgain}
|
||||
<FeedInnerHeader
|
||||
showContextMenu={showFeedHeaderContextMenu}
|
||||
error={feed.cleanError}
|
||||
params={params}
|
||||
/>
|
||||
)
|
||||
} else if (item === EMPTY_FEED_ITEM) {
|
||||
return renderEmptyState()
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return <ErrorMessage feed={feed} onPress={onPressTryAgain} />
|
||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
@@ -136,7 +165,14 @@ export const Feed = observer(function Feed({
|
||||
}
|
||||
return <FeedSlice slice={item} />
|
||||
},
|
||||
[feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
|
||||
[
|
||||
feed,
|
||||
params,
|
||||
showFeedHeaderContextMenu,
|
||||
onPressTryAgain,
|
||||
onPressRetryLoadMore,
|
||||
renderEmptyState,
|
||||
],
|
||||
)
|
||||
|
||||
const FeedFooter = React.useCallback(
|
||||
@@ -158,7 +194,13 @@ export const Feed = observer(function Feed({
|
||||
<FlatList
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
data={!feed.hasLoaded ? [LOADING_ITEM] : data}
|
||||
data={
|
||||
!feed.hasLoaded
|
||||
? isCustomFeed
|
||||
? [FEED_INNER_HEADER_LOADING, LOADING_ITEM]
|
||||
: [LOADING_ITEM]
|
||||
: data
|
||||
}
|
||||
keyExtractor={item => item._reactKey}
|
||||
renderItem={renderItem}
|
||||
ListFooterComponent={FeedFooter}
|
||||
@@ -189,6 +231,166 @@ export const Feed = observer(function Feed({
|
||||
)
|
||||
})
|
||||
|
||||
function ErrorMessage({
|
||||
feed,
|
||||
onPress,
|
||||
}: {
|
||||
feed: PostsFeedModel
|
||||
onPress?: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
]}>
|
||||
<Text style={{paddingRight: 18}}>
|
||||
{feed.cleanError?.message || feed.error}
|
||||
</Text>
|
||||
<Button
|
||||
type="default-light"
|
||||
onPress={onPress}
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityHint="Retries the last action, which errored out">
|
||||
Try again
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedHeaderLoading() {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
{
|
||||
width: '100%',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 18,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedInnerHeader({
|
||||
error,
|
||||
params,
|
||||
showContextMenu = true,
|
||||
}: {
|
||||
error?: PostsFeedModelError
|
||||
params: AppBskyFeedGetFeed.QueryParams
|
||||
showContextMenu?: boolean
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {host, rkey} = new AtUri(params.feed)
|
||||
const uri = makeRecordUri(host, 'app.bsky.feed.generator', rkey)
|
||||
const feed = useCustomFeed(uri)
|
||||
const author = feed?.data?.creator?.handle
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const shareUrl = toShareUrl(`/profile/${host}`)
|
||||
|
||||
const removeFeed = React.useCallback(async () => {
|
||||
try {
|
||||
Haptics.default()
|
||||
if (feed?.isSaved) {
|
||||
await feed?.unsave()
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
'There was an an issue removing this feed. Please check your internet connection and try again.',
|
||||
)
|
||||
store.log.error('Failed to remove feed', {err})
|
||||
}
|
||||
}, [store, feed])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
{
|
||||
width: '100%',
|
||||
paddingHorizontal: 18,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
}}>
|
||||
<Text>
|
||||
<Text style={[pal.textLight]}>By </Text>
|
||||
{author ? (
|
||||
<TextLink href={shareUrl} text={'@' + author} />
|
||||
) : (
|
||||
<Text style={[pal.textLight]}>...</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{author && showContextMenu && !isDesktop ? (
|
||||
<CustomFeedContextMenu feed={feed}>
|
||||
<View
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
paddingTop: 1,
|
||||
borderRadius: 24,
|
||||
backgroundColor: pal.view.backgroundColor,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis"
|
||||
size={18}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
</View>
|
||||
</CustomFeedContextMenu>
|
||||
) : (
|
||||
<View style={{height: 24}} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{error?.message && (
|
||||
<>
|
||||
<View style={[pal.view, {height: 1}]} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
paddingVertical: 12,
|
||||
}}>
|
||||
<Text style={{width: '100%'}}>{error.message}</Text>
|
||||
|
||||
{error.type === 'upstream' && feed?.isSaved ? (
|
||||
<Button
|
||||
type="default-light"
|
||||
style={{marginTop: 12}}
|
||||
onPress={removeFeed}
|
||||
accessibilityLabel="Remove from my feeds"
|
||||
accessibilityHint="Un-pin and remove this feed from your home screen feeds.">
|
||||
Remove from my feeds
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
feedFooter: {paddingTop: 20},
|
||||
})
|
||||
|
||||
+21
-144
@@ -21,19 +21,16 @@ import {Text} from 'view/com/util/text/Text'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {Haptics} from 'lib/haptics'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {FAB} from '../com/util/fab/FAB'
|
||||
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
|
||||
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
||||
import {EmptyState} from 'view/com/util/EmptyState'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
|
||||
import {resolveName} from 'lib/api'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {CustomFeedContextMenu} from 'view/com/feeds/CustomFeedContextMenu'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
|
||||
|
||||
@@ -122,11 +119,9 @@ export const CustomFeedScreenInner = observer(
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const isScreenFocused = useIsFocused()
|
||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {track} = useAnalytics()
|
||||
const {rkey, name: handleOrDid} = route.params
|
||||
const {rkey} = route.params
|
||||
const uri = useMemo(
|
||||
() => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey),
|
||||
[rkey, feedOwnerDid],
|
||||
@@ -185,36 +180,6 @@ export const CustomFeedScreenInner = observer(
|
||||
})
|
||||
}, [store, currentFeed])
|
||||
|
||||
const onPressAbout = React.useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: currentFeed?.displayName || '',
|
||||
message:
|
||||
currentFeed?.data.description || 'This feed has no description.',
|
||||
confirmBtnText: 'Close',
|
||||
onPressConfirm() {},
|
||||
})
|
||||
}, [store, currentFeed])
|
||||
|
||||
const onPressViewAuthor = React.useCallback(() => {
|
||||
navigation.navigate('Profile', {name: handleOrDid})
|
||||
}, [handleOrDid, navigation])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`)
|
||||
shareUrl(url)
|
||||
track('CustomFeed:Share')
|
||||
}, [handleOrDid, rkey, track])
|
||||
|
||||
const onPressReport = React.useCallback(() => {
|
||||
if (!currentFeed) return
|
||||
store.shell.openModal({
|
||||
name: 'report',
|
||||
uri: currentFeed.uri,
|
||||
cid: currentFeed.data.cid,
|
||||
})
|
||||
}, [store, currentFeed])
|
||||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||
resetMainScroll()
|
||||
@@ -243,97 +208,9 @@ export const CustomFeedScreenInner = observer(
|
||||
}
|
||||
}, [store, onSoftReset, isScreenFocused])
|
||||
|
||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||
return [
|
||||
currentFeed
|
||||
? {
|
||||
testID: 'feedHeaderDropdownAboutBtn',
|
||||
label: 'About this feed',
|
||||
onPress: onPressAbout,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'info.circle',
|
||||
},
|
||||
android: '',
|
||||
web: 'info',
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
testID: 'feedHeaderDropdownViewAuthorBtn',
|
||||
label: 'View author',
|
||||
onPress: onPressViewAuthor,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person',
|
||||
},
|
||||
android: '',
|
||||
web: ['far', 'user'],
|
||||
},
|
||||
},
|
||||
{
|
||||
testID: 'feedHeaderDropdownToggleSavedBtn',
|
||||
label: currentFeed?.isSaved
|
||||
? 'Remove from my feeds'
|
||||
: 'Add to my feeds',
|
||||
onPress: onToggleSaved,
|
||||
icon: currentFeed?.isSaved
|
||||
? {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
},
|
||||
android: 'ic_delete',
|
||||
web: 'trash',
|
||||
}
|
||||
: {
|
||||
ios: {
|
||||
name: 'plus',
|
||||
},
|
||||
android: '',
|
||||
web: 'plus',
|
||||
},
|
||||
},
|
||||
{
|
||||
testID: 'feedHeaderDropdownReportBtn',
|
||||
label: 'Report feed',
|
||||
onPress: onPressReport,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
},
|
||||
{
|
||||
testID: 'feedHeaderDropdownShareBtn',
|
||||
label: 'Share link',
|
||||
onPress: onPressShare,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
].filter(Boolean) as DropdownItem[]
|
||||
}, [
|
||||
currentFeed,
|
||||
onPressAbout,
|
||||
onToggleSaved,
|
||||
onPressReport,
|
||||
onPressShare,
|
||||
onPressViewAuthor,
|
||||
])
|
||||
|
||||
const renderEmptyState = React.useCallback(() => {
|
||||
return (
|
||||
<View style={[pal.border, {borderTopWidth: 1, paddingTop: 20}]}>
|
||||
<EmptyState icon="feed" message="This feed is empty!" />
|
||||
</View>
|
||||
)
|
||||
}, [pal.border])
|
||||
return <EmptyState icon="feed" message="This feed is empty!" />
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={s.hContentRegion}>
|
||||
@@ -403,25 +280,24 @@ export const CustomFeedScreenInner = observer(
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CustomFeedContextMenu
|
||||
feed={currentFeed}
|
||||
testID="feedHeaderDropdownBtn">
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: 12,
|
||||
paddingRight: isMobile ? 12 : 0,
|
||||
}}>
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis"
|
||||
size={20}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
</View>
|
||||
</CustomFeedContextMenu>
|
||||
</>
|
||||
) : null}
|
||||
<NativeDropdown
|
||||
testID="feedHeaderDropdownBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel="More options"
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: 12,
|
||||
paddingRight: isMobile ? 12 : 0,
|
||||
}}>
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis"
|
||||
size={20}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
</View>
|
||||
</NativeDropdown>
|
||||
</SimpleViewHeader>
|
||||
<Feed
|
||||
scrollElRef={scrollElRef}
|
||||
@@ -431,6 +307,7 @@ export const CustomFeedScreenInner = observer(
|
||||
renderEmptyState={renderEmptyState}
|
||||
extraData={[uri, isPinned]}
|
||||
style={!isTabletOrDesktop ? {flex: 1} : undefined}
|
||||
showFeedHeaderContextMenu={false}
|
||||
/>
|
||||
{isScrolledDown ? (
|
||||
<LoadLatestBtn
|
||||
|
||||
Reference in New Issue
Block a user