Skip to content

Native Share follow ups #59239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

28 changes: 19 additions & 9 deletions src/components/AttachmentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {checkIsFileImage} from './Attachments/AttachmentView';
import DefaultAttachmentView from './Attachments/AttachmentView/DefaultAttachmentView';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import ImageView from './ImageView';
import Image from './Image';
import PDFThumbnail from './PDFThumbnail';
import {PressableWithFeedback} from './Pressable';

Expand Down Expand Up @@ -79,9 +79,9 @@ function AttachmentPreview({source, aspectRatio = 1, onPress, onLoadError}: Atta
accessibilityLabel="Image Thumbnail"
>
<View style={[fillStyle, styles.br4, styles.overflowHidden, {aspectRatio}]}>
<ImageView
url={source}
fileName={fileName ?? ''}
<Image
source={{uri: source}}
style={[[styles.w100, styles.h100], styles.overflowHidden]}
/>
</View>
</PressableWithFeedback>
Expand All @@ -90,11 +90,21 @@ function AttachmentPreview({source, aspectRatio = 1, onPress, onLoadError}: Atta

if (typeof source === 'string' && Str.isPDF(source) && !isEncryptedPDF) {
return (
<PDFThumbnail
previewSourceURL={source}
onLoadError={onLoadError}
onPassword={() => setIsEncryptedPDF(true)}
/>
<PressableWithFeedback
accessibilityRole="button"
style={[styles.justifyContentStart, {aspectRatio: 1}]}
onPress={onPress}
accessible
accessibilityLabel="PDF Thumbnail"
>
<PDFThumbnail
fitPolicy={1}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fitPolicy={1}
fitPolicy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the fitPolicy={1} because I thought it looked nicer. Let me know if I should change that back.
fitPolicy={1}
image

fitPolicy={0}
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I thought fitPolicy was a boolean, nvm! 😁

previewSourceURL={source}
style={[styles.br4]}
onLoadError={onLoadError}
onPassword={() => setIsEncryptedPDF(true)}
/>
</PressableWithFeedback>
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/PDFThumbnail/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import PDFThumbnailError from './PDFThumbnailError';
import type PDFThumbnailProps from './types';

function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError, onLoadSuccess}: PDFThumbnailProps) {
function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, fitPolicy = 0, onPassword, onLoadError, onLoadSuccess}: PDFThumbnailProps) {
const styles = useThemeStyles();
const sizeStyles = [styles.w100, styles.h100];
const [failedToLoad, setFailedToLoad] = useState(false);
Expand All @@ -17,7 +17,7 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena
<View style={[sizeStyles, !failedToLoad && styles.alignItemsCenter, styles.justifyContentCenter]}>
{enabled && !failedToLoad && (
<Pdf
fitPolicy={0}
fitPolicy={fitPolicy}
trustAllCerts={false}
renderActivityIndicator={() => <FullScreenLoadingIndicator />}
source={{uri: isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL}}
Expand Down
3 changes: 3 additions & 0 deletions src/components/PDFThumbnail/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ type PDFThumbnailProps = {
/** Whether the PDF thumbnail can be loaded */
enabled?: boolean;

/** Fit policy for the PDF thumbnail */
fitPolicy?: number;

/** Callback to call if PDF is password protected */
onPassword?: () => void;

Expand Down
106 changes: 71 additions & 35 deletions src/pages/Share/ShareDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useMemo, useState} from 'react';
import React, {useEffect, useMemo, useState} from 'react';
import {SafeAreaView, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
Expand All @@ -11,7 +11,6 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {FallbackAvatar} from '@components/Icon/Expensicons';
import {PressableWithoutFeedback} from '@components/Pressable';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -24,13 +23,16 @@ import type {ShareNavigatorParamList} from '@libs/Navigation/types';
import {getReportDisplayOption} from '@libs/OptionsListUtils';
import {getReportOrDraftReport, isDraftReport} from '@libs/ReportUtils';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import variables from '@styles/variables';
import UserListItem from '@src/components/SelectionList/UserListItem';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Report as ReportType} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import KeyboardUtils from '@src/utils/keyboard';
import getFileSize from './getFileSize';
import {showErrorAlert} from './ShareRootPage';

type ShareDetailsPageProps = StackScreenProps<ShareNavigatorParamList, typeof SCREENS.SHARE.SHARE_DETAILS>;
Expand All @@ -46,10 +48,37 @@ function ShareDetailsPage({
const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE);
const isTextShared = currentAttachment?.mimeType === 'txt';
const [message, setMessage] = useState(isTextShared ? currentAttachment?.content ?? '' : '');
const [errorTitle, setErrorTitle] = useState<string | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);

const report: OnyxEntry<ReportType> = getReportOrDraftReport(reportOrAccountID);
const displayReport = useMemo(() => getReportDisplayOption(report, unknownUserDetails), [report, unknownUserDetails]);

useEffect(() => {
if (!currentAttachment?.content || errorTitle) {
return;
}
getFileSize(currentAttachment?.content).then((size) => {
if (size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setErrorTitle(translate('attachmentPicker.attachmentTooLarge'));
setErrorMessage(translate('attachmentPicker.sizeExceeded'));
}

if (size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setErrorTitle(translate('attachmentPicker.attachmentTooSmall'));
setErrorMessage(translate('attachmentPicker.sizeNotMet'));
}
});
}, [currentAttachment, errorTitle, translate]);

useEffect(() => {
if (!errorTitle || !errorMessage) {
return;
}

showErrorAlert(errorTitle, errorMessage);
}, [errorTitle, errorMessage]);

if (isEmptyObject(report)) {
return <NotFoundPage />;
}
Expand All @@ -68,7 +97,7 @@ function ShareDetailsPage({
if (isTextShared) {
addComment(report.reportID, message);
const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportOrAccountID);
Navigation.navigate(routeToNavigate);
Navigation.navigate(routeToNavigate, {forceReplace: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this change do?

Copy link
Contributor Author

@289Adam289 289Adam289 Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I forgot to comment on this line. It fixes the problem with navigation I've fixed before but I forgot about the sharing plain text case. This way when we share (text or file) and click back button we wont end up in the share flow again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

return;
}

Expand Down Expand Up @@ -100,20 +129,20 @@ function ShareDetailsPage({
};

return (
<PressableWithoutFeedback
onPress={() => {
KeyboardUtils.dismiss();
}}
accessible={false}
<ScreenWrapper
includeSafeAreaPaddingBottom
shouldEnableKeyboardAvoidingView={false}
keyboardAvoidingViewBehavior="padding"
shouldEnableMinHeight={canUseTouchScreen()}
testID={ShareDetailsPage.displayName}
>
<ScreenWrapper
includeSafeAreaPaddingBottom
shouldEnableKeyboardAvoidingView={false}
keyboardAvoidingViewBehavior="padding"
shouldEnableMinHeight={canUseTouchScreen()}
testID={ShareDetailsPage.displayName}
>
<View style={[styles.flex1, styles.flexColumn, styles.h100, styles.appBG]}>
<View style={[styles.flex1, styles.flexColumn, styles.h100, styles.appBG]}>
<PressableWithoutFeedback
onPress={() => {
KeyboardUtils.dismiss();
}}
accessible={false}
>
<HeaderWithBackButton
title={translate('share.shareToExpensify')}
shouldShowBackButton
Expand All @@ -136,20 +165,27 @@ function ShareDetailsPage({
/>
</View>
)}

<View style={[styles.ph5, styles.flex1, styles.flexColumn]}>
<View style={styles.pv5}>
<ScrollView>
<TextInput
autoFocus={false}
value={message}
multiline
onChangeText={setMessage}
accessibilityLabel={translate('share.messageInputLabel')}
label={translate('share.messageInputLabel')}
/>
</ScrollView>
</View>
</PressableWithoutFeedback>
<View style={[styles.ph5, styles.flex1, styles.flexColumn, styles.overflowHidden]}>
<View style={styles.pv3}>
<TextInput
autoFocus={false}
value={message}
multiline
scrollEnabled
autoGrowHeight
maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight}
onChangeText={setMessage}
accessibilityLabel={translate('share.messageInputLabel')}
label={translate('share.messageInputLabel')}
/>
</View>
<PressableWithoutFeedback
onPress={() => {
KeyboardUtils.dismiss();
}}
accessible={false}
>
{shouldShowAttachment && (
<>
<View style={[styles.pt6, styles.pb2]}>
Expand All @@ -176,19 +212,19 @@ function ShareDetailsPage({
</SafeAreaView>
</>
)}
</View>
</PressableWithoutFeedback>
</View>
<FixedFooter style={[styles.appBG, styles.pt2, styles.pb2]}>
<FixedFooter style={[styles.pt4]}>
<Button
success
large
text={translate('common.share')}
style={[styles.w100]}
style={styles.w100}
onPress={handleShare}
/>
</FixedFooter>
</ScreenWrapper>
</PressableWithoutFeedback>
</View>
</ScreenWrapper>
);
}

Expand Down
56 changes: 37 additions & 19 deletions src/pages/Share/ShareRootPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ShareActionHandler from '@libs/ShareActionHandlerModule';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {ShareTempFile} from '@src/types/onyx';
import getFileSize from './getFileSize';
import ShareTab from './ShareTab';
import SubmitTab from './SubmitTab';

Expand All @@ -39,33 +40,50 @@ function ShareRootPage() {
const [isFileScannable, setIsFileScannable] = useState(false);
const receiptFileFormats = Object.values(CONST.RECEIPT_ALLOWED_FILE_TYPES) as string[];
const shareFileMimetypes = Object.values(CONST.SHARE_FILE_MIMETYPE) as string[];
const [errorTitle, setErrorTitle] = useState<string | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);

useEffect(() => {
if (!errorTitle || !errorMessage) {
return;
}

showErrorAlert(errorTitle, errorMessage);
}, [errorTitle, errorMessage]);

const handleProcessFiles = useCallback(() => {
ShareActionHandler.processFiles((processedFiles) => {
const tempFile = Array.isArray(processedFiles) ? processedFiles.at(0) : (JSON.parse(processedFiles) as ShareTempFile);
if (errorTitle) {
return;
}
if (!tempFile?.mimeType || !shareFileMimetypes.includes(tempFile?.mimeType)) {
showErrorAlert(translate('attachmentPicker.wrongFileType'), translate('attachmentPicker.notAllowedExtension'));
setErrorTitle(translate('attachmentPicker.wrongFileType'));
setErrorMessage(translate('attachmentPicker.notAllowedExtension'));
return;
}

const fileRegexp = /image\/.*/;
if (fileRegexp.test(tempFile?.mimeType)) {
const isImage = /image\/.*/.test(tempFile?.mimeType);
if (tempFile?.mimeType && tempFile?.mimeType !== 'txt' && !isImage) {
getFileSize(tempFile?.content).then((size) => {
if (size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setErrorTitle(translate('attachmentPicker.attachmentTooLarge'));
setErrorMessage(translate('attachmentPicker.sizeExceeded'));
}

if (size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setErrorTitle(translate('attachmentPicker.attachmentTooSmall'));
setErrorMessage(translate('attachmentPicker.sizeNotMet'));
}
});
}

if (isImage) {
const fileObject: FileObject = {name: tempFile.id, uri: tempFile?.content, type: tempFile?.mimeType};
validateImageForCorruption(fileObject)
.then(() => {
if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
showErrorAlert(translate('attachmentPicker.attachmentTooLarge'), translate('attachmentPicker.sizeExceeded'));
}

if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
showErrorAlert(translate('attachmentPicker.attachmentTooSmall'), translate('attachmentPicker.sizeNotMet'));
}

return true;
})
.catch(() => {
showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
});
validateImageForCorruption(fileObject).catch(() => {
setErrorTitle(translate('attachmentPicker.attachmentError'));
setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
});
}

const {fileExtension} = splitExtensionFromFileName(tempFile?.content);
Expand All @@ -82,7 +100,7 @@ function ShareRootPage() {
addTempShareFile(tempFile);
}
});
}, [receiptFileFormats, shareFileMimetypes, translate]);
}, [receiptFileFormats, shareFileMimetypes, translate, errorTitle]);

useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) => {
Expand Down
23 changes: 21 additions & 2 deletions src/pages/Share/SubmitDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ function SubmitDetailsPage({
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);

const [errorTitle, setErrorTitle] = useState<string | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);

useEffect(() => {
if (!errorTitle || !errorMessage) {
return;
}

showErrorAlert(errorTitle, errorMessage);
}, [errorTitle, errorMessage]);

useEffect(() => {
initMoneyRequest(reportOrAccountID, policy, false, CONST.IOU.REQUEST_TYPE.SCAN, CONST.IOU.REQUEST_TYPE.SCAN);
}, [reportOrAccountID, policy]);
Expand Down Expand Up @@ -201,10 +212,18 @@ function SubmitDetailsPage({
shouldShowSmartScanFields={false}
isDistanceRequest={false}
onPDFLoadError={() => {
showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
if (errorTitle) {
return;
}
setErrorTitle(translate('attachmentPicker.attachmentError'));
setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
}}
onPDFPassword={() => {
showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.protectedPDFNotSupported'));
if (errorTitle) {
return;
}
setErrorTitle(translate('attachmentPicker.attachmentError'));
setErrorMessage(translate('attachmentPicker.protectedPDFNotSupported'));
}}
/>
</View>
Expand Down
10 changes: 10 additions & 0 deletions src/pages/Share/getFileSize/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import RNFS from 'react-native-fs';
import type GetFileSizeType from './types';

const getFileSize: GetFileSizeType = (uri: string) => {
return RNFS.stat(uri).then((fileStat) => {
return fileStat.size;
});
};

export default getFileSize;
Loading
Loading