Merge pull request #1888 from mattermost/merge-1.10
Merge release 1.10 into master
This commit is contained in:
commit
5531d25b7c
39 changed files with 1280 additions and 529 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
75
app/components/remove_markdown.js
Normal file
75
app/components/remove_markdown.js
Normal 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>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export default class ChannelPostList extends PureComponent {
|
|||
return (
|
||||
<View style={style.container}>
|
||||
{component}
|
||||
<AnnouncementBanner/>
|
||||
<AnnouncementBanner navigator={navigator}/>
|
||||
<RetryBarIndicator/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
});
|
||||
32
app/screens/expanded_announcement_banner/index.js
Normal file
32
app/screens/expanded_announcement_banner/index.js
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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'},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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]);
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>119</string>
|
||||
<string>123</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@
|
|||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>119</string>
|
||||
<string>123</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ export default function errorMessage(props) {
|
|||
|
||||
errorMessage.propTypes = {
|
||||
close: PropTypes.func.isRequired,
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue