Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-58314] [MM-43784] Markdown and file preview in thread list #7963

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/constants/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ export const VALID_IMAGE_MIME_TYPES = [
export const Files: Record<string, string[]> = {
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'],
Expand Down
146 changes: 146 additions & 0 deletions app/screens/global_threads/threads_list/file_card/file_card.tsx
Original file line number Diff line number Diff line change
@@ -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<CardProps> = ({post, theme}) => {
const [files, setFiles] = useState<FileModel[]>([]);
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 = (
<View
style={styles.previewThreadContainer}
>
<View style={styles.imageContainer}>
<ProgressiveImage
style={styles.imagePreview}
thumbnailUri={thumbnailUri}
imageUri={imageUri}
id={files[0].id!}
onError={handleError}
/>
</View>

{files.length > 0 && (
<View
style={styles.fileContainer}
>
<Text
style={styles.nameContainer}
numberOfLines={1}
>
{files[0].name}
</Text>
<Text
style={styles.sizeContainer}
>
{`${getFormattedFileSize(files[0].size)}`}
</Text>
</View>
)}
</View>
);
if (failed) {
file = (
<View
style={styles.previewThreadContainer}
>
<View
style={styles.fileContainer}
>
<Text
style={styles.nameContainer}
numberOfLines={1}
>
{files[0].name}
</Text>
<Text
style={styles.sizeContainer}
>
{`${getFormattedFileSize(files[0].size)}`}
</Text>
</View>
</View>
);
}
return file;
default:
file = (
<View
style={styles.previewThreadContainer}
>
<View style={styles.imageContainer}>
<FileIcon
file={files[0].toFileInfo(post.author.id)}
/>
</View>

{files.length > 0 && (
<View
style={styles.fileContainer}
>
<Text
style={styles.nameContainer}
numberOfLines={1}
>
{files[0].name}
</Text>
<Text
style={styles.sizeContainer}
>
{`${getFormattedFileSize(files[0].size)}`}
</Text>
</View>
)}
</View>

);
return file;
}
}
return null;
};

export default FileCard;

43 changes: 43 additions & 0 deletions app/screens/global_threads/threads_list/file_card/styles.ts
Original file line number Diff line number Diff line change
@@ -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',
},
}));
39 changes: 26 additions & 13 deletions app/screens/global_threads/threads_list/thread/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -120,6 +122,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
alignSelf: 'center',
color: theme.buttonColor,
},
threadText: {
overflow: 'hidden',
maxHeight: 40,
},
};
});

Expand Down Expand Up @@ -215,18 +221,25 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t
);
if (post?.message) {
postBody = (
<Text numberOfLines={2}>
<RemoveMarkdown
enableCodeSpan={true}
enableEmoji={true}
enableChannelLink={true}
enableHardBreak={true}
enableSoftBreak={true}
textStyle={textStyles}
baseStyle={styles.message}
value={post.message.substring(0, 100)} // This substring helps to avoid ANR's
/>
</Text>
<>
<View style={styles.threadText}>
<Markdown
theme={theme}
baseTextStyle={styles.message}
textStyles={textStyles}
value={post.message}
location={location}
imagesMetadata={post.metadata?.images}
/>
</View>
</>
);
} else {
postBody = (
<FileCard
post={post}
theme={theme}
/>
);
}
}
Expand Down
17 changes: 17 additions & 0 deletions app/utils/file/constants.tsx
Original file line number Diff line number Diff line change
@@ -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',
};
48 changes: 48 additions & 0 deletions app/utils/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Loading