From 1e4e9b796288842bfb9e3480baef8a295291cb20 Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 23 May 2017 12:07:21 -0400 Subject: [PATCH] Render slack attachments in posts (#555) --- app/components/post/post.js | 36 +- app/components/slack_attachments/index.js | 44 ++ .../slack_attachments/slack_attachment.js | 403 ++++++++++++++++++ 3 files changed, 476 insertions(+), 7 deletions(-) create mode 100644 app/components/slack_attachments/index.js create mode 100644 app/components/slack_attachments/slack_attachment.js diff --git a/app/components/post/post.js b/app/components/post/post.js index af3b9c9c7..c9848589d 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -18,6 +18,7 @@ import {injectIntl, intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/Ionicons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import {NavigationTypes} from 'app/constants'; import FileAttachmentList from 'app/components/file_attachment_list'; import FormattedText from 'app/components/formatted_text'; import FormattedTime from 'app/components/formatted_time'; @@ -26,7 +27,7 @@ import Markdown from 'app/components/markdown'; import OptionsContext from 'app/components/options_context'; import ProfilePicture from 'app/components/profile_picture'; import ReplyIcon from 'app/components/reply_icon'; -import {NavigationTypes} from 'app/constants'; +import SlackAttachments from 'app/components/slack_attachments'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -314,6 +315,30 @@ class Post extends PureComponent { return attachments; } + renderSlackAttachments = (baseStyle) => { + const {post, theme} = this.props; + + if (post.props) { + const {attachments} = post.props; + const textStyles = getMarkdownTextStyles(theme); + const blockStyles = getMarkdownBlockStyles(theme); + + if (attachments && attachments.length) { + return ( + + ); + } + } + + return null; + }; + renderMessage = (style, messageStyle, replyBar = false) => { const {formatMessage} = this.props.intl; const {isFlagged, post, theme} = this.props; @@ -381,6 +406,7 @@ class Post extends PureComponent { toggleSelected={this.toggleSelected} > {message} + {this.renderSlackAttachments(messageStyle)} {this.renderFileAttachments()} @@ -419,6 +445,7 @@ class Post extends PureComponent { actions={actions} cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})} /> + {this.renderSlackAttachments(messageStyle)} {this.renderFileAttachments()} {post.failed && @@ -522,7 +549,7 @@ class Post extends PureComponent { ); - messageStyle = [style.message, style.webhookMessage]; + messageStyle = style.message; } else { profilePicture = ( @@ -702,11 +729,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { systemMessage: { opacity: 0.6 }, - webhookMessage: { - color: theme.centerChannelColor, - fontSize: 16, - fontWeight: '600' - }, selected: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.1) }, diff --git a/app/components/slack_attachments/index.js b/app/components/slack_attachments/index.js new file mode 100644 index 000000000..2c82cd629 --- /dev/null +++ b/app/components/slack_attachments/index.js @@ -0,0 +1,44 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; + +import CustomPropTypes from 'app/constants/custom_prop_types'; + +import SlackAttachment from './slack_attachment'; + +export default class SlackAttachments extends PureComponent { + static propTypes = { + attachments: PropTypes.array.isRequired, + baseTextStyle: CustomPropTypes.Style, + blockStyles: PropTypes.object, + textStyles: PropTypes.object, + theme: PropTypes.object + }; + + render() { + const {attachments, baseTextStyle, blockStyles, textStyles, theme} = this.props; + const content = []; + + attachments.forEach((attachment, i) => { + content.push( + + ); + }); + + return ( + + {content} + + ); + } +} diff --git a/app/components/slack_attachments/slack_attachment.js b/app/components/slack_attachments/slack_attachment.js new file mode 100644 index 000000000..1e4cc10dc --- /dev/null +++ b/app/components/slack_attachments/slack_attachment.js @@ -0,0 +1,403 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Image, + Linking, + StyleSheet, + Text, + View +} from 'react-native'; + +import CustomPropTypes from 'app/constants/custom_prop_types'; +import Markdown from 'app/components/markdown'; +import FormattedText from 'app/components/formatted_text'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class SlackAttachment extends PureComponent { + static propTypes = { + attachment: PropTypes.object.isRequired, + baseTextStyle: CustomPropTypes.Style, + blockStyles: PropTypes.object, + textStyles: PropTypes.object, + theme: PropTypes.object + }; + + constructor(props) { + super(props); + + this.state = this.getInitState(); + } + + getCollapsedText = () => { + let text = this.props.attachment.text || ''; + if ((text.match(/\n/g) || []).length >= 5) { + text = text.split('\n').splice(0, 5).join('\n'); + } else if (text.length > 400) { + text = text.substr(0, 400); + } + + return text; + }; + + getInitState = () => { + const shouldCollapse = this.shouldCollapse(); + const uncollapsedText = this.props.attachment.text; + const collapsedText = shouldCollapse ? this.getCollapsedText() : uncollapsedText; + + return { + shouldCollapse, + collapsedText, + uncollapsedText, + text: shouldCollapse ? collapsedText : uncollapsedText, + collapsed: shouldCollapse + }; + }; + + getFieldsTable = (style) => { + const { + attachment, + baseTextStyle, + blockStyles, + textStyles + } = this.props; + const fields = attachment.fields; + if (!fields || !fields.length) { + return null; + } + + const fieldTables = []; + + let fieldInfos = []; + let rowPos = 0; + let lastWasLong = false; + let nrTables = 0; + + fields.forEach((field, i) => { + if (rowPos === 2 || !(field.short === true) || lastWasLong) { + fieldTables.push( + + {fieldInfos} + + ); + fieldInfos = []; + rowPos = 0; + nrTables += 1; + lastWasLong = false; + } + + fieldInfos.push( + + + + + {field.title} + + + + + + + + ); + + rowPos += 1; + lastWasLong = !(field.short === true); + }); + + if (fieldInfos.length > 0) { // Flush last fields + fieldTables.push( + + {fieldInfos} + + ); + } + + return ( + + {fieldTables} + + ); + }; + + openLink = (link) => { + if (Linking.canOpenURL(link)) { + Linking.openURL(link); + } + }; + + shouldCollapse = () => { + const text = this.props.attachment.text || ''; + return (text.match(/\n/g) || []).length >= 5 || text.length > 400; + }; + + toggleCollapseState = () => { + const state = this.state; + const text = state.collapsed ? state.uncollapsedText : state.collapsedText; + const collapsed = !state.collapsed; + + this.setState({collapsed, text}); + }; + + render() { + const { + attachment, + baseTextStyle, + blockStyles, + textStyles, + theme + } = this.props; + + const style = getStyleSheet(theme); + + let preText; + if (attachment.pretext) { + preText = ( + + + + ); + } + + let borderStyle; + if (attachment.color && attachment.color[0] === '#') { + borderStyle = {borderLeftColor: attachment.color}; + } + + const author = []; + if (attachment.author_name || attachment.author_icon) { + if (attachment.author_icon) { + author.push( + + ); + } + if (attachment.author_name) { + let link; + let linkStyle; + if (attachment.author_link) { + link = () => this.openLink(attachment.author_link); + linkStyle = style.authorLink; + } + author.push( + + {attachment.author_name} + + ); + } + } + + let title; + let titleStyle; + if (attachment.title) { + let titleLink; + if (attachment.title_link) { + titleStyle = style.titleLink; + titleLink = () => this.openLink(attachment.title_link); + } + + title = ( + + {attachment.title} + + ); + } + + let thumb; + let topStyle; + if (attachment.thumb_url) { + topStyle = style.topContent; + thumb = ( + + + + ); + } + + let text; + if (attachment.text) { + let moreLess; + if (this.state.shouldCollapse) { + if (this.state.collapsed) { + moreLess = ( + + ); + } else { + moreLess = ( + + ); + } + } + + text = ( + + + {moreLess} + + ); + } + + const fields = this.getFieldsTable(style); + + let image; + if (attachment.image_url) { + image = ( + + + + ); + } + + return ( + + {preText} + + + {author} + + + {title} + + {thumb} + {text} + {fields} + {image} + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + borderColor: changeOpacity(theme.centerChannelColor, 0.15), + borderWidth: 1, + flex: 1, + paddingHorizontal: 10, + paddingVertical: 7 + }, + border: { + borderLeftColor: changeOpacity(theme.linkColor, 0.6), + borderLeftWidth: 3 + }, + author: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 11 + }, + authorIcon: { + height: 12, + marginRight: 3, + width: 12 + }, + authorLink: { + color: changeOpacity(theme.linkColor, 0.5) + }, + title: { + color: theme.centerChannelColor, + fontWeight: '600', + marginVertical: 5 + }, + titleLink: { + color: theme.linkColor + }, + topContent: { + paddingRight: 60 + }, + thumbContainer: { + position: 'absolute', + right: 10, + top: 10 + }, + thumb: { + height: 45, + width: 45 + }, + moreLess: { + color: theme.linkColor, + fontSize: 12 + }, + headingContainer: { + flex: 1, + flexDirection: 'row', + marginBottom: 5, + marginTop: 10 + }, + heading: { + fontWeight: '600' + }, + bodyContainer: { + flex: 1 + }, + imageContainer: { + borderColor: changeOpacity(theme.centerChannelColor, 0.1), + borderWidth: 1, + borderRadius: 2, + marginTop: 5 + }, + image: { + flex: 1, + height: 50 + } + }); +});