diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 443a553d4689..ae74a11c7e9d 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -22,7 +22,7 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & { onPressOut?: () => void; }; -function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { +function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut, isDeleted}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; @@ -63,6 +63,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP shouldShowDownloadIcon={!!sourceID && !isOffline} shouldShowLoadingSpinnerIcon={isDownloading} isUsedAsChatAttachment + isDeleted={!!isDeleted} isUploading={!sourceID} /> diff --git a/src/components/AnchorForAttachmentsOnly/types.ts b/src/components/AnchorForAttachmentsOnly/types.ts index a5186d8c0d90..67a5bb532c27 100644 --- a/src/components/AnchorForAttachmentsOnly/types.ts +++ b/src/components/AnchorForAttachmentsOnly/types.ts @@ -9,6 +9,9 @@ type AnchorForAttachmentsOnlyProps = { /** Any additional styles to apply */ style?: StyleProp; + + /** Whether the attachment is deleted */ + isDeleted?: boolean; }; export default AnchorForAttachmentsOnlyProps; diff --git a/src/components/AttachmentDeletedIndicator.tsx b/src/components/AttachmentDeletedIndicator.tsx new file mode 100644 index 000000000000..06e700c2fd73 --- /dev/null +++ b/src/components/AttachmentDeletedIndicator.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; + +type AttachmentDeletedIndicatorProps = { + /** Additional styles for container */ + containerStyles?: StyleProp; +}; + +function AttachmentDeletedIndicator({containerStyles}: AttachmentDeletedIndicatorProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + if (!isOffline) { + return null; + } + + return ( + <> + + + + + + ); +} + +AttachmentDeletedIndicator.displayName = 'AttachmentDeletedIndicator'; + +export default AttachmentDeletedIndicator; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index e6ac9f9f21c7..23e13833df64 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -25,11 +25,14 @@ type DefaultAttachmentViewProps = { icon?: IconAsset; + /** Whether the attachment is deleted */ + isDeleted?: boolean; + /** Flag indicating if the attachment is being uploaded. */ isUploading?: boolean; }; -function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading, isDeleted}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -43,7 +46,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa /> - {fileName} + {fileName} {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 080e0ec589ec..0af1a86992e7 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -72,6 +72,9 @@ type AttachmentViewProps = Attachment & { /* Flag indicating whether the attachment has been uploaded. */ isUploaded?: boolean; + /** Whether the attachment is deleted */ + isDeleted?: boolean; + /** Flag indicating if the attachment is being uploaded. */ isUploading?: boolean; }; @@ -98,14 +101,14 @@ function AttachmentView({ duration, isUsedAsChatAttachment, isUploaded = true, + isDeleted, isUploading = false, }: AttachmentViewProps) { + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -292,6 +295,7 @@ function AttachmentView({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon || isUploading} containerStyles={containerStyles} + isDeleted={isDeleted} isUploading={isUploading} /> ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index d2e407ff8b55..122db1e7877b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -25,12 +25,15 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const isAttachment = !!htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; const tNodeChild = tnode?.domNode?.children?.at(0); const displayName = tNodeChild && 'data' in tNodeChild && typeof tNodeChild.data === 'string' ? tNodeChild.data : ''; - const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || ''; + const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); + const isDeleted = HTMLEngineUtils.isDeletedNode(tnode); + const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {}; + if (!HTMLEngineUtils.isChildOfComment(tnode)) { // This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click. // We don't have this behaviour in other links in NewDot @@ -51,13 +54,11 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { ); } - const hasStrikethroughStyle = 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; - const textDecorationLineStyle = hasStrikethroughStyle ? styles.underlineLineThrough : {}; - return ( setHasLoadFailed(true)} onMeasure={() => setHasLoadFailed(false)} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index ce822af14cb8..ad7ea87f4c9b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; import {AttachmentContext} from '@components/AttachmentContext'; +import {isDeletedNode} from '@components/HTMLEngineProvider/htmlEngineUtils'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import VideoPlayerPreview from '@components/VideoPlayerPreview'; import useCurrentReportID from '@hooks/useCurrentReportID'; @@ -25,6 +26,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]); const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]); const currentReportIDValue = useCurrentReportID(); + const isDeleted = isDeletedNode(tnode); return ( @@ -39,6 +41,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { thumbnailUrl={thumbnailUrl} videoDimensions={{width, height}} videoDuration={duration} + isDeleted={isDeleted} onShowModalPress={() => { if (!sourceURL || !type) { return; diff --git a/src/components/HTMLEngineProvider/htmlEngineUtils.ts b/src/components/HTMLEngineProvider/htmlEngineUtils.ts index 5f082424a565..fba467add14b 100644 --- a/src/components/HTMLEngineProvider/htmlEngineUtils.ts +++ b/src/components/HTMLEngineProvider/htmlEngineUtils.ts @@ -59,4 +59,12 @@ function isChildOfH1(tnode: TNode): boolean { return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'h1'); } -export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1}; +/** + * Check if the parent node has deleted style. + */ +function isDeletedNode(tnode: TNode): boolean { + const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; + return 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; +} + +export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1, isDeletedNode}; diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index f283058042eb..85f100981f85 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -9,6 +9,7 @@ import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; +import AttachmentDeletedIndicator from './AttachmentDeletedIndicator'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import type {ImageObjectPosition} from './Image/types'; @@ -56,6 +57,9 @@ type ThumbnailImageProps = { /** The object position of image */ objectPosition?: ImageObjectPosition; + /** Whether the image is deleted */ + isDeleted?: boolean; + /** Callback fired when the image fails to load */ onLoadFailure?: () => void; @@ -81,6 +85,7 @@ function ThumbnailImage({ fallbackIconColor, fallbackIconBackground, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL, + isDeleted, onLoadFailure, onMeasure, }: ThumbnailImageProps) { @@ -141,6 +146,7 @@ function ThumbnailImage({ return ( + {isDeleted && } )} - - {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive, isDisabled}) => ( - DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - onLongPress={(event) => { - if (isDisabled) { - return; - } - showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)); - }} - shouldUseHapticsOnLongPress - > - - - - - )} - + {!isDeleted ? ( + + {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive, isDisabled}) => ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={(event) => { + if (isDisabled) { + return; + } + showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)); + }} + shouldUseHapticsOnLongPress + > + + + + + )} + + ) : ( + + )} ); } diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index 2ce65f08fc20..fb188e593949 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -38,9 +38,12 @@ type VideoPlayerPreviewProps = { /** Callback executed when modal is pressed. */ onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; + + /** Whether the video is deleted */ + isDeleted?: boolean; }; -function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress}: VideoPlayerPreviewProps) { +function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress, isDeleted}: VideoPlayerPreviewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -71,11 +74,12 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi return ( - {shouldUseNarrowLayout || isThumbnail ? ( + {shouldUseNarrowLayout || isThumbnail || isDeleted ? ( ) : ( diff --git a/src/pages/home/report/comment/AttachmentCommentFragment.tsx b/src/pages/home/report/comment/AttachmentCommentFragment.tsx index 7d2d81b86e02..f9f86f0a9cd0 100644 --- a/src/pages/home/report/comment/AttachmentCommentFragment.tsx +++ b/src/pages/home/report/comment/AttachmentCommentFragment.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import RenderCommentHTML from './RenderCommentHTML'; @@ -14,8 +13,7 @@ type AttachmentCommentFragmentProps = { function AttachmentCommentFragment({addExtraMargin, html, source, styleAsDeleted}: AttachmentCommentFragmentProps) { const styles = useThemeStyles(); - const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); - const htmlContent = styleAsDeleted && isUploading ? `${html}` : html; + const htmlContent = styleAsDeleted ? `${html}` : html; return ( diff --git a/src/styles/index.ts b/src/styles/index.ts index d60c333ba3d8..58585e396b3d 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1116,6 +1116,17 @@ const styles = (theme: ThemeColors) => height: 25, }, + deletedAttachmentIndicator: { + zIndex: 20, + width: '100%', + height: '100%', + overflow: 'hidden', + }, + + deletedIndicatorOverlay: { + opacity: 0.8, + }, + // Actions actionAvatar: { borderRadius: 20,