From da8b5e92cce8114334a6eb3b41443734c36874c0 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 4 Jul 2018 13:15:03 -0400 Subject: [PATCH] MM-10012 Added Markdown rendering to expanded announcement banner (#1861) * MM-10012 Added Markdown rendering to expanded announcement banner * Updated snapshots * Re-added dismiss button to expanded announcement banner * Updated 'Don't show again' button style' --- .../announcement_banner.test.js.snap | 60 ++++++--- .../announcement_banner.js | 49 ++++---- .../announcement_banner.test.js | 18 +-- app/components/announcement_banner/index.js | 16 +-- app/components/channel_link/channel_link.js | 5 + app/components/markdown/markdown.js | 2 + app/components/remove_markdown.js | 75 +++++++++++ .../channel_post_list/channel_post_list.js | 2 +- .../expanded_announcement_banner.js | 116 ++++++++++++++++++ .../expanded_announcement_banner/index.js | 32 +++++ app/screens/index.js | 1 + assets/base/i18n/en.json | 1 - 12 files changed, 297 insertions(+), 80 deletions(-) create mode 100644 app/components/remove_markdown.js create mode 100644 app/screens/expanded_announcement_banner/expanded_announcement_banner.js create mode 100644 app/screens/expanded_announcement_banner/index.js diff --git a/app/components/announcement_banner/__snapshots__/announcement_banner.test.js.snap b/app/components/announcement_banner/__snapshots__/announcement_banner.test.js.snap index 305e02dca..9163f2a2e 100644 --- a/app/components/announcement_banner/__snapshots__/announcement_banner.test.js.snap +++ b/app/components/announcement_banner/__snapshots__/announcement_banner.test.js.snap @@ -5,12 +5,6 @@ ShallowWrapper { "length": 1, Symbol(enzyme.__root__): [Circular], Symbol(enzyme.__unrendered__): - Banner Text + - 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 + - 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__): { - 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} + { 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( - - ); - - wrapper.instance().handleDismiss(); - expect(actions.dismissBanner).toHaveBeenCalledTimes(1); - expect(actions.dismissBanner).toHaveBeenCalledWith(props.bannerText); - }); -}); \ No newline at end of file +}); diff --git a/app/components/announcement_banner/index.js b/app/components/announcement_banner/index.js index 7b27f2ae7..f560c461d 100644 --- a/app/components/announcement_banner/index.js +++ b/app/components/announcement_banner/index.js @@ -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); diff --git a/app/components/channel_link/channel_link.js b/app/components/channel_link/channel_link.js index 89f236591..f06dfc600 100644 --- a/app/components/channel_link/channel_link.js +++ b/app/components/channel_link/channel_link.js @@ -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() { diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js index db91aef76..204e6ab4f 100644 --- a/app/components/markdown/markdown.js +++ b/app/components/markdown/markdown.js @@ -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 { ); diff --git a/app/components/remove_markdown.js b/app/components/remove_markdown.js new file mode 100644 index 000000000..eb4d2562d --- /dev/null +++ b/app/components/remove_markdown.js @@ -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 {literal}; + }; + + renderNull = () => { + return null; + }; + + render() { + const ast = this.parser.parse(this.props.value); + + return {this.renderer.render(ast)}; + } +} 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 360ee666b..a79f425c2 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -228,7 +228,7 @@ export default class ChannelPostList extends PureComponent { return ( {component} - + ); diff --git a/app/screens/expanded_announcement_banner/expanded_announcement_banner.js b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js new file mode 100644 index 000000000..e47b3bcb6 --- /dev/null +++ b/app/screens/expanded_announcement_banner/expanded_announcement_banner.js @@ -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 = ( + + + + ); + } + + return ( + + + + + {dismissButton} + + ); + } +} + +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', + }, + }; +}); diff --git a/app/screens/expanded_announcement_banner/index.js b/app/screens/expanded_announcement_banner/index.js new file mode 100644 index 000000000..83837c9c3 --- /dev/null +++ b/app/screens/expanded_announcement_banner/index.js @@ -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); diff --git a/app/screens/index.js b/app/screens/index.js index 37e1e6a89..eef346b2d 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -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); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 01476101e..481edc80f 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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...",