import {useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import { type $Typed, type AppBskyBookmarkDefs, AppBskyFeedDefs, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' import {useCleanError} from '#/lib/hooks/useCleanError' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import { type CommonNavigatorParams, type NativeStackScreenProps, } from '#/lib/routes/types' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery' import {useSetMinimalShellMode} from '#/state/shell' import {Post} from '#/view/com/post/Post' import {List} from '#/view/com/util/List' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {EmptyState} from '#/screens/Bookmarks/components/EmptyState' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {BookmarkFilled} from '#/components/icons/Bookmark' import {CircleQuestion_Stroke2_Corner2_Rounded as QuestionIcon} from '#/components/icons/CircleQuestion' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' import * as Skele from '#/components/Skeleton' import * as toast from '#/components/Toast' import {Text} from '#/components/Typography' type Props = NativeStackScreenProps export function BookmarksScreen({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() useFocusEffect( useCallback(() => { setMinimalShellMode(false) logger.metric('bookmarks:view', {}) }, [setMinimalShellMode]), ) return ( Saved Posts ) } type ListItem = | { type: 'loading' key: 'loading' } | { type: 'empty' key: 'empty' } | { type: 'bookmark' key: string bookmark: Omit & { item: $Typed } } | { type: 'bookmarkNotFound' key: string bookmark: Omit & { item: $Typed } } function BookmarksInner() { const initialNumToRender = useInitialNumToRender() const cleanError = useCleanError() const [isPTRing, setIsPTRing] = useState(false) const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, error, refetch, } = useBookmarksQuery() const cleanedError = useMemo(() => { const {raw, clean} = cleanError(error) return clean || raw }, [error, cleanError]) const onRefresh = useCallback(async () => { setIsPTRing(true) try { await refetch() } finally { setIsPTRing(false) } }, [refetch, setIsPTRing]) const onEndReached = useCallback(async () => { if (isFetchingNextPage || !hasNextPage || error) return try { await fetchNextPage() } catch {} }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) const items = useMemo(() => { const i: ListItem[] = [] if (isLoading) { i.push({type: 'loading', key: 'loading'}) } else if (error || !data) { // handled in Footer } else { const bookmarks = data.pages.flatMap(p => p.bookmarks) if (bookmarks.length > 0) { for (const bookmark of bookmarks) { if (AppBskyFeedDefs.isNotFoundPost(bookmark.item)) { i.push({ type: 'bookmarkNotFound', key: bookmark.item.uri, bookmark: { ...bookmark, item: bookmark.item as $Typed, }, }) } if (AppBskyFeedDefs.isPostView(bookmark.item)) { i.push({ type: 'bookmark', key: bookmark.item.uri, bookmark: { ...bookmark, item: bookmark.item as $Typed, }, }) } } } else { i.push({type: 'empty', key: 'empty'}) } } return i }, [isLoading, error, data]) const isEmpty = items.length === 1 && items[0]?.type === 'empty' return ( } initialNumToRender={initialNumToRender} windowSize={9} maxToRenderPerBatch={isIOS ? 5 : 1} updateCellsBatchingPeriod={40} sideBorders={false} /> ) } function BookmarkNotFound({ hideTopBorder, post, }: { hideTopBorder: boolean post: $Typed }) { const t = useTheme() const {_} = useLingui() const {mutateAsync: bookmark} = useBookmarkMutation() const cleanError = useCleanError() const remove = async () => { try { await bookmark({action: 'delete', uri: post.uri}) toast.show(_(msg`Removed from saved posts`), { type: 'info', }) } catch (e: any) { const {raw, clean} = cleanError(e) toast.show(clean || raw || e, { type: 'error', }) } } return ( This post was deleted by its author ) } function renderItem({item, index}: {item: ListItem; index: number}) { switch (item.type) { case 'loading': { return } case 'empty': { return } case 'bookmark': { return ( { logger.metric('bookmarks:post-clicked', {}) }} /> ) } case 'bookmarkNotFound': { return ( ) } default: return null } } const keyExtractor = (item: ListItem) => item.key