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
This commit is contained in:
ami9000 2020-02-26 15:51:53 -08:00 committed by GitHub
parent b46996a841
commit 30594cec5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 12 deletions

View file

@ -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}

View file

@ -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}
/>
</View>
</View>

View file

@ -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';
}

View file

@ -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);
});
}
});