From 30594cec5d5c0a9471bb773ff5eb6474a9a76f01 Mon Sep 17 00:00:00 2001 From: ami9000 <20244930+ami9000@users.noreply.github.com> Date: Wed, 26 Feb 2020 15:51:53 -0800 Subject: [PATCH] MM-12148: Prevent emdash convert in a Code Block on ios (#3885) * Add fix to prevent emdash autocorrect in a Code Block on ios * Add fix to prevent emdash autocorrect in a Code Block on ios * code clean up * separate to new switchKeyboardForCodeBlocks function * added emdash prevention to edit_post and also code cleanup * moved keyboardType logic to its own utils function and added unit tests * clean up - align event argument names * removed unnecessary event check --- .../post_textbox/post_textbox_base.js | 29 ++++++-- app/screens/edit_post/edit_post.js | 30 ++++++-- app/utils/markdown.js | 21 ++++++ app/utils/markdown.test.js | 69 +++++++++++++++++++ 4 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 app/utils/markdown.test.js diff --git a/app/components/post_textbox/post_textbox_base.js b/app/components/post_textbox/post_textbox_base.js index 5793486f6..f15971796 100644 --- a/app/components/post_textbox/post_textbox_base.js +++ b/app/components/post_textbox/post_textbox_base.js @@ -43,6 +43,7 @@ import FileUploadPreview from 'app/components/file_upload_preview'; import EphemeralStore from 'app/store/ephemeral_store'; import {t} from 'app/utils/i18n'; import {confirmOutOfOfficeDisabled} from 'app/utils/status'; +import {switchKeyboardForCodeBlocks} from 'app/utils/markdown'; import { changeOpacity, makeStyleSheetFromTheme, @@ -409,17 +410,25 @@ export default class PostTextBoxBase extends PureComponent { } }; - handlePostDraftSelectionChanged = (event) => { - const cursorPosition = event.nativeEvent.selection.end; + handleOnSelectionChange = (event) => { + this.handlePostDraftSelectionChanged(event, false); + }; + + handlePostDraftSelectionChanged = (event, fromHandleTextChange) => { + const cursorPosition = fromHandleTextChange ? this.state.cursorPosition : event.nativeEvent.selection.end; + const {cursorPositionEvent} = this.props; if (cursorPositionEvent) { EventEmitter.emit(cursorPositionEvent, cursorPosition); } - this.setState({ - cursorPosition, - }); + if (Platform.OS === 'ios') { + const keyboardType = switchKeyboardForCodeBlocks(this.state.value, cursorPosition); + this.setState({cursorPosition, keyboardType}); + } else { + this.setState({cursorPosition}); + } }; handleSendMessage = () => { @@ -517,7 +526,13 @@ export default class PostTextBoxBase extends PureComponent { this.checkMessageLength(value); - this.setState(nextState); + // Workaround to avoid iOS emdash autocorrect in Code Blocks + if (Platform.OS === 'ios') { + const callback = () => this.handlePostDraftSelectionChanged(null, true); + this.setState(nextState, callback); + } else { + this.setState(nextState); + } if (value) { actions.userTyping(channelId, rootId); @@ -926,7 +941,7 @@ export default class PostTextBoxBase extends PureComponent { value={textValue} style={{...style.input, ...inputStyle, maxHeight}} onChangeText={this.handleTextChange} - onSelectionChange={this.handlePostDraftSelectionChanged} + onSelectionChange={this.handleOnSelectionChange} placeholder={intl.formatMessage(placeholder, {channelDisplayName})} placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)} multiline={true} diff --git a/app/screens/edit_post/edit_post.js b/app/screens/edit_post/edit_post.js index f097df86b..84c36cfa6 100644 --- a/app/screens/edit_post/edit_post.js +++ b/app/screens/edit_post/edit_post.js @@ -18,6 +18,7 @@ import Loading from 'app/components/loading'; import StatusBar from 'app/components/status_bar'; import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; +import {switchKeyboardForCodeBlocks} from 'app/utils/markdown'; import { changeOpacity, makeStyleSheetFromTheme, @@ -62,6 +63,7 @@ export default class EditPost extends PureComponent { message: props.post.message, cursorPosition: 0, autocompleteVisible: false, + keyboardType: 'default', }; this.rightButton.color = props.theme.sidebarHeaderTextColor; @@ -151,7 +153,14 @@ export default class EditPost extends PureComponent { }; onPostChangeText = (message) => { - this.setState({message}); + // Workaround to avoid iOS emdash autocorrect in Code Blocks + if (Platform.OS === 'ios') { + const callback = () => this.onPostSelectionChange(null, true); + this.setState({message}, callback); + } else { + this.setState({message}); + } + if (message) { this.emitCanEditPost(true); } else { @@ -159,9 +168,19 @@ export default class EditPost extends PureComponent { } }; - onPostSelectionChange = (event) => { - const cursorPosition = event.nativeEvent.selection.end; - this.setState({cursorPosition}); + handleOnSelectionChange = (event) => { + this.onPostSelectionChange(event, false); + }; + + onPostSelectionChange = (event, fromOnPostChangeText) => { + const cursorPosition = fromOnPostChangeText ? this.state.cursorPosition : event.nativeEvent.selection.end; + + if (Platform.OS === 'ios') { + const keyboardType = switchKeyboardForCodeBlocks(this.state.message, cursorPosition); + this.setState({cursorPosition, keyboardType}); + } else { + this.setState({cursorPosition}); + } }; onAutocompleteVisible = (autocompleteVisible) => { @@ -220,7 +239,8 @@ export default class EditPost extends PureComponent { underlineColorAndroid='transparent' disableFullscreenUI={true} keyboardAppearance={getKeyboardAppearanceFromTheme(this.props.theme)} - onSelectionChange={this.onPostSelectionChange} + onSelectionChange={this.handleOnSelectionChange} + keyboardType={this.state.keyboardType} /> diff --git a/app/utils/markdown.js b/app/utils/markdown.js index 3a0110dc6..9d5a80333 100644 --- a/app/utils/markdown.js +++ b/app/utils/markdown.js @@ -182,3 +182,24 @@ export function getDisplayNameForLanguage(language) { export function escapeRegex(text) { return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); } + +export function switchKeyboardForCodeBlocks(value, cursorPosition) { + const regexForCodeBlock = /^```$(.*?)^```$|^```$(.*)/gms; + + const matches = []; + let nextMatch; + while ((nextMatch = regexForCodeBlock.exec(value)) !== null) { + matches.push({ + startOfMatch: regexForCodeBlock.lastIndex - nextMatch[0].length, + endOfMatch: regexForCodeBlock.lastIndex + 1, + }); + } + + const cursorIsInsideCodeBlock = matches.some((match) => cursorPosition >= match.startOfMatch && cursorPosition <= match.endOfMatch); + + // 'email-address' keyboardType prevents iOS emdash autocorrect + if (cursorIsInsideCodeBlock) { + return 'email-address'; + } + return 'default'; +} diff --git a/app/utils/markdown.test.js b/app/utils/markdown.test.js new file mode 100644 index 000000000..079547549 --- /dev/null +++ b/app/utils/markdown.test.js @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {switchKeyboardForCodeBlocks} from './markdown'; + +describe('switchKeyboardForCodeBlocks', () => { + const testCases = [{ + name: 'Empty text input', + value: '', + cursorPosition: 0, + expected: 'default', + }, { + name: 'Cursor within an open Code Block', + value: '```', + cursorPosition: 3, + expected: 'email-address', + }, { + name: 'Cursor within a closed Code Block', + value: '```\ntest\n```', + cursorPosition: 4, + expected: 'email-address', + }, { + name: 'Cursor outside AFTER a closed Code Block', + value: '```\ntest\n```\ntest', + cursorPosition: 14, + expected: 'default', + }, { + name: 'Cursor outside BEFORE a closed Code Block', + value: 'test\n```\ntest\n```', + cursorPosition: 4, + expected: 'default', + }, { + name: 'Cursor outside BEFORE an open Code Block', + value: 'test\n```', + cursorPosition: 4, + expected: 'default', + }, { + name: 'Cursor outside between two Code Blocks', + value: '```\ntest\n```\ntest\n```\ntest', + cursorPosition: 14, + expected: 'default', + }, { + name: 'Opening triple backtick not on its own line - text after backticks', + value: '```test', + cursorPosition: 3, + expected: 'default', + }, { + name: 'Opening triple backtick not on its own line - text before backticks', + value: 'test```', + cursorPosition: 7, + expected: 'default', + }, { + name: 'Closing triple backtick not on its own line - text after backticks', + value: '```\ntest\n```test', + cursorPosition: 12, + expected: 'email-address', + }, { + name: 'Closing triple backtick not on its own line - text before backticks', + value: '```\ntest\ntest```', + cursorPosition: 16, + expected: 'email-address', + }]; + + for (const testCase of testCases) { + it(`test - ${testCase.name}`, () => { + expect(switchKeyboardForCodeBlocks(testCase.value, testCase.cursorPosition)).toEqual(testCase.expected); + }); + } +});