diff --git a/app/components/channel_loader/__snapshots__/channel_loader.test.js.snap b/app/components/channel_loader/__snapshots__/channel_loader.test.js.snap
index 37bf39c57..8f1d0b9f8 100644
--- a/app/components/channel_loader/__snapshots__/channel_loader.test.js.snap
+++ b/app/components/channel_loader/__snapshots__/channel_loader.test.js.snap
@@ -33,7 +33,7 @@ exports[`ChannelLoader should match snapshot 1`] = `
]
}
>
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
`;
diff --git a/app/components/channel_loader/channel_loader.test.js b/app/components/channel_loader/channel_loader.test.js
index df58d8773..327243c33 100644
--- a/app/components/channel_loader/channel_loader.test.js
+++ b/app/components/channel_loader/channel_loader.test.js
@@ -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,
diff --git a/app/components/post_draft/post_draft.js b/app/components/post_draft/post_draft.js
index 66d43cfe5..b00e7791e 100644
--- a/app/components/post_draft/post_draft.js
+++ b/app/components/post_draft/post_draft.js
@@ -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 (
<>
-
+
{Platform.OS === 'android' &&
{
- 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 (
-
-
+
- {this.renderTyping()}
-
-
+
+ {this.renderTyping()}
+
+
+
);
}
}
@@ -101,6 +116,7 @@ export default class Typing extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
typing: {
+ position: 'absolute',
paddingLeft: 10,
paddingTop: 3,
fontSize: 11,
diff --git a/app/components/post_draft/typing/typing.test.js b/app/components/post_draft/typing/typing.test.js
index 169b89e23..4c6a0bcfb 100644
--- a/app/components/post_draft/typing/typing.test.js
+++ b/app/components/post_draft/typing/typing.test.js
@@ -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(
,
);
- 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(
+ ,
+ );
+
+ 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(
,
);
- 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(
- ,
+ ,
);
- 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(
+ ,
+ );
+ const instance = wrapper.instance();
+
+ expect(baseProps.registerTypingAnimation).toHaveBeenCalledTimes(1);
+ expect(instance.removeTypingAnimation).not.toHaveBeenCalled();
+
+ wrapper.unmount();
+ expect(instance.removeTypingAnimation).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/constants/post_draft.js b/app/constants/post_draft.js
index b1e8cfb9b..e8ecfc948 100644
--- a/app/constants/post_draft.js
+++ b/app/constants/post_draft.js
@@ -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;
diff --git a/app/screens/channel/channel.android.js b/app/screens/channel/channel.android.js
index 7eccd0eb8..9f27850f5 100644
--- a/app/screens/channel/channel.android.js
+++ b/app/screens/channel/channel.android.js
@@ -67,11 +67,12 @@ export default class ChannelAndroid extends ChannelBase {
component = (
-
+
);
diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js
index 04f1c280a..81d325206 100644
--- a/app/screens/channel/channel.ios.js
+++ b/app/screens/channel/channel.ios.js
@@ -63,6 +63,7 @@ export default class ChannelIOS extends ChannelBase {
<>
}
diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js
index 4593f0684..62b814a67 100644
--- a/app/screens/channel/channel_base.js
+++ b/app/screens/channel/channel_base.js
@@ -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 = () => {
diff --git a/app/screens/channel/channel_base.test.js b/app/screens/channel/channel_base.test.js
index a1787068f..f6feb8e7e 100644
--- a/app/screens/channel/channel_base.test.js
+++ b/app/screens/channel/channel_base.test.js
@@ -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(
+ ,
+ {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([]);
+ });
});
diff --git a/app/screens/channel/channel_post_list/__snapshots__/channel_post_list.test.js.snap b/app/screens/channel/channel_post_list/__snapshots__/channel_post_list.test.js.snap
new file mode 100644
index 000000000..db33616c3
--- /dev/null
+++ b/app/screens/channel/channel_post_list/__snapshots__/channel_post_list.test.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ChannelPostList should match snapshot 1`] = `
+
+
+
+
+
+
+`;
diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js
index 813f41bfa..2e6782ee0 100644
--- a/app/screens/channel/channel_post_list/channel_post_list.js
+++ b/app/screens/channel/channel_post_list/channel_post_list.js
@@ -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 (
-
+
{component}
-
+
);
}
}
diff --git a/app/screens/channel/channel_post_list/channel_post_list.test.js b/app/screens/channel/channel_post_list/channel_post_list.test.js
new file mode 100644
index 000000000..44d488824
--- /dev/null
+++ b/app/screens/channel/channel_post_list/channel_post_list.test.js
@@ -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(
+ ,
+ );
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should add/remove typing animation on mount/unmount', () => {
+ const wrapper = shallow(
+ ,
+ );
+ const instance = wrapper.instance();
+
+ expect(baseProps.registerTypingAnimation).toHaveBeenCalledTimes(1);
+ expect(instance.removeTypingAnimation).not.toHaveBeenCalled();
+
+ wrapper.unmount();
+ expect(instance.removeTypingAnimation).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/app/screens/flagged_posts/flagged_posts.test.js b/app/screens/flagged_posts/flagged_posts.test.js
index 1457816eb..39d7bc0aa 100644
--- a/app/screens/flagged_posts/flagged_posts.test.js
+++ b/app/screens/flagged_posts/flagged_posts.test.js
@@ -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: {
diff --git a/app/screens/pinned_posts/pinned_posts.test.js b/app/screens/pinned_posts/pinned_posts.test.js
index b2038f4e9..d61ca24ce 100644
--- a/app/screens/pinned_posts/pinned_posts.test.js
+++ b/app/screens/pinned_posts/pinned_posts.test.js
@@ -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: {
diff --git a/app/screens/recent_mentions/recent_mentions.test.js b/app/screens/recent_mentions/recent_mentions.test.js
index 1724a9610..cf82df5b7 100644
--- a/app/screens/recent_mentions/recent_mentions.test.js
+++ b/app/screens/recent_mentions/recent_mentions.test.js
@@ -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: {
diff --git a/app/screens/thread/__snapshots__/thread.ios.test.js.snap b/app/screens/thread/__snapshots__/thread.ios.test.js.snap
index d0f70c3f1..54bc86d92 100644
--- a/app/screens/thread/__snapshots__/thread.ios.test.js.snap
+++ b/app/screens/thread/__snapshots__/thread.ios.test.js.snap
@@ -16,29 +16,38 @@ exports[`thread should match snapshot, has root post 1`] = `
/>
-
- }
- scrollViewNativeID="threadPostList"
- />
+ >
+
+ }
+ scrollViewNativeID="threadPostList"
+ />
+
@@ -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"
/>
diff --git a/app/screens/thread/thread.android.js b/app/screens/thread/thread.android.js
index bb0611af2..fbea95765 100644
--- a/app/screens/thread/thread.android.js
+++ b/app/screens/thread/thread.android.js
@@ -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 = (
<>
-
+
+
+
>
);
diff --git a/app/screens/thread/thread.ios.js b/app/screens/thread/thread.ios.js
index 6d7ad2ec1..61124d741 100644
--- a/app/screens/thread/thread.ios.js
+++ b/app/screens/thread/thread.ios.js
@@ -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 = (
<>
-
+
+
+
);
diff --git a/app/screens/thread/thread.ios.test.js b/app/screens/thread/thread.ios.test.js
index d2503ee78..5ca2440bd 100644
--- a/app/screens/thread/thread.ios.test.js
+++ b/app/screens/thread/thread.ios.test.js
@@ -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(
+ ,
+ {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);
+ });
});
diff --git a/app/screens/thread/thread_base.js b/app/screens/thread/thread_base.js
index 5d2b67bb9..c79b48c17 100644
--- a/app/screens/thread/thread_base.js
+++ b/app/screens/thread/thread_base.js
@@ -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,
+ });
+ }
}