Render slack attachments in posts (#555)
This commit is contained in:
parent
9f64676215
commit
1e4e9b7962
3 changed files with 476 additions and 7 deletions
|
|
@ -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 (
|
||||
<SlackAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseStyle}
|
||||
blockStyles={blockStyles}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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()}
|
||||
</OptionsContext>
|
||||
</View>
|
||||
|
|
@ -419,6 +445,7 @@ class Post extends PureComponent {
|
|||
actions={actions}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
/>
|
||||
{this.renderSlackAttachments(messageStyle)}
|
||||
{this.renderFileAttachments()}
|
||||
</View>
|
||||
{post.failed &&
|
||||
|
|
@ -522,7 +549,7 @@ class Post extends PureComponent {
|
|||
</Text>
|
||||
</View>
|
||||
);
|
||||
messageStyle = [style.message, style.webhookMessage];
|
||||
messageStyle = style.message;
|
||||
} else {
|
||||
profilePicture = (
|
||||
<TouchableOpacity onPress={this.viewUserProfile}>
|
||||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
44
app/components/slack_attachments/index.js
Normal file
44
app/components/slack_attachments/index.js
Normal file
|
|
@ -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(
|
||||
<SlackAttachment
|
||||
attachment={attachment}
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
key={'att_' + i}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'column'}}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
403
app/components/slack_attachments/slack_attachment.js
Normal file
403
app/components/slack_attachments/slack_attachment.js
Normal file
|
|
@ -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(
|
||||
<View
|
||||
key={`attachment__table__${nrTables}`}
|
||||
style={{flex: 1, flexDirection: 'row'}}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>
|
||||
);
|
||||
fieldInfos = [];
|
||||
rowPos = 0;
|
||||
nrTables += 1;
|
||||
lastWasLong = false;
|
||||
}
|
||||
|
||||
fieldInfos.push(
|
||||
<View
|
||||
style={{flex: 1}}
|
||||
key={`attachment__field-${i}__${nrTables}`}
|
||||
>
|
||||
<View
|
||||
style={style.headingContainer}
|
||||
key={`attachment__field-caption-${i}__${nrTables}`}
|
||||
>
|
||||
<View>
|
||||
<Text style={style.heading}>
|
||||
{field.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={style.bodyContainer}
|
||||
key={`attachment__field-${i}__${nrTables}`}
|
||||
>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={(field.value || '')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
rowPos += 1;
|
||||
lastWasLong = !(field.short === true);
|
||||
});
|
||||
|
||||
if (fieldInfos.length > 0) { // Flush last fields
|
||||
fieldTables.push(
|
||||
<View
|
||||
key={`attachment__table__${nrTables}`}
|
||||
style={{flex: 1, flexDirection: 'row'}}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{fieldTables}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<View style={{marginVertical: 5}}>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={attachment.pretext}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<Image
|
||||
source={{uri: attachment.author_icon}}
|
||||
key='author_icon'
|
||||
style={style.authorIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (attachment.author_name) {
|
||||
let link;
|
||||
let linkStyle;
|
||||
if (attachment.author_link) {
|
||||
link = () => this.openLink(attachment.author_link);
|
||||
linkStyle = style.authorLink;
|
||||
}
|
||||
author.push(
|
||||
<Text
|
||||
key='author_name'
|
||||
style={[style.author, linkStyle]}
|
||||
onPress={link}
|
||||
>
|
||||
{attachment.author_name}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let title;
|
||||
let titleStyle;
|
||||
if (attachment.title) {
|
||||
let titleLink;
|
||||
if (attachment.title_link) {
|
||||
titleStyle = style.titleLink;
|
||||
titleLink = () => this.openLink(attachment.title_link);
|
||||
}
|
||||
|
||||
title = (
|
||||
<Text
|
||||
style={[style.title, titleStyle]}
|
||||
onPress={titleLink}
|
||||
>
|
||||
{attachment.title}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
let thumb;
|
||||
let topStyle;
|
||||
if (attachment.thumb_url) {
|
||||
topStyle = style.topContent;
|
||||
thumb = (
|
||||
<View style={style.thumbContainer}>
|
||||
<Image
|
||||
source={{uri: attachment.thumb_url}}
|
||||
resizeMode='contain'
|
||||
resizeMethod='scale'
|
||||
style={style.thumb}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let text;
|
||||
if (attachment.text) {
|
||||
let moreLess;
|
||||
if (this.state.shouldCollapse) {
|
||||
if (this.state.collapsed) {
|
||||
moreLess = (
|
||||
<FormattedText
|
||||
id='post_attachment.more'
|
||||
defaultMessage='Show more...'
|
||||
onPress={this.toggleCollapseState}
|
||||
style={style.moreLess}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
moreLess = (
|
||||
<FormattedText
|
||||
id='post_attachment.collapse'
|
||||
defaultMessage='Show less...'
|
||||
onPress={this.toggleCollapseState}
|
||||
style={style.moreLess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
text = (
|
||||
<View style={topStyle}>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
value={this.state.text}
|
||||
/>
|
||||
{moreLess}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const fields = this.getFieldsTable(style);
|
||||
|
||||
let image;
|
||||
if (attachment.image_url) {
|
||||
image = (
|
||||
<View style={style.imageContainer}>
|
||||
<Image
|
||||
source={{uri: attachment.image_url}}
|
||||
style={style.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{preText}
|
||||
<View style={[style.container, style.border, borderStyle]}>
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
{author}
|
||||
</View>
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
{title}
|
||||
</View>
|
||||
{thumb}
|
||||
{text}
|
||||
{fields}
|
||||
{image}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue