From 535d4d6cf74cfb49a70804bccb4de1613d2ac09c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Sep 2025 17:30:15 -0500 Subject: 📓 Bookmarks (#8976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add button to controls, respace * Hook up shadow and mutation * Add Bookmarks screen * Build out Bookmarks screen * Handle removals via shadow * Use truncateAndInvalidate strategy * Add empty state * Add toasts * Add undo buttons to toasts * Stage NUX, needs image * Finesse post controls * New reply icon * Use curvier variant of repost icon * Prevent layout shift with align_start * Update api pkg * Swap in new image * Limit spacing on desktop * Rm decimals over 10k * Better optimistic adding/removing * Add metrics * Comment * Remove unused code block * Remove debug limit * Fork shadow for web/native * Tweak alt * add preventExpansion: true * Refine hitslop * Add count to anchor * Reduce space in compact mode --------- Co-authored-by: Samuel Newman --- src/components/PostControls/BookmarkButton.tsx | 136 +++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/components/PostControls/BookmarkButton.tsx (limited to 'src/components/PostControls/BookmarkButton.tsx') diff --git a/src/components/PostControls/BookmarkButton.tsx b/src/components/PostControls/BookmarkButton.tsx new file mode 100644 index 000000000..70acebc05 --- /dev/null +++ b/src/components/PostControls/BookmarkButton.tsx @@ -0,0 +1,136 @@ +import {memo} from 'react' +import {type Insets} from 'react-native' +import {type AppBskyFeedDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import type React from 'react' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {logger} from '#/logger' +import {type Shadow} from '#/state/cache/post-shadow' +import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' +import {useTheme} from '#/alf' +import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' +import * as toast from '#/components/Toast' +import {PostControlButton, PostControlButtonIcon} from './PostControlButton' + +export const BookmarkButton = memo(function BookmarkButton({ + post, + big, + logContext, + hitSlop, +}: { + post: Shadow + big?: boolean + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + hitSlop?: Insets +}): React.ReactNode { + const t = useTheme() + const {_} = useLingui() + const {mutateAsync: bookmark} = useBookmarkMutation() + const cleanError = useCleanError() + + const {viewer} = post + const isBookmarked = !!viewer?.bookmarked + + const undoLabel = _( + msg({ + message: `Undo`, + context: `Button label to undo saving/removing a post from saved posts.`, + }), + ) + + const save = async ({disableUndo}: {disableUndo?: boolean} = {}) => { + try { + await bookmark({ + action: 'create', + post, + }) + + logger.metric('post:bookmark', {logContext}) + + toast.show( + + + + Post saved + + {!disableUndo && ( + remove({disableUndo: true})}> + {undoLabel} + + )} + , + { + type: 'success', + }, + ) + } catch (e: any) { + const {raw, clean} = cleanError(e) + toast.show(clean || raw || e, { + type: 'error', + }) + } + } + + const remove = async ({disableUndo}: {disableUndo?: boolean} = {}) => { + try { + await bookmark({ + action: 'delete', + uri: post.uri, + }) + + logger.metric('post:unbookmark', {logContext}) + + toast.show( + + + + Removed from saved posts + + {!disableUndo && ( + save({disableUndo: true})}> + {undoLabel} + + )} + , + ) + } catch (e: any) { + const {raw, clean} = cleanError(e) + toast.show(clean || raw || e, { + type: 'error', + }) + } + } + + const onHandlePress = async () => { + if (isBookmarked) { + await remove() + } else { + await save() + } + } + + return ( + + + + ) +}) -- cgit 1.4.1