import * as React from 'react'; import { Animated, Button, Component, Styles, Text, Types, UserInterface, View } from 'reactxp'; import { colors } from '../../config.json'; import ImageView from './ImageView'; const styles = { collapsible: Styles.createViewStyle({ backgroundColor: 'rgba(25, 38, 56, 0.95)', overflow: 'hidden', }), drawer: Styles.createViewStyle({ justifyContent: 'flex-end', }), container: Styles.createViewStyle({ flexDirection: 'row', paddingTop: 8, paddingLeft: 20, paddingRight: 10, paddingBottom: 8, }), indicator: { base: Styles.createViewStyle({ width: 10, height: 10, flex: 0, borderRadius: 5, marginTop: 4, marginRight: 8, }), warning: Styles.createViewStyle({ backgroundColor: colors.yellow, }), success: Styles.createViewStyle({ backgroundColor: colors.green, }), error: Styles.createViewStyle({ backgroundColor: colors.red, }), }, textContainer: Styles.createViewStyle({ flex: 1, }), actionContainer: Styles.createViewStyle({ flex: 0, flexDirection: 'column', justifyContent: 'center', marginLeft: 5, }), actionButton: Styles.createButtonStyle({ flex: 1, justifyContent: 'center', cursor: 'default', paddingLeft: 5, paddingRight: 5, }), title: Styles.createTextStyle({ fontFamily: 'Open Sans', fontSize: 13, fontWeight: '800', lineHeight: 18, color: colors.white, }), subtitle: Styles.createTextStyle({ fontFamily: 'Open Sans', fontSize: 13, fontWeight: '600', lineHeight: 18, color: colors.white60, }), }; export class NotificationTitle extends Component { public render() { return {this.props.children}; } } export class NotificationSubtitle extends Component { public render() { return React.Children.count(this.props.children) > 0 ? ( {this.props.children} ) : null; } } export class NotificationOpenLinkAction extends Component<{ onPress: () => void }> { public state = { hovered: false, }; public render() { return ( ); } private onHoverStart = () => { this.setState({ hovered: true }); }; private onHoverEnd = () => { this.setState({ hovered: false }); }; } export class NotificationContent extends Component { public render() { return {this.props.children}; } } export class NotificationActions extends Component { public render() { return {this.props.children}; } } export class NotificationIndicator extends Component<{ type: 'success' | 'warning' | 'error' }> { public render() { return ; } } interface INotificationBannerProps { children: React.ReactNode; // Array, style?: Types.ViewStyleRuleSet; visible: boolean; animationDuration: number; } interface INotificationBannerState { contentPinnedToBottom: boolean; } export class NotificationBanner extends Component< INotificationBannerProps, INotificationBannerState > { public static defaultProps = { animationDuration: 350, }; public state = { contentPinnedToBottom: false, }; private containerRef = React.createRef(); private contentHeight = 0; private heightValue = Animated.createValue(0); private animationStyle: Types.AnimatedViewStyleRuleSet; private animation?: Types.Animated.CompositeAnimation; private didFinishFirstLayoutPass = false; constructor(props: INotificationBannerProps) { super(props); this.animationStyle = Styles.createAnimatedViewStyle({ height: this.heightValue, }); } public shouldComponentUpdate( nextProps: INotificationBannerProps, nextState: INotificationBannerState, ) { return ( this.props.children !== nextProps.children || this.props.visible !== nextProps.visible || this.state.contentPinnedToBottom !== nextState.contentPinnedToBottom ); } public componentDidUpdate(prevProps: INotificationBannerProps) { if (prevProps.visible !== this.props.visible) { // enable drawer-like animation when changing banner's visibility this.setState({ contentPinnedToBottom: true }, () => { this.animateHeightChanges(); }); } } public componentWillUnmount() { if (this.animation) { this.animation.stop(); } } public render() { return ( {this.props.children} ); } private onLayout = ({ height }: Types.ViewOnLayoutEvent) => { const oldHeight = this.contentHeight; this.contentHeight = height; // The first layout pass should not be animated because this would cause the initially visible // notification banner to slide down each time the component is mounted. if (this.didFinishFirstLayoutPass) { if (oldHeight !== height) { this.animateHeightChanges(); } } else { this.didFinishFirstLayoutPass = true; if (this.props.visible) { this.stopAnimation(); this.heightValue.setValue(height); } } }; private async animateHeightChanges() { const containerView = this.containerRef.current; if (!containerView) { return; } this.stopAnimation(); // calculate the animation duration based on travel distance const layout = await UserInterface.measureLayoutRelativeToWindow(containerView); const toValue = this.props.visible ? this.contentHeight : 0; const multiplier = Math.abs(toValue - layout.height) / Math.max(1, this.contentHeight); const duration = Math.ceil(this.props.animationDuration * multiplier); const animation = Animated.timing(this.heightValue, { toValue, easing: Animated.Easing.InOut(), duration, useNativeDriver: true, }); this.animation = animation; animation.start(({ finished }) => { if (finished) { // disable drawer-like animations for content updates when the banner is visible this.setState({ contentPinnedToBottom: false }); } }); } private stopAnimation() { if (this.animation) { this.animation.stop(); this.animation = undefined; } } }