Merge pull request #1888 from mattermost/merge-1.10

Merge release 1.10 into master
This commit is contained in:
Elias Nahum 2018-07-04 17:07:58 -04:00 committed by GitHub
commit 5531d25b7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1280 additions and 529 deletions

View file

@ -113,7 +113,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 119
versionCode 123
versionName "1.10.0"
ndk {
abiFilters "armeabi-v7a", "x86"

View file

@ -511,17 +511,19 @@ export function increasePostVisibility(channelId, focusedPostId) {
}];
const posts = result.data;
let hasMorePost = false;
if (posts) {
hasMorePost = posts.order.length >= pageSize;
// make sure to increment the posts visibility
// only if we got results
actions.push(doIncreasePostVisibility(channelId));
actions.push(setLoadMorePostsVisible(posts.order.length >= pageSize));
actions.push(setLoadMorePostsVisible(hasMorePost));
}
dispatch(batchActions(actions));
return Boolean(posts);
return hasMorePost;
};
}

View file

@ -5,12 +5,6 @@ ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
actions={
Object {
"dismissBanner": [MockFunction],
}
}
allowDismissal={true}
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={true}
@ -58,7 +52,9 @@ ShallowWrapper {
]
}
>
Banner Text
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<Icon
allowFontScaling={false}
@ -107,7 +103,9 @@ ShallowWrapper {
]
}
>
Banner Text
<RemoveMarkdown
value="Banner Text"
/>
</Text>,
<Icon
allowFontScaling={false}
@ -132,7 +130,9 @@ ShallowWrapper {
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": "Banner Text",
"children": <RemoveMarkdown
value="Banner Text"
/>,
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
@ -147,7 +147,17 @@ ShallowWrapper {
],
},
"ref": null,
"rendered": "Banner Text",
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"value": "Banner Text",
},
"ref": null,
"rendered": null,
"type": [Function],
},
"type": [Function],
},
Object {
@ -204,7 +214,9 @@ ShallowWrapper {
]
}
>
Banner Text
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<Icon
allowFontScaling={false}
@ -253,7 +265,9 @@ ShallowWrapper {
]
}
>
Banner Text
<RemoveMarkdown
value="Banner Text"
/>
</Text>,
<Icon
allowFontScaling={false}
@ -278,7 +292,9 @@ ShallowWrapper {
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": "Banner Text",
"children": <RemoveMarkdown
value="Banner Text"
/>,
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
@ -293,7 +309,17 @@ ShallowWrapper {
],
},
"ref": null,
"rendered": "Banner Text",
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"value": "Banner Text",
},
"ref": null,
"rendered": null,
"type": [Function],
},
"type": [Function],
},
Object {
@ -331,12 +357,6 @@ ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
actions={
Object {
"dismissBanner": [MockFunction],
}
}
allowDismissal={true}
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={false}

View file

@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
Animated,
StyleSheet,
Text,
@ -13,19 +12,19 @@ import {
import {intlShape} from 'react-intl';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import RemoveMarkdown from 'app/components/remove_markdown';
const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissBanner: PropTypes.func.isRequired,
}).isRequired,
allowDismissal: PropTypes.bool,
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
bannerText: PropTypes.string,
bannerTextColor: PropTypes.string,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
@ -52,30 +51,24 @@ export default class AnnouncementBanner extends PureComponent {
}
}
handleDismiss = () => {
const {actions, bannerText} = this.props;
actions.dismissBanner(bannerText);
};
handlePress = () => {
const {formatMessage} = this.context.intl;
const options = [{
text: formatMessage({id: 'mobile.announcement_banner.ok', defaultMessage: 'OK'}),
}];
const {navigator, theme} = this.props;
if (this.props.allowDismissal) {
options.push({
text: formatMessage({id: 'mobile.announcement_banner.dismiss', defaultMessage: 'Dismiss'}),
onPress: this.handleDismiss,
});
}
Alert.alert(
formatMessage({id: 'mobile.announcement_banner.title', defaultMessage: 'Announcement'}),
this.props.bannerText,
options,
{cancelable: false}
);
navigator.push({
screen: 'ExpandedAnnouncementBanner',
title: this.context.intl.formatMessage({
id: 'mobile.announcement_banner.title',
defaultMessage: 'Announcement',
}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
};
toggleBanner = (show = true) => {
@ -120,7 +113,7 @@ export default class AnnouncementBanner extends PureComponent {
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
{bannerText}
<RemoveMarkdown value={bannerText}/>
</Text>
<MaterialIcons
color={bannerTextColor}

View file

@ -12,10 +12,6 @@ jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
actions: {
dismissBanner: jest.fn(),
},
allowDismissal: true,
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,
@ -33,16 +29,4 @@ describe('AnnouncementBanner', () => {
wrapper.setProps({bannerEnabled: false});
expect(wrapper).toMatchSnapshot();
});
test('should call actions.dismissBanner on handleDismiss', () => {
const actions = {dismissBanner: jest.fn()};
const props = {...baseProps, actions};
const wrapper = shallow(
<AnnouncementBanner {...props}/>
);
wrapper.instance().handleDismiss();
expect(actions.dismissBanner).toHaveBeenCalledTimes(1);
expect(actions.dismissBanner).toHaveBeenCalledWith(props.bannerText);
});
});
});

View file

@ -1,12 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {dismissBanner} from 'app/actions/views/announcement';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import AnnouncementBanner from './announcement_banner';
@ -16,21 +14,13 @@ function mapStateToProps(state) {
const {announcement} = state.views;
return {
allowDismissal: config.AllowBannerDismissal === 'true',
bannerColor: config.BannerColor,
bannerDismissed: config.BannerText === announcement,
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
bannerText: config.BannerText,
bannerTextColor: config.BannerTextColor || '#000',
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissBanner,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);
export default connect(mapStateToProps)(AnnouncementBanner);

View file

@ -11,6 +11,7 @@ export default class ChannelLink extends React.PureComponent {
static propTypes = {
channelName: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style,
onChannelLinkPress: PropTypes.func,
textStyle: CustomPropTypes.Style,
channelsByName: PropTypes.object.isRequired,
actions: PropTypes.shape({
@ -57,6 +58,10 @@ export default class ChannelLink extends React.PureComponent {
handlePress = () => {
this.props.actions.setChannelDisplayName(this.state.channel.display_name);
this.props.actions.handleSelectChannel(this.state.channel.id);
if (this.props.onChannelLinkPress) {
this.props.onChannelLinkPress(this.state.channel);
}
}
render() {

View file

@ -39,6 +39,7 @@ export default class Markdown extends PureComponent {
isEdited: PropTypes.bool,
isSearchResult: PropTypes.bool,
navigator: PropTypes.object.isRequired,
onChannelLinkPress: PropTypes.func,
onLongPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
@ -194,6 +195,7 @@ export default class Markdown extends PureComponent {
<ChannelLink
linkStyle={this.props.textStyles.link}
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
onChannelLinkPress={this.props.onChannelLinkPress}
channelName={channelName}
/>
);

View file

@ -11,6 +11,10 @@ import {TextInput} from 'react-native';
// We're using this in place of a connected TextInput due to changes made in RN v0.54
// that break input in Chinese and Japanese when using a connected TextInput. See
// https://github.com/facebook/react-native/issues/18403 for more information.
//
// In addition to that, there's also an ugly hack to change the key on the TextInput
// when this is triggered because that same version made setNativeProps work inconsistently.
// See https://github.com/facebook/react-native/issues/18272 for more information on that.
export default class QuickTextInput extends React.PureComponent {
static propTypes = {
@ -31,16 +35,37 @@ export default class QuickTextInput extends React.PureComponent {
super(props);
this.storedValue = props.value;
this.state = {
key: 0,
};
}
componentDidMount() {
this.updateInputFromProps();
}
componentDidUpdate() {
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
// This will force the base TextInput to re-render if the value is changing
// from something other than the user typing in it. This does however cause
// the TextInput to flicker when this happens.
if (nextProps.value !== this.storedValue) {
this.setState({
key: this.state.key + 1,
});
}
this.hadFocus = this.input.isFocused();
}
componentDidUpdate(prevProps, prevState) {
if (this.props.value !== this.storedValue) {
this.updateInputFromProps();
}
if (prevState.key !== this.state.key && this.hadFocus) {
this.input.focus();
}
}
updateInputFromProps = () => {
@ -52,6 +77,14 @@ export default class QuickTextInput extends React.PureComponent {
this.storedValue = this.props.value;
}
setNativeProps(nativeProps) {
this.input.setNativeProps(nativeProps);
}
isFocused() {
return this.input.isFocused();
}
focus() {
this.input.focus();
}
@ -82,6 +115,7 @@ export default class QuickTextInput extends React.PureComponent {
return (
<TextInput
{...props}
key={this.state.key}
onChangeText={this.handleChangeText}
ref={this.setInput}
/>

View file

@ -17,34 +17,30 @@ export default class RadioButtonGroup extends PureComponent {
options: []
};
state = {};
constructor(props) {
super(props);
this.selected = null;
if (this.props.options.length) {
this.props.options.forEach((option) => {
const {
value,
checked,
} = option;
this.state = {
selected: this.getSelectedValue(props.options),
};
}
if (!this.state.selected && checked) {
this.selected = value;
}
});
componentWillReceiveProps(nextProps) {
if (this.props.options !== nextProps.options) {
this.setState({selected: this.getSelectedValue(nextProps.options)});
}
}
getSelectedValue = (options = []) => {
let selected;
for (const option in options) {
if (option.checked) {
selected = option.value;
break;
}
}
this.state = {selected: this.selected};
}
get value() {
return this.state.selected;
}
set value(value) {
this.onChange(value);
return selected;
}
onChange = (value) => {
@ -81,7 +77,7 @@ export default class RadioButtonGroup extends PureComponent {
label={label}
disabled={disabled}
onCheck={this.onChange}
checked={this.state.selected && value === this.state.selected}
checked={option.checked}
/>
);
});

View file

@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Parser} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import PropTypes from 'prop-types';
import React from 'react';
import {Text} from 'react-native';
export default class RemoveMarkdown extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.parser = this.createParser();
this.renderer = this.createRenderer();
}
createParser = () => {
return new Parser();
};
createRenderer = () => {
return new Renderer({
renderers: {
text: this.renderText,
emph: Renderer.forwardChildren,
strong: Renderer.forwardChildren,
del: Renderer.forwardChildren,
code: Renderer.forwardChildren,
link: Renderer.forwardChildren,
atMention: Renderer.forwardChildren,
channelLink: Renderer.forwardChildren,
emoji: this.renderNull,
paragraph: Renderer.forwardChildren,
heading: Renderer.forwardChildren,
codeBlock: this.renderNull,
blockQuote: this.renderNull,
list: this.renderNull,
item: this.renderNull,
hardBreak: this.renderNull,
thematicBreak: this.renderNull,
softBreak: this.renderNull,
htmlBlock: this.renderNull,
htmlInline: this.renderNull,
table: this.renderNull,
table_row: this.renderNull,
table_cell: this.renderNull,
},
});
};
renderText = ({literal}) => {
return <Text>{literal}</Text>;
};
renderNull = () => {
return null;
};
render() {
const ast = this.parser.parse(this.props.value);
return <Text>{this.renderer.render(ast)}</Text>;
}
}

View file

@ -114,7 +114,7 @@ class FilteredList extends Component {
channelId={channel.id}
channel={channel}
isSearchResult={true}
isUnread={false}
isUnread={channel.isUnread}
mentions={0}
onSelectChannel={this.onSelectChannel}
/>
@ -173,8 +173,11 @@ class FilteredList extends Component {
buildUnreadChannelsForSearch = (props, term) => {
const {unreadChannels} = props.channels;
return this.filterChannels(unreadChannels, term);
}
return this.filterChannels(unreadChannels, term).map((item) => {
item.isUnread = true;
return item;
});
};
buildCurrentDMSForSearch = (props, term) => {
const {channels, teammateNameDisplay, profiles, statuses, pastDirectMessages, groupChannelMemberDetails} = props;

View file

@ -49,7 +49,7 @@ function loadTranslation(locale) {
TRANSLATIONS.pl = require('assets/i18n/pl.json');
localeData = require('react-intl/locale-data/pl');
break;
case 'pt-BT':
case 'pt-BR':
TRANSLATIONS[locale] = require('assets/i18n/pt-BR.json');
localeData = require('react-intl/locale-data/pt');
break;

View file

@ -228,7 +228,7 @@ export default class ChannelPostList extends PureComponent {
return (
<View style={style.container}>
{component}
<AnnouncementBanner/>
<AnnouncementBanner navigator={navigator}/>
<RetryBarIndicator/>
</View>
);

View file

@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {ScrollView, View} from 'react-native';
import Button from 'react-native-button';
import FormattedText from 'app/components/formatted_text';
import Markdown from 'app/components/markdown';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ExpandedAnnouncementBanner extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissBanner: PropTypes.func.isRequired,
}).isRequired,
allowDismissal: PropTypes.bool.isRequired,
bannerText: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
}
close = () => {
this.props.navigator.pop();
};
dismissBanner = () => {
this.props.actions.dismissBanner(this.props.bannerText);
this.close();
};
handleChannelLinkPress = () => {
this.close();
};
render() {
const style = getStyleSheet(this.props.theme);
let dismissButton = null;
if (this.props.allowDismissal) {
dismissButton = (
<View style={style.dismissContainer}>
<Button
containerStyle={style.dismissButton}
onPress={this.dismissBanner}
>
<FormattedText
id='asdf'
defaultMessage={'Don\'t show again'}
style={style.dismissButtonText}
/>
</Button>
</View>
);
}
return (
<View style={style.container}>
<ScrollView
style={style.scrollContainer}
contentContainerStyle={style.textContainer}
>
<Markdown
baseTextStyle={style.baseTextStyle}
blockStyles={getMarkdownBlockStyles(this.props.theme)}
navigator={this.props.navigator}
onChannelLinkPress={this.handleChannelLinkPress}
textStyles={getMarkdownTextStyles(this.props.theme)}
value={this.props.bannerText}
/>
</ScrollView>
{dismissButton}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
},
scrollContainer: {
flex: 1,
},
textContainer: {
padding: 15,
},
baseTextStyle: {
color: theme.centerChannelColor,
fontSize: 15,
lineHeight: 20,
},
dismissContainer: {
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderTopWidth: 1,
padding: 10,
},
dismissButton: {
alignSelf: 'stretch',
backgroundColor: theme.sidebarHeaderBg,
borderRadius: 3,
padding: 15,
},
dismissButtonText: {
color: theme.sidebarHeaderTextColor,
fontSize: 15,
fontWeight: '600',
textAlign: 'center',
},
};
});

View file

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {dismissBanner} from 'app/actions/views/announcement';
import ExpandedAnnouncementBanner from './expanded_announcement_banner';
function mapStateToProps(state) {
const config = getConfig(state);
return {
allowDismissal: config.AllowBannerDismissal === 'true',
bannerText: config.BannerText,
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissBanner,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ExpandedAnnouncementBanner);

View file

@ -26,6 +26,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(require('app/screens/edit_post').default), store, Provider);
Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(require('app/screens/edit_profile').default), store, Provider);
Navigation.registerComponent('Entry', () => Entry, store, Provider);
Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapWithContextProvider(require('app/screens/expanded_announcement_banner').default), store, Provider);
Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(require('app/screens/image_preview').default), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider);

View file

@ -95,10 +95,17 @@ class MoreDirectMessages extends PureComponent {
nextProps.getRequest.status === RequestStatus.SUCCESS) {
const profiles = this.sliceProfiles(nextProps.profiles);
this.setState({profiles, showNoResults: true});
} else if (this.state.searching &&
nextProps.searchRequest.status === RequestStatus.SUCCESS) {
} else if (
this.state.searching &&
nextProps.searchRequest.status === RequestStatus.SUCCESS
) {
let profiles = nextProps.profiles;
if (this.state.selectedCount > 0) {
profiles = this.removeCurrentUserFromProfiles(profiles);
}
const exactMatches = [];
let results = filterProfilesMatchingTerm(nextProps.profiles, this.state.term).filter((p) => {
const results = filterProfilesMatchingTerm(profiles, this.state.term).filter((p) => {
if (p.username === this.state.term || p.username.startsWith(this.state.term)) {
exactMatches.push(p);
return false;
@ -107,10 +114,6 @@ class MoreDirectMessages extends PureComponent {
return true;
});
if (this.state.selectedCount > 0) {
results = this.removeCurrentUserFromProfiles(results);
}
this.setState({profiles: [...exactMatches, ...results], showNoResults: true});
}
}

View file

@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {getChannel as getChannelAction, joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getPostsAfter, getPostsBefore, getPostThread, selectPost} from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
import {makeGetChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {makeGetPostIdsAroundPost, getPost} from 'mattermost-redux/selectors/entities/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@ -29,11 +30,13 @@ function makeMapStateToProps() {
return function mapStateToProps(state) {
const {currentFocusedPostId} = state.entities.posts;
const post = getPost(state, currentFocusedPostId);
const channel = post ? getChannel(state, {id: post.channel_id}) : null;
let channel;
let postIds;
if (channel && channel.id) {
postIds = getPostIdsAroundPost(state, currentFocusedPostId, channel.id, {
if (post && post.delete_at === 0 && post.state !== Posts.POST_DELETED) {
channel = getChannel(state, {id: post.channel_id});
postIds = getPostIdsAroundPost(state, currentFocusedPostId, post.channel_id, {
postsBeforeCount: 10,
postsAfterCount: 10,
});

View file

@ -6,19 +6,14 @@ import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Alert,
Modal,
Platform,
ScrollView,
TouchableOpacity,
View,
} from 'react-native';
import deepEqual from 'deep-equal';
import {General, Preferences, RequestStatus} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import {General, RequestStatus} from 'mattermost-redux/constants';
import FormattedText from 'app/components/formatted_text';
import RadioButtonGroup from 'app/components/radio_button';
import StatusBar from 'app/components/status_bar';
import NotificationPreferences from 'app/notification_preferences';
import SettingsItem from 'app/screens/settings/settings_item';
@ -31,10 +26,8 @@ class NotificationSettings extends PureComponent {
actions: PropTypes.shape({
handleUpdateUserNotifyProps: PropTypes.func.isRequired,
}),
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
intl: intlShape.isRequired,
myPreferences: PropTypes.object.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
updateMeRequest: PropTypes.object.isRequired,
@ -42,10 +35,6 @@ class NotificationSettings extends PureComponent {
enableAutoResponder: PropTypes.bool.isRequired,
};
state = {
showEmailNotificationsModal: false,
};
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
@ -94,30 +83,25 @@ class NotificationSettings extends PureComponent {
};
goToNotificationSettingsEmail = () => {
if (Platform.OS === 'ios') {
const {currentUser, intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'NotificationSettingsEmail',
title: intl.formatMessage({
id: 'mobile.notification_settings.email_title',
defaultMessage: 'Email Notifications',
}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
currentUser,
onBack: this.saveNotificationProps,
},
});
} else {
this.setState({showEmailNotificationsModal: true});
}
const {currentUser, intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'NotificationSettingsEmail',
title: intl.formatMessage({
id: 'mobile.notification_settings.email_title',
defaultMessage: 'Email Notifications',
}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
passProps: {
currentUserId: currentUser.id,
},
});
};
goToNotificationSettingsMentions = () => {
@ -168,34 +152,6 @@ class NotificationSettings extends PureComponent {
});
};
setEmailNotifications = (emailSetting) => {
this.setState({emailSetting});
};
saveEmailNotifications = () => {
const {config, currentUser} = this.props;
const {emailSetting} = this.state;
this.setState({showEmailNotificationsModal: false});
if (emailSetting) {
let email = emailSetting;
let interval;
const emailBatchingEnabled = config.EnableEmailBatching === 'true';
if (emailBatchingEnabled && emailSetting !== 'false') {
interval = emailSetting;
email = 'true';
}
this.saveNotificationProps({
...getNotificationProps(currentUser),
email,
interval,
});
}
};
saveNotificationProps = (notifyProps) => {
const {currentUser} = this.props;
const {user_id: userId} = notifyProps;
@ -204,10 +160,6 @@ class NotificationSettings extends PureComponent {
user_id: userId,
};
if (notifyProps.interval) {
previousProps.interval = notifyProps.interval;
}
if (!deepEqual(previousProps, notifyProps)) {
this.props.actions.handleUpdateUserNotifyProps(notifyProps);
}
@ -247,154 +199,6 @@ class NotificationSettings extends PureComponent {
}
};
renderEmailNotificationSettings = (style) => {
if (Platform.OS === 'ios') {
return null;
}
const {config, currentUser, intl, myPreferences} = this.props;
const notifyProps = getNotificationProps(currentUser);
const sendEmailNotifications = config.SendEmailNotifications === 'true';
const emailBatchingEnabled = sendEmailNotifications && config.EnableEmailBatching === 'true';
const never = notifyProps.email === 'false';
let sendImmediatley = notifyProps.email === 'true';
let sendImmediatleyValue = 'true';
let fifteenMinutes;
let hourly;
let interval;
if (emailBatchingEnabled) {
const emailPreferences = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_NOTIFICATIONS);
if (emailPreferences.size) {
interval = emailPreferences.get(Preferences.EMAIL_INTERVAL).value;
}
}
if (emailBatchingEnabled && notifyProps.email !== 'false') {
sendImmediatley = interval === Preferences.INTERVAL_IMMEDIATE.toString();
fifteenMinutes = interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString();
hourly = interval === Preferences.INTERVAL_HOUR.toString();
sendImmediatleyValue = Preferences.INTERVAL_IMMEDIATE.toString();
}
let helpText;
if (sendEmailNotifications) {
helpText = (
<FormattedText
id='user.settings.notifications.emailInfo'
defaultMessage='Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes.'
values={{siteName: config.SiteName}}
style={style.modalHelpText}
/>
);
}
const emailOptions = [{
label: intl.formatMessage({
id: 'user.settings.notifications.email.immediately',
defaultMessage: 'Immediately',
}),
value: sendImmediatleyValue,
checked: sendImmediatley,
}];
if (emailBatchingEnabled) {
emailOptions.push({
label: intl.formatMessage({
id: 'user.settings.notifications.email.everyXMinutes',
defaultMessage: 'Every {count, plural, one {minute} other {{count, number} minutes}}',
}, {count: Preferences.INTERVAL_FIFTEEN_MINUTES / 60}),
value: Preferences.INTERVAL_FIFTEEN_MINUTES.toString(),
checked: fifteenMinutes,
}, {
label: intl.formatMessage({
id: 'user.settings.notifications.email.everyHour',
defaultMessage: 'Every hour',
}),
value: Preferences.INTERVAL_HOUR.toString(),
checked: hourly,
});
}
emailOptions.push({
label: intl.formatMessage({
id: 'user.settings.notifications.email.never',
defaultMessage: 'Never',
}),
value: 'false',
checked: never,
});
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showEmailNotificationsModal}
onRequestClose={() => this.setState({showEmailNotificationsModal: false})}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='user.settings.notifications.email.send'
defaultMessage='Send email notifications'
style={style.modalTitle}
/>
</View>
{!sendEmailNotifications &&
<FormattedText
id='user.settings.general.emailHelp2'
defaultMessage='Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.'
style={style.modalOptionDisabled}
/>
}
{sendEmailNotifications &&
<RadioButtonGroup
name='emailSettings'
onSelect={this.setEmailNotifications}
options={emailOptions}
/>
}
{helpText}
</View>
<View style={style.modalFooter}>
<View style={style.divider}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveEmailNotifications}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
{sendEmailNotifications &&
<View>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveEmailNotifications}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
}
</View>
</View>
</View>
</View>
</Modal>
);
};
render() {
const {theme, enableAutoResponder} = this.props;
const style = getStyleSheet(theme);
@ -458,7 +262,6 @@ class NotificationSettings extends PureComponent {
{autoResponder}
<View style={style.divider}/>
</ScrollView>
{this.renderEmailNotificationSettings(style)}
</View>
);
}
@ -483,60 +286,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: 1,
width: '100%',
},
modalOverlay: {
backgroundColor: changeOpacity('#000000', 0.6),
alignItems: 'center',
flex: 1,
},
modal: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
marginTop: 20,
width: '95%',
},
modalBody: {
paddingHorizontal: 24,
},
modalTitleContainer: {
marginBottom: 30,
marginTop: 20,
},
modalTitle: {
color: theme.centerChannelColor,
fontSize: 19,
},
modalOptionDisabled: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17,
},
modalHelpText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 13,
marginTop: 20,
},
modalFooter: {
alignItems: 'flex-end',
height: 58,
marginTop: 40,
width: '100%',
},
modalFooterContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingRight: 24,
},
modalFooterOptionContainer: {
alignItems: 'center',
height: 40,
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 5,
},
modalFooterOption: {
color: theme.linkColor,
fontSize: 14,
},
};
});

View file

@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotificationSettingsMobileAndroid should match snapshot 1`] = `
<sectionItem
action={[Function]}
actionType="default"
description={
<InjectIntl(FormattedText)
defaultMessage="Immediately"
id="user.settings.notifications.email.immediately"
/>
}
label={
<InjectIntl(FormattedText)
defaultMessage="Send email notifications"
id="user.settings.notifications.email.send"
/>
}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
}
}
/>
`;
exports[`NotificationSettingsMobileAndroid should match snapshot 2`] = `
<Component
animationType="slide"
hardwareAccelerated={false}
onRequestClose={[Function]}
transparent={true}
visible={false}
>
<Component
style={undefined}
>
<Component
style={Object {}}
>
<Component
style={Object {}}
>
<Component
style={Object {}}
>
<InjectIntl(FormattedText)
defaultMessage="Send email notifications"
id="user.settings.notifications.email.send"
style={Object {}}
/>
</Component>
<RadioButtonGroup
name="emailSettings"
onSelect={[Function]}
options={
Array [
Object {
"checked": true,
"label": "Immediately",
"value": "30",
},
Object {
"checked": false,
"label": "Never",
"value": "0",
},
]
}
/>
<InjectIntl(FormattedText)
defaultMessage="Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes."
id="user.settings.notifications.emailInfo"
style={Object {}}
values={
Object {
"siteName": "Mattermost",
}
}
/>
</Component>
<Component
style={Object {}}
>
<Component
style={Object {}}
/>
<Component
style={Object {}}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={Object {}}
>
<InjectIntl(FormattedText)
defaultMessage="CANCEL"
id="mobile.notification_settings.modal_cancel"
style={Object {}}
/>
</TouchableOpacity>
<Component>
<Component
style={
Object {
"marginRight": 10,
}
}
/>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={Object {}}
>
<InjectIntl(FormattedText)
defaultMessage="SAVE"
id="mobile.notification_settings.modal_save"
style={Object {}}
/>
</TouchableOpacity>
</Component>
</Component>
</Component>
</Component>
</Component>
</Component>
`;

View file

@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotificationSettingsEmailIos should match snapshot, renderEmailSection 1`] = `
<section
disableFooter={false}
footerDefaultMessage="Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes."
footerId="user.settings.notifications.emailInfo"
footerValues={
Object {
"siteName": "Mattermost",
}
}
headerDefaultMessage="SEND EMAIL NOTIFICATIONS"
headerId="mobile.notification_settings.email.send"
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
}
}
>
<Component>
<sectionItem
action={[Function]}
actionType="select"
actionValue="30"
label={
<InjectIntl(FormattedText)
defaultMessage="Immediately"
id="user.settings.notifications.email.immediately"
/>
}
selected={true}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
}
}
/>
<Component
style={
Object {
"backgroundColor": undefined,
"flex": 1,
"height": 1,
"marginLeft": 15,
}
}
/>
<sectionItem
action={[Function]}
actionType="select"
actionValue="0"
label={
<InjectIntl(FormattedText)
defaultMessage="Never"
id="user.settings.notifications.email.never"
/>
}
selected={false}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
}
}
/>
</Component>
</section>
`;

View file

@ -1,19 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {
get as getPreference,
getTheme,
} from 'mattermost-redux/selectors/entities/preferences';
import NotificationSettingsEmail from './notification_settings_email';
function mapStateToProps(state) {
const config = getConfig(state);
const sendEmailNotifications = config.SendEmailNotifications === 'true';
const enableEmailBatching = config.EnableEmailBatching === 'true';
const emailInterval = getPreference(
state,
Preferences.CATEGORY_NOTIFICATIONS,
Preferences.EMAIL_INTERVAL,
Preferences.INTERVAL_NEVER
).toString();
return {
config: getConfig(state),
myPreferences: getMyPreferences(state),
enableEmailBatching,
emailInterval,
sendEmailNotifications,
siteName: config.siteName || '',
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(NotificationSettingsEmail);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
savePreferences,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(NotificationSettingsEmail);

View file

@ -0,0 +1,321 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {intlShape} from 'react-intl';
import {
Modal,
ScrollView,
TouchableOpacity,
View,
} from 'react-native';
import {Preferences} from 'mattermost-redux/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
import RadioButtonGroup from 'app/components/radio_button';
import SectionItem from 'app/screens/settings/section_item';
import NotificationSettingsEmailBase from './notification_settings_email_base';
class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase {
static contextTypes = {
intl: intlShape,
};
handleClose = () => {
this.setState({showEmailNotificationsModal: false});
}
handleSaveEmailNotification = () => {
this.saveEmailNotifyProps();
this.handleClose();
};
showEmailModal = () => {
this.setState({showEmailNotificationsModal: true});
};
renderEmailSection() {
const {
sendEmailNotifications,
theme,
} = this.props;
const {interval} = this.state;
let i18nId;
let i18nMessage;
if (sendEmailNotifications) {
switch (interval) {
case Preferences.INTERVAL_IMMEDIATE.toString():
i18nId = 'user.settings.notifications.email.immediately';
i18nMessage = 'Immediately';
break;
case Preferences.INTERVAL_HOUR.toString():
i18nId = 'user.settings.notifications.email.everyHour';
i18nMessage = 'Every hour';
break;
case Preferences.INTERVAL_FIFTEEN_MINUTES.toString():
i18nId = 'mobile.user.settings.notifications.email.fifteenMinutes';
i18nMessage = 'Every 15 minutes';
break;
case Preferences.INTERVAL_NEVER.toString():
default:
i18nId = 'user.settings.notifications.email.never';
i18nMessage = 'Never';
break;
}
} else {
i18nId = 'user.settings.notifications.email.disabled';
i18nMessage = 'Email notifications are not enabled';
}
return (
<SectionItem
description={(
<FormattedText
id={i18nId}
defaultMessage={i18nMessage}
/>
)}
label={(
<FormattedText
id='user.settings.notifications.email.send'
defaultMessage='Send email notifications'
/>
)}
action={this.showEmailModal}
actionType='default'
theme={theme}
/>
);
}
renderEmailNotificationsModal(style) {
const {intl} = this.context;
const {
enableEmailBatching,
sendEmailNotifications,
siteName,
} = this.props;
const {interval} = this.state;
let helpText;
if (sendEmailNotifications) {
helpText = (
<FormattedText
id='user.settings.notifications.emailInfo'
defaultMessage='Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes.'
values={{siteName}}
style={style.modalHelpText}
/>
);
}
const emailOptions = [{
label: intl.formatMessage({
id: 'user.settings.notifications.email.immediately',
defaultMessage: 'Immediately',
}),
value: Preferences.INTERVAL_IMMEDIATE.toString(),
checked: interval === Preferences.INTERVAL_IMMEDIATE.toString(),
}];
if (enableEmailBatching) {
emailOptions.push({
label: intl.formatMessage({
id: 'mobile.user.settings.notifications.email.fifteenMinutes',
defaultMessage: 'Every 15 minutes',
}),
value: Preferences.INTERVAL_FIFTEEN_MINUTES.toString(),
checked: interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString(),
}, {
label: intl.formatMessage({
id: 'user.settings.notifications.email.everyHour',
defaultMessage: 'Every hour',
}),
value: Preferences.INTERVAL_HOUR.toString(),
checked: interval === Preferences.INTERVAL_HOUR.toString(),
});
}
emailOptions.push({
label: intl.formatMessage({
id: 'user.settings.notifications.email.never',
defaultMessage: 'Never',
}),
value: Preferences.INTERVAL_NEVER.toString(),
checked: interval === Preferences.INTERVAL_NEVER.toString(),
});
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showEmailNotificationsModal}
onRequestClose={this.handleClose}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='user.settings.notifications.email.send'
defaultMessage='Send email notifications'
style={style.modalTitle}
/>
</View>
{!sendEmailNotifications &&
<FormattedText
id='user.settings.general.emailHelp2'
defaultMessage='Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.'
style={style.modalOptionDisabled}
/>
}
{sendEmailNotifications &&
<RadioButtonGroup
name='emailSettings'
onSelect={this.setEmailNotifications}
options={emailOptions}
/>
}
{helpText}
</View>
<View style={style.modalFooter}>
<View style={style.divider}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.handleClose}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
{sendEmailNotifications &&
<View>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.handleSaveEmailNotification}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
}
</View>
</View>
</View>
</View>
</Modal>
);
}
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
alwaysBounceVertical={false}
>
{this.renderEmailSection()}
<View style={style.separator}/>
{this.renderEmailNotificationsModal(style)}
</ScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1,
width: '100%',
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1,
width: '100%',
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
},
scrollViewContent: {
paddingVertical: 0,
},
modalOverlay: {
backgroundColor: changeOpacity('#000000', 0.6),
alignItems: 'center',
flex: 1,
},
modal: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
marginTop: 20,
width: '95%',
},
modalBody: {
paddingHorizontal: 24,
},
modalTitleContainer: {
marginBottom: 30,
marginTop: 20,
},
modalTitle: {
color: theme.centerChannelColor,
fontSize: 19,
},
modalOptionDisabled: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17,
},
modalHelpText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 13,
marginTop: 20,
},
modalFooter: {
alignItems: 'flex-end',
height: 58,
marginTop: 40,
width: '100%',
},
modalFooterContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingRight: 24,
},
modalFooterOptionContainer: {
alignItems: 'center',
height: 40,
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 5,
},
modalFooterOption: {
color: theme.linkColor,
fontSize: 14,
},
};
});
export default NotificationSettingsEmailAndroid;

View file

@ -0,0 +1,102 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import {shallowWithIntl} from 'test/intl-test-helper';
import NotificationSettingsMobileAndroid from './notification_settings_email.android.js';
describe('NotificationSettingsMobileAndroid', () => {
const baseProps = {
currentUserId: 'current_user_id',
emailInterval: '30',
enableEmailBatching: false,
navigator: {setOnNavigatorEvent: () => {}}, // eslint-disable-line no-empty-function
actions: {
savePreferences: () => {}, // eslint-disable-line no-empty-function
},
sendEmailNotifications: true,
siteName: 'Mattermost',
theme: {
centerChannelBg: '#aaa',
centerChannelColor: '#aaa',
},
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsMobileAndroid {...baseProps}/>
);
const style = {
divider: {},
modal: {},
modalBody: {},
modalTitleContainer: {},
modalTitle: {},
modalOptionDisabled: {},
modalHelpText: {},
modalFooter: {},
modalFooterContainer: {},
modalFooterOptionContainer: {},
modalFooterOption: {},
};
expect(wrapper.instance().renderEmailSection()).toMatchSnapshot();
expect(wrapper.instance().renderEmailNotificationsModal(style)).toMatchSnapshot();
});
test('should match state on setEmailNotifications', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsMobileAndroid {...baseProps}/>
);
wrapper.setState({email: 'false', interval: '0'});
wrapper.instance().setEmailNotifications('30');
expect(wrapper.state({email: 'true', interval: '30'}));
wrapper.instance().setEmailNotifications('0');
expect(wrapper.state({email: 'false', interval: '0'}));
wrapper.instance().setEmailNotifications('3600');
expect(wrapper.state({email: 'true', interval: '3600'}));
});
test('should match state on handleClose', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsMobileAndroid {...baseProps}/>
);
wrapper.setState({showEmailNotificationsModal: true});
wrapper.instance().handleClose();
expect(wrapper.state('showEmailNotificationsModal')).toEqual(false);
});
test('should saveEmailNotifyProps and handleClose on handleSaveEmailNotification', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsMobileAndroid {...baseProps}/>
);
const instance = wrapper.instance();
instance.saveEmailNotifyProps = jest.fn();
instance.handleClose = jest.fn();
instance.handleSaveEmailNotification();
expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(1);
expect(instance.handleClose).toHaveBeenCalledTimes(1);
});
test('should match state on showEmailModal', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsMobileAndroid {...baseProps}/>
);
wrapper.setState({showEmailNotificationsModal: false});
wrapper.instance().showEmailModal();
expect(wrapper.state('showEmailNotificationsModal')).toEqual(true);
});
});

View file

@ -1,8 +1,7 @@
// 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 React from 'react';
import {
ScrollView,
@ -10,120 +9,34 @@ import {
} from 'react-native';
import {Preferences} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {getNotificationProps} from 'app/utils/notify_props';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import Section from 'app/screens/settings/section';
import SectionItem from 'app/screens/settings/section_item';
export default class NotificationSettingsEmail extends PureComponent {
static propTypes = {
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
myPreferences: PropTypes.object.isRequired,
navigator: PropTypes.object,
onBack: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
import NotificationSettingsEmailBase from './notification_settings_email_base';
constructor(props) {
super(props);
const {currentUser} = props;
const notifyProps = getNotificationProps(currentUser);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.state = this.setStateFromNotifyProps(notifyProps);
}
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
}
}
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent') {
switch (event.id) {
case 'willDisappear':
this.saveUserNotifyProps();
break;
}
}
};
setEmailNotifications = (value) => {
const {config} = this.props;
let email = value;
let interval;
const emailBatchingEnabled = config.EnableEmailBatching === 'true';
if (emailBatchingEnabled && value !== 'false') {
interval = value;
email = 'true';
}
this.setState({
email,
interval,
});
};
setStateFromNotifyProps = (notifyProps) => {
const {config, myPreferences} = this.props;
let interval;
if (config.SendEmailNotifications === 'true' && config.EnableEmailBatching === 'true') {
const emailPreferences = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_NOTIFICATIONS);
if (emailPreferences.size) {
interval = emailPreferences.get(Preferences.EMAIL_INTERVAL).value;
}
}
return {
...notifyProps,
interval,
};
};
saveUserNotifyProps = () => {
this.props.onBack({
...this.state,
user_id: this.props.currentUser.id,
});
};
renderEmailSection = () => {
const {config, theme} = this.props;
class NotificationSettingsEmailIos extends NotificationSettingsEmailBase {
renderEmailSection() {
const {
enableEmailBatching,
sendEmailNotifications,
siteName,
theme,
} = this.props;
const {interval} = this.state;
const style = getStyleSheet(theme);
const sendEmailNotifications = config.SendEmailNotifications === 'true';
const emailBatchingEnabled = sendEmailNotifications && config.EnableEmailBatching === 'true';
let sendImmediatley = this.state.email === 'true';
let sendImmediatleyValue = 'true';
let fifteenMinutes;
let hourly;
const never = this.state.email === 'false';
if (emailBatchingEnabled && this.state.email !== 'false') {
sendImmediatley = this.state.interval === Preferences.INTERVAL_IMMEDIATE.toString();
fifteenMinutes = this.state.interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString();
hourly = this.state.interval === Preferences.INTERVAL_HOUR.toString();
sendImmediatleyValue = Preferences.INTERVAL_IMMEDIATE.toString();
}
return (
<Section
headerId='mobile.notification_settings.email.send'
headerDefaultMessage='SEND EMAIL NOTIFICATIONS'
footerId='user.settings.notifications.emailInfo'
footerDefaultMessage='Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes.'
footerValues={{siteName: config.SiteName}}
footerValues={{siteName}}
disableFooter={!sendEmailNotifications}
theme={theme}
>
@ -138,25 +51,24 @@ export default class NotificationSettingsEmail extends PureComponent {
)}
action={this.setEmailNotifications}
actionType='select'
actionValue={sendImmediatleyValue}
selected={sendImmediatley}
actionValue={Preferences.INTERVAL_IMMEDIATE.toString()}
selected={interval === Preferences.INTERVAL_IMMEDIATE.toString()}
theme={theme}
/>
<View style={style.separator}/>
{emailBatchingEnabled &&
{enableEmailBatching &&
<View>
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.email.everyXMinutes'
defaultMessage='Every {count, plural, one {minute} other {{count, number} minutes}}'
values={{count: Preferences.INTERVAL_FIFTEEN_MINUTES / 60}}
id='mobile.user.settings.notifications.email.fifteenMinutes'
defaultMessage='Every 15 minutes'
/>
)}
action={this.setEmailNotifications}
actionType='select'
actionValue={Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
selected={fifteenMinutes}
selected={interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
theme={theme}
/>
<View style={style.separator}/>
@ -170,7 +82,7 @@ export default class NotificationSettingsEmail extends PureComponent {
action={this.setEmailNotifications}
actionType='select'
actionValue={Preferences.INTERVAL_HOUR.toString()}
selected={hourly}
selected={interval === Preferences.INTERVAL_HOUR.toString()}
theme={theme}
/>
<View style={style.separator}/>
@ -185,8 +97,8 @@ export default class NotificationSettingsEmail extends PureComponent {
)}
action={this.setEmailNotifications}
actionType='select'
actionValue='false'
selected={never}
actionValue={Preferences.INTERVAL_NEVER.toString()}
selected={interval === Preferences.INTERVAL_NEVER.toString()}
theme={theme}
/>
</View>
@ -200,7 +112,7 @@ export default class NotificationSettingsEmail extends PureComponent {
}
</Section>
);
};
}
render() {
const {theme} = this.props;
@ -227,11 +139,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40,
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
@ -253,3 +160,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
};
});
export default NotificationSettingsEmailIos;

View file

@ -0,0 +1,89 @@
// 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';
configure({adapter: new Adapter()});
import NotificationSettingsEmailIos from './notification_settings_email.ios.js';
jest.mock('app/utils/theme', () => {
const original = require.requireActual('app/utils/theme');
return {
...original,
changeOpacity: jest.fn(),
};
});
describe('NotificationSettingsEmailIos', () => {
const baseProps = {
currentUserId: 'current_user_id',
emailInterval: '30',
enableEmailBatching: false,
navigator: {setOnNavigatorEvent: () => {}}, // eslint-disable-line no-empty-function
actions: {
savePreferences: () => {}, // eslint-disable-line no-empty-function
},
sendEmailNotifications: true,
siteName: 'Mattermost',
theme: {
centerChannelBg: '#aaa',
centerChannelColor: '#aaa',
},
};
test('should match snapshot, renderEmailSection', () => {
const wrapper = shallow(
<NotificationSettingsEmailIos {...baseProps}/>
);
expect(wrapper.instance().renderEmailSection()).toMatchSnapshot();
});
test('should call saveEmailNotifyProps on onNavigatorEvent', () => {
const wrapper = shallow(
<NotificationSettingsEmailIos {...baseProps}/>
);
const instance = wrapper.instance();
instance.saveEmailNotifyProps = jest.fn();
instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'});
expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(1);
});
test('should macth state on setEmailNotifications', () => {
const wrapper = shallow(
<NotificationSettingsEmailIos {...baseProps}/>
);
wrapper.setState({email: 'false', interval: '0'});
wrapper.instance().setEmailNotifications('30');
expect(wrapper.state({email: 'true', interval: '30'}));
wrapper.instance().setEmailNotifications('0');
expect(wrapper.state({email: 'false', interval: '0'}));
wrapper.instance().setEmailNotifications('3600');
expect(wrapper.state({email: 'true', interval: '3600'}));
});
test('should call props.actions.savePreferences on saveUserNotifyProps', () => {
const props = {...baseProps, actions: {savePreferences: jest.fn()}};
const wrapper = shallow(
<NotificationSettingsEmailIos {...props}/>
);
wrapper.setState({email: 'true', interval: '3600'});
wrapper.instance().saveEmailNotifyProps();
expect(props.actions.savePreferences).toHaveBeenCalledTimes(1);
expect(props.actions.savePreferences).toBeCalledWith(
'current_user_id',
[
{category: 'notifications', name: 'email', user_id: 'current_user_id', value: 'true'},
{category: 'notifications', name: 'email_interval', user_id: 'current_user_id', value: '3600'},
]
);
});
});

View file

@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Preferences} from 'mattermost-redux/constants';
import {getEmailInterval} from 'mattermost-redux/utils/notify_props';
import {setNavigatorStyles} from 'app/utils/theme';
export default class NotificationSettingsEmailBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
savePreferences: PropTypes.func.isRequired,
}),
currentUserId: PropTypes.string.isRequired,
emailInterval: PropTypes.string.isRequired,
enableEmailBatching: PropTypes.bool.isRequired,
navigator: PropTypes.object,
sendEmailNotifications: PropTypes.bool.isRequired,
siteName: PropTypes.string,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
const {
emailInterval,
enableEmailBatching,
navigator,
sendEmailNotifications,
} = props;
this.state = {
interval: getEmailInterval(
sendEmailNotifications,
enableEmailBatching,
parseInt(emailInterval, 10),
).toString(),
showEmailNotificationsModal: false,
};
navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
}
if (
this.props.sendEmailNotifications !== nextProps.sendEmailNotifications ||
this.props.enableEmailBatching !== nextProps.enableEmailBatching ||
this.props.emailInterval !== nextProps.emailInterval
) {
this.setState({
interval: getEmailInterval(
nextProps.sendEmailNotifications,
nextProps.enableEmailBatching,
parseInt(nextProps.emailInterval, 10),
).toString(),
});
}
}
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent') {
switch (event.id) {
case 'willDisappear':
this.saveEmailNotifyProps();
break;
}
}
};
setEmailNotifications = (interval) => {
const {sendEmailNotifications} = this.props;
let email = 'false';
if (sendEmailNotifications && interval !== Preferences.INTERVAL_NEVER.toString()) {
email = 'true';
}
this.setState({
email,
interval,
});
};
saveEmailNotifyProps = () => {
const {currentUserId} = this.props;
const {email, interval} = this.state;
const emailNotify = {category: Preferences.CATEGORY_NOTIFICATIONS, user_id: currentUserId, name: 'email', value: email};
const emailInterval = {category: Preferences.CATEGORY_NOTIFICATIONS, user_id: currentUserId, name: Preferences.EMAIL_INTERVAL, value: interval};
this.props.actions.savePreferences(currentUserId, [emailNotify, emailInterval]);
};
}

View file

@ -20,7 +20,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import NotificationSettingsMentionsBase from './notification_settings_mention_base';
class NotificationSettingsMentionsIos extends NotificationSettingsMentionsBase {
class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBase {
cancelMentionKeys = () => {
this.setState({showKeywordsModal: false});
this.keywords = this.state.mention_keys;
@ -442,4 +442,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
};
});
export default injectIntl(NotificationSettingsMentionsIos);
export default injectIntl(NotificationSettingsMentionsAndroid);

View file

@ -47,10 +47,6 @@ export function makePreparePostIdsForPostList() {
continue;
}
if (post.state === Posts.POST_DELETED && post.user_id === currentUser.id) {
continue;
}
// Filter out join/leave messages if necessary
if (shouldFilterJoinLeavePost(post, showJoinLeave, currentUser.username)) {
continue;

View file

@ -51,7 +51,7 @@ const setTransforms = [
export default function configureAppStore(initialState) {
const viewsBlackListFilter = createBlacklistFilter(
'views',
['announcement', 'extension', 'login', 'root']
['extension', 'login', 'root']
);
const typingBlackListFilter = createBlacklistFilter(

View file

@ -2307,7 +2307,6 @@
"mobile.android.videos_permission_denied_description": "To upload videos from your library, please change your permission settings.",
"mobile.android.videos_permission_denied_title": "Video library access is required",
"mobile.announcement_banner.dismiss": "Dismiss",
"mobile.announcement_banner.ok": "OK",
"mobile.announcement_banner.title": "Announcement",
"mobile.channel.markAsRead": "Mark As Read",
"mobile.channel_drawer.search": "Jump to...",
@ -3319,6 +3318,7 @@
"user.settings.notifications.email.disabled_long": "Email notifications have not been enabled by your System Administrator.",
"user.settings.notifications.email.everyHour": "Every hour",
"user.settings.notifications.email.everyXMinutes": "Every {count, plural, one {minute} other {{count, number} minutes}}",
"mobile.user.settings.notifications.email.fifteenMinutes": "Every 15 minutes",
"user.settings.notifications.email.immediately": "Immediately",
"user.settings.notifications.email.never": "Never",
"user.settings.notifications.email.send": "Send email notifications",

View file

@ -2532,7 +2532,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 119;
CURRENT_PROJECT_VERSION = 123;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@ -2582,7 +2582,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 119;
CURRENT_PROJECT_VERSION = 123;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View file

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

View file

@ -23,7 +23,7 @@
<key>CFBundleShortVersionString</key>
<string>1.10.0</string>
<key>CFBundleVersion</key>
<string>119</string>
<string>123</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

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

View file

@ -43,7 +43,6 @@ export default function errorMessage(props) {
errorMessage.propTypes = {
close: PropTypes.func.isRequired,
formatMessage: PropTypes.func.isRequired,
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {

View file

@ -7,6 +7,7 @@ import {Animated, Dimensions, NavigatorIOS, StyleSheet, View} from 'react-native
import {Preferences} from 'mattermost-redux/constants';
import initialState from 'app/initial_state';
import mattermostBucket from 'app/mattermost_bucket';
import ExtensionPost from './extension_post';
@ -32,7 +33,7 @@ export default class SharedApp extends PureComponent {
};
mattermostBucket.readFromFile('entities', props.appGroupId).then((value) => {
this.entities = value;
this.entities = value || initialState;
this.setState({init: true});
});
}

View file

@ -232,7 +232,7 @@ export default class ExtensionPost extends PureComponent {
let totalSize = 0;
let exceededSize = false;
if (channel.type === General.GM_CHANNEL || channel.type === General.DM_CHANNEL) {
if (channel && (channel.type === General.GM_CHANNEL || channel.type === General.DM_CHANNEL)) {
channel = getChannel({entities}, channel.id);
}
@ -590,7 +590,7 @@ export default class ExtensionPost extends PureComponent {
const {currentUserId} = entities.users;
// If no text and no files do nothing
if (!value && !files.length) {
if ((!value && !files.length) || !channel) {
return;
}