diff --git a/.gitignore b/.gitignore index 2ea1a85..e11bbcd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ build dist # Dependency directory -node_modules \ No newline at end of file +node_modules + +.idea \ No newline at end of file diff --git a/README.md b/README.md index f50c0fe..9785995 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ import Another from './Another' const App = () => { const showToasts = () => { - Toast.success('Promised is resolved') + Toast.success({ text: 'Promised is resolved' }) } return ( @@ -93,7 +93,7 @@ import { Toast } from 'toastify-react-native' const Another = () => ( Toast.info('Lorem ipsum info', 'bottom')} + onPress={() => Toast.info({ text: 'Lorem ipsum info' })} style={styles.buttonStyle} > SHOW SOME AWESOMENESS! @@ -124,12 +124,12 @@ For a more complex example take a look at the `/example` directory. ## Available props | Name | Type | Default | Description | -| --------------------------- | ---------------------------------- | -------------- | ---------------------------------------------- | +| --------------------------- |------------------------------------| -------------- | ---------------------------------------------- | | width | number | 256 | Width of toast | | height | number | 68 | Height of the toast | | style | any | null | Style applied to the toast | | textStyle | any | null | Style applied to the toast content | -| position | top, center or bottom | top | Position of toast | +| position | top or bottom | top | Position of toast | | positionValue | number | 50 | position value of toast | | duration | number | 3000 | The display time of toast. | | animationStyle | upInUpOut, rightInOut or zoomInOut | upInUpOut | The animation style of toast | diff --git a/components/ToastManager.tsx b/components/ToastManager.tsx index 0e1ba46..4929397 100644 --- a/components/ToastManager.tsx +++ b/components/ToastManager.tsx @@ -1,138 +1,213 @@ -import Icon from 'react-native-vector-icons/Ionicons' -import Modal from 'react-native-modal' -import React, { Component } from 'react' -import { RFPercentage } from 'react-native-responsive-fontsize' -import { View, Text, Animated, Dimensions, TouchableOpacity } from 'react-native' +import React, { Component } from "react"; +import { + Animated, + EmitterSubscription, + Keyboard, + KeyboardEvent, + Text, + TouchableOpacity, + View, +} from "react-native"; +import Modal from "react-native-modal"; +import Icon from "react-native-vector-icons/Ionicons"; +import { Colors } from "../config/theme"; -import defaultProps from '../utils/defaultProps' -import { Colors } from '../config/theme' -import styles from './styles' -import { ToastManagerProps, ToastManagerState } from '../utils/interfaces' - -const { height } = Dimensions.get('window') +import { + animationStyleOptions, + defaultProps, + defaultToastData, +} from "../utils/default"; +import generateUUID from "../utils/generateUUID"; +import { + NotificationArgumentsType, + ToastManagerProps, + ToastManagerState, + ToastType, +} from "../utils/interfaces"; +import styles from "./styles"; class ToastManager extends Component { - private timer: NodeJS.Timeout - private isShow: boolean - static defaultProps = defaultProps - static __singletonRef: ToastManager | null + static defaultProps = defaultProps; + static __singletonRef: ToastManager | null; constructor(props: ToastManagerProps) { - super(props) - ToastManager.__singletonRef = this - this.timer = setTimeout(() => {}, 0) // Initialize timer with a dummy value - this.isShow = false - } + super(props); + ToastManager.__singletonRef = this; - state: any = { - isShow: false, - text: '', - opacityValue: new Animated.Value(1), - barWidth: new Animated.Value(RFPercentage(32)), - barColor: Colors.default, - icon: 'checkmark-circle', - position: this.props.position, - animationStyle: { - upInUpOut: { - animationIn: 'slideInDown', - animationOut: 'slideOutUp', - }, - rightInOut: { - animationIn: 'slideInRight', - animationOut: 'slideOutRight', - }, - zoomInOut: { - animationIn: 'zoomInDown', - animationOut: 'zoomOutUp', - }, - }, + this.state = { + toasts: [] as ToastType[], + keyboardHeight: 0, + }; } - static info = (text: string, position: string) => { - ToastManager.__singletonRef?.show(text, Colors.info, 'ios-information-circle', position) - } + static info = (toastData: Partial) => { + ToastManager.__singletonRef?.show?.({ + ...defaultToastData, + ...toastData, + barColor: Colors.info, + icon: "information-circle", + } as ToastType); + }; - static success = (text: string, position?: string) => { - ToastManager.__singletonRef?.show(text, Colors.success, 'checkmark-circle', position) - } + static success = (toastData: Partial) => { + ToastManager.__singletonRef?.show?.({ + ...defaultToastData, + ...toastData, + barColor: Colors.success, + icon: "checkmark-circle", + } as ToastType); + }; - static warn = (text: string, position: string) => { - ToastManager.__singletonRef?.show(text, Colors.warn, 'warning', position) - } + static warn = (toastData: Partial) => { + ToastManager.__singletonRef?.show?.({ + ...defaultToastData, + ...toastData, + barColor: Colors.warn, + icon: "warning", + } as ToastType); + }; - static error = (text: string, position: string) => { - ToastManager.__singletonRef?.show(text, Colors.error, 'alert-circle', position) - } + static error = (toastData: Partial) => { + ToastManager.__singletonRef?.show?.({ + ...defaultToastData, + ...toastData, + barColor: Colors.error, + icon: "alert-circle", + } as ToastType); + }; - show = (text = '', barColor = Colors.default, icon: string, position?: string) => { - const { duration } = this.props - this.state.barWidth.setValue(this.props.width) - this.setState({ - isShow: true, - duration, + show = ({ + text, + barColor, + icon, + duration, + id, + position, + width, + }: ToastType) => { + const toastId = id || generateUUID(); + const { toasts } = this.state; + + let newToasts: ToastType[] = []; + + const newToast: Omit = { text, + duration, barColor, - icon, - }) - if (position) this.setState({ position }) - this.isShow = true - if (duration !== this.props.end) this.close(duration) - } + position, + icon: icon, + barWidthAnimation: new Animated.Value(width), + width, + }; - close = (duration: number) => { - if (!this.isShow && !this.state.isShow) return - this.resetAll() - this.timer = setTimeout(() => { - this.setState({ isShow: false }) - }, duration || this.state.duration) - } + const oldToast = toasts.find((toast) => toast.id === id); - position = () => { - const { position } = this.state - if (position === 'top') return this.props.positionValue - if (position === 'center') return height / 2 - RFPercentage(9) - return height - this.props.positionValue - RFPercentage(10) - } + if (oldToast) { + newToasts = toasts.map((toast) => + toast.id === id + ? { + ...toast, + ...newToast, + } + : toast + ); + } else { + newToasts = [ + ...toasts, + { + ...newToast, + id: toastId, + }, + ]; + } + this.setState({ toasts: newToasts }, () => this.handleBar()); + }; + + keyboardDidShowListener: EmitterSubscription | null = null; + + keyboardDidHideListener: EmitterSubscription | null = null; + + keyboardDidShow = (e: KeyboardEvent) => { + this.setState({ + keyboardHeight: e.endCoordinates.height, + }); + }; + + keyboardDidHide = () => { + this.setState({ + keyboardHeight: 0, + }); + }; + + UNSAFE_componentWillMount = () => { + this.keyboardDidShowListener = Keyboard.addListener( + "keyboardDidShow", + this.keyboardDidShow + ); + this.keyboardDidHideListener = Keyboard.addListener( + "keyboardDidHide", + this.keyboardDidHide + ); + }; + + componentWillUnmount = () => { + this.keyboardDidShowListener?.remove(); + this.keyboardDidHideListener?.remove(); + }; handleBar = () => { - Animated.timing(this.state.barWidth, { - toValue: 0, - duration: this.state.duration, - useNativeDriver: false, - }).start() - } + const { toasts } = this.state; + Animated.parallel( + toasts.map((toast) => { + const duration = + // @ts-ignore + (toast.barWidthAnimation._value * toast.duration) / toast.width; - pause = () => { - this.setState({ oldDuration: this.state.duration, duration: 10000 }) - Animated.timing(this.state.barWidth, { - toValue: 0, - duration: this.state.duration, - useNativeDriver: false, - }).stop() - } + const animation = Animated.timing(toast.barWidthAnimation, { + toValue: 0, + duration, + useNativeDriver: false, + }); - resume = () => { - this.setState({ duration: this.state.oldDuration, oldDuration: 0 }) - Animated.timing(this.state.barWidth, { - toValue: 0, - duration: this.state.duration, - useNativeDriver: false, - }).start() - } + // @ts-ignore + animation.start( + () => toast.barWidthAnimation._value === 0 && this.hideToast(toast.id) + ); + return animation; + }) + ); + }; - hideToast = () => { - this.resetAll() - this.setState({ isShow: false }) - this.isShow = false - if (!this.isShow && !this.state.isShow) return - } + pause = (): void => { + const { toasts } = this.state; + toasts.forEach((toast) => { + return toast.barWidthAnimation.stopAnimation(); + }); + }; - resetAll = () => { - clearTimeout(this.timer) - } + hideToast = (toastId: string): void => { + const { toasts } = this.state; + const filtredToasts = toasts.filter((toast) => toast.id !== toastId); + if (filtredToasts.length !== toasts.length) { + this.setState({ + toasts: filtredToasts, + }); + //@ts-ignore + this.state.toasts = filtredToasts; + } + }; - render() { - this.handleBar() + renderToast = ({ + icon, + id, + barWidthAnimation, + barColor, + text, + key, + positionOffset, + height, + width, + }: NotificationArgumentsType) => { const { animationIn, animationStyle, @@ -144,41 +219,36 @@ class ToastManager extends Component { backdropColor, backdropOpacity, hasBackdrop, - width, - height, + position, style, textStyle, theme, - } = this.props - - const { - isShow, - animationStyle: stateAnimationStyle, - barColor, - icon, - text, - barWidth, - } = this.state - + } = this.props; return ( this.hideToast(id)} + onModalHide={() => this.hideToast(id)} + isVisible coverScreen={false} backdropColor={backdropColor} backdropOpacity={backdropOpacity} hasBackdrop={hasBackdrop} style={styles.modalContainer} + hideModalContentWhileAnimating > { width, height, backgroundColor: Colors[theme].back, - top: this.position(), ...style, + bottom: position === "bottom" ? positionOffset : undefined, + top: position === "top" ? positionOffset : undefined, }, ]} + onLayout={(event) => { + const layout = event.nativeEvent.layout; + const toast = this.state.toasts.find((toast) => toast.id === id); + if (toast) toast.height = layout.height; + }} > - - + this.hideToast(id)} + activeOpacity={0.9} + style={styles.hideButton} + > + - - + + {text} - + - ) + ); + }; + + render() { + const { toasts, keyboardHeight } = this.state; + return toasts.map((toast, index) => { + const positionOffset: number = toasts.reduce( + (reduceBottom, reduceToast, reduceIndex) => { + if (reduceIndex >= index) return reduceBottom; + return reduceBottom + (reduceToast.height! || 200) + 10; + }, + 0 + ); + + return this.renderToast({ + ...toast, + key: toast.id, + positionOffset: + this.props.position === "top" + ? this.props.positionValue + positionOffset + : positionOffset + this.props.positionValue + keyboardHeight, + }); + }); } } -ToastManager.defaultProps = defaultProps +ToastManager.defaultProps = defaultProps; -export default ToastManager +export default ToastManager; diff --git a/components/styles.ts b/components/styles.ts index 920748b..d49aa9c 100644 --- a/components/styles.ts +++ b/components/styles.ts @@ -1,21 +1,21 @@ -import { RFPercentage } from 'react-native-responsive-fontsize' -import { StyleSheet } from 'react-native' +import { StyleSheet } from "react-native"; +import { RFPercentage } from "react-native-responsive-fontsize"; const styles = StyleSheet.create({ modalContainer: { flex: 1, - alignItems: 'center', - justifyContent: 'center', + alignItems: "center", + justifyContent: "center", zIndex: 200, }, mainContainer: { borderRadius: 6, - position: 'absolute', - flexDirection: 'column', - alignItems: 'flex-start', - justifyContent: 'center', - shadowColor: '#000', + position: "absolute", + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + shadowColor: "#000", shadowOffset: { width: 0, height: 2, @@ -26,35 +26,36 @@ const styles = StyleSheet.create({ }, hideButton: { - position: 'absolute', + position: "absolute", top: RFPercentage(0.5), right: RFPercentage(0.5), + zIndex: 1, }, textStyle: { fontSize: RFPercentage(2.5), - fontWeight: '400', + fontWeight: "400", }, progressBarContainer: { - flexDirection: 'row', - position: 'absolute', + flexDirection: "row", + position: "absolute", height: 4, - width: '100%', + width: "100%", bottom: 0, }, content: { - width: '100%', + width: "100%", padding: RFPercentage(1.5), - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-start', + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-start", }, iconWrapper: { marginRight: RFPercentage(0.7), }, -}) +}); -export default styles +export default styles; diff --git a/package.json b/package.json index 8caaf1d..886597b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "registry": "https://npm.pkg.github.com/@zahidalidev" }, - "version": "5.0.0", + "version": "5.0.1", "description": "🎉 toastify-react-native allows you to add notifications to your react-native app (ios, android) with ease. No more nonsense!", "main": "index.ts", "scripts": { diff --git a/utils/default.ts b/utils/default.ts new file mode 100644 index 0000000..8c95337 --- /dev/null +++ b/utils/default.ts @@ -0,0 +1,52 @@ +import { Animated } from "react-native"; +import { RFPercentage } from "react-native-responsive-fontsize"; +import { AnimationStyleProps, ToastType } from "./interfaces"; +import { Colors } from "../config/theme"; + +export const defaultProps = { + theme: "light", + style: {}, + textStyle: {}, + position: "top", + positionValue: 50, + animationInTiming: 300, + animationOutTiming: 300, + backdropTransitionInTiming: 300, + backdropTransitionOutTiming: 300, + animationIn: "", + animationOut: "", + animationStyle: "slide", + hasBackdrop: false, + backdropColor: "black", + backdropOpacity: 0.2, +}; + +export const defaultToastData: Partial = { + position: "top", + duration: 3000, + text: "", + barColor: Colors.default, + icon: "checkmark-circle", + barWidthAnimation: new Animated.Value(RFPercentage(32)), + width: RFPercentage(32), + height: RFPercentage(8.5), +}; + +export const animationStyleOptions: AnimationStyleProps = { + upInUpOut: { + animationIn: "slideInDown", + animationOut: "slideOutUp", + }, + rightInOut: { + animationIn: "slideInRight", + animationOut: "slideOutRight", + }, + zoomInOut: { + animationIn: "zoomInDown", + animationOut: "zoomOutUp", + }, + slide: { + animationIn: "slideInUp", + animationOut: "slideOutDown", + }, +}; diff --git a/utils/defaultProps.ts b/utils/defaultProps.ts deleted file mode 100644 index a7a3f0d..0000000 --- a/utils/defaultProps.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RFPercentage } from 'react-native-responsive-fontsize' - -export default { - theme: 'light', - width: RFPercentage(32), - height: RFPercentage(8.5), - style: {}, - textStyle: {}, - position: 'top', - positionValue: 50, - end: 0, - duration: 3000, - animationInTiming: 300, - animationOutTiming: 300, - backdropTransitionInTiming: 300, - backdropTransitionOutTiming: 300, - animationIn: '', - animationOut: '', - animationStyle: 'upInUpOut', - hasBackdrop: false, - backdropColor: 'black', - backdropOpacity: 0.2, -} diff --git a/utils/generateUUID.ts b/utils/generateUUID.ts new file mode 100644 index 0000000..01f4801 --- /dev/null +++ b/utils/generateUUID.ts @@ -0,0 +1,2 @@ +const generateUUID = () => "id" + Math.random().toString(16).slice(2); +export default generateUUID; diff --git a/utils/interfaces.ts b/utils/interfaces.ts index 3b83236..e09fdea 100644 --- a/utils/interfaces.ts +++ b/utils/interfaces.ts @@ -1,36 +1,54 @@ -type AnimationStyle = any +import { Animated } from "react-native"; +import { ModalProps } from "react-native-modal/dist/modal"; + +type AnimationStyle = "upInUpOut" | "rightInOut" | "zoomInOut"; + +type Position = "top" | "bottom"; export interface ToastManagerProps { - positionValue: number - width: number - duration: number - end: number - animationIn?: any - animationOut?: any - backdropTransitionOutTiming: number - backdropTransitionInTiming: number - animationInTiming: number - animationOutTiming: number - backdropColor: string - backdropOpacity: number - hasBackdrop: boolean - height: number - style: any - textStyle: any - theme: any - animationStyle?: AnimationStyle - position?: any + positionValue: number; + position?: Position; + animationIn?: ModalProps["animationIn"]; + animationOut?: ModalProps["animationOut"]; + backdropTransitionOutTiming: number; + backdropTransitionInTiming: number; + animationInTiming: number; + animationOutTiming: number; + backdropColor: string; + backdropOpacity: number; + hasBackdrop: boolean; + style: any; + textStyle: any; + theme: any; + animationStyle?: AnimationStyle; } export interface ToastManagerState { - isShow: boolean - text: string - opacityValue: any - barWidth: any - barColor: string - icon: string - position: string - duration: number - oldDuration: number - animationStyle: Record + toasts: ToastType[]; + keyboardHeight: number; +} + +export interface ToastType { + position: string; + duration: number; + text: string; + barColor: string; + icon: string; + id: string; + barWidthAnimation: Animated.Value; + width: number; + height?: number; } + +export interface NotificationArgumentsType extends ToastType { + key?: string; + positionOffset?: number; +} + +export type AnimationStyleProps = Record< + string, + { + animationIn: ModalProps["animationIn"]; + animationOut: ModalProps["animationOut"]; + } +>;