-
Notifications
You must be signed in to change notification settings - Fork 4k
Confirm new expenses with a “View” toast on Spend instead of scrolling to and highlighting the row #91214
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
Closed
borys3kk
wants to merge
58
commits into
Expensify:main
from
software-mansion-labs:borys3kk-growl-poc
Closed
Confirm new expenses with a “View” toast on Spend instead of scrolling to and highlighting the row #91214
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
3778ac8
inital commits
borys3kk 5d256cb
merge main
borys3kk 034aaab
creating expense and navigating to it works reliably
borys3kk 8dae063
fix styles in Growl
borys3kk 6192f2f
show growl instantly when adding outside of search/inbox
borys3kk 85082c4
implement navigation first then show growl
borys3kk 3c7077f
merge main
borys3kk b1a7d78
fix errors
borys3kk d16069f
fix failing tests, prettier, eslint, typecheck
borys3kk 7ab84dc
reposition + unify navigation path for "View" action
borys3kk 765bd05
fix showing growl on native platforms
borys3kk 8526f56
fix positiong after resize from narrow to wide and from wide to narrow
borys3kk 0f563bc
fix growl styles in native, minimize re-renders
borys3kk 8e426f2
Merge branch 'main' of github.com:Expensify/App into borys3kk-growl-poc
borys3kk 2d46511
fix growl timeout, fix react compiler compliance
borys3kk de2fb6d
remove searchHighlightAndScroll
borys3kk ba654a8
fix lint with original justification
borys3kk 7d66275
merge main
borys3kk 905fe1d
fix failing tests
borys3kk 4f56a9c
fix growl not showing on spend page
borys3kk 90c15d9
show growls on every tab, adjust styling
borys3kk c719901
merge main
borys3kk 4e894c0
update growl to super wide rhp of report and then rhp of the expense
borys3kk 2c54256
fix eslint errors
borys3kk 63b111d
merge main
borys3kk 0f1ca9d
remove unused eslint disable directive
borys3kk 5ae4c8b
fix double growl showing
borys3kk b09881c
merge main
borys3kk 63804a1
fix failing submission tests with growls
borys3kk 74302af
merge main
borys3kk 84e2a4e
fix lint
borys3kk 1af768f
fix preserve dismiss-first navigation ownership
borys3kk 56cf623
Show tracked-expense growl immediately when the thread ID is known
borys3kk 5c84b30
add translations for each expense type
borys3kk 13685ed
merge main
borys3kk 13a286e
fix after merge errors
borys3kk 5977ddb
merge main
borys3kk b27f347
merge main
borys3kk 4140a3a
Merge branch 'main' of github.com:Expensify/App into borys3kk-growl-poc
borys3kk 337cd0f
remove all previous highlight logic
borys3kk 245045e
fix double rhp opened in empty report
borys3kk fbba6d0
Merge branch 'main' into borys3kk-growl-poc
borys3kk fe3e189
merge main
borys3kk 74b3194
fix eslint
borys3kk a349e62
merge main
borys3kk cb37ad3
batch submissions to one navigation
borys3kk 7cb6def
remove redundant useMemo
borys3kk fcfc4cd
move growl duration to const
borys3kk 828b875
preserve dismiss first navigation ownership pt2
borys3kk c3fb533
fix waiting for timeout in no iouReportID
borys3kk cae8608
fix for allTransactions reset
borys3kk d6a8d12
fix higlighting in report table
borys3kk 0753105
narrow growl types
borys3kk 3413367
merge main
borys3kk 95e0afc
fix root lookup
borys3kk 50665ca
merge main
borys3kk 1e63b7a
fix failing tests
borys3kk 02df4a6
Merge branch 'main' of github.com:Expensify/App into borys3kk-growl-poc
borys3kk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
215 changes: 215 additions & 0 deletions
215
src/components/GrowlNotification/GrowlNotificationContent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import {View} from 'react-native'; | ||
| import {Directions, Gesture, GestureDetector} from 'react-native-gesture-handler'; | ||
| import {useSharedValue, withSpring} from 'react-native-reanimated'; | ||
| import type {SvgProps} from 'react-native-svg'; | ||
| import ActivityIndicator from '@components/ActivityIndicator'; | ||
| import Button from '@components/Button'; | ||
| import Icon from '@components/Icon'; | ||
| import * as Pressables from '@components/Pressable'; | ||
| import Text from '@components/Text'; | ||
| import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useTheme from '@hooks/useTheme'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import type {GrowlAction, GrowlType} from '@libs/Growl'; | ||
| import CONST from '@src/CONST'; | ||
| import type IconAsset from '@src/types/utils/IconAsset'; | ||
| import GrowlNotificationContainer from './GrowlNotificationContainer'; | ||
|
|
||
| const INACTIVE_OFFSET = 255; | ||
| const INACTIVE_POSITION_Y = -INACTIVE_OFFSET; | ||
| // Approximate time for the slide spring to settle. Used to delay onDismissed | ||
| // until the slide-out animation has visibly finished. | ||
| const SLIDE_DURATION_MS = 400; | ||
|
|
||
| const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; | ||
|
|
||
| type GrowlNotificationContentProps = { | ||
| bodyText: string; | ||
| type: GrowlType; | ||
| duration: number; | ||
| action?: GrowlAction; | ||
| onDismissed: () => void; | ||
| }; | ||
|
|
||
| function GrowlNotificationContent({bodyText, type, duration, action, onDismissed}: GrowlNotificationContentProps) { | ||
| // Normalized: 0 = fully offscreen for the current anchor, 1 = fully visible. The container | ||
| // multiplies this against the live `inactiveY`, so the offscreen position stays correct | ||
| // even when the responsive layout flips after the growl is dismissed. | ||
| const progress = useSharedValue(0); | ||
| // Guards against double-firing the action's onPress while the slide-out animation | ||
| // is still on screen. Reset whenever new growl content arrives. | ||
| const isActionPressedRef = useRef(false); | ||
| // Holds the post-fling-out setTimeout so the content-change effect can cancel a | ||
| // pending unmount when a new growl arrives mid-dismissal. Only touched inside effects. | ||
| const dismissTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
| // Counter that triggers the dismiss-after-slide-out effect. Kept in state (rather than | ||
| // a ref) so triggerDismiss doesn't transitively read a ref — that would make the React | ||
| // Compiler reject the gesture's useMemo as "ref access during render". | ||
| const [dismissNonce, setDismissNonce] = useState(0); | ||
|
|
||
| const theme = useTheme(); | ||
| const styles = useThemeStyles(); | ||
| const {shouldUseNarrowLayout} = useResponsiveLayout(); | ||
| const icons = useMemoizedLazyExpensifyIcons(['Exclamation', 'Checkmark']); | ||
|
|
||
| // Derived live so that resizing the window flips position + slide direction during display. | ||
| const useBottomPosition = !!action && !shouldUseNarrowLayout; | ||
| const inactiveY = useBottomPosition ? INACTIVE_OFFSET : INACTIVE_POSITION_Y; | ||
|
|
||
| // Every non-loading growl variant must have an icon mapping. Loading is rendered separately | ||
| // (ActivityIndicator), so it's excluded here — keeping this map exhaustive over the rest. | ||
| type GrowlIconTypes = Record< | ||
| Exclude<GrowlType, typeof CONST.GROWL.LOADING>, | ||
| { | ||
| icon: React.FC<SvgProps> | IconAsset; | ||
| iconColor: string; | ||
| } | ||
| >; | ||
|
|
||
| const types: GrowlIconTypes = { | ||
| [CONST.GROWL.SUCCESS]: { | ||
| icon: icons.Checkmark, | ||
| iconColor: theme.success, | ||
| }, | ||
| [CONST.GROWL.ERROR]: { | ||
| icon: icons.Exclamation, | ||
| iconColor: theme.danger, | ||
| }, | ||
| [CONST.GROWL.WARNING]: { | ||
| icon: icons.Exclamation, | ||
| iconColor: theme.warning, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Animate growl notification. `targetProgress` is 0 for offscreen, 1 for visible. | ||
| */ | ||
| const fling = useCallback( | ||
| (targetProgress = 0) => { | ||
| 'worklet'; | ||
|
|
||
| progress.set( | ||
| withSpring(targetProgress, { | ||
| overshootClamping: false, | ||
| }), | ||
| ); | ||
| }, | ||
| [progress], | ||
| ); | ||
|
|
||
| /** | ||
| * Slide the growl off-screen and schedule its unmount via the dismissNonce effect. | ||
| */ | ||
| const triggerDismiss = useCallback(() => { | ||
| fling(0); | ||
| setDismissNonce((n) => n + 1); | ||
| }, [fling]); | ||
|
|
||
| // Schedule unmount once a dismiss has been requested. Effect cleanup cancels the timer | ||
| // when this effect re-runs or the component unmounts. | ||
| useEffect(() => { | ||
| if (dismissNonce === 0) { | ||
| return; | ||
| } | ||
| const timeoutId = setTimeout(onDismissed, SLIDE_DURATION_MS); | ||
| dismissTimeoutRef.current = timeoutId; | ||
| return () => { | ||
| clearTimeout(timeoutId); | ||
| dismissTimeoutRef.current = null; | ||
| }; | ||
| }, [dismissNonce, onDismissed]); | ||
|
|
||
| useEffect(() => { | ||
| isActionPressedRef.current = false; | ||
| // New content arrived — cancel any in-flight unmount-after-slide-out from the previous | ||
| // growl. (We can't reset dismissNonce here because setState in an effect body is | ||
| // disallowed by lint; clearing the timer directly via the ref achieves the same result.) | ||
| if (dismissTimeoutRef.current) { | ||
| clearTimeout(dismissTimeoutRef.current); | ||
| dismissTimeoutRef.current = null; | ||
| } | ||
|
|
||
| // Snap to fully offscreen before sliding in so the slide-in direction matches the | ||
| // current placement (from above for top, from below for bottom). | ||
| progress.set(0); | ||
| fling(1); | ||
|
|
||
| if (duration <= 0) { | ||
| // Indefinite (loading) growl - slide in but don't auto-dismiss. | ||
| return; | ||
| } | ||
|
|
||
| const autoDismissTimeoutId = setTimeout(triggerDismiss, duration); | ||
| return () => clearTimeout(autoDismissTimeoutId); | ||
| }, [bodyText, type, action, duration, fling, progress, triggerDismiss]); | ||
|
|
||
| // GestureDetector by default runs callbacks on UI thread using Reanimated. In this | ||
| // case we want to trigger an RN's Animated animation, which needs to be done on JS thread. | ||
| // Wrapped in useMemo so the React Compiler doesn't flag the gesture builder's internal | ||
| // mutable state as ref access during render. | ||
| const flingGesture = useMemo( | ||
| () => | ||
| Gesture.Fling() | ||
| .direction(useBottomPosition ? Directions.DOWN : Directions.UP) | ||
| .runOnJS(true) | ||
| .onStart(() => { | ||
| triggerDismiss(); | ||
| }), | ||
| [useBottomPosition, triggerDismiss], | ||
| ); | ||
|
|
||
| return ( | ||
| <View style={[styles.growlNotificationWrapper]}> | ||
| <GrowlNotificationContainer | ||
| progress={progress} | ||
| inactiveY={inactiveY} | ||
| useBottomPosition={useBottomPosition} | ||
| > | ||
| <PressableWithoutFeedback | ||
| accessibilityLabel={bodyText} | ||
| sentryLabel="GrowlNotification-Dismiss" | ||
| onPress={() => triggerDismiss()} | ||
| > | ||
| <GestureDetector gesture={flingGesture}> | ||
| <View style={styles.growlNotificationBox}> | ||
| {type === CONST.GROWL.LOADING ? ( | ||
| <ActivityIndicator reasonAttributes={{context: 'GrowlNotification.Loading'}} /> | ||
| ) : ( | ||
| <Icon | ||
| src={types[type].icon} | ||
| fill={types[type].iconColor} | ||
| /> | ||
| )} | ||
| <Text style={[styles.growlNotificationText, action ? styles.growlNotificationTextWithAction : styles.growlNotificationTextWithoutAction]}>{bodyText}</Text> | ||
| {!!action && ( | ||
| <Button | ||
| medium | ||
| text={action.label} | ||
| accessibilityLabel={action.label} | ||
| sentryLabel="GrowlNotification-Action" | ||
| onPress={() => { | ||
| if (isActionPressedRef.current) { | ||
| return; | ||
| } | ||
| isActionPressedRef.current = true; | ||
| triggerDismiss(); | ||
| action.onPress(); | ||
| }} | ||
| innerStyles={styles.bgTransparent} | ||
| textStyles={styles.growlNotificationActionText} | ||
| shouldUseDefaultHover={false} | ||
| hoverStyles={styles.growlNotificationActionHovered} | ||
| isNested | ||
| /> | ||
| )} | ||
| </View> | ||
| </GestureDetector> | ||
| </PressableWithoutFeedback> | ||
| </GrowlNotificationContainer> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| export default GrowlNotificationContent; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.