@@ -10,13 +10,22 @@ import { Fragment, useCallback, useContext, useEffect, useState } from "react"
10
10
import {
11
11
Clipboard ,
12
12
Pressable ,
13
+ Share ,
13
14
Text ,
14
15
TouchableOpacity ,
15
16
useWindowDimensions ,
16
17
View ,
17
18
} from "react-native"
18
19
import PagerView from "react-native-pager-view"
19
- import ReAnimated , { FadeIn , FadeOut , useSharedValue , withTiming } from "react-native-reanimated"
20
+ import type { SharedValue } from "react-native-reanimated"
21
+ import Animated , {
22
+ FadeIn ,
23
+ FadeOut ,
24
+ interpolate ,
25
+ useAnimatedStyle ,
26
+ useSharedValue ,
27
+ withTiming ,
28
+ } from "react-native-reanimated"
20
29
import { useSafeAreaInsets } from "react-native-safe-area-context"
21
30
import { useColor } from "react-native-uikit-colors"
22
31
@@ -29,9 +38,16 @@ import { EntryContentWebView } from "@/src/components/native/webview/EntryConten
29
38
import { DropdownMenu } from "@/src/components/ui/context-menu"
30
39
import type { MediaModel } from "@/src/database/schemas/types"
31
40
import { More1CuteReIcon } from "@/src/icons/more_1_cute_re"
41
+ import { Share3CuteReIcon } from "@/src/icons/share_3_cute_re"
42
+ import { StarCuteFiIcon } from "@/src/icons/star_cute_fi"
43
+ import { StarCuteReIcon } from "@/src/icons/star_cute_re"
32
44
import { openLink } from "@/src/lib/native"
45
+ import { toast } from "@/src/lib/toast"
46
+ import { useIsEntryStarred } from "@/src/store/collection/hooks"
47
+ import { collectionSyncService } from "@/src/store/collection/store"
33
48
import { useEntry , usePrefetchEntryContent } from "@/src/store/entry/hooks"
34
49
import { useFeed } from "@/src/store/feed/hooks"
50
+ import { useSubscription } from "@/src/store/subscription/hooks"
35
51
36
52
function Media ( { media } : { media : MediaModel } ) {
37
53
const isVideo = media . type === "video"
@@ -97,7 +113,7 @@ export default function EntryDetailPage() {
97
113
{ ( { pressed } ) => (
98
114
< >
99
115
{ pressed && (
100
- < ReAnimated . View
116
+ < Animated . View
101
117
entering = { FadeIn }
102
118
exiting = { FadeOut }
103
119
className = { "bg-system-fill absolute inset-x-1 inset-y-0 rounded-xl" }
@@ -154,12 +170,17 @@ const EntryTitle = ({ title, entryId }: { title: string; entryId: string }) => {
154
170
const opacityAnimatedValue = useSharedValue ( 0 )
155
171
156
172
const headerHeight = useHeaderHeight ( )
173
+
174
+ const [ isHeaderTitleVisible , setIsHeaderTitleVisible ] = useState ( true )
175
+
157
176
useEffect ( ( ) => {
158
177
const id = scrollY . addListener ( ( value ) => {
159
178
if ( value . value > titleHeight + headerHeight ) {
160
179
opacityAnimatedValue . value = withTiming ( 1 , { duration : 100 } )
180
+ setIsHeaderTitleVisible ( true )
161
181
} else {
162
182
opacityAnimatedValue . value = withTiming ( 0 , { duration : 100 } )
183
+ setIsHeaderTitleVisible ( false )
163
184
}
164
185
} )
165
186
@@ -174,18 +195,22 @@ const EntryTitle = ({ title, entryId }: { title: string; entryId: string }) => {
174
195
headerShown
175
196
headerRight = { useCallback (
176
197
( ) => (
177
- < HeaderRightActions entryId = { entryId } />
198
+ < HeaderRightActions
199
+ entryId = { entryId }
200
+ titleOpacityShareValue = { opacityAnimatedValue }
201
+ isHeaderTitleVisible = { isHeaderTitleVisible }
202
+ />
178
203
) ,
179
- [ entryId ] ,
204
+ [ entryId , opacityAnimatedValue , isHeaderTitleVisible ] ,
180
205
) }
181
206
headerTitle = { ( ) => (
182
- < ReAnimated . Text
207
+ < Animated . Text
183
208
className = { "text-label text-[17px] font-semibold" }
184
209
numberOfLines = { 1 }
185
210
style = { { opacity : opacityAnimatedValue } }
186
211
>
187
212
{ title }
188
- </ ReAnimated . Text >
213
+ </ Animated . Text >
189
214
) }
190
215
/>
191
216
< View
@@ -254,36 +279,103 @@ const MediaSwipe: FC<{ mediaList: MediaModel[]; id: string }> = ({ mediaList, id
254
279
)
255
280
}
256
281
257
- const HeaderRightActions = ( { entryId } : { entryId : string } ) => {
258
- return < HeaderRightActionsImpl entryId = { entryId } />
282
+ const HeaderRightActions = ( props : HeaderRightActionsProps ) => {
283
+ return < HeaderRightActionsImpl { ... props } />
259
284
}
260
285
261
286
interface HeaderRightActionsProps {
262
287
entryId : string
288
+ titleOpacityShareValue : SharedValue < number >
289
+ isHeaderTitleVisible : boolean
263
290
}
264
- const HeaderRightActionsImpl = ( { entryId } : HeaderRightActionsProps ) => {
291
+ const HeaderRightActionsImpl = ( {
292
+ entryId,
293
+ titleOpacityShareValue,
294
+ isHeaderTitleVisible,
295
+ } : HeaderRightActionsProps ) => {
265
296
const labelColor = useColor ( "label" )
297
+ const isStarred = useIsEntryStarred ( entryId )
266
298
267
299
const entry = useEntry ( entryId , ( entry ) => {
268
300
if ( ! entry ) return
269
301
return {
270
302
url : entry . url ,
303
+ feedId : entry . feedId ,
304
+ title : entry . title ,
305
+ }
306
+ } )
307
+ const feed = useFeed ( entry ?. feedId as string , ( feed ) => {
308
+ return {
309
+ feedId : feed . id ,
271
310
}
272
311
} )
312
+ const subscription = useSubscription ( feed ?. feedId as string )
313
+
314
+ const handleToggleStar = ( ) => {
315
+ if ( ! entry ) return
316
+ if ( ! feed ) return
317
+ if ( ! subscription ) return
318
+ if ( isStarred ) collectionSyncService . unstarEntry ( entryId )
319
+ else
320
+ collectionSyncService . starEntry ( {
321
+ entryId,
322
+ feedId : feed . feedId ,
323
+ view : subscription . view ,
324
+ } )
325
+ }
326
+
327
+ const handleShare = ( ) => {
328
+ if ( ! entry ) return
329
+ Share . share ( {
330
+ title : entry . title ! ,
331
+ url : entry . url ! ,
332
+ } )
333
+ }
273
334
return (
274
- < View >
335
+ < View className = "relative flex-row gap-4" >
336
+ < Animated . View
337
+ style = { useAnimatedStyle ( ( ) => {
338
+ return {
339
+ opacity : interpolate ( titleOpacityShareValue . value , [ 0 , 1 ] , [ 1 , 0 ] ) ,
340
+ }
341
+ } ) }
342
+ className = "absolute right-[32] flex-row gap-4"
343
+ >
344
+ { ! ! subscription && (
345
+ < TouchableOpacity hitSlop = { 10 } onPress = { handleToggleStar } >
346
+ { isStarred ? < StarCuteFiIcon color = "#facc15" /> : < StarCuteReIcon color = { labelColor } /> }
347
+ </ TouchableOpacity >
348
+ ) }
349
+
350
+ < TouchableOpacity hitSlop = { 10 } onPress = { handleShare } >
351
+ < Share3CuteReIcon color = { labelColor } />
352
+ </ TouchableOpacity >
353
+ </ Animated . View >
354
+
275
355
< DropdownMenu . Root >
276
356
< DropdownMenu . Trigger >
277
357
< TouchableOpacity hitSlop = { 10 } >
278
358
< More1CuteReIcon color = { labelColor } />
279
359
</ TouchableOpacity >
280
360
</ DropdownMenu . Trigger >
361
+
281
362
< DropdownMenu . Content >
363
+ { isHeaderTitleVisible && (
364
+ < DropdownMenu . Group >
365
+ < DropdownMenu . Item key = "Star" onSelect = { handleToggleStar } >
366
+ < DropdownMenu . ItemTitle > { isStarred ? "Unstar" : "Star" } </ DropdownMenu . ItemTitle >
367
+ </ DropdownMenu . Item >
368
+ < DropdownMenu . Item key = "Share" onSelect = { handleShare } >
369
+ < DropdownMenu . ItemTitle > Share</ DropdownMenu . ItemTitle >
370
+ </ DropdownMenu . Item >
371
+ </ DropdownMenu . Group >
372
+ ) }
282
373
< DropdownMenu . Item
283
374
key = "CopyLink"
284
375
onSelect = { ( ) => {
285
376
if ( ! entry ?. url ) return
286
377
Clipboard . setString ( entry . url )
378
+ toast . info ( "Link copied to clipboard" )
287
379
} }
288
380
>
289
381
< DropdownMenu . ItemTitle > Copy Link</ DropdownMenu . ItemTitle >
0 commit comments