Simplify list logic further to prevent misuse (#3334)

* simplify list logic further

more simplification

simplify by removing `isEmpty`

use `isFetchingNextPage` everywhere for clarity

change `isFetching` to `isFetchingNextPage` for clarity

remove some useless `useMemo`s

move `renderItem` and `keyExtractor` out of component

* clean bundle size check

* update deploy

* adjust

* adjust

* one test

* try now

* test it

* done
zio/stable
Hailey 2024-04-03 20:59:33 -07:00 committed by GitHub
parent b1bd7ab6e3
commit 8e393b16f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 241 additions and 258 deletions

View File

@ -82,15 +82,16 @@ jobs:
uses: expo/expo-github-action/fingerprint@main uses: expo/expo-github-action/fingerprint@main
with: with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }} previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
args:
- name: 👀 Debug fingerprint - name: 👀 Debug fingerprint
id: fingerprint-debug id: fingerprint-debug
run: | run: |
echo "fingerprint-diff=${{ steps.fingerprint.outputs.fingerprint-diff }}"
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}" echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}" echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
if [ "${{ steps.fingerprint.outputs.fingerprint-diff }}" != '[]' ]; then fingerprintDiff="${{ steps.fingerprint.outputs.fingerprint-diff }}"
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT" echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
else else
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT" echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
@ -98,37 +99,37 @@ jobs:
- name: 🔨 Setup EAS - name: 🔨 Setup EAS
uses: expo/expo-github-action@v8 uses: expo/expo-github-action@v8
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
with: with:
expo-version: latest expo-version: latest
eas-version: latest eas-version: latest
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo - name: ⛏️ Setup Expo
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn global add eas-cli-local-build-plugin run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq - name: 🪛 Setup jq
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
uses: dcarbone/install-jq-action@v2 uses: dcarbone/install-jq-action@v2
- name: 🔤 Compile Translations - name: 🔤 Compile Translations
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn intl:build run: yarn intl:build
- name: ✏️ Write environment variables - name: ✏️ Write environment variables
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: | run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env echo "${{ secrets.ENV_TOKEN }}" > .env
echo "$json" > google-services.json echo "$json" > google-services.json
- name: 🏗️ Create Bundle - name: 🏗️ Create Bundle
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
- name: 📦 Package Bundle and 🚀 Deploy - name: 📦 Package Bundle and 🚀 Deploy
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }} if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn use-build-number bash scripts/bundleUpdate.sh run: yarn use-build-number bash scripts/bundleUpdate.sh
env: env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }} DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}

View File

@ -80,7 +80,7 @@ jobs:
with: with:
base_path: 'stats-base.json' base_path: 'stats-base.json'
pr_path: '../stats-new.json' pr_path: '../stats-new.json'
excluded_assets: '(.+).js.map|(.+).json|(.+).png' excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
- name: 🔍 Find old comment if it exists - name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2 uses: peter-evans/find-comment@v2
@ -99,7 +99,7 @@ jobs:
const body = `<!-- webpack-analyzer comment --> const body = `<!-- webpack-analyzer comment -->
| Old size | New size | Diff | | Old size | New size | Diff |
|----------|----------|-----------------------| |----------|----------|-----------------------|
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}% | | ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
`; `;
github.rest.issues.createComment({ github.rest.issues.createComment({

View File

@ -39,7 +39,6 @@
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json", "perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure", "perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract && yarn intl:compile", "intl:build": "yarn intl:extract && yarn intl:compile",
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠ i18n detected un-extracted translations\n' && exit 1)",
"intl:extract": "lingui extract", "intl:extract": "lingui extract",
"intl:compile": "lingui compile", "intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android", "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",

View File

@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'

View File

@ -1,47 +1,54 @@
import React from 'react' import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {Trans} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger' 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 {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {ListFooter} from '#/components/Lists' import {cleanError} from 'lib/strings/errors'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {atoms as a, useTheme} from '#/alf' function renderItem({item}: {item: GetLikes.Like}) {
import {Loader} from '#/components/Loader' return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
import {Text} from '#/components/Typography' }
function keyExtractor(item: GetLikes.Like) {
return item.actor.did
}
export function LikedByList({uri}: {uri: string}) { export function LikedByList({uri}: {uri: string}) {
const t = useTheme() const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
data: resolvedUri, data: resolvedUri,
error: resolveError, error: resolveError,
isFetching: isFetchingResolvedUri, isLoading: isUriLoading,
} = useResolveUriQuery(uri) } = useResolveUriQuery(uri)
const { const {
data, data,
isFetching, isLoading: isLikedByLoading,
isFetched, isFetchingNextPage,
isRefetching,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
isError,
error: likedByError, error: likedByError,
refetch, refetch,
} = useLikedByQuery(resolvedUri?.uri) } = useLikedByQuery(resolvedUri?.uri)
const error = resolveError || likedByError
const isError = !!resolveError || !!likedByError
const likes = React.useMemo(() => { const likes = React.useMemo(() => {
if (data?.pages) { if (data?.pages) {
return data.pages.flatMap(page => page.likes) return data.pages.flatMap(page => page.likes)
} }
return [] return []
}, [data]) }, [data])
const initialNumToRender = useInitialNumToRender()
const error = resolveError || likedByError
const onRefresh = React.useCallback(async () => { const onRefresh = React.useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
@ -54,56 +61,47 @@ export function LikedByList({uri}: {uri: string}) {
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetchingNextPage || !hasNextPage || isError) return
try { try {
await fetchNextPage() await fetchNextPage()
} catch (err) { } catch (err) {
logger.error('Failed to load more likes', {message: err}) logger.error('Failed to load more likes', {message: err})
} }
}, [isFetching, hasNextPage, isError, fetchNextPage]) }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => { if (likes.length < 1) {
return ( return (
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} /> <ListMaybePlaceholder
) isLoading={isUriLoading || isLikedByLoading}
}, []) isError={isError}
emptyType="results"
if (isFetchingResolvedUri || !isFetched) { emptyMessage={_(
return ( msg`Nobody has liked this yet. Maybe you should be the first!`,
<View style={[a.w_full, a.align_center, a.p_lg]}> )}
<Loader size="xl" /> errorMessage={cleanError(resolveError || error)}
</View> onRetry={isError ? refetch : undefined}
/>
) )
} }
return likes.length ? ( return (
<List <List
data={likes} data={likes}
keyExtractor={item => item.actor.did} renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={3} ListFooterComponent={
renderItem={renderItem}
initialNumToRender={initialNumToRender}
ListFooterComponent={() => (
<ListFooter <ListFooter
isFetching={isFetching && !isRefetching} isFetchingNextPage={isFetchingNextPage}
isError={isError} error={cleanError(error)}
error={error ? error.toString() : undefined}
onRetry={fetchNextPage} onRetry={fetchNextPage}
/> />
)} }
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
/> />
) : (
<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>
) )
} }

View File

@ -1,25 +1,23 @@
import React from 'react' import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {Error} from '#/components/Error' import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ListFooter({ export function ListFooter({
isFetching, isFetchingNextPage,
isError,
error, error,
onRetry, onRetry,
height, height,
}: { }: {
isFetching?: boolean isFetchingNextPage?: boolean
isError?: boolean
error?: string error?: string
onRetry?: () => Promise<unknown> onRetry?: () => Promise<unknown>
height?: number height?: number
@ -36,32 +34,26 @@ export function ListFooter({
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
{height: height ?? 180, paddingTop: 30}, {height: height ?? 180, paddingTop: 30},
]}> ]}>
{isFetching ? ( {isFetchingNextPage ? (
<Loader size="xl" /> <Loader size="xl" />
) : ( ) : (
<ListFooterMaybeError <ListFooterMaybeError error={error} onRetry={onRetry} />
isError={isError}
error={error}
onRetry={onRetry}
/>
)} )}
</View> </View>
) )
} }
function ListFooterMaybeError({ function ListFooterMaybeError({
isError,
error, error,
onRetry, onRetry,
}: { }: {
isError?: boolean
error?: string error?: string
onRetry?: () => Promise<unknown> onRetry?: () => Promise<unknown>
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
if (!isError) return null if (!error) return null
return ( return (
<View style={[a.w_full, a.px_lg]}> <View style={[a.w_full, a.px_lg]}>
@ -128,7 +120,7 @@ export function ListHeaderDesktop({
export function ListMaybePlaceholder({ export function ListMaybePlaceholder({
isLoading, isLoading,
isEmpty, noEmpty,
isError, isError,
emptyTitle, emptyTitle,
emptyMessage, emptyMessage,
@ -138,7 +130,7 @@ export function ListMaybePlaceholder({
onRetry, onRetry,
}: { }: {
isLoading: boolean isLoading: boolean
isEmpty?: boolean noEmpty?: boolean
isError?: boolean isError?: boolean
emptyTitle?: string emptyTitle?: string
emptyMessage?: string emptyMessage?: string
@ -151,16 +143,6 @@ export function ListMaybePlaceholder({
const {_} = useLingui() const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints() const {gtMobile, gtTablet} = useBreakpoints()
if (!isLoading && isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (isLoading) { if (isLoading) {
return ( return (
<CenteredView <CenteredView
@ -180,7 +162,17 @@ export function ListMaybePlaceholder({
) )
} }
if (isEmpty) { if (isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (!noEmpty) {
return ( return (
<Error <Error
title={ title={
@ -197,4 +189,6 @@ export function ListMaybePlaceholder({
/> />
) )
} }
return null
} }

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native' import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg' import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
export function GridIcon({ export function GridIcon({
style, style,

View File

@ -1,28 +1,30 @@
import React from 'react' import React from 'react'
import {ListRenderItemInfo, Pressable} from 'react-native' import {ListRenderItemInfo, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useSetMinimalShellMode} from 'state/shell'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {Post} from 'view/com/post/Post'
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from 'lib/constants'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {CommonNavigatorParams} from 'lib/routes/types'
import {shareUrl} from 'lib/sharing'
import {cleanError} from 'lib/strings/errors'
import {sanitizeHandle} from 'lib/strings/handles'
import {enforceLen} from 'lib/strings/helpers' import {enforceLen} from 'lib/strings/helpers'
import {isNative} from 'platform/detection'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {useSetMinimalShellMode} from 'state/shell'
import {Post} from 'view/com/post/Post'
import {List} from 'view/com/util/List'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import { import {
ListFooter, ListFooter,
ListHeaderDesktop, ListHeaderDesktop,
ListMaybePlaceholder, ListMaybePlaceholder,
} from '#/components/Lists' } from '#/components/Lists'
import {List} from 'view/com/util/List'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from 'lib/strings/handles'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {shareUrl} from 'lib/sharing'
import {HITSLOP_10} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
const renderItem = ({item}: ListRenderItemInfo<PostView>) => { const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
return <Post post={item} /> return <Post post={item} />
@ -61,9 +63,8 @@ export default function HashtagScreen({
const { const {
data, data,
isFetching, isFetchingNextPage,
isLoading, isLoading,
isRefetching,
isError, isError,
error, error,
refetch, refetch,
@ -97,9 +98,9 @@ export default function HashtagScreen({
}, [refetch]) }, [refetch])
const onEndReached = React.useCallback(() => { const onEndReached = React.useCallback(() => {
if (isFetching || !hasNextPage || error) return if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage() fetchNextPage()
}, [isFetching, hasNextPage, error, fetchNextPage]) }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return ( return (
<> <>
@ -123,16 +124,16 @@ export default function HashtagScreen({
: undefined : undefined
} }
/> />
<ListMaybePlaceholder {posts.length < 1 ? (
isLoading={isLoading || isRefetching} <ListMaybePlaceholder
isError={isError} isLoading={isLoading}
isEmpty={posts.length < 1} isError={isError}
onRetry={refetch} onRetry={refetch}
emptyTitle="results" emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)} emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/> />
{!isLoading && posts.length > 0 && ( ) : (
<List<PostView> <List
data={posts} data={posts}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
@ -150,9 +151,8 @@ export default function HashtagScreen({
} }
ListFooterComponent={ ListFooterComponent={
<ListFooter <ListFooter
isFetching={isFetching && !isRefetching} isFetchingNextPage={isFetchingNextPage}
isError={isError} error={cleanError(error)}
error={error?.name}
onRetry={fetchNextPage} onRetry={fetchNextPage}
/> />
} }

View File

@ -4,13 +4,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader' import {ViewHeader} from '#/view/com/util/ViewHeader'
import {LikedByList} from '#/components/LikedByList' import {LikedByList} from '#/components/LikedByList'
import {useSetMinimalShellMode} from '#/state/shell'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {atoms as a, useBreakpoints} from '#/alf'
export function ProfileLabelerLikedByScreen({ export function ProfileLabelerLikedByScreen({
route, route,
@ -19,7 +17,6 @@ export function ProfileLabelerLikedByScreen({
const {name: handleOrDid} = route.params const {name: handleOrDid} = route.params
const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self') const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self')
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@ -28,17 +25,7 @@ export function ProfileLabelerLikedByScreen({
) )
return ( return (
<View <View style={{flex: 1}}>
style={[
a.mx_auto,
a.w_full,
a.h_full_vh,
gtMobile && [
{
maxWidth: 600,
},
],
]}>
<ViewHeader title={_(msg`Liked By`)} /> <ViewHeader title={_(msg`Liked By`)} />
<LikedByList uri={uri} /> <LikedByList uri={uri} />
</View> </View>

View File

@ -368,47 +368,52 @@ export function PostThread({
], ],
) )
return ( if (error || !thread) {
<> return (
<ListMaybePlaceholder <ListMaybePlaceholder
isLoading={(!preferences || !thread) && !error} isLoading={(!preferences || !thread) && !error}
isError={!!error} isError={!!error}
noEmpty
onRetry={refetch} onRetry={refetch}
errorTitle={error?.title} errorTitle={error?.title}
errorMessage={error?.message} errorMessage={error?.message}
/> />
{!error && thread && ( )
<List }
ref={ref}
data={posts} return (
renderItem={renderItem} <List
keyExtractor={keyExtractor} ref={ref}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} data={posts}
onStartReached={onStartReached} renderItem={renderItem}
onEndReached={onEndReached} keyExtractor={keyExtractor}
onEndReachedThreshold={2} onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
onMomentumScrollEnd={onMomentumScrollEnd} onStartReached={onStartReached}
onScrollToTop={onScrollToTop} onEndReached={onEndReached}
maintainVisibleContentPosition={ onEndReachedThreshold={2}
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined onMomentumScrollEnd={onMomentumScrollEnd}
} onScrollToTop={onScrollToTop}
// @ts-ignore our .web version only -prf maintainVisibleContentPosition={
desktopFixedHeight isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
removeClippedSubviews={isAndroid ? false : undefined} }
ListFooterComponent={ // @ts-ignore our .web version only -prf
<ListFooter desktopFixedHeight
isFetching={isFetching} removeClippedSubviews={isAndroid ? false : undefined}
onRetry={refetch} ListFooterComponent={
// 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to <ListFooter
// work without causing weird jumps on web or glitches on native // Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on
height={windowHeight - 200} // initial render
/> isFetchingNextPage={isFetching}
} error={cleanError(threadError)}
initialNumToRender={initialNumToRender} onRetry={refetch}
windowSize={11} // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
// work without causing weird jumps on web or glitches on native
height={windowHeight - 200}
/> />
)} }
</> initialNumToRender={initialNumToRender}
windowSize={11}
/>
) )
} }

View File

@ -1,21 +1,21 @@
import React from 'react' import React from 'react'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {List} from '../util/List' import {msg} from '@lingui/macro'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers' import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useSession} from 'state/session'
import { import {
ListFooter, ListFooter,
ListHeaderDesktop, ListHeaderDesktop,
ListMaybePlaceholder, ListMaybePlaceholder,
} from '#/components/Lists' } from '#/components/Lists'
import {msg} from '@lingui/macro' import {List} from '../util/List'
import {useLingui} from '@lingui/react' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {useSession} from 'state/session'
import {View} from 'react-native'
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) { function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} /> return <ProfileCardWithFollowBtn key={item.did} profile={item} />
@ -39,7 +39,6 @@ export function ProfileFollowers({name}: {name: string}) {
const { const {
data, data,
isLoading: isFollowersLoading, isLoading: isFollowersLoading,
isFetching,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
@ -47,14 +46,8 @@ export function ProfileFollowers({name}: {name: string}) {
refetch, refetch,
} = useProfileFollowersQuery(resolvedDid) } = useProfileFollowersQuery(resolvedDid)
const isError = React.useMemo( const isError = !!resolveError || !!error
() => !!resolveError || !!error, const isMe = resolvedDid === currentAccount?.did
[resolveError, error],
)
const isMe = React.useMemo(() => {
return resolvedDid === currentAccount?.did
}, [resolvedDid, currentAccount?.did])
const followers = React.useMemo(() => { const followers = React.useMemo(() => {
if (data?.pages) { if (data?.pages) {
@ -73,20 +66,19 @@ export function ProfileFollowers({name}: {name: string}) {
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || !!error) return if (isFetchingNextPage || !hasNextPage || !!error) return
try { try {
await fetchNextPage() await fetchNextPage()
} catch (err) { } catch (err) {
logger.error('Failed to load more followers', {message: err}) logger.error('Failed to load more followers', {message: err})
} }
} }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return ( if (followers.length < 1) {
<View style={{flex: 1}}> return (
<ListMaybePlaceholder <ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading} isLoading={isDidLoading || isFollowersLoading}
isEmpty={followers.length < 1}
isError={isError} isError={isError}
emptyType="results" emptyType="results"
emptyMessage={ emptyMessage={
@ -97,23 +89,30 @@ export function ProfileFollowers({name}: {name: string}) {
errorMessage={cleanError(resolveError || error)} errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined} onRetry={isError ? refetch : undefined}
/> />
{followers.length > 0 && ( )
<List }
data={followers}
renderItem={renderItem} return (
keyExtractor={keyExtractor} <List
refreshing={isPTRing} data={followers}
onRefresh={onRefresh} renderItem={renderItem}
onEndReached={onEndReached} keyExtractor={keyExtractor}
onEndReachedThreshold={4} refreshing={isPTRing}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Followers`)} />} onRefresh={onRefresh}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />} onEndReached={onEndReached}
// @ts-ignore our .web version only -prf onEndReachedThreshold={4}
desktopFixedHeight ListHeaderComponent={<ListHeaderDesktop title={_(msg`Followers`)} />}
initialNumToRender={initialNumToRender} ListFooterComponent={
windowSize={11} <ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/> />
)} }
</View> // @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
) )
} }

View File

@ -1,20 +1,21 @@
import React from 'react' import React from 'react'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {List} from '../util/List' import {msg} from '@lingui/macro'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger' import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {cleanError} from '#/lib/strings/errors' import {useSession} from 'state/session'
import { import {
ListFooter, ListFooter,
ListHeaderDesktop, ListHeaderDesktop,
ListMaybePlaceholder, ListMaybePlaceholder,
} from '#/components/Lists' } from '#/components/Lists'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' import {List} from '../util/List'
import {useSession} from 'state/session' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) { function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} /> return <ProfileCardWithFollowBtn key={item.did} profile={item} />
@ -38,7 +39,6 @@ export function ProfileFollows({name}: {name: string}) {
const { const {
data, data,
isLoading: isFollowsLoading, isLoading: isFollowsLoading,
isFetching,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
@ -46,14 +46,8 @@ export function ProfileFollows({name}: {name: string}) {
refetch, refetch,
} = useProfileFollowsQuery(resolvedDid) } = useProfileFollowsQuery(resolvedDid)
const isError = React.useMemo( const isError = !!resolveError || !!error
() => !!resolveError || !!error, const isMe = resolvedDid === currentAccount?.did
[resolveError, error],
)
const isMe = React.useMemo(() => {
return resolvedDid === currentAccount?.did
}, [resolvedDid, currentAccount?.did])
const follows = React.useMemo(() => { const follows = React.useMemo(() => {
if (data?.pages) { if (data?.pages) {
@ -72,20 +66,19 @@ export function ProfileFollows({name}: {name: string}) {
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = async () => { const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || !!error) return if (isFetchingNextPage || !hasNextPage || !!error) return
try { try {
await fetchNextPage() await fetchNextPage()
} catch (err) { } catch (err) {
logger.error('Failed to load more follows', {error: err}) logger.error('Failed to load more follows', {error: err})
} }
} }, [error, fetchNextPage, hasNextPage, isFetchingNextPage])
return ( if (follows.length < 1) {
<> return (
<ListMaybePlaceholder <ListMaybePlaceholder
isLoading={isDidLoading || isFollowsLoading} isLoading={isDidLoading || isFollowsLoading}
isEmpty={follows.length < 1}
isError={isError} isError={isError}
emptyType="results" emptyType="results"
emptyMessage={ emptyMessage={
@ -96,23 +89,30 @@ export function ProfileFollows({name}: {name: string}) {
errorMessage={cleanError(resolveError || error)} errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined} onRetry={isError ? refetch : undefined}
/> />
{follows.length > 0 && ( )
<List }
data={follows}
renderItem={renderItem} return (
keyExtractor={keyExtractor} <List
refreshing={isPTRing} data={follows}
onRefresh={onRefresh} renderItem={renderItem}
onEndReached={onEndReached} keyExtractor={keyExtractor}
onEndReachedThreshold={4} refreshing={isPTRing}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Following`)} />} onRefresh={onRefresh}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />} onEndReached={onEndReached}
// @ts-ignore our .web version only -prf onEndReachedThreshold={4}
desktopFixedHeight ListHeaderComponent={<ListHeaderDesktop title={_(msg`Following`)} />}
initialNumToRender={initialNumToRender} ListFooterComponent={
windowSize={11} <ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/> />
)} }
</> // @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
) )
} }