From a11aad3a5af9ba317349445cfdb6d38dcd8b3bf2 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Sat, 24 Apr 2021 12:50:36 +0200 Subject: [PATCH] Migrate message attachments to typescript (#5127) (#5348) --- .../button_binding/button_binding_text.tsx | 2 +- .../embedded_sub_bindings.tsx | 2 +- app/components/embedded_bindings/index.tsx | 2 +- app/components/emoji/emoji.js | 117 -------------- app/components/emoji/emoji.tsx | 116 ++++++++++++++ app/components/emoji/{index.js => index.ts} | 13 +- .../emoji_picker/emoji_picker_row.js | 2 +- .../file_attachment_list/file_attachment.js | 8 +- app/components/markdown/markdown.js | 14 +- .../markdown/markdown_emoji/markdown_emoji.js | 6 +- .../markdown_table_image.tsx | 8 +- ...s.snap => attachment_footer.test.tsx.snap} | 44 +----- ..._button.test.js => action_button.test.tsx} | 7 +- .../{action_button.js => action_button.tsx} | 40 ++--- ...xt.test.js => action_button_text.test.tsx} | 1 + ..._button_text.js => action_button_text.tsx} | 16 +- .../action_button/index.js | 26 ---- .../action_button/index.ts | 32 ++++ .../action_menu/action_menu.js | 81 ---------- ...tion_menu.test.js => action_menu.test.tsx} | 6 +- .../action_menu/action_menu.tsx | 66 ++++++++ .../action_menu/{index.js => index.ts} | 15 +- .../message_attachments/attachment_actions.js | 67 -------- .../attachment_actions.tsx | 65 ++++++++ ...chment_author.js => attachment_author.tsx} | 19 ++- .../message_attachments/attachment_fields.js | 142 ----------------- .../message_attachments/attachment_fields.tsx | 145 +++++++++++++++++ .../message_attachments/attachment_footer.js | 78 ---------- ...ter.test.js => attachment_footer.test.tsx} | 11 -- .../message_attachments/attachment_footer.tsx | 77 +++++++++ ...index.test.js.snap => index.test.tsx.snap} | 0 .../{index.test.js => index.test.tsx} | 17 +- .../attachment_image/{index.js => index.tsx} | 63 +++++--- .../message_attachments/attachment_pretext.js | 54 ------- .../attachment_pretext.tsx | 54 +++++++ ...attachment_text.js => attachment_text.tsx} | 56 ++++--- .../attachment_thumbnail.js | 44 ------ .../attachment_thumbnail.tsx | 41 +++++ ...tachment_title.js => attachment_title.tsx} | 16 +- app/components/message_attachments/index.js | 66 -------- app/components/message_attachments/index.tsx | 64 ++++++++ .../message_attachments/message_attachment.js | 147 ------------------ .../message_attachment.tsx | 147 ++++++++++++++++++ app/components/post_attachment_image/index.js | 6 +- .../post_body_additional_content.js | 2 +- .../reaction_picker/reaction_button.js | 8 +- app/components/reactions/reaction.js | 6 +- app/constants/attachment.js | 1 - app/mm-redux/types/message_attachments.ts | 30 ++++ app/screens/gallery/gallery_viewer.tsx | 2 +- .../reaction_list/reaction_header_item.js | 8 +- app/utils/{images.test.js => images.test.ts} | 17 +- app/utils/{images.js => images.ts} | 43 ++--- ...colors.js => message_attachment_colors.ts} | 7 +- 54 files changed, 1084 insertions(+), 1043 deletions(-) delete mode 100644 app/components/emoji/emoji.js create mode 100644 app/components/emoji/emoji.tsx rename app/components/emoji/{index.js => index.ts} (82%) rename app/components/message_attachments/__snapshots__/{attachment_footer.test.js.snap => attachment_footer.test.tsx.snap} (57%) rename app/components/message_attachments/action_button/{action_button.test.js => action_button.test.tsx} (96%) rename app/components/message_attachments/action_button/{action_button.js => action_button.tsx} (70%) rename app/components/message_attachments/action_button/{action_button_text.test.js => action_button_text.test.tsx} (99%) rename app/components/message_attachments/action_button/{action_button_text.js => action_button_text.tsx} (82%) delete mode 100644 app/components/message_attachments/action_button/index.js create mode 100644 app/components/message_attachments/action_button/index.ts delete mode 100644 app/components/message_attachments/action_menu/action_menu.js rename app/components/message_attachments/action_menu/{action_menu.test.js => action_menu.test.tsx} (89%) create mode 100644 app/components/message_attachments/action_menu/action_menu.tsx rename app/components/message_attachments/action_menu/{index.js => index.ts} (61%) delete mode 100644 app/components/message_attachments/attachment_actions.js create mode 100644 app/components/message_attachments/attachment_actions.tsx rename app/components/message_attachments/{attachment_author.js => attachment_author.tsx} (87%) delete mode 100644 app/components/message_attachments/attachment_fields.js create mode 100644 app/components/message_attachments/attachment_fields.tsx delete mode 100644 app/components/message_attachments/attachment_footer.js rename app/components/message_attachments/{attachment_footer.test.js => attachment_footer.test.tsx} (79%) create mode 100644 app/components/message_attachments/attachment_footer.tsx rename app/components/message_attachments/attachment_image/__snapshots__/{index.test.js.snap => index.test.tsx.snap} (100%) rename app/components/message_attachments/attachment_image/{index.test.js => index.test.tsx} (77%) rename app/components/message_attachments/attachment_image/{index.js => index.tsx} (77%) delete mode 100644 app/components/message_attachments/attachment_pretext.js create mode 100644 app/components/message_attachments/attachment_pretext.tsx rename app/components/message_attachments/{attachment_text.js => attachment_text.tsx} (67%) delete mode 100644 app/components/message_attachments/attachment_thumbnail.js create mode 100644 app/components/message_attachments/attachment_thumbnail.tsx rename app/components/message_attachments/{attachment_title.js => attachment_title.tsx} (90%) delete mode 100644 app/components/message_attachments/index.js create mode 100644 app/components/message_attachments/index.tsx delete mode 100644 app/components/message_attachments/message_attachment.js create mode 100644 app/components/message_attachments/message_attachment.tsx create mode 100644 app/mm-redux/types/message_attachments.ts rename app/utils/{images.test.js => images.test.ts} (93%) rename app/utils/{images.js => images.ts} (79%) rename app/utils/{message_attachment_colors.js => message_attachment_colors.ts} (64%) diff --git a/app/components/embedded_bindings/button_binding/button_binding_text.tsx b/app/components/embedded_bindings/button_binding/button_binding_text.tsx index 1e6f94f9c..722fd5384 100644 --- a/app/components/embedded_bindings/button_binding/button_binding_text.tsx +++ b/app/components/embedded_bindings/button_binding/button_binding_text.tsx @@ -10,7 +10,7 @@ import {reEmoji, reEmoticon, reMain} from '@constants/emoji'; import {getEmoticonName} from '@utils/emoji_utils'; export default function ButtonBindingText({message, style}: {message: string; style: StyleProp}) { - const components = [] as JSX.Element[]; + const components = [] as React.ReactNode[]; let text = message; while (text) { diff --git a/app/components/embedded_bindings/embedded_sub_bindings.tsx b/app/components/embedded_bindings/embedded_sub_bindings.tsx index 85805ce01..ac95e6296 100644 --- a/app/components/embedded_bindings/embedded_sub_bindings.tsx +++ b/app/components/embedded_bindings/embedded_sub_bindings.tsx @@ -21,7 +21,7 @@ export default function EmbeddedSubBindings(props: Props) { return null; } - const content = [] as JSX.Element[]; + const content = [] as React.ReactNode[]; bindings.forEach((binding) => { if (!binding.app_id || !binding.call) { diff --git a/app/components/embedded_bindings/index.tsx b/app/components/embedded_bindings/index.tsx index e5c9963fb..9ab539cc4 100644 --- a/app/components/embedded_bindings/index.tsx +++ b/app/components/embedded_bindings/index.tsx @@ -31,7 +31,7 @@ export default function EmbeddedBindings(props: Props) { theme, textStyles, } = props; - const content = [] as JSX.Element[]; + const content = [] as React.ReactNode[]; embeds.forEach((embed, i) => { content.push( diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js deleted file mode 100644 index a1d9d6f38..000000000 --- a/app/components/emoji/emoji.js +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Platform, - StyleSheet, - Text, -} from 'react-native'; -import FastImage from 'react-native-fast-image'; - -export default class Emoji extends React.PureComponent { - static propTypes = { - - /* - * Emoji text name. - */ - emojiName: PropTypes.string.isRequired, - - /* - * Image URL for the emoji. - */ - imageUrl: PropTypes.string.isRequired, - - /* - * Set if this is a custom emoji. - */ - isCustomEmoji: PropTypes.bool.isRequired, - - /* - * Set to render only the text and no image. - */ - displayTextOnly: PropTypes.bool, - literal: PropTypes.string, - size: PropTypes.number, - textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - unicode: PropTypes.string, - customEmojiStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - testID: PropTypes.string, - }; - - static defaultProps = { - customEmojis: new Map(), - literal: '', - imageUrl: '', - isCustomEmoji: false, - }; - - render() { - const { - customEmojiStyle, - displayTextOnly, - imageUrl, - literal, - unicode, - testID, - textStyle, - } = this.props; - - let size = this.props.size; - let fontSize = size; - if (!size && textStyle) { - const flatten = StyleSheet.flatten(textStyle); - fontSize = flatten.fontSize; - size = fontSize; - } - - if (displayTextOnly) { - return ( - - {literal} - ); - } - - const width = size; - const height = size; - - // Android can't change the size of an image after its first render, so - // force a new image to be rendered when the size changes - const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null; - - if (unicode && !imageUrl) { - const codeArray = unicode.split('-'); - const code = codeArray.reduce((acc, c) => { - return acc + String.fromCodePoint(parseInt(c, 16)); - }, ''); - - return ( - - {code} - - ); - } - - if (!imageUrl) { - return null; - } - - return ( - - ); - } -} diff --git a/app/components/emoji/emoji.tsx b/app/components/emoji/emoji.tsx new file mode 100644 index 000000000..4887c0ab5 --- /dev/null +++ b/app/components/emoji/emoji.tsx @@ -0,0 +1,116 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import { + Platform, + StyleProp, + StyleSheet, + Text, + TextStyle, +} from 'react-native'; +import FastImage, {ImageStyle} from 'react-native-fast-image'; + +type Props = { + + /* + * Emoji text name. + */ + emojiName: string; + + /* + * Image URL for the emoji. + */ + imageUrl: string; + + /* + * Set if this is a custom emoji. + */ + isCustomEmoji: boolean; + + /* + * Set to render only the text and no image. + */ + displayTextOnly?: boolean; + literal?: string; + size?: number; + textStyle?: StyleProp; + unicode?: string; + customEmojiStyle?: StyleProp; + testID?: string; +} + +const Emoji: React.FC = (props: Props) => { + const { + customEmojiStyle, + displayTextOnly, + imageUrl, + literal, + unicode, + testID, + textStyle, + } = props; + + let size = props.size; + let fontSize = size; + if (!size && textStyle) { + const flatten = StyleSheet.flatten(textStyle); + fontSize = flatten.fontSize; + size = fontSize; + } + + if (displayTextOnly) { + return ( + + {literal} + ); + } + + const width = size; + const height = size; + + if (unicode && !imageUrl) { + const codeArray = unicode.split('-'); + const code = codeArray.reduce((acc, c) => { + return acc + String.fromCodePoint(parseInt(c, 16)); + }, ''); + + return ( + + {code} + + ); + } + + if (!imageUrl) { + return null; + } + + // Android can't change the size of an image after its first render, so + // force a new image to be rendered when the size changes + const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null; + + return ( + + ); +}; + +Emoji.defaultProps = { + literal: '', + imageUrl: '', + isCustomEmoji: false, +}; + +export default Emoji; diff --git a/app/components/emoji/index.js b/app/components/emoji/index.ts similarity index 82% rename from app/components/emoji/index.js rename to app/components/emoji/index.ts index a602c8b18..b00d22bec 100644 --- a/app/components/emoji/index.js +++ b/app/components/emoji/index.ts @@ -8,12 +8,17 @@ import {getCurrentUserId} from '@mm-redux/selectors/entities/users'; import {getConfig} from '@mm-redux/selectors/entities/general'; import {Client4} from '@client/rest'; import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; +import {GlobalState} from '@mm-redux/types/store'; -import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis'; +import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from '@utils/emojis'; import Emoji from './emoji'; -function mapStateToProps(state, ownProps) { +type OwnProps = { + emojiName: string; +} + +function mapStateToProps(state: GlobalState, ownProps: OwnProps) { const config = getConfig(state); const emojiName = ownProps.emojiName; const customEmojis = getCustomEmojisByName(state); @@ -24,7 +29,7 @@ function mapStateToProps(state, ownProps) { let isCustomEmoji = false; let displayTextOnly = false; if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) { - const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)]; + const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)!]; unicode = emoji.filename; if (BuiltInEmojis.includes(emojiName)) { if (serverUrl) { @@ -35,7 +40,7 @@ function mapStateToProps(state, ownProps) { } } else if (customEmojis.has(emojiName) && serverUrl) { const emoji = customEmojis.get(emojiName); - imageUrl = Client4.getCustomEmojiImageUrl(emoji.id); + imageUrl = Client4.getCustomEmojiImageUrl(emoji!.id); isCustomEmoji = true; } else { displayTextOnly = state.entities.emojis.nonExistentEmoji.has(emojiName) || diff --git a/app/components/emoji_picker/emoji_picker_row.js b/app/components/emoji_picker/emoji_picker_row.js index 5527845f9..aa42d4de7 100644 --- a/app/components/emoji_picker/emoji_picker_row.js +++ b/app/components/emoji_picker/emoji_picker_row.js @@ -10,7 +10,7 @@ import { } from 'react-native'; import shallowEqual from 'shallow-equals'; -import Emoji from 'app/components/emoji'; +import Emoji from '@components/emoji'; export default class EmojiPickerRow extends Component { static propTypes = { diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js index 0f9f96a9e..36f6adfa6 100644 --- a/app/components/file_attachment_list/file_attachment.js +++ b/app/components/file_attachment_list/file_attachment.js @@ -13,10 +13,10 @@ import { import * as Utils from '@mm-redux/utils/file_utils'; -import TouchableWithFeedback from 'app/components/touchable_with_feedback'; -import {isDocument, isImage} from 'app/utils/file'; -import {calculateDimensions} from 'app/utils/images'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {isDocument, isImage} from '@utils/file'; +import {calculateDimensions} from '@utils/images'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import FileAttachmentDocument from './file_attachment_document'; import FileAttachmentIcon from './file_attachment_icon'; diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js index ce78bdc9f..9a51872f8 100644 --- a/app/components/markdown/markdown.js +++ b/app/components/markdown/markdown.js @@ -11,13 +11,13 @@ import { View, } from 'react-native'; -import AtMention from 'app/components/at_mention'; -import ChannelLink from 'app/components/channel_link'; -import Emoji from 'app/components/emoji'; -import FormattedText from 'app/components/formatted_text'; -import Hashtag from 'app/components/markdown/hashtag'; -import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {getScheme} from 'app/utils/url'; +import AtMention from '@components/at_mention'; +import ChannelLink from '@components/channel_link'; +import Emoji from '@components/emoji'; +import FormattedText from '@components/formatted_text'; +import Hashtag from '@components/markdown/hashtag'; +import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme'; +import {getScheme} from '@utils/url'; import MarkdownBlockQuote from './markdown_block_quote'; import MarkdownCodeBlock from './markdown_code_block'; diff --git a/app/components/markdown/markdown_emoji/markdown_emoji.js b/app/components/markdown/markdown_emoji/markdown_emoji.js index 4c4b8a7b6..de086d250 100644 --- a/app/components/markdown/markdown_emoji/markdown_emoji.js +++ b/app/components/markdown/markdown_emoji/markdown_emoji.js @@ -7,9 +7,9 @@ import React, {PureComponent} from 'react'; import {Platform, Text, View} from 'react-native'; import PropTypes from 'prop-types'; -import Emoji from 'app/components/emoji'; -import FormattedText from 'app/components/formatted_text'; -import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme'; +import Emoji from '@components/emoji'; +import FormattedText from '@components/formatted_text'; +import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme'; export default class MarkdownEmoji extends PureComponent { static propTypes = { diff --git a/app/components/markdown/markdown_table_image/markdown_table_image.tsx b/app/components/markdown/markdown_table_image/markdown_table_image.tsx index 5d4159489..aa3571d6f 100644 --- a/app/components/markdown/markdown_table_image/markdown_table_image.tsx +++ b/app/components/markdown/markdown_table_image/markdown_table_image.tsx @@ -13,6 +13,7 @@ import {calculateDimensions, isGifTooLarge, openGalleryAtIndex} from '@utils/ima import {generateId} from '@utils/file'; import type {PostImage} from '@mm-redux/types/posts'; +import {FileInfo} from '@mm-redux/types/files'; type MarkdownTableImageProps = { disable: boolean; @@ -78,8 +79,11 @@ const MarkTableImage = ({disable, imagesMetadata, postId, serverURL, source}: Ma return; } - const files = [getFileInfo()]; - openGalleryAtIndex(0, files); + const file = getFileInfo() as FileInfo; + if (!file) { + return; + } + openGalleryAtIndex(0, [file]); }, []); const onLoadFailed = useCallback(() => { diff --git a/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap b/app/components/message_attachments/__snapshots__/attachment_footer.test.tsx.snap similarity index 57% rename from app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap rename to app/components/message_attachments/__snapshots__/attachment_footer.test.tsx.snap index 1e202e71a..f74ad1640 100644 --- a/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap +++ b/app/components/message_attachments/__snapshots__/attachment_footer.test.tsx.snap @@ -27,7 +27,9 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a } /> - - - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… - - -`; diff --git a/app/components/message_attachments/action_button/action_button.test.js b/app/components/message_attachments/action_button/action_button.test.tsx similarity index 96% rename from app/components/message_attachments/action_button/action_button.test.js rename to app/components/message_attachments/action_button/action_button.test.tsx index 0ee7a3bd9..36b40a1a9 100644 --- a/app/components/message_attachments/action_button/action_button.test.js +++ b/app/components/message_attachments/action_button/action_button.test.tsx @@ -3,9 +3,11 @@ import React from 'react'; import {shallow} from 'enzyme'; + import ActionButton from './action_button'; -import {changeOpacity} from 'app/utils/theme'; -import {getStatusColors} from 'app/utils/message_attachment_colors'; + +import {changeOpacity} from '@utils/theme'; +import {getStatusColors} from '@utils/message_attachment_colors'; import Preferences from '@mm-redux/constants/preferences'; @@ -106,7 +108,6 @@ describe('ActionButton', () => { cookie: '', name: buttonConfig.name, postId: buttonConfig.id, - buttonColor: buttonConfig.style, theme: Preferences.THEMES.default, actions: { doPostActionWithCookie: jest.fn(), diff --git a/app/components/message_attachments/action_button/action_button.js b/app/components/message_attachments/action_button/action_button.tsx similarity index 70% rename from app/components/message_attachments/action_button/action_button.js rename to app/components/message_attachments/action_button/action_button.tsx index 0e063bf02..727976230 100644 --- a/app/components/message_attachments/action_button/action_button.js +++ b/app/components/message_attachments/action_button/action_button.tsx @@ -2,31 +2,33 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; import Button from 'react-native-button'; -import {preventDoubleTap} from 'app/utils/tap'; -import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; -import {getStatusColors} from 'app/utils/message_attachment_colors'; import ActionButtonText from './action_button_text'; -export default class ActionButton extends PureComponent { - static propTypes = { - actions: PropTypes.shape({ - doPostActionWithCookie: PropTypes.func.isRequired, - }).isRequired, - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - postId: PropTypes.string.isRequired, - theme: PropTypes.object.isRequired, - cookie: PropTypes.string, - disabled: PropTypes.bool, - buttonColor: PropTypes.string, - }; +import {preventDoubleTap} from '@utils/tap'; +import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; +import {getStatusColors} from '@utils/message_attachment_colors'; +import {Theme} from '@mm-redux/types/preferences'; +import {ActionResult} from '@mm-redux/types/actions'; + +type Props = { + actions: { + doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise; + }; + id: string; + name: string; + postId: string; + theme: Theme, + cookie?: string, + disabled?: boolean, + buttonColor?: string, +} +export default class ActionButton extends PureComponent { handleActionPress = preventDoubleTap(() => { const {actions, id, postId, cookie} = this.props; - actions.doPostActionWithCookie(postId, id, cookie); + actions.doPostActionWithCookie(postId, id, cookie || ''); }, 4000); render() { @@ -58,7 +60,7 @@ export default class ActionButton extends PureComponent { } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const STATUS_COLORS = getStatusColors(theme); return { button: { diff --git a/app/components/message_attachments/action_button/action_button_text.test.js b/app/components/message_attachments/action_button/action_button_text.test.tsx similarity index 99% rename from app/components/message_attachments/action_button/action_button_text.test.js rename to app/components/message_attachments/action_button/action_button_text.test.tsx index 489b16a9f..3ef5e263a 100644 --- a/app/components/message_attachments/action_button/action_button_text.test.js +++ b/app/components/message_attachments/action_button/action_button_text.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; + import ActionButtonText from './action_button_text'; describe('ActionButtonText emojis', () => { diff --git a/app/components/message_attachments/action_button/action_button_text.js b/app/components/message_attachments/action_button/action_button_text.tsx similarity index 82% rename from app/components/message_attachments/action_button/action_button_text.js rename to app/components/message_attachments/action_button/action_button_text.tsx index c947b8c83..01c280fff 100644 --- a/app/components/message_attachments/action_button/action_button_text.js +++ b/app/components/message_attachments/action_button/action_button_text.tsx @@ -3,13 +3,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Text, View, StyleSheet} from 'react-native'; -import Emoji from 'app/components/emoji'; -import {getEmoticonName} from 'app/utils/emoji_utils'; -import {reEmoji, reEmoticon, reMain} from 'app/constants/emoji'; +import {Text, View, StyleSheet, StyleProp, TextStyle} from 'react-native'; -export default function ActionButtonText({message, style}) { - const components = []; +import Emoji from '@components/emoji'; +import {reEmoji, reEmoticon, reMain} from '@constants/emoji'; +import {getEmoticonName} from '@utils/emoji_utils'; + +export default function ActionButtonText({message, style}: {message: string; style: StyleProp}) { + const components = [] as React.ReactNode[]; let text = message; while (text) { @@ -49,6 +50,9 @@ export default function ActionButtonText({message, style}) { // This is plain text, so capture as much text as possible until we hit the next possible emoji. Note that // reMain always captures at least one character, so text will always be getting shorter match = text.match(reMain); + if (!match) { + continue; + } components.push( Promise; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators, Actions>({ + doPostActionWithCookie, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ActionButton); diff --git a/app/components/message_attachments/action_menu/action_menu.js b/app/components/message_attachments/action_menu/action_menu.js deleted file mode 100644 index 8157556d5..000000000 --- a/app/components/message_attachments/action_menu/action_menu.js +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; - -import AutocompleteSelector from 'app/components/autocomplete_selector'; - -export default class ActionMenu extends PureComponent { - static propTypes = { - actions: PropTypes.shape({ - selectAttachmentMenuAction: PropTypes.func.isRequired, - }).isRequired, - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - dataSource: PropTypes.string, - defaultOption: PropTypes.string, - options: PropTypes.arrayOf(PropTypes.object), - postId: PropTypes.string.isRequired, - selected: PropTypes.object, - disabled: PropTypes.bool, - }; - - constructor(props) { - super(props); - - let selected; - if (props.defaultOption && props.options) { - selected = props.options.find((option) => option.value === props.defaultOption); - } - - this.state = { - selected, - }; - } - - static getDerivedStateFromProps(props, state) { - if (props.selected && props.selected !== state.selected) { - return { - selected: props.selected, - }; - } - - return null; - } - - handleSelect = (selected) => { - if (!selected) { - return; - } - - const { - actions, - id, - postId, - } = this.props; - - actions.selectAttachmentMenuAction(postId, id, selected.text, selected.value); - }; - - render() { - const { - name, - dataSource, - options, - disabled, - } = this.props; - const {selected} = this.state; - - return ( - - ); - } -} diff --git a/app/components/message_attachments/action_menu/action_menu.test.js b/app/components/message_attachments/action_menu/action_menu.test.tsx similarity index 89% rename from app/components/message_attachments/action_menu/action_menu.test.js rename to app/components/message_attachments/action_menu/action_menu.test.tsx index 517f15f05..029f017db 100644 --- a/app/components/message_attachments/action_menu/action_menu.test.js +++ b/app/components/message_attachments/action_menu/action_menu.test.tsx @@ -29,7 +29,7 @@ describe('ActionMenu', () => { test('should start with nothing selected when no default is selected', () => { const wrapper = shallow(); - expect(wrapper.state('selected')).toBeUndefined(); + expect(wrapper.props().selected).toBeUndefined(); }); test('should set selected based on default option', () => { @@ -39,7 +39,7 @@ describe('ActionMenu', () => { }; const wrapper = shallow(); - expect(wrapper.state('selected')).toBe(props.options[1]); + expect(wrapper.props().selected).toBe(props.options[1]); }); test('should start with previous value selected', () => { @@ -50,7 +50,7 @@ describe('ActionMenu', () => { }; const wrapper = shallow(); - expect(wrapper.state('selected')).toBe(props.selected); + expect(wrapper.props().selected).toBe(props.selected); }); test('disabled works', () => { diff --git a/app/components/message_attachments/action_menu/action_menu.tsx b/app/components/message_attachments/action_menu/action_menu.tsx new file mode 100644 index 000000000..cec409495 --- /dev/null +++ b/app/components/message_attachments/action_menu/action_menu.tsx @@ -0,0 +1,66 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import AutocompleteSelector from '@components/autocomplete_selector'; +import {PostActionOption} from '@mm-redux/types/integration_actions'; + +type Props = { + actions: { + selectAttachmentMenuAction: (postId: string, actionId: string, text: string, value: string) => void; + }; + id: string; + name: string; + dataSource?: string; + defaultOption?: string; + options?: PostActionOption[]; + postId: string; + selected?: PostActionOption; + disabled?: boolean; +} + +const ActionMenu: React.FC = (props: Props) => { + let selected: PostActionOption | undefined; + if (props.defaultOption && props.options) { + selected = props.options.find((option) => option.value === props.defaultOption); + } + + if (props.selected) { + selected = props.selected; + } + + const handleSelect = (selectedItem?: PostActionOption) => { + if (!selectedItem) { + return; + } + + const { + actions, + id, + postId, + } = props; + + actions.selectAttachmentMenuAction(postId, id, selectedItem.text, selectedItem.value); + }; + + const { + name, + dataSource, + options, + disabled, + } = props; + + return ( + + ); +}; + +export default ActionMenu; diff --git a/app/components/message_attachments/action_menu/index.js b/app/components/message_attachments/action_menu/index.ts similarity index 61% rename from app/components/message_attachments/action_menu/index.js rename to app/components/message_attachments/action_menu/index.ts index 913d338fb..d85620253 100644 --- a/app/components/message_attachments/action_menu/index.js +++ b/app/components/message_attachments/action_menu/index.ts @@ -1,14 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {bindActionCreators} from 'redux'; +import {bindActionCreators, Dispatch} from 'redux'; import {connect} from 'react-redux'; -import {selectAttachmentMenuAction} from 'app/actions/views/post'; +import {GlobalState} from '@mm-redux/types/store'; + +import {selectAttachmentMenuAction} from '@actions/views/post'; import ActionMenu from './action_menu'; -function mapStateToProps(state, ownProps) { +type OwnProps = { + postId: string; + id: string; +} + +function mapStateToProps(state: GlobalState, ownProps: OwnProps) { const actions = state.views.post.submittedMenuActions[ownProps.postId]; const selected = actions?.[ownProps.id]; @@ -17,7 +24,7 @@ function mapStateToProps(state, ownProps) { }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators({ selectAttachmentMenuAction, diff --git a/app/components/message_attachments/attachment_actions.js b/app/components/message_attachments/attachment_actions.js deleted file mode 100644 index d5f96bbef..000000000 --- a/app/components/message_attachments/attachment_actions.js +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; - -import ActionMenu from './action_menu'; -import ActionButton from './action_button'; - -export default class AttachmentActions extends PureComponent { - static propTypes = { - actions: PropTypes.array, - postId: PropTypes.string.isRequired, - }; - - render() { - const { - actions, - postId, - } = this.props; - - if (!actions?.length) { - return null; - } - - const content = []; - - actions.forEach((action) => { - if (!action.id || !action.name) { - return; - } - - switch (action.type) { - case 'select': - content.push( - , - ); - break; - case 'button': - default: - content.push( - , - ); - break; - } - }); - - return content.length ? content : null; - } -} diff --git a/app/components/message_attachments/attachment_actions.tsx b/app/components/message_attachments/attachment_actions.tsx new file mode 100644 index 000000000..52adf9cfb --- /dev/null +++ b/app/components/message_attachments/attachment_actions.tsx @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import ActionMenu from './action_menu'; +import ActionButton from './action_button'; + +import {PostAction} from '@mm-redux/types/integration_actions'; + +type Props = { + actions?: PostAction[]; + postId: string; +} +export default function AttachmentActions(props: Props) { + const { + actions, + postId, + } = props; + + if (!actions?.length) { + return null; + } + + const content = [] as React.ReactNode[]; + + actions.forEach((action) => { + if (!action.id || !action.name) { + return; + } + + switch (action.type) { + case 'select': + content.push( + , + ); + break; + case 'button': + default: + content.push( + , + ); + break; + } + }); + + return content.length ? (<>{content}) : null; +} diff --git a/app/components/message_attachments/attachment_author.js b/app/components/message_attachments/attachment_author.tsx similarity index 87% rename from app/components/message_attachments/attachment_author.js rename to app/components/message_attachments/attachment_author.tsx index d69f528db..0422e5cd6 100644 --- a/app/components/message_attachments/attachment_author.js +++ b/app/components/message_attachments/attachment_author.tsx @@ -5,19 +5,18 @@ import React, {PureComponent} from 'react'; import {Alert, Text, View} from 'react-native'; import FastImage from 'react-native-fast-image'; import {intlShape} from 'react-intl'; -import PropTypes from 'prop-types'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {tryOpenURL} from '@utils/url'; +import {Theme} from '@mm-redux/types/preferences'; -export default class AttachmentAuthor extends PureComponent { - static propTypes = { - icon: PropTypes.string, - link: PropTypes.string, - name: PropTypes.string, - theme: PropTypes.object.isRequired, - }; - +type Props = { + icon?: string; + link?: string; + name?: string; + theme: Theme; +} +export default class AttachmentAuthor extends PureComponent { static contextTypes = { intl: intlShape.isRequired, }; @@ -81,7 +80,7 @@ export default class AttachmentAuthor extends PureComponent { } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { container: { flex: 1, diff --git a/app/components/message_attachments/attachment_fields.js b/app/components/message_attachments/attachment_fields.js deleted file mode 100644 index 74c487ad0..000000000 --- a/app/components/message_attachments/attachment_fields.js +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import {Text, View} from 'react-native'; -import PropTypes from 'prop-types'; - -import Markdown from '@components/markdown'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -export default class AttachmentFields extends PureComponent { - static propTypes = { - baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - blockStyles: PropTypes.object.isRequired, - fields: PropTypes.array, - metadata: PropTypes.object, - onPermalinkPress: PropTypes.func, - textStyles: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - }; - - render() { - const { - baseTextStyle, - blockStyles, - fields, - metadata, - onPermalinkPress, - textStyles, - theme, - } = this.props; - - if (!fields?.length) { - return null; - } - - const style = getStyleSheet(theme); - const fieldTables = []; - - let fieldInfos = []; - let rowPos = 0; - let lastWasLong = false; - let nrTables = 0; - - fields.forEach((field, i) => { - if (rowPos === 2 || !(field.short === true) || lastWasLong) { - fieldTables.push( - - {fieldInfos} - , - ); - fieldInfos = []; - rowPos = 0; - nrTables += 1; - lastWasLong = false; - } - - fieldInfos.push( - - {Boolean(field.title) && ( - - - - {field.title} - - - - )} - - - - , - ); - - rowPos += 1; - lastWasLong = !(field.short === true); - }); - - if (fieldInfos.length > 0) { // Flush last fields - fieldTables.push( - - {fieldInfos} - , - ); - } - - return ( - - {fieldTables} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - field: { - alignSelf: 'stretch', - flexDirection: 'row', - }, - flex: { - flex: 1, - }, - headingContainer: { - alignSelf: 'stretch', - flexDirection: 'row', - marginBottom: 5, - marginTop: 10, - }, - heading: { - color: theme.centerChannelColor, - fontWeight: '600', - }, - table: { - flex: 1, - flexDirection: 'row', - }, - }; -}); diff --git a/app/components/message_attachments/attachment_fields.tsx b/app/components/message_attachments/attachment_fields.tsx new file mode 100644 index 000000000..46ce6b94a --- /dev/null +++ b/app/components/message_attachments/attachment_fields.tsx @@ -0,0 +1,145 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native'; + +import Markdown from '@components/markdown'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +import {Theme} from '@mm-redux/types/preferences'; +import {MessageAttachmentField} from '@mm-redux/types/message_attachments'; +import {PostMetadata} from '@mm-redux/types/posts'; + +type Props = { + baseTextStyle: StyleProp, + blockStyles?: StyleProp[], + fields?: MessageAttachmentField[], + metadata?: PostMetadata, + onPermalinkPress?: () => void, + textStyles?: StyleProp[], + theme: Theme, +} + +export default function AttachmentFields(props: Props) { + const { + baseTextStyle, + blockStyles, + fields, + metadata, + onPermalinkPress, + textStyles, + theme, + } = props; + + if (!fields?.length) { + return null; + } + + const style = getStyleSheet(theme); + const fieldTables = []; + + let fieldInfos = [] as React.ReactNode[]; + let rowPos = 0; + let lastWasLong = false; + let nrTables = 0; + + fields.forEach((field, i) => { + if (rowPos === 2 || !(field.short === true) || lastWasLong) { + fieldTables.push( + + {fieldInfos} + , + ); + fieldInfos = []; + rowPos = 0; + nrTables += 1; + lastWasLong = false; + } + + fieldInfos.push( + + {Boolean(field.title) && ( + + + + {field.title} + + + + )} + + + + , + ); + + rowPos += 1; + lastWasLong = !(field.short === true); + }); + + if (fieldInfos.length > 0) { // Flush last fields + fieldTables.push( + + {fieldInfos} + , + ); + } + + return ( + + {fieldTables} + + ); +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + field: { + alignSelf: 'stretch', + flexDirection: 'row', + }, + flex: { + flex: 1, + }, + headingContainer: { + alignSelf: 'stretch', + flexDirection: 'row', + marginBottom: 5, + marginTop: 10, + }, + heading: { + color: theme.centerChannelColor, + fontWeight: '600', + }, + table: { + flex: 1, + flexDirection: 'row', + }, + }; +}); diff --git a/app/components/message_attachments/attachment_footer.js b/app/components/message_attachments/attachment_footer.js deleted file mode 100644 index 9e0a140b1..000000000 --- a/app/components/message_attachments/attachment_footer.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import {Text, View, Platform} from 'react-native'; -import FastImage from 'react-native-fast-image'; -import PropTypes from 'prop-types'; -import truncate from 'lodash/truncate'; - -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment'; - -export default class AttachmentFooter extends PureComponent { - static propTypes = { - text: PropTypes.string, - icon: PropTypes.string, - theme: PropTypes.object.isRequired, - }; - - render() { - const { - text, - icon, - theme, - } = this.props; - - if (!text) { - return null; - } - - const style = getStyleSheet(theme); - - return ( - - {Boolean(icon) && - - } - - {truncate(text, {length: MAX_ATTACHMENT_FOOTER_LENGTH, omission: '…'})} - - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - flexDirection: 'row', - marginTop: 5, - }, - icon: { - height: 12, - width: 12, - marginRight: 5, - ...Platform.select({ - ios: { - marginTop: 1, - }, - android: { - marginTop: 2, - }, - }), - }, - text: { - color: changeOpacity(theme.centerChannelColor, 0.5), - fontSize: 11, - }, - }; -}); diff --git a/app/components/message_attachments/attachment_footer.test.js b/app/components/message_attachments/attachment_footer.test.tsx similarity index 79% rename from app/components/message_attachments/attachment_footer.test.js rename to app/components/message_attachments/attachment_footer.test.tsx index 1bbf8cd9e..2d697404c 100644 --- a/app/components/message_attachments/attachment_footer.test.js +++ b/app/components/message_attachments/attachment_footer.test.tsx @@ -4,7 +4,6 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment'; import AttachmentFooter from './attachment_footer'; import Preferences from '@mm-redux/constants/preferences'; @@ -51,14 +50,4 @@ describe('AttachmentFooter', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - - test('it matches snapshot when the footer is longer than the maximum length', () => { - const props = { - ...baseProps, - text: 'a'.repeat(MAX_ATTACHMENT_FOOTER_LENGTH + 1), - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); }); diff --git a/app/components/message_attachments/attachment_footer.tsx b/app/components/message_attachments/attachment_footer.tsx new file mode 100644 index 000000000..457803cd0 --- /dev/null +++ b/app/components/message_attachments/attachment_footer.tsx @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text, View, Platform} from 'react-native'; +import FastImage from 'react-native-fast-image'; + +import {Theme} from '@mm-redux/types/preferences'; + +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +type Props = { + text?: string; + icon?: string; + theme: Theme; +} + +export default function AttachmentFooter(props: Props) { + const { + text, + icon, + theme, + } = props; + + if (!text) { + return null; + } + + const style = getStyleSheet(theme); + + return ( + + {Boolean(icon) && + + } + + {text} + + + ); +} + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + flex: 1, + flexDirection: 'row', + marginTop: 5, + }, + icon: { + height: 12, + width: 12, + marginRight: 5, + ...Platform.select({ + ios: { + marginTop: 1, + }, + android: { + marginTop: 2, + }, + }), + }, + text: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 11, + }, + }; +}); diff --git a/app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap b/app/components/message_attachments/attachment_image/__snapshots__/index.test.tsx.snap similarity index 100% rename from app/components/message_attachments/attachment_image/__snapshots__/index.test.js.snap rename to app/components/message_attachments/attachment_image/__snapshots__/index.test.tsx.snap diff --git a/app/components/message_attachments/attachment_image/index.test.js b/app/components/message_attachments/attachment_image/index.test.tsx similarity index 77% rename from app/components/message_attachments/attachment_image/index.test.js rename to app/components/message_attachments/attachment_image/index.test.tsx index e34fdf714..bbaff02dd 100644 --- a/app/components/message_attachments/attachment_image/index.test.js +++ b/app/components/message_attachments/attachment_image/index.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import Preferences from '@mm-redux/constants/preferences'; -import AttachmentImage from './index'; +import AttachmentImage, {State, Props} from './index'; describe('AttachmentImage', () => { const baseProps = { @@ -15,7 +15,7 @@ describe('AttachmentImage', () => { imageMetadata: {width: 32, height: 32}, imageUrl: 'https://images.com/image.png', theme: Preferences.THEMES.default, - }; + } as Props; test('it matches snapshot', () => { const wrapper = shallow(); @@ -23,7 +23,7 @@ describe('AttachmentImage', () => { }); test('it sets state based on props', () => { - const wrapper = shallow(); + const wrapper = shallow(); const state = wrapper.state(); expect(state.hasImage).toBe(true); @@ -32,8 +32,11 @@ describe('AttachmentImage', () => { }); test('it does not render image if no imageUrl is provided', () => { - const props = {...baseProps, imageUrl: null, imageMetadata: null}; - const wrapper = shallow(); + const props = {...baseProps}; + delete props.imageUrl; + delete props.imageMetadata; + + const wrapper = shallow(); const state = wrapper.state(); expect(state.hasImage).toBe(false); @@ -41,7 +44,7 @@ describe('AttachmentImage', () => { }); test('it updates image when imageUrl prop changes', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setProps({ imageUrl: 'https://someothersite.com/picture.png', @@ -58,7 +61,7 @@ describe('AttachmentImage', () => { }); test('it does not update image when an unrelated prop changes', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.setProps({ theme: {...Preferences.THEMES.default}, diff --git a/app/components/message_attachments/attachment_image/index.js b/app/components/message_attachments/attachment_image/index.tsx similarity index 77% rename from app/components/message_attachments/attachment_image/index.js rename to app/components/message_attachments/attachment_image/index.tsx index 0f5b1e38f..af00a4c25 100644 --- a/app/components/message_attachments/attachment_image/index.js +++ b/app/components/message_attachments/attachment_image/index.tsx @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; import {View} from 'react-native'; import ProgressiveImage from '@components/progressive_image'; @@ -11,20 +10,37 @@ import {generateId} from '@utils/file'; import {isGifTooLarge, openGalleryAtIndex, calculateDimensions} from '@utils/images'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {Theme} from '@mm-redux/types/preferences'; +import {PostImage} from '@mm-redux/types/posts'; +import {FileInfo} from '@mm-redux/types/files'; + const VIEWPORT_IMAGE_OFFSET = 100; const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10; -export default class AttachmentImage extends PureComponent { - static propTypes = { - deviceHeight: PropTypes.number.isRequired, - deviceWidth: PropTypes.number.isRequired, - imageMetadata: PropTypes.object, - imageUrl: PropTypes.string, - postId: PropTypes.string, - theme: PropTypes.object.isRequired, - }; +export type Props = { + deviceHeight: number; + deviceWidth: number; + imageMetadata?: PostImage; + imageUrl?: string; + postId?: string; + theme: Theme; +} - constructor(props) { +export type State = { + hasImage: boolean; + imageUri: string | null; + originalHeight?: number; + originalWidth?: number; + height?: number; + width?: number; +} + +export default class AttachmentImage extends PureComponent { + private fileId: string; + private mounted = false; + private maxImageWidth = 0; + + constructor(props: Props) { super(props); this.fileId = generateId(); @@ -48,7 +64,7 @@ export default class AttachmentImage extends PureComponent { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) { this.setImageUrl(this.props.imageUrl); } @@ -62,6 +78,16 @@ export default class AttachmentImage extends PureComponent { originalWidth, } = this.state; + if (!imageUrl) { + return { + id: this.fileId, + post_id: postId, + uri, + width: originalWidth, + height: originalHeight, + } as FileInfo; + } + let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?')); const extension = filename.split('.').pop(); @@ -70,7 +96,7 @@ export default class AttachmentImage extends PureComponent { filename = `${filename}${ext}`; } - return { + const out = { id: this.fileId, name: filename, extension, @@ -79,7 +105,8 @@ export default class AttachmentImage extends PureComponent { uri, width: originalWidth, height: originalHeight, - }; + } as FileInfo; + return out; } handlePreviewImage = () => { @@ -87,7 +114,7 @@ export default class AttachmentImage extends PureComponent { openGalleryAtIndex(0, files); }; - setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => { + setImageDimensions = (imageUri: string | null, dimensions: {width?: number; height?: number;}, originalWidth: number, originalHeight: number) => { if (this.mounted) { this.setState({ ...dimensions, @@ -98,12 +125,12 @@ export default class AttachmentImage extends PureComponent { } }; - setImageDimensionsFromMeta = (imageUri, imageMetadata) => { + setImageDimensionsFromMeta = (imageUri: string | null, imageMetadata: PostImage) => { const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth); this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height); }; - setImageUrl = (imageURL) => { + setImageUrl = (imageURL: string) => { const {imageMetadata} = this.props; if (imageMetadata) { @@ -158,7 +185,7 @@ export default class AttachmentImage extends PureComponent { } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { container: { marginTop: 5, diff --git a/app/components/message_attachments/attachment_pretext.js b/app/components/message_attachments/attachment_pretext.js deleted file mode 100644 index 6bc7cfec8..000000000 --- a/app/components/message_attachments/attachment_pretext.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import {StyleSheet, View} from 'react-native'; -import PropTypes from 'prop-types'; - -import Markdown from '@components/markdown'; - -export default class AttachmentPreText extends PureComponent { - static propTypes = { - baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - blockStyles: PropTypes.object.isRequired, - metadata: PropTypes.object, - onPermalinkPress: PropTypes.func, - textStyles: PropTypes.object.isRequired, - value: PropTypes.string, - }; - - render() { - const { - baseTextStyle, - blockStyles, - metadata, - onPermalinkPress, - value, - textStyles, - } = this.props; - - if (!value) { - return null; - } - - return ( - - - - ); - } -} - -const style = StyleSheet.create({ - container: { - marginTop: 5, - }, -}); diff --git a/app/components/message_attachments/attachment_pretext.tsx b/app/components/message_attachments/attachment_pretext.tsx new file mode 100644 index 000000000..73313eda6 --- /dev/null +++ b/app/components/message_attachments/attachment_pretext.tsx @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; + +import Markdown from '@components/markdown'; + +import {PostMetadata} from '@mm-redux/types/posts'; + +type Props = { + baseTextStyle: StyleProp; + blockStyles?: StyleProp[]; + metadata?: PostMetadata; + onPermalinkPress?: () => void; + textStyles?: StyleProp[]; + value?: string; +} +export default function AttachmentPreText(props: Props) { + const { + baseTextStyle, + blockStyles, + metadata, + onPermalinkPress, + value, + textStyles, + } = props; + + if (!value) { + return null; + } + + return ( + + + + ); +} + +const style = StyleSheet.create({ + container: { + marginTop: 5, + }, +}); diff --git a/app/components/message_attachments/attachment_text.js b/app/components/message_attachments/attachment_text.tsx similarity index 67% rename from app/components/message_attachments/attachment_text.js rename to app/components/message_attachments/attachment_text.tsx index 9ce69df86..8ea46b61d 100644 --- a/app/components/message_attachments/attachment_text.js +++ b/app/components/message_attachments/attachment_text.tsx @@ -2,30 +2,42 @@ // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; -import {ScrollView, StyleSheet, View} from 'react-native'; -import PropTypes from 'prop-types'; +import {LayoutChangeEvent, ScrollView, StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; import Markdown from '@components/markdown'; import ShowMoreButton from '@components/show_more_button'; +import {PostMetadata} from '@mm-redux/types/posts'; +import {Theme} from '@mm-redux/types/preferences'; + const SHOW_MORE_HEIGHT = 60; -export default class AttachmentText extends PureComponent { - static propTypes = { - baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - blockStyles: PropTypes.object.isRequired, - deviceHeight: PropTypes.number.isRequired, - hasThumbnail: PropTypes.bool, - metadata: PropTypes.object, - onPermalinkPress: PropTypes.func, - textStyles: PropTypes.object.isRequired, - theme: PropTypes.object, - value: PropTypes.string, - }; +type Props = { + baseTextStyle: StyleProp, + blockStyles?: StyleProp[], + deviceHeight: number, + hasThumbnail?: boolean, + metadata?: PostMetadata, + onPermalinkPress?: () => void, + textStyles?: StyleProp[], + theme?: Theme, + value?: string, +} - static getDerivedStateFromProps(nextProps, prevState) { +type State = { + collapsed: boolean; + isLongText: boolean; + maxHeight: number; +} + +function getMaxHeight(deviceHeight: number) { + return Math.round((deviceHeight * 0.4) + SHOW_MORE_HEIGHT); +} + +export default class AttachmentText extends PureComponent { + static getDerivedStateFromProps(nextProps: Props, prevState: State) { const {deviceHeight} = nextProps; - const maxHeight = Math.round((deviceHeight * 0.4) + SHOW_MORE_HEIGHT); + const maxHeight = getMaxHeight(deviceHeight); if (maxHeight !== prevState.maxHeight) { return { @@ -36,16 +48,18 @@ export default class AttachmentText extends PureComponent { return null; } - constructor(props) { + constructor(props: Props) { super(props); + const maxHeight = getMaxHeight(props.deviceHeight); this.state = { collapsed: true, isLongText: false, + maxHeight, }; } - handleLayout = (event) => { + handleLayout = (event: LayoutChangeEvent) => { const {height} = event.nativeEvent.layout; const {maxHeight} = this.state; @@ -81,7 +95,7 @@ export default class AttachmentText extends PureComponent { return ( - - - ); - } -} - -const style = StyleSheet.create({ - container: { - position: 'absolute', - right: 10, - top: 10, - }, - image: { - height: 45, - width: 45, - }, -}); diff --git a/app/components/message_attachments/attachment_thumbnail.tsx b/app/components/message_attachments/attachment_thumbnail.tsx new file mode 100644 index 000000000..e5ec7ac8a --- /dev/null +++ b/app/components/message_attachments/attachment_thumbnail.tsx @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleSheet, View} from 'react-native'; +import FastImage from 'react-native-fast-image'; + +import {isValidUrl} from '@utils/url'; + +type Props = { + url?: string; +} +export default function AttachmentThumbnail(props: Props) { + const {url: uri} = props; + + if (!isValidUrl(uri)) { + return null; + } + + return ( + + + + ); +} + +const style = StyleSheet.create({ + container: { + position: 'absolute', + right: 10, + top: 10, + }, + image: { + height: 45, + width: 45, + }, +}); diff --git a/app/components/message_attachments/attachment_title.js b/app/components/message_attachments/attachment_title.tsx similarity index 90% rename from app/components/message_attachments/attachment_title.js rename to app/components/message_attachments/attachment_title.tsx index b428de376..b03e1d701 100644 --- a/app/components/message_attachments/attachment_title.js +++ b/app/components/message_attachments/attachment_title.tsx @@ -4,19 +4,19 @@ import React, {PureComponent} from 'react'; import {Alert, Text, View} from 'react-native'; import {intlShape} from 'react-intl'; -import PropTypes from 'prop-types'; import Markdown from '@components/markdown'; import {makeStyleSheetFromTheme} from '@utils/theme'; import {tryOpenURL} from '@utils/url'; -export default class AttachmentTitle extends PureComponent { - static propTypes = { - link: PropTypes.string, - theme: PropTypes.object.isRequired, - value: PropTypes.string, - }; +import {Theme} from '@mm-redux/types/preferences'; +type Props = { + link?: string; + theme: Theme; + value?: string; +} +export default class AttachmentTitle extends PureComponent { static contextTypes = { intl: intlShape.isRequired, }; @@ -93,7 +93,7 @@ export default class AttachmentTitle extends PureComponent { } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { container: { marginTop: 3, diff --git a/app/components/message_attachments/index.js b/app/components/message_attachments/index.js deleted file mode 100644 index be0e80000..000000000 --- a/app/components/message_attachments/index.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; - -import MessageAttachment from './message_attachment'; - -export default class MessageAttachments extends PureComponent { - static propTypes = { - attachments: PropTypes.array.isRequired, - baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - blockStyles: PropTypes.object, - deviceHeight: PropTypes.number.isRequired, - deviceWidth: PropTypes.number.isRequired, - postId: PropTypes.string.isRequired, - metadata: PropTypes.object, - onHashtagPress: PropTypes.func, - onPermalinkPress: PropTypes.func, - theme: PropTypes.object, - textStyles: PropTypes.object, - }; - - render() { - const { - attachments, - baseTextStyle, - blockStyles, - deviceHeight, - deviceWidth, - metadata, - onHashtagPress, - onPermalinkPress, - postId, - theme, - textStyles, - } = this.props; - const content = []; - - attachments.forEach((attachment, i) => { - content.push( - , - ); - }); - - return ( - - {content} - - ); - } -} diff --git a/app/components/message_attachments/index.tsx b/app/components/message_attachments/index.tsx new file mode 100644 index 000000000..2b29ea62a --- /dev/null +++ b/app/components/message_attachments/index.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; + +import {MessageAttachment as MessageAttachmentType} from '@mm-redux/types/message_attachments'; +import {PostMetadata} from '@mm-redux/types/posts'; +import {Theme} from '@mm-redux/types/preferences'; + +import MessageAttachment from './message_attachment'; + +type Props = { + attachments: MessageAttachmentType[], + baseTextStyle?: StyleProp, + blockStyles?: StyleProp[], + deviceHeight: number, + deviceWidth: number, + postId: string, + metadata?: PostMetadata, + onPermalinkPress?: () => void, + theme: Theme, + textStyles?: StyleProp[], +} + +export default function MessageAttachments(props: Props) { + const { + attachments, + baseTextStyle, + blockStyles, + deviceHeight, + deviceWidth, + metadata, + onPermalinkPress, + postId, + theme, + textStyles, + } = props; + const content = [] as React.ReactNode[]; + + attachments.forEach((attachment, i) => { + content.push( + , + ); + }); + + return ( + + {content} + + ); +} diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js deleted file mode 100644 index d87a5b13f..000000000 --- a/app/components/message_attachments/message_attachment.js +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {View} from 'react-native'; - -import {getStatusColors} from '@utils/message_attachment_colors'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; - -import AttachmentActions from './attachment_actions'; -import AttachmentAuthor from './attachment_author'; -import AttachmentFields from './attachment_fields'; -import AttachmentImage from './attachment_image'; -import AttachmentPreText from './attachment_pretext'; -import AttachmentText from './attachment_text'; -import AttachmentThumbnail from './attachment_thumbnail'; -import AttachmentTitle from './attachment_title'; -import AttachmentFooter from './attachment_footer'; - -export default class MessageAttachment extends PureComponent { - static propTypes = { - attachment: PropTypes.object.isRequired, - baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), - blockStyles: PropTypes.object, - deviceHeight: PropTypes.number.isRequired, - deviceWidth: PropTypes.number.isRequired, - metadata: PropTypes.object, - postId: PropTypes.string.isRequired, - onPermalinkPress: PropTypes.func, - theme: PropTypes.object, - textStyles: PropTypes.object, - }; - - render() { - const { - attachment, - baseTextStyle, - blockStyles, - deviceHeight, - deviceWidth, - metadata, - onPermalinkPress, - postId, - textStyles, - theme, - } = this.props; - - const style = getStyleSheet(theme); - const STATUS_COLORS = getStatusColors(theme); - const hasImage = Boolean(metadata?.images?.[attachment.image_url]); - - let borderStyle; - if (attachment.color) { - if (attachment.color[0] === '#') { - borderStyle = {borderLeftColor: attachment.color}; - } else if (STATUS_COLORS.hasOwnProperty(attachment.color)) { - borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]}; - } - } - - return ( - - - - - - - - - - - {hasImage && - - } - - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - borderBottomColor: changeOpacity(theme.centerChannelColor, 0.15), - borderRightColor: changeOpacity(theme.centerChannelColor, 0.15), - borderTopColor: changeOpacity(theme.centerChannelColor, 0.15), - borderBottomWidth: 1, - borderRightWidth: 1, - borderTopWidth: 1, - marginTop: 5, - padding: 12, - }, - border: { - borderLeftColor: changeOpacity(theme.linkColor, 0.6), - borderLeftWidth: 3, - }, - }; -}); diff --git a/app/components/message_attachments/message_attachment.tsx b/app/components/message_attachments/message_attachment.tsx new file mode 100644 index 000000000..e66470653 --- /dev/null +++ b/app/components/message_attachments/message_attachment.tsx @@ -0,0 +1,147 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; + +import {getStatusColors} from '@utils/message_attachment_colors'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import {MessageAttachment as MessageAttachmentType} from '@mm-redux/types/message_attachments'; +import {PostMetadata} from '@mm-redux/types/posts'; +import {Theme} from '@mm-redux/types/preferences'; + +import AttachmentActions from './attachment_actions'; +import AttachmentAuthor from './attachment_author'; +import AttachmentFields from './attachment_fields'; +import AttachmentImage from './attachment_image'; +import AttachmentPreText from './attachment_pretext'; +import AttachmentText from './attachment_text'; +import AttachmentThumbnail from './attachment_thumbnail'; +import AttachmentTitle from './attachment_title'; +import AttachmentFooter from './attachment_footer'; + +type Props = { + attachment: MessageAttachmentType, + baseTextStyle?: StyleProp, + blockStyles?: StyleProp[], + deviceHeight: number, + deviceWidth: number, + postId: string, + metadata?: PostMetadata, + onPermalinkPress?: () => void, + theme: Theme, + textStyles?: StyleProp[], +} +export default function MessageAttachment(props: Props) { + const { + attachment, + baseTextStyle, + blockStyles, + deviceHeight, + deviceWidth, + metadata, + onPermalinkPress, + postId, + textStyles, + theme, + } = props; + + const style = getStyleSheet(theme); + const STATUS_COLORS = getStatusColors(theme); + const hasImage = Boolean(metadata?.images?.[attachment.image_url]); + + let borderStyle; + if (attachment.color) { + if (attachment.color[0] === '#') { + borderStyle = {borderLeftColor: attachment.color}; + } else if (STATUS_COLORS.hasOwnProperty(attachment.color)) { + borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]}; + } + } + + return ( + + + + + + + + + + + {hasImage && + + } + + + ); +} + +const getStyleSheet = makeStyleSheetFromTheme((theme:Theme) => { + return { + container: { + borderBottomColor: changeOpacity(theme.centerChannelColor, 0.15), + borderRightColor: changeOpacity(theme.centerChannelColor, 0.15), + borderTopColor: changeOpacity(theme.centerChannelColor, 0.15), + borderBottomWidth: 1, + borderRightWidth: 1, + borderTopWidth: 1, + marginTop: 5, + padding: 12, + }, + border: { + borderLeftColor: changeOpacity(theme.linkColor, 0.6), + borderLeftWidth: 3, + }, + }; +}); diff --git a/app/components/post_attachment_image/index.js b/app/components/post_attachment_image/index.js index 3d998b713..72c71dec3 100644 --- a/app/components/post_attachment_image/index.js +++ b/app/components/post_attachment_image/index.js @@ -5,9 +5,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import {StyleSheet, View} from 'react-native'; -import ProgressiveImage from 'app/components/progressive_image'; -import TouchableWithFeedback from 'app/components/touchable_with_feedback'; -import {isGifTooLarge} from 'app/utils/images'; +import ProgressiveImage from '@components/progressive_image'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {isGifTooLarge} from '@utils/images'; export default class PostAttachmentImage extends React.PureComponent { static propTypes = { diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index ab6bd87ed..af9552687 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -355,7 +355,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort { if (attachments && attachments.length) { if (!MessageAttachments) { - MessageAttachments = require('app/components/message_attachments').default; + MessageAttachments = require('@components/message_attachments').default; } return ( diff --git a/app/components/reaction_picker/reaction_button.js b/app/components/reaction_picker/reaction_button.js index 2b1e6c8b4..fd265f8c1 100644 --- a/app/components/reaction_picker/reaction_button.js +++ b/app/components/reaction_picker/reaction_button.js @@ -8,13 +8,13 @@ import { TouchableWithoutFeedback, } from 'react-native'; -import Emoji from 'app/components/emoji'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {hapticFeedback} from 'app/utils/general'; +import Emoji from '@components/emoji'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {hapticFeedback} from '@utils/general'; import { LARGE_CONTAINER_SIZE, LARGE_ICON_SIZE, -} from 'app/constants/reaction_picker'; +} from '@constants/reaction_picker'; export default class ReactionButton extends PureComponent { static propTypes = { diff --git a/app/components/reactions/reaction.js b/app/components/reactions/reaction.js index 9b06e4c0e..211de7447 100644 --- a/app/components/reactions/reaction.js +++ b/app/components/reactions/reaction.js @@ -8,9 +8,9 @@ import { Text, } from 'react-native'; -import Emoji from 'app/components/emoji'; -import TouchableWithFeedback from 'app/components/touchable_with_feedback'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import Emoji from '@components/emoji'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; export default class Reaction extends PureComponent { static propTypes = { diff --git a/app/constants/attachment.js b/app/constants/attachment.js index 80f945ae4..d9ac02c83 100644 --- a/app/constants/attachment.js +++ b/app/constants/attachment.js @@ -2,4 +2,3 @@ // See LICENSE.txt for license information. export const ATTACHMENT_DOWNLOAD = 'attachment_download'; -export const MAX_ATTACHMENT_FOOTER_LENGTH = 300; diff --git a/app/mm-redux/types/message_attachments.ts b/app/mm-redux/types/message_attachments.ts new file mode 100644 index 000000000..6bf723f09 --- /dev/null +++ b/app/mm-redux/types/message_attachments.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {PostAction} from './integration_actions'; + +export type MessageAttachment = { + id: number; + fallback: string; + color: string; + pretext: string; + author_name: string; + author_link: string; + author_icon: string; + title: string; + title_link: string; + text: string; + fields: MessageAttachmentField[]; + image_url: string; + thumb_url: string; + footer: string; + footer_icon: string; + timestamp: number | string; + actions?: PostAction[]; +}; + +export type MessageAttachmentField = { + title: string; + value: any; + short: boolean; +} diff --git a/app/screens/gallery/gallery_viewer.tsx b/app/screens/gallery/gallery_viewer.tsx index 552cd1747..36e73c57f 100644 --- a/app/screens/gallery/gallery_viewer.tsx +++ b/app/screens/gallery/gallery_viewer.tsx @@ -95,7 +95,7 @@ const GalleryViewer = (props: GalleryProps) => { const imgHeight = currentFile.height || height; const imgWidth = currentFile.width || width; const calculatedDimensions = calculateDimensions(imgHeight, imgWidth, width, height); - const imgCanvas = vec.create(calculatedDimensions.width, calculatedDimensions.height); + const imgCanvas = vec.create(calculatedDimensions.width || 0, calculatedDimensions.height || 0); const minImgVec = vec.min(vec.multiply(-0.5, imgCanvas, sub(scale, 1)), 0); const maxImgVec = vec.max(vec.minus(minImgVec), 0); diff --git a/app/screens/reaction_list/reaction_header_item.js b/app/screens/reaction_list/reaction_header_item.js index c67a281f1..1fad3fb26 100644 --- a/app/screens/reaction_list/reaction_header_item.js +++ b/app/screens/reaction_list/reaction_header_item.js @@ -8,12 +8,12 @@ import { TouchableOpacity, } from 'react-native'; -import Emoji from 'app/components/emoji'; -import {makeStyleSheetFromTheme} from 'app/utils/theme'; +import Emoji from '@components/emoji'; +import {makeStyleSheetFromTheme} from '@utils/theme'; -import FormattedText from 'app/components/formatted_text'; +import FormattedText from '@components/formatted_text'; -import {ALL_EMOJIS} from 'app/constants/emoji'; +import {ALL_EMOJIS} from '@constants/emoji'; export default class ReactionHeaderItem extends PureComponent { static propTypes = { diff --git a/app/utils/images.test.js b/app/utils/images.test.ts similarity index 93% rename from app/utils/images.test.js rename to app/utils/images.test.ts index e7eb743c7..26fb4a015 100644 --- a/app/utils/images.test.js +++ b/app/utils/images.test.ts @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {calculateDimensions, isGifTooLarge} from 'app/utils/images'; +import {calculateDimensions, isGifTooLarge} from '@utils/images'; import { IMAGE_MAX_HEIGHT, IMAGE_MIN_DIMENSION, -} from 'app/constants/image'; +} from '@constants/image'; const PORTRAIT_VIEWPORT = 315; @@ -13,18 +13,18 @@ describe('Images calculateDimensions', () => { it('image with falsy height should return null height and width', () => { const falsyHeights = [0, null, undefined, NaN, '', false]; falsyHeights.forEach((falsyHeight) => { - const {height, width} = calculateDimensions(falsyHeight, 20, PORTRAIT_VIEWPORT); - expect(height).toEqual(null); - expect(width).toEqual(null); + const {height, width} = calculateDimensions(falsyHeight as number, 20, PORTRAIT_VIEWPORT); + expect(height).toEqual(undefined); + expect(width).toEqual(undefined); }); }); it('image with falsy width should return null height and width', () => { const falsyWidths = [0, null, undefined, NaN, '', false]; falsyWidths.forEach((falsyWidth) => { - const {height, width} = calculateDimensions(20, falsyWidth, PORTRAIT_VIEWPORT); - expect(height).toEqual(null); - expect(width).toEqual(null); + const {height, width} = calculateDimensions(20, falsyWidth as number, PORTRAIT_VIEWPORT); + expect(height).toEqual(undefined); + expect(width).toEqual(undefined); }); }); @@ -93,7 +93,6 @@ describe('isGifTooLarge', () => { const testCases = [ { name: 'no image metadata', - imageMetadata: null, expected: false, }, { diff --git a/app/utils/images.js b/app/utils/images.ts similarity index 79% rename from app/utils/images.js rename to app/utils/images.ts index a9d4f720b..d0d0b5ac3 100644 --- a/app/utils/images.js +++ b/app/utils/images.ts @@ -12,14 +12,16 @@ import { VIEWPORT_IMAGE_OFFSET, VIEWPORT_IMAGE_REPLY_OFFSET, } from '@constants/image'; -import {isImage} from './file'; -export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHeight = 0) => { +import {PostImage} from '@mm-redux/types/posts'; +import {FileInfo} from '@mm-redux/types/files'; + +import {isImage} from './file'; +import {Options, SharedElementTransition, StackAnimationOptions, ViewAnimationOptions} from 'react-native-navigation'; + +export const calculateDimensions = (height?: number, width?: number, viewPortWidth = 0, viewPortHeight = 0) => { if (!height || !width) { - return { - height: null, - width: null, - }; + return {}; } const ratio = height / width; @@ -61,7 +63,7 @@ export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHe }; }; -export function getViewPortWidth(isReplyPost, permanentSidebar = false) { +export function getViewPortWidth(isReplyPost: boolean, permanentSidebar = false) { const {width, height} = Dimensions.get('window'); let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET; @@ -76,7 +78,7 @@ export function getViewPortWidth(isReplyPost, permanentSidebar = false) { return portraitPostWidth; } -export function openGalleryAtIndex(index, files) { +export function openGalleryAtIndex(index: number, files: FileInfo[]) { Keyboard.dismiss(); requestAnimationFrame(() => { const screen = 'Gallery'; @@ -85,9 +87,10 @@ export function openGalleryAtIndex(index, files) { files, }; const windowHeight = Dimensions.get('window').height; - const sharedElementTransitions = []; - const contentPush = {}; - const contentPop = {}; + const sharedElementTransitions: SharedElementTransition[] = []; + + const contentPush = {} as ViewAnimationOptions; + const contentPop = {} as ViewAnimationOptions; const file = files[index]; if (isImage(file)) { @@ -95,14 +98,14 @@ export function openGalleryAtIndex(index, files) { fromId: `image-${file.id}`, toId: `gallery-${file.id}`, duration: 300, - interpolation: {type: 'accelerateDecelerate', factor: 9}, + interpolation: {type: 'accelerateDecelerate'}, }); } else { contentPush.y = { from: windowHeight, to: 0, duration: 300, - interpolation: {mode: 'decelerate'}, + interpolation: {type: 'decelerate'}, }; if (Platform.OS === 'ios') { @@ -125,7 +128,7 @@ export function openGalleryAtIndex(index, files) { } } - const options = { + const options: Options = { layout: { backgroundColor: '#000', componentBackgroundColor: '#000', @@ -146,9 +149,9 @@ export function openGalleryAtIndex(index, files) { }; if (Object.keys(contentPush).length) { - options.animations.push = { - ...options.animations.push, - ...Platform.select({ + options.animations!.push = { + ...options.animations!.push, + ...Platform.select({ android: contentPush, ios: { content: contentPush, @@ -158,7 +161,7 @@ export function openGalleryAtIndex(index, files) { } if (Object.keys(contentPop).length) { - options.animations.pop = Platform.select({ + options.animations!.pop = Platform.select({ android: contentPop, ios: { content: contentPop, @@ -172,7 +175,7 @@ export function openGalleryAtIndex(index, files) { // isGifTooLarge returns true if we think that the GIF may cause the device to run out of memory when rendered // based on the image's dimensions and frame count. -export function isGifTooLarge(imageMetadata) { +export function isGifTooLarge(imageMetadata?: PostImage) { if (imageMetadata?.format !== 'gif') { // Not a gif or from an older server that doesn't count frames return false; @@ -181,5 +184,5 @@ export function isGifTooLarge(imageMetadata) { const {frame_count: frameCount, height, width} = imageMetadata; // Try to estimate the in-memory size of the gif to prevent the device out of memory - return width * height * frameCount > MAX_GIF_SIZE; + return width * height * (frameCount || 1) > MAX_GIF_SIZE; } diff --git a/app/utils/message_attachment_colors.js b/app/utils/message_attachment_colors.ts similarity index 64% rename from app/utils/message_attachment_colors.js rename to app/utils/message_attachment_colors.ts index 0e081e06d..c7c4710d6 100644 --- a/app/utils/message_attachment_colors.js +++ b/app/utils/message_attachment_colors.ts @@ -1,7 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -export function getStatusColors(theme) { +import {Theme} from '@mm-redux/types/preferences'; +import {Dictionary} from '@mm-redux/types/utilities'; + +export function getStatusColors(theme: Theme) { return { good: '#00c100', warning: '#dede01', @@ -9,5 +12,5 @@ export function getStatusColors(theme) { default: theme.centerChannelColor, primary: theme.buttonBg, success: theme.onlineIndicator, - }; + } as Dictionary; }