diff --git a/src/Navigation.tsx b/src/Navigation.tsx index b535b7a9aa..2da5a0d626 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -80,6 +80,7 @@ import {msg} from '@lingui/macro' import {i18n, MessageDescriptor} from '@lingui/core' import HashtagScreen from '#/screens/Hashtag' import {logEvent} from './lib/statsig/statsig' +import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' const navigationRef = createNavigationContainerRef() @@ -199,6 +200,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => ProfileFeedLikedByScreen} options={{title: title(msg`Liked by`)}} /> + ProfileLabelerLikedByScreen} + options={{title: title(msg`Liked by`)}} + /> Storybook} diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 8a79a90294..6d9b835894 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -49,6 +49,9 @@ export const atoms = { h_full: { height: '100%', }, + h_full_vh: web({ + height: '100vh', + }), /* * Border radius @@ -524,6 +527,10 @@ export const atoms = { /* * Margin */ + mx_auto: { + marginLeft: 'auto', + marginRight: 'auto', + }, m_2xs: { margin: tokens.space._2xs, }, diff --git a/src/components/LikedByList.tsx b/src/components/LikedByList.tsx new file mode 100644 index 0000000000..e416198b3f --- /dev/null +++ b/src/components/LikedByList.tsx @@ -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 ( + + ) + }, []) + + if (isFetchingResolvedUri || !isFetched) { + return ( + + + + ) + } + + if (resolveError || isError) { + return ( + + + + {cleanError(resolveError || error)} + + + + + + + + ) + } + + return likes.length ? ( + 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={() => ( + + {(isFetching || isFetchingNextPage) && } + + )} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + ) : ( + + + + + Nobody has liked this yet. Maybe you should be the first! + + + + + ) +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 59495401d5..95af2f237d 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -21,6 +21,7 @@ export type CommonNavigatorParams = { PostRepostedBy: {name: string; rkey: string} ProfileFeed: {name: string; rkey: string} ProfileFeedLikedBy: {name: string; rkey: string} + ProfileLabelerLikedBy: {name: string} Debug: undefined DebugMod: undefined Log: undefined diff --git a/src/routes.ts b/src/routes.ts index 23ec880de2..e5326b0fdc 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -21,6 +21,7 @@ export const router = new Router({ PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', ProfileFeed: '/profile/:name/feed/:rkey', ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by', + ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by', Debug: '/sys/debug', DebugMod: '/sys/debug-mod', Log: '/sys/log', diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index b6e1de28fd..2c3a99891f 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -35,10 +35,9 @@ import { Heart2_Stroke2_Corner0_Rounded as Heart, Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, } from '#/components/icons/Heart2' -import {LikesDialog} from '#/components/LikesDialog' -import * as Dialog from '#/components/Dialog' import {DialogOuterProps} from '#/components/Dialog' import * as Prompt from '#/components/Prompt' +import {Link} from '#/components/Link' interface Props { profile: AppBskyActorDefs.ProfileViewDetailed @@ -64,7 +63,6 @@ let ProfileHeaderLabeler = ({ const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const {track} = useAnalytics() - const likesControl = Dialog.useDialogControl() const cantSubscribePrompt = Prompt.usePromptControl() const moderation = useMemo( @@ -237,9 +235,14 @@ let ProfileHeaderLabeler = ({ {typeof labeler.likeCount === 'number' && ( - + )} )} - ) diff --git a/src/screens/Profile/ProfileLabelerLikedBy.tsx b/src/screens/Profile/ProfileLabelerLikedBy.tsx new file mode 100644 index 0000000000..6ea5d580ac --- /dev/null +++ b/src/screens/Profile/ProfileLabelerLikedBy.tsx @@ -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) { + 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 ( + + + + + ) +}