Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet-mobile): Add slide in and out animations #3791

Merged
merged 8 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/wallet-mobile/src/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ export const AppNavigator = () => {
onReady={onReady}
ref={navRef}
>
<NotificationUIHandler />

<ModalProvider>
<Stack.Navigator
screenOptions={{
Expand Down Expand Up @@ -229,6 +227,8 @@ export const AppNavigator = () => {
)}
</Stack.Navigator>
</ModalProvider>

<NotificationUIHandler />
</NavigationContainer>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {NotificationPopup} from './common/NotificationPopup'
import {NotificationStack} from './common/NotificationStack'

const displayLimit = 3
const displayTime = 20 * 1000

export const NotificationUIHandler = () => {
const enabled = useNotificationDisplaySettings()
Expand All @@ -29,6 +28,7 @@ export const NotificationUIHandler = () => {
event={event}
onCancel={() => removeEvent(event.id)}
onPress={() => removeEvent(event.id)}
onExpired={() => removeEvent(event.id)}
/>
))}
</NotificationStack>
Expand All @@ -38,14 +38,13 @@ export const NotificationUIHandler = () => {
const useCollectNewNotifications = ({enabled}: {enabled: boolean}) => {
const manager = useNotificationManager()
const walletManager = useWalletManager()
const selectedWalletId = walletManager.selected.wallet?.id
const selectedWalletId = walletManager.selected.wallet?.id ?? ''
const [events, setEvents] = React.useState<Notifications.Event[]>([])

React.useEffect(() => {
if (!enabled) return
const pushEvent = (event: Notifications.Event) => {
setEvents((e) => [...e, event])
setTimeout(() => setEvents((e) => e.filter((ev) => ev.id !== event.id)), displayTime)
}

const subscription = manager.newEvents$.subscribe((event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const NotificationsDevScreen = () => {
const Screen = () => {
const manager = useNotificationManager()
const walletManager = useWalletManager()
const selectedWalletId = walletManager.selected.wallet?.id ?? 'walletId'
const selectedWalletId = walletManager.selected.wallet?.id ?? ''

const handleOnTriggerTransactionReceived = () => {
manager.events.push(
Expand All @@ -50,7 +50,7 @@ const Screen = () => {
<View style={{padding: 16, gap: 8}}>
<Text style={{fontSize: 24}}>Notifications Playground</Text>

<Button title="Trigger Transacrion Received Notification" onPress={handleOnTriggerTransactionReceived} />
<Button title="Trigger Transaction Received Notification" onPress={handleOnTriggerTransactionReceived} />

<Text style={{fontSize: 24}}>Settings</Text>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ type Props = {
event: Notifications.Event
onPress: () => void
onCancel: () => void
onExpired: () => void
}

export const NotificationPopup = ({event, onPress, onCancel}: Props) => {
export const NotificationPopup = ({event, onPress, onCancel, onExpired}: Props) => {
const navigation = useWalletNavigation()
const strings = useStrings()

if (event.trigger === Notifications.Trigger.TransactionReceived) {
return (
<SwipeOutWrapper onSwipeOut={onCancel}>
<SwipeOutWrapper onSwipeOut={onCancel} onExpired={onExpired}>
<NotificationItem
onPress={() => {
onPress()
Expand All @@ -37,7 +38,7 @@ export const NotificationPopup = ({event, onPress, onCancel}: Props) => {

if (event.trigger === Notifications.Trigger.RewardsUpdated) {
return (
<SwipeOutWrapper onSwipeOut={onCancel}>
<SwipeOutWrapper onSwipeOut={onCancel} onExpired={onExpired}>
<NotificationItem
onPress={() => {
onPress()
Expand Down Expand Up @@ -121,7 +122,7 @@ const useStyles = () => {
...atoms.gap_xs,
},
title: {
...atoms.body_2_md_regular,
...atoms.body_2_md_medium,
...atoms.font_semibold,
},
description: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const useStyles = () => {
left: 0,
right: 0,
...atoms.z_50,
...atoms.p_lg,
...atoms.px_lg,
},
flex: {
...atoms.gap_sm,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import * as React from 'react'
import {Animated, Dimensions, PanResponder} from 'react-native'
import {Animated, Dimensions, Easing, PanResponder} from 'react-native'

type Props = {
children: React.ReactNode
onSwipeOut: () => void
onExpired: () => void
}

export const SwipeOutWrapper = ({children, onSwipeOut}: Props) => {
const {pan, panResponder} = usePanAnimation({onRelease: onSwipeOut})
const notificationDisplayTime = 20 * 1000 // 20 seconds
const fadeInTime = 200
const fadeOutPaddingTime = 100

export const SwipeOutWrapper = ({children, onSwipeOut, onExpired}: Props) => {
const {pan, panResponder, fadeIn, opacity, fadeOut, translateY} = usePanAnimation({onRelease: onSwipeOut})
const onExpiredRef = React.useRef(onExpired)
onExpiredRef.current = onExpired

React.useEffect(() => {
const expiredTimeout = setTimeout(() => onExpiredRef.current(), notificationDisplayTime)
const fadeOutTimeout = setTimeout(() => fadeOut(), notificationDisplayTime - fadeInTime - fadeOutPaddingTime)

return () => {
clearTimeout(expiredTimeout)
clearTimeout(fadeOutTimeout)
}
}, [fadeIn, fadeOut])

React.useEffect(() => {
// When executed without setTimeout, the animation does not start
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 guesses here, probably mounting issue, try to wrap the fadeIn within the useLayoutEffect it should work, otherwise you can leverage -> requestAnimationFrame as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works well now, thanks!

const fadeInTimeout = setTimeout(() => fadeIn(), 1)
return () => clearTimeout(fadeInTimeout)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const fadeInTimeout = setTimeout(() => fadeIn(), 1)
return () => clearTimeout(fadeInTimeout)
fadeIn()

Copy link
Member

@stackchain stackchain Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs testing, cuz if its not an issue on mounting, better run with InteractionManager or requestAnimationFrame.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe animations clashing

}, [fadeIn])

return (
<Animated.View
style={{
transform: [{translateX: pan.x}],
transform: [{translateX: pan.x}, {translateY}],
opacity,
}}
{...panResponder.panHandlers}
>
Expand All @@ -23,9 +47,45 @@ export const SwipeOutWrapper = ({children, onSwipeOut}: Props) => {

const usePanAnimation = ({onRelease}: {onRelease: () => void}) => {
const pan = React.useRef(new Animated.ValueXY()).current
const opacity = React.useRef(new Animated.Value(0)).current
const translateY = React.useRef(new Animated.Value(-50)).current
const screenWidth = Dimensions.get('window').width
const screenLimitInPercentAfterWhichShouldRelease = 0.3

const fadeIn = React.useCallback(() => {
Animated.parallel([
Animated.timing(opacity, {
toValue: 1,
duration: fadeInTime,
useNativeDriver: false,
easing: Easing.inOut(Easing.ease),
}),
Animated.timing(translateY, {
toValue: 0,
duration: fadeInTime,
useNativeDriver: false,
easing: Easing.inOut(Easing.ease),
}),
]).start()
}, [opacity, translateY])

const fadeOut = React.useCallback(() => {
Animated.parallel([
Animated.timing(opacity, {
toValue: 0,
duration: fadeInTime,
useNativeDriver: false,
easing: Easing.inOut(Easing.ease),
}),
Animated.timing(translateY, {
toValue: -50,
duration: fadeInTime,
useNativeDriver: false,
easing: Easing.inOut(Easing.ease),
}),
]).start()
}, [opacity, translateY])

const panResponder = React.useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
Expand All @@ -50,5 +110,5 @@ const usePanAnimation = ({onRelease}: {onRelease: () => void}) => {
}),
).current

return {pan, panResponder}
return {pan, panResponder, fadeIn, fadeOut, opacity, translateY}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"start": {
"line": 163,
"column": 9,
"index": 4901
"index": 4904
},
"end": {
"line": 166,
"column": 3,
"index": 5003
"index": 5006
}
},
{
Expand All @@ -21,12 +21,12 @@
"start": {
"line": 167,
"column": 16,
"index": 5021
"index": 5024
},
"end": {
"line": 170,
"column": 3,
"index": 5131
"index": 5134
}
},
{
Expand All @@ -36,12 +36,12 @@
"start": {
"line": 171,
"column": 18,
"index": 5151
"index": 5154
},
"end": {
"line": 174,
"column": 3,
"index": 5265
"index": 5268
}
},
{
Expand All @@ -51,12 +51,12 @@
"start": {
"line": 175,
"column": 23,
"index": 5290
"index": 5293
},
"end": {
"line": 178,
"column": 3,
"index": 5402
"index": 5405
}
}
]
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {useQuery, useQueryClient, UseQueryOptions} from 'react-query'
import {Notifications as NotificationTypes} from '@yoroi/types'
import {useNotificationManager} from './NotificationProvider'
import {useEffect} from 'react'
import * as React from 'react'

export const useReceivedNotificationEvents = (
options: UseQueryOptions<ReadonlyArray<NotificationTypes.Event>, Error> = {},
) => {
const queryClient = useQueryClient()
const manager = useNotificationManager()
useEffect(() => {
React.useEffect(() => {
const subscription = manager.unreadCounterByGroup$.subscribe(() =>
queryClient.invalidateQueries(['receivedNotificationEvents']),
)
Expand Down
Loading