From 27c764d2887c01d5f6d1971ffe2f2bba50516b62 Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 13 Feb 2018 12:23:11 -0300 Subject: [PATCH] [ICU-579] Display Announcement banner (#1428) * Announcement banner * Feedback review --- app/actions/views/announcement.js | 11 ++ .../announcement_banner.js | 144 ++++++++++++++++++ app/components/announcement_banner/index.js | 36 +++++ app/constants/view.js | 3 +- app/reducers/views/announcement.js | 17 +++ app/reducers/views/index.js | 2 + .../channel_post_list/channel_post_list.js | 2 + app/store/index.js | 2 +- app/store/middleware.js | 1 + assets/base/i18n/en.json | 3 + 10 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 app/actions/views/announcement.js create mode 100644 app/components/announcement_banner/announcement_banner.js create mode 100644 app/components/announcement_banner/index.js create mode 100644 app/reducers/views/announcement.js 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}?",