[MM-22034] Tighten up post draft UI (#3898)

* Tighten up post draft UI

* Revert "MM-15307 Updated to use InteractionManager (#3666)"

This reverts commit e08155c81b.

* Address PR review comments

* Update snapshot test

* Don't return null if no files

* Fix progress text and padding issues

* Fixes per Matt's review

* Make linter happy
This commit is contained in:
Miguel Alatzar 2020-02-10 07:46:00 -07:00 committed by GitHub
parent 26df779330
commit abaed71f47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 139 additions and 164 deletions

View file

@ -1,5 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-confusing-arrow */
export const ConditionalWrapper = ({conditional, wrapper, children}) => conditional ? wrapper(children) : children;

View file

@ -168,12 +168,10 @@ export default class FileUploadItem extends PureComponent {
const realFill = Number(fill.toFixed(0));
return (
<View style={styles.progressContent}>
<View style={styles.progressCirclePercentage}>
<Text style={styles.progressText}>
{`${realFill}%`}
</Text>
</View>
<View>
<Text style={styles.progressText}>
{`${realFill}%`}
</Text>
</View>
);
};
@ -204,8 +202,8 @@ export default class FileUploadItem extends PureComponent {
<FileAttachmentIcon
file={file}
theme={theme}
wrapperHeight={60}
wrapperWidth={60}
wrapperHeight={53}
wrapperWidth={53}
/>
</View>
);
@ -227,13 +225,12 @@ export default class FileUploadItem extends PureComponent {
{file.loading && !file.failed &&
<View style={styles.progressCircleContent}>
<AnimatedCircularProgress
size={64}
size={36}
fill={progress}
width={4}
width={2}
backgroundColor='rgba(255, 255, 255, 0.5)'
tintColor='white'
rotation={0}
style={styles.progressCircle}
>
{this.renderProgress}
</AnimatedCircularProgress>
@ -254,51 +251,33 @@ export default class FileUploadItem extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
preview: {
paddingTop: 12,
paddingTop: 5,
marginLeft: 12,
},
previewContainer: {
height: 64,
width: 64,
elevation: 10,
height: 56,
width: 56,
borderRadius: 4,
},
progressCircle: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
width: '100%',
},
progressCircleContent: {
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.4)',
height: 64,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
height: 56,
width: 56,
justifyContent: 'center',
position: 'absolute',
width: 64,
},
progressCirclePercentage: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
progressContent: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
left: 0,
position: 'absolute',
width: '100%',
borderRadius: 4,
},
progressText: {
color: 'white',
fontSize: 18,
fontSize: 11,
fontWeight: 'bold',
},
filePreview: {
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 5,
borderRadius: 4,
borderWidth: 1,
width: 64,
height: 64,
width: 56,
height: 56,
},
}));

View file

@ -8,6 +8,7 @@ import {
ScrollView,
Text,
View,
Platform,
} from 'react-native';
import * as Animatable from 'react-native-animatable';
@ -18,9 +19,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
import FileUploadItem from './file_upload_item';
const initial = {opacity: 0, scale: 0};
const final = {opacity: 1, scale: 1};
const showFiles = {opacity: 1, height: 81};
const showFiles = {opacity: 1, height: 68};
const hideFiles = {opacity: 0, height: 0};
const hideError = {height: 0};
@ -93,14 +92,9 @@ export default class FileUploadPreview extends PureComponent {
handleFileMaxWarning = () => {
this.setState({showFileMaxWarning: true});
if (this.errorRef.current) {
this.makeErrorVisible(true, 20, null, () => {
this.errorRef.current.transition(initial, final, 350, 'ease-in');
});
this.makeErrorVisible(true, 20);
setTimeout(() => {
this.makeErrorVisible(false, 20, 350, () => {
this.errorRef.current.transition(final, initial, 350, 'ease-out');
this.clearErrorsFromState(400);
});
this.makeErrorVisible(false, 20);
}, 5000);
}
};
@ -108,29 +102,20 @@ export default class FileUploadPreview extends PureComponent {
handleFileSizeWarning = (message) => {
if (this.errorRef.current) {
if (message) {
this.setState({fileSizeWarning: message});
this.makeErrorVisible(true, 42, null, () => {
this.errorRef.current.transition(initial, final, 350, 'ease-in');
});
this.setState({fileSizeWarning: message.replace(': ', ':\n')});
this.makeErrorVisible(true, 40);
} else {
this.makeErrorVisible(false, 42, 350, () => {
this.errorRef.current.transition(final, initial, 350, 'ease-out');
this.clearErrorsFromState(400);
});
this.makeErrorVisible(false, 20);
}
}
};
makeErrorVisible = (visible, height, delay, callback) => {
makeErrorVisible = (visible, height) => {
if (this.errorContainerRef.current) {
if (visible) {
this.errorContainerRef.current.transition(hideError, {height}, 100);
setTimeout(callback, delay || 150);
this.errorContainerRef.current.transition(hideError, {height}, 200, 'ease-out');
} else {
callback();
setTimeout(() => {
this.errorContainerRef.current.transition({height}, hideError, 300);
}, delay || 150);
this.errorContainerRef.current.transition({height}, hideError, 200, 'ease-in');
}
}
}
@ -153,15 +138,18 @@ export default class FileUploadPreview extends PureComponent {
render() {
const {fileSizeWarning, showFileMaxWarning} = this.state;
const style = getStyleSheet(this.props.theme);
const {theme, files} = this.props;
const style = getStyleSheet(theme);
const fileContainerStyle = {
paddingBottom: files.length ? 5 : 0,
};
return (
<View style={style.previewContainer}>
<Animatable.View
style={style.fileContainer}
style={[style.fileContainer, fileContainerStyle]}
ref={this.containerRef}
isInteraction={true}
duration={300}
>
<ScrollView
horizontal={true}
@ -212,15 +200,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
display: 'flex',
flexDirection: 'row',
height: 0,
alignItems: 'center',
},
errorContainer: {
height: 0,
},
errorTextContainer: {
marginTop: 5,
marginTop: Platform.select({
ios: 4,
android: 2,
}),
marginHorizontal: 12,
opacity: 0,
flex: 1,
},
scrollView: {

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {View, Platform} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@ -37,7 +37,8 @@ export default class FileUploadRemove extends PureComponent {
<Icon
name='close-circle'
color={changeOpacity(theme.centerChannelColor, 0.64)}
size={18}
size={21}
style={style.removeIcon}
/>
</View>
</TouchableWithFeedback>
@ -50,17 +51,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
tappableContainer: {
position: 'absolute',
elevation: 11,
top: -2,
right: -16,
top: -5,
right: -10,
width: 32,
height: 32,
},
removeButton: {
borderRadius: 20,
borderRadius: 12,
alignSelf: 'center',
paddingTop: 6,
paddingHorizontal: 1,
marginTop: Platform.select({
ios: 5.4,
android: 4.75,
}),
backgroundColor: theme.centerChannelBg,
},
removeIcon: {
position: 'relative',
top: Platform.select({
ios: 1,
android: 0,
}),
},
};
});

View file

@ -29,7 +29,7 @@ export default class FileUploadRetry extends PureComponent {
>
<Icon
name='md-refresh'
size={50}
size={25}
color='#fff'
/>
</TouchableWithFeedback>
@ -45,5 +45,6 @@ const style = StyleSheet.create({
width: '100%',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
},
});

View file

@ -1,18 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PasteableTextInput should render pasteable text input 1`] = `
<ConditionalWrapper
conditional={true}
wrapper={[Function]}
<TextInput
allowFontScaling={true}
onPaste={[MockFunction]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
>
<TextInput
allowFontScaling={true}
onContentSizeChange={[Function]}
onPaste={[MockFunction]}
rejectResponderTermination={true}
underlineColorAndroid="transparent"
>
My Text
</TextInput>
</ConditionalWrapper>
My Text
</TextInput>
`;

View file

@ -3,10 +3,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Animated, Easing, NativeEventEmitter, NativeModules, Platform, TextInput} from 'react-native';
import {TextInput, NativeEventEmitter, NativeModules} from 'react-native';
import CustomTextInput from './custom_text_input';
import {ConditionalWrapper} from 'app/components/conditionalWrapper';
import {ViewTypes} from 'app/constants';
const {OnPasteEventManager} = NativeModules;
const OnPasteEventEmitter = new NativeEventEmitter(OnPasteEventManager);
@ -18,8 +16,6 @@ export class PasteableTextInput extends React.PureComponent {
forwardRef: PropTypes.any,
}
inputHeight = new Animated.Value(ViewTypes.INPUT_INITIAL_HEIGHT);
componentDidMount() {
this.subscription = OnPasteEventEmitter.addListener('onPaste', this.onPaste);
}
@ -35,52 +31,14 @@ export class PasteableTextInput extends React.PureComponent {
return onPaste?.(null, event);
}
animateHeight = (event) => {
if (Platform.OS === 'ios') {
const {height} = event.nativeEvent.contentSize;
const {style, value} = this.props;
const newHeight = Math.min(style.maxHeight, height + ViewTypes.INPUT_VERTICAL_PADDING);
if (value) {
this.inputHeight.setValue(newHeight);
} else {
requestAnimationFrame(() => {
Animated.timing(this.inputHeight, {
toValue: ViewTypes.INPUT_INITIAL_HEIGHT,
duration: 350,
delay: 100,
easing: Easing.inOut(Easing.sin),
}).start();
});
}
}
}
wrapperLayout = (children) => {
return (
<Animated.View
ref={this.containerRef}
style={{height: this.inputHeight}}
>
{children}
</Animated.View>
);
}
render() {
const {forwardRef, ...props} = this.props;
return (
<ConditionalWrapper
conditional={Platform.OS === 'ios'}
wrapper={this.wrapperLayout}
>
<CustomTextInput
{...props}
ref={forwardRef}
onContentSizeChange={this.animateHeight}
/>
</ConditionalWrapper>
<CustomTextInput
{...props}
ref={forwardRef}
/>
);
}
}

View file

@ -14,7 +14,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "center",
"paddingBottom": 8,
"paddingBottom": 2,
},
null,
]
@ -24,6 +24,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
contentContainerStyle={
Object {
"alignItems": "stretch",
"paddingTop": 7,
}
}
disableScrollViewPanResponder={true}
@ -50,6 +51,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
keyboardType="default"
multiline={true}
onChangeText={[Function]}
onContentSizeChange={null}
onEndEditing={[Function]}
onPaste={[Function]}
onSelectionChange={[Function]}
@ -58,13 +60,13 @@ exports[`PostTextBox should match, full snapshot 1`] = `
style={
Object {
"color": "#3d3c40",
"fontSize": 16,
"fontSize": 15,
"lineHeight": 20,
"maxHeight": 150,
"minHeight": 38,
"minHeight": 30,
"paddingBottom": 6,
"paddingHorizontal": 12,
"paddingTop": 12,
"paddingTop": 6,
}
}
underlineColorAndroid="transparent"
@ -82,6 +84,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
"display": "flex",
"flexDirection": "row",
"justifyContent": "space-between",
"paddingBottom": 1,
}
}
>

View file

@ -8,8 +8,6 @@ import {
AppState,
BackHandler,
findNodeHandle,
Image,
InteractionManager,
Keyboard,
NativeModules,
Platform,
@ -17,6 +15,7 @@ import {
TouchableOpacity,
ScrollView,
View,
Image,
} from 'react-native';
import {intlShape} from 'react-intl';
import RNFetchBlob from 'rn-fetch-blob';
@ -51,6 +50,8 @@ import {
} from 'app/utils/theme';
const {RNTextInputReset} = NativeModules;
const INPUT_LINE_HEIGHT = 20;
const EXTRA_INPUT_PADDING = 3;
export default class PostTextBoxBase extends PureComponent {
static propTypes = {
@ -122,6 +123,7 @@ export default class PostTextBoxBase extends PureComponent {
channelId: props.channelId,
channelTimezoneCount: 0,
longMessageAlertShown: false,
extraInputPadding: 0,
};
}
@ -651,10 +653,26 @@ export default class PostTextBoxBase extends PureComponent {
actions.handleClearFiles(channelId, rootId);
}
InteractionManager.runAfterInteractions(() => {
if (Platform.OS === 'ios') {
// On iOS, if the PostTextbox height increases from its
// initial height (due to a multiline post or a post whose
// message wraps, for example), then when the text is cleared
// the PostTextbox height decrease will be animated. This
// animation in conjunction with the PostList animation as it
// receives the newly created post is causing issues in the iOS
// PostList component as it fails to properly react to its content
// size changes. While a proper fix is determined for the PostList
// component, a small delay in triggering the height decrease
// animation gives the PostList enough time to first handle content
// size changes from the new post.
setTimeout(() => {
this.handleTextChange('');
this.setState({sendingMessage: false});
}, 250);
} else {
this.handleTextChange('');
this.setState({sendingMessage: false});
});
}
this.changeDraft('');
@ -838,6 +856,16 @@ export default class PostTextBoxBase extends PureComponent {
}
};
handleInputSizeChange = ({nativeEvent: {contentSize}}) => {
const {extraInputPadding} = this.state;
const numLines = contentSize.height / INPUT_LINE_HEIGHT;
if (numLines >= 2 && extraInputPadding !== EXTRA_INPUT_PADDING) {
this.setState({extraInputPadding: EXTRA_INPUT_PADDING});
} else if (numLines < 2 && extraInputPadding !== 0) {
this.setState({extraInputPadding: 0});
}
}
renderDeactivatedChannel = () => {
const {intl} = this.context;
const style = getStyleSheet(this.props.theme);
@ -861,7 +889,7 @@ export default class PostTextBoxBase extends PureComponent {
return this.archivedView(theme, style);
}
const {value} = this.state;
const {value, extraInputPadding} = this.state;
const textValue = channelIsLoading ? '' : value;
const placeholder = this.getPlaceHolder();
@ -871,6 +899,12 @@ export default class PostTextBoxBase extends PureComponent {
maxHeight = 88;
}
const inputStyle = {};
if (extraInputPadding) {
inputStyle.paddingBottom = style.input.paddingBottom + extraInputPadding;
inputStyle.paddingTop = style.input.paddingTop + extraInputPadding;
}
return (
<View
style={[style.inputWrapper, padding(isLandscape)]}
@ -890,6 +924,7 @@ export default class PostTextBoxBase extends PureComponent {
<PasteableTextInput
ref={this.input}
value={textValue}
style={{...style.input, ...inputStyle, maxHeight}}
onChangeText={this.handleTextChange}
onSelectionChange={this.handlePostDraftSelectionChanged}
placeholder={intl.formatMessage(placeholder, {channelDisplayName})}
@ -897,13 +932,13 @@ export default class PostTextBoxBase extends PureComponent {
multiline={true}
blurOnSubmit={false}
underlineColorAndroid='transparent'
style={{...style.input, maxHeight}}
keyboardType={this.state.keyboardType}
onEndEditing={this.handleEndEditing}
disableFullscreenUI={true}
editable={!channelIsReadOnly}
onPaste={this.handlePasteFiles}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onContentSizeChange={Platform.OS === 'android' ? this.handleInputSizeChange : null}
/>
{!channelIsReadOnly &&
<React.Fragment>
@ -947,6 +982,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: Platform.select({
ios: 1,
android: 2,
}),
},
slashIcon: {
width: ICON_SIZE,
@ -969,12 +1008,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
input: {
color: theme.centerChannelColor,
fontSize: 16,
lineHeight: 20,
fontSize: 15,
lineHeight: INPUT_LINE_HEIGHT,
paddingHorizontal: 12,
paddingTop: 12,
paddingBottom: 6,
minHeight: 38,
paddingTop: Platform.select({
ios: 6,
android: 8,
}),
paddingBottom: Platform.select({
ios: 6,
android: 2,
}),
minHeight: 30,
},
inputContainer: {
flex: 1,
@ -982,12 +1027,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
inputContentContainer: {
alignItems: 'stretch',
paddingTop: Platform.select({
ios: 7,
android: 0,
}),
},
inputWrapper: {
alignItems: 'flex-end',
flexDirection: 'row',
justifyContent: 'center',
paddingBottom: 8,
paddingBottom: 2,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20),

View file

@ -121,7 +121,4 @@ export default {
NotificationLevels,
SidebarSectionTypes,
IOS_HORIZONTAL_LANDSCAPE: 44,
INPUT_LINE_HEIGHT: 20,
INPUT_VERTICAL_PADDING: 18,
INPUT_INITIAL_HEIGHT: 38,
};