diff --git a/app/actions/views/announcement.js b/app/actions/views/announcement.js
new file mode 100644
index 000000000..ba2b97a49
--- /dev/null
+++ b/app/actions/views/announcement.js
@@ -0,0 +1,11 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {ViewTypes} from 'app/constants';
+
+export function dismissBanner(text) {
+ return {
+ type: ViewTypes.ANNOUNCEMENT_BANNER,
+ data: text
+ };
+}
diff --git a/app/components/announcement_banner/announcement_banner.js b/app/components/announcement_banner/announcement_banner.js
new file mode 100644
index 000000000..9579ec6b4
--- /dev/null
+++ b/app/components/announcement_banner/announcement_banner.js
@@ -0,0 +1,144 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {
+ Alert,
+ Animated,
+ StyleSheet,
+ Text,
+ TouchableOpacity
+} from 'react-native';
+import {intlShape} from 'react-intl';
+import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
+
+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
+ };
+
+ static contextTypes = {
+ intl: intlShape
+ };
+
+ state = {
+ bannerHeight: new Animated.Value(0)
+ };
+
+ componentWillMount() {
+ const {bannerDismissed, bannerEnabled, bannerText} = this.props;
+ const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
+ this.toggleBanner(showBanner);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.bannerText !== nextProps.bannerText ||
+ this.props.bannerEnabled !== nextProps.bannerEnabled ||
+ this.props.bannerDismissed !== nextProps.bannerDismissed) {
+ const showBanner = nextProps.bannerEnabled && !nextProps.bannerDismissed && Boolean(nextProps.bannerText);
+ this.toggleBanner(showBanner);
+ }
+ }
+
+ 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'})
+ }];
+
+ 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}
+ );
+ };
+
+ toggleBanner = (show = true) => {
+ const value = show ? 38 : 0;
+ Animated.timing(this.state.bannerHeight, {
+ toValue: value,
+ duration: 350
+ }).start();
+ };
+
+ render() {
+ const {bannerHeight} = this.state;
+
+ const bannerStyle = {
+ backgroundColor: this.props.bannerColor,
+ height: bannerHeight
+ };
+
+ const bannerTextStyle = {
+ color: this.props.bannerTextColor
+ };
+
+ return (
+
+
+
+ {this.props.bannerText}
+
+
+
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ bannerContainer: {
+ paddingHorizontal: 10,
+ position: 'absolute',
+ top: 0,
+ overflow: 'hidden',
+ width: '100%'
+ },
+ wrapper: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row'
+ },
+ bannerText: {
+ flex: 1,
+ fontSize: 14,
+ marginRight: 5
+ }
+});
diff --git a/app/components/announcement_banner/index.js b/app/components/announcement_banner/index.js
new file mode 100644
index 000000000..59920d9e4
--- /dev/null
+++ b/app/components/announcement_banner/index.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2018-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 AnnouncementBanner from './announcement_banner';
+
+function mapStateToProps(state) {
+ const config = getConfig(state);
+ const license = getLicense(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
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ dismissBanner
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);
diff --git a/app/constants/view.js b/app/constants/view.js
index 289af6fa8..b69d0b071 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -61,7 +61,8 @@ const ViewTypes = keyMirror({
SET_LAST_UPGRADE_CHECK: null,
ADD_RECENT_EMOJI: null,
- EXTENSION_SELECTED_TEAM_ID: null
+ EXTENSION_SELECTED_TEAM_ID: null,
+ ANNOUNCEMENT_BANNER: null
});
export default {
diff --git a/app/reducers/views/announcement.js b/app/reducers/views/announcement.js
new file mode 100644
index 000000000..db16ea005
--- /dev/null
+++ b/app/reducers/views/announcement.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {UserTypes} from 'mattermost-redux/action_types';
+
+import {ViewTypes} from 'app/constants';
+
+export default function banner(state = '', action) {
+ switch (action.type) {
+ case ViewTypes.ANNOUNCEMENT_BANNER:
+ return action.data;
+ case UserTypes.LOGOUT_SUCCESS:
+ return '';
+ default:
+ return state;
+ }
+}
diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js
index 7dcea57aa..5c812c603 100644
--- a/app/reducers/views/index.js
+++ b/app/reducers/views/index.js
@@ -3,6 +3,7 @@
import {combineReducers} from 'redux';
+import announcement from './announcement';
import channel from './channel';
import clientUpgrade from './client_upgrade';
import extension from './extension';
@@ -17,6 +18,7 @@ import team from './team';
import thread from './thread';
export default combineReducers({
+ announcement,
channel,
clientUpgrade,
extension,
diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js
index 18d5e5d51..f6b33cc0c 100644
--- a/app/screens/channel/channel_post_list/channel_post_list.js
+++ b/app/screens/channel/channel_post_list/channel_post_list.js
@@ -9,6 +9,7 @@ import {
View
} from 'react-native';
+import AnnouncementBanner from 'app/components/announcement_banner';
import PostList from 'app/components/post_list';
import PostListRetry from 'app/components/post_list_retry';
import RetryBarIndicator from 'app/components/retry_bar_indicator';
@@ -159,6 +160,7 @@ export default class ChannelPostList extends PureComponent {
return (
{component}
+
);
diff --git a/app/store/index.js b/app/store/index.js
index f340d816e..2eab2cbd5 100644
--- a/app/store/index.js
+++ b/app/store/index.js
@@ -46,7 +46,7 @@ const setTransforms = [
export default function configureAppStore(initialState) {
const viewsBlackListFilter = createBlacklistFilter(
'views',
- ['extension', 'login', 'root']
+ ['announcement', 'extension', 'login', 'root']
);
const typingBlackListFilter = createBlacklistFilter(
diff --git a/app/store/middleware.js b/app/store/middleware.js
index 0aec38c8c..4a515424f 100644
--- a/app/store/middleware.js
+++ b/app/store/middleware.js
@@ -319,6 +319,7 @@ function cleanupState(action, keepCurrent = false) {
users: payload.entities.users
},
views: {
+ announcement: payload.views.announcement,
...resetPayload.views,
channel: {
...resetPayload.views.channel,
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 50bfca07d..a7279d3cf 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1940,6 +1940,9 @@
"mobile.android.photos_permission_denied_title": "Photo library access is required",
"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.title": "Announcement",
+ "mobile.announcement_banner.ok": "OK",
+ "mobile.announcement_banner.dismiss": "Dismiss",
"mobile.channel.markAsRead": "Mark As Read",
"mobile.channel_drawer.search": "Jump to...",
"mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to delete the {term} {name}?",