[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
@@ -5,77 +5,70 @@ exports[`FileAttachment should match snapshot 1`] = `
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 2,
|
||||
"borderColor": "rgba(61,60,64,0.4)",
|
||||
"borderRadius": 5,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginRight": 10,
|
||||
"marginTop": 10,
|
||||
"width": 300,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
type="opacity"
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
backgroundColor="#fff"
|
||||
file={
|
||||
Object {
|
||||
"mime_type": "image/png",
|
||||
}
|
||||
}
|
||||
iconHeight={60}
|
||||
iconWidth={60}
|
||||
onCaptureRef={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
wrapperHeight={80}
|
||||
wrapperWidth={80}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderLeftColor": "rgba(61,60,64,0.2)",
|
||||
"borderLeftWidth": 1,
|
||||
"flex": 1,
|
||||
"paddingHorizontal": 8,
|
||||
"paddingVertical": 5,
|
||||
"marginHorizontal": 20,
|
||||
"marginVertical": 10,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
/>
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
type="opacity"
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={
|
||||
Object {
|
||||
"mime_type": "image/png",
|
||||
}
|
||||
}
|
||||
iconHeight={48}
|
||||
iconWidth={36}
|
||||
onCaptureRef={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
wrapperHeight={48}
|
||||
wrapperWidth={36}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Dimensions,
|
||||
PixelRatio,
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {isDocument, isGif} from 'app/utils/file';
|
||||
import {calculateDimensions} from 'app/utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import FileAttachmentDocument from './file_attachment_document';
|
||||
@@ -28,10 +32,14 @@ export default class FileAttachment extends PureComponent {
|
||||
onLongPress: PropTypes.func,
|
||||
onPreviewPress: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
wrapperWidth: PropTypes.number,
|
||||
isSingleImage: PropTypes.bool,
|
||||
nonVisibleImagesCount: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onPreviewPress: () => true,
|
||||
wrapperWidth: 300,
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
@@ -51,7 +59,7 @@ export default class FileAttachment extends PureComponent {
|
||||
};
|
||||
|
||||
renderFileInfo() {
|
||||
const {file, theme} = this.props;
|
||||
const {file, onLongPress, theme} = this.props;
|
||||
const {data} = file;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -60,24 +68,31 @@ export default class FileAttachment extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.attachmentContainer}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileName}
|
||||
>
|
||||
{file.caption.trim()}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
style={style.attachmentContainer}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileInfo}
|
||||
style={style.fileName}
|
||||
>
|
||||
{`${data.extension.toUpperCase()} ${Utils.getFormattedFileSize(data)}`}
|
||||
{file.caption.trim()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileInfo}
|
||||
>
|
||||
{`${Utils.getFormattedFileSize(data)}`}
|
||||
</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,75 +100,118 @@ export default class FileAttachment extends PureComponent {
|
||||
this.documentElement = ref;
|
||||
};
|
||||
|
||||
renderMoreImagesOverlay = (value) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.moreImagesWrapper}>
|
||||
<Text style={style.moreImagesText}>
|
||||
{`+${value}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
getImageDimensions = (file) => {
|
||||
const {isSingleImage, wrapperWidth} = this.props;
|
||||
const viewPortHeight = this.getViewPortHeight();
|
||||
|
||||
if (isSingleImage) {
|
||||
return calculateDimensions(file?.height, file?.width, wrapperWidth, viewPortHeight);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
getViewPortHeight = () => {
|
||||
const dimensions = Dimensions.get('window');
|
||||
const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45;
|
||||
|
||||
return viewPortHeight;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
canDownloadFiles,
|
||||
file,
|
||||
theme,
|
||||
onLongPress,
|
||||
isSingleImage,
|
||||
nonVisibleImagesCount,
|
||||
} = this.props;
|
||||
const {data} = file;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if ((data && data.has_preview_image) || file.loading || isGif(data)) {
|
||||
const imageDimensions = this.getImageDimensions(data);
|
||||
|
||||
fileAttachmentComponent = (
|
||||
<TouchableWithFeedback
|
||||
key={`${this.props.id}${file.loading}`}
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
style={{width: imageDimensions?.width}}
|
||||
>
|
||||
<FileAttachmentImage
|
||||
file={data || {}}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
imageDimensions={imageDimensions}
|
||||
/>
|
||||
{this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
} else if (isDocument(data)) {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentDocument
|
||||
ref={this.setDocumentRef}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={[style.fileWrapper]}>
|
||||
<View style={style.iconWrapper}>
|
||||
<FileAttachmentDocument
|
||||
ref={this.setDocumentRef}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
{this.renderFileInfo()}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={data}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
<View style={[style.fileWrapper]}>
|
||||
<View style={style.iconWrapper}>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={data}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
{this.renderFileInfo()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style.fileWrapper]}>
|
||||
{fileAttachmentComponent}
|
||||
<TouchableWithFeedback
|
||||
style={style.fileInfoContainer}
|
||||
onLongPress={onLongPress}
|
||||
onPress={this.handlePreviewPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
{this.renderFileInfo()}
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
return fileAttachmentComponent;
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const scale = Dimensions.get('window').width / 320;
|
||||
|
||||
return {
|
||||
attachmentContainer: {
|
||||
flex: 1,
|
||||
@@ -168,33 +226,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 3,
|
||||
},
|
||||
fileInfo: {
|
||||
marginLeft: 2,
|
||||
fontSize: 14,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
fileInfoContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 5,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
fileName: {
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
marginLeft: 2,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: theme.centerChannelColor,
|
||||
paddingRight: 10,
|
||||
},
|
||||
fileWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
marginRight: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 2,
|
||||
width: 300,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
borderRadius: 5,
|
||||
},
|
||||
iconWrapper: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: 10,
|
||||
},
|
||||
circularProgress: {
|
||||
width: '100%',
|
||||
@@ -211,5 +264,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
moreImagesWrapper: {
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
borderRadius: 5,
|
||||
},
|
||||
moreImagesText: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: Math.round(PixelRatio.roundToNearestPixel(24 * scale)),
|
||||
fontFamily: 'Open Sans',
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Platform,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import OpenFile from 'react-native-doc-viewer';
|
||||
@@ -27,8 +28,9 @@ import mattermostBucket from 'app/mattermost_bucket';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
import {ATTACHMENT_ICON_HEIGHT, ATTACHMENT_ICON_WIDTH} from 'app/constants/attachment';
|
||||
|
||||
const {DOCUMENTS_PATH} = DeviceTypes;
|
||||
const DOWNLOADING_OFFSET = 28;
|
||||
const TEXT_PREVIEW_FORMATS = [
|
||||
'application/json',
|
||||
'application/x-x509-ca-cert',
|
||||
@@ -50,10 +52,10 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
iconHeight: 47,
|
||||
iconWidth: 47,
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
iconHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
iconWidth: ATTACHMENT_ICON_WIDTH,
|
||||
wrapperHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
wrapperWidth: ATTACHMENT_ICON_WIDTH,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -284,16 +286,6 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
renderProgress = () => {
|
||||
const {wrapperWidth} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
|
||||
{this.renderFileAttachmentIcon()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
showDownloadDisabledAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
@@ -338,14 +330,6 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
renderFileAttachmentIcon = () => {
|
||||
const {backgroundColor, iconHeight, iconWidth, file, theme, wrapperHeight, wrapperWidth} = this.props;
|
||||
const {downloading} = this.state;
|
||||
let height = wrapperHeight;
|
||||
let width = wrapperWidth;
|
||||
|
||||
if (downloading) {
|
||||
height -= DOWNLOADING_OFFSET;
|
||||
width -= DOWNLOADING_OFFSET;
|
||||
}
|
||||
|
||||
return (
|
||||
<FileAttachmentIcon
|
||||
@@ -354,29 +338,39 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
theme={theme}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
wrapperHeight={height}
|
||||
wrapperWidth={width}
|
||||
wrapperHeight={wrapperHeight}
|
||||
wrapperWidth={wrapperWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {onLongPress, theme, wrapperHeight} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
renderDownloadProgres = () => {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<Text style={{fontSize: 10, color: theme.centerChannelColor, fontWeight: '600'}}>
|
||||
{`${this.state.progress}%`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {onLongPress, theme} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
let fileAttachmentComponent;
|
||||
if (downloading) {
|
||||
fileAttachmentComponent = (
|
||||
<CircularProgress
|
||||
size={wrapperHeight}
|
||||
fill={progress}
|
||||
width={circularProgressWidth}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</CircularProgress>
|
||||
<View style={[style.circularProgressContent]}>
|
||||
<CircularProgress
|
||||
size={40}
|
||||
fill={progress}
|
||||
width={circularProgressWidth}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
>
|
||||
{this.renderDownloadProgres}
|
||||
</CircularProgress>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = this.renderFileAttachmentIcon();
|
||||
@@ -396,11 +390,9 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
const style = StyleSheet.create({
|
||||
circularProgressContent: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: -circularProgressWidth,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: -(circularProgressWidth - 2),
|
||||
top: 4,
|
||||
width: 36,
|
||||
height: 48,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,9 +19,13 @@ import imageIcon from 'assets/images/icons/image.png';
|
||||
import patchIcon from 'assets/images/icons/patch.png';
|
||||
import pdfIcon from 'assets/images/icons/pdf.png';
|
||||
import pptIcon from 'assets/images/icons/ppt.png';
|
||||
import textIcon from 'assets/images/icons/text.png';
|
||||
import videoIcon from 'assets/images/icons/video.png';
|
||||
import wordIcon from 'assets/images/icons/word.png';
|
||||
|
||||
import {ATTACHMENT_ICON_HEIGHT, ATTACHMENT_ICON_WIDTH} from 'app/constants/attachment';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const ICON_PATH_FROM_FILE_TYPE = {
|
||||
audio: audioIcon,
|
||||
code: codeIcon,
|
||||
@@ -31,6 +35,7 @@ const ICON_PATH_FROM_FILE_TYPE = {
|
||||
pdf: pdfIcon,
|
||||
presentation: pptIcon,
|
||||
spreadsheet: excelIcon,
|
||||
text: textIcon,
|
||||
video: videoIcon,
|
||||
word: wordIcon,
|
||||
};
|
||||
@@ -44,14 +49,14 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
onCaptureRef: PropTypes.func,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
backgroundColor: '#fff',
|
||||
iconHeight: 60,
|
||||
iconWidth: 60,
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
iconHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
iconWidth: ATTACHMENT_ICON_WIDTH,
|
||||
wrapperHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
wrapperWidth: ATTACHMENT_ICON_WIDTH,
|
||||
};
|
||||
|
||||
getFileIconPath(file) {
|
||||
@@ -68,16 +73,17 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {backgroundColor, file, iconHeight, iconWidth, wrapperHeight, wrapperWidth} = this.props;
|
||||
const {backgroundColor, file, iconHeight, iconWidth, wrapperHeight, wrapperWidth, theme} = this.props;
|
||||
const source = this.getFileIconPath(file);
|
||||
const bgColor = backgroundColor || theme.centerChannelBg || 'transparent';
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[styles.fileIconWrapper, {backgroundColor, height: wrapperHeight, width: wrapperWidth}]}
|
||||
style={[styles.fileIconWrapper, {backgroundColor: bgColor, height: wrapperHeight, width: wrapperWidth}]}
|
||||
>
|
||||
<Image
|
||||
style={[styles.icon, {height: iconHeight, width: iconWidth}]}
|
||||
style={{maxHeight: iconHeight, maxWidth: iconWidth, tintColor: changeOpacity(theme.centerChannelColor, 20)}}
|
||||
source={source}
|
||||
/>
|
||||
</View>
|
||||
@@ -89,12 +95,5 @@ const styles = StyleSheet.create({
|
||||
fileIconWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
},
|
||||
icon: {
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,9 +15,13 @@ import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {isGif} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import thumb from 'assets/images/thumb.png';
|
||||
|
||||
const SMALL_IMAGE_MAX_HEIGHT = 48;
|
||||
const SMALL_IMAGE_MAX_WIDTH = 48;
|
||||
|
||||
const IMAGE_SIZE = {
|
||||
Fullsize: 'fullsize',
|
||||
Preview: 'preview',
|
||||
@@ -35,22 +39,18 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
]),
|
||||
imageWidth: PropTypes.number,
|
||||
onCaptureRef: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
resizeMode: PropTypes.string,
|
||||
resizeMethod: PropTypes.string,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number,
|
||||
isSingleImage: PropTypes.bool,
|
||||
imageDimensions: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fadeInOnLoad: false,
|
||||
imageHeight: 80,
|
||||
imageSize: IMAGE_SIZE.Preview,
|
||||
imageWidth: 80,
|
||||
loading: false,
|
||||
resizeMode: 'cover',
|
||||
resizeMethod: 'resize',
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -72,15 +72,11 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
calculateNeededWidth = (height, width, newHeight) => {
|
||||
const ratio = width / height;
|
||||
|
||||
let newWidth = newHeight * ratio;
|
||||
if (newWidth < newHeight) {
|
||||
newWidth = newHeight;
|
||||
boxPlaceholder = () => {
|
||||
if (this.props.isSingleImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return newWidth;
|
||||
return (<View style={style.boxPlaceholder}/>);
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
@@ -91,27 +87,7 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
file,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
imageSize,
|
||||
resizeMethod,
|
||||
resizeMode,
|
||||
wrapperHeight,
|
||||
wrapperWidth,
|
||||
} = this.props;
|
||||
|
||||
let height = imageHeight;
|
||||
let width = imageWidth;
|
||||
let imageStyle = {height, width};
|
||||
if (imageSize === IMAGE_SIZE.Preview) {
|
||||
height = 80;
|
||||
width = this.calculateNeededWidth(file.height, file.width, height) || 80;
|
||||
imageStyle = {height, width, position: 'absolute', top: 0, left: 0, borderBottomLeftRadius: 2, borderTopLeftRadius: 2};
|
||||
}
|
||||
|
||||
imageProps = (file) => {
|
||||
const imageProps = {};
|
||||
if (file.localPath) {
|
||||
imageProps.defaultSource = {uri: file.localPath};
|
||||
@@ -119,20 +95,73 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
|
||||
imageProps.imageUri = Client4.getFilePreviewUrl(file.id);
|
||||
}
|
||||
return imageProps;
|
||||
};
|
||||
|
||||
renderSmallImage = () => {
|
||||
const {file, isSingleImage, resizeMethod, theme} = this.props;
|
||||
|
||||
let wrapperStyle = style.fileImageWrapper;
|
||||
|
||||
if (isSingleImage) {
|
||||
wrapperStyle = style.singleSmallImageWrapper;
|
||||
|
||||
if (file.width > SMALL_IMAGE_MAX_WIDTH) {
|
||||
wrapperStyle = [wrapperStyle, {width: '100%'}];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}
|
||||
style={[
|
||||
wrapperStyle,
|
||||
style.smallImageBorder,
|
||||
{borderColor: changeOpacity(theme.centerChannelColor, 0.4)},
|
||||
]}
|
||||
>
|
||||
{this.boxPlaceholder()}
|
||||
<View style={style.smallImageOverlay}>
|
||||
<ProgressiveImage
|
||||
style={{height: file.height, width: file.width}}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
filename={file.name}
|
||||
resizeMode={'contain'}
|
||||
resizeMethod={resizeMethod}
|
||||
{...this.imageProps(file)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
file,
|
||||
imageDimensions,
|
||||
resizeMethod,
|
||||
resizeMode,
|
||||
} = this.props;
|
||||
|
||||
if (file.height <= SMALL_IMAGE_MAX_HEIGHT || file.width <= SMALL_IMAGE_MAX_WIDTH) {
|
||||
return this.renderSmallImage();
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={style.fileImageWrapper}
|
||||
>
|
||||
{this.boxPlaceholder()}
|
||||
<ProgressiveImage
|
||||
style={imageStyle}
|
||||
style={[this.props.isSingleImage ? null : style.imagePreview, imageDimensions]}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
filename={file.name}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
{...imageProps}
|
||||
{...this.imageProps(file)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -140,17 +169,28 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
fileImageWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottomLeftRadius: 2,
|
||||
borderTopLeftRadius: 2,
|
||||
imagePreview: {
|
||||
...StyleSheet.absoluteFill,
|
||||
},
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
fileImageWrapper: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
boxPlaceholder: {
|
||||
paddingBottom: '100%',
|
||||
},
|
||||
smallImageBorder: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
},
|
||||
smallImageOverlay: {
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
singleSmallImageWrapper: {
|
||||
height: SMALL_IMAGE_MAX_HEIGHT,
|
||||
width: SMALL_IMAGE_MAX_WIDTH,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {isDocument, isGif, isVideo} from 'app/utils/file';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {previewImageAtIndex} from 'app/utils/images';
|
||||
@@ -18,7 +20,11 @@ import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
export default class FileAttachmentList extends Component {
|
||||
const MAX_VISIBLE_ROW_IMAGES = 4;
|
||||
const VIEWPORT_IMAGE_OFFSET = 70;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||
|
||||
export default class FileAttachmentList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
|
||||
@@ -30,6 +36,7 @@ export default class FileAttachmentList extends Component {
|
||||
onLongPress: PropTypes.func,
|
||||
postId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isReplyPost: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -40,19 +47,25 @@ export default class FileAttachmentList extends Component {
|
||||
super(props);
|
||||
|
||||
this.items = [];
|
||||
this.previewItems = [];
|
||||
|
||||
this.filesForGallery = this.getFilesForGallery(props);
|
||||
this.state = {
|
||||
loadingFiles: props.files.length === 0,
|
||||
};
|
||||
|
||||
this.buildGalleryFiles(props).then((results) => {
|
||||
this.buildGalleryFiles().then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {files} = this.props;
|
||||
|
||||
this.mounted = true;
|
||||
this.handlePermanentSidebar();
|
||||
this.handleDimensions();
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
|
||||
if (files.length === 0) {
|
||||
this.loadFilesForPost();
|
||||
}
|
||||
@@ -60,7 +73,8 @@ export default class FileAttachmentList extends Component {
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.files !== nextProps.files) {
|
||||
this.buildGalleryFiles(nextProps).then((results) => {
|
||||
this.filesForGallery = this.getFilesForGallery(nextProps);
|
||||
this.buildGalleryFiles().then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
@@ -72,20 +86,33 @@ export default class FileAttachmentList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadFilesForPost = async () => {
|
||||
await this.props.actions.loadFilesForPostIfNecessary(this.props.postId);
|
||||
this.setState({
|
||||
loadingFiles: false,
|
||||
});
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
buildGalleryFiles = async (props) => {
|
||||
const {files} = props;
|
||||
attachmentIndex = (fileId) => {
|
||||
return this.filesForGallery.findIndex((file) => file.id === fileId) || 0;
|
||||
};
|
||||
|
||||
attachmentManifest = (attachments) => {
|
||||
return attachments.reduce((info, file) => {
|
||||
if (this.isImage(file)) {
|
||||
info.imageAttachments.push(file);
|
||||
} else {
|
||||
info.nonImageAttachments.push(file);
|
||||
}
|
||||
return info;
|
||||
}, {imageAttachments: [], nonImageAttachments: []});
|
||||
};
|
||||
|
||||
buildGalleryFiles = async () => {
|
||||
const results = [];
|
||||
|
||||
if (files && files.length) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (this.filesForGallery && this.filesForGallery.length) {
|
||||
for (let i = 0; i < this.filesForGallery.length; i++) {
|
||||
const file = this.filesForGallery[i];
|
||||
const caption = file.name;
|
||||
|
||||
if (isDocument(file) || isVideo(file) || (!file.has_preview_image && !isGif(file))) {
|
||||
@@ -116,16 +143,141 @@ export default class FileAttachmentList extends Component {
|
||||
return results;
|
||||
};
|
||||
|
||||
getFilesForGallery = (props) => {
|
||||
const manifest = this.attachmentManifest(props.files);
|
||||
const files = manifest.imageAttachments.concat(manifest.nonImageAttachments);
|
||||
const results = [];
|
||||
|
||||
if (files && files.length) {
|
||||
files.forEach((file) => {
|
||||
results.push(file);
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
getPortraitPostWidth = () => {
|
||||
const {isReplyPost} = this.props;
|
||||
const {width, height} = Dimensions.get('window');
|
||||
const permanentSidebar = DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar;
|
||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||
|
||||
if (permanentSidebar) {
|
||||
portraitPostWidth -= TABLET_WIDTH;
|
||||
}
|
||||
|
||||
if (isReplyPost) {
|
||||
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
||||
}
|
||||
|
||||
return portraitPostWidth;
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref, idx) => {
|
||||
this.items[idx] = ref;
|
||||
};
|
||||
|
||||
handleDimensions = () => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = preventDoubleTap((idx) => {
|
||||
previewImageAtIndex(this.items, idx, this.galleryFiles);
|
||||
});
|
||||
|
||||
renderItems = () => {
|
||||
const {canDownloadFiles, fileIds, files} = this.props;
|
||||
isImage = (file) => (file.has_preview_image || isGif(file));
|
||||
|
||||
isSingleImage = (files) => (files.length === 1 && this.isImage(files[0]));
|
||||
|
||||
loadFilesForPost = async () => {
|
||||
await this.props.actions.loadFilesForPostIfNecessary(this.props.postId);
|
||||
this.setState({
|
||||
loadingFiles: false,
|
||||
});
|
||||
}
|
||||
|
||||
renderItems = (items, moreImagesCount, includeGutter = false) => {
|
||||
const {canDownloadFiles, onLongPress, theme} = this.props;
|
||||
const isSingleImage = this.isSingleImage(items);
|
||||
let nonVisibleImagesCount;
|
||||
let container = styles.container;
|
||||
const containerWithGutter = [container, styles.gutter];
|
||||
|
||||
return items.map((file, idx) => {
|
||||
const f = {
|
||||
caption: file.name,
|
||||
data: file,
|
||||
};
|
||||
|
||||
if (moreImagesCount && idx === MAX_VISIBLE_ROW_IMAGES - 1) {
|
||||
nonVisibleImagesCount = moreImagesCount;
|
||||
}
|
||||
|
||||
if (idx !== 0 && includeGutter) {
|
||||
container = containerWithGutter;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={container}
|
||||
key={file.id}
|
||||
>
|
||||
<FileAttachment
|
||||
key={file.id}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={f}
|
||||
id={file.id}
|
||||
index={this.attachmentIndex(file.id)}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onPreviewPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
wrapperWidth={this.getPortraitPostWidth()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
renderImageRow = (images) => {
|
||||
if (images.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visibleImages = images.slice(0, MAX_VISIBLE_ROW_IMAGES);
|
||||
const {portraitPostWidth} = this.state;
|
||||
|
||||
let nonVisibleImagesCount;
|
||||
if (images.length > MAX_VISIBLE_ROW_IMAGES) {
|
||||
nonVisibleImagesCount = images.length - MAX_VISIBLE_ROW_IMAGES;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.row, {width: portraitPostWidth}]}>
|
||||
{ this.renderItems(visibleImages, nonVisibleImagesCount, true) }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {canDownloadFiles, fileIds, files, isFailed} = this.props;
|
||||
|
||||
if (!files.length && fileIds.length > 0) {
|
||||
return fileIds.map((id, idx) => (
|
||||
@@ -140,45 +292,29 @@ export default class FileAttachmentList extends Component {
|
||||
));
|
||||
}
|
||||
|
||||
return files.map((file, idx) => {
|
||||
const f = {
|
||||
caption: file.name,
|
||||
data: file,
|
||||
};
|
||||
|
||||
return (
|
||||
<FileAttachment
|
||||
key={file.id}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={f}
|
||||
id={file.id}
|
||||
index={idx}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onPreviewPress={this.handlePreviewPress}
|
||||
onLongPress={this.props.onLongPress}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {fileIds, isFailed} = this.props;
|
||||
const manifest = this.attachmentManifest(files);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
scrollEnabled={fileIds.length > 1}
|
||||
style={[(isFailed && styles.failed)]}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
>
|
||||
{this.renderItems()}
|
||||
</ScrollView>
|
||||
<View style={[isFailed && styles.failed]}>
|
||||
{this.renderImageRow(manifest.imageAttachments)}
|
||||
{this.renderItems(manifest.nonImageAttachments)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 5,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gutter: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
failed: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
|
||||
@@ -10,9 +10,50 @@ jest.mock('react-native-doc-viewer', () => ({
|
||||
openDoc: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('PostAttachmentOpenGraph', () => {
|
||||
describe('FileAttachmentList', () => {
|
||||
const loadFilesForPostIfNecessary = jest.fn().mockImplementationOnce(() => Promise.resolve({data: {}}));
|
||||
|
||||
const files = [{
|
||||
create_at: 1546893090093,
|
||||
delete_at: 0,
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 171,
|
||||
id: 'fileId',
|
||||
mime_type: 'image/png',
|
||||
name: 'image01.png',
|
||||
post_id: 'postId',
|
||||
size: 14894,
|
||||
update_at: 1546893090093,
|
||||
user_id: 'userId',
|
||||
width: 425,
|
||||
},
|
||||
{
|
||||
create_at: 1546893090093,
|
||||
delete_at: 0,
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 800,
|
||||
id: 'otherFileId',
|
||||
mime_type: 'image/png',
|
||||
name: 'image02.png',
|
||||
post_id: 'postId',
|
||||
size: 24894,
|
||||
update_at: 1546893090093,
|
||||
user_id: 'userId',
|
||||
width: 555,
|
||||
}];
|
||||
|
||||
const nonImage = {
|
||||
extension: 'other',
|
||||
id: 'fileId',
|
||||
mime_type: 'other/type',
|
||||
name: 'file01.other',
|
||||
post_id: 'postId',
|
||||
size: 14894,
|
||||
user_id: 'userId',
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
actions: {
|
||||
loadFilesForPostIfNecessary,
|
||||
@@ -21,21 +62,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
deviceHeight: 680,
|
||||
deviceWidth: 660,
|
||||
fileIds: ['fileId'],
|
||||
files: [{
|
||||
create_at: 1546893090093,
|
||||
delete_at: 0,
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 171,
|
||||
id: 'fileId',
|
||||
mime_type: 'image/png',
|
||||
name: 'image.png',
|
||||
post_id: 'postId',
|
||||
size: 14894,
|
||||
update_at: 1546893090093,
|
||||
user_id: 'userId',
|
||||
width: 425,
|
||||
}],
|
||||
files: [files[0]],
|
||||
postId: 'postId',
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
@@ -48,6 +75,93 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with two image files', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with three image files', () => {
|
||||
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, thirdImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with four image files', () => {
|
||||
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
|
||||
const fourthImage = {...files[1], id: 'fourthFileId', name: 'image04.png'};
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, thirdImage, fourthImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with more than four image files', () => {
|
||||
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
|
||||
const fourthImage = {...files[1], id: 'fourthFileId', name: 'image04.png'};
|
||||
const fifthImage = {...files[1], id: 'fifthFileId', name: 'image05.png'};
|
||||
const sixthImage = {...files[1], id: 'sixthFileId', name: 'image06.png'};
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, thirdImage, fourthImage, fifthImage, sixthImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with non-image attachment', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [nonImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with combination of image and non-image attachments', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, nonImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call loadFilesForPostIfNecessary when files does not exist', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
|
||||
@@ -190,11 +190,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
imageSize='fullsize'
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -202,8 +198,6 @@ export default class FileUploadItem extends PureComponent {
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
/>
|
||||
@@ -260,6 +254,7 @@ const styles = StyleSheet.create({
|
||||
height: 100,
|
||||
width: 100,
|
||||
elevation: 10,
|
||||
borderRadius: 5,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
backgroundColor: '#fff',
|
||||
|
||||
@@ -32,6 +32,14 @@ exports[`AttachmentImage it matches snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<Connect(ProgressiveImage)
|
||||
imageStyle={
|
||||
Object {
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 2.5,
|
||||
"marginRight": 5,
|
||||
"marginTop": 2.5,
|
||||
}
|
||||
}
|
||||
imageUri="https://images.com/image.png"
|
||||
resizeMode="contain"
|
||||
style={
|
||||
|
||||
@@ -141,6 +141,7 @@ export default class AttachmentImage extends PureComponent {
|
||||
progressiveImage = (
|
||||
<ProgressiveImage
|
||||
ref={this.setImageRef}
|
||||
imageStyle={style.attachmentMargin}
|
||||
style={{height, width}}
|
||||
imageUri={imageUri}
|
||||
resizeMode='contain'
|
||||
@@ -178,5 +179,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderRadius: 2,
|
||||
flex: 1,
|
||||
},
|
||||
attachmentMargin: {
|
||||
marginTop: 2.5,
|
||||
marginLeft: 2.5,
|
||||
marginBottom: 5,
|
||||
marginRight: 5,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -247,6 +247,7 @@ export default class PostBody extends PureComponent {
|
||||
isFailed={isFailed}
|
||||
onLongPress={this.showPostOptions}
|
||||
postId={post.id}
|
||||
isReplyPost={this.props.isReplyPost}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'reac
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground);
|
||||
|
||||
@@ -18,6 +18,7 @@ export default class ProgressiveImage extends PureComponent {
|
||||
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,
|
||||
@@ -96,7 +97,17 @@ export default class ProgressiveImage extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {style, defaultSource, isBackgroundImage, theme, tintDefaultSource, onError, resizeMode, resizeMethod} = this.props;
|
||||
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);
|
||||
@@ -117,13 +128,15 @@ export default class ProgressiveImage extends PureComponent {
|
||||
ImageComponent = Animated.Image;
|
||||
}
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let defaultImage;
|
||||
if (hasDefaultSource && tintDefaultSource) {
|
||||
defaultImage = (
|
||||
<View style={styles.defaultImageContainer}>
|
||||
<DefaultComponent
|
||||
source={defaultSource}
|
||||
style={{flex: 1, tintColor: changeOpacity(theme.centerChannelColor, 0.2)}}
|
||||
style={styles.defaultImageTint}
|
||||
resizeMode='center'
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
@@ -139,7 +152,7 @@ export default class ProgressiveImage extends PureComponent {
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={defaultSource}
|
||||
style={StyleSheet.absoluteFill}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
>
|
||||
{this.props.children}
|
||||
</DefaultComponent>
|
||||
@@ -155,7 +168,7 @@ export default class ProgressiveImage extends PureComponent {
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={{uri: thumb}}
|
||||
style={StyleSheet.absoluteFill}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
blurRadius={5}
|
||||
>
|
||||
{this.props.children}
|
||||
@@ -167,7 +180,7 @@ export default class ProgressiveImage extends PureComponent {
|
||||
resizeMethod={resizeMethod}
|
||||
onError={onError}
|
||||
source={{uri}}
|
||||
style={[StyleSheet.absoluteFill, styles.attachmentMargin]}
|
||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||
>
|
||||
{this.props.children}
|
||||
</ImageComponent>
|
||||
@@ -180,19 +193,19 @@ export default class ProgressiveImage extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
defaultImageContainer: {
|
||||
flex: 1,
|
||||
position: 'absolute',
|
||||
height: 80,
|
||||
width: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
attachmentMargin: {
|
||||
marginTop: 2.5,
|
||||
marginLeft: 2.5,
|
||||
marginBottom: 5,
|
||||
marginRight: 5,
|
||||
},
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const MAX_ATTACHMENT_FOOTER_LENGTH = 300;
|
||||
export const ATTACHMENT_ICON_HEIGHT = 48;
|
||||
export const ATTACHMENT_ICON_WIDTH = 36;
|
||||
|
||||
@@ -206,8 +206,8 @@ export default class ImagePreview extends PureComponent {
|
||||
backgroundColor='transparent'
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
iconHeight={100}
|
||||
iconWidth={100}
|
||||
iconHeight={200}
|
||||
iconWidth={200}
|
||||
theme={theme}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
@@ -223,8 +223,8 @@ export default class ImagePreview extends PureComponent {
|
||||
backgroundColor='transparent'
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
iconHeight={150}
|
||||
iconWidth={150}
|
||||
iconHeight={200}
|
||||
iconWidth={200}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
@@ -366,7 +366,7 @@ export default class ImagePreview extends PureComponent {
|
||||
if (imageDimensions) {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
const {height, width} = imageDimensions;
|
||||
const {style, ...otherProps} = imageProps;
|
||||
const {style, source} = imageProps;
|
||||
const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
|
||||
const flattenStyle = StyleSheet.flatten(style);
|
||||
const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar);
|
||||
@@ -375,7 +375,7 @@ export default class ImagePreview extends PureComponent {
|
||||
return (
|
||||
<View style={[style, {justifyContent: 'center', alignItems: 'center'}]}>
|
||||
<Image
|
||||
{...otherProps}
|
||||
source={source}
|
||||
style={imageStyle}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
assets/base/images/icons/excel.png
Normal file → Executable file
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1006 B After Width: | Height: | Size: 2.5 KiB |
BIN
assets/base/images/icons/patch.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
assets/base/images/icons/ppt.png
Normal file → Executable file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
assets/base/images/icons/text.png
Normal file
|
After Width: | Height: | Size: 943 B |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 1.6 KiB |
BIN
assets/base/images/icons/word.png
Normal file → Executable file
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 2.4 KiB |