From 9af108026e190f39f726c4395536e2a3ac6e25ff Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 28 Apr 2020 11:23:39 -0400 Subject: [PATCH] MM-24285 Use FastImage instead of Image (#4218) * Enable ESLint no-unused-vars * Use FastImage instead of Image * Update fast-image patch to support multiple cookies * Fix ESLint errors * Have jest run timers for post_textbox tests * Feedback review * Update snapshots --- .eslintrc.json | 2 +- app/actions/views/login.test.js | 1 - app/actions/views/root.js | 2 +- app/actions/views/user.js | 5 +- app/actions/websocket.test.js | 4 +- .../file_attachment_list.test.js.snap | 12 +- .../file_attachment_image.js | 37 +- .../file_attachment_list.js | 78 +-- .../file_upload_item/file_upload_item.js | 32 +- app/components/image_viewport.js | 55 ++ .../markdown/markdown_image/index.js | 3 - .../markdown/markdown_image/markdown_image.js | 91 +--- app/components/markdown/transform.test.js | 2 +- .../attachment_footer.test.js.snap | 4 +- .../message_attachments/attachment_author.js | 13 +- .../message_attachments/attachment_footer.js | 13 +- .../post_attachment_opengraph.test.js.snap | 4 +- .../post_attachment_opengraph.js | 25 +- .../post_attachment_opengraph.test.js | 6 +- .../post_body_additional_content.js | 473 ++++++++---------- .../post_profile_picture.js | 5 +- .../post_textbox/post_textbox.test.js | 2 + .../profile_picture/profile_picture.js | 13 +- .../progressive_image.test.js.snap | 122 ++++- .../progressive_image/progressive_image.js | 188 +++---- .../progressive_image.test.js | 94 ++-- app/components/team_icon/team_icon.js | 20 +- app/components/video_controls.js | 20 +- app/constants/device.js | 1 + app/constants/image.js | 3 + app/constants/index.js | 2 + app/init/global_event_handler.js | 2 - app/mattermost.js | 1 - app/mm-redux/actions/bots.test.js | 1 - app/mm-redux/actions/channels.test.js | 2 +- app/mm-redux/actions/files.test.js | 1 - app/mm-redux/actions/groups.test.js | 2 +- app/mm-redux/actions/helpers.test.js | 1 - app/mm-redux/actions/preferences.test.js | 2 +- app/mm-redux/actions/teams.test.js | 2 +- app/mm-redux/client/client4.ts | 1 - .../entities/channel_categories.test.js | 2 +- .../reducers/entities/channels.test.js | 2 +- app/mm-redux/reducers/entities/search.test.js | 6 +- .../entities/channel_categories.test.js | 1 - .../selectors/entities/channels.test.js | 2 - app/mm-redux/utils/user_utils.test.js | 13 - app/reducers/views/post.js | 1 - app/screens/edit_post/edit_post.js | 2 - .../__snapshots__/image_preview.test.js.snap | 7 +- app/screens/image_preview/image_preview.js | 12 +- app/screens/notification/index.js | 6 +- app/screens/notification/notification.js | 8 +- app/screens/table_image/table_image.js | 4 +- app/store/middlewares/helpers.js | 2 - app/store/middlewares/message_retention.js | 1 - app/store/middlewares/sentry.js | 10 +- app/store/utils.js | 2 - app/utils/images.js | 27 +- app/utils/push_notifications.js | 2 +- babel.config.js | 1 + index.js | 1 + ios/Podfile.lock | 2 +- patches/react-native-fast-image+8.1.5.patch | 13 +- share_extension/android/actions/index.js | 1 - .../android/extension_post/extension_post.js | 4 +- test/setup.js | 73 +-- 67 files changed, 739 insertions(+), 813 deletions(-) create mode 100644 app/components/image_viewport.js diff --git a/.eslintrc.json b/.eslintrc.json index 9c0bc2547..4dc53db69 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -30,7 +30,7 @@ "@typescript-eslint/camelcase": 0, "@typescript-eslint/no-undefined": 0, "@typescript-eslint/no-non-null-assertion": 0, - "@typescript-eslint/no-unused-vars": 0, + "@typescript-eslint/no-unused-vars": 2, "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-var-requires": 0, diff --git a/app/actions/views/login.test.js b/app/actions/views/login.test.js index dd2a6028f..e8bcf278c 100644 --- a/app/actions/views/login.test.js +++ b/app/actions/views/login.test.js @@ -5,7 +5,6 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import {Client4} from '@mm-redux/client'; -import * as GeneralActions from '@mm-redux/actions/general'; import {handleSuccessfulLogin} from 'app/actions/views/login'; diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 188469b61..8825fdb33 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions'; import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types'; import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels'; -import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from '@mm-redux/actions/general'; +import {getDataRetentionPolicy} from '@mm-redux/actions/general'; import {receivedNewPost} from '@mm-redux/actions/posts'; import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams'; import {Client4} from '@mm-redux/client'; diff --git a/app/actions/views/user.js b/app/actions/views/user.js index 3067977b0..233ca69a2 100644 --- a/app/actions/views/user.js +++ b/app/actions/views/user.js @@ -212,10 +212,7 @@ export function logout(skipServerLogout = false) { } export function forceLogoutIfNecessary(error) { - return async (dispatch, getState) => { - const state = getState(); - const currentUserId = getCurrentUserId(state); - + return async (dispatch) => { if (error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) { dispatch(logout(true)); return true; diff --git a/app/actions/websocket.test.js b/app/actions/websocket.test.js index 55f331bff..01ee420a2 100644 --- a/app/actions/websocket.test.js +++ b/app/actions/websocket.test.js @@ -11,9 +11,7 @@ import configureMockStore from 'redux-mock-store'; import {ChannelTypes, GeneralTypes, RoleTypes, TeamTypes, UserTypes} from '@mm-redux/action_types'; import * as ChannelActions from '@mm-redux/actions/channels'; import * as PostActions from '@mm-redux/actions/posts'; -import * as PreferenceActions from '@mm-redux/actions/preferences'; import * as TeamActions from '@mm-redux/actions/teams'; -import * as UserActions from '@mm-redux/actions/users'; import {Client4} from '@mm-redux/client'; import {General, Posts, RequestStatus} from '@mm-redux/constants'; import * as PostSelectors from '@mm-redux/selectors/entities/posts'; @@ -901,7 +899,7 @@ describe('Actions.Websocket doReconnect', () => { }); describe('Actions.Websocket notVisibleUsersActions', () => { - const mockStore = configureMockStore([thunk]); + configureMockStore([thunk]); const channel1 = TestHelper.fakeChannelWithId(''); const channel2 = TestHelper.fakeChannelWithId(''); diff --git a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap index 92df63048..d31af611a 100644 --- a/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap +++ b/app/components/file_attachment_list/__snapshots__/file_attachment_list.test.js.snap @@ -17,7 +17,7 @@ exports[`FileAttachmentList should match snapshot with a single image file 1`] = "marginTop": 5, }, Object { - "width": undefined, + "width": 680, }, ] } @@ -109,7 +109,7 @@ exports[`FileAttachmentList should match snapshot with combination of image and "marginTop": 5, }, Object { - "width": undefined, + "width": 680, }, ] } @@ -332,7 +332,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = ` "marginTop": 5, }, Object { - "width": undefined, + "width": 680, }, ] } @@ -637,7 +637,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file "marginTop": 5, }, Object { - "width": undefined, + "width": 680, }, ] } @@ -1014,7 +1014,7 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = ` "marginTop": 5, }, Object { - "width": undefined, + "width": 680, }, ] } @@ -1248,7 +1248,7 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = ` "marginTop": 5, }, Object { - "width": undefined, + "width": 680, }, ] } diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js index 1a5d2625d..9deccbbed 100644 --- a/app/components/file_attachment_list/file_attachment_image.js +++ b/app/components/file_attachment_list/file_attachment_image.js @@ -4,7 +4,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Animated, View, StyleSheet, } from 'react-native'; @@ -12,11 +11,11 @@ import FastImage from 'react-native-fast-image'; import {Client4} from '@mm-redux/client'; -import ProgressiveImage from 'app/components/progressive_image'; -import {isGif} from 'app/utils/file'; -import {changeOpacity} from 'app/utils/theme'; +import ProgressiveImage from '@components/progressive_image'; +import {isGif} from '@utils/file'; +import {changeOpacity} from '@utils/theme'; -import thumb from 'assets/images/thumb.png'; +import brokenImageIcon from 'assets/images/icons/brokenimage.png'; const SMALL_IMAGE_MAX_HEIGHT = 48; const SMALL_IMAGE_MAX_WIDTH = 48; @@ -57,11 +56,7 @@ export default class FileAttachmentImage extends PureComponent { const {file} = props; if (file && file.id && !file.localPath) { - const headers = { - Authorization: `Bearer ${Client4.getToken()}`, - 'X-CSRF-Token': Client4.csrf, - 'X-Requested-With': 'XMLHttpRequest', - }; + const headers = Client4.getOptions({}).headers; const preloadImages = [ {uri: Client4.getFileThumbnailUrl(file.id), headers}, @@ -76,9 +71,7 @@ export default class FileAttachmentImage extends PureComponent { } this.state = { - opacity: new Animated.Value(0), - requesting: true, - retry: 0, + failed: false, }; } @@ -97,9 +90,17 @@ export default class FileAttachmentImage extends PureComponent { } }; + handleError = () => { + this.setState({failed: true}); + } + imageProps = (file) => { const imageProps = {}; - if (file.localPath) { + const {failed} = this.state; + + if (failed) { + imageProps.defaultSource = brokenImageIcon; + } else if (file.localPath) { imageProps.defaultSource = {uri: file.localPath}; } else if (file.id) { imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id); @@ -134,9 +135,9 @@ export default class FileAttachmentImage extends PureComponent { { return this.filesForGallery.findIndex((file) => file.id === fileId) || 0; }; @@ -147,45 +127,10 @@ export default class FileAttachmentList extends PureComponent { return results; }; - getPortraitPostWidth = () => { - const {isReplyPost} = this.props; - const {width, height} = Dimensions.get('window'); - const permanentSidebar = DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar; - let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET; - - if (permanentSidebar) { - portraitPostWidth -= TABLET_WIDTH; - } - - if (isReplyPost) { - portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET; - } - - return portraitPostWidth; - }; - handleCaptureRef = (ref, idx) => { this.items[idx] = ref; }; - handleDimensions = () => { - if (this.mounted) { - if (DeviceTypes.IS_TABLET) { - mattermostManaged.isRunningInSplitView().then((result) => { - const isSplitView = Boolean(result.isSplitView); - this.setState({isSplitView}); - }); - } - } - }; - - handlePermanentSidebar = async () => { - if (DeviceTypes.IS_TABLET && this.mounted) { - const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS); - this.setState({permanentSidebar: enabled === 'true'}); - } - }; - handlePreviewPress = preventDoubleTap((idx) => { previewImageAtIndex(this.items, idx, this.galleryFiles); }); @@ -199,7 +144,7 @@ export default class FileAttachmentList extends PureComponent { } renderItems = (items, moreImagesCount, includeGutter = false) => { - const {canDownloadFiles, onLongPress, theme} = this.props; + const {canDownloadFiles, isReplyPost, onLongPress, theme} = this.props; const isSingleImage = this.isSingleImage(items); let nonVisibleImagesCount; let container = styles.container; @@ -236,7 +181,7 @@ export default class FileAttachmentList extends PureComponent { theme={theme} isSingleImage={isSingleImage} nonVisibleImagesCount={nonVisibleImagesCount} - wrapperWidth={this.getPortraitPostWidth()} + wrapperWidth={getViewPortWidth(isReplyPost, this.hasPermanentSidebar())} /> ); @@ -248,8 +193,9 @@ export default class FileAttachmentList extends PureComponent { return null; } + const {isReplyPost} = this.props; const visibleImages = images.slice(0, MAX_VISIBLE_ROW_IMAGES); - const {portraitPostWidth} = this.state; + const portraitPostWidth = getViewPortWidth(isReplyPost, this.hasPermanentSidebar()); let nonVisibleImagesCount; if (images.length > MAX_VISIBLE_ROW_IMAGES) { diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.js index d957cb818..2000149f0 100644 --- a/app/components/file_upload_preview/file_upload_item/file_upload_item.js +++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.js @@ -3,21 +3,21 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Text, View} from 'react-native'; +import {StyleSheet, Text, View} from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; import {AnimatedCircularProgress} from 'react-native-circular-progress'; import {Client4} from '@mm-redux/client'; -import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image'; -import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; -import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retry'; -import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove'; +import FileAttachmentImage from '@components/file_attachment_list/file_attachment_image'; +import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon'; +import FileUploadRetry from '@components/file_upload_preview/file_upload_retry'; +import FileUploadRemove from '@components/file_upload_preview/file_upload_remove'; +import {buildFileUploadData, encodeHeaderURIStringToUTF8} from '@utils/file'; +import {emptyFunction} from '@utils/general'; +import ImageCacheManager from '@utils/image_cache_manager'; + import mattermostBucket from 'app/mattermost_bucket'; -import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file'; -import {emptyFunction} from 'app/utils/general'; -import ImageCacheManager from 'app/utils/image_cache_manager'; -import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; export default class FileUploadItem extends PureComponent { static propTypes = { @@ -132,10 +132,8 @@ export default class FileUploadItem extends PureComponent { const fileData = buildFileUploadData(file); const headers = { - Authorization: `Bearer ${Client4.getToken()}`, - 'X-Requested-With': 'XMLHttpRequest', + ...Client4.getOptions({method: 'post'}).headers, 'Content-Type': 'multipart/form-data', - 'X-CSRF-Token': Client4.csrf, }; const fileInfo = { @@ -164,7 +162,6 @@ export default class FileUploadItem extends PureComponent { }; renderProgress = (fill) => { - const styles = getStyleSheet(this.props.theme); const realFill = Number(fill.toFixed(0)); return ( @@ -184,7 +181,6 @@ export default class FileUploadItem extends PureComponent { theme, } = this.props; const {progress} = this.state; - const styles = getStyleSheet(theme); let filePreviewComponent; if (this.isImageType()) { @@ -193,6 +189,7 @@ export default class FileUploadItem extends PureComponent { ); @@ -249,7 +246,7 @@ export default class FileUploadItem extends PureComponent { } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ +const styles = StyleSheet.create({ preview: { paddingTop: 5, marginLeft: 12, @@ -274,10 +271,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ fontWeight: 'bold', }, filePreview: { - borderColor: changeOpacity(theme.centerChannelColor, 0.15), - borderRadius: 4, - borderWidth: 1, width: 56, height: 56, }, -})); +}); diff --git a/app/components/image_viewport.js b/app/components/image_viewport.js new file mode 100644 index 000000000..7a251780e --- /dev/null +++ b/app/components/image_viewport.js @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import AsyncStorage from '@react-native-community/async-storage'; +import {PureComponent} from 'react'; +import {Dimensions} from 'react-native'; + +import {DeviceTypes} from '@constants'; +import EventEmitter from '@mm-redux/utils/event_emitter'; + +import mattermostManaged from 'app/mattermost_managed'; + +export default class ImageViewPort extends PureComponent { + state = {}; + + componentDidMount() { + this.mounted = true; + this.handlePermanentSidebar(); + this.handleDimensions(); + EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar); + Dimensions.addEventListener('change', this.handleDimensions); + } + + componentWillUnmount() { + this.mounted = false; + EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar); + Dimensions.removeEventListener('change', this.handleDimensions); + } + + handleDimensions = () => { + if (this.mounted) { + if (DeviceTypes.IS_TABLET) { + mattermostManaged.isRunningInSplitView().then((result) => { + const isSplitView = Boolean(result.isSplitView); + this.setState({isSplitView}); + }); + } + } + }; + + handlePermanentSidebar = async () => { + if (DeviceTypes.IS_TABLET && this.mounted) { + const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS); + this.setState({permanentSidebar: enabled === 'true'}); + } + }; + + hasPermanentSidebar = () => { + return DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar; + }; + + render() { + return null; + } +} \ No newline at end of file diff --git a/app/components/markdown/markdown_image/index.js b/app/components/markdown/markdown_image/index.js index 41dc8e0e0..bb8fa550e 100644 --- a/app/components/markdown/markdown_image/index.js +++ b/app/components/markdown/markdown_image/index.js @@ -5,13 +5,10 @@ import {connect} from 'react-redux'; import {getCurrentUrl} from '@mm-redux/selectors/entities/general'; -import {getDimensions} from 'app/selectors/device'; - import MarkdownImage from './markdown_image'; function mapStateToProps(state) { return { - ...getDimensions(state), serverURL: getCurrentUrl(state), }; } diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js index 7112a0dfa..71859a21f 100644 --- a/app/components/markdown/markdown_image/markdown_image.js +++ b/app/components/markdown/markdown_image/markdown_image.js @@ -13,29 +13,27 @@ import { Text, View, } from 'react-native'; +import FastImage from 'react-native-fast-image'; + +import ImageViewPort from '@components/image_viewport'; +import ProgressiveImage from '@components/progressive_image'; +import FormattedText from '@components/formatted_text'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {CustomPropTypes} from '@constants'; +import EphemeralStore from '@store/ephemeral_store'; +import BottomSheet from '@utils/bottom_sheet'; +import {calculateDimensions, getViewPortWidth, isGifTooLarge, previewImageAtIndex} from '@utils/images'; +import {normalizeProtocol} from '@utils/url'; -import FormattedText from 'app/components/formatted_text'; -import ProgressiveImage from 'app/components/progressive_image'; -import TouchableWithFeedback from 'app/components/touchable_with_feedback'; -import CustomPropTypes from 'app/constants/custom_prop_types'; -import EphemeralStore from 'app/store/ephemeral_store'; import mattermostManaged from 'app/mattermost_managed'; -import BottomSheet from 'app/utils/bottom_sheet'; -import {previewImageAtIndex, calculateDimensions, isGifTooLarge} from 'app/utils/images'; -import {normalizeProtocol} from 'app/utils/url'; - import brokenImageIcon from 'assets/images/icons/brokenimage.png'; const ANDROID_MAX_HEIGHT = 4096; const ANDROID_MAX_WIDTH = 4096; -const VIEWPORT_IMAGE_OFFSET = 66; -const VIEWPORT_IMAGE_REPLY_OFFSET = 13; -export default class MarkdownImage extends React.PureComponent { +export default class MarkdownImage extends ImageViewPort { static propTypes = { children: PropTypes.node, - deviceHeight: PropTypes.number.isRequired, - deviceWidth: PropTypes.number.isRequired, imagesMetadata: PropTypes.object, linkDestination: PropTypes.string, isReplyPost: PropTypes.bool, @@ -57,38 +55,11 @@ export default class MarkdownImage extends React.PureComponent { failed: false, uri: null, }; - - this.mounted = false; } componentDidMount() { - this.mounted = true; - - this.setImageUrl(this.getSource()); - } - - static getDerivedStateFromProps(props) { - const imageMetadata = props.imagesMetadata?.[props.source]; - - if (imageMetadata) { - return { - originalHeight: imageMetadata.height, - originalWidth: imageMetadata.width, - }; - } - - return null; - } - - componentDidUpdate(prevProps) { - if (this.props.source !== prevProps.source) { - // getSource also depends on serverURL, but that shouldn't change while this is mounted - this.setImageUrl(this.getSource()); - } - } - - componentWillUnmount() { - this.mounted = false; + super.componentDidMount(); + this.loadImageSize(this.getSource()); } setImageRef = (ref) => { @@ -100,19 +71,14 @@ export default class MarkdownImage extends React.PureComponent { } getSource = () => { - let source = this.props.source; + let uri = this.props.source; - if (source.startsWith('/')) { - source = EphemeralStore.currentServerUrl + source; + if (uri.startsWith('/')) { + uri = EphemeralStore.currentServerUrl + uri; } - return source; - }; - - getViewPortWidth = () => { - const {deviceHeight, deviceWidth, isReplyPost} = this.props; - const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; - return deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0); + FastImage.preload([{uri}]); + return uri; }; handleSizeReceived = (width, height) => { @@ -179,7 +145,6 @@ export default class MarkdownImage extends React.PureComponent { const { originalHeight, originalWidth, - uri, } = this.state; const link = this.getSource(); let filename = link.substring(link.lastIndexOf('/') + 1, link.indexOf('?') === -1 ? link.length : link.indexOf('?')); @@ -196,9 +161,9 @@ export default class MarkdownImage extends React.PureComponent { width: originalWidth, height: originalHeight, }, - source: {uri}, + source: {link}, data: { - localPath: uri, + localPath: link, }, }]; @@ -211,21 +176,15 @@ export default class MarkdownImage extends React.PureComponent { } }; - setImageUrl = (imageURL) => { - const uri = imageURL; - - this.setState({uri}); - this.loadImageSize(uri); - }; - render() { if (isGifTooLarge(this.props.imagesMetadata?.[this.props.source])) { return null; } let image = null; - const {originalHeight, originalWidth, uri} = this.state; - const {height, width} = calculateDimensions(originalHeight, originalWidth, this.getViewPortWidth()); + const {originalHeight, originalWidth} = this.state; + const uri = this.getSource(); + const {height, width} = calculateDimensions(originalHeight, originalWidth, getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar())); if (width && height) { if (Platform.OS === 'android' && (width > ANDROID_MAX_WIDTH || height > ANDROID_MAX_HEIGHT)) { @@ -269,7 +228,7 @@ export default class MarkdownImage extends React.PureComponent { } } else if (this.state.failed) { image = ( - diff --git a/app/components/markdown/transform.test.js b/app/components/markdown/transform.test.js index 1e83e9ed6..9797a3af8 100644 --- a/app/components/markdown/transform.test.js +++ b/app/components/markdown/transform.test.js @@ -3055,7 +3055,7 @@ function nodeToString(node) { } const ignoredKeys = {_sourcepos: true, _lastLineBlank: true, _open: true, _string_content: true, _info: true, _isFenced: true, _fenceChar: true, _fenceLength: true, _fenceOffset: true, _onEnter: true, _onExit: true}; -function astToJson(node, visited = [], indent = '') { // eslint-disable-line no-unused-vars +function astToJson(node, visited = [], indent = '') { // eslint-disable-line @typescript-eslint/no-unused-vars let out = '{'; const myVisited = [...visited]; diff --git a/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap b/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap index 5dc1bfdb1..1e202e71a 100644 --- a/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap +++ b/app/components/message_attachments/__snapshots__/attachment_footer.test.js.snap @@ -10,7 +10,7 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a } } > - - { const {link} = this.props; if (link && Linking.canOpenURL(link)) { @@ -39,7 +48,7 @@ export default class AttachmentAuthor extends PureComponent { return ( {Boolean(icon) && - {Boolean(icon) && - - - - { // should return null expect(wrapper.instance().renderImage()).toMatchSnapshot(); expect(wrapper.state('hasImage')).toEqual(false); - expect(wrapper.find(Image).exists()).toEqual(false); + expect(wrapper.find(FastImage).exists()).toEqual(false); expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(false); const images = [{height: 440, width: 1200, url: 'https://mattermost.com/logo.png'}]; @@ -90,7 +90,7 @@ describe('PostAttachmentOpenGraph', () => { expect(wrapper.instance().renderImage()).toMatchSnapshot(); expect(wrapper.state('hasImage')).toEqual(true); - expect(wrapper.find(Image).exists()).toEqual(true); + expect(wrapper.find(FastImage).exists()).toEqual(true); expect(wrapper.find(TouchableWithFeedback).exists()).toEqual(true); }); 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 3c291ae64..fd25bc920 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 @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {PureComponent} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { Alert, @@ -14,26 +14,22 @@ import { import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube'; import {intlShape} from 'react-intl'; +import ImageViewPort from '@components/image_viewport'; +import PostAttachmentImage from '@components/post_attachment_image'; +import ProgressiveImage from '@components/progressive_image'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import CustomPropTypes from '@constants/custom_prop_types'; import EventEmitter from '@mm-redux/utils/event_emitter'; +import {calculateDimensions, getViewPortWidth, previewImageAtIndex} from '@utils/images'; +import {getYouTubeVideoId, isImageLink, isYoutubeLink} from '@utils/url'; -import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout'; -import PostAttachmentImage from 'app/components/post_attachment_image'; -import ProgressiveImage from 'app/components/progressive_image'; -import TouchableWithFeedback from 'app/components/touchable_with_feedback'; - -import {DeviceTypes} from 'app/constants'; -import CustomPropTypes from 'app/constants/custom_prop_types'; -import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; -import {getYouTubeVideoId, isImageLink, isYoutubeLink} from 'app/utils/url'; - -const VIEWPORT_IMAGE_OFFSET = 66; -const VIEWPORT_IMAGE_REPLY_OFFSET = 13; -const MAX_YOUTUBE_IMAGE_HEIGHT = 150; -const MAX_YOUTUBE_IMAGE_WIDTH = 297; +const MAX_YOUTUBE_IMAGE_HEIGHT = 202; +const MAX_YOUTUBE_IMAGE_WIDTH = 360; +const MAX_IMAGE_HEIGHT = 150; let MessageAttachments; let PostAttachmentOpenGraph; -export default class PostBodyAdditionalContent extends PureComponent { +export default class PostBodyAdditionalContent extends ImageViewPort { static propTypes = { actions: PropTypes.shape({ getRedirectLocation: PropTypes.func.isRequired, @@ -73,7 +69,7 @@ export default class PostBodyAdditionalContent extends PureComponent { if (this.isImage() && props.metadata && props.metadata.images) { const img = props.metadata.images[props.link]; if (img && img.height && img.width) { - dimensions = calculateDimensions(img.height, img.width, this.getViewPortWidth(props)); + dimensions = calculateDimensions(img.height, img.width, getViewPortWidth(props.isReplyPost, this.hasPermanentSidebar())); } } @@ -82,13 +78,10 @@ export default class PostBodyAdditionalContent extends PureComponent { linkLoaded: false, ...dimensions, }; - - this.mounted = false; } componentDidMount() { - this.mounted = true; - + super.componentDidMount(); this.load(); } @@ -98,174 +91,20 @@ export default class PostBodyAdditionalContent extends PureComponent { } } - componentWillUnmount() { - this.mounted = false; - } - - isImage = (specificLink) => { - const {metadata, link} = this.props; - - if (isImageLink(specificLink || link)) { - return true; - } - - if (metadata && metadata.images) { - return Boolean(metadata.images[specificLink] || metadata.images[link]); - } - - return false; - }; - - getImageUrl = (link) => { - let imageUrl; - - if (this.isImage()) { - imageUrl = link; - } else if (isYoutubeLink(link)) { - const videoId = getYouTubeVideoId(link); - imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; - } - - return imageUrl; - } - - load = (linkChanged = false) => { - const {link, expandedLink, actions} = this.props; - - if (link) { - let imageUrl = this.getImageUrl(link); - - if (!imageUrl) { - if (!expandedLink || linkChanged) { - actions.getRedirectLocation(link); - } else { - imageUrl = this.getImageUrl(expandedLink); - } - } - - if (imageUrl) { - this.getImageSize(imageUrl); - } - } - }; - - calculateYouTubeImageDimensions = (width, height) => { - const {deviceHeight, deviceWidth} = this.props; - let maxHeight = MAX_YOUTUBE_IMAGE_HEIGHT; - const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; - let maxWidth = deviceSize - 78; - - if (maxWidth > MAX_YOUTUBE_IMAGE_WIDTH) { - maxWidth = MAX_YOUTUBE_IMAGE_WIDTH; - } - - if (height <= MAX_YOUTUBE_IMAGE_HEIGHT) { - maxHeight = height; - } else { - maxHeight = (height / width) * maxWidth; - if (maxHeight > MAX_YOUTUBE_IMAGE_HEIGHT) { - maxHeight = MAX_YOUTUBE_IMAGE_HEIGHT; - } - } - - if (height > width) { - maxWidth = (width / height) * maxHeight; - } - - return {width: maxWidth, height: maxHeight}; - }; - - generateStaticEmbed = (isYouTube, isImage) => { - const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props; - - if (isYouTube || (isImage && !openGraphData)) { - return null; - } - - const attachments = this.getMessageAttachment(); - if (attachments) { - return attachments; - } - - if (!openGraphData && metadata) { - return null; - } - - if (link && showLinkPreviews) { - if (!PostAttachmentOpenGraph) { - PostAttachmentOpenGraph = require('app/components/post_attachment_opengraph').default; - } - - return ( - - ); - } - - return null; - }; - generateToggleableEmbed = (isImage, isYouTube) => { let {link} = this.props; const {expandedLink} = this.props; if (expandedLink) { link = expandedLink; } - const {width, height, uri} = this.state; if (link) { if (isYouTube) { - const videoId = getYouTubeVideoId(link); - const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; - const thumbUrl = `https://i.ytimg.com/vi/${videoId}/default.jpg`; - - return ( - - - - - - - - ); + return this.renderYouTubeVideo(link); } if (isImage) { - const imageMetadata = this.props.metadata?.images?.[link]; - - return ( - - ); + return this.renderImage(link); } } @@ -291,83 +130,17 @@ export default class PostBodyAdditionalContent extends PureComponent { } }; - getViewPortWidth = (props) => { - const {deviceHeight, deviceWidth, isReplyPost} = props; - const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth; - const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0); - const tabletOffset = DeviceTypes.IS_TABLET ? TABLET_WIDTH : 0; + getImageUrl = (link) => { + let imageUrl; - return viewPortWidth - tabletOffset; - }; - - setImageSize = (uri, originalWidth, originalHeight) => { - if (!this.mounted) { - return; + if (this.isImage()) { + imageUrl = link; + } else if (isYoutubeLink(link)) { + const videoId = getYouTubeVideoId(link); + imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; } - if (!originalWidth && !originalHeight) { - this.setState({linkLoadError: true}); - return; - } - - const {link} = this.props; - const viewPortWidth = this.getViewPortWidth(this.props); - - let dimensions; - if (isYoutubeLink(link)) { - dimensions = this.calculateYouTubeImageDimensions(originalWidth, originalHeight); - } else { - dimensions = calculateDimensions(originalHeight, originalWidth, viewPortWidth); - } - - this.setState({ - ...dimensions, - originalHeight, - originalWidth, - linkLoaded: true, - uri, - }); - }; - - getMessageAttachment = () => { - const { - postId, - postProps, - baseTextStyle, - blockStyles, - deviceHeight, - deviceWidth, - metadata, - onHashtagPress, - onPermalinkPress, - textStyles, - theme, - } = this.props; - const {attachments} = postProps; - - if (attachments && attachments.length) { - if (!MessageAttachments) { - MessageAttachments = require('app/components/message_attachments').default; - } - - return ( - - ); - } - - return null; + return imageUrl; }; getYouTubeTime = (link) => { @@ -430,6 +203,44 @@ export default class PostBodyAdditionalContent extends PureComponent { previewImageAtIndex([imageRef], 0, files); }; + isImage = (specificLink) => { + const {metadata, link} = this.props; + + if (isImageLink(specificLink || link)) { + return true; + } + + if (metadata && metadata.images) { + return Boolean(metadata.images[specificLink] || metadata.images[link]); + } + + return false; + }; + + load = (linkChanged = false) => { + const {link, expandedLink, actions} = this.props; + + if (link) { + let imageUrl = this.getImageUrl(link); + + if (isYoutubeLink(link)) { + return; + } + + if (!imageUrl) { + if (!expandedLink || linkChanged) { + actions.getRedirectLocation(link); + } else { + imageUrl = this.getImageUrl(expandedLink); + } + } + + if (imageUrl) { + this.getImageSize(imageUrl); + } + } + }; + playYouTubeVideo = () => { const {link} = this.props; const videoId = getYouTubeVideoId(link); @@ -480,6 +291,164 @@ export default class PostBodyAdditionalContent extends PureComponent { ); }; + renderImage = (link) => { + const imageMetadata = this.props.metadata?.images?.[link]; + const {width, height, uri} = this.state; + + return ( + + ); + }; + + renderMessageAttachment = () => { + const { + postId, + postProps, + baseTextStyle, + blockStyles, + deviceHeight, + deviceWidth, + metadata, + onHashtagPress, + onPermalinkPress, + textStyles, + theme, + } = this.props; + const {attachments} = postProps; + + if (attachments && attachments.length) { + if (!MessageAttachments) { + MessageAttachments = require('app/components/message_attachments').default; + } + + return ( + + ); + } + + return null; + }; + + renderOpenGraph = (isYouTube, isImage) => { + const {isReplyPost, link, metadata, openGraphData, showLinkPreviews, theme} = this.props; + + if (isYouTube || (isImage && !openGraphData)) { + return null; + } + + const attachments = this.renderMessageAttachment(); + if (attachments) { + return attachments; + } + + if (!openGraphData && metadata) { + return null; + } + + if (link && showLinkPreviews) { + if (!PostAttachmentOpenGraph) { + PostAttachmentOpenGraph = require('app/components/post_attachment_opengraph').default; + } + + return ( + + ); + } + + return null; + }; + + renderYouTubeVideo = (link) => { + const videoId = getYouTubeVideoId(link); + const dimensions = calculateDimensions( + MAX_YOUTUBE_IMAGE_HEIGHT, + MAX_YOUTUBE_IMAGE_WIDTH, + getViewPortWidth(this.props.isReplyPost, this.hasPermanentSidebar()), + ); + const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; + + return ( + + + + + + + + ); + } + + setImageSize = (uri, originalWidth, originalHeight) => { + if (!this.mounted) { + return; + } + + if (!originalWidth && !originalHeight) { + this.setState({linkLoadError: true}); + return; + } + + const {isReplyPost, link} = this.props; + const viewPortWidth = getViewPortWidth(isReplyPost, this.hasPermanentSidebar()); + + let dimensions; + if (isYoutubeLink(link)) { + dimensions = calculateDimensions(MAX_YOUTUBE_IMAGE_HEIGHT, MAX_YOUTUBE_IMAGE_WIDTH, viewPortWidth); + } else { + dimensions = calculateDimensions(originalHeight, originalWidth, viewPortWidth); + } + + this.setState({ + ...dimensions, + originalHeight, + originalWidth, + linkLoaded: true, + uri, + }); + }; + render() { let {link} = this.props; const {openGraphData, postProps, expandedLink} = this.props; @@ -504,7 +473,7 @@ export default class PostBodyAdditionalContent extends PureComponent { } } - return this.generateStaticEmbed(isYouTube, isImage && !linkLoadError); + return this.renderOpenGraph(isYouTube, isImage && !linkLoadError); } } diff --git a/app/components/post_profile_picture/post_profile_picture.js b/app/components/post_profile_picture/post_profile_picture.js index 699979dac..7f7e95b08 100644 --- a/app/components/post_profile_picture/post_profile_picture.js +++ b/app/components/post_profile_picture/post_profile_picture.js @@ -3,7 +3,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Image, Platform, StyleSheet, View} from 'react-native'; +import {Platform, StyleSheet, View} from 'react-native'; +import FastImage from 'react-native-fast-image'; import AppIcon from 'app/components/app_icon'; import ProfilePicture from 'app/components/profile_picture'; @@ -72,7 +73,7 @@ export default class PostProfilePicture extends PureComponent { width: frameSize, }, style.buffer]} > - ({ launchCamera: jest.fn(), })); diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index 7d0dc3995..883e6ef01 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -3,7 +3,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Image, Platform, View} from 'react-native'; +import {Platform, View} from 'react-native'; +import FastImage from 'react-native-fast-image'; import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {Client4} from '@mm-redux/client'; @@ -61,7 +62,10 @@ export default class ProfilePicture extends PureComponent { } else if (edit && imageUri) { this.setImageURL(imageUri); } else if (user) { - this.setImageURL(Client4.getProfilePictureUrl(user.id, user.last_picture_update)); + const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update); + FastImage.preload([{uri, headers: Client4.getOptions({}).headers}]); + + this.setImageURL(uri); this.clearProfileImageUri(); } } @@ -100,6 +104,7 @@ export default class ProfilePicture extends PureComponent { if (nextUrl && url !== nextUrl) { // empty function is so that promise unhandled is not triggered in dev mode + FastImage.preload([{uri: nextUrl, headers: Client4.getOptions({}).headers}]); this.setImageURL(nextUrl); this.clearProfileImageUri(); } @@ -150,7 +155,7 @@ export default class ProfilePicture extends PureComponent { }; image = ( - diff --git a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap index cd2c28710..23f0b5da9 100644 --- a/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap +++ b/app/components/progressive_image/__snapshots__/progressive_image.test.js.snap @@ -1,11 +1,109 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ProgressiveImage should match snapshot 1`] = ` +exports[`ProgressiveImage should match snapshot for BackgroundImage 1`] = ` + + +`; + +exports[`ProgressiveImage should match snapshot for Default Image 1`] = ` + + +`; + +exports[`ProgressiveImage should match snapshot for Image 1`] = ` + + + - - + `; diff --git a/app/components/progressive_image/progressive_image.js b/app/components/progressive_image/progressive_image.js index 7aaecdae1..d74385c05 100644 --- a/app/components/progressive_image/progressive_image.js +++ b/app/components/progressive_image/progressive_image.js @@ -3,23 +3,22 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {Animated, Image, ImageBackground, Platform, View, StyleSheet} from 'react-native'; +import {Animated, ImageBackground, View, StyleSheet} from 'react-native'; import FastImage from 'react-native-fast-image'; -import {Client4} from '@mm-redux/client'; - import CustomPropTypes from 'app/constants/custom_prop_types'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import thumb from 'assets/images/thumb.png'; + const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground); -const AnimatedImage = Animated.createAnimatedComponent(FastImage); +const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); export default class ProgressiveImage extends PureComponent { static propTypes = { isBackgroundImage: PropTypes.bool, children: CustomPropTypes.Children, defaultSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), // this should be provided by the component - filename: PropTypes.string, imageUri: PropTypes.string, imageStyle: CustomPropTypes.Style, onError: PropTypes.func, @@ -27,90 +26,35 @@ export default class ProgressiveImage extends PureComponent { resizeMode: PropTypes.string, style: CustomPropTypes.Style, theme: PropTypes.object.isRequired, - thumbnailUri: PropTypes.string, tintDefaultSource: PropTypes.bool, }; static defaultProps = { style: {}, + resizeMode: 'contain', }; constructor(props) { super(props); - this.subscribedToCache = true; - this.state = { - intensity: new Animated.Value(80), - thumb: null, - uri: null, - failedImageLoad: false, + intensity: new Animated.Value(0), }; } - componentDidMount() { - this.load(); + onLoadImageEnd = () => { + Animated.timing(this.state.intensity, { + duration: 300, + toValue: 100, + useNativeDriver: true, + }).start(); } - componentDidUpdate(prevProps, prevState) { - if (this.props.filename !== prevProps.filename || - this.props.imageUri !== prevProps.imageUri || - this.props.thumbnailUri !== prevProps.thumbnailUri) { - this.load(); - } - - const {intensity, thumb, uri} = this.state; - if (uri && thumb && uri !== thumb && prevState.uri !== uri) { - Animated.timing(intensity, { - duration: 300, - toValue: 0, - useNativeDriver: Platform.OS === 'android', - }).start(); - } - } - - componentWillUnmount() { - this.subscribedToCache = false; - } - - load = () => { - const {imageUri, thumbnailUri} = this.props; - - if (thumbnailUri) { - this.setThumbnail(thumbnailUri); - } else if (imageUri) { - this.setImage(imageUri); - } - }; - - loadFullImage = () => { - if (!this.state.uri) { - const {imageUri} = this.props; - this.setImage(imageUri); - } - } - - setImage = (uri) => { - if (this.subscribedToCache) { - this.setState({uri}); - } - }; - - setThumbnail = (thumb) => { - if (this.subscribedToCache) { - if (!thumb && !this.state.failedImageLoad) { - this.load(); - this.setState({failedImageLoad: true}); - } else { - this.setState({thumb, uri: null}); - } - } - }; - render() { const { defaultSource, imageStyle, + imageUri, isBackgroundImage, onError, resizeMode, @@ -119,15 +63,6 @@ export default class ProgressiveImage extends PureComponent { theme, tintDefaultSource, } = this.props; - const {uri, intensity, thumb} = this.state; - const hasDefaultSource = Boolean(defaultSource); - const hasPreview = Boolean(thumb); - const hasURI = Boolean(uri); - const isImageReady = uri && uri !== thumb; - const opacity = intensity.interpolate({ - inputRange: [50, 100], - outputRange: [0.5, 1], - }); let DefaultComponent; let ImageComponent; @@ -135,20 +70,33 @@ export default class ProgressiveImage extends PureComponent { DefaultComponent = ImageBackground; ImageComponent = AnimatedImageBackground; } else { - DefaultComponent = Image; - ImageComponent = AnimatedImage; + DefaultComponent = Animated.Image; + ImageComponent = AnimatedFastImage; } const styles = getStyleSheet(theme); - let defaultImage; - if (hasDefaultSource && tintDefaultSource) { - defaultImage = ( - + if (isBackgroundImage) { + return ( + + + {this.props.children} + + + ); + } + + if (defaultSource) { + return ( + @@ -156,57 +104,41 @@ export default class ProgressiveImage extends PureComponent { ); - } else { - defaultImage = ( + } + + const opacity = this.state.intensity.interpolate({ + inputRange: [20, 100], + outputRange: [0.2, 1], + }); + const defaultOpacity = this.state.intensity.interpolate({ + inputRange: [0, 100], + outputRange: [0.5, 0], + }); + + const containerStyle = { + backgroundColor: changeOpacity(theme.centerChannelColor, defaultOpacity), + }; + + return ( + - {this.props.children} - - ); - } - - if (hasDefaultSource && !hasPreview && !hasURI) { - return ( - - {defaultImage} - {hasPreview && - - } - - ); - } - - let source; - if (hasPreview && !isImageReady) { - source = {uri: thumb}; - } else if (isImageReady) { - source = {uri}; - } - - return ( - - {source && + source={thumb} + style={[imageStyle, {tintColor: theme.centerChannelColor, opacity: defaultOpacity}]} + /> {this.props.children} - } - {hasPreview && - - } - + ); } } @@ -214,10 +146,6 @@ export default class ProgressiveImage extends PureComponent { const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { defaultImageContainer: { - flex: 1, - position: 'absolute', - height: 80, - width: 80, alignItems: 'center', justifyContent: 'center', }, diff --git a/app/components/progressive_image/progressive_image.test.js b/app/components/progressive_image/progressive_image.test.js index dd82b2911..ead3f23ec 100644 --- a/app/components/progressive_image/progressive_image.test.js +++ b/app/components/progressive_image/progressive_image.test.js @@ -15,74 +15,48 @@ jest.mock('react-native-fast-image', () => ({ jest.useFakeTimers(); describe('ProgressiveImage', () => { - const baseProps = { - isBackgroundImage: false, - defaultSource: 0, - filename: 'defaultImage', - imageUri: 'https://images.com/image.png', - onError: jest.fn(), - resizeMethod: 'auto', - resizeMode: 'contain', - theme: Preferences.THEMES.default, - thumbnailUri: 'https://images.com/image.png', - tintDefaultSource: false, - }; + test('should match snapshot for Image', () => { + const baseProps = { + isBackgroundImage: false, + imageUri: 'https://images.com/image.png', + onError: jest.fn(), + resizeMethod: 'auto', + resizeMode: 'contain', + theme: Preferences.THEMES.default, + tintDefaultSource: false, + }; - test('should match snapshot', () => { const wrapper = shallow(); expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should load image', () => { + test('should match snapshot for Default Image', () => { + const baseProps = { + isBackgroundImage: false, + defaultSource: 'https://images.com/image.png', + onError: jest.fn(), + resizeMethod: 'auto', + resizeMode: 'contain', + theme: Preferences.THEMES.default, + tintDefaultSource: false, + }; + const wrapper = shallow(); - const instance = wrapper.instance(); - jest.spyOn(instance, 'load'); - - // should load image on componentDidMount - instance.componentDidMount(); - expect(instance.load).toHaveBeenCalledTimes(1); - - // should not re-load image on componentDidUpdate with same props - instance.componentDidUpdate(baseProps); - expect(instance.load).toHaveBeenCalledTimes(1); - - // should re-load image on componentDidUpdate when props changed - wrapper.setProps({filename: 'newImage'}); - expect(instance.load).toHaveBeenCalledTimes(2); - - wrapper.setProps({imageUri: 'https://images.com/new_image.png'}); - expect(instance.load).toHaveBeenCalledTimes(3); - - wrapper.setProps({thumbnailUri: 'https://images.com/new_image.png'}); - expect(instance.load).toHaveBeenCalledTimes(4); + expect(wrapper.getElement()).toMatchSnapshot(); }); - test('should set image when imageUri is set but not the thumbnailUri', () => { - const wrapper = shallow( - , - ); - const instance = wrapper.instance(); - jest.spyOn(instance, 'setImage'); + test('should match snapshot for BackgroundImage', () => { + const baseProps = { + isBackgroundImage: true, + imageUri: 'https://images.com/image.png', + onError: jest.fn(), + resizeMethod: 'auto', + resizeMode: 'contain', + theme: Preferences.THEMES.default, + tintDefaultSource: false, + }; - instance.componentDidMount(); - jest.runAllTimers(); - expect(instance.setImage).toHaveBeenCalledTimes(1); - }); - - test('should set thumbnail when thumbnailUri and imageUri are set', () => { - const wrapper = shallow( - , - ); - const instance = wrapper.instance(); - jest.spyOn(instance, 'setThumbnail'); - instance.load(); - - jest.runAllTimers(); - expect(instance.setThumbnail).toHaveBeenCalledTimes(1); + const wrapper = shallow(); + expect(wrapper.getElement()).toMatchSnapshot(); }); }); \ No newline at end of file diff --git a/app/components/team_icon/team_icon.js b/app/components/team_icon/team_icon.js index f4e6122cc..7cdd5ad91 100644 --- a/app/components/team_icon/team_icon.js +++ b/app/components/team_icon/team_icon.js @@ -3,12 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; - -import { - Image, - Text, - View, -} from 'react-native'; +import {Text, View} from 'react-native'; +import FastImage from 'react-native-fast-image'; import {Client4} from '@mm-redux/client'; @@ -39,7 +35,7 @@ export default class TeamIcon extends React.PureComponent { this.mounted = true; if (lastIconUpdate) { - this.setImageURL(Client4.getTeamIconUrl(teamId, lastIconUpdate)); + this.preloadTeamIcon(teamId, lastIconUpdate); } } @@ -48,7 +44,7 @@ export default class TeamIcon extends React.PureComponent { this.setImageURL(null); } else if (this.props.lastIconUpdate && this.props.lastIconUpdate !== prevProps.lastIconUpdate) { const {lastIconUpdate, teamId} = this.props; - this.setImageURL(Client4.getTeamIconUrl(teamId, lastIconUpdate)); + this.preloadTeamIcon(teamId, lastIconUpdate); } } @@ -56,6 +52,12 @@ export default class TeamIcon extends React.PureComponent { this.mounted = false; } + preloadTeamIcon = (teamId, lastIconUpdate) => { + const uri = Client4.getTeamIconUrl(teamId, lastIconUpdate); + FastImage.preload([{uri, headers: Client4.getOptions({}).headers}]); + this.setImageURL(uri); + }; + setImageURL = (teamIcon) => { if (this.mounted) { this.setState({imageError: false, teamIcon}); @@ -76,7 +78,7 @@ export default class TeamIcon extends React.PureComponent { let teamIconContent; if (this.state.teamIcon) { teamIconContent = ( - this.setState({imageError: true})} diff --git a/app/components/video_controls.js b/app/components/video_controls.js index abf35da5b..61ed42017 100644 --- a/app/components/video_controls.js +++ b/app/components/video_controls.js @@ -10,12 +10,12 @@ import { ActivityIndicator, Animated, AppState, - Image, TouchableOpacity, StyleSheet, Text, View, } from 'react-native'; +import FastImage from 'react-native-fast-image'; import Slider from 'react-native-slider'; import fullscreenImage from 'assets/images/video_player/fullscreen.png'; @@ -150,8 +150,7 @@ export default class VideoControls extends PureComponent { renderControls() { return ( - - + { this.props.isLoading ? this.setLoadingView() : this.setPlayerControls(this.props.playerState) } @@ -182,7 +181,10 @@ export default class VideoControls extends PureComponent { style={styles.fullScreenContainer} onPress={this.props.onFullScreen} > - + @@ -221,7 +223,7 @@ export default class VideoControls extends PureComponent { style={[styles.playButton, {backgroundColor: this.props.mainColor}]} onPress={pressAction} > - @@ -279,7 +281,6 @@ const styles = StyleSheet.create({ right: 0, }, controlsRow: { - flex: 1, alignItems: 'center', justifyContent: 'center', alignSelf: 'stretch', @@ -307,9 +308,11 @@ const styles = StyleSheet.create({ resizeMode: 'stretch', }, progressContainer: { + position: 'absolute', flexDirection: 'row', justifyContent: 'flex-end', - marginBottom: -25, + bottom: 25, + marginLeft: 16, }, progressColumnContainer: { flex: 1, @@ -318,7 +321,8 @@ const styles = StyleSheet.create({ alignSelf: 'stretch', alignItems: 'center', justifyContent: 'center', - paddingLeft: 20, + paddingLeft: 10, + paddingTop: 8, }, progressSlider: { alignSelf: 'stretch', diff --git a/app/constants/device.js b/app/constants/device.js index a46bbc035..7cfd41a9b 100644 --- a/app/constants/device.js +++ b/app/constants/device.js @@ -21,4 +21,5 @@ export default { IS_TABLET: DeviceInfo.isTablet(), VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`, PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS', + TABLET_WIDTH: 250, }; diff --git a/app/constants/image.js b/app/constants/image.js index e5e61c557..448a80912 100644 --- a/app/constants/image.js +++ b/app/constants/image.js @@ -3,3 +3,6 @@ export const IMAGE_MAX_HEIGHT = 350; export const IMAGE_MIN_DIMENSION = 50; +export const MAX_GIF_SIZE = 100 * 1024 * 1024; +export const VIEWPORT_IMAGE_OFFSET = 70; +export const VIEWPORT_IMAGE_REPLY_OFFSET = 11; diff --git a/app/constants/index.js b/app/constants/index.js index 6678203b0..cdd661b49 100644 --- a/app/constants/index.js +++ b/app/constants/index.js @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import CustomPropTypes from './custom_prop_types'; import DeepLinkTypes from './deep_linking'; import DeviceTypes from './device'; import ListTypes from './list'; @@ -10,6 +11,7 @@ import ViewTypes, {UpgradeTypes} from './view'; import WebsocketEvents from './websocket'; export { + CustomPropTypes, DeepLinkTypes, DeviceTypes, ListTypes, diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index 429ed41a5..2ca97e36b 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -6,13 +6,11 @@ import AsyncStorage from '@react-native-community/async-storage'; import CookieManager from 'react-native-cookies'; import DeviceInfo from 'react-native-device-info'; import RNFetchBlob from 'rn-fetch-blob'; -import {batchActions} from 'redux-batched-actions'; import semver from 'semver/preload'; import {setAppState, setServerVersion} from '@mm-redux/actions/general'; import {autoUpdateTimezone} from '@mm-redux/actions/timezone'; import {close as closeWebSocket} from '@actions/websocket'; -import {GeneralTypes} from '@mm-redux/action_types'; import {Client4} from '@mm-redux/client'; import {General} from '@mm-redux/constants'; import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; diff --git a/app/mattermost.js b/app/mattermost.js index 4c2d4f34d..1eee8ab33 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -29,7 +29,6 @@ import {captureJSException} from '@utils/sentry'; const init = async () => { const credentials = await getAppCredentials(); - const dt = Date.now(); const MMKVStorage = await getStorage(); const {store} = configureStore(MMKVStorage); diff --git a/app/mm-redux/actions/bots.test.js b/app/mm-redux/actions/bots.test.js index 7ff9b9a17..18e3010e8 100644 --- a/app/mm-redux/actions/bots.test.js +++ b/app/mm-redux/actions/bots.test.js @@ -5,7 +5,6 @@ import assert from 'assert'; import nock from 'nock'; import * as BotActions from '@mm-redux/actions/bots'; -import * as UserActions from '@mm-redux/actions/users'; import {Client4} from '@mm-redux/client'; import TestHelper from 'test/test_helper'; diff --git a/app/mm-redux/actions/channels.test.js b/app/mm-redux/actions/channels.test.js index 4c339c354..a7e0f9675 100644 --- a/app/mm-redux/actions/channels.test.js +++ b/app/mm-redux/actions/channels.test.js @@ -1644,7 +1644,7 @@ describe('Actions.Channels', () => { post('/channels/search'). reply(200, [TestHelper.basicChannel, userChannel]); - const {data} = await store.dispatch(Actions.searchAllChannels('test', 0)); + await store.dispatch(Actions.searchAllChannels('test', 0)); const moreRequest = store.getState().requests.channels.getAllChannels; if (moreRequest.status === RequestStatus.FAILURE) { diff --git a/app/mm-redux/actions/files.test.js b/app/mm-redux/actions/files.test.js index 9f939071c..8c6188f55 100644 --- a/app/mm-redux/actions/files.test.js +++ b/app/mm-redux/actions/files.test.js @@ -7,7 +7,6 @@ import nock from 'nock'; import * as Actions from '@mm-redux/actions/files'; import {Client4} from '@mm-redux/client'; -import {RequestStatus} from '../constants'; import TestHelper from 'test/test_helper'; import configureStore from 'test/test_store'; diff --git a/app/mm-redux/actions/groups.test.js b/app/mm-redux/actions/groups.test.js index 185763c25..c36656fbf 100644 --- a/app/mm-redux/actions/groups.test.js +++ b/app/mm-redux/actions/groups.test.js @@ -7,7 +7,7 @@ import nock from 'nock'; import * as Actions from '@mm-redux/actions/groups'; import {Client4} from '@mm-redux/client'; -import {RequestStatus, Groups} from '../constants'; +import {Groups} from '../constants'; import TestHelper from 'test/test_helper'; import configureStore from 'test/test_store'; diff --git a/app/mm-redux/actions/helpers.test.js b/app/mm-redux/actions/helpers.test.js index fa7b44160..9832c6b60 100644 --- a/app/mm-redux/actions/helpers.test.js +++ b/app/mm-redux/actions/helpers.test.js @@ -3,7 +3,6 @@ import assert from 'assert'; -import {UserTypes} from '@mm-redux/action_types'; import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers'; import {Client4} from '@mm-redux/client'; import {ClientError} from '@mm-redux/client/client4'; diff --git a/app/mm-redux/actions/preferences.test.js b/app/mm-redux/actions/preferences.test.js index a24dd3de1..5e5c92f19 100644 --- a/app/mm-redux/actions/preferences.test.js +++ b/app/mm-redux/actions/preferences.test.js @@ -7,7 +7,7 @@ import nock from 'nock'; import * as Actions from '@mm-redux/actions/preferences'; import {login} from '@mm-redux/actions/users'; import {Client4} from '@mm-redux/client'; -import {Preferences, RequestStatus} from '../constants'; +import {Preferences} from '../constants'; import TestHelper from 'test/test_helper'; import configureStore from 'test/test_store'; diff --git a/app/mm-redux/actions/teams.test.js b/app/mm-redux/actions/teams.test.js index e631d4466..d6fa96720 100644 --- a/app/mm-redux/actions/teams.test.js +++ b/app/mm-redux/actions/teams.test.js @@ -747,7 +747,7 @@ describe('Actions.Teams', () => { post('/teams/search'). reply(200, [TestHelper.basicTeam, userTeam]); - const {data} = await store.dispatch(Actions.searchTeams('test', 0)); + await store.dispatch(Actions.searchTeams('test', 0)); const moreRequest = store.getState().requests.teams.getTeams; if (moreRequest.status === RequestStatus.FAILURE) { diff --git a/app/mm-redux/client/client4.ts b/app/mm-redux/client/client4.ts index 72550d020..0df5e0ba6 100644 --- a/app/mm-redux/client/client4.ts +++ b/app/mm-redux/client/client4.ts @@ -21,7 +21,6 @@ import {IncomingWebhook, OutgoingWebhook, Command, OAuthApp, DialogSubmission} f import {CustomEmoji} from '@mm-redux/types/emojis'; import {Config} from '@mm-redux/types/config'; import {Bot, BotPatch} from '@mm-redux/types/bots'; -import {Dictionary} from '@mm-redux/types/utilities'; import {SyncablePatch} from '@mm-redux/types/groups'; const FormData = require('form-data'); diff --git a/app/mm-redux/reducers/entities/channel_categories.test.js b/app/mm-redux/reducers/entities/channel_categories.test.js index 75c7eeb5d..a1d894d2c 100644 --- a/app/mm-redux/reducers/entities/channel_categories.test.js +++ b/app/mm-redux/reducers/entities/channel_categories.test.js @@ -3,7 +3,7 @@ import {CategoryTypes} from '../../constants/channel_categories'; -import {ChannelCategoryTypes, TeamTypes} from '@mm-redux/action_types'; +import {TeamTypes} from '@mm-redux/action_types'; import * as Reducers from './channel_categories'; diff --git a/app/mm-redux/reducers/entities/channels.test.js b/app/mm-redux/reducers/entities/channels.test.js index 21a710fec..e6d945fd4 100644 --- a/app/mm-redux/reducers/entities/channels.test.js +++ b/app/mm-redux/reducers/entities/channels.test.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ChannelTypes, UserTypes} from '@mm-redux/action_types'; +import {ChannelTypes} from '@mm-redux/action_types'; import deepFreeze from '@mm-redux/utils/deep_freeze'; import channelsReducer, * as Reducers from './channels'; diff --git a/app/mm-redux/reducers/entities/search.test.js b/app/mm-redux/reducers/entities/search.test.js index 509a52965..f0e474095 100644 --- a/app/mm-redux/reducers/entities/search.test.js +++ b/app/mm-redux/reducers/entities/search.test.js @@ -3,11 +3,7 @@ import assert from 'assert'; -import { - PostTypes, - SearchTypes, - UserTypes, -} from '@mm-redux/action_types'; +import {PostTypes, SearchTypes} from '@mm-redux/action_types'; import reducer from '@mm-redux/reducers/entities/search'; describe('reducers.entities.search', () => { diff --git a/app/mm-redux/selectors/entities/channel_categories.test.js b/app/mm-redux/selectors/entities/channel_categories.test.js index c70e5c295..3e7845700 100644 --- a/app/mm-redux/selectors/entities/channel_categories.test.js +++ b/app/mm-redux/selectors/entities/channel_categories.test.js @@ -680,7 +680,6 @@ describe('makeFilterManuallyClosedDMs', () => { const currentUser = {id: 'currentUser'}; const otherUser1 = {id: 'otherUser1'}; const otherUser2 = {id: 'otherUser2'}; - const otherUser3 = {id: 'otherUser3'}; test('should filter DMs based on preferences', () => { const filterManuallyClosedDMs = Selectors.makeFilterManuallyClosedDMs(); diff --git a/app/mm-redux/selectors/entities/channels.test.js b/app/mm-redux/selectors/entities/channels.test.js index adaa2ef81..3fbf35318 100644 --- a/app/mm-redux/selectors/entities/channels.test.js +++ b/app/mm-redux/selectors/entities/channels.test.js @@ -7,8 +7,6 @@ import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; import TestHelper from 'test/test_helper'; import {sortChannelsByDisplayName, getDirectChannelName} from '@mm-redux/utils/channel_utils'; import * as Selectors from '@mm-redux/selectors/entities/channels'; -import * as TeamSelectors from '@mm-redux/selectors/entities/teams'; -import * as PreferencesSelectors from '@mm-redux/selectors/entities/preferences'; import {General, Preferences, Permissions} from '../../constants'; const sortUsernames = (a, b) => a.localeCompare(b, General.DEFAULT_LOCALE, {numeric: true}); diff --git a/app/mm-redux/utils/user_utils.test.js b/app/mm-redux/utils/user_utils.test.js index d6862a328..a07c4e118 100644 --- a/app/mm-redux/utils/user_utils.test.js +++ b/app/mm-redux/utils/user_utils.test.js @@ -20,19 +20,6 @@ describe('user utils', () => { first_name: 'test', last_name: 'user', }; - const testUser1 = { - id: 'test_user_id', - username: 'username', - first_name: 'First', - last_name: 'Last', - }; - const testUser2 = { - id: 'test_user_id_2', - username: 'username2', - first_name: 'First2', - last_name: 'Last2', - nickname: 'nick2', - }; it('should return username', () => { assert.equal(displayUsername(userObj, 'UNKNOWN_PREFERENCE'), 'testUser'); }); diff --git a/app/reducers/views/post.js b/app/reducers/views/post.js index e805928b7..ade8b8212 100644 --- a/app/reducers/views/post.js +++ b/app/reducers/views/post.js @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {combineReducers} from 'redux'; -import {UserTypes} from '@mm-redux/action_types'; import {ViewTypes} from 'app/constants'; diff --git a/app/screens/edit_post/edit_post.js b/app/screens/edit_post/edit_post.js index 7fc9e7a12..e2bf78617 100644 --- a/app/screens/edit_post/edit_post.js +++ b/app/screens/edit_post/edit_post.js @@ -11,8 +11,6 @@ import { import {Navigation} from 'react-native-navigation'; import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view'; -import {RequestStatus} from '@mm-redux/constants'; - import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete'; import ErrorText from 'app/components/error_text'; import Loading from 'app/components/loading'; diff --git a/app/screens/image_preview/__snapshots__/image_preview.test.js.snap b/app/screens/image_preview/__snapshots__/image_preview.test.js.snap index d53e2f2d4..2d36508bc 100644 --- a/app/screens/image_preview/__snapshots__/image_preview.test.js.snap +++ b/app/screens/image_preview/__snapshots__/image_preview.test.js.snap @@ -154,8 +154,9 @@ exports[`ImagePreview should match snapshot 1`] = ` "opacity": 1, }, Object { - "height": 64, "justifyContent": "center", + "maxHeight": 64, + "minHeight": 32, "overflow": "hidden", "position": "absolute", "width": "100%", @@ -186,10 +187,10 @@ exports[`ImagePreview should match snapshot 1`] = ` style={ Object { "bottom": 0, - "height": 64, "justifyContent": "flex-end", "left": 0, - "paddingBottom": 5, + "maxHeight": 64, + "paddingBottom": 0, "paddingHorizontal": 24, "position": "absolute", "right": 0, diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js index 01cdd8181..629ea3a0d 100644 --- a/app/screens/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -46,7 +46,7 @@ const {VIDEOS_PATH} = DeviceTypes; const {View: AnimatedView} = Animated; const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView); const HEADER_HEIGHT = 48; -const ANIM_CONFIG = {duration: 300}; +const ANIM_CONFIG = {duration: 300, userNativeDriver: true}; export default class ImagePreview extends PureComponent { static propTypes = { @@ -375,11 +375,12 @@ export default class ImagePreview extends PureComponent { const flattenStyle = StyleSheet.flatten(style); const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar); const imageStyle = {...flattenStyle, ...calculatedDimensions}; + const src = {...source, cache: FastImage.cacheControl.cacheOnly}; return ( @@ -637,7 +638,8 @@ const style = StyleSheet.create({ textAlign: 'center', }, footerContainer: { - height: 64, + minHeight: 32, + maxHeight: 64, justifyContent: 'center', overflow: 'hidden', position: 'absolute', @@ -648,10 +650,10 @@ const style = StyleSheet.create({ bottom: 0, left: 0, right: 0, - height: 64, + maxHeight: 64, justifyContent: 'flex-end', paddingHorizontal: 24, - paddingBottom: 5, + paddingBottom: 0, }, filename: { color: 'white', diff --git a/app/screens/notification/index.js b/app/screens/notification/index.js index f651bbc3c..46f22496e 100644 --- a/app/screens/notification/index.js +++ b/app/screens/notification/index.js @@ -5,10 +5,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {loadFromPushNotification} from 'app/actions/views/root'; -import {getDimensions} from 'app/selectors/device'; import {getChannel} from '@mm-redux/selectors/entities/channels'; -import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences'; import {getUser} from '@mm-redux/selectors/entities/users'; import {getConfig} from '@mm-redux/selectors/entities/general'; @@ -16,7 +15,6 @@ import Notification from './notification'; function mapStateToProps(state, ownProps) { const {data} = ownProps.notification; - const {deviceWidth} = getDimensions(state); let user; if (data.sender_id) { @@ -31,10 +29,8 @@ function mapStateToProps(state, ownProps) { return { config, channel, - deviceWidth, user, teammateNameDisplay: getTeammateNameDisplaySetting(state), - theme: getTheme(state), }; } diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js index 3338e7594..73f129ce1 100644 --- a/app/screens/notification/notification.js +++ b/app/screens/notification/notification.js @@ -4,7 +4,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Image, InteractionManager, Platform, StyleSheet, @@ -12,6 +11,7 @@ import { Text, View, } from 'react-native'; +import FastImage from 'react-native-fast-image'; import {Navigation} from 'react-native-navigation'; import * as Animatable from 'react-native-animatable'; import {PanGestureHandler} from 'react-native-gesture-handler'; @@ -41,10 +41,8 @@ export default class Notification extends PureComponent { componentId: PropTypes.string.isRequired, channel: PropTypes.object, config: PropTypes.object, - deviceWidth: PropTypes.number.isRequired, notification: PropTypes.object.isRequired, teammateNameDisplay: PropTypes.string, - theme: PropTypes.object.isRequired, user: PropTypes.object, }; @@ -158,7 +156,7 @@ export default class Notification extends PureComponent { const {data} = notification; let icon = ( - @@ -168,7 +166,7 @@ export default class Notification extends PureComponent { const overrideIconURL = Client4.getAbsoluteUrl(data.override_icon_url); // eslint-disable-line camelcase const wsIcon = data.override_icon_url ? {uri: overrideIconURL} : webhookIcon; icon = ( - diff --git a/app/screens/table_image/table_image.js b/app/screens/table_image/table_image.js index bc8f64ea3..24429c25f 100644 --- a/app/screens/table_image/table_image.js +++ b/app/screens/table_image/table_image.js @@ -5,11 +5,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import { ActivityIndicator, - Image, ScrollView, StyleSheet, View, } from 'react-native'; +import FastImage from 'react-native-fast-image'; export default class TableImage extends React.PureComponent { static propTypes = { @@ -81,7 +81,7 @@ export default class TableImage extends React.PureComponent { style={style.scrollContainer} contentContainerStyle={style.container} > - diff --git a/app/store/middlewares/helpers.js b/app/store/middlewares/helpers.js index 238a15f68..ca227731f 100644 --- a/app/store/middlewares/helpers.js +++ b/app/store/middlewares/helpers.js @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import DeviceInfo from 'react-native-device-info'; - export function getLastChannelForTeam(payload) { if (payload?.views?.team?.lastChannelForTeam) { const lastChannelForTeam = {...payload.views.team.lastChannelForTeam}; diff --git a/app/store/middlewares/message_retention.js b/app/store/middlewares/message_retention.js index 33b648667..bb4f61d0d 100644 --- a/app/store/middlewares/message_retention.js +++ b/app/store/middlewares/message_retention.js @@ -30,7 +30,6 @@ export default function messageRetention(store) { const build = DeviceInfo.getBuildNumber(); const version = DeviceInfo.getVersion(); const previousVersion = app?.version; - const previousBuild = app?.build; action.payload = { ...action.payload, diff --git a/app/store/middlewares/sentry.js b/app/store/middlewares/sentry.js index d4f21efdb..c092494f9 100644 --- a/app/store/middlewares/sentry.js +++ b/app/store/middlewares/sentry.js @@ -11,14 +11,10 @@ export default function createSentryMiddleware() { Sentry = require('@sentry/react-native'); } - return (store) => { // eslint-disable-line no-unused-vars - return (next) => { - return (action) => { - Sentry.addBreadcrumb(makeBreadcrumbFromAction(action)); + return () => (next) => (action) => { + Sentry.addBreadcrumb(makeBreadcrumbFromAction(action)); - return next(action); - }; - }; + return next(action); }; } diff --git a/app/store/utils.js b/app/store/utils.js index 231444568..b9ce94470 100644 --- a/app/store/utils.js +++ b/app/store/utils.js @@ -48,7 +48,6 @@ export function waitForHydration(store, callback) { let executed = false; // this is to prevent a race condition when subcription runs before unsubscribed let state = store.getState(); let root = state.views?.root; - let persist = state._persist; //eslint-disable-line no-underscore-dangle if (root?.hydrationComplete && !executed) { if (callback && typeof callback === 'function') { @@ -59,7 +58,6 @@ export function waitForHydration(store, callback) { const subscription = () => { state = store.getState(); root = state.views?.root; - persist = state._persist; //eslint-disable-line no-underscore-dangle if (root?.hydrationComplete && !executed) { unsubscribeFromStore(); if (callback && typeof callback === 'function') { diff --git a/app/utils/images.js b/app/utils/images.js index 104779914..493c1c23c 100644 --- a/app/utils/images.js +++ b/app/utils/images.js @@ -1,13 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Keyboard} from 'react-native'; +import {Dimensions, Keyboard} from 'react-native'; +import {showModalOverCurrentContext} from '@actions/navigation'; +import {DeviceTypes} from '@constants'; import { IMAGE_MAX_HEIGHT, IMAGE_MIN_DIMENSION, -} from 'app/constants/image'; -import {showModalOverCurrentContext} from 'app/actions/navigation'; + MAX_GIF_SIZE, + VIEWPORT_IMAGE_OFFSET, + VIEWPORT_IMAGE_REPLY_OFFSET, +} from '@constants/image'; let previewComponents; @@ -58,6 +62,21 @@ export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHe }; }; +export function getViewPortWidth(isReplyPost, permanentSidebar = false) { + const {width, height} = Dimensions.get('window'); + let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET; + + if (permanentSidebar) { + portraitPostWidth -= DeviceTypes.TABLET_WIDTH; + } + + if (isReplyPost) { + portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET; + } + + return portraitPostWidth; +} + export function previewImageAtIndex(components, index, files) { previewComponents = components; const component = components[index]; @@ -94,8 +113,6 @@ function getItemMeasures(index, cb) { }); } -const MAX_GIF_SIZE = 100 * 1024 * 1024; - // 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) { diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index 87a465480..5a04ec772 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -63,7 +63,7 @@ class PushNotificationUtils { }; onPushNotification = async (deviceNotification) => { - const {dispatch, getState} = Store.redux; + const {dispatch} = Store.redux; const {data, foreground, message, userInteraction} = deviceNotification; const notification = { data, diff --git a/babel.config.js b/babel.config.js index a045ed23a..3ae18fa24 100644 --- a/babel.config.js +++ b/babel.config.js @@ -14,6 +14,7 @@ module.exports = { alias: { assets: './dist/assets', '@actions': './app/actions', + '@components': './app/components', '@constants': './app/constants', '@i18n': './app/i18n', '@init': './app/init', diff --git a/index.js b/index.js index e56dba743..89b2d352a 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ if (__DEV__) { 'Warning: componentWillReceiveProps', 'Warning: StatusBarIOS', '`-[RCTRootView cancelTouches]`', + 'Animated', // Hide warnings caused by React Native (https://github.com/facebook/react-native/issues/20841) 'Require cycle: node_modules/react-native/Libraries/Network/fetch.js', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 358d34fbd..d6da1aa67 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -550,7 +550,7 @@ SPEC CHECKSUMS: jail-monkey: d7c5048b2336f22ee9c9e0efa145f1f917338ea9 libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3 MMKV: 7bb6c30f9ff2ea45bc2398c86e66dd1cb63cfe20 - MMKVCore: f1e0aad4fc330e7cbfbdff16720017bf0973db5a + MMKVCore: f455d4bf01bb5257511c1062fdbc3d7ebdec216b Permission-Camera: 8f0e5decca5f28f70f28a8dc31f012c9bad40ad8 Permission-PhotoLibrary: b209bf23b784c9e1409a57d81c6d11ab1d3079c1 RCTRequired: cec6a34b3ac8a9915c37e7e4ad3aa74726ce4035 diff --git a/patches/react-native-fast-image+8.1.5.patch b/patches/react-native-fast-image+8.1.5.patch index ba6b38a40..4ddd9e165 100644 --- a/patches/react-native-fast-image+8.1.5.patch +++ b/patches/react-native-fast-image+8.1.5.patch @@ -1,14 +1,15 @@ diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java new file mode 100644 -index 0000000..f2306f6 +index 0000000..a302394 --- /dev/null +++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageCookieJar.java -@@ -0,0 +1,38 @@ +@@ -0,0 +1,45 @@ +package com.dylanvann.fastimage; + +import android.webkit.CookieManager; + +import java.util.Collections; ++import java.util.LinkedList; +import java.util.List; + +import okhttp3.Cookie; @@ -39,7 +40,13 @@ index 0000000..f2306f6 + return Collections.emptyList(); + } + -+ return Collections.singletonList(Cookie.parse(url, cookie)); ++ String[] pairs = cookie.split(";"); ++ List cookies = new LinkedList(); ++ for (String pair : pairs) { ++ cookies.add(Cookie.parse(url, pair)); ++ } ++ ++ return cookies; + } +} diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java diff --git a/share_extension/android/actions/index.js b/share_extension/android/actions/index.js index a3f9748d7..13a57f995 100644 --- a/share_extension/android/actions/index.js +++ b/share_extension/android/actions/index.js @@ -1,7 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels'; import {getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels'; import {getChannelByName} from '@mm-redux/utils/channel_utils'; diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index 9caf85230..8df471b3d 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -9,7 +9,6 @@ import {intlShape} from 'react-intl'; import { Alert, - Image, NativeModules, PermissionsAndroid, ScrollView, @@ -17,6 +16,7 @@ import { TextInput, View, } from 'react-native'; +import FastImage from 'react-native-fast-image'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import Video from 'react-native-video'; import LocalAuth from 'react-native-local-auth'; @@ -505,7 +505,7 @@ export default class ExtensionPost extends PureComponent { key={`item-${index}`} style={styles.imageContainer} > - { jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); jest.mock('../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter'); +jest.mock('react-native-cookies', () => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + openURL: jest.fn(), + canOpenURL: jest.fn(), + getInitialURL: jest.fn(), + clearAll: jest.fn(), +})); + jest.mock('react-native-device-info', () => { return { getVersion: () => '0.0.0', @@ -120,13 +129,37 @@ jest.mock('react-native-device-info', () => { }; }); -jest.mock('react-native-cookies', () => ({ - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - openURL: jest.fn(), - canOpenURL: jest.fn(), - getInitialURL: jest.fn(), - clearAll: jest.fn(), +jest.mock('react-native-fast-image', () => { + const FastImage = require.requireActual('react-native-fast-image').default; + FastImage.preload = jest.fn(); + + return FastImage; +}); + +jest.mock('rn-fetch-blob', () => ({ + fs: { + dirs: { + DocumentDir: () => jest.fn(), + CacheDir: '/data/com.mattermost.beta/cache', + }, + exists: jest.fn(), + existsWithDiffExt: jest.fn(), + unlink: jest.fn(), + mv: jest.fn(), + }, + fetch: jest.fn(), + config: jest.fn(), +})); + +jest.mock('rn-fetch-blob/fs', () => ({ + dirs: { + DocumentDir: () => jest.fn(), + CacheDir: '/data/com.mattermost.beta/cache', + }, + exists: jest.fn(), + existsWithDiffExt: jest.fn(), + unlink: jest.fn(), + mv: jest.fn(), })); jest.mock('react-native-navigation', () => { @@ -200,32 +233,6 @@ beforeEach(() => { errors = []; }); -jest.mock('rn-fetch-blob', () => ({ - fs: { - dirs: { - DocumentDir: () => jest.fn(), - CacheDir: '/data/com.mattermost.beta/cache', - }, - exists: jest.fn(), - existsWithDiffExt: jest.fn(), - unlink: jest.fn(), - mv: jest.fn(), - }, - fetch: jest.fn(), - config: jest.fn(), -})); - -jest.mock('rn-fetch-blob/fs', () => ({ - dirs: { - DocumentDir: () => jest.fn(), - CacheDir: '/data/com.mattermost.beta/cache', - }, - exists: jest.fn(), - existsWithDiffExt: jest.fn(), - unlink: jest.fn(), - mv: jest.fn(), -})); - global.requestAnimationFrame = (callback) => { setTimeout(callback, 0); };