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