Merge pull request #2076 from mattermost/release-1.12

Merge Release 1.12 into master
This commit is contained in:
Elias Nahum 2018-09-05 10:04:26 -03:00 committed by GitHub
commit 74a45fd931
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2008 additions and 282 deletions

View file

@ -113,7 +113,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 26
versionCode 135
versionCode 136
versionName "1.12.0"
multiDexEnabled = true
ndk {

View file

@ -14,13 +14,6 @@ import {
import CustomPropTypes from 'app/constants/custom_prop_types';
import ImageCacheManager from 'app/utils/image_cache_manager';
const scaleEmojiBasedOnDevice = (size) => {
if (Platform.OS === 'ios') {
return size * 1.1; // slightly larger emojis look better on ios
}
return size * PixelRatio.get();
};
export default class Emoji extends React.PureComponent {
static propTypes = {
@ -60,30 +53,28 @@ export default class Emoji extends React.PureComponent {
this.state = {
imageUrl: null,
originalWidth: 0,
originalHeight: 0,
};
}
componentWillMount() {
const {displayTextOnly, imageUrl} = this.props;
this.mounted = true;
if (!this.props.displayTextOnly && this.props.imageUrl) {
ImageCacheManager.cache(this.props.imageUrl, this.props.imageUrl, this.updateImageHeight);
if (!displayTextOnly && imageUrl) {
ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.emojiName !== this.props.emojiName) {
const {displayTextOnly, emojiName, imageUrl} = nextProps;
if (emojiName !== this.props.emojiName) {
this.setState({
imageUrl: null,
originalWidth: 0,
originalHeight: 0,
});
}
if (!nextProps.displayTextOnly && nextProps.imageUrl &&
nextProps.imageUrl !== this.props.imageUrl) {
ImageCacheManager.cache(nextProps.imageUrl, nextProps.imageUrl, this.updateImageHeight);
if (!displayTextOnly && imageUrl &&
imageUrl !== this.props.imageUrl) {
ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl);
}
}
@ -91,21 +82,15 @@ export default class Emoji extends React.PureComponent {
this.mounted = false;
}
updateImageHeight = (imageUrl) => {
setImageUrl = (imageUrl) => {
let prefix = '';
if (Platform.OS === 'android') {
prefix = 'file://';
}
const uri = `${prefix}${imageUrl}`;
Image.getSize(uri, (originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
imageUrl: uri,
originalWidth,
originalHeight,
});
}
this.setState({
imageUrl: uri,
});
};
@ -122,26 +107,15 @@ export default class Emoji extends React.PureComponent {
if (!size && textStyle) {
const flatten = StyleSheet.flatten(textStyle);
fontSize = flatten.fontSize;
size = scaleEmojiBasedOnDevice(fontSize);
size = fontSize * (Platform.OS === 'android' ? PixelRatio.get() : 1);
}
if (displayTextOnly) {
return <Text style={textStyle}>{literal}</Text>;
}
let width = size;
let height = size;
let {originalHeight, originalWidth} = this.state;
originalHeight = scaleEmojiBasedOnDevice(originalHeight);
originalWidth = scaleEmojiBasedOnDevice(originalWidth);
if (originalHeight && originalWidth) {
if (originalWidth > originalHeight) {
height = (size * originalHeight) / originalWidth;
} else if (originalWidth < originalHeight) {
// This may cause text to reflow, but its impossible to add a horizontal margin
width = (size * originalWidth) / originalHeight;
}
}
const width = size;
const height = size;
let marginTop = 0;
if (textStyle) {

View file

@ -407,6 +407,7 @@ export default class EmojiPicker extends PureComponent {
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
removeClippedSubviews={true}
/>
);
} else {
@ -558,6 +559,7 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
overflow: 'hidden',
},
listView: {
backgroundColor: theme.centerChannelBg,

View file

@ -64,7 +64,7 @@ export default class EmojiPickerRow extends Component {
/>
</TouchableOpacity>
);
}
};
render() {
const {emojiGutter, items} = this.props;
@ -86,6 +86,7 @@ const styles = StyleSheet.create({
emoji: {
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
emojiLeft: {
marginLeft: 0,

View file

@ -14,6 +14,19 @@ export default class FailedNetworkAction extends PureComponent {
static propTypes = {
onRetry: PropTypes.func,
theme: PropTypes.object.isRequired,
errorTitle: PropTypes.object,
errorDescription: PropTypes.object,
};
static defaultProps = {
errorTitle: {
id: 'mobile.failed_network_action.title',
defaultMessage: 'No internet connection',
},
errorDescription: {
id: 'mobile.failed_network_action.description',
defaultMessage: 'There seems to be a problem with your internet connection. Make sure you have an active connection and try again.',
},
};
render() {
@ -28,13 +41,13 @@ export default class FailedNetworkAction extends PureComponent {
width={76}
/>
<FormattedText
id='mobile.failed_network_action.title'
defaultMessage='No internet connection'
id={this.props.errorTitle.id}
defaultMessage={this.props.errorTitle.defaultMessage}
style={style.title}
/>
<FormattedText
id='mobile.failed_network_action.description'
defaultMessage='There seems to be a problem with your internet connection. Make sure you have an active connection and try again.'
id={this.props.errorDescription.id}
defaultMessage={this.props.errorDescription.message}
style={style.description}
/>
{onRetry &&

View file

@ -13,9 +13,10 @@ import {
View,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import ProgressiveImage from 'app/components/progressive_image';
import ShowMoreButton from 'app/components/show_more_button';
import CustomPropTypes from 'app/constants/custom_prop_types';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
@ -103,10 +104,10 @@ export default class MessageAttachment extends PureComponent {
const {height} = event.nativeEvent.layout;
const {height: deviceHeight} = Dimensions.get('window');
if (height >= (deviceHeight * 1.2)) {
if (height >= (deviceHeight * 0.6)) {
this.setState({
isLongText: true,
maxHeight: (deviceHeight * 0.6),
maxHeight: (deviceHeight * 0.4),
});
}
};
@ -385,23 +386,6 @@ export default class MessageAttachment extends PureComponent {
let text;
if (attachment.text) {
let moreLessLocale = {id: 'post_attachment.collapse', defaultMessage: 'Show less...'};
if (collapsed) {
moreLessLocale = {id: 'post_attachment.more', defaultMessage: 'Show more...'};
}
let moreLess;
if (isLongText) {
moreLess = (
<FormattedText
id={moreLessLocale.id}
defaultMessage={moreLessLocale.defaultMessage}
onPress={this.toggleCollapseState}
style={style.moreLess}
/>
);
}
text = (
<View
onLayout={this.measurePost}
@ -421,7 +405,12 @@ export default class MessageAttachment extends PureComponent {
onPermalinkPress={onPermalinkPress}
/>
</View>
{moreLess}
{isLongText &&
<ShowMoreButton
onPress={this.toggleCollapseState}
showMore={collapsed}
/>
}
</View>
);
}
@ -523,11 +512,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: 45,
width: 45,
},
moreLess: {
color: theme.linkColor,
fontSize: 12,
marginTop: 5,
},
headingContainer: {
alignSelf: 'stretch',
flexDirection: 'row',

View file

@ -8,7 +8,7 @@ import RNBottomSheet from 'react-native-bottom-sheet';
export default class OptionsContext extends PureComponent {
static propTypes = {
actions: PropTypes.array,
getPostActions: PropTypes.func,
cancelText: PropTypes.string,
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired,
@ -16,13 +16,13 @@ export default class OptionsContext extends PureComponent {
};
static defaultProps = {
actions: [],
getPostActions: () => [],
cancelText: 'Cancel',
};
show = (additionalAction) => {
const {actions, cancelText} = this.props;
const nextActions = [...actions];
const {getPostActions, cancelText} = this.props;
const nextActions = getPostActions();
if (additionalAction && !additionalAction.nativeEvent && additionalAction.text) {
const copyPostIndex = nextActions.findIndex((action) => action.copyPost);
nextActions.splice(copyPostIndex + 1, 0, additionalAction);
@ -45,11 +45,11 @@ export default class OptionsContext extends PureComponent {
};
handleHideUnderlay = () => {
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
};
handleShowUnderlay = () => {
this.props.toggleSelected(true, this.props.actions.length > 0);
this.props.toggleSelected(true, this.props.getPostActions().length > 0);
};
render() {

View file

@ -8,14 +8,14 @@ import ToolTip from 'app/components/tooltip';
export default class OptionsContext extends PureComponent {
static propTypes = {
actions: PropTypes.array,
getPostActions: PropTypes.func,
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired,
toggleSelected: PropTypes.func.isRequired,
};
static defaultProps = {
actions: [],
getPostActions: () => [],
additionalActions: [],
};
@ -23,16 +23,10 @@ export default class OptionsContext extends PureComponent {
super(props);
this.state = {
actions: props.actions,
actions: props.getPostActions(),
};
}
componentWillReceiveProps(nextProps) {
if (this.props.actions !== nextProps.actions) {
this.setState({actions: nextProps.actions});
}
}
handleHideUnderlay = () => {
if (!this.isShowing) {
this.props.toggleSelected(false, false);
@ -45,11 +39,11 @@ export default class OptionsContext extends PureComponent {
handleHide = () => {
this.isShowing = false;
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
};
handleShow = () => {
this.isShowing = this.props.actions.length > 0;
this.isShowing = this.props.getPostActions().length > 0;
this.props.toggleSelected(true, this.isShowing);
};
@ -59,12 +53,12 @@ export default class OptionsContext extends PureComponent {
}
this.setState({
actions: this.props.actions,
actions: this.props.getPostActions(),
});
};
show = (additionalAction) => {
const nextActions = [...this.props.actions];
const nextActions = this.props.getPostActions();
if (additionalAction && additionalAction.text && !additionalAction.nativeEvent) {
const copyPostIndex = nextActions.findIndex((action) => action.copyPost);
nextActions.splice(copyPostIndex + 1, 0, additionalAction);
@ -80,7 +74,7 @@ export default class OptionsContext extends PureComponent {
};
handlePress = () => {
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
this.props.onPress();
};

View file

@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
import {General, Posts} from 'mattermost-redux/constants';
import {getCurrentChannelId, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
@ -16,8 +17,6 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {addReaction} from 'app/actions/views/emoji';
import {getDimensions} from 'app/selectors/device';
import {Posts} from 'mattermost-redux/constants';
import Post from './post';
@ -78,33 +77,36 @@ function makeMapStateToProps() {
}
}
}
const {deviceWidth} = getDimensions(state);
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
let canDelete = false;
let canEdit = false;
let canEditUntil = -1;
if (post) {
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post);
if (canEdit && license.IsLicensed === 'true' &&
(config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1'))
) {
canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000);
}
}
return {
channelIsReadOnly: isCurrentChannelReadOnly(state),
config,
canDelete,
canEdit,
canEditUntil,
currentTeamUrl: getCurrentTeamUrl(state),
currentUserId,
deviceWidth,
post,
isFirstReply,
isLastReply,
consecutivePost: isConsecutivePost(state, ownProps),
hasComments: getCommentCountForPost(state, {post}) > 0,
commentedOnPost,
license,
theme: getTheme(state),
isFlagged: isPostFlagged(post.id, myPreferences),
};

View file

@ -25,9 +25,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {getToolTipVisible} from 'app/utils/tooltip';
import {Posts} from 'mattermost-redux/constants';
import DelayedAction from 'mattermost-redux/utils/delayed_action';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {editDisable, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import Config from 'assets/config';
@ -41,10 +40,8 @@ export default class Post extends PureComponent {
removePost: PropTypes.func.isRequired,
}).isRequired,
channelIsReadOnly: PropTypes.bool,
config: PropTypes.object.isRequired,
currentTeamUrl: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
deviceWidth: PropTypes.number.isRequired,
highlight: PropTypes.bool,
style: ViewPropTypes.style,
post: PropTypes.object,
@ -56,10 +53,10 @@ export default class Post extends PureComponent {
hasComments: PropTypes.bool,
isSearchResult: PropTypes.bool,
commentedOnPost: PropTypes.object,
license: PropTypes.object.isRequired,
managedConfig: PropTypes.object.isRequired,
navigator: PropTypes.object,
canEdit: PropTypes.bool.isRequired,
canEditUntil: PropTypes.number.isRequired,
canDelete: PropTypes.bool.isRequired,
onPermalinkPress: PropTypes.func,
shouldRenderReplyButton: PropTypes.bool,
@ -83,37 +80,6 @@ export default class Post extends PureComponent {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
const {config, license, currentUserId, post} = props;
this.editDisableAction = new DelayedAction(this.handleEditDisable);
if (post) {
editDisable(config, license, currentUserId, post, this.editDisableAction);
}
this.state = {
canEdit: this.props.canEdit,
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.config !== this.props.config ||
nextProps.license !== this.props.license ||
nextProps.currentUserId !== this.props.currentUserId ||
nextProps.post !== this.props.post) {
const {config, license, currentUserId, post} = nextProps;
editDisable(config, license, currentUserId, post, this.editDisableAction);
this.setState({
canEdit: nextProps.canEdit,
});
}
}
componentWillUnmount() {
this.editDisableAction.cancel();
}
goToUserProfile = () => {
const {intl} = this.context;
const {navigator, post, theme} = this.props;
@ -165,7 +131,6 @@ export default class Post extends PureComponent {
text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}),
style: 'destructive',
onPress: () => {
this.editDisableAction.cancel();
actions.deletePost(post);
if (post.user_id === currentUserId) {
actions.removePost(post);
@ -479,7 +444,8 @@ export default class Post extends PureComponent {
<PostBody
ref={'postBody'}
canDelete={this.props.canDelete}
canEdit={this.state.canEdit}
canEdit={this.props.canEdit}
canEditUntil={this.props.canEditUntil}
highlight={highlight}
channelIsReadOnly={channelIsReadOnly}
isSearchResult={isSearchResult}

View file

@ -6,14 +6,12 @@ import PropTypes from 'prop-types';
import {
Dimensions,
Platform,
Text,
TouchableHighlight,
TouchableOpacity,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/Ionicons';
import LinearGradient from 'react-native-linear-gradient';
import {Posts} from 'mattermost-redux/constants';
@ -21,6 +19,7 @@ import CombinedSystemMessage from 'app/components/combined_system_message';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import OptionsContext from 'app/components/options_context';
import ShowMoreButton from 'app/components/show_more_button';
import {emptyFunction} from 'app/utils/general';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
@ -41,6 +40,7 @@ export default class PostBody extends PureComponent {
canAddReaction: PropTypes.bool,
canDelete: PropTypes.bool,
canEdit: PropTypes.bool,
canEditUntil: PropTypes.number.isRequired,
channelIsReadOnly: PropTypes.bool.isRequired,
fileIds: PropTypes.array,
hasBeenDeleted: PropTypes.bool,
@ -120,6 +120,7 @@ export default class PostBody extends PureComponent {
const {formatMessage} = this.context.intl;
const {
canEdit,
canEditUntil,
canDelete,
canAddReaction,
channelIsReadOnly,
@ -169,7 +170,7 @@ export default class PostBody extends PureComponent {
}
}
if (canEdit) {
if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) {
actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit});
}
@ -336,64 +337,12 @@ export default class PostBody extends PureComponent {
);
};
renderShowMoreOption = (style) => {
const {highlight, theme} = this.props;
const {isLongPost} = this.state;
if (!isLongPost) {
return null;
}
const gradientColors = [];
if (highlight) {
gradientColors.push(
changeOpacity(theme.mentionHighlightBg, 0),
changeOpacity(theme.mentionHighlightBg, 0.15),
changeOpacity(theme.mentionHighlightBg, 0.5),
);
} else {
gradientColors.push(
changeOpacity(theme.centerChannelBg, 0),
changeOpacity(theme.centerChannelBg, 0.75),
theme.centerChannelBg,
);
}
return (
<View>
<LinearGradient
colors={gradientColors}
locations={[0, 0.7, 1]}
style={style.showMoreGradient}
/>
<View style={style.showMoreContainer}>
<View style={style.showMoreDividerLeft}/>
<TouchableOpacity
onPress={this.openLongPost}
style={style.showMoreButtonContainer}
>
<View style={style.showMoreButton}>
<Text style={style.showMorePlusSign}>
{'+'}
</Text>
<FormattedText
id='mobile.post_body.show_more'
defaultMessage='Show More'
style={style.showMoreText}
/>
</View>
</TouchableOpacity>
<View style={style.showMoreDividerRight}/>
</View>
</View>
);
};
render() {
const {formatMessage} = this.context.intl;
const {
hasBeenDeleted,
hasBeenEdited,
highlight,
isFailed,
isPending,
isPostAddChannelMember,
@ -488,7 +437,7 @@ export default class PostBody extends PureComponent {
body = (
<View style={style.messageBody}>
<OptionsContext
actions={this.getPostActions()}
getPostActions={this.getPostActions}
ref='options'
onPress={onPress}
toggleSelected={toggleSelected}
@ -496,7 +445,12 @@ export default class PostBody extends PureComponent {
>
<View onLayout={this.measurePost}>
{messageComponent}
{this.renderShowMoreOption(style)}
{isLongPost &&
<ShowMoreButton
highlight={highlight}
onPress={this.openLongPost}
/>
}
</View>
{this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)}
{this.renderFileAttachments()}
@ -562,56 +516,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
systemMessage: {
opacity: 0.6,
},
showMoreGradient: {
flex: 1,
height: 50,
position: 'absolute',
top: -50,
width: '100%',
},
showMoreContainer: {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
flexDirection: 'row',
position: 'relative',
top: -7.5,
},
showMoreDividerLeft: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
flex: 1,
height: 1,
marginRight: 10,
},
showMoreDividerRight: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
flex: 1,
height: 1,
marginLeft: 10,
},
showMoreButtonContainer: {
backgroundColor: theme.centerChannelBg,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRadius: 4,
borderWidth: 1,
height: 37,
paddingHorizontal: 10,
},
showMoreButton: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
},
showMorePlusSign: {
color: theme.linkColor,
fontSize: 16,
fontWeight: '600',
marginRight: 8,
},
showMoreText: {
color: theme.linkColor,
fontSize: 13,
fontWeight: '600',
},
};
});

View file

@ -215,7 +215,7 @@ export default class PostHeader extends PureComponent {
}
return (
<View>
<React.Fragment>
<View style={[style.postInfoContainer, (isPendingOrFailedPost && style.pendingPost)]}>
<View style={{flexDirection: 'row', flex: 1}}>
{this.getDisplayName(style)}
@ -253,7 +253,7 @@ export default class PostHeader extends PureComponent {
{this.renderCommentedOnMessage(style)}
</View>
}
</View>
</React.Fragment>
);
}
}

View file

@ -234,9 +234,11 @@ export default class PostList extends PureComponent {
nextConfig = await mattermostManaged.getLocalConfig();
}
this.setState({
managedConfig: nextConfig,
});
if (Object.keys(nextConfig).length) {
this.setState({
managedConfig: nextConfig,
});
}
};
keyExtractor = (item) => {

View file

@ -28,9 +28,10 @@ function withLayout(WrappedComponent) {
};
render() {
const {index, onLayoutCalled, shouldCallOnLayout, ...otherProps} = this.props; //eslint-disable-line no-unused-vars
return (
<View onLayout={this.onLayout}>
<WrappedComponent {...this.props}/>
<WrappedComponent {...otherProps}/>
</View>
);
}

View file

@ -0,0 +1,893 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShowMoreButton should match, button snapshot 1`] = `
<Component
style={Object {}}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={Object {}}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={Object {}}
/>
</Component>
`;
exports[`ShowMoreButton should match, button snapshot 2`] = `
<Component
style={Object {}}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={Object {}}
>
-
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show Less"
id="post_info.message.show_less"
style={Object {}}
/>
</Component>
`;
exports[`ShowMoreButton should match, full snapshot 1`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <ShowMoreButton
highlight={false}
onPress={[MockFunction]}
showMore={true}
theme={
Object {
"centerChannelBg": "#2f3e4e",
"centerChannelColor": "#dddddd",
}
}
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<LinearGradient
colors={
Array [
"rgba(47,62,78,0)",
"rgba(47,62,78,0.75)",
"#2f3e4e",
]
}
locations={
Array [
0,
0.7,
1,
]
}
style={
Object {
"flex": 1,
"height": 50,
"position": "absolute",
"top": -50,
"width": "100%",
}
}
/>,
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"position": "relative",
"top": 10,
}
}
>
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginRight": 10,
}
}
/>
<TouchableOpacity
activeOpacity={0.2}
onPress={[MockFunction]}
style={
Object {
"backgroundColor": "#2f3e4e",
"borderColor": "rgba(221,221,221,0.2)",
"borderRadius": 4,
"borderWidth": 1,
"height": 37,
"paddingHorizontal": 10,
}
}
>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>
</Component>
</TouchableOpacity>
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginLeft": 10,
}
}
/>
</Component>,
],
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"colors": Array [
"rgba(47,62,78,0)",
"rgba(47,62,78,0.75)",
"#2f3e4e",
],
"locations": Array [
0,
0.7,
1,
],
"style": Object {
"flex": 1,
"height": 50,
"position": "absolute",
"top": -50,
"width": "100%",
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginRight": 10,
}
}
/>,
<TouchableOpacity
activeOpacity={0.2}
onPress={[MockFunction]}
style={
Object {
"backgroundColor": "#2f3e4e",
"borderColor": "rgba(221,221,221,0.2)",
"borderRadius": 4,
"borderWidth": 1,
"height": 37,
"paddingHorizontal": 10,
}
}
>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>
</Component>
</TouchableOpacity>,
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginLeft": 10,
}
}
/>,
],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"position": "relative",
"top": 10,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"style": Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginRight": 10,
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"activeOpacity": 0.2,
"children": <Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>
</Component>,
"onPress": [MockFunction],
"style": Object {
"backgroundColor": "#2f3e4e",
"borderColor": "rgba(221,221,221,0.2)",
"borderRadius": 4,
"borderWidth": 1,
"height": 37,
"paddingHorizontal": 10,
},
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>,
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>,
],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": "+",
"ellipsizeMode": "tail",
"style": Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
},
},
"ref": null,
"rendered": "+",
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"defaultMessage": "Show More",
"id": "post_info.message.show_more",
"style": Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"style": Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginLeft": 10,
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
],
"type": [Function],
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<LinearGradient
colors={
Array [
"rgba(47,62,78,0)",
"rgba(47,62,78,0.75)",
"#2f3e4e",
]
}
locations={
Array [
0,
0.7,
1,
]
}
style={
Object {
"flex": 1,
"height": 50,
"position": "absolute",
"top": -50,
"width": "100%",
}
}
/>,
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"position": "relative",
"top": 10,
}
}
>
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginRight": 10,
}
}
/>
<TouchableOpacity
activeOpacity={0.2}
onPress={[MockFunction]}
style={
Object {
"backgroundColor": "#2f3e4e",
"borderColor": "rgba(221,221,221,0.2)",
"borderRadius": 4,
"borderWidth": 1,
"height": 37,
"paddingHorizontal": 10,
}
}
>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>
</Component>
</TouchableOpacity>
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginLeft": 10,
}
}
/>
</Component>,
],
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"colors": Array [
"rgba(47,62,78,0)",
"rgba(47,62,78,0.75)",
"#2f3e4e",
],
"locations": Array [
0,
0.7,
1,
],
"style": Object {
"flex": 1,
"height": 50,
"position": "absolute",
"top": -50,
"width": "100%",
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginRight": 10,
}
}
/>,
<TouchableOpacity
activeOpacity={0.2}
onPress={[MockFunction]}
style={
Object {
"backgroundColor": "#2f3e4e",
"borderColor": "rgba(221,221,221,0.2)",
"borderRadius": 4,
"borderWidth": 1,
"height": 37,
"paddingHorizontal": 10,
}
}
>
<Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>
</Component>
</TouchableOpacity>,
<Component
style={
Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginLeft": 10,
}
}
/>,
],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"position": "relative",
"top": 10,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"style": Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginRight": 10,
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"activeOpacity": 0.2,
"children": <Component
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>
</Component>,
"onPress": [MockFunction],
"style": Object {
"backgroundColor": "#2f3e4e",
"borderColor": "rgba(221,221,221,0.2)",
"borderRadius": 4,
"borderWidth": 1,
"height": 37,
"paddingHorizontal": 10,
},
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
style={
Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
}
}
>
+
</Text>,
<InjectIntl(FormattedText)
defaultMessage="Show More"
id="post_info.message.show_more"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
}
}
/>,
],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": "+",
"ellipsizeMode": "tail",
"style": Object {
"color": undefined,
"fontSize": 16,
"fontWeight": "600",
"marginRight": 8,
},
},
"ref": null,
"rendered": "+",
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"defaultMessage": "Show More",
"id": "post_info.message.show_more",
"style": Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"style": Object {
"backgroundColor": "rgba(221,221,221,0.2)",
"flex": 1,
"height": 1,
"marginLeft": 10,
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
],
"type": [Function],
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;

View file

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ShowMoreButton from './show_more_button';
function mapStateToProps(state) {
return {
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(ShowMoreButton);

View file

@ -0,0 +1,142 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text, TouchableOpacity, View} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ShowMoreButton extends PureComponent {
static propTypes = {
highlight: PropTypes.bool,
onPress: PropTypes.func.isRequired,
showMore: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
};
static defaultProps = {
showMore: true,
};
renderButton(showMore, style) {
let sign = '+';
let textId = 'post_info.message.show_more';
let textMessage = 'Show More';
if (!showMore) {
sign = '-';
textId = 'post_info.message.show_less';
textMessage = 'Show Less';
}
return (
<View style={style.button}>
<Text style={style.sign}>{sign}</Text>
<FormattedText
id={textId}
defaultMessage={textMessage}
style={style.text}
/>
</View>
);
}
render() {
const {highlight, showMore, theme} = this.props;
const style = getStyleSheet(theme, showMore);
let gradientColors = [
changeOpacity(theme.centerChannelBg, 0),
changeOpacity(theme.centerChannelBg, 0.75),
theme.centerChannelBg,
];
if (highlight) {
gradientColors = [
changeOpacity(theme.mentionHighlightBg, 0),
changeOpacity(theme.mentionHighlightBg, 0.15),
changeOpacity(theme.mentionHighlightBg, 0.5),
];
}
return (
<View>
{showMore &&
<LinearGradient
colors={gradientColors}
locations={[0, 0.7, 1]}
style={style.gradient}
/>
}
<View style={style.container}>
<View style={style.dividerLeft}/>
<TouchableOpacity
onPress={this.props.onPress}
style={style.buttonContainer}
>
{this.renderButton(showMore, style)}
</TouchableOpacity>
<View style={style.dividerRight}/>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme, showMore) => {
return {
gradient: {
flex: 1,
height: 50,
position: 'absolute',
top: -50,
width: '100%',
},
container: {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
flexDirection: 'row',
position: 'relative',
top: showMore ? -7.5 : 10,
},
dividerLeft: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
flex: 1,
height: 1,
marginRight: 10,
},
buttonContainer: {
backgroundColor: theme.centerChannelBg,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRadius: 4,
borderWidth: 1,
height: 37,
paddingHorizontal: 10,
},
button: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
},
sign: {
color: theme.linkColor,
fontSize: 16,
fontWeight: '600',
marginRight: 8,
},
text: {
color: theme.linkColor,
fontSize: 13,
fontWeight: '600',
},
dividerRight: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
flex: 1,
height: 1,
marginLeft: 10,
},
};
});

View file

@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {TouchableOpacity} from 'react-native';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import LinearGradient from 'react-native-linear-gradient';
import ShowMoreButton from './show_more_button';
describe('ShowMoreButton', () => {
const baseProps = {
highlight: false,
onPress: jest.fn(),
showMore: true,
theme: {
centerChannelBg: '#2f3e4e',
centerChannelColor: '#dddddd',
},
};
test('should match, full snapshot', () => {
const wrapper = shallow(
<ShowMoreButton {...baseProps}/>
);
expect(wrapper).toMatchSnapshot();
});
test('should match, button snapshot', () => {
const wrapper = shallow(
<ShowMoreButton {...baseProps}/>
);
expect(wrapper.instance().renderButton(true, {button: {}, sign: {}, text: {}})).toMatchSnapshot();
expect(wrapper.instance().renderButton(false, {button: {}, sign: {}, text: {}})).toMatchSnapshot();
});
test('should LinearGradient exists', () => {
const wrapper = shallow(
<ShowMoreButton {...baseProps}/>
);
expect(wrapper.find(LinearGradient).exists()).toBe(true);
wrapper.setProps({showMore: false});
expect(wrapper.find(LinearGradient).exists()).toBe(false);
});
test('should call props.onPress on press of TouchableOpacity', () => {
const onPress = jest.fn();
const wrapper = shallow(
<ShowMoreButton
{...baseProps}
onPress={onPress}
/>
);
wrapper.find(TouchableOpacity).props().onPress();
expect(onPress).toHaveBeenCalledTimes(1);
});
});

View file

@ -29,6 +29,7 @@ export default class ChannelInfo extends PureComponent {
closeGMChannel: PropTypes.func.isRequired,
deleteChannel: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
getChannel: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
loadChannelsByTeamName: PropTypes.func.isRequired,
favoriteChannel: PropTypes.func.isRequired,
@ -202,6 +203,9 @@ export default class ChannelInfo extends PureComponent {
displayName: channel.display_name,
}
);
if (result.error.server_error_id === 'api.channel.delete_channel.deleted.app_error') {
this.props.actions.getChannel(channel.id);
}
} else {
this.close();
}

View file

@ -7,6 +7,7 @@ import {connect} from 'react-redux';
import {
favoriteChannel,
getChannelStats,
getChannel,
deleteChannel,
unfavoriteChannel,
updateChannelNotifyProps,
@ -89,6 +90,7 @@ function mapDispatchToProps(dispatch) {
closeGMChannel,
deleteChannel,
getChannelStats,
getChannel,
leaveChannel,
loadChannelsByTeamName,
favoriteChannel,

View file

@ -52,6 +52,7 @@ export default class EditChannel extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
patchChannel: PropTypes.func.isRequired,
getChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}),
navigator: PropTypes.object.isRequired,
@ -204,7 +205,7 @@ export default class EditChannel extends PureComponent {
return {error: formatMessage(messages.name_lowercase)};
};
onUpdateChannel = () => {
onUpdateChannel = async () => {
Keyboard.dismiss();
const {displayName, channelURL, purpose, header} = this.state;
const {channel: {id, type}} = this.props;
@ -230,7 +231,10 @@ export default class EditChannel extends PureComponent {
}
}
this.props.actions.patchChannel(id, channel);
const data = await this.props.actions.patchChannel(id, channel);
if (data.error && data.error.server_error_id === 'store.sql_channel.update.archived_channel.app_error') {
this.props.actions.getChannel(id);
}
};
onNavigatorEvent = (event) => {

View file

@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {patchChannel} from 'mattermost-redux/actions/channels';
import {patchChannel, getChannel} from 'mattermost-redux/actions/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setChannelDisplayName} from 'app/actions/views/channel';
@ -33,6 +33,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
patchChannel,
getChannel,
setChannelDisplayName,
}, dispatch),
};

View file

@ -11,6 +11,7 @@ import {
import DeviceInfo from 'react-native-device-info';
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -56,15 +57,14 @@ const lazyLoadReplyPushNotifications = () => {
*/
export default class Entry extends PureComponent {
static propTypes = {
config: PropTypes.object,
theme: PropTypes.object,
navigator: PropTypes.object,
isLandscape: PropTypes.bool,
hydrationComplete: PropTypes.bool,
enableTimezone: PropTypes.bool,
deviceTimezone: PropTypes.string,
initializeModules: PropTypes.func.isRequired,
actions: PropTypes.shape({
autoUpdateTimezone: PropTypes.func.isRequired,
setDeviceToken: PropTypes.func.isRequired,
}).isRequired,
};
@ -132,6 +132,7 @@ export default class Entry extends PureComponent {
this.setAppCredentials();
this.setStartupThemes();
this.handleNotification();
this.loadSystemEmojis();
if (Platform.OS === 'android') {
this.launchForAndroid();
@ -226,6 +227,11 @@ export default class Entry extends PureComponent {
}
};
loadSystemEmojis = () => {
const EmojiIndicesByAlias = require('app/utils/emojis').EmojiIndicesByAlias;
setSystemEmojis(EmojiIndicesByAlias);
};
renderLogin = () => {
const SelectServer = lazyLoadSelectServer();
const props = {

View file

@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {setDeviceToken} from 'mattermost-redux/actions/general';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {isLandscape} from 'app/selectors/device';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
@ -16,16 +15,12 @@ const lazyLoadEntry = () => {
};
function mapStateToProps(state) {
const config = getConfig(state);
const enableTimezone = isTimezoneEnabled(state);
const deviceTimezone = getDeviceTimezone();
return {
config,
theme: getTheme(state),
isLandscape: isLandscape(state),
hydrationComplete: state.views.root.hydrationComplete,
enableTimezone,
deviceTimezone,
};
@ -34,8 +29,8 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setDeviceToken,
autoUpdateTimezone,
setDeviceToken,
}, dispatch),
};
}

View file

@ -10,11 +10,14 @@ import {getCurrentChannelId, filterPostIds} from 'mattermost-redux/selectors/ent
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list';
import {handleSearchDraftChanged} from 'app/actions/views/search';
import {getDeviceUtcOffset, getUtcOffsetForTimeZone, isTimezoneEnabled} from 'app/utils/timezone';
import Search from './search';
@ -30,6 +33,11 @@ function makeMapStateToProps() {
const {recent} = state.entities.search;
const {searchPosts: searchRequest} = state.requests.search;
const currentUser = getCurrentUser(state);
const enableTimezone = isTimezoneEnabled(state);
const userCurrentTimezone = enableTimezone ? getUserCurrentTimezone(currentUser.timezone) : '';
const timezoneOffsetInSeconds = (userCurrentTimezone.length > 0 ? getUtcOffsetForTimeZone(userCurrentTimezone) : getDeviceUtcOffset()) * 60;
const serverVersion = state.entities.general.serverVersion;
const enableDateSuggestion = isMinimumServerVersion(serverVersion, 5, 3);
@ -43,6 +51,7 @@ function makeMapStateToProps() {
searchingStatus: searchRequest.status,
theme: getTheme(state),
enableDateSuggestion,
timezoneOffsetInSeconds,
};
};
}

View file

@ -31,7 +31,6 @@ import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import mattermostManaged from 'app/mattermost_managed';
import {preventDoubleTap} from 'app/utils/tap';
import {getDeviceUtcOffset} from 'app/utils/timezone';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDisplayName from './channel_display_name';
@ -65,6 +64,7 @@ export default class Search extends PureComponent {
searchingStatus: PropTypes.string,
theme: PropTypes.object.isRequired,
enableDateSuggestion: PropTypes.bool,
timezoneOffsetInSeconds: PropTypes.number.isRequired,
};
static defaultProps = {
@ -469,8 +469,7 @@ export default class Search extends PureComponent {
});
// timezone offset in seconds
const timeZoneOffset = getDeviceUtcOffset() * 60;
actions.searchPostsWithParams(currentTeamId, {terms: terms.trim(), is_or_search: isOrSearch, time_zone_offset: timeZoneOffset}, true);
actions.searchPostsWithParams(currentTeamId, {terms: terms.trim(), is_or_search: isOrSearch, time_zone_offset: this.props.timezoneOffsetInSeconds}, true);
};
handleSearchButtonPress = preventDoubleTap((text) => {

View file

@ -0,0 +1,609 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectTeam should match snapshot for fail of teams 1`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <SelectTeam
actions={
Object {
"getTeams": [Function],
"handleTeamChange": [MockFunction],
"joinTeam": [MockFunction],
"logout": [MockFunction],
"markChannelAsRead": [MockFunction],
}
}
currentChannelId="someId"
currentUrl="test"
joinTeamRequest={Object {}}
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
},
}
}
teams={Array []}
teamsRequest={
Object {
"status": "failure",
}
}
theme={Object {}}
userWithoutTeams={false}
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"errorDescription": Object {
"defaultMessage": "Make sure you have an active connection and try again.",
"id": "mobile.failed_network_action.shortDescription",
},
"errorTitle": Object {
"defaultMessage": "Team Not Found",
"id": "error.team_not_found.title",
},
"onRetry": [Function],
"theme": Object {},
},
"ref": null,
"rendered": null,
"type": [Function],
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"errorDescription": Object {
"defaultMessage": "Make sure you have an active connection and try again.",
"id": "mobile.failed_network_action.shortDescription",
},
"errorTitle": Object {
"defaultMessage": "Team Not Found",
"id": "error.team_not_found.title",
},
"onRetry": [Function],
"theme": Object {},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;
exports[`SelectTeam should match snapshot for teams 1`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <SelectTeam
actions={
Object {
"getTeams": [Function],
"handleTeamChange": [MockFunction],
"joinTeam": [MockFunction],
"logout": [MockFunction],
"markChannelAsRead": [MockFunction],
}
}
currentChannelId="someId"
currentUrl="test"
joinTeamRequest={Object {}}
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
},
}
}
teams={
Array [
Object {
"id": "kemjcpu9bi877yegqjs18ndp4r",
"invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae",
"name": "test",
},
]
}
teamsRequest={
Object {
"status": "success",
}
}
theme={Object {}}
userWithoutTeams={false}
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Connect(StatusBar) />,
<Component
style={
Object {
"alignItems": "center",
"flexDirection": "row",
"marginHorizontal": 16,
"marginTop": 20,
}
}
>
<Component
style={
Object {
"marginRight": 15,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="Open teams you can join"
id="mobile.select_team.join_open"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>
</Component>
<Component
style={
Object {
"backgroundColor": undefined,
"height": 1,
"width": "100%",
}
}
/>
</Component>,
<FlatList
data={
Array [
Object {
"id": "kemjcpu9bi877yegqjs18ndp4r",
"invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae",
"name": "test",
},
]
}
disableVirtualization={false}
horizontal={false}
initialNumToRender={10}
keyExtractor={[Function]}
maxToRenderPerBatch={10}
numColumns={1}
onEndReachedThreshold={2}
renderItem={[Function]}
scrollEventThrottle={50}
updateCellsBatchingPeriod={50}
viewabilityConfig={
Object {
"viewAreaCoveragePercentThreshold": 3,
"waitForInteraction": false,
}
}
windowSize={21}
/>,
],
"style": Object {
"backgroundColor": undefined,
"flex": 1,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Component
style={
Object {
"marginRight": 15,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="Open teams you can join"
id="mobile.select_team.join_open"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>
</Component>,
<Component
style={
Object {
"backgroundColor": undefined,
"height": 1,
"width": "100%",
}
}
/>,
],
"style": Object {
"alignItems": "center",
"flexDirection": "row",
"marginHorizontal": 16,
"marginTop": 20,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": <InjectIntl(FormattedText)
defaultMessage="Open teams you can join"
id="mobile.select_team.join_open"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>,
"style": Object {
"marginRight": 15,
},
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"defaultMessage": "Open teams you can join",
"id": "mobile.select_team.join_open",
"style": Object {
"color": undefined,
"fontSize": 13,
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"style": Object {
"backgroundColor": undefined,
"height": 1,
"width": "100%",
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"data": Array [
Object {
"id": "kemjcpu9bi877yegqjs18ndp4r",
"invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae",
"name": "test",
},
],
"disableVirtualization": false,
"horizontal": false,
"initialNumToRender": 10,
"keyExtractor": [Function],
"maxToRenderPerBatch": 10,
"numColumns": 1,
"onEndReachedThreshold": 2,
"renderItem": [Function],
"scrollEventThrottle": 50,
"updateCellsBatchingPeriod": 50,
"viewabilityConfig": Object {
"viewAreaCoveragePercentThreshold": 3,
"waitForInteraction": false,
},
"windowSize": 21,
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Connect(StatusBar) />,
<Component
style={
Object {
"alignItems": "center",
"flexDirection": "row",
"marginHorizontal": 16,
"marginTop": 20,
}
}
>
<Component
style={
Object {
"marginRight": 15,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="Open teams you can join"
id="mobile.select_team.join_open"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>
</Component>
<Component
style={
Object {
"backgroundColor": undefined,
"height": 1,
"width": "100%",
}
}
/>
</Component>,
<FlatList
data={
Array [
Object {
"id": "kemjcpu9bi877yegqjs18ndp4r",
"invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae",
"name": "test",
},
]
}
disableVirtualization={false}
horizontal={false}
initialNumToRender={10}
keyExtractor={[Function]}
maxToRenderPerBatch={10}
numColumns={1}
onEndReachedThreshold={2}
renderItem={[Function]}
scrollEventThrottle={50}
updateCellsBatchingPeriod={50}
viewabilityConfig={
Object {
"viewAreaCoveragePercentThreshold": 3,
"waitForInteraction": false,
}
}
windowSize={21}
/>,
],
"style": Object {
"backgroundColor": undefined,
"flex": 1,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {},
"ref": null,
"rendered": null,
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": Array [
<Component
style={
Object {
"marginRight": 15,
}
}
>
<InjectIntl(FormattedText)
defaultMessage="Open teams you can join"
id="mobile.select_team.join_open"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>
</Component>,
<Component
style={
Object {
"backgroundColor": undefined,
"height": 1,
"width": "100%",
}
}
/>,
],
"style": Object {
"alignItems": "center",
"flexDirection": "row",
"marginHorizontal": 16,
"marginTop": 20,
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": <InjectIntl(FormattedText)
defaultMessage="Open teams you can join"
id="mobile.select_team.join_open"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>,
"style": Object {
"marginRight": 15,
},
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"defaultMessage": "Open teams you can join",
"id": "mobile.select_team.join_open",
"style": Object {
"color": undefined,
"fontSize": 13,
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"style": Object {
"backgroundColor": undefined,
"height": 1,
"width": "100%",
},
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"data": Array [
Object {
"id": "kemjcpu9bi877yegqjs18ndp4r",
"invite_id": "ojsnudhqzbfzpk6e4n6ip1hwae",
"name": "test",
},
],
"disableVirtualization": false,
"horizontal": false,
"initialNumToRender": 10,
"keyExtractor": [Function],
"maxToRenderPerBatch": 10,
"numColumns": 1,
"onEndReachedThreshold": 2,
"renderItem": [Function],
"scrollEventThrottle": 50,
"updateCellsBatchingPeriod": 50,
"viewabilityConfig": Object {
"viewAreaCoveragePercentThreshold": 3,
"waitForInteraction": false,
},
"windowSize": 21,
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;

View file

@ -28,7 +28,7 @@ function mapStateToProps(state) {
}
return {
teamsRequest: state.requests.teams.getMyTeams,
teamsRequest: state.requests.teams.getTeams,
teams: Object.values(getJoinableTeams(state)).sort(sortTeams),
currentChannelId: getCurrentChannelId(state),
joinTeamRequest: state.requests.teams.joinTeam,

View file

@ -15,6 +15,7 @@ import {
import {RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import FailedNetworkAction from 'app/components/failed_network_action';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
@ -28,6 +29,16 @@ const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;
const TEAMS_PER_PAGE = 200;
const errorTitle = {
id: 'error.team_not_found.title',
defaultMessage: 'Team Not Found',
};
const errorDescription = {
id: 'mobile.failed_network_action.shortDescription',
defaultMessage: 'Make sure you have an active connection and try again.',
};
export default class SelectTeam extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@ -44,6 +55,7 @@ export default class SelectTeam extends PureComponent {
userWithoutTeams: PropTypes.bool,
teams: PropTypes.array.isRequired,
theme: PropTypes.object,
teamsRequest: PropTypes.object.isRequired,
};
constructor(props) {
@ -57,9 +69,7 @@ export default class SelectTeam extends PureComponent {
}
componentDidMount() {
this.props.actions.getTeams(0, TEAMS_PER_PAGE).then(() => {
this.buildData(this.props);
});
this.getTeams();
}
componentWillReceiveProps(nextProps) {
@ -77,6 +87,14 @@ export default class SelectTeam extends PureComponent {
}
}
getTeams = () => {
this.setState({loading: true});
this.props.actions.getTeams(0, TEAMS_PER_PAGE).then(() => {
this.setState({loading: false});
this.buildData(this.props);
});
}
buildData = (props) => {
if (props.teams.length) {
this.setState({teams: props.teams});
@ -213,10 +231,21 @@ export default class SelectTeam extends PureComponent {
const {teams} = this.state;
const styles = getStyleSheet(theme);
if (this.state.joining) {
if (this.state.joining || this.state.loading) {
return <Loading/>;
}
if (this.props.teamsRequest.status === RequestStatus.FAILURE) {
return (
<FailedNetworkAction
onRetry={this.getTeams}
theme={theme}
errorTitle={errorTitle}
errorDescription={errorDescription}
/>
);
}
return (
<View style={styles.container}>
<StatusBar/>

View file

@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import {RequestStatus} from 'mattermost-redux/constants';
configure({adapter: new Adapter()});
import SelectTeam from './select_team.js';
jest.mock('rn-fetch-blob', () => ({
fs: {
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
},
}));
jest.mock('rn-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
jest.mock('app/utils/theme', () => {
const original = require.requireActual('app/utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
const getTeams = async () => {
return {
error: {},
};
};
describe('SelectTeam', () => {
const actions = {
getTeams,
handleTeamChange: jest.fn(),
joinTeam: jest.fn(),
logout: jest.fn(),
markChannelAsRead: jest.fn(),
};
const baseProps = {
actions,
currentChannelId: 'someId',
currentUrl: 'test',
joinTeamRequest: {},
navigator: {
setOnNavigatorEvent: jest.fn(),
},
userWithoutTeams: false,
teams: [],
theme: {},
teamsRequest: {
status: RequestStatus.FAILURE,
},
};
test('should match snapshot for fail of teams', async () => {
const wrapper = shallow(
<SelectTeam {...baseProps}/>,
);
expect(wrapper.state('loading')).toEqual(true);
await getTeams();
expect(wrapper.state('loading')).toEqual(false);
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for teams', async () => {
const props = {
...baseProps,
teams: [{
id: 'kemjcpu9bi877yegqjs18ndp4r',
invite_id: 'ojsnudhqzbfzpk6e4n6ip1hwae',
name: 'test',
}],
teamsRequest: {
status: RequestStatus.SUCCESS,
},
};
const wrapper = shallow(
<SelectTeam {...props}/>,
);
await getTeams();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -5,13 +5,18 @@ import DeviceInfo from 'react-native-device-info';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import moment from 'moment-timezone';
export function getDeviceTimezone() {
return DeviceInfo.getTimezone();
}
export function getDeviceUtcOffset() {
const reverseOffsetInMinutes = new Date().getTimezoneOffset();
return -reverseOffsetInMinutes;
return moment().utcOffset();
}
export function getUtcOffsetForTimeZone(timezone) {
return moment.tz(timezone).utcOffset();
}
export function isTimezoneEnabled(state) {

View file

@ -40,10 +40,10 @@ function deepDiff(o1, o2, p) {
}
}
function whyDidYouUpdate(prevProps, prevState) {
function whyDidYouUpdate(theClass, prevProps, prevState) {
deepDiff({props: prevProps, state: prevState},
{props: this.props, state: this.state},
this.constructor.name);
{props: theClass.props, state: theClass.state},
theClass.constructor.name);
}
export default whyDidYouUpdate;

View file

@ -2455,6 +2455,7 @@
"mobile.failed_network_action.description": "There seems to be a problem with your internet connection. Make sure you have an active connection and try again.",
"mobile.failed_network_action.retry": "Try Again",
"mobile.failed_network_action.title": "No internet connection",
"mobile.failed_network_action.shortDescription": "Make sure you have an active connection and try again.",
"mobile.file_upload.browse": "Browse Files",
"mobile.file_upload.camera": "Take Photo or Video",
"mobile.file_upload.library": "Photo Library",

View file

@ -2427,7 +2427,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 135;
CURRENT_PROJECT_VERSION = 136;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@ -2476,7 +2476,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 135;
CURRENT_PROJECT_VERSION = 136;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View file

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

View file

@ -23,7 +23,7 @@
<key>CFBundleShortVersionString</key>
<string>1.12.0</string>
<key>CFBundleVersion</key>
<string>135</string>
<string>136</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

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

11
package-lock.json generated
View file

@ -10545,8 +10545,15 @@
"moment": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
"integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==",
"dev": true
"integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ=="
},
"moment-timezone": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz",
"integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==",
"requires": {
"moment": ">= 2.9.0"
}
},
"morgan": {
"version": "1.9.0",

View file

@ -18,6 +18,7 @@
"jsc-android": "216113.0.3",
"mattermost-redux": "github:mattermost/mattermost-redux#28be3683bff6ae8c563883bb0ff04d7bd9ea31ee",
"mime-db": "1.33.0",
"moment-timezone": "0.5.21",
"prop-types": "15.6.1",
"react": "16.3.2",
"react-intl": "2.4.0",