Skip to content

Commit

Permalink
Add a UI4 modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-edge authored and paullinator committed Jan 12, 2024
1 parent 190c898 commit 9b961ae
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 3 deletions.
6 changes: 3 additions & 3 deletions src/components/modals/TextInputModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Alert } from '../themed/Alert'
import { FilledTextInput } from '../themed/FilledTextInput'
import { MainButton } from '../themed/MainButton'
import { ModalMessage, ModalTitle } from '../themed/ModalParts'
import { ThemedModal } from '../themed/ThemedModal'
import { ModalUi4 } from '../ui4/ModalUi4'

interface Props {
// Resolves to the entered string, or void if cancelled.
Expand Down Expand Up @@ -91,7 +91,7 @@ export function TextInputModal(props: Props) {
}

return (
<ThemedModal warning={warning} bridge={bridge} onCancel={() => bridge.resolve(undefined)}>
<ModalUi4 warning={warning} bridge={bridge} onCancel={() => bridge.resolve(undefined)}>
{title != null ? <ModalTitle>{title}</ModalTitle> : null}
{typeof message === 'string' ? <ModalMessage>{message}</ModalMessage> : <>{message}</>}
{warningMessage != null ? <Alert type="warning" title={lstrings.string_warning} marginRem={0.5} message={warningMessage} numberOfLines={0} /> : null}
Expand Down Expand Up @@ -126,6 +126,6 @@ export function TextInputModal(props: Props) {
) : (
<MainButton alignSelf="center" label={submitLabel} marginRem={0.5} onPress={handleSubmit} type="primary" />
)}
</ThemedModal>
</ModalUi4>
)
}
203 changes: 203 additions & 0 deletions src/components/ui4/ModalUi4.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import * as React from 'react'
import { BackHandler, Dimensions, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'
import { AirshipBridge } from 'react-native-airship'
import { Gesture, GestureDetector, ScrollView } from 'react-native-gesture-handler'
import { cacheStyles } from 'react-native-patina'
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
import AntDesignIcon from 'react-native-vector-icons/AntDesign'
import { BlurView } from 'rn-id-blurview'

import { useHandler } from '../../hooks/useHandler'
import { fixSides, mapSides, sidesToMargin } from '../../util/sides'
import { maybeComponent } from '../hoc/maybeComponent'
import { Theme, useTheme } from '../services/ThemeContext'

export interface ModalPropsUi4<T = unknown> {
bridge: AirshipBridge<T>
children?: React.ReactNode

// Internal padding to place inside the component.
paddingRem?: number[] | number

// Include a scroll area:
scroll?: boolean

// Gives the box a border:
warning?: boolean

// Called when the user taps outside the modal or clicks the back button.
// If this is missing, the modal will not be closable.
onCancel?: () => void
}

const safeAreaGap = 64 // Overkill to avoid bottom of screen
const duration = 300

/**
* A modal that slides a modal up from the bottom of the screen
* and dims the rest of the app.
*/
export function ModalUi4<T>(props: ModalPropsUi4<T>): JSX.Element {
const { bridge, children, paddingRem, scroll = false, warning = false, onCancel } = props
const theme = useTheme()
const styles = getStyles(theme)

// Use margin instead of padding to give children the ability to bypass the
// default "padding," if necessary
const childrenMargin = sidesToMargin(mapSides(fixSides(paddingRem, 0.5), theme.rem))
const closeThreshold = theme.rem(6)
const dragSlop = theme.rem(1)

//
// Shared state
//

const opacity = useSharedValue(0)
const offset = useSharedValue(Dimensions.get('window').height)

const handleCancel = useHandler(() => {
if (onCancel != null) onCancel()
})

//
// Effects
//

React.useEffect(() => bridge.on('clear', handleCancel), [bridge, handleCancel])

React.useEffect(() => {
// Animate in:
opacity.value = withTiming(1, { duration })
offset.value = withTiming(0, { duration })

// Animate out:
bridge.on('result', () => {
opacity.value = withTiming(0, { duration })
offset.value = withTiming(Dimensions.get('window').height, { duration }, () => runOnJS(bridge.remove)())
})
}, [bridge, opacity, offset])

React.useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
handleCancel()
return true
})
return () => backHandler.remove()
}, [handleCancel])

const gesture = Gesture.Pan()
.onUpdate(e => {
offset.value = e.translationY
})
.onEnd(() => {
if (offset.value > closeThreshold) {
runOnJS(handleCancel)()
}
offset.value = withTiming(0, { duration })
})

//
// Dynamic styles
//

const underlayStyle = useAnimatedStyle(() => ({
opacity: opacity.value
}))

const bodyStyle = useAnimatedStyle(() => ({
transform: [{ translateY: Math.max(-dragSlop, offset.value) }]
}))

const bodyLayout = {
borderColor: warning ? theme.warningText : theme.modalBorderColor,
borderWidth: warning ? 4 : theme.modalBorderWidth,
marginBottom: -safeAreaGap - dragSlop,
paddingBottom: safeAreaGap + dragSlop
}

return (
<>
<TouchableWithoutFeedback onPress={handleCancel}>
<Animated.View style={[styles.underlay, underlayStyle]} />
</TouchableWithoutFeedback>
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.body, bodyStyle, bodyLayout]}>
{/* Need another Biew here because BlurView doesn't accept rounded corners in its styling */}
<View style={styles.blurContainer}>
<BlurView blurType={theme.isDark ? 'dark' : 'light'} style={StyleSheet.absoluteFill} overlayColor="#00000000" />
</View>

<View style={styles.dragBarContainer}>
<View style={styles.dragBar} />
</View>
{scroll ? (
<MaybeScrollView when={scroll} style={childrenMargin}>
{children}
</MaybeScrollView>
) : (
<View style={childrenMargin}>{children}</View>
)}

{onCancel == null ? null : (
<TouchableOpacity style={styles.closeIcon} onPress={onCancel}>
<AntDesignIcon name="close" color={theme.deactivatedText} size={theme.rem(1)} />
</TouchableOpacity>
)}
</Animated.View>
</GestureDetector>
</>
)
}

const getStyles = cacheStyles((theme: Theme) => ({
underlay: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0
},
body: {
alignSelf: 'flex-end',
backgroundColor: theme.modalBackgroundUi4,
borderTopLeftRadius: theme.rem(1),
borderTopRightRadius: theme.rem(1),
flexShrink: 1,
width: theme.rem(30) // This works as a maxWidth because flexShrink is set
},

blurContainer: {
position: 'absolute',
width: '100%',
height: '100%',
borderTopLeftRadius: theme.rem(1),
borderTopRightRadius: theme.rem(1),
overflow: 'hidden'
},
dragBarContainer: {
alignItems: 'center',
left: 0,
position: 'absolute',
right: 0,
top: 0
},
dragBar: {
// TODO: Replace this color we have a proper design:
backgroundColor: theme.deactivatedText,
borderRadius: theme.rem(0.125),
height: theme.rem(0.25),
marginTop: theme.rem(0.5),
width: theme.rem(3)
},
closeIcon: {
alignItems: 'center',
height: theme.rem(2),
justifyContent: 'center',
position: 'absolute',
right: 0,
top: 0,
width: theme.rem(2)
}
}))

const MaybeScrollView = maybeComponent(ScrollView)
2 changes: 2 additions & 0 deletions src/theme/variables/edgeDark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ export const edgeDark: Theme = {
start: { x: 1, y: 0 }
},

modalBackgroundUi4: 'rgba(255, 255, 255, 0.376)',

txDirBgReceiveUi4: palette.greenOp60,
txDirBgSendUi4: palette.redOp60,
txDirBgSwapUi4: palette.grayOp70,
Expand Down
2 changes: 2 additions & 0 deletions src/theme/variables/edgeLight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ export const edgeLight: Theme = {
start: { x: 1, y: 0 }
},

modalBackgroundUi4: '#ffffff80',

txDirBgReceiveUi4: palette.greenOp60,
txDirBgSendUi4: palette.redOp60,
txDirBgSwapUi4: palette.grayOp70,
Expand Down
2 changes: 2 additions & 0 deletions src/theme/variables/testDark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ export const testDark: Theme = {
start: { x: 1, y: 0 }
},

modalBackgroundUi4: '#00000080',

txDirBgReceiveUi4: palette.greenOp60,
txDirBgSendUi4: palette.redOp60,
txDirBgSwapUi4: palette.grayOp70,
Expand Down
2 changes: 2 additions & 0 deletions src/theme/variables/testLight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ export const testLight: Theme = {
start: { x: 1, y: 0 }
},

modalBackgroundUi4: '#ffffff80',

txDirBgReceiveUi4: palette.greenOp60,
txDirBgSendUi4: palette.redOp60,
txDirBgSwapUi4: palette.grayOp70,
Expand Down
2 changes: 2 additions & 0 deletions src/types/Theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ export interface Theme {
fioCardGradientUi4: ThemeGradientParams
swapCardGradientUi4: ThemeGradientParams

modalBackgroundUi4: string

txDirBgReceiveUi4: string
txDirBgSendUi4: string
txDirBgSwapUi4: string
Expand Down

0 comments on commit 9b961ae

Please sign in to comment.