MM-21368 Notify user when editing message that's too long (#4371)
* MM-21368 Notify user when editing message that's too long * Disable 'Save' button to prevent edits from being posted * Display error message specific to the message size and allows max, instead of the current, default "Invalid Message" error. * Split message length error into text (left justified) and counts (right justified) UX guidance from PR review * UX Review: Wrap for longer error text + spacing adjustment * UX Review: Align split messages to top + add spacing in-between * UX Review: Align error message box padding with content
This commit is contained in:
parent
6de0a30f70
commit
446f95ff6b
7 changed files with 95 additions and 45 deletions
|
|
@ -14,18 +14,17 @@ import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
|||
import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getChannelTimezones} from '@mm-redux/actions/channels';
|
||||
|
||||
import {executeCommand} from 'app/actions/views/command';
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {handleClearFiles, handleClearFailedFiles, initUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
import {getChannelMembersForDm} from 'app/selectors/channel';
|
||||
import {getAllowedServerMaxFileSize} from 'app/utils/file';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {executeCommand} from '@actions/views/command';
|
||||
import {addReactionToLatestPost} from '@actions/views/emoji';
|
||||
import {handleClearFiles, handleClearFailedFiles, initUploadFiles} from '@actions/views/file_upload';
|
||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from '@selectors/views';
|
||||
import {getChannelMembersForDm} from '@selectors/channel';
|
||||
import {getAllowedServerMaxFileSize} from '@utils/file';
|
||||
import {isLandscape} from '@selectors/device';
|
||||
|
||||
import PostDraft from './post_draft';
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
|
||||
export function mapStateToProps(state, ownProps) {
|
||||
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
|
||||
const config = getConfig(state);
|
||||
|
|
@ -81,7 +80,7 @@ export function mapStateToProps(state, ownProps) {
|
|||
files: currentDraft.files,
|
||||
isLandscape: isLandscape(state),
|
||||
isTimezoneEnabled,
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH,
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH_FALLBACK,
|
||||
maxFileSize: getAllowedServerMaxFileSize(config),
|
||||
membersCount,
|
||||
theme: getTheme(state),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const MAX_FILE_COUNT = 5;
|
||||
export const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
|
||||
export const INSERT_TO_DRAFT = 'insert_to_draft';
|
||||
export const INSERT_TO_COMMENT = 'insert_to_comment';
|
||||
export const ICON_SIZE = 24;
|
||||
export const ACCESSORIES_CONTAINER_NATIVE_ID = 'channelAccessoriesContainer';
|
||||
export const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
|
||||
export const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
|
||||
export const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
|
||||
export const ICON_SIZE = 24;
|
||||
export const INSERT_TO_COMMENT = 'insert_to_comment';
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ export default class EditPost extends PureComponent {
|
|||
closeButton: PropTypes.object,
|
||||
deviceHeight: PropTypes.number,
|
||||
deviceWidth: PropTypes.number,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
maxMessageLength: PropTypes.number,
|
||||
post: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
|
@ -57,10 +58,10 @@ export default class EditPost extends PureComponent {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: props.post.message,
|
||||
cursorPosition: 0,
|
||||
autocompleteVisible: false,
|
||||
cursorPosition: 0,
|
||||
keyboardType: 'default',
|
||||
message: props.post.message,
|
||||
};
|
||||
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
|
|
@ -128,6 +129,7 @@ export default class EditPost extends PureComponent {
|
|||
this.setState({
|
||||
editing: true,
|
||||
error: null,
|
||||
errorExtra: null,
|
||||
});
|
||||
|
||||
this.emitEditing(true);
|
||||
|
|
@ -144,6 +146,11 @@ export default class EditPost extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
isMessageTooLong = (message) => {
|
||||
const {maxMessageLength} = this.props;
|
||||
return (message && message.trim().length > maxMessageLength);
|
||||
}
|
||||
|
||||
onPostChangeText = (message) => {
|
||||
// Workaround to avoid iOS emdash autocorrect in Code Blocks
|
||||
if (Platform.OS === 'ios') {
|
||||
|
|
@ -153,8 +160,21 @@ export default class EditPost extends PureComponent {
|
|||
this.setState({message});
|
||||
}
|
||||
|
||||
const tooLong = this.isMessageTooLong(message);
|
||||
if (tooLong) {
|
||||
const errorLine = this.context.intl.formatMessage({
|
||||
id: 'mobile.message_length.message_split_left',
|
||||
defaultMessage: 'Message exceeds the character limit',
|
||||
});
|
||||
|
||||
const errorExtra = `${message.trim().length} / ${this.props.maxMessageLength}`;
|
||||
this.setState({error: errorLine, errorExtra});
|
||||
} else {
|
||||
this.setState({error: null, errorExtra: null});
|
||||
}
|
||||
|
||||
if (message) {
|
||||
this.emitCanEditPost(true);
|
||||
this.emitCanEditPost(!tooLong);
|
||||
} else {
|
||||
this.emitCanEditPost(false);
|
||||
}
|
||||
|
|
@ -181,7 +201,7 @@ export default class EditPost extends PureComponent {
|
|||
|
||||
render() {
|
||||
const {deviceHeight, deviceWidth, theme, isLandscape} = this.props;
|
||||
const {editing, message, error, autocompleteVisible} = this.state;
|
||||
const {editing, message, error, errorExtra, autocompleteVisible} = this.state;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
|
|
@ -194,15 +214,31 @@ export default class EditPost extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
let inputContainerStyle = style.inputContainer;
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<View style={[style.errorContainer, {width: deviceWidth}]}>
|
||||
<View style={style.errorWrapper}>
|
||||
<ErrorText error={error}/>
|
||||
inputContainerStyle = [inputContainerStyle, {marginTop: 0}];
|
||||
|
||||
if (errorExtra) {
|
||||
displayError = (
|
||||
<View style={[style.errorContainerSplit, {width: deviceWidth}]}>
|
||||
<ErrorText
|
||||
error={error}
|
||||
textStyle={style.errorWrap}
|
||||
/>
|
||||
<ErrorText error={errorExtra}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
displayError = (
|
||||
<View style={[style.errorContainer, {width: deviceWidth}]}>
|
||||
<View style={style.errorWrapper}>
|
||||
<ErrorText error={error}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const height = Platform.OS === 'android' ? (deviceHeight / 2) - 40 : (deviceHeight / 2);
|
||||
|
|
@ -216,7 +252,7 @@ export default class EditPost extends PureComponent {
|
|||
<StatusBar/>
|
||||
<View style={style.scrollView}>
|
||||
{displayError}
|
||||
<View style={[style.inputContainer, padding(isLandscape), {height}]}>
|
||||
<View style={[inputContainerStyle, padding(isLandscape), {height}]}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.messageRef}
|
||||
value={message}
|
||||
|
|
@ -262,9 +298,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
errorContainer: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
errorContainerSplit: {
|
||||
paddingHorizontal: 15,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
errorWrapper: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorWrap: {
|
||||
flexShrink: 1,
|
||||
paddingRight: 20,
|
||||
},
|
||||
inputContainer: {
|
||||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ describe('EditPost', () => {
|
|||
[false],
|
||||
]);
|
||||
expect(instance.setState.mock.calls).toEqual([
|
||||
[{editing: true, error: null}],
|
||||
[{editing: true, error: null, errorExtra: null}],
|
||||
[{editing: false, error}, instance.focus],
|
||||
]);
|
||||
});
|
||||
|
|
@ -89,8 +89,8 @@ describe('EditPost', () => {
|
|||
[false],
|
||||
]);
|
||||
expect(instance.setState.mock.calls).toEqual([
|
||||
[{editing: true, error: null}],
|
||||
[{editing: true, error: null, errorExtra: null}],
|
||||
[{editing: false}, instance.close],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,18 +5,23 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {editPost} from '@mm-redux/actions/posts';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {getDimensions, isLandscape} from 'app/selectors/device';
|
||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {getDimensions, isLandscape} from '@selectors/device';
|
||||
|
||||
import EditPost from './edit_post';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const config = getConfig(state);
|
||||
|
||||
return {
|
||||
...getDimensions(state),
|
||||
isLandscape: isLandscape(state),
|
||||
maxMessageLength: (config && parseInt(config.MaxPostSize || 0, 10)) || MAX_MESSAGE_LENGTH_FALLBACK,
|
||||
post: ownProps.post,
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@
|
|||
"mobile.markdown.link.copy_url": "Copy URL",
|
||||
"mobile.mention.copy_mention": "Copy Mention",
|
||||
"mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}",
|
||||
"mobile.message_length.message_split_left": "Message exceeds the character limit",
|
||||
"mobile.message_length.title": "Message Length",
|
||||
"mobile.more_dms.add_more": "You can add {remaining, number} more users",
|
||||
"mobile.more_dms.cannot_add_more": "You cannot add more users",
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ import {Client4} from '@mm-redux/client';
|
|||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getFormattedFileSize, lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import PaperPlane from 'app/components/post_draft/quick_actions/send_action/paper_plane';
|
||||
import {MAX_FILE_COUNT} from 'app/constants/post_draft';
|
||||
import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials';
|
||||
import Loading from '@components/loading';
|
||||
import PaperPlane from '@components/post_draft/quick_actions/send_action/paper_plane';
|
||||
import {MAX_FILE_COUNT, MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {getCurrentServerUrl, getAppCredentials} from '@init/credentials';
|
||||
import {getExtensionFromMime} from '@utils/file';
|
||||
import {setCSRFFromCookie} from '@utils/security';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {getExtensionFromMime} from 'app/utils/file';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import {
|
||||
ExcelSvg,
|
||||
|
|
@ -59,7 +59,6 @@ const extensionSvg = {
|
|||
};
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
const INPUT_HEIGHT = 150;
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
|
||||
export default class ExtensionPost extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -386,7 +385,7 @@ export default class ExtensionPost extends PureComponent {
|
|||
const {currentUserId} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
if (value.length > MAX_MESSAGE_LENGTH) {
|
||||
if (value.length > MAX_MESSAGE_LENGTH_FALLBACK) {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.share_extension.too_long_title',
|
||||
|
|
@ -397,7 +396,7 @@ export default class ExtensionPost extends PureComponent {
|
|||
defaultMessage: 'Character count: {count}/{max}',
|
||||
}, {
|
||||
count: value.length,
|
||||
max: MAX_MESSAGE_LENGTH,
|
||||
max: MAX_MESSAGE_LENGTH_FALLBACK,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
|
@ -588,7 +587,7 @@ export default class ExtensionPost extends PureComponent {
|
|||
|
||||
renderMessageLengthRemaining = () => {
|
||||
const {value} = this.state;
|
||||
const messageLengthRemaining = MAX_MESSAGE_LENGTH - value.length;
|
||||
const messageLengthRemaining = MAX_MESSAGE_LENGTH_FALLBACK - value.length;
|
||||
|
||||
if (value.length === 0) {
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue