From 736a955be8afee0940d281cd67e0983228294949 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 22 Nov 2018 19:47:27 -0300 Subject: [PATCH] Use post metadata to prevent scroll pop (#2304) * Use post metadata to prevent scroll pop * Fix not to fetch for reactions every time * Update post-metadata mm-redux ref * Update mattermost-redux to include post-metadata --- app/components/channel_intro/channel_intro.js | 1 + .../file_attachment_list.js | 8 +- .../load_more_posts/load_more_posts.js | 1 + app/components/markdown/markdown.js | 2 + .../markdown/markdown_image/markdown_image.js | 31 +- .../attachement_pretext.js | 57 ++ .../message_attachments/attachment_actions.js | 76 +++ .../message_attachments/attachment_author.js | 81 +++ .../message_attachments/attachment_fields.js | 143 +++++ .../message_attachments/attachment_image.js | 169 ++++++ .../message_attachments/attachment_text.js | 111 ++++ .../attachment_thumbnail.js | 43 ++ .../message_attachments/attachment_title.js | 65 +++ app/components/message_attachments/index.js | 9 + .../message_attachments/message_attachment.js | 540 +++--------------- .../post_attachment_opengraph.test.js.snap | 64 ++- .../post_attachment_opengraph.js | 100 ++-- .../post_attachment_opengraph.test.js | 9 + app/components/post_body/index.js | 4 + app/components/post_body/post_body.js | 34 +- .../post_body_additional_content.js | 129 +++-- app/components/reactions/reactions.js | 6 +- package-lock.json | 4 +- package.json | 2 +- 24 files changed, 1111 insertions(+), 578 deletions(-) create mode 100644 app/components/message_attachments/attachement_pretext.js create mode 100644 app/components/message_attachments/attachment_actions.js create mode 100644 app/components/message_attachments/attachment_author.js create mode 100644 app/components/message_attachments/attachment_fields.js create mode 100644 app/components/message_attachments/attachment_image.js create mode 100644 app/components/message_attachments/attachment_text.js create mode 100644 app/components/message_attachments/attachment_thumbnail.js create mode 100644 app/components/message_attachments/attachment_title.js diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index e68fb18d7..80ad62ed3 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -355,6 +355,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { marginTop: 60, marginHorizontal: 12, marginBottom: 12, + overflow: 'hidden', }, displayName: { color: theme.centerChannelColor, diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index e207f539b..d6503ae4d 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -28,7 +28,7 @@ export default class FileAttachmentList extends Component { deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, fileIds: PropTypes.array.isRequired, - files: PropTypes.array.isRequired, + files: PropTypes.array, isFailed: PropTypes.bool, navigator: PropTypes.object, onLongPress: PropTypes.func, @@ -49,8 +49,10 @@ export default class FileAttachmentList extends Component { } componentDidMount() { - const {postId} = this.props; - this.props.actions.loadFilesForPostIfNecessary(postId); + const {files, postId} = this.props; + if (!files || !files.length) { + this.props.actions.loadFilesForPostIfNecessary(postId); + } } componentWillReceiveProps(nextProps) { diff --git a/app/components/load_more_posts/load_more_posts.js b/app/components/load_more_posts/load_more_posts.js index 5b0e95134..a0c892e20 100644 --- a/app/components/load_more_posts/load_more_posts.js +++ b/app/components/load_more_posts/load_more_posts.js @@ -72,6 +72,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { justifyContent: 'center', height: 28, marginVertical: 10, + overflow: 'hidden', }, text: { fontSize: 14, diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js index 0c0c14e26..ea79afc72 100644 --- a/app/components/markdown/markdown.js +++ b/app/components/markdown/markdown.js @@ -42,6 +42,7 @@ export default class Markdown extends PureComponent { autolinkedUrlSchemes: PropTypes.array.isRequired, baseTextStyle: CustomPropTypes.Style, blockStyles: PropTypes.object, + imageMetadata: PropTypes.object, isEdited: PropTypes.bool, isReplyPost: PropTypes.bool, isSearchResult: PropTypes.bool, @@ -182,6 +183,7 @@ export default class Markdown extends PureComponent { return ( { + const {deviceHeight, deviceWidth, isReplyPost} = this.props; + const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; + return deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0); + }; + handleSizeReceived = (width, height) => { if (!this.mounted) { return; } - const {deviceHeight, deviceWidth, isReplyPost} = this.props; - const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; - const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0); - const dimensions = calculateDimensions(height, width, viewPortWidth); - this.setState({ - ...dimensions, originalHeight: height, originalWidth: width, }); @@ -186,7 +190,9 @@ export default class MarkdownImage extends React.Component { }; loadImageSize = (source) => { - Image.getSize(source, this.handleSizeReceived, this.handleSizeFailed); + if (!this.state.originalWidth) { + Image.getSize(source, this.handleSizeReceived, this.handleSizeFailed); + } }; setImageUrl = (imageURL) => { @@ -198,7 +204,8 @@ export default class MarkdownImage extends React.Component { render() { let image = null; - const {height, uri, width} = this.state; + const {originalHeight, originalWidth, uri} = this.state; + const {height, width} = calculateDimensions(originalHeight, originalWidth, this.getViewPortWidth()); if (width && height) { if (Platform.OS === 'android' && (width > ANDROID_MAX_WIDTH || height > ANDROID_MAX_HEIGHT)) { diff --git a/app/components/message_attachments/attachement_pretext.js b/app/components/message_attachments/attachement_pretext.js new file mode 100644 index 000000000..48080bfac --- /dev/null +++ b/app/components/message_attachments/attachement_pretext.js @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {StyleSheet, View} from 'react-native'; +import PropTypes from 'prop-types'; + +import Markdown from 'app/components/markdown'; +import CustomPropTypes from 'app/constants/custom_prop_types'; + +export default class AttachmentPreText extends PureComponent { + static propTypes = { + baseTextStyle: CustomPropTypes.Style.isRequired, + blockStyles: PropTypes.object.isRequired, + metadata: PropTypes.object, + navigator: PropTypes.object.isRequired, + onPermalinkPress: PropTypes.func, + textStyles: PropTypes.object.isRequired, + value: PropTypes.string, + }; + + render() { + const { + baseTextStyle, + blockStyles, + metadata, + navigator, + onPermalinkPress, + value, + textStyles, + } = this.props; + + if (!value) { + return null; + } + + return ( + + + + ); + } +} + +const style = StyleSheet.create({ + container: { + marginTop: 5, + }, +}); diff --git a/app/components/message_attachments/attachment_actions.js b/app/components/message_attachments/attachment_actions.js new file mode 100644 index 000000000..186c95b54 --- /dev/null +++ b/app/components/message_attachments/attachment_actions.js @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {StyleSheet, View} from 'react-native'; +import PropTypes from 'prop-types'; + +import ActionMenu from './action_menu'; +import ActionButton from './action_button'; + +export default class AttachmentActions extends PureComponent { + static propTypes = { + actions: PropTypes.array, + navigator: PropTypes.object.isRequired, + postId: PropTypes.string.isRequired, + }; + + render() { + const { + actions, + navigator, + postId, + } = this.props; + + if (!actions?.length) { + return null; + } + + const content = []; + + actions.forEach((action) => { + if (!action.id || !action.name) { + return; + } + + switch (action.type) { + case 'select': + content.push( + + ); + break; + case 'button': + default: + content.push( + + ); + break; + } + }); + + return ( + + {content} + + ); + } +} + +const style = StyleSheet.create({ + container: { + flex: 1, + }, +}); diff --git a/app/components/message_attachments/attachment_author.js b/app/components/message_attachments/attachment_author.js new file mode 100644 index 000000000..e9475dd52 --- /dev/null +++ b/app/components/message_attachments/attachment_author.js @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {Image, Linking, Text, View} from 'react-native'; +import PropTypes from 'prop-types'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class AttachmentAuthor extends PureComponent { + static propTypes = { + icon: PropTypes.string, + link: PropTypes.string, + name: PropTypes.string, + theme: PropTypes.object.isRequired, + }; + + openLink = () => { + const {link} = this.props; + if (link && Linking.canOpenURL(link)) { + Linking.openURL(link); + } + }; + + render() { + const { + icon, + link, + name, + theme, + } = this.props; + + if (!icon && !name) { + return null; + } + + const style = getStyleSheet(theme); + + return ( + + {Boolean(icon) && + + } + {Boolean(name) && + + {name} + + } + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + flexDirection: 'row', + }, + name: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 11, + }, + icon: { + height: 12, + marginRight: 3, + width: 12, + }, + link: { + color: changeOpacity(theme.linkColor, 0.5), + }, + }; +}); diff --git a/app/components/message_attachments/attachment_fields.js b/app/components/message_attachments/attachment_fields.js new file mode 100644 index 000000000..cb96cff17 --- /dev/null +++ b/app/components/message_attachments/attachment_fields.js @@ -0,0 +1,143 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {Text, View} from 'react-native'; +import PropTypes from 'prop-types'; + +import Markdown from 'app/components/markdown'; +import CustomPropTypes from 'app/constants/custom_prop_types'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class AttachmentFields extends PureComponent { + static propTypes = { + baseTextStyle: CustomPropTypes.Style.isRequired, + blockStyles: PropTypes.object.isRequired, + fields: PropTypes.array, + metadata: PropTypes.object, + navigator: PropTypes.object.isRequired, + onPermalinkPress: PropTypes.func, + textStyles: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + }; + + render() { + const { + baseTextStyle, + blockStyles, + fields, + metadata, + navigator, + onPermalinkPress, + textStyles, + theme, + } = this.props; + + if (!fields?.length) { + return null; + } + + const style = getStyleSheet(theme); + 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} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + field: { + alignSelf: 'stretch', + flexDirection: 'row', + }, + flex: { + flex: 1, + }, + headingContainer: { + alignSelf: 'stretch', + flexDirection: 'row', + marginBottom: 5, + marginTop: 10, + }, + heading: { + color: theme.centerChannelColor, + fontWeight: '600', + }, + table: { + flex: 1, + flexDirection: 'row', + }, + }; +}); diff --git a/app/components/message_attachments/attachment_image.js b/app/components/message_attachments/attachment_image.js new file mode 100644 index 000000000..1eafb2c02 --- /dev/null +++ b/app/components/message_attachments/attachment_image.js @@ -0,0 +1,169 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Image, TouchableWithoutFeedback, View} from 'react-native'; + +import ProgressiveImage from 'app/components/progressive_image'; +import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; +import ImageCacheManager from 'app/utils/image_cache_manager'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +const VIEWPORT_IMAGE_OFFSET = 100; +const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10; + +export default class AttachmentImage extends PureComponent { + static propTypes = { + deviceHeight: PropTypes.number.isRequired, + deviceWidth: PropTypes.number.isRequired, + imageUrl: PropTypes.string, + metadata: PropTypes.object, + navigator: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + hasImage: Boolean(props.imageUrl), + imageUri: null, + }; + } + + componentDidMount() { + this.mounted = true; + const {imageUrl, metadata} = this.props; + + this.setViewPortMaxWidth(); + if (metadata?.images[imageUrl]) { + const img = metadata.images[imageUrl]; + this.setImageDimensionsFromMeta(null, img); + } + + if (imageUrl) { + ImageCacheManager.cache(null, imageUrl, this.setImageUrl); + } + } + + handlePreviewImage = () => { + const {imageUrl, navigator} = this.props; + const { + imageUri: uri, + originalHeight, + originalWidth, + } = this.state; + + let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?')); + const extension = filename.split('.').pop(); + + if (extension === filename) { + const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.')); + filename = `${filename}${ext}`; + } + + const files = [{ + caption: filename, + dimensions: { + height: originalHeight, + width: originalWidth, + }, + source: {uri}, + data: { + localPath: uri, + }, + }]; + previewImageAtIndex(navigator, [this.refs.item], 0, files); + }; + + setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => { + if (this.mounted) { + this.setState({ + ...dimensions, + originalWidth, + originalHeight, + imageUri, + }); + } + }; + + setImageDimensionsFromMeta = (imageUri, img) => { + const dimensions = calculateDimensions(img.height, img.width, this.maxImageWidth); + this.setImageDimensions(imageUri, dimensions, img.width, img.height); + }; + + setImageUrl = (imageURL) => { + const {imageUrl: attachmentImageUrl, metadata} = this.props; + + if (metadata?.images[attachmentImageUrl]) { + this.setImageDimensionsFromMeta(imageURL, metadata.images[attachmentImageUrl]); + return; + } + + Image.getSize(imageURL, (width, height) => { + const dimensions = calculateDimensions(height, width, this.maxImageWidth); + this.setImageDimensions(imageURL, dimensions, width, height); + }, () => null); + }; + + setViewPortMaxWidth = () => { + const {deviceWidth, deviceHeight} = this.props; + const viewPortWidth = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; + this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET; + }; + + render() { + const {theme} = this.props; + const {hasImage, height, imageUri, width} = this.state; + + if (!hasImage) { + return null; + } + + const style = getStyleSheet(theme); + + let progressiveImage; + if (imageUri) { + progressiveImage = ( + + ); + } else { + progressiveImage = (); + } + + return ( + + + {progressiveImage} + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + marginTop: 5, + }, + imageContainer: { + borderColor: changeOpacity(theme.centerChannelColor, 0.1), + borderWidth: 1, + borderRadius: 2, + flex: 1, + padding: 5, + }, + }; +}); diff --git a/app/components/message_attachments/attachment_text.js b/app/components/message_attachments/attachment_text.js new file mode 100644 index 000000000..4cac8f821 --- /dev/null +++ b/app/components/message_attachments/attachment_text.js @@ -0,0 +1,111 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {StyleSheet, View} from 'react-native'; +import PropTypes from 'prop-types'; + +import Markdown from 'app/components/markdown'; +import ShowMoreButton from 'app/components/show_more_button'; +import CustomPropTypes from 'app/constants/custom_prop_types'; + +export default class AttachmentText extends PureComponent { + static propTypes = { + baseTextStyle: CustomPropTypes.Style.isRequired, + blockStyles: PropTypes.object.isRequired, + deviceHeight: PropTypes.number.isRequired, + hasThumbnail: PropTypes.bool, + metadata: PropTypes.object, + navigator: PropTypes.object.isRequired, + onPermalinkPress: PropTypes.func, + textStyles: PropTypes.object.isRequired, + value: PropTypes.string, + }; + + static getDerivedStateFromProps(nextProps, prevState) { + const {deviceHeight} = nextProps; + const maxHeight = deviceHeight * 0.4; + + if (maxHeight !== prevState.maxHeight) { + return { + maxHeight, + }; + } + + return null; + } + + constructor(props) { + super(props); + + this.state = { + collapsed: true, + isLongText: false, + }; + } + + handleLayout = (event) => { + const {height} = event.nativeEvent.layout; + const {deviceHeight} = this.props; + + if (height >= (deviceHeight * 0.6)) { + this.setState({ + isLongText: true, + }); + } + }; + + toggleCollapseState = () => { + const {collapsed} = this.state; + this.setState({collapsed: !collapsed}); + }; + + render() { + const { + baseTextStyle, + blockStyles, + hasThumbnail, + metadata, + navigator, + onPermalinkPress, + value, + textStyles, + } = this.props; + const {collapsed, isLongText, maxHeight} = this.state; + + if (!value) { + return null; + } + + return ( + + + + + {isLongText && + + } + + ); + } +} + +const style = StyleSheet.create({ + container: { + paddingRight: 60, + }, +}); diff --git a/app/components/message_attachments/attachment_thumbnail.js b/app/components/message_attachments/attachment_thumbnail.js new file mode 100644 index 000000000..fe7f443be --- /dev/null +++ b/app/components/message_attachments/attachment_thumbnail.js @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {Image, StyleSheet, View} from 'react-native'; +import PropTypes from 'prop-types'; + +export default class AttachmentThumbnail extends PureComponent { + static propTypes = { + url: PropTypes.string, + }; + + render() { + const {url: uri} = this.props; + + if (!uri) { + return null; + } + + return ( + + + + ); + } +} + +const style = StyleSheet.create({ + container: { + position: 'absolute', + right: 10, + top: 10, + }, + image: { + height: 45, + width: 45, + }, +}); diff --git a/app/components/message_attachments/attachment_title.js b/app/components/message_attachments/attachment_title.js new file mode 100644 index 000000000..c50c8aef2 --- /dev/null +++ b/app/components/message_attachments/attachment_title.js @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import {Linking, Text, View} from 'react-native'; +import PropTypes from 'prop-types'; + +import {makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class AttachmentTitle extends PureComponent { + static propTypes = { + link: PropTypes.string, + theme: PropTypes.object.isRequired, + value: PropTypes.string, + }; + + openLink = () => { + const {link} = this.props; + if (link && Linking.canOpenURL(link)) { + Linking.openURL(link); + } + }; + + render() { + const { + link, + value, + theme, + } = this.props; + + if (!value) { + return null; + } + + const style = getStyleSheet(theme); + + return ( + + + {value} + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + flexDirection: 'row', + }, + title: { + color: theme.centerChannelColor, + fontWeight: '600', + marginBottom: 5, + }, + link: { + color: theme.linkColor, + }, + }; +}); diff --git a/app/components/message_attachments/index.js b/app/components/message_attachments/index.js index da6e1ddbe..e3b287545 100644 --- a/app/components/message_attachments/index.js +++ b/app/components/message_attachments/index.js @@ -14,7 +14,10 @@ export default class MessageAttachments extends PureComponent { attachments: PropTypes.array.isRequired, baseTextStyle: CustomPropTypes.Style, blockStyles: PropTypes.object, + deviceHeight: PropTypes.number.isRequired, + deviceWidth: PropTypes.number.isRequired, postId: PropTypes.string.isRequired, + metadata: PropTypes.object, navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, @@ -27,6 +30,9 @@ export default class MessageAttachments extends PureComponent { attachments, baseTextStyle, blockStyles, + deviceHeight, + deviceWidth, + metadata, navigator, onHashtagPress, onPermalinkPress, @@ -42,7 +48,10 @@ export default class MessageAttachments extends PureComponent { attachment={attachment} baseTextStyle={baseTextStyle} blockStyles={blockStyles} + deviceHeight={deviceHeight} + deviceWidth={deviceWidth} key={'att_' + i} + metadata={metadata} navigator={navigator} onHashtagPress={onHashtagPress} onPermalinkPress={onPermalinkPress} diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js index 7e6131dd0..bbacfe925 100644 --- a/app/components/message_attachments/message_attachment.js +++ b/app/components/message_attachments/message_attachment.js @@ -3,30 +3,20 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import { - Dimensions, - Image, - Linking, - Platform, - Text, - TouchableWithoutFeedback, - View, -} from 'react-native'; - -import Markdown from 'app/components/markdown'; -import ProgressiveImage from 'app/components/progressive_image'; -import ShowMoreButton from 'app/components/show_more_button'; +import {View} from 'react-native'; import CustomPropTypes from 'app/constants/custom_prop_types'; -import ImageCacheManager from 'app/utils/image_cache_manager'; -import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import ActionButton from './action_button'; -import ActionMenu from './action_menu'; +import AttachmentActions from './attachment_actions'; +import AttachmentAuthor from './attachment_author'; +import AttachmentFields from './attachment_fields'; +import AttachmentImage from './attachment_image'; +import AttachmentPreText from './attachement_pretext'; +import AttachmentText from './attachment_text'; +import AttachmentThumbnail from './attachment_thumbnail'; +import AttachmentTitle from './attachment_title'; -const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10; -const VIEWPORT_IMAGE_OFFSET = 32; const STATUS_COLORS = { good: '#00c100', warning: '#dede01', @@ -38,6 +28,9 @@ export default class MessageAttachment extends PureComponent { attachment: PropTypes.object.isRequired, baseTextStyle: CustomPropTypes.Style, blockStyles: PropTypes.object, + deviceHeight: PropTypes.number.isRequired, + deviceWidth: PropTypes.number.isRequired, + metadata: PropTypes.object, navigator: PropTypes.object.isRequired, postId: PropTypes.string.isRequired, onPermalinkPress: PropTypes.func, @@ -45,282 +38,23 @@ export default class MessageAttachment extends PureComponent { textStyles: PropTypes.object, }; - constructor(props) { - super(props); - - this.state = { - collapsed: true, - imageUri: null, - isLongText: false, - }; - } - - componentWillMount() { - if (this.props.attachment.image_url) { - ImageCacheManager.cache(null, this.props.attachment.image_url, this.setImageUrl); - } - } - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - getActionView = (style) => { - const {attachment, postId, navigator} = this.props; - const {actions} = attachment; - - if (!actions || !actions.length) { - return null; - } - - const content = []; - - actions.forEach((action) => { - if (!action.id || !action.name) { - return; - } - - switch (action.type) { - case 'select': - content.push( - - ); - break; - case 'button': - default: - content.push( - - ); - break; - } - }); - - return ( - - {content} - - ); - }; - - measurePost = (event) => { - const {height} = event.nativeEvent.layout; - const {height: deviceHeight} = Dimensions.get('window'); - - if (height >= (deviceHeight * 0.6)) { - this.setState({ - isLongText: true, - maxHeight: (deviceHeight * 0.4), - }); - } - }; - - getFieldsTable = (style) => { + render() { const { attachment, baseTextStyle, blockStyles, + deviceHeight, + deviceWidth, + metadata, navigator, onPermalinkPress, + postId, 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} - - ); - }; - - handleLayout = (event) => { - if (!this.maxImageWidth) { - const {height, width} = event.nativeEvent.layout; - const viewPortWidth = width > height ? height : width; - this.maxImageWidth = viewPortWidth - VIEWPORT_IMAGE_OFFSET; - } - }; - - handlePreviewImage = () => { - const {attachment, navigator} = this.props; - const { - imageUri: uri, - originalHeight, - originalWidth, - } = this.state; - const link = attachment.image_url; - let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?')); - const extension = filename.split('.').pop(); - - if (extension === filename) { - const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.')); - filename = `${filename}${ext}`; - } - - const files = [{ - caption: filename, - dimensions: { - height: originalHeight, - width: originalWidth, - }, - source: {uri}, - data: { - localPath: uri, - }, - }]; - previewImageAtIndex(navigator, [this.refs.item], 0, files); - }; - - openLink = (link) => { - if (Linking.canOpenURL(link)) { - Linking.openURL(link); - } - }; - - setImageUrl = (imageUri) => { - Image.getSize(imageUri, (width, height) => { - const dimensions = calculateDimensions(height, width, this.maxImageWidth); - if (this.mounted) { - this.setState({ - ...dimensions, - originalWidth: width, - originalHeight: height, - imageUri, - }); - } - }, () => null); - }; - - toggleCollapseState = () => { - this.setState((prevState) => ({collapsed: !prevState.collapsed})); - }; - - render() { // eslint-disable-line complexity - const { - attachment, - baseTextStyle, - blockStyles, - textStyles, - navigator, - onPermalinkPress, theme, } = this.props; - const { - height, - imageUri, - width, - collapsed, - isLongText, - maxHeight, - } = this.state; - const style = getStyleSheet(theme); - let preText; - if (attachment.pretext) { - preText = ( - - - - ); - } - let borderStyle; if (attachment.color) { if (attachment.color[0] === '#') { @@ -330,144 +64,66 @@ export default class MessageAttachment extends PureComponent { } } - 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) { - text = ( - - - - - {isLongText && - - } - - ); - } - - const fields = this.getFieldsTable(style); - const actions = this.getActionView(style); - - let image; - if (imageUri) { - image = ( - - - - - - ); - } - return ( - {preText} + - - {author} - - - {title} - - {thumb} - {text} - {fields} - {actions} - {image} + + + + + + + ); @@ -490,57 +146,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { 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', - marginBottom: 5, - }, - titleLink: { - color: theme.linkColor, - }, - topContent: { - paddingRight: 60, - }, - thumbContainer: { - position: 'absolute', - right: 10, - top: 10, - }, - thumb: { - height: 45, - width: 45, - }, - headingContainer: { - alignSelf: 'stretch', - flexDirection: 'row', - marginBottom: 5, - marginTop: 10, - }, - heading: { - color: theme.centerChannelColor, - fontWeight: '600', - }, - bodyContainer: { - flex: 1, - }, - imageContainer: { - borderColor: changeOpacity(theme.centerChannelColor, 0.1), - borderWidth: 1, - borderRadius: 2, - marginTop: 5, - padding: 5, - }, }; }); diff --git a/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap b/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap index 214821d9c..230481de0 100644 --- a/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap +++ b/app/components/post_attachment_opengraph/__snapshots__/post_attachment_opengraph.test.js.snap @@ -73,6 +73,42 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri + + + + + `; @@ -213,23 +249,23 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage 2`] = ` { if (!openGraphData) { this.props.actions.getOpenGraphMetadata(url); } - } + }; - getBestImageUrl(data) { + getBestImageUrl = (data) => { if (!data || !data.images) { - return; + return { + hasImage: false, + }; } + const {imageMetadata} = this.props; const bestDimensions = { width: this.getViewPostWidth(), height: MAX_IMAGE_HEIGHT, @@ -86,17 +86,30 @@ export default class PostAttachmentOpenGraph extends PureComponent { const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height'); const imageUrl = bestImage.secure_url || bestImage.url; + let ogImage; + if (imageMetadata && imageMetadata[imageUrl]) { + ogImage = imageMetadata[imageUrl]; + } - this.setState({ - hasImage: true, - ...bestDimensions, - openGraphImageUrl: imageUrl, - }); + if (!ogImage) { + ogImage = data.images.find((i) => i.url === imageUrl || i.secure_url === imageUrl); + } + + let dimensions = bestDimensions; + if (ogImage?.width && ogImage?.height) { + dimensions = calculateDimensions(ogImage.height, ogImage.width, this.getViewPostWidth()); + } if (imageUrl) { ImageCacheManager.cache(this.getFilename(imageUrl), imageUrl, this.getImageSize); } - } + + return { + hasImage: true, + ...dimensions, + openGraphImageUrl: imageUrl, + }; + }; getFilename = (link) => { let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?')); @@ -107,22 +120,42 @@ export default class PostAttachmentOpenGraph extends PureComponent { filename = `${filename}${ext}`; } - return `og-${filename}`; + return `og-${filename.replace(/:/g, '-')}`; }; getImageSize = (imageUrl) => { - Image.getSize(imageUrl, (width, height) => { - const dimensions = calculateDimensions(height, width, this.getViewPostWidth()); + const {imageMetadata, openGraphData} = this.props; + const {openGraphImageUrl} = this.state; - if (this.mounted) { - this.setState({ - ...dimensions, - originalHeight: height, - originalWidth: width, - imageUrl, - }); - } - }, () => null); + let ogImage; + if (imageMetadata && imageMetadata[openGraphImageUrl]) { + ogImage = imageMetadata[openGraphImageUrl]; + } + + if (!ogImage) { + ogImage = openGraphData.images.find((i) => i.url === openGraphImageUrl || i.secure_url === openGraphImageUrl); + } + + if (ogImage?.width && ogImage?.height) { + this.setImageSize(imageUrl, ogImage.width, ogImage.height); + } else { + Image.getSize(imageUrl, (width, height) => { + this.setImageSize(imageUrl, width, height); + }, () => null); + } + }; + + setImageSize = (imageUrl, originalWidth, originalHeight) => { + if (this.mounted) { + const dimensions = calculateDimensions(originalHeight, originalWidth, this.getViewPostWidth()); + + this.setState({ + imageUrl, + originalWidth, + originalHeight, + ...dimensions, + }); + } }; getViewPostWidth = () => { @@ -180,7 +213,7 @@ export default class PostAttachmentOpenGraph extends PureComponent { ); - } + }; renderImage = () => { if (!this.state.hasImage) { @@ -201,11 +234,10 @@ export default class PostAttachmentOpenGraph extends PureComponent { return ( ); - } + }; render() { const { @@ -225,12 +257,12 @@ export default class PostAttachmentOpenGraph extends PureComponent { theme, } = this.props; + const style = getStyleSheet(theme); + if (!openGraphData) { return null; } - const style = getStyleSheet(theme); - let siteName; if (openGraphData.site_name) { siteName = ( diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js index a436b873c..146dd4fa4 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.test.js @@ -18,6 +18,9 @@ describe('PostAttachmentOpenGraph', () => { site_name: 'Mattermost', title: 'Title', url: 'https://mattermost.com/', + images: [{ + secure_url: 'https://www.mattermost.org/wp-content/uploads/2016/03/logoHorizontal_WS.png', + }], }; const baseProps = { actions: { @@ -25,6 +28,12 @@ describe('PostAttachmentOpenGraph', () => { }, deviceHeight: 600, deviceWidth: 400, + imageMetadata: { + 'https://www.mattermost.org/wp-content/uploads/2016/03/logoHorizontal_WS.png': { + width: 1165, + height: 265, + }, + }, isReplyPost: false, link: 'https://mattermost.com/', navigator: {}, diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js index 0926f1f96..1ca1438df 100644 --- a/app/components/post_body/index.js +++ b/app/components/post_body/index.js @@ -21,6 +21,8 @@ import { } from 'mattermost-redux/utils/post_utils'; import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; +import {getDimensions} from 'app/selectors/device'; + import {hasEmojisOnly} from 'app/utils/emoji_utils'; import PostBody from './post_body'; @@ -76,6 +78,7 @@ function makeMapStateToProps() { const {isEmojiOnly, shouldRenderJumboEmoji} = memoizeHasEmojisOnly(post.message, customEmojis); return { + metadata: post.metadata, postProps: post.props || {}, postType: post.type || '', fileIds: post.file_ids, @@ -92,6 +95,7 @@ function makeMapStateToProps() { shouldRenderJumboEmoji, theme: getTheme(state), canDelete, + ...getDimensions(state), }; }; } diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 33dbaf3c8..8216ea0c1 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -4,7 +4,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Dimensions, TouchableOpacity, View, } from 'react-native'; @@ -29,10 +28,13 @@ let PostAddChannelMember; let PostBodyAdditionalContent; let Reactions; +const SHOW_MORE_HEIGHT = 60; + export default class PostBody extends PureComponent { static propTypes = { canDelete: PropTypes.bool, channelIsReadOnly: PropTypes.bool.isRequired, + deviceHeight: PropTypes.number.isRequired, fileIds: PropTypes.array, hasBeenDeleted: PropTypes.bool, hasBeenEdited: PropTypes.bool, @@ -46,6 +48,7 @@ export default class PostBody extends PureComponent { isReplyPost: PropTypes.bool, isSearchResult: PropTypes.bool, isSystemMessage: PropTypes.bool, + metadata: PropTypes.object, managedConfig: PropTypes.object, message: PropTypes.string, navigator: PropTypes.object.isRequired, @@ -75,19 +78,28 @@ export default class PostBody extends PureComponent { intl: intlShape.isRequired, }; + static getDerivedStateFromProps(nextProps, prevState) { + const maxHeight = (nextProps.deviceHeight * 0.6) + SHOW_MORE_HEIGHT; + if (maxHeight !== prevState.maxHeight) { + return { + maxHeight, + }; + } + + return null; + } + state = { isLongPost: false, }; measurePost = (event) => { const {height} = event.nativeEvent.layout; - const {height: deviceHeight} = Dimensions.get('window'); - const {showLongPost} = this.props; + const {deviceHeight, showLongPost} = this.props; if (!showLongPost && height >= (deviceHeight * 1.2)) { this.setState({ isLongPost: true, - maxHeight: (deviceHeight * 0.6), }); } }; @@ -207,7 +219,7 @@ export default class PostBody extends PureComponent { } let attachments; - if (fileIds.length > 0) { + if (fileIds.length) { if (!FileAttachmentList) { FileAttachmentList = require('app/components/file_attachment_list').default; } @@ -226,7 +238,11 @@ export default class PostBody extends PureComponent { } renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => { - const {isReplyPost, message, navigator, onHashtagPress, onPermalinkPress, postId, postProps} = this.props; + const {isReplyPost, message, navigator, onHashtagPress, onPermalinkPress, postId, postProps, metadata} = this.props; + + if (metadata && !metadata.embeds) { + return null; + } if (!PostBodyAdditionalContent) { PostBodyAdditionalContent = require('app/components/post_body_additional_content').default; @@ -238,6 +254,7 @@ export default class PostBody extends PureComponent { blockStyles={blockStyles} navigator={navigator} message={message} + metadata={metadata} postId={postId} postProps={postProps} textStyles={textStyles} @@ -286,6 +303,7 @@ export default class PostBody extends PureComponent { isSearchResult, isSystemMessage, message, + metadata, navigator, onFailedPostPress, onHashtagPress, @@ -351,12 +369,13 @@ export default class PostBody extends PureComponent { messageComponent = ( { return { flex: { flex: 1, + overflow: 'hidden', }, row: { flexDirection: 'row', diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index 7ec33087f..6f739b06b 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -26,6 +26,7 @@ import {getYouTubeVideoId, isImageLink, isYoutubeLink} from 'app/utils/url'; const VIEWPORT_IMAGE_OFFSET = 66; const VIEWPORT_IMAGE_REPLY_OFFSET = 13; const MAX_YOUTUBE_IMAGE_HEIGHT = 150; +const MAX_YOUTUBE_IMAGE_WIDTH = 297; let MessageAttachments; let PostAttachmentOpenGraph; @@ -39,6 +40,7 @@ export default class PostBodyAdditionalContent extends PureComponent { googleDeveloperKey: PropTypes.string, deviceHeight: PropTypes.number.isRequired, deviceWidth: PropTypes.number.isRequired, + metadata: PropTypes.object, isReplyPost: PropTypes.bool, link: PropTypes.string, message: PropTypes.string.isRequired, @@ -60,12 +62,23 @@ export default class PostBodyAdditionalContent extends PureComponent { constructor(props) { super(props); + let dimensions = { + height: 0, + width: 0, + }; + + if (this.isImage() && props.metadata && props.metadata.images) { + const img = props.metadata.images[props.link]; + if (img && img.height && img.width) { + dimensions = calculateDimensions(img.height, img.width, this.getViewPortWidth(props)); + } + } + this.state = { linkLoadError: false, linkLoaded: false, - width: 0, - height: 0, shortenedLink: null, + ...dimensions, }; this.mounted = false; @@ -86,11 +99,25 @@ export default class PostBodyAdditionalContent extends PureComponent { } } + isImage = (specificLink) => { + const {metadata, link} = this.props; + + if (isImageLink(specificLink || link)) { + return true; + } + + if (metadata && metadata.images) { + return Boolean(metadata.images[specificLink] || metadata.images[link]); + } + + return false; + }; + load = async (props) => { const {link} = props; if (link) { let imageUrl; - if (isImageLink(link)) { + if (this.isImage()) { imageUrl = link; } else if (isYoutubeLink(link)) { const videoId = getYouTubeVideoId(link); @@ -105,7 +132,7 @@ export default class PostBodyAdditionalContent extends PureComponent { } if (shortenedLink) { - if (isImageLink(shortenedLink)) { + if (this.isImage(shortenedLink)) { imageUrl = shortenedLink; } else if (isYoutubeLink(shortenedLink)) { const videoId = getYouTubeVideoId(shortenedLink); @@ -151,22 +178,28 @@ export default class PostBodyAdditionalContent extends PureComponent { return null; } - const {isReplyPost, link, navigator, openGraphData, showLinkPreviews, theme} = this.props; + const {isReplyPost, link, metadata, navigator, openGraphData, showLinkPreviews, theme} = this.props; const attachments = this.getMessageAttachment(); if (attachments) { return attachments; } + if (!openGraphData) { + return null; + } + if (link && showLinkPreviews) { if (!PostAttachmentOpenGraph) { PostAttachmentOpenGraph = require('app/components/post_attachment_opengraph').default; } + return ( ); @@ -199,7 +232,7 @@ export default class PostBodyAdditionalContent extends PureComponent { { - const {deviceHeight, deviceWidth, link, isReplyPost} = this.props; - const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; - const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0); + const {link, metadata} = this.props; + let img; if (link && path) { - Image.getSize(path, (width, height) => { - if (!this.mounted) { - return; - } + if (metadata && metadata.images) { + img = metadata.images[link]; + } - if (!width && !height) { - this.setState({linkLoadError: true}); - return; - } - - let dimensions; - if (isYoutubeLink(link)) { - dimensions = this.calculateYouTubeImageDimensions(width, height); - } else { - dimensions = calculateDimensions(height, width, viewPortWidth); - } - - this.setState({ - ...dimensions, - originalHeight: height, - originalWidth: width, - linkLoaded: true, - uri: path, - }); - }, () => this.setState({linkLoadError: true})); + if (img && img.height && img.width) { + this.setImageSize(path, img.width, img.height); + } else { + Image.getSize(path, (width, height) => { + this.setImageSize(path, width, height); + }, () => this.setState({linkLoadError: true})); + } } }; + getViewPortWidth = (props) => { + const {deviceHeight, deviceWidth, isReplyPost} = props; + const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; + return deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0); + }; + + setImageSize = (uri, originalWidth, originalHeight) => { + if (!this.mounted) { + return; + } + + if (!originalWidth && !originalHeight) { + this.setState({linkLoadError: true}); + return; + } + + const {link} = this.props; + const viewPortWidth = this.getViewPortWidth(this.props); + + let dimensions; + if (isYoutubeLink(link)) { + dimensions = this.calculateYouTubeImageDimensions(originalWidth, originalHeight); + } else { + dimensions = calculateDimensions(originalHeight, originalWidth, viewPortWidth); + } + + this.setState({ + ...dimensions, + originalHeight, + originalWidth, + linkLoaded: true, + uri, + }); + }; + getMessageAttachment = () => { const { postId, postProps, baseTextStyle, blockStyles, + deviceHeight, + deviceWidth, + metadata, navigator, onHashtagPress, onPermalinkPress, @@ -300,6 +356,9 @@ export default class PostBodyAdditionalContent extends PureComponent { attachments={attachments} baseTextStyle={baseTextStyle} blockStyles={blockStyles} + deviceHeight={deviceHeight} + deviceWidth={deviceWidth} + metadata={metadata} navigator={navigator} postId={postId} textStyles={textStyles} @@ -430,8 +489,8 @@ export default class PostBodyAdditionalContent extends PureComponent { } const isYouTube = isYoutubeLink(link); - const isImage = isImageLink(link); - const isOpenGraph = Boolean(openGraphData && openGraphData.description); + const isImage = this.isImage(); + const isOpenGraph = Boolean(openGraphData); if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) { const embed = this.generateToggleableEmbed(isImage, isYouTube); diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js index bd6d69439..e8d2d9399 100644 --- a/app/components/reactions/reactions.js +++ b/app/components/reactions/reactions.js @@ -44,8 +44,10 @@ export default class Reactions extends PureComponent { }; componentDidMount() { - const {actions, postId} = this.props; - actions.getReactionsForPost(postId); + const {actions, postId, reactions} = this.props; + if (!reactions?.size) { + actions.getReactionsForPost(postId); + } } handleAddReaction = preventDoubleTap(() => { diff --git a/package-lock.json b/package-lock.json index 293da7379..51c022016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8229,8 +8229,8 @@ "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" }, "mattermost-redux": { - "version": "github:mattermost/mattermost-redux#b1f68abe9ee70540aeee5ffc755d94248c358feb", - "from": "github:mattermost/mattermost-redux#b1f68abe9ee70540aeee5ffc755d94248c358feb", + "version": "github:mattermost/mattermost-redux#b4b186513b80108aec0edfdf19e7577965f5224a", + "from": "github:mattermost/mattermost-redux#b4b186513b80108aec0edfdf19e7577965f5224a", "requires": { "deep-equal": "1.0.1", "eslint-plugin-header": "1.2.0", diff --git a/package.json b/package.json index 4945681e2..842e7c716 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "intl": "1.2.5", "jail-monkey": "1.0.0", "jsc-android": "236355.1.0", - "mattermost-redux": "github:mattermost/mattermost-redux#b1f68abe9ee70540aeee5ffc755d94248c358feb", + "mattermost-redux": "github:mattermost/mattermost-redux#b4b186513b80108aec0edfdf19e7577965f5224a", "mime-db": "1.37.0", "moment-timezone": "0.5.23", "prop-types": "15.6.2",