Revise custom feed header style
This commit is contained in:
@@ -22,6 +22,7 @@ export function isNetworkError(e: unknown) {
|
|||||||
return (
|
return (
|
||||||
str.includes('Abort') ||
|
str.includes('Abort') ||
|
||||||
str.includes('Network request failed') ||
|
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
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
|
export type PostsFeedModelError = {
|
||||||
|
type: 'network' | 'upstream'
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
/**
|
/**
|
||||||
* Formats the feed in a flat array with no threading of replies, just
|
* Formats the feed in a flat array with no threading of replies, just
|
||||||
@@ -47,6 +52,7 @@ export class PostsFeedModel {
|
|||||||
isBlockedBy = false
|
isBlockedBy = false
|
||||||
error = ''
|
error = ''
|
||||||
loadMoreError = ''
|
loadMoreError = ''
|
||||||
|
cleanError?: PostsFeedModelError = undefined
|
||||||
params: QueryParams
|
params: QueryParams
|
||||||
hasMore = true
|
hasMore = true
|
||||||
pollCursor: string | undefined
|
pollCursor: string | undefined
|
||||||
@@ -137,6 +143,7 @@ export class PostsFeedModel {
|
|||||||
this.hasNewLatest = false
|
this.hasNewLatest = false
|
||||||
this.hasLoaded = false
|
this.hasLoaded = false
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
this.cleanError = undefined
|
||||||
this.hasMore = true
|
this.hasMore = true
|
||||||
this.pollCursor = undefined
|
this.pollCursor = undefined
|
||||||
this.slices = []
|
this.slices = []
|
||||||
@@ -281,6 +288,7 @@ export class PostsFeedModel {
|
|||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
this.cleanError = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
_xIdle(error?: any, loadMoreError?: any) {
|
_xIdle(error?: any, loadMoreError?: any) {
|
||||||
@@ -290,7 +298,8 @@ export class PostsFeedModel {
|
|||||||
this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError
|
this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError
|
||||||
this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError
|
this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError
|
||||||
this.error = cleanError(error)
|
this.error = cleanError(error)
|
||||||
this.loadMoreError = cleanError(loadMoreError)
|
this.loadMoreError = cleanError(loadMoreError) // TODO
|
||||||
|
this.cleanError = this.error ? this._cleanError(this.error) : undefined
|
||||||
if (error) {
|
if (error) {
|
||||||
this.rootStore.log.error('Posts feed request failed', 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+186
-11
@@ -8,10 +8,15 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {AtUri, AppBskyFeedGetFeed} from '@atproto/api'
|
||||||
|
|
||||||
|
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 {FlatList} from '../util/Views'
|
||||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {PostsFeedModel, PostsFeedModelError} from 'state/models/feeds/posts'
|
||||||
import {PostsFeedModel} from 'state/models/feeds/posts'
|
|
||||||
import {FeedSlice} from './FeedSlice'
|
import {FeedSlice} from './FeedSlice'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||||
@@ -19,11 +24,18 @@ import {s} from 'lib/styles'
|
|||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
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'
|
||||||
|
|
||||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_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({
|
export const Feed = observer(function Feed({
|
||||||
feed,
|
feed,
|
||||||
@@ -38,6 +50,7 @@ export const Feed = observer(function Feed({
|
|||||||
headerOffset = 0,
|
headerOffset = 0,
|
||||||
ListHeaderComponent,
|
ListHeaderComponent,
|
||||||
extraData,
|
extraData,
|
||||||
|
showFeedHeaderContextMenu,
|
||||||
}: {
|
}: {
|
||||||
feed: PostsFeedModel
|
feed: PostsFeedModel
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
@@ -51,16 +64,23 @@ export const Feed = observer(function Feed({
|
|||||||
headerOffset?: number
|
headerOffset?: number
|
||||||
ListHeaderComponent?: () => JSX.Element
|
ListHeaderComponent?: () => JSX.Element
|
||||||
extraData?: any
|
extraData?: any
|
||||||
|
showFeedHeaderContextMenu?: boolean
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||||
|
const params = feed.params as AppBskyFeedGetFeed.QueryParams
|
||||||
|
const isCustomFeed = Boolean(params.feed)
|
||||||
|
|
||||||
const data = React.useMemo(() => {
|
const data = React.useMemo(() => {
|
||||||
let feedItems: any[] = []
|
let feedItems: any[] = []
|
||||||
if (feed.hasLoaded) {
|
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])
|
feedItems = feedItems.concat([ERROR_ITEM])
|
||||||
}
|
}
|
||||||
if (feed.isEmpty) {
|
if (feed.isEmpty) {
|
||||||
@@ -79,6 +99,7 @@ export const Feed = observer(function Feed({
|
|||||||
feed.isEmpty,
|
feed.isEmpty,
|
||||||
feed.slices,
|
feed.slices,
|
||||||
feed.loadMoreError,
|
feed.loadMoreError,
|
||||||
|
isCustomFeed,
|
||||||
])
|
])
|
||||||
|
|
||||||
// events
|
// events
|
||||||
@@ -115,15 +136,20 @@ export const Feed = observer(function Feed({
|
|||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({item}: {item: any}) => {
|
({item}: {item: any}) => {
|
||||||
if (item === EMPTY_FEED_ITEM) {
|
if (item === FEED_INNER_HEADER_LOADING) {
|
||||||
return renderEmptyState()
|
return <FeedHeaderLoading />
|
||||||
} else if (item === ERROR_ITEM) {
|
} else if (item === FEED_INNER_HEADER) {
|
||||||
return (
|
return (
|
||||||
<ErrorMessage
|
<FeedInnerHeader
|
||||||
message={feed.error}
|
showContextMenu={showFeedHeaderContextMenu}
|
||||||
onPressTryAgain={onPressTryAgain}
|
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) {
|
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||||
return (
|
return (
|
||||||
<LoadMoreRetryBtn
|
<LoadMoreRetryBtn
|
||||||
@@ -136,7 +162,14 @@ export const Feed = observer(function Feed({
|
|||||||
}
|
}
|
||||||
return <FeedSlice slice={item} />
|
return <FeedSlice slice={item} />
|
||||||
},
|
},
|
||||||
[feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
|
[
|
||||||
|
feed,
|
||||||
|
params,
|
||||||
|
showFeedHeaderContextMenu,
|
||||||
|
onPressTryAgain,
|
||||||
|
onPressRetryLoadMore,
|
||||||
|
renderEmptyState,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const FeedFooter = React.useCallback(
|
const FeedFooter = React.useCallback(
|
||||||
@@ -158,7 +191,13 @@ export const Feed = observer(function Feed({
|
|||||||
<FlatList
|
<FlatList
|
||||||
testID={testID ? `${testID}-flatlist` : undefined}
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
ref={scrollElRef}
|
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}
|
keyExtractor={item => item._reactKey}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
ListFooterComponent={FeedFooter}
|
ListFooterComponent={FeedFooter}
|
||||||
@@ -189,6 +228,142 @@ 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}>
|
||||||
|
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 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}`)
|
||||||
|
|
||||||
|
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' ? (
|
||||||
|
<Button type="default-light" style={{marginTop: 12}}>
|
||||||
|
Remove from my feeds
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
feedFooter: {paddingTop: 20},
|
feedFooter: {paddingTop: 20},
|
||||||
})
|
})
|
||||||
|
|||||||
+19
-138
@@ -21,19 +21,16 @@ import {Text} from 'view/com/util/text/Text'
|
|||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from 'view/com/util/Toast'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||||
import {shareUrl} from 'lib/sharing'
|
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
|
||||||
import {Haptics} from 'lib/haptics'
|
import {Haptics} from 'lib/haptics'
|
||||||
import {ComposeIcon2} from 'lib/icons'
|
import {ComposeIcon2} from 'lib/icons'
|
||||||
import {FAB} from '../com/util/fab/FAB'
|
import {FAB} from '../com/util/fab/FAB'
|
||||||
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
|
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
|
||||||
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
||||||
import {EmptyState} from 'view/com/util/EmptyState'
|
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 {resolveName} from 'lib/api'
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {CustomFeedContextMenu} from 'view/com/feeds/CustomFeedContextMenu'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
|
||||||
|
|
||||||
@@ -122,11 +119,9 @@ export const CustomFeedScreenInner = observer(
|
|||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const isScreenFocused = useIsFocused()
|
const isScreenFocused = useIsFocused()
|
||||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||||
const {track} = useAnalytics()
|
const {rkey} = route.params
|
||||||
const {rkey, name: handleOrDid} = route.params
|
|
||||||
const uri = useMemo(
|
const uri = useMemo(
|
||||||
() => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey),
|
() => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey),
|
||||||
[rkey, feedOwnerDid],
|
[rkey, feedOwnerDid],
|
||||||
@@ -185,36 +180,6 @@ export const CustomFeedScreenInner = observer(
|
|||||||
})
|
})
|
||||||
}, [store, currentFeed])
|
}, [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(() => {
|
const onScrollToTop = React.useCallback(() => {
|
||||||
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||||
resetMainScroll()
|
resetMainScroll()
|
||||||
@@ -243,90 +208,6 @@ export const CustomFeedScreenInner = observer(
|
|||||||
}
|
}
|
||||||
}, [store, onSoftReset, isScreenFocused])
|
}, [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(() => {
|
const renderEmptyState = React.useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<View style={[pal.border, {borderTopWidth: 1, paddingTop: 20}]}>
|
<View style={[pal.border, {borderTopWidth: 1, paddingTop: 20}]}>
|
||||||
@@ -403,25 +284,24 @@ export const CustomFeedScreenInner = observer(
|
|||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</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}
|
) : 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>
|
</SimpleViewHeader>
|
||||||
<Feed
|
<Feed
|
||||||
scrollElRef={scrollElRef}
|
scrollElRef={scrollElRef}
|
||||||
@@ -431,6 +311,7 @@ export const CustomFeedScreenInner = observer(
|
|||||||
renderEmptyState={renderEmptyState}
|
renderEmptyState={renderEmptyState}
|
||||||
extraData={[uri, isPinned]}
|
extraData={[uri, isPinned]}
|
||||||
style={!isTabletOrDesktop ? {flex: 1} : undefined}
|
style={!isTabletOrDesktop ? {flex: 1} : undefined}
|
||||||
|
showFeedHeaderContextMenu={false}
|
||||||
/>
|
/>
|
||||||
{isScrolledDown ? (
|
{isScrolledDown ? (
|
||||||
<LoadLatestBtn
|
<LoadLatestBtn
|
||||||
|
|||||||
Reference in New Issue
Block a user