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
This commit is contained in:
Elias Nahum 2018-11-22 19:47:27 -03:00 committed by GitHub
parent d8abeabd70
commit 736a955be8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1111 additions and 578 deletions

View file

@ -355,6 +355,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginTop: 60,
marginHorizontal: 12,
marginBottom: 12,
overflow: 'hidden',
},
displayName: {
color: theme.centerChannelColor,

View file

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

View file

@ -72,6 +72,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
height: 28,
marginVertical: 10,
overflow: 'hidden',
},
text: {
fontSize: 14,

View file

@ -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 (
<MarkdownImage
linkDestination={linkDestination}
imageMetadata={this.props.imageMetadata}
isReplyPost={this.props.isReplyPost}
navigator={this.props.navigator}
source={src}

View file

@ -36,6 +36,7 @@ export default class MarkdownImage extends React.Component {
children: PropTypes.node,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imageMetadata: PropTypes.object,
linkDestination: PropTypes.string,
isReplyPost: PropTypes.bool,
navigator: PropTypes.object.isRequired,
@ -51,9 +52,10 @@ export default class MarkdownImage extends React.Component {
constructor(props) {
super(props);
const dimensions = props?.imageMetadata?.[props.source];
this.state = {
width: 0,
height: 0,
originalHeight: dimensions?.height || 0,
originalWidth: dimensions?.width || 0,
failed: false,
uri: null,
};
@ -71,10 +73,12 @@ export default class MarkdownImage extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.source !== nextProps.source) {
const dimensions = nextProps?.imageMetadata?.[nextProps.source];
this.setState({
width: 0,
height: 0,
failed: false,
originalHeight: dimensions?.height || 0,
originalWidth: dimensions?.width || 0,
});
// getSource also depends on serverURL, but that shouldn't change while this is mounted
@ -96,18 +100,18 @@ export default class MarkdownImage extends React.Component {
return source;
};
getViewPortWidth = () => {
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)) {

View file

@ -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 (
<View style={style.container}>
<Markdown
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
imageMetadata={metadata?.images}
value={value}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
/>
</View>
);
}
}
const style = StyleSheet.create({
container: {
marginTop: 5,
},
});

View file

@ -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(
<ActionMenu
key={action.id}
id={action.id}
name={action.name}
dataSource={action.data_source}
options={action.options}
postId={postId}
navigator={navigator}
/>
);
break;
case 'button':
default:
content.push(
<ActionButton
key={action.id}
id={action.id}
name={action.name}
postId={postId}
/>
);
break;
}
});
return (
<View style={style.container}>
{content}
</View>
);
}
}
const style = StyleSheet.create({
container: {
flex: 1,
},
});

View file

@ -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 (
<View style={style.container}>
{Boolean(icon) &&
<Image
source={{uri: icon}}
key='author_icon'
style={style.icon}
/>
}
{Boolean(name) &&
<Text
key='author_name'
style={[style.name, Boolean(link) && style.link]}
onPress={this.openLink}
>
{name}
</Text>
}
</View>
);
}
}
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),
},
};
});

View file

@ -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(
<View
key={`attachment__table__${nrTables}`}
style={style.field}
>
{fieldInfos}
</View>
);
fieldInfos = [];
rowPos = 0;
nrTables += 1;
lastWasLong = false;
}
fieldInfos.push(
<View
style={style.flex}
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.flex}
key={`attachment__field-${i}__${nrTables}`}
>
<Markdown
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
imageMetadata={metadata?.images}
value={(field.value || '')}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
/>
</View>
</View>
);
rowPos += 1;
lastWasLong = !(field.short === true);
});
if (fieldInfos.length > 0) { // Flush last fields
fieldTables.push(
<View
key={`attachment__table__${nrTables}`}
style={style.table}
>
{fieldInfos}
</View>
);
}
return (
<View>
{fieldTables}
</View>
);
}
}
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',
},
};
});

View file

@ -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 = (
<ProgressiveImage
ref='image'
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
/>
);
} else {
progressiveImage = (<View style={{width, height}}/>);
}
return (
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={[style.container, {width: this.maxImageWidth + VIEWPORT_IMAGE_CONTAINER_OFFSET}]}
>
<View
ref='item'
style={[style.imageContainer, {width, height}]}
>
{progressiveImage}
</View>
</TouchableWithoutFeedback>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginTop: 5,
},
imageContainer: {
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
borderWidth: 1,
borderRadius: 2,
flex: 1,
padding: 5,
},
};
});

View file

@ -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 (
<View style={hasThumbnail && style.container}>
<View
style={[(isLongText && collapsed && {maxHeight, overflow: 'hidden'})]}
removeClippedSubviews={isLongText && collapsed}
>
<Markdown
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
imageMetadata={metadata?.images}
value={value}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
/>
</View>
{isLongText &&
<ShowMoreButton
onPress={this.toggleCollapseState}
showMore={collapsed}
/>
}
</View>
);
}
}
const style = StyleSheet.create({
container: {
paddingRight: 60,
},
});

View file

@ -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 (
<View style={style.container}>
<Image
source={{uri}}
resizeMode='contain'
resizeMethod='scale'
style={style.image}
/>
</View>
);
}
}
const style = StyleSheet.create({
container: {
position: 'absolute',
right: 10,
top: 10,
},
image: {
height: 45,
width: 45,
},
});

View file

@ -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 (
<View style={style.container}>
<Text
style={[style.title, Boolean(link) && style.link]}
onPress={this.openLink}
>
{value}
</Text>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
},
title: {
color: theme.centerChannelColor,
fontWeight: '600',
marginBottom: 5,
},
link: {
color: theme.linkColor,
},
};
});

View file

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

View file

@ -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(
<ActionMenu
key={action.id}
id={action.id}
name={action.name}
dataSource={action.data_source}
options={action.options}
postId={postId}
navigator={navigator}
/>
);
break;
case 'button':
default:
content.push(
<ActionButton
key={action.id}
id={action.id}
name={action.name}
postId={postId}
/>
);
break;
}
});
return (
<View style={style.bodyContainer}>
{content}
</View>
);
};
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(
<View
key={`attachment__table__${nrTables}`}
style={{alignSelf: 'stretch', 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 || '')}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
/>
</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>
);
};
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 = (
<View style={{marginTop: 5}}>
<Markdown
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
value={attachment.pretext}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
/>
</View>
);
}
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(
<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) {
text = (
<View
onLayout={this.measurePost}
style={topStyle}
>
<View
style={[(isLongText && collapsed && {maxHeight, overflow: 'hidden'})]}
removeClippedSubviews={isLongText && collapsed && Platform.OS !== 'android'}
>
<Markdown
baseTextStyle={baseTextStyle}
textStyles={textStyles}
blockStyles={blockStyles}
value={attachment.text}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
/>
</View>
{isLongText &&
<ShowMoreButton
onPress={this.toggleCollapseState}
showMore={collapsed}
/>
}
</View>
);
}
const fields = this.getFieldsTable(style);
const actions = this.getActionView(style);
let image;
if (imageUri) {
image = (
<View
ref='item'
style={[style.imageContainer, {width: this.maxImageWidth + VIEWPORT_IMAGE_CONTAINER_OFFSET}]}
>
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={{height, width}}
>
<ProgressiveImage
ref='image'
style={{height, width}}
imageUri={imageUri}
resizeMode='contain'
/>
</TouchableWithoutFeedback>
</View>
);
}
return (
<View>
{preText}
<AttachmentPreText
baseTextStyle={baseTextStyle}
blockStyles={blockStyles}
metadata={metadata}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
textStyles={textStyles}
value={attachment.pretext}
/>
<View
onLayout={this.handleLayout}
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}
{actions}
{image}
<AttachmentAuthor
icon={attachment.author_icon}
link={attachment.author_link}
name={attachment.author_name}
theme={theme}
/>
<AttachmentTitle
link={attachment.title_link}
theme={theme}
value={attachment.title}
/>
<AttachmentThumbnail url={attachment.thumb_url}/>
<AttachmentText
baseTextStyle={baseTextStyle}
blockStyles={blockStyles}
deviceHeight={deviceHeight}
hasThumbnail={Boolean(attachment.thumb_url)}
metadata={metadata}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
textStyles={textStyles}
value={attachment.text}
/>
<AttachmentFields
baseTextStyle={baseTextStyle}
blockStyles={blockStyles}
fields={attachment.fields}
metadata={metadata}
navigator={navigator}
onPermalinkPress={onPermalinkPress}
textStyles={textStyles}
theme={theme}
/>
<AttachmentActions
actions={attachment.actions}
navigator={navigator}
postId={postId}
/>
<AttachmentImage
deviceHeight={deviceHeight}
deviceWidth={deviceWidth}
imageUrl={attachment.image_url}
metadata={metadata}
navigator={navigator}
theme={theme}
/>
</View>
</View>
);
@ -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,
},
};
});

View file

@ -73,6 +73,42 @@ exports[`PostAttachmentOpenGraph should match snapshot, without image and descri
</Text>
</TouchableOpacity>
</View>
<View
style={
Array [
Object {
"alignItems": "center",
"borderColor": "rgba(61,60,64,0.2)",
"borderRadius": 3,
"borderWidth": 1,
"marginTop": 5,
},
Object {
"height": 69.83261802575107,
"width": 307,
},
]
}
>
<TouchableWithoutFeedback
onPress={[Function]}
>
<Component
resizeMode="contain"
style={
Array [
Object {
"borderRadius": 3,
},
Object {
"height": 69.83261802575107,
"width": 307,
},
]
}
/>
</TouchableWithoutFeedback>
</View>
</View>
`;
@ -213,23 +249,23 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage 2`] = `
<View
style={
Object {
"alignItems": "center",
"borderColor": "rgba(61,60,64,0.2)",
"borderRadius": 3,
"borderWidth": 1,
"marginTop": 5,
}
Array [
Object {
"alignItems": "center",
"borderColor": "rgba(61,60,64,0.2)",
"borderRadius": 3,
"borderWidth": 1,
"marginTop": 5,
},
Object {
"height": 112.56666666666666,
"width": 307,
},
]
}
>
<TouchableWithoutFeedback
onPress={[Function]}
style={
Object {
"height": 150,
"width": 307,
}
}
>
<Component
resizeMode="contain"
@ -239,7 +275,7 @@ exports[`PostAttachmentOpenGraph should match state and snapshot, on renderImage
"borderRadius": 3,
},
Object {
"height": 150,
"height": 112.56666666666666,
"width": 307,
},
]

View file

@ -28,6 +28,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
}).isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
imageMetadata: PropTypes.object,
isReplyPost: PropTypes.bool,
link: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
@ -38,15 +39,11 @@ export default class PostAttachmentOpenGraph extends PureComponent {
constructor(props) {
super(props);
this.state = {
hasImage: false,
imageUrl: null,
};
this.state = this.getBestImageUrl(props.openGraphData);
}
componentDidMount() {
this.mounted = true;
this.getBestImageUrl(this.props.openGraphData);
}
componentWillMount() {
@ -60,7 +57,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
}
if (this.props.openGraphData !== nextProps.openGraphData) {
this.getBestImageUrl(nextProps.openGraphData);
this.setState(this.getBestImageUrl(nextProps.openGraphData));
}
}
@ -68,17 +65,20 @@ export default class PostAttachmentOpenGraph extends PureComponent {
this.mounted = false;
}
fetchData(url, openGraphData) {
fetchData = (url, openGraphData) => {
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 {
</Text>
</View>
);
}
};
renderImage = () => {
if (!this.state.hasImage) {
@ -201,11 +234,10 @@ export default class PostAttachmentOpenGraph extends PureComponent {
return (
<View
ref='item'
style={style.imageContainer}
style={[style.imageContainer, {width, height}]}
>
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={{width, height}}
>
<Image
style={[style.image, {width, height}]}
@ -215,7 +247,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
</TouchableWithoutFeedback>
</View>
);
}
};
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 = (

View file

@ -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: {},

View file

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

View file

@ -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 = (
<View style={style.row}>
<View
style={[style.flex, (isPendingOrFailedPost && style.pendingPost), (isLongPost && {maxHeight, overflow: 'hidden'})]}
style={[style.flex, (isPendingOrFailedPost && style.pendingPost), (isLongPost && {maxHeight})]}
removeClippedSubviews={isLongPost}
>
<Markdown
baseTextStyle={messageStyle}
blockStyles={blockStyles}
imageMetadata={metadata?.images}
isEdited={hasBeenEdited}
isReplyPost={isReplyPost}
isSearchResult={isSearchResult}
@ -420,6 +439,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
flex: {
flex: 1,
overflow: 'hidden',
},
row: {
flexDirection: 'row',

View file

@ -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 (
<PostAttachmentOpenGraph
isReplyPost={isReplyPost}
link={link}
navigator={navigator}
openGraphData={openGraphData}
imageMetadata={metadata && metadata.images}
theme={theme}
/>
);
@ -199,7 +232,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
<ProgressiveImage
isBackgroundImage={true}
imageUri={imgUrl}
style={[styles.image, {width, height: imgHeight}]}
style={[styles.image, {width: width || MAX_YOUTUBE_IMAGE_WIDTH, height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
thumbnailUri={thumbUrl}
resizeMode='cover'
onError={this.handleLinkLoadError}
@ -243,45 +276,68 @@ export default class PostBodyAdditionalContent extends PureComponent {
};
getImageSize = (path) => {
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);

View file

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

4
package-lock.json generated
View file

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

View file

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