diff --git a/app/constants/files.ts b/app/constants/files.ts index 0a90dbcd759..6e1434f6544 100644 --- a/app/constants/files.ts +++ b/app/constants/files.ts @@ -33,11 +33,12 @@ export const VALID_IMAGE_MIME_TYPES = [ export const Files: Record = { AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'], CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'ts', 'tex', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'], - IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg', 'tiff', 'tif', 'svg', 'psd', 'xcf'], + IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg', 'tiff', 'tif', 'psd', 'xcf'], PATCH_TYPES: ['patch'], PDF_TYPES: ['pdf'], PRESENTATION_TYPES: ['ppt', 'pptx', 'odp'], SPREADSHEET_TYPES: ['xls', 'xlsx', 'csv', 'ods'], + SVG_TYPES: ['svg'], TEXT_TYPES: ['txt', 'rtf'], VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv', 'ogm', 'mpeg'], WORD_TYPES: ['doc', 'docx', 'odt'], diff --git a/app/screens/global_threads/threads_list/file_card/file_card.tsx b/app/screens/global_threads/threads_list/file_card/file_card.tsx new file mode 100644 index 00000000000..458ccaac671 --- /dev/null +++ b/app/screens/global_threads/threads_list/file_card/file_card.tsx @@ -0,0 +1,146 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React, {useCallback, useEffect, useState} from 'react'; +import {Text, View} from 'react-native'; + +import {buildFilePreviewUrl, buildFileThumbnailUrl} from '@actions/remote/file'; +import FileIcon from '@app/components/files/file_icon'; +import ProgressiveImage from '@app/components/progressive_image'; +import {useServerUrl} from '@app/context/server'; +import {FileTypes} from '@app/utils/file/constants'; +import {getFormat, getFormattedFileSize} from '@utils/file'; + +import {getStyleSheet} from './styles'; + +import type FileModel from '@typings/database/models/servers/file'; +import type PostModel from '@typings/database/models/servers/post'; + +type CardProps = { + post: PostModel; + theme: Theme; +}; + +export const FileCard: React.FC = ({post, theme}) => { + const [files, setFiles] = useState([]); + const [failed, setFailed] = useState(false); + const styles = getStyleSheet(theme); + const serverUrl = useServerUrl(); + + useEffect(() => { + const fetchFiles = async () => { + const fetchedFiles = await post.files.fetch(); + setFiles(fetchedFiles); + }; + + fetchFiles(); + }, [post]); + + const handleError = useCallback(() => { + setFailed(true); + }, []); + + let file = null; + if (files.length > 0) { + const extension = files[0].extension; + const thumbnailUri = buildFileThumbnailUrl(serverUrl, files[0].id); + const imageUri = buildFilePreviewUrl(serverUrl, files[0].id); + const fileType = getFormat(extension); + + switch (fileType) { + case FileTypes.IMAGE: + file = ( + + + + + + {files.length > 0 && ( + + + {files[0].name} + + + {`${getFormattedFileSize(files[0].size)}`} + + + )} + + ); + if (failed) { + file = ( + + + + {files[0].name} + + + {`${getFormattedFileSize(files[0].size)}`} + + + + ); + } + return file; + default: + file = ( + + + + + + {files.length > 0 && ( + + + {files[0].name} + + + {`${getFormattedFileSize(files[0].size)}`} + + + )} + + + ); + return file; + } + } + return null; +}; + +export default FileCard; + diff --git a/app/screens/global_threads/threads_list/file_card/styles.ts b/app/screens/global_threads/threads_list/file_card/styles.ts new file mode 100644 index 00000000000..a505deff826 --- /dev/null +++ b/app/screens/global_threads/threads_list/file_card/styles.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {blendColors, makeStyleSheetFromTheme, changeOpacity} from '@app/utils/theme'; + +export const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + previewThreadContainer: { + flexDirection: 'row', + borderColor: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3), + borderWidth: 1, + width: 350, + alignItems: 'center', + height: 65, + paddingLeft: 6, + paddingRight: 6, + borderRadius: 4, + overflow: 'hidden', + }, + fileContainer: { + flexDirection: 'row', + flex: 1, + }, + nameContainer: { + color: theme.centerChannelColor, + flex: 1, + paddingLeft: 6, + paddingRight: 6, + }, + sizeContainer: { + fontSize: 12, + color: changeOpacity(theme.centerChannelColor, 0.64), + }, + imagePreview: { + maxWidth: 50, + maxHeight: 50, + minHeight: 50, + minWidth: 50, + borderRadius: 20, + }, + imageContainer: { + borderRadius: 4, + overflow: 'hidden', + }, +})); diff --git a/app/screens/global_threads/threads_list/thread/thread.tsx b/app/screens/global_threads/threads_list/thread/thread.tsx index c85a1e11b2f..21e584dd5a7 100644 --- a/app/screens/global_threads/threads_list/thread/thread.tsx +++ b/app/screens/global_threads/threads_list/thread/thread.tsx @@ -7,9 +7,9 @@ import {Text, TouchableHighlight, View} from 'react-native'; import {switchToChannelById} from '@actions/remote/channel'; import {fetchAndSwitchToThread} from '@actions/remote/thread'; +import Markdown from '@app/components/markdown'; import FormattedText from '@components/formatted_text'; import FriendlyDate from '@components/friendly_date'; -import RemoveMarkdown from '@components/remove_markdown'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; @@ -22,6 +22,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {displayUsername} from '@utils/user'; +import FileCard from '../file_card/file_card'; + import ThreadFooter from './thread_footer'; import type ChannelModel from '@typings/database/models/servers/channel'; @@ -120,6 +122,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { alignSelf: 'center', color: theme.buttonColor, }, + threadText: { + overflow: 'hidden', + maxHeight: 40, + }, }; }); @@ -215,18 +221,25 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t ); if (post?.message) { postBody = ( - - - + <> + + + + + ); + } else { + postBody = ( + ); } } diff --git a/app/utils/file/constants.tsx b/app/utils/file/constants.tsx new file mode 100644 index 00000000000..f882240ecf9 --- /dev/null +++ b/app/utils/file/constants.tsx @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +export const FileTypes = { + TEXT: 'text', + IMAGE: 'image', + AUDIO: 'audio', + VIDEO: 'video', + SPREADSHEET: 'spreadsheet', + CODE: 'code', + WORD: 'word', + PRESENTATION: 'presentation', + PDF: 'pdf', + PATCH: 'patch', + SVG: 'svg', + OTHER: 'other', + LICENSE_EXTENSION: '.mattermost-license', +}; diff --git a/app/utils/file/index.ts b/app/utils/file/index.ts index ac495ecbfb1..39c2a145c6d 100644 --- a/app/utils/file/index.ts +++ b/app/utils/file/index.ts @@ -18,6 +18,8 @@ import {logError} from '@utils/log'; import {deleteEntitiesFile, getIOSAppGroupDetails} from '@utils/mattermost_managed'; import {urlSafeBase64Encode} from '@utils/security'; +import {FileTypes} from './constants'; + import type {PastedFile} from '@mattermost/react-native-paste-input'; import type FileModel from '@typings/database/models/servers/file'; import type {IntlShape} from 'react-intl'; @@ -326,6 +328,52 @@ export function getFormattedFileSize(bytes: number): string { return `${bytes} B`; } +export const getFormat = (ext: string) => { + if (Files.TEXT_TYPES.indexOf(ext) > -1) { + return FileTypes.TEXT; + } + + if (Files.IMAGE_TYPES.indexOf(ext) > -1) { + return FileTypes.IMAGE; + } + + if (Files.AUDIO_TYPES.indexOf(ext) > -1) { + return FileTypes.AUDIO; + } + + if (Files.VIDEO_TYPES.indexOf(ext) > -1) { + return FileTypes.VIDEO; + } + + if (Files.SPREADSHEET_TYPES.indexOf(ext) > -1) { + return FileTypes.SPREADSHEET; + } + + if (Files.CODE_TYPES.indexOf(ext) > -1) { + return FileTypes.CODE; + } + + if (Files.WORD_TYPES.indexOf(ext) > -1) { + return FileTypes.WORD; + } + + if (Files.PRESENTATION_TYPES.indexOf(ext) > -1) { + return FileTypes.PRESENTATION; + } + + if (Files.PDF_TYPES.indexOf(ext) > -1) { + return FileTypes.PDF; + } + + if (Files.PATCH_TYPES.indexOf(ext) > -1) { + return FileTypes.PATCH; + } + + if (Files.SVG_TYPES.indexOf(ext) > -1) { + return FileTypes.SVG; + } + return FileTypes.OTHER; +}; export function getFileType(file: FileInfo | ExtractedFileInfo): string { if (!file || !file.extension) {