[MM-21155] Fix typing animations (#4373)
* Fix typing animations * Remove animations on unmount * Remove obsolete rn-placeholder mocks * Apply thread animation for Android * No need to call scrollToInitialIndexIfNeeded * Fix snapshot test * Add context
This commit is contained in:
parent
9c9aee69f4
commit
81dd01ba2c
21 changed files with 426 additions and 169 deletions
|
|
@ -33,7 +33,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -44,18 +44,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -73,7 +73,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -84,18 +84,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -113,7 +113,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -124,18 +124,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -153,7 +153,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -164,18 +164,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -193,7 +193,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -204,18 +204,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -233,7 +233,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -244,18 +244,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -273,7 +273,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -284,18 +284,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -313,7 +313,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -324,18 +324,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -353,7 +353,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -364,18 +364,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -393,7 +393,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -404,18 +404,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -433,7 +433,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -444,18 +444,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -473,7 +473,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -484,18 +484,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
|
@ -513,7 +513,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
Animation={[Function]}
|
||||
Left={[Function]}
|
||||
styles={
|
||||
|
|
@ -524,18 +524,18 @@ exports[`ChannelLoader should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={80}
|
||||
/>
|
||||
<UNDEFINED
|
||||
<Unknown
|
||||
color="rgba(61,60,64,0.15)"
|
||||
width={60}
|
||||
/>
|
||||
</UNDEFINED>
|
||||
</Unknown>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ import Preferences from '@mm-redux/constants/preferences';
|
|||
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
jest.mock('rn-placeholder', () => ({
|
||||
ImageContent: () => null,
|
||||
}));
|
||||
|
||||
describe('ChannelLoader', () => {
|
||||
const baseProps = {
|
||||
channelIsLoading: true,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
|||
|
||||
export default class PostDraft extends PureComponent {
|
||||
static propTypes = {
|
||||
registerTypingAnimation: PropTypes.func.isRequired,
|
||||
addReactionToLatestPost: PropTypes.func.isRequired,
|
||||
canPost: PropTypes.bool.isRequired,
|
||||
channelDisplayName: PropTypes.string,
|
||||
|
|
@ -513,13 +514,17 @@ export default class PostDraft extends PureComponent {
|
|||
maxMessageLength,
|
||||
screenId,
|
||||
valueEvent,
|
||||
registerTypingAnimation,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const readonly = channelIsReadOnly || !canPost;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typing theme={theme}/>
|
||||
<Typing
|
||||
theme={theme}
|
||||
registerTypingAnimation={registerTypingAnimation}
|
||||
/>
|
||||
{Platform.OS === 'android' &&
|
||||
<Autocomplete
|
||||
cursorPositionEvent={cursorPositionEvent}
|
||||
|
|
|
|||
|
|
@ -8,43 +8,52 @@ import {
|
|||
Text,
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SafeAreaView from '@components/safe_area_view';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {TYPING_VISIBLE, TYPING_HEIGHT} from '@constants/post_draft';
|
||||
|
||||
export default class Typing extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
typing: PropTypes.array.isRequired,
|
||||
registerTypingAnimation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
typing: [],
|
||||
};
|
||||
|
||||
state = {
|
||||
typingHeight: new Animated.Value(0),
|
||||
typingBottom = new Animated.Value(0);
|
||||
|
||||
componentDidMount() {
|
||||
this.removeTypingAnimation = this.props.registerTypingAnimation(this.typingAnimation);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.typing.length && !prevProps.typing.length) {
|
||||
this.animateTyping(true);
|
||||
EventEmitter.emit(TYPING_VISIBLE, true);
|
||||
} else if (!this.props.typing.length) {
|
||||
this.animateTyping();
|
||||
EventEmitter.emit(TYPING_VISIBLE, false);
|
||||
}
|
||||
}
|
||||
|
||||
animateTyping = (show = false) => {
|
||||
const [height, duration] = show ?
|
||||
[20, 200] :
|
||||
componentWillUnmount() {
|
||||
this.removeTypingAnimation();
|
||||
}
|
||||
|
||||
typingAnimation = (visible = false) => {
|
||||
const [bottom, duration] = visible ?
|
||||
[TYPING_HEIGHT, 200] :
|
||||
[0, 400];
|
||||
|
||||
Animated.timing(this.state.typingHeight, {
|
||||
toValue: height,
|
||||
return Animated.timing(this.typingBottom, {
|
||||
toValue: bottom,
|
||||
duration,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
renderTyping = () => {
|
||||
|
|
@ -85,15 +94,21 @@ export default class Typing extends PureComponent {
|
|||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
return (
|
||||
<AnimatedView style={{height: this.state.typingHeight}}>
|
||||
<Text
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
<Animated.View style={{bottom: this.typingBottom}}>
|
||||
<SafeAreaView
|
||||
excludeHeader={true}
|
||||
excludeFooter={true}
|
||||
useLandscapeMargin={true}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</AnimatedView>
|
||||
<Text
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</SafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -101,6 +116,7 @@ export default class Typing extends PureComponent {
|
|||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
typing: {
|
||||
position: 'absolute',
|
||||
paddingLeft: 10,
|
||||
paddingTop: 3,
|
||||
fontSize: 11,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
const {View: AnimatedView} = Animated;
|
||||
import {Animated} from 'react-native';
|
||||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {TYPING_VISIBLE} from '@constants/post_draft';
|
||||
|
||||
import Typing from './typing';
|
||||
|
||||
|
|
@ -15,17 +16,35 @@ describe('Typing', () => {
|
|||
theme: {
|
||||
centerChannelColor: 'blue',
|
||||
},
|
||||
registerTypingAnimation: jest.fn(() => {
|
||||
return jest.fn();
|
||||
}),
|
||||
};
|
||||
|
||||
EventEmitter.emit = jest.fn();
|
||||
|
||||
test('should render component without error', () => {
|
||||
const wrapper = shallow(
|
||||
<Typing {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(AnimatedView).exists()).toBe(true);
|
||||
expect(wrapper.find(Animated.View).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should call animateTyping when next typing props is not empty and current is empty', () => {
|
||||
test('should not emit TYPING_VISIBLE when typing props is not empty and previous is not empty', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
typing: ['user2'],
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<Typing {...props}/>,
|
||||
);
|
||||
|
||||
wrapper.setProps({typing: ['user2 and user3']});
|
||||
expect(EventEmitter.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should emit TYPING_VISIBLE with true when typing props is not empty and previous is empty', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
typing: [],
|
||||
|
|
@ -33,22 +52,30 @@ describe('Typing', () => {
|
|||
const wrapper = shallow(
|
||||
<Typing {...props}/>,
|
||||
);
|
||||
wrapper.instance().animateTyping = jest.fn();
|
||||
|
||||
wrapper.setProps({typing: ['user2']});
|
||||
expect(wrapper.instance().animateTyping).toHaveBeenCalledWith(true);
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith(TYPING_VISIBLE, true);
|
||||
});
|
||||
|
||||
test('should call animateTyping when next typing props is not empty', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
};
|
||||
test('should emit TYPING_VISIBLE with false when typing props is empty', () => {
|
||||
const wrapper = shallow(
|
||||
<Typing {...props}/>,
|
||||
<Typing {...baseProps}/>,
|
||||
);
|
||||
wrapper.instance().animateTyping = jest.fn();
|
||||
|
||||
wrapper.setProps({typing: []});
|
||||
expect(wrapper.instance().animateTyping).toHaveBeenCalledWith();
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith(TYPING_VISIBLE, false);
|
||||
});
|
||||
|
||||
test('should add/remove typing animation on mount/unmount', () => {
|
||||
const wrapper = shallow(
|
||||
<Typing {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
expect(baseProps.registerTypingAnimation).toHaveBeenCalledTimes(1);
|
||||
expect(instance.removeTypingAnimation).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.unmount();
|
||||
expect(instance.removeTypingAnimation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ export const INSERT_TO_DRAFT = 'insert_to_draft';
|
|||
export const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
|
||||
export const MAX_FILE_COUNT = 5;
|
||||
export const MAX_MESSAGE_LENGTH_FALLBACK = 4000;
|
||||
export const TYPING_VISIBLE = 'typingVisible';
|
||||
export const TYPING_HEIGHT = 18;
|
||||
|
|
|
|||
|
|
@ -67,11 +67,12 @@ export default class ChannelAndroid extends ChannelBase {
|
|||
component = (
|
||||
<KeyboardLayout>
|
||||
<View style={style.flex}>
|
||||
<ChannelPostList/>
|
||||
<ChannelPostList registerTypingAnimation={this.registerTypingAnimation}/>
|
||||
</View>
|
||||
<PostDraft
|
||||
ref={this.postDraft}
|
||||
screenId={this.props.componentId}
|
||||
registerTypingAnimation={this.registerTypingAnimation}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export default class ChannelIOS extends ChannelBase {
|
|||
<>
|
||||
<ChannelPostList
|
||||
updateNativeScrollView={this.updateNativeScrollView}
|
||||
registerTypingAnimation={this.registerTypingAnimation}
|
||||
/>
|
||||
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
|
||||
<Autocomplete
|
||||
|
|
@ -101,6 +102,7 @@ export default class ChannelIOS extends ChannelBase {
|
|||
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
|
||||
ref={this.postDraft}
|
||||
screenId={this.props.componentId}
|
||||
registerTypingAnimation={this.registerTypingAnimation}
|
||||
/>
|
||||
</KeyboardTrackingView>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Keyboard, StyleSheet} from 'react-native';
|
||||
import {Animated, Keyboard, StyleSheet} from 'react-native';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {showModal, showModalOverCurrentContext} from '@actions/navigation';
|
||||
import LocalConfig from '@assets/config';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import {TYPING_VISIBLE} from '@constants/post_draft';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {unsupportedServer} from '@utils/supported_server';
|
||||
|
|
@ -65,6 +66,8 @@ export default class ChannelBase extends PureComponent {
|
|||
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
|
||||
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
|
||||
}
|
||||
|
||||
this.typingAnimations = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -80,6 +83,7 @@ export default class ChannelBase extends PureComponent {
|
|||
} = this.props;
|
||||
EventEmitter.on(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
|
||||
EventEmitter.on('leave_team', this.handleLeaveTeam);
|
||||
EventEmitter.on(TYPING_VISIBLE, this.runTypingAnimations);
|
||||
|
||||
if (currentTeamId) {
|
||||
this.loadChannels(currentTeamId);
|
||||
|
|
@ -147,6 +151,23 @@ export default class ChannelBase extends PureComponent {
|
|||
componentWillUnmount() {
|
||||
EventEmitter.off(NavigationTypes.BLUR_POST_DRAFT, this.blurPostDraft);
|
||||
EventEmitter.off('leave_team', this.handleLeaveTeam);
|
||||
EventEmitter.off(TYPING_VISIBLE, this.runTypingAnimations);
|
||||
}
|
||||
|
||||
registerTypingAnimation = (animation) => {
|
||||
const length = this.typingAnimations.push(animation);
|
||||
const removeAnimation = () => {
|
||||
const animationIndex = length - 1;
|
||||
this.typingAnimations = this.typingAnimations.filter((a, index) => index !== animationIndex);
|
||||
};
|
||||
|
||||
return removeAnimation;
|
||||
}
|
||||
|
||||
runTypingAnimations = (typingVisible) => {
|
||||
Animated.parallel(
|
||||
this.typingAnimations.map((animation) => animation(typingVisible)),
|
||||
).start();
|
||||
}
|
||||
|
||||
blurPostDraft = () => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Preferences from '@mm-redux/constants/preferences';
|
|||
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import * as NavigationActions from 'app/actions/navigation';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
|
||||
import ChannelBase from './channel_base';
|
||||
|
||||
|
|
@ -79,4 +80,20 @@ describe('ChannelBase', () => {
|
|||
[componentIds[0], newThemeOptions],
|
||||
]);
|
||||
});
|
||||
|
||||
test('registerTypingAnimation should return a callback that removes the typing animation', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelBase {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
expect(instance.typingAnimations).toStrictEqual([]);
|
||||
|
||||
const removeAnimation = instance.registerTypingAnimation(emptyFunction);
|
||||
expect(instance.typingAnimations).toStrictEqual([emptyFunction]);
|
||||
|
||||
removeAnimation();
|
||||
expect(instance.typingAnimations).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ChannelPostList should match snapshot 1`] = `
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
Object {
|
||||
"paddingBottom": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(PostList)
|
||||
channelId="channel-id"
|
||||
extraData={false}
|
||||
indicateNewMessages={true}
|
||||
lastPostIndex={-1}
|
||||
loadMorePostsVisible={false}
|
||||
onLoadMoreUp={[Function]}
|
||||
onPostPress={[Function]}
|
||||
postIds={Array []}
|
||||
refreshing={false}
|
||||
renderFooter={[Function]}
|
||||
renderReplies={true}
|
||||
scrollViewNativeID="channel-id"
|
||||
showMoreMessagesButton={true}
|
||||
/>
|
||||
<Connect(AnnouncementBanner) />
|
||||
<Connect(RetryBarIndicator) />
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
`;
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Keyboard,
|
||||
Platform,
|
||||
View,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
|
||||
import {getLastPostIndex} from '@mm-redux/utils/post_list';
|
||||
|
|
@ -20,6 +21,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
|||
import telemetry from 'app/telemetry';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
import {TYPING_HEIGHT} from '@constants/post_draft';
|
||||
|
||||
let ChannelIntro = null;
|
||||
let LoadMorePosts = null;
|
||||
|
||||
|
|
@ -43,6 +46,7 @@ export default class ChannelPostList extends PureComponent {
|
|||
refreshing: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
updateNativeScrollView: PropTypes.func,
|
||||
registerTypingAnimation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -56,10 +60,13 @@ export default class ChannelPostList extends PureComponent {
|
|||
|
||||
this.isLoadingMoreBottom = false;
|
||||
this.isLoadingMoreTop = false;
|
||||
|
||||
this.bottomPadding = new Animated.Value(0);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on('goToThread', this.goToThread);
|
||||
this.removeTypingAnimation = this.props.registerTypingAnimation(this.bottomPaddingAnimation);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
|
@ -78,11 +85,24 @@ export default class ChannelPostList extends PureComponent {
|
|||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('goToThread', this.goToThread);
|
||||
this.removeTypingAnimation();
|
||||
}
|
||||
|
||||
bottomPaddingAnimation = (visible) => {
|
||||
const [padding, duration] = visible ?
|
||||
[TYPING_HEIGHT, 200] :
|
||||
[0, 400];
|
||||
|
||||
return Animated.timing(this.bottomPadding, {
|
||||
toValue: padding,
|
||||
duration,
|
||||
useNativeDriver: false,
|
||||
});
|
||||
}
|
||||
|
||||
goToThread = (post) => {
|
||||
telemetry.start(['post_list:thread']);
|
||||
const {actions, channelId} = this.props;
|
||||
const {actions, channelId, registerTypingAnimation} = this.props;
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
|
@ -94,6 +114,7 @@ export default class ChannelPostList extends PureComponent {
|
|||
const passProps = {
|
||||
channelId,
|
||||
rootId,
|
||||
registerTypingAnimation,
|
||||
};
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
|
|
@ -198,12 +219,12 @@ export default class ChannelPostList extends PureComponent {
|
|||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Animated.View style={[style.container, {paddingBottom: this.bottomPadding}]}>
|
||||
<View style={style.separator}/>
|
||||
{component}
|
||||
<AnnouncementBanner/>
|
||||
<RetryBarIndicator/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
describe('ChannelPostList', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
loadPostsIfNecessaryWithRetry: jest.fn(),
|
||||
loadThreadIfNecessary: jest.fn(),
|
||||
increasePostVisibility: jest.fn(),
|
||||
selectPost: jest.fn(),
|
||||
recordLoadTime: jest.fn(),
|
||||
refreshChannelWithRetry: jest.fn(),
|
||||
},
|
||||
channelId: 'channel-id',
|
||||
loadMorePostsVisible: false,
|
||||
refreshing: false,
|
||||
theme: Preferences.THEMES.default,
|
||||
registerTypingAnimation: jest.fn(() => {
|
||||
return jest.fn();
|
||||
}),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelPostList {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should add/remove typing animation on mount/unmount', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelPostList {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
expect(baseProps.registerTypingAnimation).toHaveBeenCalledTimes(1);
|
||||
expect(instance.removeTypingAnimation).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.unmount();
|
||||
expect(instance.removeTypingAnimation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -10,10 +10,6 @@ import {shallowWithIntl} from 'test/intl-test-helper';
|
|||
|
||||
import FlaggedPosts from './flagged_posts';
|
||||
|
||||
jest.mock('rn-placeholder', () => ({
|
||||
ImageContent: () => null,
|
||||
}));
|
||||
|
||||
describe('FlaggedPosts', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ import {shallowWithIntl} from 'test/intl-test-helper';
|
|||
|
||||
import PinnedPosts from './pinned_posts';
|
||||
|
||||
jest.mock('rn-placeholder', () => ({
|
||||
ImageContent: () => null,
|
||||
}));
|
||||
|
||||
describe('PinnedPosts', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ import {shallowWithIntl} from 'test/intl-test-helper';
|
|||
|
||||
import RecentMentions from './recent_mentions';
|
||||
|
||||
jest.mock('rn-placeholder', () => ({
|
||||
ImageContent: () => null,
|
||||
}));
|
||||
|
||||
describe('RecentMentions', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
|
|
|
|||
|
|
@ -16,29 +16,38 @@ exports[`thread should match snapshot, has root post 1`] = `
|
|||
/>
|
||||
<Connect(StatusBar) />
|
||||
<React.Fragment>
|
||||
<Connect(PostList)
|
||||
currentUserId="member_user_id"
|
||||
indicateNewMessages={false}
|
||||
lastPostIndex={2}
|
||||
lastViewedAt={0}
|
||||
location="thread"
|
||||
onPostPress={[Function]}
|
||||
postIds={
|
||||
Array [
|
||||
"root_id",
|
||||
"post_id_1",
|
||||
"post_id_2",
|
||||
]
|
||||
<ForwardRef(AnimatedComponentWrapper)
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"paddingBottom": 0,
|
||||
}
|
||||
}
|
||||
renderFooter={
|
||||
<Loading
|
||||
color="#3d3c40"
|
||||
size="large"
|
||||
style={Object {}}
|
||||
/>
|
||||
}
|
||||
scrollViewNativeID="threadPostList"
|
||||
/>
|
||||
>
|
||||
<Connect(PostList)
|
||||
currentUserId="member_user_id"
|
||||
indicateNewMessages={false}
|
||||
lastPostIndex={2}
|
||||
lastViewedAt={0}
|
||||
location="thread"
|
||||
onPostPress={[Function]}
|
||||
postIds={
|
||||
Array [
|
||||
"root_id",
|
||||
"post_id_1",
|
||||
"post_id_2",
|
||||
]
|
||||
}
|
||||
renderFooter={
|
||||
<Loading
|
||||
color="#3d3c40"
|
||||
size="large"
|
||||
style={Object {}}
|
||||
/>
|
||||
}
|
||||
scrollViewNativeID="threadPostList"
|
||||
/>
|
||||
</ForwardRef(AnimatedComponentWrapper)>
|
||||
<View
|
||||
nativeID="threadAccessoriesContainer"
|
||||
>
|
||||
|
|
@ -60,6 +69,21 @@ exports[`thread should match snapshot, has root post 1`] = `
|
|||
channelId="channel_id"
|
||||
channelIsArchived={false}
|
||||
cursorPositionEvent="onThreadTextBoxCursorChange"
|
||||
registerTypingAnimation={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
[Function],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": [MockFunction],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
rootId="root_id"
|
||||
valueEvent="onThreadTextBoxValueChange"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {Animated, View} from 'react-native';
|
||||
|
||||
import KeyboardLayout from '@components/layout/keyboard_layout';
|
||||
import Loading from '@components/loading';
|
||||
|
|
@ -24,28 +24,32 @@ export default class ThreadAndroid extends ThreadBase {
|
|||
rootId,
|
||||
channelIsArchived,
|
||||
theme,
|
||||
registerTypingAnimation,
|
||||
} = this.props;
|
||||
|
||||
let content;
|
||||
if (this.hasRootPost()) {
|
||||
content = (
|
||||
<>
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
lastPostIndex={-1}
|
||||
onPostPress={this.hideKeyboard}
|
||||
location={THREAD}
|
||||
/>
|
||||
<Animated.View style={{flex: 1, paddingBottom: this.bottomPadding}}>
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
lastPostIndex={-1}
|
||||
onPostPress={this.hideKeyboard}
|
||||
location={THREAD}
|
||||
/>
|
||||
</Animated.View>
|
||||
<PostDraft
|
||||
ref={this.postDraft}
|
||||
channelId={channelId}
|
||||
channelIsArchived={channelIsArchived}
|
||||
rootId={rootId}
|
||||
screenId={this.props.componentId}
|
||||
registerTypingAnimation={registerTypingAnimation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {Animated, View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from '@components/autocomplete';
|
||||
|
|
@ -37,6 +37,7 @@ export default class ThreadIOS extends ThreadBase {
|
|||
rootId,
|
||||
channelIsArchived,
|
||||
theme,
|
||||
registerTypingAnimation,
|
||||
} = this.props;
|
||||
|
||||
let content;
|
||||
|
|
@ -44,17 +45,19 @@ export default class ThreadIOS extends ThreadBase {
|
|||
if (this.hasRootPost()) {
|
||||
content = (
|
||||
<>
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
postIds={postIds}
|
||||
lastPostIndex={getLastPostIndex(postIds)}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
onPostPress={this.hideKeyboard}
|
||||
location={THREAD}
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
/>
|
||||
<Animated.View style={{flex: 1, paddingBottom: this.bottomPadding}}>
|
||||
<PostList
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
postIds={postIds}
|
||||
lastPostIndex={getLastPostIndex(postIds)}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
onPostPress={this.hideKeyboard}
|
||||
location={THREAD}
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
/>
|
||||
</Animated.View>
|
||||
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
|
||||
<Autocomplete
|
||||
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
|
||||
|
|
@ -80,6 +83,7 @@ export default class ThreadIOS extends ThreadBase {
|
|||
rootId={rootId}
|
||||
screenId={this.props.componentId}
|
||||
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
|
||||
registerTypingAnimation={registerTypingAnimation}
|
||||
/>
|
||||
</KeyboardTrackingView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ describe('thread', () => {
|
|||
postIds: ['root_id', 'post_id_1', 'post_id_2'],
|
||||
channelIsArchived: false,
|
||||
threadLoadingStatus: {status: RequestStatus.STARTED},
|
||||
registerTypingAnimation: jest.fn(() => {
|
||||
return jest.fn();
|
||||
}),
|
||||
};
|
||||
|
||||
test('should match snapshot, has root post', () => {
|
||||
|
|
@ -70,4 +73,18 @@ describe('thread', () => {
|
|||
wrapper.setProps({postIds: newPostIds});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should add/remove typing animation on mount/unmount', () => {
|
||||
const wrapper = shallow(
|
||||
<ThreadIOS {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
expect(baseProps.registerTypingAnimation).toHaveBeenCalledTimes(1);
|
||||
expect(instance.removeTypingAnimation).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.unmount();
|
||||
expect(instance.removeTypingAnimation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {Animated, Keyboard} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {General, RequestStatus} from '@mm-redux/constants';
|
||||
|
|
@ -11,6 +11,7 @@ import {General, RequestStatus} from '@mm-redux/constants';
|
|||
import Loading from 'app/components/loading';
|
||||
import DeletedPost from 'app/components/deleted_post';
|
||||
import {popTopScreen, mergeNavigationOptions} from 'app/actions/navigation';
|
||||
import {TYPING_HEIGHT} from '@constants/post_draft';
|
||||
|
||||
export default class ThreadBase extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -25,6 +26,7 @@ export default class ThreadBase extends PureComponent {
|
|||
rootId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
registerTypingAnimation: PropTypes.func.isRequired,
|
||||
channelIsArchived: PropTypes.bool,
|
||||
threadLoadingStatus: PropTypes.object,
|
||||
};
|
||||
|
|
@ -64,6 +66,12 @@ export default class ThreadBase extends PureComponent {
|
|||
this.state = {
|
||||
lastViewedAt: props.myMember && props.myMember.last_viewed_at,
|
||||
};
|
||||
|
||||
this.bottomPadding = new Animated.Value(0);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.removeTypingAnimation = this.props.registerTypingAnimation(this.bottomPaddingAnimation);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
|
@ -79,6 +87,7 @@ export default class ThreadBase extends PureComponent {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.props.actions.selectPost('');
|
||||
this.removeTypingAnimation();
|
||||
}
|
||||
|
||||
close = () => {
|
||||
|
|
@ -109,4 +118,16 @@ export default class ThreadBase extends PureComponent {
|
|||
|
||||
return null;
|
||||
};
|
||||
|
||||
bottomPaddingAnimation = (visible) => {
|
||||
const [padding, duration] = visible ?
|
||||
[TYPING_HEIGHT, 200] :
|
||||
[0, 400];
|
||||
|
||||
return Animated.timing(this.bottomPadding, {
|
||||
toValue: padding,
|
||||
duration,
|
||||
useNativeDriver: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue