From 446f95ff6b2033fc205acbfa884f72a502582cb7 Mon Sep 17 00:00:00 2001 From: Amit Uttam Date: Wed, 10 Jun 2020 13:01:40 -0300 Subject: [PATCH] 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 --- app/components/post_draft/index.js | 19 +++-- app/constants/post_draft.js | 13 ++-- app/screens/edit_post/edit_post.js | 69 +++++++++++++++---- app/screens/edit_post/edit_post.test.js | 6 +- app/screens/edit_post/index.js | 9 ++- assets/base/i18n/en.json | 1 + .../android/extension_post/extension_post.js | 23 +++---- 7 files changed, 95 insertions(+), 45 deletions(-) diff --git a/app/components/post_draft/index.js b/app/components/post_draft/index.js index 12ec408f9..aaea7d352 100644 --- a/app/components/post_draft/index.js +++ b/app/components/post_draft/index.js @@ -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), diff --git a/app/constants/post_draft.js b/app/constants/post_draft.js index 1394d3a83..b1e8cfb9b 100644 --- a/app/constants/post_draft.js +++ b/app/constants/post_draft.js @@ -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'; \ No newline at end of file +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; diff --git a/app/screens/edit_post/edit_post.js b/app/screens/edit_post/edit_post.js index e2bf78617..c857bb383 100644 --- a/app/screens/edit_post/edit_post.js +++ b/app/screens/edit_post/edit_post.js @@ -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 = ( - - - + inputContainerStyle = [inputContainerStyle, {marginTop: 0}]; + + if (errorExtra) { + displayError = ( + + + - - ); + ); + } else { + displayError = ( + + + + + + ); + } } const height = Platform.OS === 'android' ? (deviceHeight / 2) - 40 : (deviceHeight / 2); @@ -216,7 +252,7 @@ export default class EditPost extends PureComponent { {displayError} - + { errorContainer: { paddingHorizontal: 10, }, + errorContainerSplit: { + paddingHorizontal: 15, + flexDirection: 'row', + justifyContent: 'space-between', + }, errorWrapper: { alignItems: 'center', }, + errorWrap: { + flexShrink: 1, + paddingRight: 20, + }, inputContainer: { borderTopWidth: 1, borderBottomWidth: 1, diff --git a/app/screens/edit_post/edit_post.test.js b/app/screens/edit_post/edit_post.test.js index f4939e3fc..234b83106 100644 --- a/app/screens/edit_post/edit_post.test.js +++ b/app/screens/edit_post/edit_post.test.js @@ -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], ]); }); -}); \ No newline at end of file +}); diff --git a/app/screens/edit_post/index.js b/app/screens/edit_post/index.js index fde99b751..d36724da1 100644 --- a/app/screens/edit_post/index.js +++ b/app/screens/edit_post/index.js @@ -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), }; } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index a118231be..f25743d4a 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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", diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index 6fd62a307..92d4e34f9 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -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;