Merge branch 'master' into mark-as-unread

This commit is contained in:
Harrison Healey 2019-09-25 09:06:21 -04:00
commit c017913911
34 changed files with 387 additions and 184 deletions

23
.circleci/config.yml Normal file
View file

@ -0,0 +1,23 @@
version: 2.1
jobs:
test:
working_directory: ~/mattermost-mobile
docker:
- image: circleci/node:10
steps:
- checkout
- run: |
echo assets/base/config.json
cat assets/base/config.json
# Avoid installing pods
touch .podinstall
# Run tests
make test || exit 1
workflows:
version: 2
pr-test:
jobs:
- test

View file

@ -123,7 +123,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 232
versionCode 233
versionName "1.24.0"
multiDexEnabled = true
ndk {

View file

@ -15,6 +15,8 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import Emoji from 'app/components/emoji';
import {BuiltInEmojis} from 'app/utils/emojis';
import {getEmojiByName} from 'app/utils/emoji_utils';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
@ -138,10 +140,16 @@ export default class EmojiSuggestion extends Component {
// We are going to set a double : on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
let completedDraft;
let prefix = ':';
if (Platform.OS === 'ios') {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `::${emoji}: `);
prefix = '::';
}
const emojiData = getEmojiByName(emoji);
if (emojiData?.filename && !BuiltInEmojis.includes(emojiData.filename)) {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, String.fromCodePoint(parseInt(emojiData.filename, 16)));
} else {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `${prefix}${emoji}: `);
}
if (value.length > cursorPosition) {
@ -150,7 +158,7 @@ export default class EmojiSuggestion extends Component {
onChangeText(completedDraft);
if (Platform.OS === 'ios') {
if (Platform.OS === 'ios' && (!emojiData?.filename || BuiltInEmojis.includes(emojiData?.filename))) {
// This is the second part of the hack were we replace the double : with just one
// after the auto correct vanished
setTimeout(() => {
@ -178,6 +186,7 @@ export default class EmojiSuggestion extends Component {
<View style={style.emoji}>
<Emoji
emojiName={item}
textStyle={style.emojiText}
size={20}
/>
</View>
@ -225,6 +234,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 13,
color: theme.centerChannelColor,
},
emojiText: {
color: '#000',
fontWeight: 'bold',
},
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,

View file

@ -38,6 +38,7 @@ export default class Emoji extends React.PureComponent {
literal: PropTypes.string,
size: PropTypes.number,
textStyle: CustomPropTypes.Style,
unicode: PropTypes.string,
};
static defaultProps = {
@ -116,6 +117,19 @@ export default class Emoji extends React.PureComponent {
// force a new image to be rendered when the size changes
const key = Platform.OS === 'android' ? (height + '-' + width) : null;
if (this.props.unicode && !this.props.imageUrl) {
const codeArray = this.props.unicode.split('-');
const code = codeArray.reduce((acc, c) => {
return acc + String.fromCodePoint(parseInt(c, 16));
}, '');
return (
<Text style={[this.props.textStyle, {fontSize: size}]}>
{code}
</Text>
);
}
if (!imageUrl) {
return (
<Image

View file

@ -9,7 +9,7 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {Client4} from 'mattermost-redux/client';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
import Emoji from './emoji';
@ -19,11 +19,15 @@ function mapStateToProps(state, ownProps) {
const customEmojis = getCustomEmojisByName(state);
let imageUrl = '';
let unicode;
let isCustomEmoji = false;
let displayTextOnly = false;
if (EmojiIndicesByAlias.has(emojiName)) {
if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) {
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
unicode = emoji.filename;
if (BuiltInEmojis.includes(emojiName)) {
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
}
} else if (customEmojis.has(emojiName)) {
const emoji = customEmojis.get(emojiName);
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
@ -33,7 +37,6 @@ function mapStateToProps(state, ownProps) {
config.EnableCustomEmoji !== 'true' ||
config.ExperimentalEnablePostMetadata === 'true' ||
getCurrentUserId(state) === '' ||
!isMinimumServerVersion(Client4.getServerVersion(), 4, 7) ||
isMinimumServerVersion(Client4.getServerVersion(), 5, 12);
}
@ -41,6 +44,7 @@ function mapStateToProps(state, ownProps) {
imageUrl,
isCustomEmoji,
displayTextOnly,
unicode,
};
}

View file

@ -75,7 +75,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
disableVirtualization={false}
getItemLayout={[Function]}
horizontal={false}
initialNumToRender={10}
initialNumToRender={50}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="always"
@ -86,7 +86,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
pageSize={30}
pageSize={50}
removeClippedSubviews={false}
renderItem={[Function]}
renderSectionHeader={[Function]}

View file

@ -2,61 +2,20 @@
// See LICENSE.txt for license information.
import React from 'react';
import {
FlatList,
SectionList,
View,
} from 'react-native';
import {View} from 'react-native';
import SearchBar from 'app/components/search_bar';
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
import EmojiPickerBase, {getStyleSheetFromTheme, SECTION_MARGIN} from './emoji_picker_base';
import EmojiPickerBase, {getStyleSheetFromTheme} from './emoji_picker_base';
export default class EmojiPicker extends EmojiPickerBase {
render() {
const {formatMessage} = this.context.intl;
const {deviceWidth, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const {theme} = this.props;
const {searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
keyboardShouldPersistTaps='always'
style={styles.flatList}
data={filteredEmojis}
keyExtractor={this.flatListKeyExtractor}
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
removeClippedSubviews={true}
/>
);
} else {
listComponent = (
<SectionList
ref={this.attachSectionList}
showsVerticalScrollIndicator={false}
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * 2)}]}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
keyboardShouldPersistTaps='always'
getItemLayout={this.sectionListGetItemLayout}
removeClippedSubviews={true}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
onMomentumScrollEnd={this.onMomentumScrollEnd}
pageSize={30}
ListFooterComponent={this.renderFooter}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={1}
/>
);
}
const searchBarInput = {
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
@ -87,7 +46,7 @@ export default class EmojiPicker extends EmojiPickerBase {
/>
</View>
<View style={styles.container}>
{listComponent}
{this.renderListComponent(2)}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>

View file

@ -3,9 +3,7 @@
import React from 'react';
import {
FlatList,
KeyboardAvoidingView,
SectionList,
View,
} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
@ -16,58 +14,17 @@ import SearchBar from 'app/components/search_bar';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
import EmojiPickerBase, {getStyleSheetFromTheme, SECTION_MARGIN} from './emoji_picker_base';
const SCROLLVIEW_NATIVE_ID = 'emojiPicker';
import EmojiPickerBase, {getStyleSheetFromTheme, SCROLLVIEW_NATIVE_ID} from './emoji_picker_base';
export default class EmojiPicker extends EmojiPickerBase {
render() {
const {formatMessage} = this.context.intl;
const {deviceWidth, isLandscape, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const {isLandscape, theme} = this.props;
const {searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2;
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
data={filteredEmojis}
initialListSize={10}
keyboardShouldPersistTaps='always'
keyExtractor={this.flatListKeyExtractor}
nativeID={SCROLLVIEW_NATIVE_ID}
pageSize={10}
renderItem={this.flatListRenderItem}
style={styles.flatList}
/>
);
} else {
listComponent = (
<SectionList
getItemLayout={this.sectionListGetItemLayout}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
ListFooterComponent={this.renderFooter}
nativeID={SCROLLVIEW_NATIVE_ID}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={0}
onMomentumScrollEnd={this.onMomentumScrollEnd}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
pageSize={30}
ref={this.attachSectionList}
removeClippedSubviews={false}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
sections={emojis}
showsVerticalScrollIndicator={false}
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
/>
);
}
let keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 50 : 30;
if (isLandscape) {
keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 10;
@ -111,7 +68,7 @@ export default class EmojiPicker extends EmojiPickerBase {
/>
</View>
<View style={[styles.container]}>
{listComponent}
{this.renderListComponent(shorten)}
{!searchTerm &&
<KeyboardTrackingView
ref={this.keyboardTracker}

View file

@ -6,7 +6,9 @@ import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
ActivityIndicator,
FlatList,
Platform,
SectionList,
Text,
TouchableOpacity,
View,
@ -28,10 +30,11 @@ import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone
import EmojiPickerRow from './emoji_picker_row';
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const EMOJI_GUTTER = 7;
const EMOJIS_PER_PAGE = 200;
const SECTION_HEADER_HEIGHT = 28;
export const SECTION_MARGIN = 15;
const SECTION_MARGIN = 15;
export const SCROLLVIEW_NATIVE_ID = 'emojiPicker';
export function filterEmojiSearchInput(searchText) {
return searchText.toLowerCase().replace(/^:|:$/g, '');
@ -69,7 +72,7 @@ export default class EmojiPicker extends PureComponent {
this.sectionListGetItemLayout = sectionListGetItemLayout({
getItemHeight: () => {
return EMOJI_SIZE + (EMOJI_GUTTER * 2);
return (EMOJI_SIZE + 5) + (EMOJI_GUTTER * 2);
},
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
});
@ -198,10 +201,6 @@ export default class EmojiPicker extends PureComponent {
});
};
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
};
searchEmojis = (searchTerm) => {
const {emojis, fuse} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
@ -232,6 +231,54 @@ export default class EmojiPicker extends PureComponent {
);
};
renderListComponent = (shorten) => {
const {deviceWidth, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
data={filteredEmojis}
initialListSize={10}
keyboardShouldPersistTaps='always'
keyExtractor={this.flatListKeyExtractor}
nativeID={SCROLLVIEW_NATIVE_ID}
pageSize={10}
renderItem={this.flatListRenderItem}
style={styles.flatList}
/>
);
} else {
listComponent = (
<SectionList
getItemLayout={this.sectionListGetItemLayout}
initialNumToRender={50}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
ListFooterComponent={this.renderFooter}
nativeID={SCROLLVIEW_NATIVE_ID}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
onMomentumScrollEnd={this.onMomentumScrollEnd}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
pageSize={50}
ref={this.attachSectionList}
removeClippedSubviews={false}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
sections={emojis}
showsVerticalScrollIndicator={false}
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
/>
);
}
return listComponent;
};
flatListKeyExtractor = (item) => item;
flatListRenderItem = ({item}) => {
@ -271,7 +318,7 @@ export default class EmojiPicker extends PureComponent {
}
this.props.actions.incrementEmojiPickerPage();
}
};
onScroll = (e) => {
if (this.state.jumpToSection) {
@ -324,7 +371,7 @@ export default class EmojiPicker extends PureComponent {
this.scrollToSection(index);
}, 200);
}
}
};
renderSectionHeader = ({section}) => {
const {theme} = this.props;
@ -351,7 +398,7 @@ export default class EmojiPicker extends PureComponent {
if (isCustomSection && this.props.customEmojiPage === 0) {
this.loadMoreCustomEmojis();
}
}
};
renderSectionIcons = () => {
const {theme} = this.props;
@ -393,7 +440,7 @@ export default class EmojiPicker extends PureComponent {
<ActivityIndicator/>
</View>
);
}
};
}
export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {

View file

@ -27,11 +27,12 @@ export default class EmojiPickerRow extends Component {
renderEmojis = (emoji, index, emojis) => {
const {emojiGutter, emojiSize} = this.props;
const size = emojiSize + 5;
const style = [
styles.emoji,
{
width: emojiSize,
height: emojiSize,
width: size,
height: size,
marginHorizontal: emojiGutter,
},
];
@ -60,6 +61,7 @@ export default class EmojiPickerRow extends Component {
>
<Emoji
emojiName={emoji.name}
textStyle={styles.emojiText}
size={emojiSize}
/>
</TouchableOpacity>
@ -79,7 +81,7 @@ export default class EmojiPickerRow extends Component {
const styles = StyleSheet.create({
columnStyle: {
alignSelf: 'stretch',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
},
@ -88,6 +90,10 @@ const styles = StyleSheet.create({
justifyContent: 'center',
overflow: 'hidden',
},
emojiText: {
color: '#000',
fontWeight: 'bold',
},
emojiLeft: {
marginLeft: 0,
},

View file

@ -12,7 +12,7 @@ import {getCustomEmojis, searchCustomEmojis} from 'mattermost-redux/actions/emoj
import {incrementEmojiPickerPage} from 'app/actions/views/emoji';
import {getDimensions, isLandscape} from 'app/selectors/device';
import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
import {BuiltInEmojis, CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
import {t} from 'app/utils/i18n';
import EmojiPicker from './emoji_picker';
@ -96,6 +96,11 @@ const getEmojisBySection = createSelector(
});
const customEmojiItems = [];
BuiltInEmojis.forEach((emoji) => {
customEmojiItems.push({
name: emoji,
});
});
for (const [key] of customEmojis) {
customEmojiItems.push({

View file

@ -32,7 +32,7 @@ import PostBody from './post_body';
const POST_TIMEOUT = 20000;
function makeMapStateToProps() {
export function makeMapStateToProps() {
const memoizeHasEmojisOnly = memoizeResult((message, customEmojis) => hasEmojisOnly(message, customEmojis));
const getReactionsForPost = makeGetReactionsForPost();
@ -61,9 +61,10 @@ function makeMapStateToProps() {
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
const channelIsArchived = channel?.delete_at !== 0; //eslint-disable-line camelcase
let canDelete = false;
if (post && !ownProps.channelIsArchived) {
if (post && !channelIsArchived) {
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
}

View file

@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import * as PostUtils from 'mattermost-redux/utils/post_utils';
import {makeMapStateToProps} from './index.js';
jest.mock('mattermost-redux/selectors/entities/channels', () => {
const channels = require.requireActual('mattermost-redux/selectors/entities/channels');
return {
...channels,
getChannel: jest.fn(),
canManageChannelMembers: jest.fn(),
getCurrentChannelId: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/preferences', () => {
const preferences = require.requireActual('mattermost-redux/selectors/entities/preferences');
return {
...preferences,
getTheme: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/general', () => {
const general = require.requireActual('mattermost-redux/selectors/entities/general');
return {
...general,
getConfig: jest.fn(),
getLicense: jest.fn().mockReturnValue({}),
};
});
jest.mock('mattermost-redux/selectors/entities/users', () => {
const users = require.requireActual('mattermost-redux/selectors/entities/users');
return {
...users,
getCurrentUserId: jest.fn(),
getCurrentUserRoles: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/teams', () => {
const teams = require.requireActual('mattermost-redux/selectors/entities/teams');
return {
...teams,
getCurrentTeamId: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/emojis', () => {
const emojis = require.requireActual('mattermost-redux/selectors/entities/emojis');
return {
...emojis,
getCustomEmojisByName: jest.fn(),
};
});
jest.mock('mattermost-redux/selectors/entities/posts', () => {
const posts = require.requireActual('mattermost-redux/selectors/entities/posts');
return {
...posts,
makeGetReactionsForPost: () => jest.fn(),
};
});
jest.mock('app/selectors/device', () => ({
getDimensions: jest.fn(),
}));
describe('makeMapStateToProps', () => {
const defaultState = {
entities: {
general: {
serverVersion: '',
},
},
};
const defaultOwnProps = {
post: {},
};
test('should not call canDeletePost if post is not defined', () => {
const canDeletePost = jest.spyOn(PostUtils, 'canDeletePost');
const mapStateToProps = makeMapStateToProps();
const ownProps = {
post: '',
};
const props = mapStateToProps(defaultState, ownProps);
expect(props.canDelete).toBe(false);
expect(canDeletePost).not.toHaveBeenCalled();
});
test('should not call canDeletePost if post is defined and channel is archived', () => {
const canDeletePost = jest.spyOn(PostUtils, 'canDeletePost');
const mapStateToProps = makeMapStateToProps();
getChannel.mockReturnValueOnce({delete_at: 1}); //eslint-disable-line camelcase
const props = mapStateToProps(defaultState, defaultOwnProps);
expect(props.canDelete).toBe(false);
expect(canDeletePost).not.toHaveBeenCalled();
});
test('should call canDeletePost if post is defined and channel is not archived', () => {
const canDeletePost = jest.spyOn(PostUtils, 'canDeletePost');
const mapStateToProps = makeMapStateToProps();
getChannel.mockReturnValue({delete_at: 0}); //eslint-disable-line camelcase
mapStateToProps(defaultState, defaultOwnProps);
expect(canDeletePost).toHaveBeenCalledTimes(1);
});
});

View file

@ -299,7 +299,7 @@ export default class PostList extends PureComponent {
scrollToBottom = () => {
setTimeout(() => {
if (this.flatListRef?.current) {
if (this.flatListRef.current) {
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
}
}, 250);
@ -316,7 +316,7 @@ export default class PostList extends PureComponent {
scrollToIndex = (index) => {
this.animationFrameInitialIndex = requestAnimationFrame(() => {
if (this.flatListRef?.current && index > 0 && index <= this.getItemCount()) {
if (this.flatListRef.current && index > 0 && index <= this.getItemCount()) {
this.flatListScrollToIndex(index);
}
});

View file

@ -64,7 +64,9 @@ describe('PostList', () => {
const indexInRange = baseProps.postIds.length;
const indexOutOfRange = [-1, indexInRange + 1];
instance.flatListRef = {};
instance.flatListRef = {
current: null,
};
instance.scrollToIndex(indexInRange);
expect(flatListScrollToIndex).not.toHaveBeenCalled();

View file

@ -7,11 +7,12 @@ import assert from 'assert';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from 'mattermost-redux/constants/preferences';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import Fade from 'app/components/fade';
import SendButton from 'app/components/send_button';
import PasteableTextInput from 'app/components/pasteable_text_input';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import EphemeralStore from 'app/store/ephemeral_store';
import PostTextbox from './post_textbox.ios';
@ -62,6 +63,7 @@ describe('PostTextBox', () => {
cursorPositionEvent: '',
valueEvent: '',
isLandscape: false,
screenId: 'NavigationScreen1',
};
test('should match, full snapshot', () => {
@ -343,6 +345,7 @@ describe('PostTextBox', () => {
test('should show error dialog if error occured', () => {
jest.spyOn(Alert, 'alert').mockReturnValue(null);
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
EphemeralStore.addNavigationComponentId('NavigationScreen1');
wrapper.find(PasteableTextInput).first().simulate('paste', {error: 'some error'}, []);
expect(Alert.alert).toHaveBeenCalled();
});
@ -350,6 +353,7 @@ describe('PostTextBox', () => {
test('should show file max warning and not uploading', () => {
jest.spyOn(EventEmitter, 'emit').mockReturnValue(null);
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
EphemeralStore.addNavigationComponentId('NavigationScreen1');
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
{
fileSize: 1000,
@ -414,6 +418,7 @@ describe('PostTextBox', () => {
test('should upload images', () => {
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
EphemeralStore.addNavigationComponentId('NavigationScreen1');
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
{
fileSize: 1000,
@ -424,6 +429,20 @@ describe('PostTextBox', () => {
]);
expect(baseProps.actions.initUploadFiles).toHaveBeenCalled();
});
test('should NOT upload images when not the top most screen', () => {
const wrapper = shallowWithIntl(<PostTextbox {...baseProps}/>);
EphemeralStore.addNavigationComponentId('NavigationScreen2');
wrapper.find(PasteableTextInput).first().simulate('paste', null, [
{
fileSize: 1000,
fileName: 'fileName.png',
type: 'images/png',
url: 'path/to/image',
},
]);
expect(baseProps.actions.initUploadFiles).not.toHaveBeenCalled();
});
});
});

View file

@ -24,11 +24,12 @@ import AttachmentButton from 'app/components/attachment_button';
import Fade from 'app/components/fade';
import FormattedMarkdownText from 'app/components/formatted_markdown_text';
import FormattedText from 'app/components/formatted_text';
import SendButton from 'app/components/send_button';
import PasteableTextInput from 'app/components/pasteable_text_input';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import SendButton from 'app/components/send_button';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_CONTENT_HEIGHT, MAX_FILE_COUNT} from 'app/constants/post_textbox';
import {NOTIFY_ALL_MEMBERS} from 'app/constants/view';
import EphemeralStore from 'app/store/ephemeral_store';
import {t} from 'app/utils/i18n';
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
import {
@ -36,7 +37,6 @@ import {
makeStyleSheetFromTheme,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
const {RNTextInputReset} = NativeModules;
@ -83,6 +83,7 @@ export default class PostTextBoxBase extends PureComponent {
isTimezoneEnabled: PropTypes.bool,
currentChannel: PropTypes.object,
isLandscape: PropTypes.bool.isRequired,
screenId: PropTypes.string.isRequired,
};
static defaultProps = {
@ -412,19 +413,19 @@ export default class PostTextBoxBase extends PureComponent {
this.props.actions.initUploadFiles(images, this.props.rootId);
};
isFileLoading() {
isFileLoading = () => {
const {files} = this.props;
return files.some((file) => file.loading);
}
};
isSendButtonVisible() {
isSendButtonVisible = () => {
return this.canSend() || this.isFileLoading();
}
};
isSendButtonEnabled() {
isSendButtonEnabled = () => {
return this.canSend() && !this.isFileLoading() && !this.state.sendingMessage;
}
};
sendMessage = () => {
const {value} = this.state;
@ -445,7 +446,7 @@ export default class PostTextBoxBase extends PureComponent {
textContainsAtAllAtChannel = (text) => {
const textWithoutCode = text.replace(/(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)| *(`{3,}|~{3,})[ .]*(\S+)? *\n([\s\S]*?\s*)\3 *(?:\n+|$)/g, '');
return (/\B@(all|channel)\b/i).test(textWithoutCode);
}
};
showSendToAllOrChannelAlert = (currentMembersCount) => {
const {intl} = this.context;
@ -505,7 +506,7 @@ export default class PostTextBoxBase extends PureComponent {
},
],
);
}
};
doSubmitMessage = () => {
const {actions, currentUserId, channelId, files, rootId} = this.props;
@ -562,7 +563,7 @@ export default class PostTextBoxBase extends PureComponent {
}
EventEmitter.emit('scroll-to-bottom');
}
};
getStatusFromSlashCommand = (message) => {
const tokens = message.split(' ');
@ -704,29 +705,31 @@ export default class PostTextBoxBase extends PureComponent {
},
]
);
}
};
handlePasteImages = (error, images) => {
if (error) {
this.showPasteImageErrorDialog();
return;
}
if (this.props.screenId === EphemeralStore.getNavigationTopComponentId()) {
if (error) {
this.showPasteImageErrorDialog();
return;
}
const {maxFileSize, files} = this.props;
const availableCount = MAX_FILE_COUNT - files.length;
if (images.length > availableCount) {
this.onShowFileMaxWarning();
return;
}
const {maxFileSize, files} = this.props;
const availableCount = MAX_FILE_COUNT - files.length;
if (images.length > availableCount) {
this.onShowFileMaxWarning();
return;
}
const largeImage = images.find((image) => image.fileSize > maxFileSize);
if (largeImage) {
this.onShowFileSizeWarning(largeImage.fileName);
return;
}
const largeImage = images.find((image) => image.fileSize > maxFileSize);
if (largeImage) {
this.onShowFileSizeWarning(largeImage.fileName);
return;
}
this.handleUploadFiles(images);
}
this.handleUploadFiles(images);
}
};
renderDeactivatedChannel = () => {
const {intl} = this.context;
@ -740,7 +743,7 @@ export default class PostTextBoxBase extends PureComponent {
})}
</Text>
);
}
};
renderTextBox = () => {
const {intl} = this.context;

View file

@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
Text,
TouchableOpacity,
} from 'react-native';
@ -46,6 +47,7 @@ export default class Reaction extends PureComponent {
<Emoji
emojiName={emojiName}
size={20}
textStyle={{color: 'black', fontWeight: 'bold'}}
padding={5}
/>
<Text style={styles.count}>{count}</Text>
@ -73,8 +75,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginRight: 6,
marginBottom: 5,
marginTop: 10,
paddingVertical: 2,
paddingHorizontal: 6,
...Platform.select({
android: {
paddingBottom: 2,
},
}),
},
};
});

View file

@ -108,12 +108,8 @@ function loadTranslation(locale) {
}
}
let momentLocale = DEFAULT_LOCALE;
function setMomentLocale(locale) {
if (momentLocale !== locale) {
momentLocale = moment.locale(locale);
}
export function resetMomentLocale() {
moment.locale(DEFAULT_LOCALE);
}
export function getTranslations(locale) {
@ -121,8 +117,6 @@ export function getTranslations(locale) {
loadTranslation(locale);
}
setMomentLocale(locale.toLowerCase());
return TRANSLATIONS[locale] || TRANSLATIONS[DEFAULT_LOCALE];
}

View file

@ -18,7 +18,7 @@ import {selectDefaultChannel} from 'app/actions/views/channel';
import {showOverlay} from 'app/actions/navigation';
import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root';
import {NavigationTypes, ViewTypes} from 'app/constants';
import {getTranslations} from 'app/i18n';
import {getTranslations, resetMomentLocale} from 'app/i18n';
import mattermostManaged from 'app/mattermost_managed';
import PushNotifications from 'app/push_notifications';
import {getCurrentLocale} from 'app/selectors/i18n';
@ -145,6 +145,7 @@ class GlobalEventHandler {
this.store.dispatch(setServerVersion(''));
deleteFileCache();
removeAppCredentials();
resetMomentLocale();
PushNotifications.clearNotifications();

View file

@ -6,6 +6,7 @@ import thunk from 'redux-thunk';
import intitialState from 'app/initial_state';
import PushNotification from 'app/push_notifications';
import * as I18n from 'app/i18n';
import GlobalEventHandler from './global_event_handler';
@ -35,11 +36,13 @@ GlobalEventHandler.store = store;
// TODO: Add Android test as part of https://mattermost.atlassian.net/browse/MM-17110
describe('GlobalEventHandler', () => {
it('should clear notifications on logout', async () => {
it('should clear notifications and reset moment locale on logout', async () => {
const clearNotifications = jest.spyOn(PushNotification, 'clearNotifications');
const resetMomentLocale = jest.spyOn(I18n, 'resetMomentLocale');
await GlobalEventHandler.onLogout();
expect(clearNotifications).toHaveBeenCalled();
expect(resetMomentLocale).toHaveBeenCalledWith();
});
it('should call onAppStateChange after configuration', () => {

View file

@ -37,6 +37,7 @@ export default class ChannelAndroid extends ChannelBase {
</View>
<PostTextbox
ref={this.postTextbox}
screenId={this.props.componentId}
/>
</KeyboardLayout>
<ChannelLoader

View file

@ -80,6 +80,7 @@ export default class ChannelIOS extends ChannelBase {
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
ref={this.postTextbox}
screenId={this.props.componentId}
/>
</KeyboardTrackingView>
</React.Fragment>

View file

@ -268,7 +268,7 @@ export default class ChannelInfo extends PureComponent {
defaultMessage: "We couldn't archive the channel {displayName}. Please check your connection and try again.",
},
{
displayName: channel.display_name,
displayName: channel.display_name.trim(),
}
);
if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
@ -290,7 +290,7 @@ export default class ChannelInfo extends PureComponent {
message,
{
term: term.toLowerCase(),
name: channel.display_name,
name: channel.display_name.trim(),
}
),
[{

View file

@ -60,9 +60,7 @@ function channelInfoRow(props) {
value={detail}
/>
);
}
if (rightArrow) {
} else if (rightArrow) {
actionElement = (
<Icon
name='angle-right'

View file

@ -44,10 +44,11 @@ export default class ThreadAndroid extends ThreadBase {
postTextBox = (
<PostTextbox
channelIsArchived={channelIsArchived}
rootId={rootId}
channelId={channelId}
channelIsArchived={channelIsArchived}
onCloseChannel={this.onCloseChannel}
rootId={rootId}
screenId={this.props.componentId}
/>
);
} else {

View file

@ -72,12 +72,13 @@ export default class ThreadIOS extends ThreadBase {
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
>
<PostTextbox
ref={this.postTextbox}
channelIsArchived={channelIsArchived}
rootId={rootId}
channelId={channelId}
onCloseChannel={this.onCloseChannel}
channelIsArchived={channelIsArchived}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
onCloseChannel={this.onCloseChannel}
ref={this.postTextbox}
rootId={rootId}
screenId={this.props.componentId}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
/>
</KeyboardTrackingView>

View file

@ -3,7 +3,7 @@
import emojiRegex from 'emoji-regex';
import {EmojiIndicesByAlias} from './emojis';
import {Emojis, EmojiIndicesByAlias} from './emojis';
const RE_NAMED_EMOJI = /(:([a-zA-Z0-9_-]+):)/g;
@ -105,3 +105,11 @@ export function doesMatchNamedEmoji(emojiName) {
return false;
}
export function getEmojiByName(emojiName) {
if (EmojiIndicesByAlias.has(emojiName)) {
return Emojis[EmojiIndicesByAlias.get(emojiName)];
}
return null;
}

File diff suppressed because one or more lines are too long

View file

@ -2836,7 +2836,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 232;
CURRENT_PROJECT_VERSION = 233;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@ -2897,7 +2897,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 232;
CURRENT_PROJECT_VERSION = 233;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View file

@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>232</string>
<string>233</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.24.0</string>
<key>CFBundleVersion</key>
<string>232</string>
<string>233</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

@ -19,6 +19,6 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>232</string>
<string>233</string>
</dict>
</plist>

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.24.0</string>
<key>CFBundleVersion</key>
<string>232</string>
<string>233</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>