diff --git a/src/App.js b/src/App.js
index 7ec82b9a4f8a..284c6115d7b8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -22,6 +22,7 @@ import ThemeProvider from './styles/themes/ThemeProvider';
import ThemeStylesProvider from './styles/ThemeStylesProvider';
import {CurrentReportIDContextProvider} from './components/withCurrentReportID';
import {EnvironmentProvider} from './components/withEnvironment';
+import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
import * as Session from './libs/actions/Session';
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
@@ -58,6 +59,7 @@ function App() {
KeyboardStateProvider,
PopoverContextProvider,
CurrentReportIDContextProvider,
+ ReportAttachmentsProvider,
PickerStateProvider,
EnvironmentProvider,
ThemeProvider,
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js
new file mode 100644
index 000000000000..3aeef8482e2d
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js
@@ -0,0 +1,115 @@
+import React, {useContext, useState} from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import CONST from '../../../CONST';
+import styles from '../../../styles/styles';
+import useLocalize from '../../../hooks/useLocalize';
+import PressableWithoutFeedback from '../../Pressable/PressableWithoutFeedback';
+import Text from '../../Text';
+import Button from '../../Button';
+import AttachmentView from '../AttachmentView';
+import SafeAreaConsumer from '../../SafeAreaConsumer';
+import ReportAttachmentsContext from '../../../pages/home/report/ReportAttachmentsContext';
+
+const propTypes = {
+ /** 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,
+
+ /** The source (URL) of the attachment */
+ source: PropTypes.string,
+
+ /** Additional information about the attachment file */
+ file: PropTypes.shape({
+ /** File name of the attachment */
+ name: PropTypes.string,
+ }),
+
+ /** Whether the attachment has been flagged */
+ hasBeenFlagged: PropTypes.bool,
+ }).isRequired,
+
+ /** Whether the attachment is currently being viewed in the carousel */
+ isFocused: PropTypes.bool.isRequired,
+
+ /** onPress callback */
+ onPress: PropTypes.func,
+};
+
+const defaultProps = {
+ onPress: undefined,
+};
+
+function CarouselItem({item, isFocused, onPress}) {
+ 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 renderButton = (style) => (
+
+ );
+
+ if (isHidden) {
+ const children = (
+ <>
+ {translate('moderation.flaggedContent')}
+ {renderButton([styles.mt2])}
+ >
+ );
+ return onPress ? (
+
+ {children}
+
+ ) : (
+ {children}
+ );
+ }
+
+ return (
+
+
+
+
+
+ {item.hasBeenFlagged && (
+
+ {({safeAreaPaddingBottomStyle}) => {renderButton([styles.m4, styles.alignSelfCenter])}}
+
+ )}
+
+ );
+}
+
+CarouselItem.propTypes = propTypes;
+CarouselItem.defaultProps = defaultProps;
+
+export default CarouselItem;
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
index b967d5ab0066..d5da25c89576 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
@@ -28,10 +28,12 @@ function extractAttachmentsFromReport(report, reportActions) {
// By iterating actions in chronological order and prepending each attachment
// we ensure correct order of attachments even across actions with multiple attachments.
attachments.unshift({
+ reportActionID: attribs['data-id'],
source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
isAuthTokenRequired: Boolean(expensifySource),
file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]},
isReceipt: false,
+ hasBeenFlagged: attribs['data-flagged'] === 'true',
});
},
});
@@ -62,7 +64,10 @@ function extractAttachmentsFromReport(report, reportActions) {
}
}
- htmlParser.write(_.get(action, ['message', 0, 'html']));
+ const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], '');
+ const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN;
+ const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
+ htmlParser.write(html);
});
htmlParser.end();
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 53c2c840d95d..6f53f2e1fe7e 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -5,7 +5,6 @@ import _ from 'underscore';
import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
import styles from '../../../styles/styles';
import CarouselActions from './CarouselActions';
-import AttachmentView from '../AttachmentView';
import withWindowDimensions from '../../withWindowDimensions';
import CarouselButtons from './CarouselButtons';
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
@@ -15,6 +14,7 @@ import withLocalize from '../../withLocalize';
import compose from '../../../libs/compose';
import useCarouselArrows from './useCarouselArrows';
import useWindowDimensions from '../../../hooks/useWindowDimensions';
+import CarouselItem from './CarouselItem';
import Navigation from '../../../libs/Navigation/Navigation';
import BlockingView from '../../BlockingViews/BlockingView';
import * as Illustrations from '../../Icon/Illustrations';
@@ -143,21 +143,20 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
/**
* Defines how a single attachment should be rendered
* @param {Object} item
+ * @param {String} item.reportActionID
* @param {Boolean} item.isAuthTokenRequired
* @param {String} item.source
* @param {Object} item.file
* @param {String} item.file.name
+ * @param {Boolean} item.hasBeenFlagged
* @returns {JSX.Element}
*/
const renderItem = useCallback(
({item}) => (
- setShouldShowArrows(!shouldShowArrows) : undefined}
- isUsedInCarousel
/>
),
[activeSource, setShouldShowArrows, shouldShowArrows],
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index 4162cfae88e9..95cda7c2f5c9 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -5,11 +5,11 @@ import _ from 'underscore';
import AttachmentCarouselPager from './Pager';
import styles from '../../../styles/styles';
import CarouselButtons from './CarouselButtons';
-import AttachmentView from '../AttachmentView';
import ONYXKEYS from '../../../ONYXKEYS';
import {propTypes, defaultProps} from './attachmentCarouselPropTypes';
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
import useCarouselArrows from './useCarouselArrows';
+import CarouselItem from './CarouselItem';
import Navigation from '../../../libs/Navigation/Navigation';
import BlockingView from '../../BlockingViews/BlockingView';
import * as Illustrations from '../../Icon/Illustrations';
@@ -85,17 +85,14 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose,
/**
* Defines how a single attachment should be rendered
- * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
+ * @param {{ reportActionID: String, isAuthTokenRequired: Boolean, source: String, file: { name: String }, hasBeenFlagged: Boolean }} item
* @returns {JSX.Element}
*/
const renderItem = useCallback(
({item}) => (
- setShouldShowArrows(!shouldShowArrows)}
/>
),
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 8425f78a3a10..34430de87751 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -67,6 +67,7 @@ import * as BankAccounts from '../../../libs/actions/BankAccounts';
import usePrevious from '../../../hooks/usePrevious';
import ReportScreenContext from '../ReportScreenContext';
import Permissions from '../../../libs/Permissions';
+import ReportAttachmentsContext from './ReportAttachmentsContext';
const propTypes = {
...windowDimensionsPropTypes,
@@ -129,6 +130,7 @@ function ReportActionItem(props) {
const [isHidden, setIsHidden] = useState(false);
const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
const {reactionListRef} = useContext(ReportScreenContext);
+ const {updateHiddenAttachments} = useContext(ReportAttachmentsContext);
const textInputRef = useRef();
const popoverAnchorRef = useRef();
const downloadedPreviews = useRef([]);
@@ -136,6 +138,18 @@ function ReportActionItem(props) {
const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);
+ const updateHiddenState = useCallback(
+ (isHiddenValue) => {
+ setIsHidden(isHiddenValue);
+ const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message));
+ if (!isAttachment) {
+ return;
+ }
+ updateHiddenAttachments(props.action.reportActionID, isHiddenValue);
+ },
+ [props.action.reportActionID, props.action.message, updateHiddenAttachments],
+ );
+
useEffect(
() => () => {
// ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components,
@@ -361,7 +375,7 @@ function ReportActionItem(props) {