Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
597 changes: 0 additions & 597 deletions src/components/AttachmentModal.tsx

This file was deleted.

18 changes: 17 additions & 1 deletion src/libs/AvatarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {TranslationPaths} from '@src/languages/types';
import type {FileObject} from '@src/types/utils/Attachment';
import {splitExtensionFromFileName, validateImageForCorruption} from './fileDownload/FileUtils';
import getImageResolution from './fileDownload/getImageResolution';
import tryResolveUrlFromApiRoot from './tryResolveUrlFromApiRoot';
import type {AvatarSource} from './UserUtils';

/**
* Validation result containing error information if validation fails
Expand Down Expand Up @@ -108,4 +110,18 @@ async function validateAvatarImage(image: FileObject): Promise<ValidationResult>
return {isValid: true};
}

export {isValidExtension, isValidSize, isValidResolution, validateAvatarImage};
function getValidatedImageSource(source: AvatarSource | undefined) {
const numberSource = Number(source);

if (!Number.isNaN(numberSource) && numberSource !== 0) {
return numberSource;
}

if (typeof source === 'string') {
return tryResolveUrlFromApiRoot(decodeURIComponent(source));
}

return undefined;
}
Comment thread
mountiny marked this conversation as resolved.

export {isValidExtension, isValidSize, isValidResolution, validateAvatarImage, getValidatedImageSource};
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ type AttachmentModalContentData = {
file: FileObject | undefined;
};

type ThreeDotsMenuItemGeneratorProps = AttachmentModalContentData & {
type ThreeDotsMenuItemFactoryProps = AttachmentModalContentData & {
isLocalSource: boolean;
};

type ThreeDotsMenuItemGenerator = (props: ThreeDotsMenuItemGeneratorProps) => PopoverMenuItem[];
type ThreeDotsMenuItemFactory = (props: ThreeDotsMenuItemFactoryProps) => PopoverMenuItem[];

type DownloadAttachmentCallback = (props: AttachmentModalContentData) => void;

Expand Down Expand Up @@ -58,7 +58,7 @@ type AttachmentModalBaseContentProps = {
headerTitle?: string;

/** The menu items for the three dots button */
threeDotsMenuItems?: PopoverMenuItem[] | ThreeDotsMenuItemGenerator;
threeDotsMenuItems?: PopoverMenuItem[] | ThreeDotsMenuItemFactory;

/** The report that has this attachment */
report?: OnyxEntry<OnyxTypes.Report>;
Expand Down Expand Up @@ -123,6 +123,6 @@ export type {
DownloadAttachmentCallback,
AttachmentContent,
AttachmentContentProps,
ThreeDotsMenuItemGenerator,
ThreeDotsMenuItemGeneratorProps,
ThreeDotsMenuItemFactory,
ThreeDotsMenuItemFactoryProps,
};
3 changes: 3 additions & 0 deletions src/pages/media/AttachmentModalScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useContext, useMemo} from 'react';
import Log from '@libs/Log';
import SCREENS from '@src/SCREENS';
import AttachmentModalContext from './AttachmentModalContext';
import ProfileAvatarModalContent from './routes/ProfileAvatarModalContent';
Expand Down Expand Up @@ -91,6 +92,8 @@ function AttachmentModalScreen<Screen extends AttachmentModalScreenType>({route,
);
}

Log.warn('Unknown attachment modal screen. Make sure to add the new screen as a route to the AttachmentModalScreen component.', {route});

return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ function ShareDetailsAttachmentModalContent({route, navigation}: AttachmentModal
/>
);
}
ShareDetailsAttachmentModalContent.displayName = 'ReportAttachmentModalContent';
ShareDetailsAttachmentModalContent.displayName = 'ShareDetailsAttachmentModalContent';

export default ShareDetailsAttachmentModalContent;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {getReportAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
import {canEditFieldOfMoneyRequest, isMoneyRequestReport, isTrackExpenseReport} from '@libs/ReportUtils';
import {getRequestType, hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, isReceiptBeingScanned} from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import type {AttachmentModalBaseContentProps, ThreeDotsMenuItemGenerator} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent/types';
import type {AttachmentModalBaseContentProps, ThreeDotsMenuItemFactory} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent/types';
import AttachmentModalContainer from '@pages/media/AttachmentModalScreen/AttachmentModalContainer';
import type {AttachmentModalScreenProps} from '@pages/media/AttachmentModalScreen/types';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -151,7 +151,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre

const allowDownload = !isEReceipt;

const threeDotsMenuItems: ThreeDotsMenuItemGenerator = useCallback(
const threeDotsMenuItems: ThreeDotsMenuItemFactory = useCallback(
({file, source: innerSource, isLocalSource}) => {
const menuItems = [];
if (shouldShowReplaceReceiptButton) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import useOnyx from '@hooks/useOnyx';
import {openReport} from '@libs/actions/Report';
import validateAttachmentFile from '@libs/AttachmentUtils';
import type {AttachmentValidationResult} from '@libs/AttachmentUtils';
import {getValidatedImageSource} from '@libs/AvatarUtils';
import {isReportNotFound} from '@libs/ReportUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import type {AttachmentModalBaseContentProps} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent/types';
import AttachmentModalContainer from '@pages/media/AttachmentModalScreen/AttachmentModalContainer';
import useDownloadAttachment from '@pages/media/AttachmentModalScreen/routes/hooks/useDownloadAttachment';
Expand Down Expand Up @@ -70,7 +70,7 @@ function ReportAddAttachmentModalContent({route, navigation}: AttachmentModalScr
fetchReport();
}, [reportID, fetchReport, shouldFetchReport]);

const [source, setSource] = useState(() => Number(sourceParam) || (typeof sourceParam === 'string' ? tryResolveUrlFromApiRoot(decodeURIComponent(sourceParam)) : undefined));
const [source, setSource] = useState(() => getValidatedImageSource(sourceParam));

const [validFiles, setValidFiles] = useState<FileObject | FileObject[] | undefined>(fileParam);
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useOriginalReportID from '@hooks/useOriginalReportID';
import {openReport} from '@libs/actions/Report';
import {getValidatedImageSource} from '@libs/AvatarUtils';
import Navigation from '@libs/Navigation/Navigation';
import {isReportNotFound} from '@libs/ReportUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import type {AttachmentModalBaseContentProps} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent/types';
import AttachmentModalContainer from '@pages/media/AttachmentModalScreen/AttachmentModalContainer';
import useDownloadAttachment from '@pages/media/AttachmentModalScreen/routes/hooks/useDownloadAttachment';
Expand Down Expand Up @@ -91,15 +91,18 @@ function ReportAttachmentModalContent({route, navigation}: AttachmentModalScreen
isAuthTokenRequired,
});

const source = useMemo(() => Number(sourceParam) || (typeof sourceParam === 'string' ? tryResolveUrlFromApiRoot(decodeURIComponent(sourceParam)) : undefined), [sourceParam]);
const modalType = useReportAttachmentModalType();
const source = useMemo(() => getValidatedImageSource(sourceParam), [sourceParam]);
const modalType = useReportAttachmentModalType(source);

// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = !isLoading && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID;

const contentProps = useMemo<AttachmentModalBaseContentProps>(
() => ({
// In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource
type,
report,
shouldShowNotFoundPage: !isLoading && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID,
shouldShowNotFoundPage,
isAuthTokenRequired: !!isAuthTokenRequired,
attachmentLink: attachmentLink ?? '',
originalFileName: originalFileName ?? '',
Expand All @@ -112,7 +115,21 @@ function ReportAttachmentModalContent({route, navigation}: AttachmentModalScreen
onDownloadAttachment,
onCarouselAttachmentChange,
}),
[accountID, attachmentID, attachmentLink, headerTitle, isAuthTokenRequired, isLoading, onCarouselAttachmentChange, onDownloadAttachment, originalFileName, report, source, type],
[
accountID,
attachmentID,
attachmentLink,
headerTitle,
isAuthTokenRequired,
isLoading,
onCarouselAttachmentChange,
onDownloadAttachment,
originalFileName,
report,
shouldShowNotFoundPage,
source,
type,
],
);

return (
Expand Down
3 changes: 3 additions & 0 deletions src/pages/media/AttachmentModalScreen/routes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type {AttachmentModalScreensParamList} from '@libs/Navigation/types';
import type {AttachmentModalScreenBaseParams, AttachmentModalScreenType} from '@pages/media/AttachmentModalScreen/types';
import type Modify from '@src/types/utils/Modify';

/**
* The navigation params a specific attachment modal screen.
*/
type AttachmentModalScreenParams<Screen extends AttachmentModalScreenType> = Modify<AttachmentModalScreenBaseParams, AttachmentModalScreensParamList[Screen]>;

export default AttachmentModalScreenParams;
5 changes: 0 additions & 5 deletions src/pages/media/AttachmentModalScreen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import type SCREENS from '@src/SCREENS';
import type ModalType from '@src/types/utils/ModalType';
import type {AttachmentModalBaseContentProps} from './AttachmentModalBaseContent/types';

/**
* Modal render prop component that exposes modal launching triggers that can be used
* to display a full size image or PDF modally with optional confirmation button.
*/

type AttachmentModalContainerModalProps = {
/** The type of the modal */
modalType?: ModalType;
Expand Down
35 changes: 34 additions & 1 deletion tests/unit/AvatarUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {getApiRoot} from '@libs/ApiUtils';
import CONST from '@src/CONST';
import {isValidExtension, isValidResolution, isValidSize, validateAvatarImage} from '@src/libs/AvatarUtils';
import {getValidatedImageSource, isValidExtension, isValidResolution, isValidSize, validateAvatarImage} from '@src/libs/AvatarUtils';
import * as FileUtils from '@src/libs/fileDownload/FileUtils';
import * as getImageResolution from '@src/libs/fileDownload/getImageResolution';
import type {Request} from '@src/types/onyx';
import type {FileObject} from '@src/types/utils/Attachment';

jest.mock('@src/libs/fileDownload/FileUtils');
Expand Down Expand Up @@ -335,4 +337,35 @@ describe('AvatarUtils', () => {
expect(result.isValid).toBe(true);
});
});

describe('getValidatedImageSource', () => {
it('should validate number sources', () => {
expect(getValidatedImageSource(0)).toBe(undefined);
expect(getValidatedImageSource(1)).toBe(1);
});

it('should decode string source', () => {
const encodedImageFileName = 'avatar.jpg%3Fv%3D123';
const decodedImageFileName = decodeURIComponent(encodedImageFileName);
expect(getValidatedImageSource(encodedImageFileName)).toBe(decodedImageFileName);
});

it('should validate string source', () => {
const imageFileName = 'avatar.jpg';
const absoluteImageFilePath = `/${imageFileName}`;

const encodedImageFileName = encodeURIComponent(imageFileName);
const absoluteEncodedImageFilePath = `/${encodedImageFileName}`;

const apiRoot = getApiRoot({shouldUseSecure: false} as Request);
const prodImageFileUrl = `${apiRoot}${imageFileName}`;
const encodedProdImageFileUrl = `${apiRoot}${encodedImageFileName}`;

expect(getValidatedImageSource(absoluteImageFilePath)).toBe(prodImageFileUrl);
expect(getValidatedImageSource(absoluteEncodedImageFilePath)).toBe(encodedProdImageFileUrl);

expect(getValidatedImageSource(prodImageFileUrl)).toBe(prodImageFileUrl);
expect(getValidatedImageSource(encodedProdImageFileUrl)).toBe(prodImageFileUrl);
});
});
});
Loading