From 9dc95ccba4cfb257a44e5e062ea7542035e34f9e Mon Sep 17 00:00:00 2001 From: Nash Vail Date: Thu, 25 May 2017 12:26:19 +0530 Subject: [PATCH] Improved behaviour of UnreadNotice --- src/chat/Chat.js | 19 +++---- src/chat/UnreadNotice.js | 105 ++++++++++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/src/chat/Chat.js b/src/chat/Chat.js index ff3d848bed..692ef9cf4b 100644 --- a/src/chat/Chat.js +++ b/src/chat/Chat.js @@ -4,7 +4,7 @@ import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import styles from '../styles'; import { OfflineNotice } from '../common'; -import { canSendToNarrow, isPrivateNarrow, isStreamNarrow, isTopicNarrow } from '../utils/narrow'; +import { canSendToNarrow } from '../utils/narrow'; import { filterUnreadMessageIds, countUnread } from '../utils/unread'; import { registerAppActivity } from '../utils/activity'; import { queueMarkAsRead } from '../api'; @@ -16,6 +16,8 @@ import UnreadNotice from './UnreadNotice'; export default class Chat extends React.Component { + scrollOffset = 0; + handleMessageListScroll = e => { if (!e.visibleIds) { return; // temporary fix for Android @@ -30,6 +32,9 @@ export default class Chat extends React.Component { queueMarkAsRead(auth, unreadMessageIds); } + // Calculates the amount user has scrolled up from the very bottom + this.scrollOffset = e.contentSize.height - e.contentOffset.y - e.layoutMeasurement.height; + registerAppActivity(auth); }; @@ -41,17 +46,13 @@ export default class Chat extends React.Component { const unreadCount = countUnread(messages.map(msg => msg.id), readIds); const WrapperView = Platform.OS === 'ios' ? KeyboardAvoidingView : View; - const isNarrowWithComposeBox = isStreamNarrow(narrow) || - isTopicNarrow(narrow) || - isPrivateNarrow(narrow); - return ( - {(unreadCount > 0) && } {!isOnline && } {noMessages && } diff --git a/src/chat/UnreadNotice.js b/src/chat/UnreadNotice.js index c29db0edf5..1e292898e5 100644 --- a/src/chat/UnreadNotice.js +++ b/src/chat/UnreadNotice.js @@ -29,65 +29,114 @@ const styles = StyleSheet.create({ } }); -const POSITIONS = { - top: 'top', - bottom: 'bottom' -}; - const showAnimationConfig = { toValue: 1, duration: 400, - useNativeDriver: true, - easing: Easing.bezier(0.17, 0.67, 0.11, 0.99) + easing: Easing.bezier(0.17, 0.67, 0.11, 0.99), + useNativeDriver: true }; const hideAnimationConfig = { toValue: 0, duration: 400, - useNativeDriver: true, - easing: Easing.bezier(0.17, 0.67, 0.11, 0.99) + easing: Easing.bezier(0.17, 0.67, 0.11, 0.99), + useNativeDriver: true }; +// Duration after which notice should hide +const HIDE_DELAY = 1500; +// The translation of notice interpolates this.translateAnimation from 0 to 1 +// Fraction of interpolation at which notice goes into peeking state +const PEEKING_FRACTION = 0.3; +// Amount notice should translate to get itself into the screen +const MAX_TRANSLATION = 40; + +// Notice States +const STATE_HIDDEN = 'hidden'; +const STATE_VISIBLE = 'visible'; +const STATE_PEEKING = 'peeking'; + export default class UnreadNotice extends React.Component { constructor(props) { super(props); this.state = { translateAnimation: new Animated.Value(0), + noticeState: STATE_HIDDEN }; + + this.hideTimeout = null; } - componentDidMount() { - this.show(); + componentWillReceiveProps(nextProps) { + const { unreadCount, scrollOffset } = nextProps; + const { noticeState } = this.state; + + const shouldBecomeVisible = (nextProps.unreadCount > this.props.unreadCount > 0) && + scrollOffset > 0; + + if (noticeState === STATE_PEEKING && unreadCount === 0) this.hidePeekingNotice(); + + if (noticeState === STATE_HIDDEN && shouldBecomeVisible) this.show(); + else if (noticeState === STATE_PEEKING && shouldBecomeVisible) this.show(); + else if (noticeState === STATE_VISIBLE && !shouldBecomeVisible) this.hide(); } + show = () => { + this.state.translateAnimation.setValue(0); + Animated.timing(this.state.translateAnimation, showAnimationConfig).start(() => { + this.setState({ + noticeState: STATE_VISIBLE + }); + + // Notice should go into peeking state + clearTimeout(this.hideTimeout); + this.hideTimeout = setTimeout(this.hide, HIDE_DELAY); + }); + }; + hide = () => { + const { unreadCount } = this.props; this.state.translateAnimation.setValue(1); - Animated.timing(this.state.translateAnimation, hideAnimationConfig).start(); - }; + Animated.timing(this.state.translateAnimation, + Object.assign({}, hideAnimationConfig, { toValue: unreadCount === 0 ? 0 : PEEKING_FRACTION }) + ).start(() => { + this.setState({ + noticeState: this.state.translateAnimation._value === 0 ? // eslint-disable-line + STATE_HIDDEN : + STATE_PEEKING + }); + }); + } - show = () => { - this.state.translateAnimation.setValue(0); - Animated.timing(this.state.translateAnimation, showAnimationConfig).start(); + hidePeekingNotice = () => { + this.state.translateAnimation.setValue(PEEKING_FRACTION); + Animated.timing(this.state.translateAnimation, hideAnimationConfig).start(() => { + this.setState({ + noticeState: STATE_HIDDEN + }); + }); }; dynamicContainerStyles = () => { - const { position, shouldOffsetForInput } = this.props; - const translationMultiplier = position === POSITIONS.top ? -1 : 1; + const { shouldOffsetForInput } = this.props; // In narrows where ComposeBox is present translate beyond ComposeBox to avoid blocking it - const translateTo = shouldOffsetForInput && position === POSITIONS.bottom ? -40 : 0; - const translateFrom = shouldOffsetForInput && position === POSITIONS.bottom ? 0 : 50; + const translateFrom = shouldOffsetForInput ? 0 : MAX_TRANSLATION; + const translateTo = shouldOffsetForInput ? -MAX_TRANSLATION : 0; return { ...StyleSheet.flatten(styles.unreadContainer), - bottom: position === POSITIONS.bottom ? 0 : null, - top: position === POSITIONS.top ? 0 : null, + bottom: 0, + opacity: this.state.translateAnimation.interpolate({ + inputRange: [0, PEEKING_FRACTION, 1], + outputRange: [0, 1, 1] + }), transform: [ { translateY: this.state.translateAnimation.interpolate({ inputRange: [0, 1], - outputRange: [translationMultiplier * translateFrom, translateTo] + outputRange: [translateFrom, translateTo] }) } ] @@ -95,15 +144,15 @@ export default class UnreadNotice extends React.Component { }; render() { - const { count } = this.props; + const { unreadCount } = this.props; return ( - + - {count === 0 ? - 'No' : count < 100 ? - count : '99+'} unread {count === 1 ? 'message' : 'messages'} + {unreadCount === 0 ? + 'No' : unreadCount < 100 ? + unreadCount : '99+'} unread {unreadCount === 1 ? 'message' : 'messages'} );