Skip to content

Commit

Permalink
Merge pull request Expensify#24564 from bernhardoj/fix/22915-hide-att…
Browse files Browse the repository at this point in the history
…achment-in-carousel

Hide flagged attachment in attachment carousel
  • Loading branch information
amyevans authored Sep 12, 2023
2 parents 83a448b + 4df91a2 commit 6188d7b
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -58,6 +59,7 @@ function App() {
KeyboardStateProvider,
PopoverContextProvider,
CurrentReportIDContextProvider,
ReportAttachmentsProvider,
PickerStateProvider,
EnvironmentProvider,
ThemeProvider,
Expand Down
115 changes: 115 additions & 0 deletions src/components/Attachments/AttachmentCarousel/CarouselItem.js
Original file line number Diff line number Diff line change
@@ -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) => (
<Button
small
style={style}
onPress={() => setIsHidden(!isHidden)}
>
<Text
style={styles.buttonSmallText}
selectable={false}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')}
</Text>
</Button>
);

if (isHidden) {
const children = (
<>
<Text style={[styles.textLabelSupporting, styles.textAlignCenter, styles.lh20]}>{translate('moderation.flaggedContent')}</Text>
{renderButton([styles.mt2])}
</>
);
return onPress ? (
<PressableWithoutFeedback
style={[styles.attachmentRevealButtonContainer]}
onPress={onPress}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
>
{children}
</PressableWithoutFeedback>
) : (
<View style={[styles.attachmentRevealButtonContainer]}>{children}</View>
);
}

return (
<View style={[styles.flex1]}>
<View style={[styles.flex1]}>
<AttachmentView
source={item.source}
file={item.file}
isAuthTokenRequired={item.isAuthTokenRequired}
isFocused={isFocused}
onPress={onPress}
isUsedInCarousel
/>
</View>

{item.hasBeenFlagged && (
<SafeAreaConsumer>
{({safeAreaPaddingBottomStyle}) => <View style={[styles.appBG, safeAreaPaddingBottomStyle]}>{renderButton([styles.m4, styles.alignSelfCenter])}</View>}
</SafeAreaConsumer>
)}
</View>
);
}

CarouselItem.propTypes = propTypes;
CarouselItem.defaultProps = defaultProps;

export default CarouselItem;
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
},
});
Expand Down Expand Up @@ -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();

Expand Down
11 changes: 5 additions & 6 deletions src/components/Attachments/AttachmentCarousel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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}) => (
<AttachmentView
source={item.source}
file={item.file}
isAuthTokenRequired={item.isAuthTokenRequired}
<CarouselItem
item={item}
isFocused={activeSource === item.source}
onPress={canUseTouchScreen ? () => setShouldShowArrows(!shouldShowArrows) : undefined}
isUsedInCarousel
/>
),
[activeSource, setShouldShowArrows, shouldShowArrows],
Expand Down
11 changes: 4 additions & 7 deletions src/components/Attachments/AttachmentCarousel/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}) => (
<AttachmentView
source={item.source}
file={item.file}
isAuthTokenRequired={item.isAuthTokenRequired}
<CarouselItem
item={item}
isFocused={activeSource === item.source}
isUsedInCarousel
onPress={() => setShouldShowArrows(!shouldShowArrows)}
/>
),
Expand Down
16 changes: 15 additions & 1 deletion src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -129,13 +130,26 @@ 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([]);
const prevDraftMessage = usePrevious(props.draftMessage);
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,
Expand Down Expand Up @@ -362,7 +376,7 @@ function ReportActionItem(props) {
<Button
small
style={[styles.mt2, styles.alignSelfStart]}
onPress={() => setIsHidden(!isHidden)}
onPress={() => updateHiddenState(!isHidden)}
>
<Text
style={styles.buttonSmallText}
Expand Down
42 changes: 42 additions & 0 deletions src/pages/home/report/ReportAttachmentsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, {useEffect, useMemo, useRef} from 'react';
import PropTypes from 'prop-types';
import useCurrentReportID from '../../../hooks/useCurrentReportID';

const ReportAttachmentsContext = React.createContext();

const propTypes = {
/** Rendered child component */
children: PropTypes.node.isRequired,
};

function ReportAttachmentsProvider(props) {
const currentReportID = useCurrentReportID();
const hiddenAttachments = useRef({});

useEffect(() => {
// We only want to store the attachment visibility for the current report.
// If the current report ID changes, clear the ref.
hiddenAttachments.current = {};
}, [currentReportID]);

const contextValue = useMemo(
() => ({
isAttachmentHidden: (reportActionID) => hiddenAttachments.current[reportActionID],
updateHiddenAttachments: (reportActionID, value) => {
hiddenAttachments.current = {
...hiddenAttachments.current,
[reportActionID]: value,
};
},
}),
[],
);

return <ReportAttachmentsContext.Provider value={contextValue}>{props.children}</ReportAttachmentsContext.Provider>;
}

ReportAttachmentsProvider.propTypes = propTypes;
ReportAttachmentsProvider.displayName = 'ReportAttachmentsProvider';

export default ReportAttachmentsContext;
export {ReportAttachmentsProvider};
7 changes: 7 additions & 0 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2553,6 +2553,13 @@ const styles = {
position: 'absolute',
},

attachmentRevealButtonContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
...spacing.ph4,
},

arrowIcon: {
height: 40,
width: 40,
Expand Down

0 comments on commit 6188d7b

Please sign in to comment.