Skip to content

Commit

Permalink
Merge pull request #36658 from software-mansion-labs/@szymczak/Attach…
Browse files Browse the repository at this point in the history
…mentCarousel

[TS migration] Migrate AttachmentCarousel to typescript
  • Loading branch information
puneetlath authored Mar 25, 2024
2 parents 5440a6b + f5a6712 commit bcd4878
Show file tree
Hide file tree
Showing 31 changed files with 384 additions and 568 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow
role={CONST.ROLE.BUTTON}
>
<AttachmentView
// @ts-expect-error TODO: Remove this once AttachmentView (https://github.com/Expensify/App/issues/25150) is migrated to TypeScript.
source={sourceURLWithAuth}
file={{name: displayName}}
shouldShowDownloadIcon={!isOffline}
Expand Down
30 changes: 10 additions & 20 deletions src/components/AttachmentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type ModalType from '@src/types/utils/ModalType';
import AttachmentCarousel from './Attachments/AttachmentCarousel';
import AttachmentCarouselPagerContext from './Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
import AttachmentView from './Attachments/AttachmentView';
import type {Attachment} from './Attachments/types';
import BlockingView from './BlockingViews/BlockingView';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
Expand Down Expand Up @@ -61,15 +62,6 @@ type AttachmentModalOnyxProps = {
parentReportActions: OnyxEntry<OnyxTypes.ReportActions>;
};

type Attachment = {
source: AvatarSource;
isAuthTokenRequired: boolean;
file: FileObject;
isReceipt: boolean;
hasBeenFlagged?: boolean;
reportActionID?: string;
};

type ImagePickerResponse = {
height: number;
name: string;
Expand All @@ -79,7 +71,7 @@ type ImagePickerResponse = {
width: number;
};

type FileObject = File | ImagePickerResponse;
type FileObject = Partial<File | ImagePickerResponse>;

type ChildrenProps = {
displayFileInModal: (data: FileObject) => void;
Expand Down Expand Up @@ -181,7 +173,7 @@ function AttachmentModal({
const [isAuthTokenRequiredState, setIsAuthTokenRequiredState] = useState(isAuthTokenRequired);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState<TranslationPaths | null>(null);
const [attachmentInvalidReason, setAttachmentInvalidReason] = useState<TranslationPaths | null>(null);
const [sourceState, setSourceState] = useState(() => source);
const [sourceState, setSourceState] = useState<AvatarSource>(() => source);
const [modalType, setModalType] = useState<ModalType>(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE);
const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1));
Expand All @@ -190,7 +182,7 @@ function AttachmentModal({
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid);

const [file, setFile] = useState<Partial<FileObject> | undefined>(
const [file, setFile] = useState<FileObject | undefined>(
originalFileName
? {
name: originalFileName,
Expand All @@ -211,7 +203,7 @@ function AttachmentModal({
(attachment: Attachment) => {
setSourceState(attachment.source);
setFile(attachment.file);
setIsAuthTokenRequiredState(attachment.isAuthTokenRequired);
setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false);
onCarouselAttachmentChange(attachment);
},
[onCarouselAttachmentChange],
Expand All @@ -222,7 +214,7 @@ function AttachmentModal({
*/
const getModalType = useCallback(
(sourceURL: string, fileObject: FileObject) =>
sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name || translate('attachmentView.unknownFilename'))))
sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name ?? translate('attachmentView.unknownFilename'))))
? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
: CONST.MODAL.MODAL_TYPE.CENTERED,
[translate],
Expand Down Expand Up @@ -292,14 +284,14 @@ function AttachmentModal({
}, [transaction, report]);

const isValidFile = useCallback((fileObject: FileObject) => {
if (fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
if (fileObject.size !== undefined && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge');
setAttachmentInvalidReason('attachmentPicker.sizeExceeded');
return false;
}

if (fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
if (fileObject.size !== undefined && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall');
setAttachmentInvalidReason('attachmentPicker.sizeNotMet');
Expand Down Expand Up @@ -352,7 +344,7 @@ function AttachmentModal({
setSourceState(inputSource);
setFile(updatedFile);
setModalType(inputModalType);
} else {
} else if (fileObject.uri) {
const inputModalType = getModalType(fileObject.uri, fileObject);
setIsModalOpen(true);
setSourceState(fileObject.uri);
Expand Down Expand Up @@ -536,7 +528,6 @@ function AttachmentModal({
onNavigate={onNavigate}
onClose={closeModal}
source={source}
onToggleKeyboard={updateConfirmButtonVisibility}
setDownloadButtonVisibility={setDownloadButtonVisibility}
/>
) : (
Expand All @@ -546,7 +537,6 @@ function AttachmentModal({
!shouldShowNotFoundPage && (
<AttachmentCarouselPagerContext.Provider value={context}>
<AttachmentView
// @ts-expect-error TODO: Remove this once Attachments (https://github.com/Expensify/App/issues/24969) is migrated to TypeScript.
containerStyles={[styles.mh5]}
source={sourceForAttachmentView}
isAuthTokenRequired={isAuthTokenRequiredState}
Expand Down Expand Up @@ -637,4 +627,4 @@ export default withOnyx<AttachmentModalProps, AttachmentModalOnyxProps>({
},
})(memo(AttachmentModal));

export type {Attachment, FileObject};
export type {FileObject};
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import PropTypes from 'prop-types';
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {PixelRatio, View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';

const propTypes = {
type AttachmentCarouselCellRendererProps = {
/** Cell Container styles */
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
style?: StyleProp<ViewStyle>;
};

const defaultProps = {
style: [],
};

function AttachmentCarouselCellRenderer(props) {
function AttachmentCarouselCellRenderer(props: AttachmentCarouselCellRendererProps) {
const styles = useThemeStyles();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
Expand All @@ -28,8 +24,6 @@ function AttachmentCarouselCellRenderer(props) {
);
}

AttachmentCarouselCellRenderer.propTypes = propTypes;
AttachmentCarouselCellRenderer.defaultProps = defaultProps;
AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer';

export default React.memo(AttachmentCarouselCellRenderer);
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import {useEffect} from 'react';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import CONST from '@src/CONST';

const propTypes = {
type CarouselActionsProps = {
/** Callback to cycle through attachments */
onCycleThroughAttachments: PropTypes.func.isRequired,
onCycleThroughAttachments: (deltaSlide: number) => void;
};

function CarouselActions({onCycleThroughAttachments}) {
function CarouselActions({onCycleThroughAttachments}: CarouselActionsProps) {
useEffect(() => {
const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT;
const unsubscribeLeftKey = KeyboardShortcut.subscribe(
shortcutLeftConfig.shortcutKey,
(e) => {
if (lodashGet(e, 'target.blur')) {
(event) => {
if (event?.target instanceof HTMLElement) {
// prevents focus from highlighting around the modal
e.target.blur();
event.target.blur();
}

onCycleThroughAttachments(-1);
},
shortcutLeftConfig.descriptionKey,
Expand All @@ -29,12 +26,11 @@ function CarouselActions({onCycleThroughAttachments}) {
const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT;
const unsubscribeRightKey = KeyboardShortcut.subscribe(
shortcutRightConfig.shortcutKey,
(e) => {
if (lodashGet(e, 'target.blur')) {
(event) => {
if (event?.target instanceof HTMLElement) {
// prevents focus from highlighting around the modal
e.target.blur();
event.target.blur();
}

onCycleThroughAttachments(1);
},
shortcutRightConfig.descriptionKey,
Expand All @@ -50,6 +46,4 @@ function CarouselActions({onCycleThroughAttachments}) {
return null;
}

CarouselActions.propTypes = propTypes;

export default CarouselActions;
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import * as AttachmentCarouselViewPropTypes from '@components/Attachments/propTypes';
import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import Tooltip from '@components/Tooltip';
Expand All @@ -11,36 +9,34 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';

const propTypes = {
type CarouselButtonsProps = {
/** Where the arrows should be visible */
shouldShowArrows: PropTypes.bool.isRequired,
shouldShowArrows: boolean;

/** The current page index */
page: PropTypes.number.isRequired,
page: number;

/** The attachments from the carousel */
attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired,
attachments: Attachment[];

/** Callback to go one page back */
onBack: PropTypes.func.isRequired,
onBack: () => void;

/** Callback to go one page forward */
onForward: PropTypes.func.isRequired,
onForward: () => void;

autoHideArrow: PropTypes.func,
cancelAutoHideArrow: PropTypes.func,
};
/** Callback for autohiding carousel button arrows */
autoHideArrow?: () => void;

const defaultProps = {
autoHideArrow: () => {},
cancelAutoHideArrow: () => {},
/** Callback for cancelling autohiding of carousel button arrows */
cancelAutoHideArrow?: () => void;
};

function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}) {
function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
const theme = useTheme();
const styles = useThemeStyles();
const isBackDisabled = page === 0;
const isForwardDisabled = page === _.size(attachments) - 1;

const isForwardDisabled = page === attachments.length - 1;
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();

Expand Down Expand Up @@ -80,8 +76,6 @@ function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward
) : null;
}

CarouselButtons.propTypes = propTypes;
CarouselButtons.defaultProps = defaultProps;
CarouselButtons.displayName = 'CarouselButtons';

export default CarouselButtons;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, {useContext, useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import AttachmentView from '@components/Attachments/AttachmentView';
import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
Expand All @@ -12,55 +12,27 @@ import useThemeStyles from '@hooks/useThemeStyles';
import ReportAttachmentsContext from '@pages/home/report/ReportAttachmentsContext';
import CONST from '@src/CONST';

const propTypes = {
type CarouselItemProps = {
/** Attachment required information such as the source and file name */
item: PropTypes.shape({
/** Report action ID of the attachment */
reportActionID: PropTypes.string,

/** Whether source URL requires authentication */
isAuthTokenRequired: PropTypes.bool,

/** URL to full-sized attachment or SVG function */
source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,

/** Additional information about the attachment file */
file: PropTypes.shape({
/** File name of the attachment */
name: PropTypes.string.isRequired,
}).isRequired,

/** Whether the attachment has been flagged */
hasBeenFlagged: PropTypes.bool,

/** The id of the transaction related to the attachment */
transactionID: PropTypes.string,

duration: PropTypes.number,
}).isRequired,
item: Attachment;

/** onPress callback */
onPress: PropTypes.func,
onPress?: () => void;

isModalHovered: PropTypes.bool,
/** Whether attachment carousel modal is hovered over */
isModalHovered?: boolean;

/** Whether the attachment is currently being viewed in the carousel */
isFocused: PropTypes.bool.isRequired,
};

const defaultProps = {
onPress: undefined,
isModalHovered: false,
isFocused: boolean;
};

function CarouselItem({item, onPress, isFocused, isModalHovered}) {
function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isAttachmentHidden} = useContext(ReportAttachmentsContext);
// eslint-disable-next-line es/no-nullish-coalescing-operators
const [isHidden, setIsHidden] = useState(() => isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged);
const [isHidden, setIsHidden] = useState(() => (item.reportActionID ? isAttachmentHidden(item.reportActionID) : item.hasBeenFlagged));

const renderButton = (style) => (
const renderButton = (style: StyleProp<ViewStyle>) => (
<Button
small
style={style}
Expand All @@ -87,7 +59,8 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
style={[styles.attachmentRevealButtonContainer]}
onPress={onPress}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
accessibilityLabel={item.file?.name || translate('attachmentView.unknownFilename')}
>
{children}
</PressableWithoutFeedback>
Expand All @@ -108,7 +81,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
reportActionID={item.reportActionID}
isHovered={isModalHovered}
isFocused={isFocused}
optionalVideoDuration={item.duration}
duration={item.duration}
/>
</View>

Expand All @@ -121,8 +94,6 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
);
}

CarouselItem.propTypes = propTypes;
CarouselItem.defaultProps = defaultProps;
CarouselItem.displayName = 'CarouselItem';

export default CarouselItem;
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import type {ForwardedRef} from 'react';
import {createContext} from 'react';
import type PagerView from 'react-native-pager-view';
import type {SharedValue} from 'react-native-reanimated';
import type {AttachmentSource} from '@components/Attachments/types';

/** The pager items array is used within the pager to render and navigate between the images */
type AttachmentCarouselPagerItems = {
/** The source of the image is used to identify each attachment/page in the pager */
source: string;
source: AttachmentSource;

/** The index of the pager item determines the order of the images in the pager */
index: number;
Expand Down
Loading

0 comments on commit bcd4878

Please sign in to comment.