Skip to content

Commit

Permalink
Improved behaviour of UnreadNotice
Browse files Browse the repository at this point in the history
  • Loading branch information
nashvail authored and borisyankov committed May 25, 2017
1 parent 8297fdc commit 9dc95cc
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 37 deletions.
19 changes: 10 additions & 9 deletions src/chat/Chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -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);
};

Expand All @@ -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 (
<ActionSheetProvider>
<WrapperView style={styles.screen} behavior="padding">
{(unreadCount > 0) && <UnreadNotice
position="bottom"
count={unreadCount}
shouldOffsetForInput={isNarrowWithComposeBox}
{<UnreadNotice
unreadCount={unreadCount}
scrollOffset={this.scrollOffset}
shouldOffsetForInput={canSendToNarrow(narrow)}
/>}
{!isOnline && <OfflineNotice />}
{noMessages && <NoMessages narrow={narrow} />}
Expand Down
105 changes: 77 additions & 28 deletions src/chat/UnreadNotice.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,81 +29,130 @@ 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]
})
}
]
};
};

render() {
const { count } = this.props;
const { unreadCount } = this.props;

return (
<Animated.View style={this.dynamicContainerStyles()}>
<IconDownArrow style={[styles.icon, styles.downArrowIcon]} />
<IconDownArrow style={styles.icon} />
<Text style={styles.unreadText}>
{count === 0 ?
'No' : count < 100 ?
count : '99+'} unread {count === 1 ? 'message' : 'messages'}
{unreadCount === 0 ?
'No' : unreadCount < 100 ?
unreadCount : '99+'} unread {unreadCount === 1 ? 'message' : 'messages'}
</Text>
</Animated.View>
);
Expand Down

0 comments on commit 9dc95cc

Please sign in to comment.