parent
592b3805f1
commit
438edd96f6
12 changed files with 1108 additions and 39 deletions
|
|
@ -94,7 +94,7 @@ const File = ({
|
|||
} else {
|
||||
onPress(index);
|
||||
}
|
||||
}, [index]);
|
||||
}, [index, onPress]);
|
||||
|
||||
const {styles, onGestureEvent, ref} = useGalleryItem(galleryIdentifier, index, handlePreviewPress);
|
||||
|
||||
|
|
|
|||
616
app/components/files/files.test.tsx
Normal file
616
app/components/files/files.test.tsx
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {render, fireEvent} from '@testing-library/react-native';
|
||||
import React, {useMemo, type ComponentProps} from 'react';
|
||||
import {DeviceEventEmitter, Text, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import {Events} from '@constants';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useImageAttachments} from '@hooks/files';
|
||||
import {mockFileInfo} from '@test/api_mocks/file';
|
||||
import {isImage, isVideo} from '@utils/file';
|
||||
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
|
||||
import {getViewPortWidth} from '@utils/images';
|
||||
|
||||
import File from './file';
|
||||
import Files from './files';
|
||||
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
|
||||
jest.mock('@hooks/device', () => ({
|
||||
useIsTablet: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('@hooks/files', () => ({
|
||||
useImageAttachments: jest.fn().mockReturnValue({
|
||||
images: [],
|
||||
nonImages: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@hooks/utils', () => ({
|
||||
usePreventDoubleTap: (fn: () => void) => fn,
|
||||
}));
|
||||
|
||||
jest.mock('@utils/file', () => ({
|
||||
isImage: jest.fn().mockReturnValue(true),
|
||||
isVideo: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('@utils/gallery', () => ({
|
||||
fileToGalleryItem: jest.fn(),
|
||||
openGalleryAtIndex: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@utils/images', () => ({
|
||||
getViewPortWidth: jest.fn().mockReturnValue(300),
|
||||
}));
|
||||
|
||||
jest.mock('./file', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mocked(File).mockImplementation((props) => (
|
||||
<View testID={props.file.id}>
|
||||
<Text testID={`${props.file.id}-galleryIdentifier`}>{props.galleryIdentifier}</Text>
|
||||
<Text testID={`${props.file.id}-canDownloadFiles`}>{String(props.canDownloadFiles)}</Text>
|
||||
<Text testID={`${props.file.id}-index`}>{props.index}</Text>
|
||||
<Text testID={`${props.file.id}-isSingleImage`}>{String(props.isSingleImage)}</Text>
|
||||
<Text testID={`${props.file.id}-nonVisibleImagesCount`}>{String(props.nonVisibleImagesCount)}</Text>
|
||||
<Text testID={`${props.file.id}-publicLinkEnabled`}>{String(props.publicLinkEnabled)}</Text>
|
||||
<Text testID={`${props.file.id}-wrapperWidth`}>{props.wrapperWidth}</Text>
|
||||
<Text testID={`${props.file.id}-inViewPort`}>{String(props.inViewPort)}</Text>
|
||||
<TouchableOpacity
|
||||
testID={`${props.file.id}-onPress`}
|
||||
onPress={() => props.onPress(props.index)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID={`${props.file.id}-updateFileForGallery`}
|
||||
onPress={() => props.updateFileForGallery(props.index, {...props.file, uri: 'updated'})}
|
||||
/>
|
||||
</View>
|
||||
));
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof Files> {
|
||||
return {
|
||||
canDownloadFiles: true,
|
||||
failed: false,
|
||||
filesInfo: [],
|
||||
isReplyPost: false,
|
||||
layoutWidth: 300,
|
||||
location: 'test-location',
|
||||
postId: 'test-post-id',
|
||||
postProps: {},
|
||||
publicLinkEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Files', () => {
|
||||
it('should render attachments, with images in the image row', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
mockFileInfo({id: '3'}),
|
||||
mockFileInfo({id: '4'}),
|
||||
];
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1' || f.id === '2'),
|
||||
nonImages: fi.filter((f) => f.id === '3' || f.id === '4'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const {getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('1')).toBeVisible();
|
||||
expect(getByTestId('2')).toBeVisible();
|
||||
expect(getByTestId('3')).toBeVisible();
|
||||
expect(getByTestId('4')).toBeVisible();
|
||||
expect(getByTestId('image-row')).toContainElement(getByTestId('1'));
|
||||
expect(getByTestId('image-row')).toContainElement(getByTestId('2'));
|
||||
expect(getByTestId('image-row')).not.toContainElement(getByTestId('3'));
|
||||
expect(getByTestId('image-row')).not.toContainElement(getByTestId('4'));
|
||||
});
|
||||
|
||||
it('should not show the image row if no images', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '3'}),
|
||||
mockFileInfo({id: '4'}),
|
||||
];
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1' || f.id === '2'),
|
||||
nonImages: fi.filter((f) => f.id === '3' || f.id === '4'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const {getByTestId, queryByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('3')).toBeVisible();
|
||||
expect(getByTestId('4')).toBeVisible();
|
||||
expect(queryByTestId('image-row')).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should have different opacity if failed', () => {
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.failed = true;
|
||||
|
||||
const {getByTestId, rerender} = render(
|
||||
<Files {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('files-container')).toHaveStyle({opacity: 0.5});
|
||||
|
||||
baseProps.failed = false;
|
||||
rerender(<Files {...baseProps}/>);
|
||||
|
||||
expect(getByTestId('files-container')).not.toHaveStyle({opacity: 0.5});
|
||||
});
|
||||
|
||||
it('should drill all relevant props', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.filesInfo = filesInfo;
|
||||
baseProps.canDownloadFiles = false;
|
||||
baseProps.publicLinkEnabled = false;
|
||||
|
||||
const {getByTestId, rerender} = render(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('false');
|
||||
expect(getByTestId('1-publicLinkEnabled')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-publicLinkEnabled')).toHaveTextContent('false');
|
||||
|
||||
baseProps.canDownloadFiles = true;
|
||||
baseProps.publicLinkEnabled = true;
|
||||
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('true');
|
||||
expect(getByTestId('1-publicLinkEnabled')).toHaveTextContent('true');
|
||||
expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('true');
|
||||
expect(getByTestId('2-publicLinkEnabled')).toHaveTextContent('true');
|
||||
|
||||
baseProps.canDownloadFiles = true;
|
||||
baseProps.publicLinkEnabled = false;
|
||||
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('true');
|
||||
expect(getByTestId('1-publicLinkEnabled')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('true');
|
||||
expect(getByTestId('2-publicLinkEnabled')).toHaveTextContent('false');
|
||||
|
||||
baseProps.canDownloadFiles = false;
|
||||
baseProps.publicLinkEnabled = true;
|
||||
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('false');
|
||||
expect(getByTestId('1-publicLinkEnabled')).toHaveTextContent('true');
|
||||
expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-publicLinkEnabled')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('should set layoutWidth if provided', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.filesInfo = filesInfo;
|
||||
baseProps.layoutWidth = 400;
|
||||
|
||||
const {getByTestId, rerender} = render(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-wrapperWidth')).toHaveTextContent('400');
|
||||
expect(getByTestId('2-wrapperWidth')).toHaveTextContent('400');
|
||||
expect(getByTestId('image-row')).toHaveStyle({width: 400});
|
||||
|
||||
baseProps.layoutWidth = 500;
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-wrapperWidth')).toHaveTextContent('500');
|
||||
expect(getByTestId('2-wrapperWidth')).toHaveTextContent('500');
|
||||
expect(getByTestId('image-row')).toHaveStyle({width: 500});
|
||||
});
|
||||
|
||||
it('should use ((getViewportWidth result) - 6) if layoutWidth is not provided', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
jest.mocked(getViewPortWidth).mockReturnValue(300);
|
||||
jest.mocked(useIsTablet).mockReturnValue(false);
|
||||
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.filesInfo = filesInfo;
|
||||
baseProps.layoutWidth = undefined;
|
||||
baseProps.isReplyPost = false;
|
||||
|
||||
const {getByTestId, rerender} = render(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-wrapperWidth')).toHaveTextContent('294');
|
||||
expect(getByTestId('2-wrapperWidth')).toHaveTextContent('294');
|
||||
expect(getByTestId('image-row')).toHaveStyle({width: 294});
|
||||
expect(getViewPortWidth).toHaveBeenCalledWith(false, false);
|
||||
|
||||
jest.mocked(getViewPortWidth).mockReturnValue(400);
|
||||
jest.mocked(useIsTablet).mockReturnValue(true);
|
||||
baseProps.isReplyPost = true;
|
||||
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-wrapperWidth')).toHaveTextContent('394');
|
||||
expect(getByTestId('2-wrapperWidth')).toHaveTextContent('394');
|
||||
expect(getByTestId('image-row')).toHaveStyle({width: 394});
|
||||
expect(getViewPortWidth).toHaveBeenCalledWith(true, true);
|
||||
});
|
||||
|
||||
it('calling onPress on the child should open gallery', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
jest.mocked(fileToGalleryItem).mockImplementation((file) => ({
|
||||
height: 100,
|
||||
id: file.id || '',
|
||||
lastPictureUpdate: 0,
|
||||
mime_type: file.mime_type,
|
||||
name: file.name,
|
||||
type: 'image',
|
||||
uri: file.uri || '',
|
||||
width: file.width,
|
||||
}));
|
||||
|
||||
const {getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('1-onPress'));
|
||||
expect(openGalleryAtIndex).toHaveBeenCalledWith(
|
||||
'test-post-id-fileAttachments-test-location',
|
||||
0,
|
||||
expect.arrayContaining([expect.objectContaining({id: '1'}), expect.objectContaining({id: '2'})]),
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('2-onPress'));
|
||||
expect(openGalleryAtIndex).toHaveBeenCalledWith(
|
||||
'test-post-id-fileAttachments-test-location',
|
||||
1,
|
||||
expect.arrayContaining([expect.objectContaining({id: '1'}), expect.objectContaining({id: '2'})]),
|
||||
);
|
||||
});
|
||||
|
||||
it('calling updateFileForGallery updates the file', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', uri: 'original'}),
|
||||
mockFileInfo({id: '2', uri: 'original'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
jest.mocked(fileToGalleryItem).mockImplementation((file) => ({
|
||||
height: 100,
|
||||
id: file.id || '',
|
||||
lastPictureUpdate: 0,
|
||||
mime_type: file.mime_type,
|
||||
name: file.name,
|
||||
type: 'image',
|
||||
uri: file.uri || '',
|
||||
width: file.width,
|
||||
}));
|
||||
|
||||
const {getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('1-updateFileForGallery'));
|
||||
fireEvent.press(getByTestId('1-onPress'));
|
||||
expect(openGalleryAtIndex).toHaveBeenCalledWith(
|
||||
'test-post-id-fileAttachments-test-location',
|
||||
0,
|
||||
expect.arrayContaining([expect.objectContaining({id: '1', uri: 'updated'}), expect.objectContaining({id: '2', uri: 'original'})]),
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('2-onPress'));
|
||||
expect(openGalleryAtIndex).toHaveBeenCalledWith(
|
||||
'test-post-id-fileAttachments-test-location',
|
||||
1,
|
||||
expect.arrayContaining([expect.objectContaining({id: '1', uri: 'updated'}), expect.objectContaining({id: '2', uri: 'original'})]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update files for gallery on new props', () => {
|
||||
const {rerender, getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={[]}
|
||||
/>,
|
||||
);
|
||||
const newFilesInfo = [
|
||||
mockFileInfo({id: '1', name: 'image1.png', user_id: 'user1'}),
|
||||
];
|
||||
rerender(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={newFilesInfo}
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('1')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should set inViewPort to true on ITEM_IN_VIEWPORT event', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', name: 'image1.png', user_id: 'user1'}),
|
||||
mockFileInfo({id: '2', name: 'image2.png', user_id: 'user2'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const {getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'unrelated-event': true});
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'test-location-test-post-id': true});
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('true');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('should ignore ITEM_IN_VIEWPORT event if not for the current post or location', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', name: 'image1.png', user_id: 'user1'}),
|
||||
mockFileInfo({id: '2', name: 'image2.png', user_id: 'user2'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1'),
|
||||
nonImages: fi.filter((f) => f.id === '2'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const baseProps = getBaseProps();
|
||||
baseProps.filesInfo = filesInfo;
|
||||
baseProps.location = 'location1';
|
||||
baseProps.postId = 'post1';
|
||||
|
||||
const {getByTestId, rerender} = render(<Files {...baseProps}/>);
|
||||
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
|
||||
baseProps.location = 'location2';
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'location1-post1': true});
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
|
||||
baseProps.postId = 'post2';
|
||||
rerender(<Files {...baseProps}/>);
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'location2-post1': true});
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('false');
|
||||
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'location2-post2': true});
|
||||
expect(getByTestId('1-inViewPort')).toHaveTextContent('true');
|
||||
expect(getByTestId('2-inViewPort')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('should pass isSingleImage to the children', () => {
|
||||
let filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '3'}),
|
||||
mockFileInfo({id: '4'}),
|
||||
];
|
||||
|
||||
const selectImages = (f: FileInfo | FileModel | undefined) => f?.id === '1' || f?.id === '2';
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => selectImages(f)),
|
||||
nonImages: fi.filter((f) => !selectImages(f)),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
jest.mocked(isImage).mockImplementation((f) => selectImages(f));
|
||||
jest.mocked(isVideo).mockReturnValue(false);
|
||||
|
||||
const {rerender, getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-isSingleImage')).toHaveTextContent('true');
|
||||
expect(getByTestId('3-isSingleImage')).toHaveTextContent('true');
|
||||
expect(getByTestId('4-isSingleImage')).toHaveTextContent('true');
|
||||
|
||||
jest.mocked(isImage).mockReturnValue(false);
|
||||
jest.mocked(isVideo).mockImplementation((f) => selectImages(f));
|
||||
rerender(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-isSingleImage')).toHaveTextContent('true');
|
||||
expect(getByTestId('3-isSingleImage')).toHaveTextContent('true');
|
||||
expect(getByTestId('4-isSingleImage')).toHaveTextContent('true');
|
||||
|
||||
filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
mockFileInfo({id: '3'}),
|
||||
mockFileInfo({id: '4'}),
|
||||
];
|
||||
|
||||
jest.mocked(isImage).mockImplementation((f) => selectImages(f));
|
||||
jest.mocked(isVideo).mockReturnValue(false);
|
||||
|
||||
rerender(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-isSingleImage')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-isSingleImage')).toHaveTextContent('false');
|
||||
expect(getByTestId('3-isSingleImage')).toHaveTextContent('false');
|
||||
expect(getByTestId('4-isSingleImage')).toHaveTextContent('false');
|
||||
|
||||
jest.mocked(isImage).mockReturnValue(false);
|
||||
jest.mocked(isVideo).mockImplementation((f) => selectImages(f));
|
||||
rerender(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-isSingleImage')).toHaveTextContent('false');
|
||||
expect(getByTestId('2-isSingleImage')).toHaveTextContent('false');
|
||||
expect(getByTestId('3-isSingleImage')).toHaveTextContent('false');
|
||||
expect(getByTestId('4-isSingleImage')).toHaveTextContent('false');
|
||||
});
|
||||
|
||||
it('should trim more than 4 images and properly add the non visible images count to the last image', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
mockFileInfo({id: '3'}),
|
||||
mockFileInfo({id: '4'}),
|
||||
mockFileInfo({id: '5'}),
|
||||
mockFileInfo({id: '6'}),
|
||||
mockFileInfo({id: '7'}),
|
||||
mockFileInfo({id: '8'}),
|
||||
mockFileInfo({id: '9'}),
|
||||
mockFileInfo({id: '10'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1' || f.id === '2' || f.id === '3' || f.id === '4' || f.id === '5'),
|
||||
nonImages: fi.filter((f) => f.id === '6' || f.id === '7' || f.id === '8' || f.id === '9' || f.id === '10'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const {getByTestId, queryByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('2-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('3-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('4-nonVisibleImagesCount')).toHaveTextContent('1');
|
||||
expect(queryByTestId('5-nonVisibleImagesCount')).not.toBeVisible();
|
||||
expect(getByTestId('6-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('7-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('8-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('9-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
expect(getByTestId('10-nonVisibleImagesCount')).toHaveTextContent('undefined');
|
||||
});
|
||||
|
||||
it('should add gutter to the container of to all elements but the first only on image row', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1'}),
|
||||
mockFileInfo({id: '2'}),
|
||||
mockFileInfo({id: '3'}),
|
||||
mockFileInfo({id: '4'}),
|
||||
mockFileInfo({id: '5'}),
|
||||
mockFileInfo({id: '6'}),
|
||||
];
|
||||
|
||||
jest.mocked(useImageAttachments).mockImplementation((fi) => {
|
||||
return useMemo(() => ({
|
||||
images: fi.filter((f) => f.id === '1' || f.id === '2' || f.id === '3'),
|
||||
nonImages: fi.filter((f) => f.id === '4' || f.id === '5' || f.id === '6'),
|
||||
}), [fi]);
|
||||
});
|
||||
|
||||
const {getByTestId} = render(
|
||||
<Files
|
||||
{...getBaseProps()}
|
||||
filesInfo={filesInfo}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('1-file-container')).not.toHaveStyle({marginLeft: 8});
|
||||
expect(getByTestId('2-file-container')).toHaveStyle({marginLeft: 8});
|
||||
expect(getByTestId('3-file-container')).toHaveStyle({marginLeft: 8});
|
||||
expect(getByTestId('4-file-container')).not.toHaveStyle({marginLeft: 8});
|
||||
expect(getByTestId('5-file-container')).not.toHaveStyle({marginLeft: 8});
|
||||
expect(getByTestId('6-file-container')).not.toHaveStyle({marginLeft: 8});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {DeviceEventEmitter, type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
|
|
@ -9,10 +9,10 @@ import {Events} from '@constants';
|
|||
import {GalleryInit} from '@context/gallery';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useImageAttachments} from '@hooks/files';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import {isImage, isVideo} from '@utils/file';
|
||||
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
|
||||
import {getViewPortWidth} from '@utils/images';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import File from './file';
|
||||
|
||||
|
|
@ -49,29 +49,38 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, location, postId, postProps, publicLinkEnabled}: FilesProps) => {
|
||||
const Files = ({
|
||||
canDownloadFiles,
|
||||
failed,
|
||||
filesInfo,
|
||||
isReplyPost,
|
||||
layoutWidth,
|
||||
location,
|
||||
postId,
|
||||
postProps,
|
||||
publicLinkEnabled,
|
||||
}: FilesProps) => {
|
||||
const galleryIdentifier = `${postId}-fileAttachments-${location}`;
|
||||
const [inViewPort, setInViewPort] = useState(false);
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const {images: imageAttachments, nonImages: nonImageAttachments} = useImageAttachments(filesInfo, publicLinkEnabled);
|
||||
|
||||
const filesForGallery = useMemo(() => imageAttachments.concat(nonImageAttachments),
|
||||
[imageAttachments, nonImageAttachments]);
|
||||
const {images: imageAttachments, nonImages: nonImageAttachments} = useImageAttachments(filesInfo);
|
||||
const [filesForGallery, setFilesForGallery] = useState(() => [...imageAttachments, ...nonImageAttachments]);
|
||||
|
||||
const attachmentIndex = (fileId: string) => {
|
||||
return filesForGallery.findIndex((file) => file.id === fileId) || 0;
|
||||
};
|
||||
|
||||
const handlePreviewPress = preventDoubleTap((idx: number) => {
|
||||
const handlePreviewPress = usePreventDoubleTap(useCallback((idx: number) => {
|
||||
const items = filesForGallery.map((f) => fileToGalleryItem(f, f.user_id, postProps));
|
||||
openGalleryAtIndex(galleryIdentifier, idx, items);
|
||||
});
|
||||
}, [filesForGallery, galleryIdentifier, postProps]));
|
||||
|
||||
const updateFileForGallery = (idx: number, file: FileInfo) => {
|
||||
'worklet';
|
||||
filesForGallery[idx] = file;
|
||||
};
|
||||
const updateFileForGallery = useCallback((idx: number, file: FileInfo) => {
|
||||
const newFilesForGallery = [...filesForGallery];
|
||||
newFilesForGallery[idx] = file;
|
||||
setFilesForGallery(newFilesForGallery);
|
||||
}, [filesForGallery]);
|
||||
|
||||
const isSingleImage = useMemo(() => filesInfo.filter((f) => isImage(f) || isVideo(f)).length === 1, [filesInfo]);
|
||||
|
||||
|
|
@ -79,6 +88,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
let nonVisibleImagesCount: number;
|
||||
let container: StyleProp<ViewStyle> = items.length > 1 ? styles.container : undefined;
|
||||
const containerWithGutter = [container, styles.gutter];
|
||||
const wrapperWidth = getViewPortWidth(isReplyPost, isTablet) - 6;
|
||||
|
||||
return items.map((file, idx) => {
|
||||
if (moreImagesCount && idx === MAX_VISIBLE_ROW_IMAGES - 1) {
|
||||
|
|
@ -91,6 +101,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
return (
|
||||
<View
|
||||
style={[container, styles.marginTop]}
|
||||
testID={`${file.id}-file-container`}
|
||||
key={file.id}
|
||||
>
|
||||
<File
|
||||
|
|
@ -104,7 +115,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
updateFileForGallery={updateFileForGallery}
|
||||
wrapperWidth={layoutWidth || (getViewPortWidth(isReplyPost, isTablet) - 6)}
|
||||
wrapperWidth={layoutWidth || wrapperWidth}
|
||||
inViewPort={inViewPort}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -126,7 +137,10 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.row, {width: portraitPostWidth}]}>
|
||||
<View
|
||||
style={[styles.row, {width: portraitPostWidth}]}
|
||||
testID='image-row'
|
||||
>
|
||||
{ renderItems(visibleImages, nonVisibleImagesCount, true) }
|
||||
</View>
|
||||
);
|
||||
|
|
@ -140,11 +154,18 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
});
|
||||
|
||||
return () => onScrollEnd.remove();
|
||||
}, []);
|
||||
}, [location, postId]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilesForGallery([...imageAttachments, ...nonImageAttachments]);
|
||||
}, [imageAttachments, nonImageAttachments]);
|
||||
|
||||
return (
|
||||
<GalleryInit galleryIdentifier={galleryIdentifier}>
|
||||
<Animated.View style={[failed && styles.failed]}>
|
||||
<Animated.View
|
||||
testID='files-container'
|
||||
style={failed ? styles.failed : undefined}
|
||||
>
|
||||
{renderImageRow()}
|
||||
{renderItems(nonImageAttachments)}
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ const FileResults = ({
|
|||
const containerStyle = useMemo(() => ([paddingTop, {flexGrow: 1}]), [paddingTop]);
|
||||
const numOptions = getNumberFileMenuOptions(canDownloadFiles, publicLinkEnabled);
|
||||
|
||||
const {images: imageAttachments, nonImages: nonImageAttachments} = useImageAttachments(fileInfos, publicLinkEnabled);
|
||||
const {images: imageAttachments, nonImages: nonImageAttachments} = useImageAttachments(fileInfos);
|
||||
const filesForGallery = useMemo(() => imageAttachments.concat(nonImageAttachments), [imageAttachments, nonImageAttachments]);
|
||||
|
||||
const channelNames = useMemo(() => getChannelNamesWithID(fileChannels), [fileChannels]);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
let operator: ServerDataOperator;
|
||||
|
||||
let posts: Post[] = [];
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
posts = [
|
||||
{
|
||||
id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
|
|
@ -101,8 +101,8 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
files: [
|
||||
{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
|
|
@ -169,13 +169,15 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
metadata: {},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await DatabaseManager.init(['baseHandler.test.com']);
|
||||
operator = DatabaseManager.serverDatabases['baseHandler.test.com']!.operator;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
DatabaseManager.destroyServerDatabase('baseHandler.test.com');
|
||||
});
|
||||
|
||||
it('=> HandleDraft: should write to the the Draft table', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
|
|
@ -264,8 +266,8 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
files: [
|
||||
{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
|
|
@ -367,6 +369,174 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
prepareRecordsOnly: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> HandlePosts: should remove files no longer present in the post', async () => {
|
||||
const postWithMetadata = posts[0];
|
||||
const uploadedFiles = postWithMetadata.metadata.files!;
|
||||
const updatedPosts: Post[] = [
|
||||
{
|
||||
...postWithMetadata,
|
||||
update_at: Date.now(),
|
||||
metadata: {
|
||||
...postWithMetadata.metadata,
|
||||
files: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const order = [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
];
|
||||
|
||||
const actionType = ActionType.POSTS.RECEIVED_IN_CHANNEL;
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType,
|
||||
order,
|
||||
posts,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
let files = await operator.database.get('File').query(Q.where('post_id', postWithMetadata.id)).fetch();
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].id).toBe(uploadedFiles[0].id);
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType,
|
||||
order: [uploadedFiles[0].id!],
|
||||
posts: updatedPosts,
|
||||
});
|
||||
|
||||
files = await operator.database.get('File').query(Q.where('post_id', postWithMetadata.id)).fetch();
|
||||
expect(files).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('=> HandlePosts: should add new files if new files are added', async () => {
|
||||
const postWithMetadata = posts[0];
|
||||
const uploadedFiles = postWithMetadata.metadata.files!;
|
||||
const updatedPosts: Post[] = [
|
||||
{
|
||||
...postWithMetadata,
|
||||
update_at: Date.now(),
|
||||
metadata: {
|
||||
...postWithMetadata.metadata,
|
||||
files: [
|
||||
...postWithMetadata.metadata.files!,
|
||||
{
|
||||
id: 'another-file-id',
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const order = [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
];
|
||||
|
||||
const actionType = ActionType.POSTS.RECEIVED_IN_CHANNEL;
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType,
|
||||
order,
|
||||
posts,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
let files = await operator.database.get('File').query(Q.where('post_id', postWithMetadata.id)).fetch();
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].id).toBe(uploadedFiles[0].id);
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType,
|
||||
order: [uploadedFiles[0].id!],
|
||||
posts: updatedPosts,
|
||||
});
|
||||
|
||||
files = await operator.database.get('File').query(Q.where('post_id', postWithMetadata.id)).fetch();
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files.map((file) => file.id)).toEqual(expect.arrayContaining(['f1oxe5rtepfs7n3zifb4sso7po', 'another-file-id']));
|
||||
});
|
||||
|
||||
it('=> HandlePosts: should substitute files new files are added and old files are removed', async () => {
|
||||
const postWithMetadata = posts[0];
|
||||
const uploadedFiles = postWithMetadata.metadata.files!;
|
||||
const updatedPosts: Post[] = [
|
||||
{
|
||||
...postWithMetadata,
|
||||
update_at: Date.now(),
|
||||
metadata: {
|
||||
...postWithMetadata.metadata,
|
||||
files: [
|
||||
{
|
||||
id: 'another-file-id',
|
||||
user_id: 'q3mzxua9zjfczqakxdkowc6u6yy',
|
||||
post_id: '8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const order = [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
'3y3w3a6gkbg73bnj3xund9o5ic',
|
||||
];
|
||||
|
||||
const actionType = ActionType.POSTS.RECEIVED_IN_CHANNEL;
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType,
|
||||
order,
|
||||
posts,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
let files = await operator.database.get('File').query(Q.where('post_id', postWithMetadata.id)).fetch();
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].id).toBe(uploadedFiles[0].id);
|
||||
|
||||
await operator.handlePosts({
|
||||
actionType,
|
||||
order: [uploadedFiles[0].id!],
|
||||
posts: updatedPosts,
|
||||
});
|
||||
|
||||
files = await operator.database.get('File').query(Q.where('post_id', postWithMetadata.id)).fetch();
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].id).toBe('another-file-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('*** Operator: merge chunks ***', () => {
|
||||
|
|
|
|||
|
|
@ -217,8 +217,8 @@ const PostHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercla
|
|||
return result;
|
||||
}, new Set<string>());
|
||||
|
||||
const database: Database = this.database;
|
||||
if (deletedPostIds.size) {
|
||||
const database: Database = this.database;
|
||||
const postsToDelete = await database.get<PostModel>(POST).query(Q.where('id', Q.oneOf(Array.from(deletedPostIds)))).fetch();
|
||||
if (postsToDelete.length) {
|
||||
await database.write(async () => {
|
||||
|
|
@ -260,6 +260,14 @@ const PostHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercla
|
|||
batch.push(...postFiles);
|
||||
}
|
||||
|
||||
const allFiles = await database.get<FileModel>(MM_TABLES.SERVER.FILE).query(Q.where('post_id', Q.oneOf(uniquePosts.map((p) => p.id)))).fetch();
|
||||
const receivedFilesSet = new Set(files.map((f) => f.id));
|
||||
allFiles.forEach((f) => {
|
||||
if (!receivedFilesSet.has(f.id)) {
|
||||
batch.push(f.prepareDestroyPermanently());
|
||||
}
|
||||
});
|
||||
|
||||
if (emojis.length) {
|
||||
const postEmojis = await this.handleCustomEmojis({emojis, prepareRecordsOnly: true});
|
||||
batch.push(...postEmojis);
|
||||
|
|
|
|||
145
app/hooks/files.test.ts
Normal file
145
app/hooks/files.test.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook} from '@testing-library/react-hooks';
|
||||
|
||||
import {buildFilePreviewUrl, buildFileUrl} from '@actions/remote/file';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {mockFileInfo} from '@test/api_mocks/file';
|
||||
import {isGif, isImage, isVideo} from '@utils/file';
|
||||
|
||||
import {useImageAttachments} from './files';
|
||||
|
||||
jest.mock('@actions/remote/file', () => ({
|
||||
buildFilePreviewUrl: jest.fn(),
|
||||
buildFileUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@utils/file', () => ({
|
||||
isGif: jest.fn(),
|
||||
isImage: jest.fn(),
|
||||
isVideo: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useImageAttachments', () => {
|
||||
const serverUrl = 'https://example.com';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useServerUrl).mockReturnValue(serverUrl);
|
||||
});
|
||||
|
||||
it('should separate images and non-images correctly', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', localPath: 'path/to/image1', uri: `${serverUrl}/files/image1`}),
|
||||
mockFileInfo({id: '2', localPath: 'path/to/video1', uri: `${serverUrl}/files/video1`}),
|
||||
mockFileInfo({id: '3', localPath: 'path/to/file1', uri: `${serverUrl}/files/file1`}),
|
||||
];
|
||||
|
||||
jest.mocked(isImage).mockImplementation((file) => file?.id === '1');
|
||||
jest.mocked(isVideo).mockImplementation((file) => file?.id === '2');
|
||||
jest.mocked(isGif).mockReturnValue(false);
|
||||
jest.mocked(buildFilePreviewUrl).mockImplementation((url, id) => `${url}/preview/${id}`);
|
||||
jest.mocked(buildFileUrl).mockImplementation((url, id) => `${url}/file/${id}`);
|
||||
|
||||
const {result} = renderHook(() => useImageAttachments(filesInfo));
|
||||
|
||||
expect(result.current.images).toEqual([
|
||||
mockFileInfo({id: '1', localPath: 'path/to/image1', uri: 'path/to/image1'}),
|
||||
mockFileInfo({id: '2', localPath: 'path/to/video1', uri: 'path/to/video1'}),
|
||||
]);
|
||||
|
||||
expect(result.current.nonImages).toEqual([
|
||||
mockFileInfo({id: '3', localPath: 'path/to/file1', uri: `${serverUrl}/files/file1`}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use preview URL for images without localPath', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', localPath: '', uri: `${serverUrl}/files/image1`}),
|
||||
];
|
||||
|
||||
jest.mocked(isImage).mockReturnValue(true);
|
||||
jest.mocked(isVideo).mockReturnValue(false);
|
||||
jest.mocked(isGif).mockReturnValue(false);
|
||||
jest.mocked(buildFilePreviewUrl).mockImplementation((url, id) => `${url}/preview/${id}`);
|
||||
|
||||
const {result} = renderHook(() => useImageAttachments(filesInfo));
|
||||
|
||||
expect(result.current.images).toEqual([
|
||||
mockFileInfo({id: '1', localPath: '', uri: 'https://example.com/preview/1'}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use file URL for gifs and videos without local path', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', localPath: '', uri: `${serverUrl}/files/image1`}),
|
||||
mockFileInfo({id: '2', localPath: '', uri: `${serverUrl}/files/video1`}),
|
||||
];
|
||||
|
||||
jest.mocked(isImage).mockImplementation((file) => file?.id === '1' || file?.id === '3');
|
||||
jest.mocked(isVideo).mockImplementation((file) => file?.id === '2');
|
||||
jest.mocked(isGif).mockImplementation((file) => file?.id === '1');
|
||||
jest.mocked(buildFileUrl).mockImplementation((url, id) => `${url}/file/${id}`);
|
||||
|
||||
const {result} = renderHook(() => useImageAttachments(filesInfo));
|
||||
|
||||
expect(result.current.images).toEqual([
|
||||
mockFileInfo({id: '1', localPath: '', uri: 'https://example.com/file/1'}),
|
||||
mockFileInfo({id: '2', localPath: '', uri: 'https://example.com/file/2'}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the same values if the same arguments are passed', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '1', localPath: 'path/to/image1', uri: `${serverUrl}/files/image1`}),
|
||||
mockFileInfo({id: '2', localPath: 'path/to/video1', uri: `${serverUrl}/files/video1`}),
|
||||
];
|
||||
|
||||
jest.mocked(isImage).mockImplementation((file) => file?.id === '1');
|
||||
jest.mocked(isVideo).mockImplementation((file) => file?.id === '2');
|
||||
jest.mocked(isGif).mockReturnValue(false);
|
||||
jest.mocked(buildFilePreviewUrl).mockImplementation((url, id) => `${url}/preview/${id}`);
|
||||
jest.mocked(buildFileUrl).mockImplementation((url, id) => `${url}/file/${id}`);
|
||||
|
||||
const {result, rerender} = renderHook(() => useImageAttachments(filesInfo));
|
||||
|
||||
const firstResult = result.current;
|
||||
|
||||
rerender();
|
||||
|
||||
const secondResult = result.current;
|
||||
|
||||
expect(firstResult).toBe(secondResult);
|
||||
});
|
||||
|
||||
it('should handle empty filesInfo array', () => {
|
||||
const filesInfo: FileInfo[] = [];
|
||||
|
||||
const {result} = renderHook(() => useImageAttachments(filesInfo));
|
||||
|
||||
expect(result.current.images).toEqual([]);
|
||||
expect(result.current.nonImages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle files with no id', () => {
|
||||
const filesInfo = [
|
||||
mockFileInfo({id: '', localPath: 'path/to/image1', uri: `${serverUrl}/files/image1`}),
|
||||
mockFileInfo({id: '', localPath: '', uri: `${serverUrl}/files/image2`}),
|
||||
];
|
||||
|
||||
jest.mocked(isImage).mockReturnValue(true);
|
||||
jest.mocked(isVideo).mockReturnValue(false);
|
||||
jest.mocked(isGif).mockReturnValue(false);
|
||||
jest.mocked(buildFilePreviewUrl).mockImplementation((url, id) => `${url}/preview/${id}`);
|
||||
|
||||
const {result} = renderHook(() => useImageAttachments(filesInfo));
|
||||
|
||||
expect(result.current.images).toEqual([
|
||||
mockFileInfo({id: '', localPath: 'path/to/image1', uri: 'path/to/image1'}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -49,32 +49,30 @@ const getFileInfo = async (serverUrl: string, bookmarks: ChannelBookmarkModel[],
|
|||
}
|
||||
};
|
||||
|
||||
export const useImageAttachments = (filesInfo: FileInfo[], publicLinkEnabled: boolean) => {
|
||||
export const useImageAttachments = (filesInfo: FileInfo[]) => {
|
||||
const serverUrl = useServerUrl();
|
||||
return useMemo(() => {
|
||||
return filesInfo.reduce(({images, nonImages}: {images: FileInfo[]; nonImages: FileInfo[]}, file) => {
|
||||
const imageFile = isImage(file);
|
||||
const videoFile = isVideo(file);
|
||||
if (imageFile || (videoFile && publicLinkEnabled)) {
|
||||
if (imageFile || videoFile) {
|
||||
let uri;
|
||||
if (file.localPath) {
|
||||
uri = file.localPath;
|
||||
} else {
|
||||
uri = (isGif(file) || videoFile) ? buildFileUrl(serverUrl, file.id!) : buildFilePreviewUrl(serverUrl, file.id!);
|
||||
// If no local path and no id, we skip the image
|
||||
if (!file.id) {
|
||||
return {images, nonImages};
|
||||
}
|
||||
uri = (isGif(file) || videoFile) ? buildFileUrl(serverUrl, file.id) : buildFilePreviewUrl(serverUrl, file.id);
|
||||
}
|
||||
images.push({...file, uri});
|
||||
} else {
|
||||
let uri = file.uri;
|
||||
if (videoFile) {
|
||||
// fallback if public links are not enabled
|
||||
uri = buildFileUrl(serverUrl, file.id!);
|
||||
}
|
||||
|
||||
nonImages.push({...file, uri});
|
||||
nonImages.push({...file});
|
||||
}
|
||||
return {images, nonImages};
|
||||
}, {images: [], nonImages: []});
|
||||
}, [filesInfo, publicLinkEnabled]);
|
||||
}, [filesInfo, serverUrl]);
|
||||
};
|
||||
|
||||
export const useChannelBookmarkFiles = (bookmarks: ChannelBookmarkModel[], publicLinkEnabled: boolean) => {
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export function useGalleryItem(
|
|||
) {
|
||||
const gallery = useGallery(identifier);
|
||||
const ref = useAnimatedRef<any>();
|
||||
const {opacity, activeIndex} = gallery!.sharedValues;
|
||||
const {opacity, activeIndex} = gallery.sharedValues;
|
||||
|
||||
const styles = useAnimatedStyle(() => {
|
||||
return {
|
||||
|
|
|
|||
73
app/hooks/utils.test.tsx
Normal file
73
app/hooks/utils.test.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook, act} from '@testing-library/react-hooks';
|
||||
|
||||
import {usePreventDoubleTap} from './utils';
|
||||
|
||||
describe('usePreventDoubleTap', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
it('should allow the first tap', () => {
|
||||
const {result} = renderHook(() => usePreventDoubleTap(callback));
|
||||
|
||||
act(() => {
|
||||
result.current();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent the second tap within the delay', () => {
|
||||
const {result} = renderHook(() => usePreventDoubleTap(callback));
|
||||
|
||||
act(() => {
|
||||
result.current();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
result.current();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should allow the tap after the delay', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const {result} = renderHook(() => usePreventDoubleTap(callback));
|
||||
|
||||
act(() => {
|
||||
result.current();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
result.current();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(750);
|
||||
result.current();
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it('should return the same result if the same callback is passed', () => {
|
||||
const {result, rerender} = renderHook(() => usePreventDoubleTap(callback));
|
||||
|
||||
const firstResult = result.current;
|
||||
|
||||
rerender();
|
||||
|
||||
const secondResult = result.current;
|
||||
|
||||
expect(firstResult).toBe(secondResult);
|
||||
});
|
||||
});
|
||||
19
app/hooks/utils.ts
Normal file
19
app/hooks/utils.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
const DELAY = 750;
|
||||
|
||||
export const usePreventDoubleTap = <T extends Function>(callback: T) => {
|
||||
const lastTapRef = useRef<number | null>(null);
|
||||
|
||||
return useCallback((...args: unknown[]) => {
|
||||
const now = Date.now();
|
||||
if (lastTapRef.current && now - lastTapRef.current < DELAY) {
|
||||
return;
|
||||
}
|
||||
lastTapRef.current = now;
|
||||
callback(...args);
|
||||
}, [callback]);
|
||||
};
|
||||
19
test/api_mocks/file.ts
Normal file
19
test/api_mocks/file.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function mockFileInfo(overwrite: Partial<FileInfo> = {}): FileInfo {
|
||||
return {
|
||||
id: '1',
|
||||
localPath: 'path/to/image1',
|
||||
uri: '',
|
||||
has_preview_image: true,
|
||||
extension: 'png',
|
||||
height: 100,
|
||||
width: 100,
|
||||
mime_type: 'image/png',
|
||||
name: 'image1',
|
||||
size: 100,
|
||||
user_id: '1',
|
||||
...overwrite,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue