Skip to content

Commit 6188d7b

Browse files
authored
Merge pull request #24564 from bernhardoj/fix/22915-hide-attachment-in-carousel
Hide flagged attachment in attachment carousel
2 parents 83a448b + 4df91a2 commit 6188d7b

File tree

8 files changed

+196
-15
lines changed

8 files changed

+196
-15
lines changed

src/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import ThemeProvider from './styles/themes/ThemeProvider';
2222
import ThemeStylesProvider from './styles/ThemeStylesProvider';
2323
import {CurrentReportIDContextProvider} from './components/withCurrentReportID';
2424
import {EnvironmentProvider} from './components/withEnvironment';
25+
import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
2526
import * as Session from './libs/actions/Session';
2627
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
2728
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
@@ -58,6 +59,7 @@ function App() {
5859
KeyboardStateProvider,
5960
PopoverContextProvider,
6061
CurrentReportIDContextProvider,
62+
ReportAttachmentsProvider,
6163
PickerStateProvider,
6264
EnvironmentProvider,
6365
ThemeProvider,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, {useContext, useState} from 'react';
2+
import {View} from 'react-native';
3+
import PropTypes from 'prop-types';
4+
import CONST from '../../../CONST';
5+
import styles from '../../../styles/styles';
6+
import useLocalize from '../../../hooks/useLocalize';
7+
import PressableWithoutFeedback from '../../Pressable/PressableWithoutFeedback';
8+
import Text from '../../Text';
9+
import Button from '../../Button';
10+
import AttachmentView from '../AttachmentView';
11+
import SafeAreaConsumer from '../../SafeAreaConsumer';
12+
import ReportAttachmentsContext from '../../../pages/home/report/ReportAttachmentsContext';
13+
14+
const propTypes = {
15+
/** Attachment required information such as the source and file name */
16+
item: PropTypes.shape({
17+
/** Report action ID of the attachment */
18+
reportActionID: PropTypes.string,
19+
20+
/** Whether source URL requires authentication */
21+
isAuthTokenRequired: PropTypes.bool,
22+
23+
/** The source (URL) of the attachment */
24+
source: PropTypes.string,
25+
26+
/** Additional information about the attachment file */
27+
file: PropTypes.shape({
28+
/** File name of the attachment */
29+
name: PropTypes.string,
30+
}),
31+
32+
/** Whether the attachment has been flagged */
33+
hasBeenFlagged: PropTypes.bool,
34+
}).isRequired,
35+
36+
/** Whether the attachment is currently being viewed in the carousel */
37+
isFocused: PropTypes.bool.isRequired,
38+
39+
/** onPress callback */
40+
onPress: PropTypes.func,
41+
};
42+
43+
const defaultProps = {
44+
onPress: undefined,
45+
};
46+
47+
function CarouselItem({item, isFocused, onPress}) {
48+
const {translate} = useLocalize();
49+
const {isAttachmentHidden} = useContext(ReportAttachmentsContext);
50+
// eslint-disable-next-line es/no-nullish-coalescing-operators
51+
const [isHidden, setIsHidden] = useState(isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged);
52+
53+
const renderButton = (style) => (
54+
<Button
55+
small
56+
style={style}
57+
onPress={() => setIsHidden(!isHidden)}
58+
>
59+
<Text
60+
style={styles.buttonSmallText}
61+
selectable={false}
62+
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
63+
>
64+
{isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')}
65+
</Text>
66+
</Button>
67+
);
68+
69+
if (isHidden) {
70+
const children = (
71+
<>
72+
<Text style={[styles.textLabelSupporting, styles.textAlignCenter, styles.lh20]}>{translate('moderation.flaggedContent')}</Text>
73+
{renderButton([styles.mt2])}
74+
</>
75+
);
76+
return onPress ? (
77+
<PressableWithoutFeedback
78+
style={[styles.attachmentRevealButtonContainer]}
79+
onPress={onPress}
80+
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
81+
accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
82+
>
83+
{children}
84+
</PressableWithoutFeedback>
85+
) : (
86+
<View style={[styles.attachmentRevealButtonContainer]}>{children}</View>
87+
);
88+
}
89+
90+
return (
91+
<View style={[styles.flex1]}>
92+
<View style={[styles.flex1]}>
93+
<AttachmentView
94+
source={item.source}
95+
file={item.file}
96+
isAuthTokenRequired={item.isAuthTokenRequired}
97+
isFocused={isFocused}
98+
onPress={onPress}
99+
isUsedInCarousel
100+
/>
101+
</View>
102+
103+
{item.hasBeenFlagged && (
104+
<SafeAreaConsumer>
105+
{({safeAreaPaddingBottomStyle}) => <View style={[styles.appBG, safeAreaPaddingBottomStyle]}>{renderButton([styles.m4, styles.alignSelfCenter])}</View>}
106+
</SafeAreaConsumer>
107+
)}
108+
</View>
109+
);
110+
}
111+
112+
CarouselItem.propTypes = propTypes;
113+
CarouselItem.defaultProps = defaultProps;
114+
115+
export default CarouselItem;

src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ function extractAttachmentsFromReport(report, reportActions) {
2828
// By iterating actions in chronological order and prepending each attachment
2929
// we ensure correct order of attachments even across actions with multiple attachments.
3030
attachments.unshift({
31+
reportActionID: attribs['data-id'],
3132
source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
3233
isAuthTokenRequired: Boolean(expensifySource),
3334
file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]},
3435
isReceipt: false,
36+
hasBeenFlagged: attribs['data-flagged'] === 'true',
3537
});
3638
},
3739
});
@@ -62,7 +64,10 @@ function extractAttachmentsFromReport(report, reportActions) {
6264
}
6365
}
6466

65-
htmlParser.write(_.get(action, ['message', 0, 'html']));
67+
const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], '');
68+
const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN;
69+
const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
70+
htmlParser.write(html);
6671
});
6772
htmlParser.end();
6873

src/components/Attachments/AttachmentCarousel/index.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import _ from 'underscore';
55
import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
66
import styles from '../../../styles/styles';
77
import CarouselActions from './CarouselActions';
8-
import AttachmentView from '../AttachmentView';
98
import withWindowDimensions from '../../withWindowDimensions';
109
import CarouselButtons from './CarouselButtons';
1110
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
@@ -15,6 +14,7 @@ import withLocalize from '../../withLocalize';
1514
import compose from '../../../libs/compose';
1615
import useCarouselArrows from './useCarouselArrows';
1716
import useWindowDimensions from '../../../hooks/useWindowDimensions';
17+
import CarouselItem from './CarouselItem';
1818
import Navigation from '../../../libs/Navigation/Navigation';
1919
import BlockingView from '../../BlockingViews/BlockingView';
2020
import * as Illustrations from '../../Icon/Illustrations';
@@ -143,21 +143,20 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
143143
/**
144144
* Defines how a single attachment should be rendered
145145
* @param {Object} item
146+
* @param {String} item.reportActionID
146147
* @param {Boolean} item.isAuthTokenRequired
147148
* @param {String} item.source
148149
* @param {Object} item.file
149150
* @param {String} item.file.name
151+
* @param {Boolean} item.hasBeenFlagged
150152
* @returns {JSX.Element}
151153
*/
152154
const renderItem = useCallback(
153155
({item}) => (
154-
<AttachmentView
155-
source={item.source}
156-
file={item.file}
157-
isAuthTokenRequired={item.isAuthTokenRequired}
156+
<CarouselItem
157+
item={item}
158158
isFocused={activeSource === item.source}
159159
onPress={canUseTouchScreen ? () => setShouldShowArrows(!shouldShowArrows) : undefined}
160-
isUsedInCarousel
161160
/>
162161
),
163162
[activeSource, setShouldShowArrows, shouldShowArrows],

src/components/Attachments/AttachmentCarousel/index.native.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import _ from 'underscore';
55
import AttachmentCarouselPager from './Pager';
66
import styles from '../../../styles/styles';
77
import CarouselButtons from './CarouselButtons';
8-
import AttachmentView from '../AttachmentView';
98
import ONYXKEYS from '../../../ONYXKEYS';
109
import {propTypes, defaultProps} from './attachmentCarouselPropTypes';
1110
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
1211
import useCarouselArrows from './useCarouselArrows';
12+
import CarouselItem from './CarouselItem';
1313
import Navigation from '../../../libs/Navigation/Navigation';
1414
import BlockingView from '../../BlockingViews/BlockingView';
1515
import * as Illustrations from '../../Icon/Illustrations';
@@ -85,17 +85,14 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose,
8585

8686
/**
8787
* Defines how a single attachment should be rendered
88-
* @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
88+
* @param {{ reportActionID: String, isAuthTokenRequired: Boolean, source: String, file: { name: String }, hasBeenFlagged: Boolean }} item
8989
* @returns {JSX.Element}
9090
*/
9191
const renderItem = useCallback(
9292
({item}) => (
93-
<AttachmentView
94-
source={item.source}
95-
file={item.file}
96-
isAuthTokenRequired={item.isAuthTokenRequired}
93+
<CarouselItem
94+
item={item}
9795
isFocused={activeSource === item.source}
98-
isUsedInCarousel
9996
onPress={() => setShouldShowArrows(!shouldShowArrows)}
10097
/>
10198
),

src/pages/home/report/ReportActionItem.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import * as BankAccounts from '../../../libs/actions/BankAccounts';
6767
import usePrevious from '../../../hooks/usePrevious';
6868
import ReportScreenContext from '../ReportScreenContext';
6969
import Permissions from '../../../libs/Permissions';
70+
import ReportAttachmentsContext from './ReportAttachmentsContext';
7071

7172
const propTypes = {
7273
...windowDimensionsPropTypes,
@@ -129,13 +130,26 @@ function ReportActionItem(props) {
129130
const [isHidden, setIsHidden] = useState(false);
130131
const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
131132
const {reactionListRef} = useContext(ReportScreenContext);
133+
const {updateHiddenAttachments} = useContext(ReportAttachmentsContext);
132134
const textInputRef = useRef();
133135
const popoverAnchorRef = useRef();
134136
const downloadedPreviews = useRef([]);
135137
const prevDraftMessage = usePrevious(props.draftMessage);
136138
const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
137139
const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);
138140

141+
const updateHiddenState = useCallback(
142+
(isHiddenValue) => {
143+
setIsHidden(isHiddenValue);
144+
const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message));
145+
if (!isAttachment) {
146+
return;
147+
}
148+
updateHiddenAttachments(props.action.reportActionID, isHiddenValue);
149+
},
150+
[props.action.reportActionID, props.action.message, updateHiddenAttachments],
151+
);
152+
139153
useEffect(
140154
() => () => {
141155
// ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components,
@@ -362,7 +376,7 @@ function ReportActionItem(props) {
362376
<Button
363377
small
364378
style={[styles.mt2, styles.alignSelfStart]}
365-
onPress={() => setIsHidden(!isHidden)}
379+
onPress={() => updateHiddenState(!isHidden)}
366380
>
367381
<Text
368382
style={styles.buttonSmallText}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, {useEffect, useMemo, useRef} from 'react';
2+
import PropTypes from 'prop-types';
3+
import useCurrentReportID from '../../../hooks/useCurrentReportID';
4+
5+
const ReportAttachmentsContext = React.createContext();
6+
7+
const propTypes = {
8+
/** Rendered child component */
9+
children: PropTypes.node.isRequired,
10+
};
11+
12+
function ReportAttachmentsProvider(props) {
13+
const currentReportID = useCurrentReportID();
14+
const hiddenAttachments = useRef({});
15+
16+
useEffect(() => {
17+
// We only want to store the attachment visibility for the current report.
18+
// If the current report ID changes, clear the ref.
19+
hiddenAttachments.current = {};
20+
}, [currentReportID]);
21+
22+
const contextValue = useMemo(
23+
() => ({
24+
isAttachmentHidden: (reportActionID) => hiddenAttachments.current[reportActionID],
25+
updateHiddenAttachments: (reportActionID, value) => {
26+
hiddenAttachments.current = {
27+
...hiddenAttachments.current,
28+
[reportActionID]: value,
29+
};
30+
},
31+
}),
32+
[],
33+
);
34+
35+
return <ReportAttachmentsContext.Provider value={contextValue}>{props.children}</ReportAttachmentsContext.Provider>;
36+
}
37+
38+
ReportAttachmentsProvider.propTypes = propTypes;
39+
ReportAttachmentsProvider.displayName = 'ReportAttachmentsProvider';
40+
41+
export default ReportAttachmentsContext;
42+
export {ReportAttachmentsProvider};

src/styles/styles.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2553,6 +2553,13 @@ const styles = {
25532553
position: 'absolute',
25542554
},
25552555

2556+
attachmentRevealButtonContainer: {
2557+
flex: 1,
2558+
alignItems: 'center',
2559+
justifyContent: 'center',
2560+
...spacing.ph4,
2561+
},
2562+
25562563
arrowIcon: {
25572564
height: 40,
25582565
width: 40,

0 commit comments

Comments
 (0)