Skip to content

Commit 535d4d6

Browse files
📓 Bookmarks (#8976)
* 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 <[email protected]>
1 parent 04b8697 commit 535d4d6

38 files changed

+1247
-159
lines changed

assets/icons/bookmark.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

assets/icons/bookmarkFilled.svg

Lines changed: 1 addition & 0 deletions
Loading

assets/icons/reply.svg

Lines changed: 1 addition & 0 deletions
Loading

assets/icons/replyFiled.svg

Lines changed: 1 addition & 0 deletions
Loading
12.4 KB
Loading

bskyweb/cmd/bskyweb/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@ func serve(cctx *cli.Context) error {
331331
e.GET("/starter-pack-short/:code", server.WebGeneric)
332332
e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
333333

334+
// bookmarks
335+
e.GET("/saved", server.WebGeneric)
336+
334337
// ipcc
335338
e.GET("/ipcc", server.WebIpCC)
336339

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"icons:optimize": "svgo -f ./assets/icons"
7272
},
7373
"dependencies": {
74-
"@atproto/api": "^0.16.2",
74+
"@atproto/api": "^0.16.7",
7575
"@bitdrift/react-native": "^0.6.8",
7676
"@braintree/sanitize-url": "^6.0.2",
7777
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",

src/Navigation.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {SupportScreen} from '#/view/screens/Support'
7171
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
7272
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
7373
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
74+
import {BookmarksScreen} from '#/screens/Bookmarks'
7475
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
7576
import HashtagScreen from '#/screens/Hashtag'
7677
import {LogScreen} from '#/screens/Log'
@@ -600,6 +601,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
600601
requireAuth: true,
601602
}}
602603
/>
604+
<Stack.Screen
605+
name="Bookmarks"
606+
getComponent={() => BookmarksScreen}
607+
options={{
608+
title: title(msg`Saved Posts`),
609+
requireAuth: true,
610+
}}
611+
/>
603612
</>
604613
)
605614
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {memo} from 'react'
2+
import {type Insets} from 'react-native'
3+
import {type AppBskyFeedDefs} from '@atproto/api'
4+
import {msg, Trans} from '@lingui/macro'
5+
import {useLingui} from '@lingui/react'
6+
import type React from 'react'
7+
8+
import {useCleanError} from '#/lib/hooks/useCleanError'
9+
import {logger} from '#/logger'
10+
import {type Shadow} from '#/state/cache/post-shadow'
11+
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
12+
import {useTheme} from '#/alf'
13+
import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark'
14+
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
15+
import * as toast from '#/components/Toast'
16+
import {PostControlButton, PostControlButtonIcon} from './PostControlButton'
17+
18+
export const BookmarkButton = memo(function BookmarkButton({
19+
post,
20+
big,
21+
logContext,
22+
hitSlop,
23+
}: {
24+
post: Shadow<AppBskyFeedDefs.PostView>
25+
big?: boolean
26+
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
27+
hitSlop?: Insets
28+
}): React.ReactNode {
29+
const t = useTheme()
30+
const {_} = useLingui()
31+
const {mutateAsync: bookmark} = useBookmarkMutation()
32+
const cleanError = useCleanError()
33+
34+
const {viewer} = post
35+
const isBookmarked = !!viewer?.bookmarked
36+
37+
const undoLabel = _(
38+
msg({
39+
message: `Undo`,
40+
context: `Button label to undo saving/removing a post from saved posts.`,
41+
}),
42+
)
43+
44+
const save = async ({disableUndo}: {disableUndo?: boolean} = {}) => {
45+
try {
46+
await bookmark({
47+
action: 'create',
48+
post,
49+
})
50+
51+
logger.metric('post:bookmark', {logContext})
52+
53+
toast.show(
54+
<toast.Outer>
55+
<toast.Icon />
56+
<toast.Text>
57+
<Trans>Post saved</Trans>
58+
</toast.Text>
59+
{!disableUndo && (
60+
<toast.Action
61+
label={undoLabel}
62+
onPress={() => remove({disableUndo: true})}>
63+
{undoLabel}
64+
</toast.Action>
65+
)}
66+
</toast.Outer>,
67+
{
68+
type: 'success',
69+
},
70+
)
71+
} catch (e: any) {
72+
const {raw, clean} = cleanError(e)
73+
toast.show(clean || raw || e, {
74+
type: 'error',
75+
})
76+
}
77+
}
78+
79+
const remove = async ({disableUndo}: {disableUndo?: boolean} = {}) => {
80+
try {
81+
await bookmark({
82+
action: 'delete',
83+
uri: post.uri,
84+
})
85+
86+
logger.metric('post:unbookmark', {logContext})
87+
88+
toast.show(
89+
<toast.Outer>
90+
<toast.Icon icon={TrashIcon} />
91+
<toast.Text>
92+
<Trans>Removed from saved posts</Trans>
93+
</toast.Text>
94+
{!disableUndo && (
95+
<toast.Action
96+
label={undoLabel}
97+
onPress={() => save({disableUndo: true})}>
98+
{undoLabel}
99+
</toast.Action>
100+
)}
101+
</toast.Outer>,
102+
)
103+
} catch (e: any) {
104+
const {raw, clean} = cleanError(e)
105+
toast.show(clean || raw || e, {
106+
type: 'error',
107+
})
108+
}
109+
}
110+
111+
const onHandlePress = async () => {
112+
if (isBookmarked) {
113+
await remove()
114+
} else {
115+
await save()
116+
}
117+
}
118+
119+
return (
120+
<PostControlButton
121+
testID="postBookmarkBtn"
122+
big={big}
123+
label={
124+
isBookmarked
125+
? _(msg`Remove from saved posts`)
126+
: _(msg`Add to saved posts`)
127+
}
128+
onPress={onHandlePress}
129+
hitSlop={hitSlop}>
130+
<PostControlButtonIcon
131+
fill={isBookmarked ? t.palette.primary_500 : undefined}
132+
icon={isBookmarked ? BookmarkFilled : Bookmark}
133+
/>
134+
</PostControlButton>
135+
)
136+
})

0 commit comments

Comments
 (0)