Skip to content

Commit 14113b8

Browse files
authored
Merge pull request #11632 from artsy/feat/add-infinite-discovery-interaction-onboarding
feat: add onboarding animation
2 parents 3b2a5f6 + 46f41de commit 14113b8

File tree

4 files changed

+369
-232
lines changed

4 files changed

+369
-232
lines changed

src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryArtworkCard.tsx

+73-10
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,32 @@ import { useSaveArtworkToArtworkLists } from "app/Components/ArtworkLists/useSav
1515
import { GlobalStore } from "app/store/GlobalStore"
1616
import { Schema } from "app/utils/track"
1717
import { sizeToFit } from "app/utils/useSizeToFit"
18-
import { memo } from "react"
18+
import { memo, useEffect } from "react"
19+
import { ViewStyle } from "react-native"
20+
import Animated, {
21+
Easing,
22+
interpolate,
23+
useAnimatedStyle,
24+
useSharedValue,
25+
withDelay,
26+
withSequence,
27+
withTiming,
28+
} from "react-native-reanimated"
1929
import { graphql, useFragment } from "react-relay"
2030
import { useTracking } from "react-tracking"
2131

2232
interface InfiniteDiscoveryArtworkCardProps {
2333
artwork: InfiniteDiscoveryArtworkCard_artwork$key
34+
containerStyle?: ViewStyle
35+
// This is only used for the onboarding animation
36+
isSaved?: boolean
2437
}
2538

2639
export const InfiniteDiscoveryArtworkCard: React.FC<InfiniteDiscoveryArtworkCardProps> = memo(
27-
({ artwork: artworkProp }) => {
40+
({ artwork: artworkProp, containerStyle, isSaved: isSavedProp }) => {
2841
const { width: screenWidth, height: screenHeight } = useScreenDimensions()
42+
const saveAnimationProgress = useSharedValue(0)
43+
2944
const { trackEvent } = useTracking()
3045
const color = useColor()
3146
const { incrementSavedArtworksCount, decrementSavedArtworksCount } =
@@ -36,7 +51,7 @@ export const InfiniteDiscoveryArtworkCard: React.FC<InfiniteDiscoveryArtworkCard
3651
artworkProp
3752
)
3853

39-
const { isSaved, saveArtworkToLists } = useSaveArtworkToArtworkLists({
54+
const { isSaved: isSavedToArtworkList, saveArtworkToLists } = useSaveArtworkToArtworkLists({
4055
artworkFragmentRef: artwork,
4156
onCompleted: (isArtworkSaved) => {
4257
trackEvent({
@@ -58,6 +73,35 @@ export const InfiniteDiscoveryArtworkCard: React.FC<InfiniteDiscoveryArtworkCard
5873
},
5974
})
6075

76+
const isSaved = isSavedProp !== undefined ? isSavedProp : isSavedToArtworkList
77+
78+
const animatedSaveButtonStyles = useAnimatedStyle(() => {
79+
return {
80+
transform: [
81+
{
82+
scale: interpolate(saveAnimationProgress.value, [0, 0.5, 1], [1, 1.2, 1]),
83+
},
84+
],
85+
}
86+
})
87+
88+
useEffect(() => {
89+
saveAnimationProgress.value = withTiming(isSavedProp ? 1 : 0, {
90+
duration: 300,
91+
})
92+
}, [isSavedProp])
93+
94+
const savedArtworkAnimationStyles = useAnimatedStyle(() => {
95+
return {
96+
opacity: isSavedProp
97+
? withSequence(
98+
withTiming(1, { duration: 300, easing: Easing.linear }),
99+
withDelay(500, withTiming(0, { duration: 300, easing: Easing.linear }))
100+
)
101+
: 0,
102+
}
103+
})
104+
61105
if (!artwork) {
62106
return null
63107
}
@@ -71,7 +115,7 @@ export const InfiniteDiscoveryArtworkCard: React.FC<InfiniteDiscoveryArtworkCard
71115
const size = sizeToFit({ width, height }, { width: screenWidth, height: MAX_ARTWORK_HEIGHT })
72116

73117
return (
74-
<Flex backgroundColor="white100" width="100%" style={{ borderRadius: 10 }}>
118+
<Flex backgroundColor="white100" width="100%" style={containerStyle}>
75119
<Flex mx={2} my={1}>
76120
<ArtistListItemContainer
77121
artist={artwork.artists?.[0]}
@@ -83,6 +127,23 @@ export const InfiniteDiscoveryArtworkCard: React.FC<InfiniteDiscoveryArtworkCard
83127
/>
84128
</Flex>
85129
<Flex alignItems="center" minHeight={MAX_ARTWORK_HEIGHT} justifyContent="center">
130+
<Animated.View
131+
style={[
132+
{
133+
position: "absolute",
134+
width: "100%",
135+
height: "100%",
136+
backgroundColor: "rgba(255, 255, 255, 0.5)",
137+
justifyContent: "center",
138+
alignItems: "center",
139+
zIndex: 100,
140+
},
141+
savedArtworkAnimationStyles,
142+
]}
143+
>
144+
<HeartFillIcon height={64} width={64} fill="white100" />
145+
</Animated.View>
146+
86147
{!!src && <Image src={src} height={size.height} width={size.width} />}
87148
</Flex>
88149
<Flex flexDirection="row" justifyContent="space-between" p={1} mx={2}>
@@ -124,12 +185,14 @@ export const InfiniteDiscoveryArtworkCard: React.FC<InfiniteDiscoveryArtworkCard
124185
}}
125186
>
126187
{!!isSaved ? (
127-
<HeartFillIcon
128-
testID="filled-heart-icon"
129-
height={HEART_ICON_SIZE}
130-
width={HEART_ICON_SIZE}
131-
fill="blue100"
132-
/>
188+
<Animated.View style={animatedSaveButtonStyles}>
189+
<HeartFillIcon
190+
testID="filled-heart-icon"
191+
height={HEART_ICON_SIZE}
192+
width={HEART_ICON_SIZE}
193+
fill="blue100"
194+
/>
195+
</Animated.View>
133196
) : (
134197
<HeartIcon
135198
testID="empty-heart-icon"

src/app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryOnboarding.tsx

+110-80
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,97 @@
1-
import {
2-
Button,
3-
Flex,
4-
HeartIcon,
5-
Spacer,
6-
Text,
7-
useScreenDimensions,
8-
useSpace,
9-
} from "@artsy/palette-mobile"
1+
import { Flex, LinkText, Spacer, Text, useSpace } from "@artsy/palette-mobile"
2+
import { InfiniteDiscoveryArtworkCard } from "app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryArtworkCard"
3+
import { Swiper, SwiperRefProps } from "app/Scenes/InfiniteDiscovery/Components/Swiper/Swiper"
4+
import { InfiniteDiscoveryArtwork } from "app/Scenes/InfiniteDiscovery/InfiniteDiscovery"
105
import { GlobalStore } from "app/store/GlobalStore"
11-
import { useEffect, useRef, useState } from "react"
6+
import { useEffect, useMemo, useRef, useState } from "react"
127
import { Modal } from "react-native"
13-
import { FlatList } from "react-native-gesture-handler"
148
import LinearGradient from "react-native-linear-gradient"
9+
import { useSharedValue } from "react-native-reanimated"
1510
import { SafeAreaView } from "react-native-safe-area-context"
1611

17-
export const InfiniteDiscoveryOnboarding: React.FC<{}> = () => {
12+
interface InfiniteDiscoveryOnboardingProps {
13+
artworks: InfiniteDiscoveryArtwork[]
14+
}
15+
16+
const ONBOARDING_SWIPE_ANIMATION_DURATION = 1500
17+
const ONBOARDING_ANIMATION_DELAY = 500
18+
const ONBOARDING_SAVED_HINT_DURATION = 1500
19+
20+
export const InfiniteDiscoveryOnboarding: React.FC<InfiniteDiscoveryOnboardingProps> = ({
21+
artworks,
22+
}) => {
23+
const space = useSpace()
24+
const [showSavedHint, setShowSavedHint] = useState(false)
25+
26+
const swiperRef = useRef<SwiperRefProps>(null)
27+
28+
const cards = useMemo(() => {
29+
return artworks.map((artwork, i) => (
30+
<InfiniteDiscoveryArtworkCard
31+
artwork={artwork}
32+
key={artwork.internalID}
33+
containerStyle={{
34+
paddingVertical: space(1),
35+
borderRadius: 10,
36+
shadowRadius: 3,
37+
shadowColor: "black",
38+
shadowOpacity: 0.2,
39+
shadowOffset: { height: 0, width: 0 },
40+
}}
41+
// Only show the saved hint for the upper card - since the cards are reverted in the swiper,
42+
// the upper card index is the last in the array
43+
isSaved={i === artworks.length - 1 ? showSavedHint : false}
44+
/>
45+
))
46+
}, [artworks, showSavedHint])
47+
1848
const [isVisible, setIsVisible] = useState(false)
49+
const isRewindRequested = useSharedValue(false)
1950

2051
const hasInteractedWithOnboarding = GlobalStore.useAppState(
2152
(state) => state.infiniteDiscovery.hasInteractedWithOnboarding
2253
)
2354

24-
console.log({ hasInteractedWithOnboarding })
2555
useEffect(() => {
2656
setTimeout(() => {
2757
if (!hasInteractedWithOnboarding) {
2858
setIsVisible(true)
2959
}
30-
}, 2000)
60+
}, 1000)
3161
}, [hasInteractedWithOnboarding])
3262

33-
const space = useSpace()
34-
const { width } = useScreenDimensions()
35-
const flatlistRef = useRef<FlatList>(null)
36-
const [index, setIndex] = useState(0)
37-
38-
const handleNext = () => {
39-
const newIndex = index + 1
40-
41-
if (newIndex < STEPS.length) {
42-
setIndex(newIndex)
43-
flatlistRef.current?.scrollToIndex({ animated: true, index: newIndex })
44-
} else {
45-
setIsVisible(false)
46-
}
63+
const showOnboardingAnimation = () => {
64+
setShowSavedHint(true)
65+
66+
setTimeout(() => {
67+
swiperRef.current?.swipeLeftThenRight(ONBOARDING_SWIPE_ANIMATION_DURATION)
68+
}, ONBOARDING_ANIMATION_DELAY + ONBOARDING_SAVED_HINT_DURATION)
69+
70+
setTimeout(
71+
() => {
72+
setShowSavedHint(false)
73+
},
74+
ONBOARDING_SWIPE_ANIMATION_DURATION +
75+
ONBOARDING_SAVED_HINT_DURATION +
76+
ONBOARDING_ANIMATION_DELAY
77+
)
4778
}
4879

80+
useEffect(() => {
81+
if (!isVisible) {
82+
return
83+
}
84+
85+
// Wait for a second before showing the animation
86+
setTimeout(() => {
87+
showOnboardingAnimation()
88+
// Show the animation every 5 seconds afterwards
89+
setInterval(() => {
90+
showOnboardingAnimation()
91+
}, 5000)
92+
}, 1000)
93+
}, [setShowSavedHint, isVisible])
94+
4995
return (
5096
<Modal animationType="fade" visible={isVisible} transparent>
5197
<Flex flex={1} backgroundColor="transparent">
@@ -63,39 +109,47 @@ export const InfiniteDiscoveryOnboarding: React.FC<{}> = () => {
63109
<SafeAreaView
64110
style={{ flex: 1, justifyContent: "flex-end", backgroundColor: "transparent" }}
65111
>
66-
<Flex
67-
flex={1}
68-
width="100%"
69-
backgroundColor="black15"
70-
alignSelf="center"
71-
justifyContent="center"
72-
alignItems="center"
73-
opacity={0.7}
74-
></Flex>
75-
<Flex justifyContent="flex-end" px={2}>
76-
<FlatList
77-
ref={flatlistRef}
78-
data={STEPS}
79-
scrollEnabled={false}
80-
style={{ marginHorizontal: -space(2), flexGrow: 0 }}
81-
renderItem={({ item }) => (
82-
<Flex width={width} px={2} justifyContent="flex-end">
83-
{item.title}
84-
{item.description}
85-
</Flex>
86-
)}
87-
keyExtractor={(item) => item.key}
88-
horizontal
89-
showsHorizontalScrollIndicator={false}
90-
pagingEnabled
112+
<Flex flex={1} pointerEvents="none">
113+
<Swiper
114+
containerStyle={{ flex: 1, transform: [{ scale: 0.8 }] }}
115+
cards={cards}
116+
isRewindRequested={isRewindRequested}
117+
onTrigger={() => {}}
118+
swipedIndexCallsOnTrigger={2}
119+
onNewCardReached={() => {}}
120+
onRewind={() => {}}
121+
onSwipe={() => {}}
122+
ref={swiperRef}
91123
/>
124+
</Flex>
125+
126+
<Flex justifyContent="flex-end" px={2}>
127+
<Text>Welcome to Discovery Daily</Text>
128+
129+
<Spacer y={1} />
130+
131+
<Text variant="lg-display">
132+
Start{" "}
133+
<Text variant="lg-display" fontWeight="500">
134+
swiping
135+
</Text>{" "}
136+
to discover art, and{" "}
137+
<Text variant="lg-display" fontWeight="500">
138+
save
139+
</Text>{" "}
140+
the works you love.
141+
</Text>
92142

93143
<Spacer y={2} />
94144

95145
<Flex alignItems="flex-end">
96-
<Button variant="outline" onPress={handleNext}>
97-
{index === STEPS.length - 1 ? "Done" : "Next"}
98-
</Button>
146+
<LinkText
147+
onPress={() => {
148+
setIsVisible(false)
149+
}}
150+
>
151+
Tap to get started
152+
</LinkText>
99153
</Flex>
100154
</Flex>
101155
</SafeAreaView>
@@ -104,27 +158,3 @@ export const InfiniteDiscoveryOnboarding: React.FC<{}> = () => {
104158
</Modal>
105159
)
106160
}
107-
108-
const STEPS = [
109-
{
110-
key: "introduction",
111-
title: (
112-
<Text variant="sm-display" color="black60" mb={0.5}>
113-
Welcome to Discover Daily
114-
</Text>
115-
),
116-
description: <Text variant="lg-display">A new way of browsing works on Artsy.</Text>,
117-
},
118-
{
119-
key: "swipeArtworks",
120-
description: <Text variant="lg-display">Swipe artworks to the left to see the next work</Text>,
121-
},
122-
{
123-
key: "favouriteArtworks",
124-
description: (
125-
<Text variant="lg-display">
126-
Press <HeartIcon height={24} width={24} /> when you like an artwork you see
127-
</Text>
128-
),
129-
},
130-
]

0 commit comments

Comments
 (0)