From 4cfdd08bb570a9977d56be429df5c1a08a3d49d4 Mon Sep 17 00:00:00 2001 From: Osama Date: Mon, 17 Aug 2020 09:29:29 +0800 Subject: [PATCH] =?UTF-8?q?update(=F0=9F=9A=9A):=20housekeeping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/src/common/components/CardModal.tsx | 135 ++++++++------ .../src/common/components/LoadingPopup.tsx | 4 +- template/src/common/components/Snackbar.tsx | 167 +++++++++++++++--- .../src/common/components/TouchableScale.tsx | 42 ----- .../src/common/exceptions/BaseException.ts | 2 +- .../src/common/exceptions/HttpException.ts | 9 +- template/src/common/helpers/layoutUtil.ts | 23 ++- .../src/common/hooks/useScaleTransition.ts | 62 ------- .../error-boundary/RootErrorBoundary.tsx | 20 +-- .../services/network/github/githubService.ts | 15 +- 10 files changed, 263 insertions(+), 216 deletions(-) delete mode 100644 template/src/common/components/TouchableScale.tsx delete mode 100644 template/src/common/hooks/useScaleTransition.ts diff --git a/template/src/common/components/CardModal.tsx b/template/src/common/components/CardModal.tsx index 62452a1..47c337a 100644 --- a/template/src/common/components/CardModal.tsx +++ b/template/src/common/components/CardModal.tsx @@ -1,4 +1,4 @@ -import React, { memo, ReactNode } from "react" +import React, { ReactNode, useMemo } from "react" import Animated, { cond, multiply, @@ -9,6 +9,7 @@ import Animated, { Value, } from "react-native-reanimated" import { bin, spring, useValue } from "react-native-redash" +import { StyleSheet, ViewStyle } from "react-native" import { layoutUtil } from "../helpers/layoutUtil" const HEIGHT_OFFSET = 0.7 @@ -16,81 +17,99 @@ const TUCK_IN_HEIGHT = -0.05 * layoutUtil.height interface Props { children: ReactNode - tuckedIn: boolean + visible: boolean cardHeight?: number almostTuckedIn?: boolean + cardStyle?: ViewStyle } -const CardModal = memo( - ({ - children, - tuckedIn, - cardHeight = layoutUtil.height, - almostTuckedIn = false, - }: Props) => { - const animation = useValue(0) - const heightValue = multiply(-cardHeight, HEIGHT_OFFSET) +const CardModal = ({ + children, + visible, + cardHeight = layoutUtil.height, + almostTuckedIn = false, + cardStyle, +}: Props) => { + const animation = useValue(0) + const heightValue = useMemo(() => multiply(-cardHeight, HEIGHT_OFFSET), [ + cardHeight, + ]) - useCode( - () => [ - cond( - // down animation - or(not(bin(tuckedIn)), bin(almostTuckedIn)), + const isTuckedIn = bin(visible) + const isAlmostTuckedIn = bin(almostTuckedIn) + + useCode( + () => [ + cond( + // down animation + or(not(isTuckedIn), isAlmostTuckedIn), + set( + animation, + spring({ + to: cond(isAlmostTuckedIn, TUCK_IN_HEIGHT, 0), + from: animation, + config: { + damping: new Value(20), + mass: 0.6, + }, + }) + ), + cond(or(isTuckedIn, not(isAlmostTuckedIn)), [ + // up animation set( animation, spring({ - to: cond(bin(almostTuckedIn), TUCK_IN_HEIGHT, 0), + to: heightValue, from: animation, config: { damping: new Value(20), + mass: 0.8, }, }) ), - cond(or(bin(tuckedIn), not(bin(almostTuckedIn))), [ - // up animation - set( - animation, - spring({ - to: heightValue, - from: animation, - config: { - damping: new Value(20), - }, - }) - ), - ]) - ), - ], - [tuckedIn, heightValue, almostTuckedIn] - ) + ]) + ), + ], + [visible, heightValue, almostTuckedIn] + ) - return ( + return ( + <> {children} - ) - } -) + + ) +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: "white", + borderTopRightRadius: 16, + borderTopLeftRadius: 16, + position: "absolute", + flex: 1, + width: "100%", + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 3, + }, + shadowOpacity: 0.27, + shadowRadius: 6.65, + elevation: 6, + zIndex: 10, + }, +}) -CardModal.displayName = "CardModal" -export default CardModal +export default React.memo(CardModal) diff --git a/template/src/common/components/LoadingPopup.tsx b/template/src/common/components/LoadingPopup.tsx index 08d8c20..cd3bc53 100644 --- a/template/src/common/components/LoadingPopup.tsx +++ b/template/src/common/components/LoadingPopup.tsx @@ -9,7 +9,7 @@ interface LoadingPopupProps { } const LoadingPopup = ({ visible }: LoadingPopupProps) => { const opacity = useTimingTransition(visible, { - duration: visible ? 400 : 100, + duration: visible ? 100 : 50, }) return ( @@ -20,7 +20,7 @@ const LoadingPopup = ({ visible }: LoadingPopupProps) => { > - Loading... This could take a minute. + Please wait... diff --git a/template/src/common/components/Snackbar.tsx b/template/src/common/components/Snackbar.tsx index 14c5ac3..d8bd3c6 100644 --- a/template/src/common/components/Snackbar.tsx +++ b/template/src/common/components/Snackbar.tsx @@ -1,16 +1,47 @@ -import React from "react" +import React, { useEffect, useMemo, useRef } from "react" import { StyleSheet, Text, View } from "react-native" +import { TouchableOpacity } from "react-native-gesture-handler" import Animated, { cond, neq, set, useCode } from "react-native-reanimated" import { bin, timing, useValue } from "react-native-redash" +import { useSafeArea } from "react-native-safe-area-context" -const HEIGHT = 54 +const SNACKBAR_HEIGHT = 54 -interface Props { +interface SnackbarProps { visible: boolean message: string + onDismiss: () => void + onPress?: () => void + btnTitle?: string + unsafeView?: boolean } -const Snackbar = ({ visible, message }: Props) => { - const translateY = useValue(HEIGHT) +const Snackbar = ({ + visible, + message, + onDismiss, + btnTitle, + onPress, + unsafeView, +}: SnackbarProps) => { + const timeoutRef = useRef(-1) + const insets = useSafeArea() + const safeArea = !unsafeView + ? insets + : { top: 0, bottom: 0, left: 0, right: 0 } + const snackbarHeight = + SNACKBAR_HEIGHT + safeArea.bottom + safeArea.bottom / 2 + 10 + const translateY = useValue(snackbarHeight) + const opacity = useMemo( + () => + timing({ + to: 1, + from: 0.2, + duration: 200, + }), + // eslint-disable-next-line + [message] + ) + useCode( () => [ cond( @@ -20,56 +51,140 @@ const Snackbar = ({ visible, message }: Props) => { timing({ from: translateY, to: 0, + duration: 250, }) ), cond( - neq(translateY, HEIGHT), + neq(translateY, snackbarHeight), set( translateY, timing({ from: translateY, - to: HEIGHT, + to: snackbarHeight, + duration: 150, }) ) ) ), ], - [visible] + [visible, snackbarHeight, translateY] ) + useEffect(() => { + if (visible) { + timeoutRef.current = setTimeout(() => { + onDismiss() + }, 3000) + } + + return clearTimeoutRef + }, [onDismiss, visible]) + + const clearTimeoutRef = () => clearTimeout(timeoutRef.current) + + const handleOnPress = () => { + onDismiss() + clearTimeout(timeoutRef.current) + setTimeout(() => { + onPress!() + }, 150) + } + return ( - - - {message} - - + + + + {message} + + {onPress && ( + + + {btnTitle} + + + )} + + + Dismiss + + + + ) } + const styles = StyleSheet.create({ - text: { - marginLeft: 38, - color: "#fff", + touchable: { + flex: 1, + justifyContent: "center", + width: 90, + }, + dismissText: { + paddingHorizontal: 19, fontWeight: "bold", + textAlign: "center", + }, + text: { + color: "white", + fontSize: 14, + flex: 1, + paddingHorizontal: 22, }, snackbar: { - height: HEIGHT, + height: SNACKBAR_HEIGHT, width: "100%", - position: "absolute", bottom: 0, justifyContent: "center", - backgroundColor: "grey", + alignItems: "center", + flexDirection: "row", }, container: { width: "100%", position: "absolute", bottom: 0, + zIndex: 10, }, }) + export default Snackbar diff --git a/template/src/common/components/TouchableScale.tsx b/template/src/common/components/TouchableScale.tsx deleted file mode 100644 index 409d98b..0000000 --- a/template/src/common/components/TouchableScale.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { ReactNode } from "react" -import { StyleProp, ViewStyle } from "react-native" -import { TapGestureHandler } from "react-native-gesture-handler" -import Animated, { Easing } from "react-native-reanimated" -import useScaleTransition, { - ScaleTransitionProps, -} from "../hooks/useScaleTransition" - -interface Props { - children: ReactNode - containerStyle: StyleProp> - config?: Partial -} - -/** - * Performs a scale transition when pressed. - * - * Currently the gesture handler does not respond inside react-native's . - * https://github.com/software-mansion/ react-native-gesture-handler/issues/139 - * - * You can use the Portal component to create your own . - */ -const TouchableScale = ({ children, containerStyle, config }: Props) => { - const animConfig: ScaleTransitionProps = { - duration: config?.duration || 100, - regularScale: config?.regularScale || 1, - scaleWhenPressed: config?.scaleWhenPressed || 0.9, - easing: config?.easing || Easing.inOut(Easing.ease), - onPress: config?.onPress, - } - - const { scale, gestureHandler } = useScaleTransition(animConfig) - return ( - - - {children} - - - ) -} - -export default TouchableScale diff --git a/template/src/common/exceptions/BaseException.ts b/template/src/common/exceptions/BaseException.ts index cf80914..a85ecc6 100644 --- a/template/src/common/exceptions/BaseException.ts +++ b/template/src/common/exceptions/BaseException.ts @@ -25,7 +25,7 @@ export default class BaseException extends Error { } constructor(status: Nullable = 'unknown', message: Nullable = 'unknown', url: Nullable = 'unknown', originalError: Nullable) { - super() + super(message ?? undefined) this._status = status this._message = message diff --git a/template/src/common/exceptions/HttpException.ts b/template/src/common/exceptions/HttpException.ts index 8e6c6f7..d409e3b 100644 --- a/template/src/common/exceptions/HttpException.ts +++ b/template/src/common/exceptions/HttpException.ts @@ -1,8 +1,13 @@ import BaseException from "./BaseException" export default class HttpException extends BaseException { - constructor(status: Nullable = 'unknown', message: Nullable = 'unknown', url: Nullable = 'unknown', originalError: Nullable) { + constructor( + status: Nullable = "unknown", + message: Nullable = "unknown", + url: Nullable = "unknown", + originalError: Nullable + ) { super(status, message, url, originalError) - this._type = 'HttpException' + this._type = "HttpException" } } \ No newline at end of file diff --git a/template/src/common/helpers/layoutUtil.ts b/template/src/common/helpers/layoutUtil.ts index 23d2873..cbc075b 100644 --- a/template/src/common/helpers/layoutUtil.ts +++ b/template/src/common/helpers/layoutUtil.ts @@ -1,19 +1,24 @@ -import { Dimensions, Platform } from "react-native" +import { Dimensions } from "react-native" +import Animated, { call, useCode } from 'react-native-reanimated' // Initial dimensions of the view (not accurate after e.g. rotation) const { height, width } = Dimensions.get("window") +const useDebug = (values: { [key: string]: Animated.Node }) => { + const keys = Object.keys(values) + const nodes = Object.values(values) -// Useful to check if the iOS device has a notch. -const isIphoneX = - Platform.OS === "ios" && - !Platform.isPad && - !Platform.isTVOS && - (height === 812 || width === 812 || height === 896 || width === 896) - + useCode( + () => + call(nodes, (arrayOfNodes) => { + keys.map((key, i) => console.log(key + " " + arrayOfNodes[i])) + }), + [keys] + ) +} export const layoutUtil = { width, height, - isIphoneX, + useDebug } diff --git a/template/src/common/hooks/useScaleTransition.ts b/template/src/common/hooks/useScaleTransition.ts deleted file mode 100644 index 728a34b..0000000 --- a/template/src/common/hooks/useScaleTransition.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useRef } from "react" -import { State } from "react-native-gesture-handler" -import { - call, - cond, - eq, - or, - set, - useCode, - Value, -} from "react-native-reanimated" -import { onGestureEvent, withTimingTransition } from "react-native-redash" - -/** - * Helper for gesture-based scale transitions using Reanimated. - */ -export interface ScaleTransitionProps { - scaleWhenPressed: number - regularScale: number - duration: number - easing?: any - onPress?: () => void -} -const useScaleTransition = ({ - duration, - scaleWhenPressed, - regularScale, - easing, - onPress, -}: ScaleTransitionProps) => { - const state = useRef(new Value(State.UNDETERMINED)).current - const pressed = useRef(new Value(regularScale)).current - const scale = useRef( - withTimingTransition(pressed, { - easing, - duration, - }) - ).current - - useCode( - () => [ - cond(eq(state, State.BEGAN), set(pressed, scaleWhenPressed)), - cond( - or(eq(state, State.END), eq(state, State.FAILED)), - cond(eq(pressed, scaleWhenPressed), [ - set(pressed, regularScale), - cond( - eq(state, State.END), - call([], () => onPress && onPress()) - ), - ]) - ), - ], - [duration, scaleWhenPressed, regularScale, easing, onPress] - ) - - const gestureHandler = onGestureEvent({ state }) - - return { gestureHandler, scale } -} - -export default useScaleTransition diff --git a/template/src/features/error-boundary/RootErrorBoundary.tsx b/template/src/features/error-boundary/RootErrorBoundary.tsx index 3d0d393..3b7c91b 100644 --- a/template/src/features/error-boundary/RootErrorBoundary.tsx +++ b/template/src/features/error-boundary/RootErrorBoundary.tsx @@ -12,7 +12,8 @@ import { import SplashScreen from "react-native-bootsplash" /** - * Displays a friendly UI to the user in the case of an error. + * Example UI to show in the case of a JavaScript error. + * TODO: also handle native exceptions. */ export default class RootErrorBoundary extends Component { private static NO_STACK = "No stack trace." @@ -76,18 +77,17 @@ export default class RootErrorBoundary extends Component { App couldn{"'"}t keep going... - - It would be great if you can report this! - SHOW ERROR - - RESTART APP - + {__DEV__ && ( + + RESTART APP + + )} ) diff --git a/template/src/services/network/github/githubService.ts b/template/src/services/network/github/githubService.ts index ca7a74f..4becb07 100644 --- a/template/src/services/network/github/githubService.ts +++ b/template/src/services/network/github/githubService.ts @@ -17,7 +17,9 @@ type Args = Parameters /** * Wrapper around API instance. * By default, apisauce does not throw on failure. - * For SWR to work, the fetcher needs to throw on failure. + * + * In order for useSWR() to correctly return the error, + * the fetcher needs to throw on failure. */ const client = { /** @@ -32,7 +34,7 @@ const client = { } // Logging -__DEV__ && client.addMonitor(res => console.log(res)) +__DEV__ && client.addMonitor(console.log) @@ -50,7 +52,11 @@ const api = { const util = { throwOnError: (response: ApiResponse) => { if (!response.ok) { - const error = new HttpException(response.status || 'unknown', response.problem, response.config?.url || 'unknown', response) + const error = new HttpException( + response.status || 'unknown', + response.problem, + response.config?.url || 'unknown', + response) __DEV__ && console.log(error) @@ -65,5 +71,6 @@ const util = { export const githubService = { api, paths, - client + client, + util }