diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap
index 899c6c738..dfb411f45 100644
--- a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap
+++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap
@@ -268,6 +268,69 @@ exports[`channelInfo should match snapshot 1`] = `
}
}
/>
+
+
+
+
);
} else {
@@ -72,7 +75,9 @@ function channelInfoRow(props) {
const RowComponent = (
- {iconElement}
+
+ {iconElement}
+
{
fontSize: 15,
paddingVertical: 15,
},
+ iconContainer: {
+ width: 17,
+ height: 17,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
leftIcon: {
width: 17,
},
diff --git a/app/screens/channel_info/notification_preference/index.js b/app/screens/channel_info/notification_preference/index.js
new file mode 100644
index 000000000..a9b5f75df
--- /dev/null
+++ b/app/screens/channel_info/notification_preference/index.js
@@ -0,0 +1,19 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+import {getMyCurrentChannelMembership} from '@mm-redux/selectors/entities/channels';
+
+import NotificationPreference from './notification_preference';
+
+function mapStateToProps(state) {
+ const channelMember = getMyCurrentChannelMembership(state);
+
+ return {
+ channelId: channelMember?.channel_id,
+ userId: channelMember?.user_id,
+ notifyProps: channelMember?.notify_props,
+ };
+}
+
+export default connect(mapStateToProps)(NotificationPreference);
diff --git a/app/screens/channel_info/notification_preference/notification_preference.tsx b/app/screens/channel_info/notification_preference/notification_preference.tsx
new file mode 100644
index 000000000..bf5920919
--- /dev/null
+++ b/app/screens/channel_info/notification_preference/notification_preference.tsx
@@ -0,0 +1,88 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {PureComponent} from 'react';
+import {intlShape} from 'react-intl';
+
+import {goToScreen} from '@actions/navigation';
+import {ViewTypes} from '@constants';
+import {ChannelNotifyProps} from '@mm-redux/types/channels';
+import {Theme} from '@mm-redux/types/preferences';
+import ChannelInfoRow from '@screens/channel_info/channel_info_row';
+import {t} from '@utils/i18n';
+import {preventDoubleTap} from '@utils/tap';
+
+interface NotificationPreferenceProps {
+ channelId: string;
+ userId: string;
+ notifyProps: ChannelNotifyProps;
+ isLandscape: boolean;
+ theme: Theme;
+}
+
+export default class NotificationPreference extends PureComponent {
+ static contextTypes = {
+ intl: intlShape.isRequired,
+ };
+
+ goToChannelNotificationPreference = preventDoubleTap(() => {
+ const {intl} = this.context;
+ const screen = 'ChannelNotificationPreference';
+ const title = intl.formatMessage({id: 'channel_header.notificationPreference', defaultMessage: 'Mobile Notifications'});
+
+ goToScreen(screen, title, this.props);
+ });
+
+ notificationLevelToText = (notifyLevel: string) => {
+ const {intl} = this.context;
+
+ let textId = '';
+ let defaultMsg = '';
+ switch (notifyLevel) {
+ case ViewTypes.NotificationLevels.DEFAULT: {
+ textId = t('channel_header.notificationPreference.default');
+ defaultMsg = 'Default';
+ break;
+ }
+ case ViewTypes.NotificationLevels.ALL: {
+ textId = t('channel_header.notificationPreference.all');
+ defaultMsg = 'All';
+ break;
+ }
+ case ViewTypes.NotificationLevels.MENTION: {
+ textId = t('channel_header.notificationPreference.mention');
+ defaultMsg = 'Mentions';
+ break;
+ }
+ case ViewTypes.NotificationLevels.NONE: {
+ textId = t('channel_header.notificationPreference.none');
+ defaultMsg = 'Never';
+ break;
+ }
+ }
+
+ return intl.formatMessage({id: textId, defaultMessage: defaultMsg});
+ }
+
+ render() {
+ const {isLandscape, theme, notifyProps, userId, channelId} = this.props;
+ const pushNotifyLevel = notifyProps.push || ViewTypes.NotificationLevels.DEFAULT;
+
+ if (!userId || !channelId) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+}
diff --git a/app/screens/channel_notification_preference/channel_notification_preference.android.js b/app/screens/channel_notification_preference/channel_notification_preference.android.js
new file mode 100644
index 000000000..b5407a2b4
--- /dev/null
+++ b/app/screens/channel_notification_preference/channel_notification_preference.android.js
@@ -0,0 +1,80 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {
+ ScrollView,
+ View,
+} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import RadioButtonGroup from 'app/components/radio_button';
+import StatusBar from 'app/components/status_bar';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import ChannelNotificationPreferenceBase from './channel_notification_preference_base';
+
+export default class ChannelNotificationPreferenceAndroid extends ChannelNotificationPreferenceBase {
+ getRadioItems = () => {
+ const {intl} = this.context;
+ const items = this.getItems();
+ const radioItems = [];
+ items.forEach((element) => {
+ const e = {
+ label: intl.formatMessage({
+ id: element.id,
+ defaultMessage: element.defaultMessage,
+ }),
+ value: element.value,
+ checked: element.checked,
+ };
+ radioItems.push(e);
+ });
+ return radioItems;
+ }
+
+ render() {
+ const {theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ const options = this.getRadioItems();
+
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ backgroundColor: changeOpacity(theme.centerChannelBg, 0.03),
+ },
+ wrapper: {
+ marginLeft: 16,
+ },
+ header: {
+ color: changeOpacity(theme.centerChannelColor, 0.56),
+ fontSize: 13,
+ marginTop: 10,
+ padding: 16,
+ },
+ };
+});
diff --git a/app/screens/channel_notification_preference/channel_notification_preference.ios.js b/app/screens/channel_notification_preference/channel_notification_preference.ios.js
new file mode 100644
index 000000000..8f8ffb1f3
--- /dev/null
+++ b/app/screens/channel_notification_preference/channel_notification_preference.ios.js
@@ -0,0 +1,95 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {
+ ScrollView,
+ View,
+} from 'react-native';
+import SafeAreaView from 'app/components/safe_area_view';
+import FormattedText from 'app/components/formatted_text';
+import StatusBar from 'app/components/status_bar';
+import SectionItem from 'app/screens/settings/section_item';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
+import ChannelNotificationPreferenceBase from './channel_notification_preference_base';
+
+export default class ChannelNotificationPreferenceIos extends ChannelNotificationPreferenceBase {
+ render() {
+ const {theme, isLandscape} = this.props;
+ const style = getStyleSheet(theme);
+
+ const items = this.getItems();
+
+ return (
+
+
+
+
+
+
+ {items.map((item) => (
+
+
+
+ )}
+ action={this.handlePress}
+ actionType='select'
+ actionValue={item.value}
+ selected={item.checked}
+ theme={theme}
+ isLandscape={isLandscape}
+ />
+ ),
+ )}
+
+
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
+ },
+ header: {
+ color: changeOpacity(theme.centerChannelColor, 0.56),
+ fontSize: 13,
+ textTransform: 'uppercase',
+ marginTop: 10,
+ padding: 16,
+ },
+ divider: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
+ height: 1,
+ width: '100%',
+ marginLeft: 16,
+ },
+ scrollView: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
+ },
+ contentContainer: {
+ backgroundColor: changeOpacity(theme.centerChannelBg, 1),
+ },
+ };
+});
diff --git a/app/screens/channel_notification_preference/channel_notification_preference.test.js b/app/screens/channel_notification_preference/channel_notification_preference.test.js
new file mode 100644
index 000000000..dc57735ee
--- /dev/null
+++ b/app/screens/channel_notification_preference/channel_notification_preference.test.js
@@ -0,0 +1,69 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+
+import {ViewTypes} from '@constants';
+import Preferences from '@mm-redux/constants/preferences';
+import SectionItem from '@screens/settings/section_item';
+
+import {shallowWithIntl} from 'test/intl-test-helper';
+import ChannelNotificationPreference from './channel_notification_preference';
+
+function makeProps(pushNotificationLevel) {
+ return {
+ actions: {
+ updateChannelNotifyProps: jest.fn(),
+ },
+ channelId: 'channel_id',
+ userId: 'user_id',
+ notifyProps: {
+ push: pushNotificationLevel,
+ },
+ theme: Preferences.THEMES.default,
+ isLandscape: false,
+ };
+}
+
+function checkNotificationSelected(pushNotificationLevel, trueIdx) {
+ const wrapper = shallowWithIntl(
+ ,
+ );
+
+ const sectionItems = wrapper.find(SectionItem);
+
+ expect(sectionItems.exists()).toBe(true);
+ expect(sectionItems.length).toBe(4);
+
+ sectionItems.forEach((sectionItem, idx) => {
+ expect(sectionItem.prop('selected')).toBe(idx === trueIdx);
+ });
+}
+
+describe('ChannelNotificationPreference', () => {
+ test('should have correct setting selected', () => {
+ checkNotificationSelected(null, 0);
+ checkNotificationSelected(ViewTypes.NotificationLevels.DEFAULT, 0);
+ checkNotificationSelected(ViewTypes.NotificationLevels.ALL, 1);
+ checkNotificationSelected(ViewTypes.NotificationLevels.MENTION, 2);
+ checkNotificationSelected(ViewTypes.NotificationLevels.NONE, 3);
+ });
+
+ test('should save on click', () => {
+ const props = makeProps('default');
+ const wrapper = shallowWithIntl(
+ ,
+ );
+
+ // click on 'Never' -- last item
+ wrapper.find(SectionItem).at(3).dive().simulate('press');
+
+ expect(props.actions.updateChannelNotifyProps).toHaveBeenCalledTimes(1);
+ expect(props.actions.updateChannelNotifyProps).toBeCalledWith(
+ props.userId,
+ props.channelId,
+ {push: ViewTypes.NotificationLevels.NONE},
+ );
+ });
+});
diff --git a/app/screens/channel_notification_preference/channel_notification_preference_base.js b/app/screens/channel_notification_preference/channel_notification_preference_base.js
new file mode 100644
index 000000000..f36c25c22
--- /dev/null
+++ b/app/screens/channel_notification_preference/channel_notification_preference_base.js
@@ -0,0 +1,95 @@
+// 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 {intlShape} from 'react-intl';
+
+import {ViewTypes} from '@constants';
+import {alertErrorWithFallback} from 'app/utils/general';
+import {t} from '@utils/i18n';
+import {preventDoubleTap} from 'app/utils/tap';
+
+export default class ChannelNotificationPreferenceBase extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ updateChannelNotifyProps: PropTypes.func.isRequired,
+ }),
+ channelId: PropTypes.string.isRequired,
+ isLandscape: PropTypes.bool.isRequired,
+ notifyProps: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired,
+ userId: PropTypes.string.isRequired,
+ };
+
+ static contextTypes = {
+ intl: intlShape.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ notificationLevel: props.notifyProps?.push || ViewTypes.NotificationLevels.DEFAULT,
+ };
+ }
+
+ getItems = () => {
+ const {notificationLevel} = this.state;
+ return [{
+ id: t('channel_notifications.preference.global_default'),
+ defaultMessage: 'Global default (Mentions)',
+ value: ViewTypes.NotificationLevels.DEFAULT,
+ checked: notificationLevel === ViewTypes.NotificationLevels.DEFAULT,
+ }, {
+ id: t('channel_notifications.preference.all_activity'),
+ defaultMessage: 'For all activity',
+ value: ViewTypes.NotificationLevels.ALL,
+ checked: notificationLevel === ViewTypes.NotificationLevels.ALL,
+ }, {
+ id: t('channel_notifications.preference.only_mentions'),
+ defaultMessage: 'Only mentions and direct messages',
+ value: ViewTypes.NotificationLevels.MENTION,
+ checked: notificationLevel === ViewTypes.NotificationLevels.MENTION,
+ }, {
+ id: t('channel_notifications.preference.never'),
+ defaultMessage: 'Never',
+ value: ViewTypes.NotificationLevels.NONE,
+ checked: notificationLevel === ViewTypes.NotificationLevels.NONE,
+ }];
+ }
+
+ handlePress = preventDoubleTap(async (newNotificationLevel) => {
+ const {actions, channelId, userId} = this.props;
+ const {notificationLevel} = this.state;
+
+ if (newNotificationLevel === notificationLevel) {
+ // tapped on current selection.
+ return;
+ }
+
+ this.setState({
+ notificationLevel: newNotificationLevel,
+ });
+
+ const props = {push: newNotificationLevel};
+
+ const {error} = await actions.updateChannelNotifyProps(userId, channelId, props);
+ if (error) {
+ const {intl} = this.context;
+ alertErrorWithFallback(
+ intl,
+ error,
+ {
+ id: t('channel_notifications.preference.save_error'),
+ defaultMessage: "We couldn't save notification preference. Please check your connection and try again.",
+ },
+ );
+
+ // restore old value.
+ this.setState({
+ notificationLevel,
+ });
+ }
+ });
+}
diff --git a/app/screens/channel_notification_preference/index.js b/app/screens/channel_notification_preference/index.js
new file mode 100644
index 000000000..d8ac34547
--- /dev/null
+++ b/app/screens/channel_notification_preference/index.js
@@ -0,0 +1,26 @@
+// 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 {updateChannelNotifyProps} from '@mm-redux/actions/channels';
+import {getTheme} from '@mm-redux/selectors/entities/preferences';
+import {isLandscape} from 'app/selectors/device';
+
+import ChannelNotificationPreference from './channel_notification_preference';
+
+function mapStateToProps(state) {
+ const theme = getTheme(state);
+ return {
+ theme,
+ isLandscape: isLandscape(state),
+ };
+}
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: bindActionCreators({
+ updateChannelNotifyProps,
+ }, dispatch),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChannelNotificationPreference);
diff --git a/app/screens/index.js b/app/screens/index.js
index 735102747..7157a2411 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -33,6 +33,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('ChannelAddMembers', () => wrapper(require('app/screens/channel_add_members').default), () => require('app/screens/channel_add_members').default);
Navigation.registerComponent('ChannelInfo', () => wrapper(require('app/screens/channel_info').default), () => require('app/screens/channel_info').default);
Navigation.registerComponent('ChannelMembers', () => wrapper(require('app/screens/channel_members').default), () => require('app/screens/channel_members').default);
+ Navigation.registerComponent('ChannelNotificationPreference', () => wrapper(require('app/screens/channel_notification_preference').default), () => require('app/screens/channel_notification_preference').default);
Navigation.registerComponent('ClientUpgrade', () => wrapper(require('app/screens/client_upgrade').default), () => require('app/screens/client_upgrade').default);
Navigation.registerComponent('ClockDisplaySettings', () => wrapper(require('app/screens/settings/clock_display').default), () => require('app/screens/settings/clock_display').default);
Navigation.registerComponent('Code', () => wrapper(require('app/screens/code').default), () => require('app/screens/code').default);
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 096dcbe5e..7051bc052 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -17,6 +17,11 @@
"channel_header.addMembers": "Add Members",
"channel_header.directchannel.you": "{displayname} (you) ",
"channel_header.manageMembers": "Manage Members",
+ "channel_header.notificationPreference": "Mobile Notifications",
+ "channel_header.notificationPreference.all": "All",
+ "channel_header.notificationPreference.default": "Default",
+ "channel_header.notificationPreference.mention": "Mentions",
+ "channel_header.notificationPreference.none": "Never",
"channel_header.pinnedPosts": "Pinned Messages",
"channel_header.viewMembers": "View Members",
"channel_info.header": "Header:",
@@ -35,6 +40,12 @@
"channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.ignoreChannelMentions.settings": "Ignore @channel, @here, @all",
"channel_notifications.muteChannel.settings": "Mute channel",
+ "channel_notifications.preference.all_activity": "For all activity",
+ "channel_notifications.preference.global_default": "Global default (Mentions)",
+ "channel_notifications.preference.header": "Send Notifications",
+ "channel_notifications.preference.never": "Never",
+ "channel_notifications.preference.only_mentions": "Only mentions and direct messages",
+ "channel_notifications.preference.save_error": "We couldn't save notification preference. Please check your connection and try again.",
"channel.channelHasGuests": "This channel has guests",
"channel.hasGuests": "This group message has guests",
"channel.isGuest": "This person is a guest",