Liked by screen
This commit is contained in:
@@ -80,6 +80,7 @@ import {msg} from '@lingui/macro'
|
|||||||
import {i18n, MessageDescriptor} from '@lingui/core'
|
import {i18n, MessageDescriptor} from '@lingui/core'
|
||||||
import HashtagScreen from '#/screens/Hashtag'
|
import HashtagScreen from '#/screens/Hashtag'
|
||||||
import {logEvent} from './lib/statsig/statsig'
|
import {logEvent} from './lib/statsig/statsig'
|
||||||
|
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
@@ -199,6 +200,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => ProfileFeedLikedByScreen}
|
getComponent={() => ProfileFeedLikedByScreen}
|
||||||
options={{title: title(msg`Liked by`)}}
|
options={{title: title(msg`Liked by`)}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ProfileLabelerLikedBy"
|
||||||
|
getComponent={() => ProfileLabelerLikedByScreen}
|
||||||
|
options={{title: title(msg`Liked by`)}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Debug"
|
name="Debug"
|
||||||
getComponent={() => Storybook}
|
getComponent={() => Storybook}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const atoms = {
|
|||||||
h_full: {
|
h_full: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
|
h_full_vh: web({
|
||||||
|
height: '100vh',
|
||||||
|
}),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Border radius
|
* Border radius
|
||||||
@@ -524,6 +527,10 @@ export const atoms = {
|
|||||||
/*
|
/*
|
||||||
* Margin
|
* Margin
|
||||||
*/
|
*/
|
||||||
|
mx_auto: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
m_2xs: {
|
m_2xs: {
|
||||||
margin: tokens.space._2xs,
|
margin: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {List} from '#/view/com/util/List'
|
||||||
|
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||||
|
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||||
|
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from './icons/ArrowRotateCounterClockwise'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
|
export function LikedByList({uri}: {uri: string}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
|
const {
|
||||||
|
data: resolvedUri,
|
||||||
|
error: resolveError,
|
||||||
|
isFetching: isFetchingResolvedUri,
|
||||||
|
} = useResolveUriQuery(uri)
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isFetching,
|
||||||
|
isFetched,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useLikedByQuery(resolvedUri?.uri)
|
||||||
|
const likes = React.useMemo(() => {
|
||||||
|
if (data?.pages) {
|
||||||
|
return data.pages.flatMap(page => page.likes)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
setIsPTRing(true)
|
||||||
|
try {
|
||||||
|
await refetch()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to refresh likes', {message: err})
|
||||||
|
}
|
||||||
|
setIsPTRing(false)
|
||||||
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
|
const onEndReached = React.useCallback(async () => {
|
||||||
|
if (isFetching || !hasNextPage || isError) return
|
||||||
|
try {
|
||||||
|
await fetchNextPage()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load more likes', {message: err})
|
||||||
|
}
|
||||||
|
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||||
|
|
||||||
|
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
|
||||||
|
return (
|
||||||
|
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (isFetchingResolvedUri || !isFetched) {
|
||||||
|
return (
|
||||||
|
<View style={[a.w_full, a.align_center, a.p_lg]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolveError || isError) {
|
||||||
|
return (
|
||||||
|
<View style={[a.p_lg]}>
|
||||||
|
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
|
||||||
|
<Text style={[a.text_md, a.pb_lg]}>
|
||||||
|
{cleanError(resolveError || error)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.justify_end]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg``)}
|
||||||
|
onPress={onRefresh}
|
||||||
|
size="small"
|
||||||
|
variant="solid"
|
||||||
|
color="primary">
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Try again</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={Refresh} position="right" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return likes.length ? (
|
||||||
|
<List
|
||||||
|
data={likes}
|
||||||
|
keyExtractor={item => item.actor.did}
|
||||||
|
refreshing={isPTRing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
renderItem={renderItem}
|
||||||
|
initialNumToRender={15}
|
||||||
|
contentContainerStyle={{borderWidth: 0}}
|
||||||
|
// FIXME(dan)
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
ListFooterComponent={() => (
|
||||||
|
<View style={[a.w_full, a.align_center, a.p_lg]}>
|
||||||
|
{(isFetching || isFetchingNextPage) && <Loader size="xl" />}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
// @ts-ignore our .web version only -prf
|
||||||
|
desktopFixedHeight
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={[a.p_lg]}>
|
||||||
|
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Nobody has liked this yet. Maybe you should be the first!
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ export type CommonNavigatorParams = {
|
|||||||
PostRepostedBy: {name: string; rkey: string}
|
PostRepostedBy: {name: string; rkey: string}
|
||||||
ProfileFeed: {name: string; rkey: string}
|
ProfileFeed: {name: string; rkey: string}
|
||||||
ProfileFeedLikedBy: {name: string; rkey: string}
|
ProfileFeedLikedBy: {name: string; rkey: string}
|
||||||
|
ProfileLabelerLikedBy: {name: string}
|
||||||
Debug: undefined
|
Debug: undefined
|
||||||
DebugMod: undefined
|
DebugMod: undefined
|
||||||
Log: undefined
|
Log: undefined
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const router = new Router({
|
|||||||
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
||||||
ProfileFeed: '/profile/:name/feed/:rkey',
|
ProfileFeed: '/profile/:name/feed/:rkey',
|
||||||
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
||||||
|
ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by',
|
||||||
Debug: '/sys/debug',
|
Debug: '/sys/debug',
|
||||||
DebugMod: '/sys/debug-mod',
|
DebugMod: '/sys/debug-mod',
|
||||||
Log: '/sys/log',
|
Log: '/sys/log',
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ import {
|
|||||||
Heart2_Stroke2_Corner0_Rounded as Heart,
|
Heart2_Stroke2_Corner0_Rounded as Heart,
|
||||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
||||||
} from '#/components/icons/Heart2'
|
} from '#/components/icons/Heart2'
|
||||||
import {LikesDialog} from '#/components/LikesDialog'
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
import {DialogOuterProps} from '#/components/Dialog'
|
import {DialogOuterProps} from '#/components/Dialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
@@ -64,7 +63,6 @@ let ProfileHeaderLabeler = ({
|
|||||||
const {currentAccount, hasSession} = useSession()
|
const {currentAccount, hasSession} = useSession()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const likesControl = Dialog.useDialogControl()
|
|
||||||
const cantSubscribePrompt = Prompt.usePromptControl()
|
const cantSubscribePrompt = Prompt.usePromptControl()
|
||||||
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
@@ -237,9 +235,14 @@ let ProfileHeaderLabeler = ({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{typeof labeler.likeCount === 'number' && (
|
{typeof labeler.likeCount === 'number' && (
|
||||||
<Button
|
<Link
|
||||||
|
to={{
|
||||||
|
screen: 'ProfileLabelerLikedBy',
|
||||||
|
params: {
|
||||||
|
name: labeler.creator.handle || labeler.creator.did,
|
||||||
|
},
|
||||||
|
}}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
onPress={() => likesControl.open()}
|
|
||||||
label={_(
|
label={_(
|
||||||
msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
|
msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
|
||||||
)}>
|
)}>
|
||||||
@@ -257,13 +260,12 @@ let ProfileHeaderLabeler = ({
|
|||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<LikesDialog control={likesControl} uri={labeler.uri} />
|
|
||||||
<CantSubscribePrompt control={cantSubscribePrompt} />
|
<CantSubscribePrompt control={cantSubscribePrompt} />
|
||||||
</ProfileHeaderShell>
|
</ProfileHeaderShell>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
|
import {LikedByList as PostLikedByComponent} from '#/components/LikedByList'
|
||||||
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
|
||||||
|
|
||||||
|
export function ProfileLabelerLikedByScreen({
|
||||||
|
route,
|
||||||
|
}: NativeStackScreenProps<CommonNavigatorParams, 'ProfileLabelerLikedBy'>) {
|
||||||
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
const {name: handleOrDid} = route.params
|
||||||
|
const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self')
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
React.useCallback(() => {
|
||||||
|
setMinimalShellMode(false)
|
||||||
|
}, [setMinimalShellMode]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.mx_auto,
|
||||||
|
a.w_full,
|
||||||
|
a.h_full_vh,
|
||||||
|
gtMobile && [
|
||||||
|
a.border_l,
|
||||||
|
a.border_r,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{
|
||||||
|
maxWidth: 600,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}>
|
||||||
|
<ViewHeader showBorder title={_(msg`Liked By`)} />
|
||||||
|
<PostLikedByComponent uri={uri} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user