diff --git a/webapp/channels/src/components/__snapshots__/size_aware_image.test.jsx.snap b/webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap similarity index 95% rename from webapp/channels/src/components/__snapshots__/size_aware_image.test.jsx.snap rename to webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap index 3c26233b8e..3066414075 100644 --- a/webapp/channels/src/components/__snapshots__/size_aware_image.test.jsx.snap +++ b/webapp/channels/src/components/__snapshots__/size_aware_image.test.tsx.snap @@ -41,10 +41,10 @@ exports[`components/SizeAwareImage should load download and copy link buttons wh onKeyDown={[Function]} onLoad={[Function]} src="https://example.com/image.png" - tabIndex="0" + tabIndex={0} /> { return index > 0 ? url.substring(index + 1) : null; }; - showModal = (e: MouseEvent, link: string) => { + showModal = (e: KeyboardEvent | MouseEvent, link = '') => { const extension = this.getFileExtensionFromUrl(link); if (!this.props.imageIsLink && extension) { diff --git a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.test.tsx b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.test.tsx index e44ecf16be..85ef78651f 100644 --- a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.test.tsx +++ b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.test.tsx @@ -114,7 +114,7 @@ describe('components/post_view/MessageAttachment', () => { , ); - wrapper.instance().showModal({preventDefault: () => {}}, 'https://example.com/image.png'); + wrapper.instance().showModal({preventDefault: () => {}} as unknown as React.KeyboardEvent | React.MouseEvent, 'https://example.com/image.png'); expect(props.actions.openModal).toHaveBeenCalledTimes(1); }); diff --git a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx index 39d69e962b..bb5041083b 100644 --- a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx +++ b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx @@ -3,7 +3,7 @@ import truncate from 'lodash/truncate'; import React from 'react'; -import type {CSSProperties} from 'react'; +import type {KeyboardEvent, MouseEvent, CSSProperties} from 'react'; import type {PostAction, PostActionOption} from '@mattermost/types/integration_actions'; import type { @@ -312,14 +312,14 @@ export default class MessageAttachment extends React.PureComponent ); }; - handleFormattedTextClick = (e: React.MouseEvent) => Utils.handleFormattedTextClick(e, this.props.currentRelativeTeamUrl); + handleFormattedTextClick = (e: MouseEvent) => Utils.handleFormattedTextClick(e, this.props.currentRelativeTeamUrl); getFileExtensionFromUrl = (url: string) => { const index = url.lastIndexOf('.'); return index > 0 ? url.substring(index + 1) : null; }; - showModal = (e: {preventDefault: () => void}, link: string) => { + showModal = (e: (KeyboardEvent | MouseEvent), link = '') => { e.preventDefault(); const extension = this.getFileExtensionFromUrl(link); diff --git a/webapp/channels/src/components/post_view/post_image/post_image.tsx b/webapp/channels/src/components/post_view/post_image/post_image.tsx index c784c4a56c..17c1791837 100644 --- a/webapp/channels/src/components/post_view/post_image/post_image.tsx +++ b/webapp/channels/src/components/post_view/post_image/post_image.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import type {KeyboardEvent, MouseEvent} from 'react'; import type {Post, PostImage as PostImageMetadata} from '@mattermost/types/posts'; @@ -23,7 +24,7 @@ interface Props { } export default class PostImage extends React.PureComponent { - showModal = (e: React.MouseEvent, link: string) => { + showModal = (e: (KeyboardEvent | MouseEvent), link = '') => { e.preventDefault(); this.props.actions.openModal({ diff --git a/webapp/channels/src/components/single_image_view/single_image_view.test.tsx b/webapp/channels/src/components/single_image_view/single_image_view.test.tsx index 6cff7f8482..f1cb54aa84 100644 --- a/webapp/channels/src/components/single_image_view/single_image_view.test.tsx +++ b/webapp/channels/src/components/single_image_view/single_image_view.test.tsx @@ -85,7 +85,7 @@ describe('components/SingleImageView', () => { , ); expect(wrapper.state('loaded')).toEqual(false); - wrapper.find(SizeAwareImage).prop('onImageLoaded')(); + wrapper.find(SizeAwareImage).prop('onImageLoaded')?.({height: 0, width: 0}); expect(wrapper.state('loaded')).toEqual(true); expect(wrapper).toMatchSnapshot(); }); diff --git a/webapp/channels/src/components/single_image_view/single_image_view.tsx b/webapp/channels/src/components/single_image_view/single_image_view.tsx index 1fd5f064fa..8ee64d9787 100644 --- a/webapp/channels/src/components/single_image_view/single_image_view.tsx +++ b/webapp/channels/src/components/single_image_view/single_image_view.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import React from 'react'; +import type {KeyboardEvent, MouseEvent} from 'react'; import type {FileInfo} from '@mattermost/types/files'; @@ -82,7 +83,7 @@ export default class SingleImageView extends React.PureComponent { } }; - handleImageClick = (e: React.MouseEvent) => { + handleImageClick = (e: (KeyboardEvent | MouseEvent)) => { e.preventDefault(); this.props.actions.openModal({ diff --git a/webapp/channels/src/components/size_aware_image.test.jsx b/webapp/channels/src/components/size_aware_image.test.tsx similarity index 91% rename from webapp/channels/src/components/size_aware_image.test.jsx rename to webapp/channels/src/components/size_aware_image.test.tsx index ec6f02cc22..9d7861b730 100644 --- a/webapp/channels/src/components/size_aware_image.test.jsx +++ b/webapp/channels/src/components/size_aware_image.test.tsx @@ -7,11 +7,13 @@ import {Provider} from 'react-redux'; import LoadingImagePreview from 'components/loading_image_preview'; import SizeAwareImage from 'components/size_aware_image'; +import type {Props} from 'components/size_aware_image'; import mockStore from 'tests/test_store'; +import {TestHelper} from 'utils/test_helper'; describe('components/SizeAwareImage', () => { - const baseProps = { + const baseProps: Props = { dimensions: { height: 200, width: 300, @@ -21,9 +23,9 @@ describe('components/SizeAwareImage', () => { getFilePublicLink: jest.fn().mockReturnValue(Promise.resolve({data: {link: 'https://example.com/image.png'}})), src: 'https://example.com/image.png', className: 'class', - fileInfo: { + fileInfo: TestHelper.getFileInfoMock({ name: 'photo-1533709752211-118fcaf03312', - }, + }), enablePublicLink: true, }; @@ -69,11 +71,11 @@ describe('components/SizeAwareImage', () => { test('should render a mini preview when showLoader is true and preview is set', () => { const props = { ...baseProps, - fileInfo: { + fileInfo: TestHelper.getFileInfoMock({ ...baseProps.fileInfo, mime_type: 'mime_type', mini_preview: 'mini_preview', - }, + }), }; const wrapper = mount(); @@ -108,17 +110,22 @@ describe('components/SizeAwareImage', () => { const height = 123; const width = 1234; - const wrapper = shallow(); + const wrapper = shallow(); - wrapper.find('img').prop('onLoad')({target: {naturalHeight: height, naturalWidth: width}}); + wrapper.find('img')?.prop('onLoad')?.({target: {naturalHeight: height, naturalWidth: width}} as unknown as React.SyntheticEvent); expect(wrapper.state('loaded')).toBe(true); expect(baseProps.onImageLoaded).toHaveBeenCalledWith({height, width}); }); test('should call onImageLoadFail when image load fails and should have svg', () => { const wrapper = mount(); - - wrapper.find(SizeAwareImage).find('img').prop('onError')(); + const errorEvent = { + target: {}, + currentTarget: {}, + preventDefault: () => {}, + stopPropagation: () => {}, + } as React.SyntheticEvent; + wrapper.find(SizeAwareImage).find('img').prop('onError')?.(errorEvent); expect(wrapper.find(SizeAwareImage).state('error')).toBe(true); expect(wrapper.find(SizeAwareImage).find('svg').exists()).toEqual(true); @@ -147,7 +154,7 @@ describe('components/SizeAwareImage', () => { expect(wrapper.find('div.small-image__container').exists()).toEqual(true); expect(wrapper.find('div.small-image__container').prop('className')). - toEqual('small-image__container cursor--pointer a11y--active'); + toEqual('small-image__container cursor--pointer a11y--active small-image__container--min-width'); }); test('should properly set container div width', () => { diff --git a/webapp/channels/src/components/size_aware_image.jsx b/webapp/channels/src/components/size_aware_image.tsx similarity index 81% rename from webapp/channels/src/components/size_aware_image.jsx rename to webapp/channels/src/components/size_aware_image.tsx index e0b7b8063b..99ea7415c4 100644 --- a/webapp/channels/src/components/size_aware_image.jsx +++ b/webapp/channels/src/components/size_aware_image.tsx @@ -4,12 +4,15 @@ /* eslint-disable @mattermost/use-external-link */ import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; +import type {KeyboardEvent, MouseEvent, SyntheticEvent} from 'react'; import {FormattedMessage} from 'react-intl'; import {DownloadOutlineIcon, LinkVariantIcon, CheckIcon} from '@mattermost/compass-icons/components'; +import type {FileInfo} from '@mattermost/types/files'; +import type {PostImage} from '@mattermost/types/posts'; +import type {ActionFunc} from 'mattermost-redux/types/actions'; import {getFileMiniPreviewUrl} from 'mattermost-redux/utils/file_utils'; import LoadingImagePreview from 'components/loading_image_preview'; @@ -23,83 +26,103 @@ const MIN_IMAGE_SIZE = 48; const MIN_IMAGE_SIZE_FOR_INTERNAL_BUTTONS = 100; const MAX_IMAGE_HEIGHT = 350; +export type Props = { + + /* + * The source URL of the image + */ + src: string; + + /* + * dimensions object to create empty space required to prevent scroll pop + */ + dimensions?: Partial; + fileInfo?: FileInfo; + + /** + * fileURL of the original image + */ + fileURL?: string; + + alt?: string; + height?: string; + width?: string; + title?: string; + + /* + * Boolean value to pass for showing a loader when image is being loaded + */ + showLoader?: boolean; + + /* + * A callback that is called as soon as the image component has a height value + */ + onImageLoaded?: ({height, width}: {height: number; width: number}) => void; + + /* + * A callback that is called when image load fails + */ + onImageLoadFail?: () => void; + + /* + * Fetch the onClick function + */ + onClick?: (e: (KeyboardEvent | MouseEvent), link?: string) => void; + + /* + * css classes that can added to the img as well as parent div on svg for placeholder + */ + className?: string; + + /* + * Enables the logic of surrounding small images with a bigger container div for better click/tap targeting + */ + handleSmallImageContainer?: boolean; + + /** + * Enables copy URL functionality through a button on image hover. + */ + enablePublicLink?: boolean; + + /** + * Action to fetch public link of an image from server. + */ + getFilePublicLink?: () => ActionFunc; + + /* + * Prevents display of utility buttons when image in a location that makes them inappropriate + */ + hideUtilities?: boolean; +} + +type State = { + loaded: boolean; + isSmallImage: boolean; + linkCopiedRecently: boolean; + linkCopyInProgress: boolean; + error: boolean; + imageWidth: number; +} + // SizeAwareImage is a component used for rendering images where the dimensions of the image are important for // ensuring that the page is laid out correctly. -export default class SizeAwareImage extends React.PureComponent { - static propTypes = { +export default class SizeAwareImage extends React.PureComponent { + public heightTimeout = 0; + public mounted = false; + public timeout: NodeJS.Timeout|null = null; - /* - * The source URL of the image - */ - src: PropTypes.string.isRequired, - - /* - * dimensions object to create empty space required to prevent scroll pop - */ - dimensions: PropTypes.object, - fileInfo: PropTypes.object, - - /** - * fileURL of the original image - */ - fileURL: PropTypes.string, - - /* - * Boolean value to pass for showing a loader when image is being loaded - */ - showLoader: PropTypes.bool, - - /* - * A callback that is called as soon as the image component has a height value - */ - onImageLoaded: PropTypes.func, - - /* - * A callback that is called when image load fails - */ - onImageLoadFail: PropTypes.func, - - /* - * Fetch the onClick function - */ - onClick: PropTypes.func, - - /* - * css classes that can added to the img as well as parent div on svg for placeholder - */ - className: PropTypes.string, - - /* - * Enables the logic of surrounding small images with a bigger container div for better click/tap targeting - */ - handleSmallImageContainer: PropTypes.bool, - - /** - * Enables copy URL functionality through a button on image hover. - */ - enablePublicLink: PropTypes.bool, - - /** - * Action to fetch public link of an image from server. - */ - getFilePublicLink: PropTypes.func, - - /* - * Prevents display of utility buttons when image in a location that makes them inappropriate - */ - hideUtilities: PropTypes.bool, - }; - - constructor(props) { + constructor(props: Props) { super(props); const {dimensions} = props; this.state = { loaded: false, isSmallImage: this.dimensionsAvailable(dimensions) ? this.isSmallImage( - dimensions.width, dimensions.height) : false, + dimensions?.width ?? 0, dimensions?.height ?? 0) : false, linkCopiedRecently: false, linkCopyInProgress: false, + error: false, + imageWidth: 0, }; this.heightTimeout = 0; @@ -113,17 +136,17 @@ export default class SizeAwareImage extends React.PureComponent { this.mounted = false; } - dimensionsAvailable = (dimensions) => { + dimensionsAvailable = (dimensions?: Partial) => { return dimensions && dimensions.width && dimensions.height; }; - isSmallImage = (width, height) => { + isSmallImage = (width: number, height: number) => { return width < MIN_IMAGE_SIZE || height < MIN_IMAGE_SIZE; }; - handleLoad = (event) => { + handleLoad = (event: SyntheticEvent) => { if (this.mounted) { - const image = event.target; + const image = event.target as HTMLImageElement; const isSmallImage = this.isSmallImage(image.naturalWidth, image.naturalHeight); this.setState({ loaded: true, @@ -147,13 +170,13 @@ export default class SizeAwareImage extends React.PureComponent { } }; - handleImageClick = (e) => { + handleImageClick = (e: MouseEvent) => { this.props.onClick?.(e, this.props.src); }; - onEnterKeyDown = (e) => { + onEnterKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { - this.handleImageClick(e); + this.props.onClick?.(e, this.props.src); } }; @@ -197,7 +220,7 @@ export default class SizeAwareImage extends React.PureComponent { MAX_IMAGE_HEIGHT ? MAX_IMAGE_HEIGHT / dimensions.height : 1; - const height = dimensions.height * ratio; - const width = dimensions.width * ratio; + const ratio = (dimensions?.height ?? 0) > MAX_IMAGE_HEIGHT ? MAX_IMAGE_HEIGHT / (dimensions?.height ?? 1) : 1; + const height = (dimensions?.height ?? 0) * ratio; + const width = (dimensions?.width ?? 0) * ratio; const miniPreview = getFileMiniPreviewUrl(fileInfo); @@ -397,13 +420,13 @@ export default class SizeAwareImage extends React.PureComponent { fallback = (
@@ -454,7 +477,7 @@ export default class SizeAwareImage extends React.PureComponent { }, 1500); }; - copyLinkToAsset = () => { + copyLinkToAsset = async () => { // if linkCopyInProgress is true return if (this.state.linkCopyInProgress !== true) { // set linkCopyInProgress to true to prevent multiple api calls @@ -468,11 +491,12 @@ export default class SizeAwareImage extends React.PureComponent { } // copying public link to clipboard - this.props.getFilePublicLink().then((data) => { - const fileURL = data.data.link; + if (this.props.getFilePublicLink) { + const data: any = await this.props.getFilePublicLink(); + const fileURL = data.data?.link; copyToClipboard(fileURL ?? ''); this.startCopyTimer(); - }); + } } };