import * as React from 'react'; import { Animated, Image, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { ACTION, ALERT_TYPE, ENV } from '../config'; import { Color, getImage } from '../service'; export type IConfigDialog = { type: ALERT_TYPE; title?: string; textBody?: string; button?: string; autoClose?: number | boolean; closeOnOverlayTap?: boolean; onPressButton?: () => void; onShow?: () => void; onHide?: () => void; }; type IProps = { isDark: boolean; config?: Pick; }; type IState = { styles: ReturnType; overlayClose?: boolean; visible: boolean; config?: IConfigDialog; }; export class Dialog extends React.Component { /** * @type {React.RefObject} */ public static instance: React.RefObject = React.createRef(); /** * @param {IConfig} args */ public static show = (args: IConfigDialog): void => { Dialog.instance.current?._open(args); }; /** * */ public static hide = (): void => { Dialog.instance.current?._close(); }; private _timeout?: NodeJS.Timeout; /** * @type {Animated.Value} * @private */ private readonly _opacity: Animated.Value; /** * @type {Animated.Value} * @private */ private readonly _positionDialog: Animated.Value; /** * @type {number} * @private */ private _popupHeight: number; /** * @param {IProps} props */ constructor(props: IProps) { super(props); this._opacity = new Animated.Value(0); this._positionDialog = new Animated.Value(ENV.WINDOWS.HEIGHT + 30); this._popupHeight = 0; this.state = { styles: __styles(props.isDark), visible: false, }; } /** * @param {Readonly} prevProps */ public componentDidUpdate = (prevProps: Readonly): void => { if (prevProps.isDark !== this.props.isDark) { // eslint-disable-next-line react/no-did-update-set-state this.setState((prevState) => ({ ...prevState, styles: __styles(this.props.isDark) })); } }; /** * @param {IConfig} config */ private _open = async (config: IConfigDialog): Promise => { if (this.state.visible) { if (this._timeout) { clearTimeout(this._timeout); } await this._startAnimation(ACTION.CLOSE, false); await new Promise((resolve) => this.setState((prevState) => ({ ...prevState, config }), resolve)); await this._startAnimation(ACTION.OPEN); return; } this.setState((prevState) => ({ ...prevState, visible: true, config })); }; /** * @return {Promise} */ private _close = async (): Promise => { if (!this.state.visible) { return; } if (this._timeout) { clearTimeout(this._timeout); } await this._startAnimation(ACTION.CLOSE); const onHide = this.state.config?.onHide; await new Promise((resolve) => this.setState((prevState) => ({ ...prevState, config: undefined, visible: false }), resolve)); onHide?.(); }; /** * @return {Promise} */ private _showModalHandler = async (): Promise => { await this._startAnimation(ACTION.OPEN); const autoClose = this.state.config?.autoClose !== undefined ? this.state.config?.autoClose : this.props.config?.autoClose; if (autoClose) { const duration = typeof autoClose === 'number' ? autoClose : ENV.AUTO_CLOSE; this._timeout = setTimeout(() => { this._close(); }, duration); } this.state.config!.onShow?.(); }; /** * @param {ACTION} action * @param opacity * @return {Promise} */ private _startAnimation = (action: ACTION, opacity: boolean = true): Promise => { const open = action === ACTION.OPEN; let animations = [ opacity ? Animated.timing(this._opacity, { toValue: action === ACTION.OPEN ? 1 : 0, duration: 300, useNativeDriver: false, }) : null!, Animated[open ? 'spring' : 'timing'](this._positionDialog, { toValue: open ? ENV.WINDOWS.HEIGHT / 2 - this._popupHeight / 2 : ENV.WINDOWS.HEIGHT + 30, ...(open ? { bounciness: 12 } : { duration: 250 }), useNativeDriver: true, }), ].filter(Boolean); if (!open) { animations = animations.reverse(); } return new Promise((resolve) => { Animated.sequence(animations).start(() => resolve()); }); }; /** * @return {JSX.Element} */ private _buttonRender = (): JSX.Element => { const { styles } = this.state; const { type, onPressButton, button } = this.state.config!; if (button) { return ( {button} ); } return <>; }; /** * @return {JSX.Element} */ private _OverlayCloseRender = (): JSX.Element => { if (this.state.config?.closeOnOverlayTap === false ? false : this.props.config?.closeOnOverlayTap !== false) { // eslint-disable-next-line react-native/no-inline-styles return ; } return <>; }; /** * @return {JSX.Element} */ private _CardRender = (): JSX.Element => { if (!this.state.config) { return <>; } const { styles, config: { title, type, textBody }, } = this.state; const { _buttonRender } = this; return ( (this._popupHeight = height)} style={StyleSheet.flatten([styles.cardContainer, { transform: [{ translateY: this._positionDialog }] }])} > {title && {title}} {textBody && {textBody}} <_buttonRender /> ); }; /** * @return {JSX.Element} */ public render = (): JSX.Element => { const { visible, styles } = this.state; const { _OverlayCloseRender, _CardRender } = this; return ( <_OverlayCloseRender /> <_CardRender /> ); }; } const __styles = (isDark: boolean) => StyleSheet.create({ backgroundContainer: { ...StyleSheet.absoluteFillObject, backgroundColor: '#00000070', }, cardContainer: { alignSelf: 'center', maxWidth: 400, width: '80%', minHeight: 250, borderRadius: 24, paddingHorizontal: 12, paddingTop: 50, paddingBottom: 24, position: 'absolute', backgroundColor: Color.get('card', isDark), }, cardBody: { flex: 1, justifyContent: 'space-evenly', alignItems: 'center', overflow: 'hidden', }, cardFooter: {}, titleLabel: { fontWeight: 'bold', fontSize: 20, color: Color.get('label', isDark), }, descLabel: { textAlign: 'center', color: Color.get('label', isDark), }, button: { borderRadius: 50, height: 40, width: 130, justifyContent: 'center', alignItems: 'center', alignSelf: 'center', marginTop: 12, }, buttonLabel: { color: '#fff', fontWeight: 'bold', fontSize: 16, }, [ALERT_TYPE.SUCCESS]: { backgroundColor: Color.get('success', isDark), }, [ALERT_TYPE.DANGER]: { backgroundColor: Color.get('danger', isDark), }, [ALERT_TYPE.WARNING]: { backgroundColor: Color.get('warning', isDark), }, [ALERT_TYPE.INFO]: { backgroundColor: Color.get('info', isDark), }, backendImage: { position: 'absolute', alignSelf: 'center', justifyContent: 'center', height: 50, width: 50, backgroundColor: '#FBFBFB', borderRadius: 100, marginTop: -10, }, image: { alignSelf: 'center', width: 80, aspectRatio: 1, position: 'absolute', top: -30, }, [`${ALERT_TYPE.SUCCESS}Image`]: { tintColor: Color.get('success', isDark), }, [`${ALERT_TYPE.DANGER}Image`]: { tintColor: Color.get('danger', isDark), }, [`${ALERT_TYPE.WARNING}Image`]: { tintColor: Color.get('warning', isDark), }, [`${ALERT_TYPE.INFO}Image`]: { tintColor: Color.get('info', isDark), }, });