mattermost-mobile/app/components/progressive_image/progressive_image.js
Amit Uttam 66a6e5c932 [MM-10813] Design update for Post attachments (#3511)
* [MM-10813] Design update for Post attachments

Fixes https://github.com/mattermost/mattermost-server/issues/12841

* Updated visual treatment for Post attachments (images, documents, videos)
* Grouped image attachments, expanding to fit Post width dimension
* Itemized listing of non-image attachments
* Special handling of "small" image attachments (<48 point width/height)

* Set attachment post width to max width of portrait orientation

Accounts for post display offset and extra spacing used for rendering post replies.

* Use available Post real estate: flex: 1 instead of width 100%

* Image spacing responsibility: AttachmentList -> AttachmentImage

* Fit download progress circle correctly over new attachment icons

* Layers progress circle over the icon, rather than under.
* Uses offset constant as far as possible, rather than fixed point spaces.

* Refactor props and 'more' counting for image file attachment(s)

* Implement conditional gutter between image attachments

Flex's `justifyContent: space-between` won't work in this case because of the use of `absoluteFill` and `paddingBottom: 100%` in the box placeholder for an image attachment to auto-fill *all* available flex space.

* Additional snapshots for Post file attachment scenarios

* Use new 'text' icon for text files (.txt, .rtf)

Depends on https://github.com/mattermost/mattermost-redux/pull/979

Even without the change to mattermost-redux above, text files will default to pre-existing "code" icon.

* Set file attachment icon background to theme

Default to transparent. Override if explicitly specified.

* Treat animated GIFs as images when auto-adjusting attachment width

* Fix images layout, progressive image margins, and gallery for images and videos

* fix on iPad (splitview, permanent sidebar) card types and image sizes

* Add all files back to the gallery
2019-11-15 22:41:09 -05:00

211 lines
6.5 KiB
JavaScript

// 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 {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import ImageCacheManager from 'app/utils/image_cache_manager';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground);
export default class ProgressiveImage extends PureComponent {
static propTypes = {
isBackgroundImage: PropTypes.bool,
children: CustomPropTypes.Children,
defaultSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), // this should be provided by the component
filename: PropTypes.string,
imageUri: PropTypes.string,
imageStyle: CustomPropTypes.Style,
onError: PropTypes.func,
resizeMethod: PropTypes.string,
resizeMode: PropTypes.string,
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
thumbnailUri: PropTypes.string,
tintDefaultSource: PropTypes.bool,
};
static defaultProps = {
style: {},
};
constructor(props) {
super(props);
this.subscribedToCache = true;
this.state = {
intensity: new Animated.Value(80),
thumb: null,
uri: null,
};
}
componentDidMount() {
this.load();
}
componentDidUpdate(prevProps, prevState) {
if (this.props.filename !== prevProps.filename ||
this.props.imageUri !== prevProps.imageUri ||
this.props.thumbnailUri !== prevProps.thumbnailUri) {
this.load();
}
const {intensity, thumb, uri} = this.state;
if (uri && thumb && uri !== thumb && prevState.uri !== uri) {
Animated.timing(intensity, {
duration: 300,
toValue: 0,
useNativeDriver: Platform.OS === 'android',
}).start();
}
}
componentWillUnmount() {
this.subscribedToCache = false;
}
load = () => {
const {filename, imageUri, thumbnailUri} = this.props;
if (thumbnailUri) {
ImageCacheManager.cache(filename, thumbnailUri, this.setThumbnail);
} else if (imageUri) {
ImageCacheManager.cache(filename, imageUri, this.setImage);
}
};
setImage = (uri) => {
if (this.subscribedToCache) {
this.setState({uri});
}
};
setThumbnail = (thumb) => {
if (this.subscribedToCache) {
const {filename, imageUri} = this.props;
this.setState({thumb}, () => {
setTimeout(() => {
ImageCacheManager.cache(filename, imageUri, this.setImage);
}, 300);
});
}
};
render() {
const {
defaultSource,
imageStyle,
isBackgroundImage,
onError,
resizeMode,
resizeMethod,
style,
theme,
tintDefaultSource,
} = this.props;
const {uri, intensity, thumb} = this.state;
const hasDefaultSource = Boolean(defaultSource);
const hasPreview = Boolean(thumb);
const hasURI = Boolean(uri);
const isImageReady = uri && uri !== thumb;
const opacity = intensity.interpolate({
inputRange: [50, 100],
outputRange: [0.5, 1],
});
let DefaultComponent;
let ImageComponent;
if (isBackgroundImage) {
DefaultComponent = ImageBackground;
ImageComponent = AnimatedImageBackground;
} else {
DefaultComponent = Image;
ImageComponent = Animated.Image;
}
const styles = getStyleSheet(theme);
let defaultImage;
if (hasDefaultSource && tintDefaultSource) {
defaultImage = (
<View style={styles.defaultImageContainer}>
<DefaultComponent
source={defaultSource}
style={styles.defaultImageTint}
resizeMode='center'
resizeMethod={resizeMethod}
onError={onError}
>
{this.props.children}
</DefaultComponent>
</View>
);
} else {
defaultImage = (
<DefaultComponent
resizeMode={resizeMode}
resizeMethod={resizeMethod}
onError={onError}
source={defaultSource}
style={[StyleSheet.absoluteFill, imageStyle]}
>
{this.props.children}
</DefaultComponent>
);
}
return (
<View style={style}>
{(hasDefaultSource && !hasPreview && !hasURI) && defaultImage}
{hasPreview && !isImageReady &&
<ImageComponent
resizeMode={resizeMode}
resizeMethod={resizeMethod}
onError={onError}
source={{uri: thumb}}
style={[StyleSheet.absoluteFill, imageStyle]}
blurRadius={5}
>
{this.props.children}
</ImageComponent>
}
{isImageReady &&
<ImageComponent
resizeMode={resizeMode}
resizeMethod={resizeMethod}
onError={onError}
source={{uri}}
style={[StyleSheet.absoluteFill, imageStyle]}
>
{this.props.children}
</ImageComponent>
}
{hasPreview &&
<Animated.View style={[StyleSheet.absoluteFill, {backgroundColor: theme.centerChannelBg, opacity}]}/>
}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
defaultImageContainer: {
flex: 1,
position: 'absolute',
height: 80,
width: 80,
alignItems: 'center',
justifyContent: 'center',
},
defaultImageTint: {
flex: 1,
tintColor: changeOpacity(theme.centerChannelColor, 0.2),
},
};
});