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'
This commit is contained in:
Harrison Healey 2018-07-04 13:15:03 -04:00 committed by Elias Nahum
parent 76a82aa627
commit da8b5e92cc
12 changed files with 297 additions and 80 deletions

View file

@ -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}

View file

@ -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}

View file

@ -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);
});
});
});

View file

@ -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);

View file

@ -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() {

View file

@ -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}
/>
);

View 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>;
}
}

View file

@ -228,7 +228,7 @@ export default class ChannelPostList extends PureComponent {
return (
<View style={style.container}>
{component}
<AnnouncementBanner/>
<AnnouncementBanner navigator={navigator}/>
<RetryBarIndicator/>
</View>
);

View file

@ -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',
},
};
});

View 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);

View file

@ -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);

View file

@ -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...",