diff --git a/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap b/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap
index a26f8be29..afadadbab 100644
--- a/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap
+++ b/app/components/file_attachment_list/__snapshots__/file_attachment.test.js.snap
@@ -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,
},
]
}
>
-
-
-
-
+ >
+
+
+
+
`;
diff --git a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
index f1387431d..92df63048 100644
--- a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
+++ b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap
@@ -1,71 +1,1395 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`PostAttachmentOpenGraph should match snapshot with a single image file 1`] = `
-
-
+
-
+ >
+
+
+
+
+`;
+
+exports[`FileAttachmentList should match snapshot with combination of image and non-image attachments 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FileAttachmentList should match snapshot with four image files 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FileAttachmentList should match snapshot with more than four image files 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FileAttachmentList should match snapshot with non-image attachment 1`] = `
+
+
+
+
+
+`;
+
+exports[`FileAttachmentList should match snapshot with three image files 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FileAttachmentList should match snapshot with two image files 1`] = `
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js
index e7d0b8009..7a50c637d 100644
--- a/app/components/file_attachment_list/file_attachment.js
+++ b/app/components/file_attachment_list/file_attachment.js
@@ -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 (
-
-
- {file.caption.trim()}
-
-
+
+
- {`${data.extension.toUpperCase()} ${Utils.getFormattedFileSize(data)}`}
+ {file.caption.trim()}
-
-
+
+
+ {`${Utils.getFormattedFileSize(data)}`}
+
+
+
+
);
}
@@ -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 (
+
+
+ {`+${value}`}
+
+
+ );
+ };
+
+ 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 = (
+ {this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
);
} else if (isDocument(data)) {
fileAttachmentComponent = (
-
+
+
+
+
+ {this.renderFileInfo()}
+
);
} else {
fileAttachmentComponent = (
-
-
-
+
+
+
+
+
+
+ {this.renderFileInfo()}
+
);
}
- return (
-
- {fileAttachmentComponent}
-
- {this.renderFileInfo()}
-
-
- );
+ 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',
+ },
};
});
diff --git a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js
index 896c6d894..8fc6419c2 100644
--- a/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js
+++ b/app/components/file_attachment_list/file_attachment_document/file_attachment_document.js
@@ -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 (
-
- {this.renderFileAttachmentIcon()}
-
- );
- };
-
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 (
);
}
- render() {
- const {onLongPress, theme, wrapperHeight} = this.props;
- const {downloading, progress} = this.state;
+ renderDownloadProgres = () => {
+ const {theme} = this.props;
+ return (
+
+ {`${this.state.progress}%`}
+
+ );
+ };
+ render() {
+ const {onLongPress, theme} = this.props;
+ const {downloading, progress} = this.state;
let fileAttachmentComponent;
if (downloading) {
fileAttachmentComponent = (
-
- {this.renderProgress}
-
+
+
+ {this.renderDownloadProgres}
+
+
);
} 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,
},
});
diff --git a/app/components/file_attachment_list/file_attachment_icon.js b/app/components/file_attachment_list/file_attachment_icon.js
index 0c84b0282..08f6ba039 100644
--- a/app/components/file_attachment_list/file_attachment_icon.js
+++ b/app/components/file_attachment_list/file_attachment_icon.js
@@ -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 (
@@ -89,12 +95,5 @@ const styles = StyleSheet.create({
fileIconWrapper: {
alignItems: 'center',
justifyContent: 'center',
- borderTopLeftRadius: 2,
- borderBottomLeftRadius: 2,
- },
- icon: {
- borderTopLeftRadius: 2,
- borderBottomLeftRadius: 2,
- backgroundColor: '#fff',
},
});
diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js
index e087447da..b37afe6fa 100644
--- a/app/components/file_attachment_list/file_attachment_image.js
+++ b/app/components/file_attachment_list/file_attachment_image.js
@@ -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 ();
};
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 (
+ {this.boxPlaceholder()}
+
+
+
+
+ );
+ };
+
+ 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 (
+
+ {this.boxPlaceholder()}
);
@@ -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',
},
});
diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js
index fd8a15596..ceb1fde43 100644
--- a/app/components/file_attachment_list/file_attachment_list.js
+++ b/app/components/file_attachment_list/file_attachment_list.js
@@ -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 (
+
+
+
+ );
+ });
+ };
+
+ 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 (
+
+ { this.renderItems(visibleImages, nonVisibleImagesCount, true) }
+
+ );
+ };
+
+ 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 (
-
- );
- });
- };
-
- render() {
- const {fileIds, isFailed} = this.props;
+ const manifest = this.attachmentManifest(files);
return (
- 1}
- style={[(isFailed && styles.failed)]}
- keyboardShouldPersistTaps={'always'}
- >
- {this.renderItems()}
-
+
+ {this.renderImageRow(manifest.imageAttachments)}
+ {this.renderItems(manifest.nonImageAttachments)}
+
);
}
}
const styles = StyleSheet.create({
+ row: {
+ flex: 1,
+ flexDirection: 'row',
+ marginTop: 5,
+ },
+ container: {
+ flex: 1,
+ },
+ gutter: {
+ marginLeft: 8,
+ },
failed: {
opacity: 0.5,
},
diff --git a/app/components/file_attachment_list/file_attachment_list.test.js b/app/components/file_attachment_list/file_attachment_list.test.js
index 245e0bac1..69dcb609d 100644
--- a/app/components/file_attachment_list/file_attachment_list.test.js
+++ b/app/components/file_attachment_list/file_attachment_list.test.js
@@ -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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('should match snapshot with non-image attachment', () => {
+ const props = {
+ ...baseProps,
+ files: [nonImage],
+ };
+
+ const wrapper = shallow(
+
+ );
+
+ 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(
+
+ );
+
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
test('should call loadFilesForPostIfNecessary when files does not exist', async () => {
const props = {
...baseProps,
diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.js
index ff33c5243..0304b61c2 100644
--- a/app/components/file_upload_preview/file_upload_item/file_upload_item.js
+++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.js
@@ -190,11 +190,7 @@ export default class FileUploadItem extends PureComponent {
filePreviewComponent = (
);
} else {
@@ -202,8 +198,6 @@ export default class FileUploadItem extends PureComponent {
@@ -260,6 +254,7 @@ const styles = StyleSheet.create({
height: 100,
width: 100,
elevation: 10,
+ borderRadius: 5,
...Platform.select({
ios: {
backgroundColor: '#fff',
diff --git a/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap b/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap
index c941946d9..e4b92da1f 100644
--- a/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap
+++ b/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap
@@ -32,6 +32,14 @@ exports[`AttachmentImage it matches snapshot 1`] = `
}
>
{
borderRadius: 2,
flex: 1,
},
+ attachmentMargin: {
+ marginTop: 2.5,
+ marginLeft: 2.5,
+ marginBottom: 5,
+ marginRight: 5,
+ },
};
});
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index b49a69f92..0ddd542f0 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -247,6 +247,7 @@ export default class PostBody extends PureComponent {
isFailed={isFailed}
onLongPress={this.showPostOptions}
postId={post.id}
+ isReplyPost={this.props.isReplyPost}
/>
);
}
diff --git a/app/components/progressive_image/progressive_image.js b/app/components/progressive_image/progressive_image.js
index 1b4d73bd7..68fb5f276 100644
--- a/app/components/progressive_image/progressive_image.js
+++ b/app/components/progressive_image/progressive_image.js
@@ -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 = (
{this.props.children}
@@ -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}
@@ -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),
+ },
+ };
});
diff --git a/app/constants/attachment.js b/app/constants/attachment.js
index 7f234bf52..532fcf90b 100644
--- a/app/constants/attachment.js
+++ b/app/constants/attachment.js
@@ -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;
diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js
index 2f0214773..150e8d879 100644
--- a/app/screens/image_preview/image_preview.js
+++ b/app/screens/image_preview/image_preview.js
@@ -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 (
diff --git a/assets/base/images/icons/audio.png b/assets/base/images/icons/audio.png
index 93cf6ef33..dc4d3cb49 100644
Binary files a/assets/base/images/icons/audio.png and b/assets/base/images/icons/audio.png differ
diff --git a/assets/base/images/icons/code.png b/assets/base/images/icons/code.png
index c0c8458f8..14c159dea 100644
Binary files a/assets/base/images/icons/code.png and b/assets/base/images/icons/code.png differ
diff --git a/assets/base/images/icons/excel.png b/assets/base/images/icons/excel.png
old mode 100644
new mode 100755
index f9e9bc83a..dfb83aceb
Binary files a/assets/base/images/icons/excel.png and b/assets/base/images/icons/excel.png differ
diff --git a/assets/base/images/icons/generic.png b/assets/base/images/icons/generic.png
index e89b90ff0..d17636fcf 100644
Binary files a/assets/base/images/icons/generic.png and b/assets/base/images/icons/generic.png differ
diff --git a/assets/base/images/icons/image.png b/assets/base/images/icons/image.png
index 8aafdbef1..993621dc9 100644
Binary files a/assets/base/images/icons/image.png and b/assets/base/images/icons/image.png differ
diff --git a/assets/base/images/icons/patch.png b/assets/base/images/icons/patch.png
old mode 100644
new mode 100755
index c4b423525..fdc66287a
Binary files a/assets/base/images/icons/patch.png and b/assets/base/images/icons/patch.png differ
diff --git a/assets/base/images/icons/pdf.png b/assets/base/images/icons/pdf.png
index c716eed79..ad73c128e 100644
Binary files a/assets/base/images/icons/pdf.png and b/assets/base/images/icons/pdf.png differ
diff --git a/assets/base/images/icons/ppt.png b/assets/base/images/icons/ppt.png
old mode 100644
new mode 100755
index 225b5e53a..5e3787b00
Binary files a/assets/base/images/icons/ppt.png and b/assets/base/images/icons/ppt.png differ
diff --git a/assets/base/images/icons/text.png b/assets/base/images/icons/text.png
new file mode 100644
index 000000000..52cb3f170
Binary files /dev/null and b/assets/base/images/icons/text.png differ
diff --git a/assets/base/images/icons/video.png b/assets/base/images/icons/video.png
index ed0c2918c..9256f20b4 100644
Binary files a/assets/base/images/icons/video.png and b/assets/base/images/icons/video.png differ
diff --git a/assets/base/images/icons/word.png b/assets/base/images/icons/word.png
old mode 100644
new mode 100755
index ee4295454..abf02590a
Binary files a/assets/base/images/icons/word.png and b/assets/base/images/icons/word.png differ