Skip to content

Commit

Permalink
Merge pull request #38808 from software-mansion-labs/travel/trip-room…
Browse files Browse the repository at this point in the history
…-preview

[VIP-Travel] Create Trip Room Preview
  • Loading branch information
grgia authored Jun 18, 2024
2 parents 2995925 + 46f7bbe commit f6085d2
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 13 deletions.
4 changes: 3 additions & 1 deletion src/components/ReportActionItem/TripDetailsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SpacerView from '@components/SpacerView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
Expand All @@ -32,6 +33,7 @@ type ReservationViewProps = {
function ReservationView({reservation}: ReservationViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();

const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type);
Expand Down Expand Up @@ -125,7 +127,7 @@ function ReservationView({reservation}: ReservationViewProps) {
onSecondaryInteraction={() => {}}
iconHeight={20}
iconWidth={20}
iconStyles={[styles.tripReservationIconContainer, styles.mr3]}
iconStyles={[StyleUtils.getTripReservationIconContainer(false), styles.mr3]}
secondaryIconFill={theme.icon}
hoverAndPressStyle={styles.hoveredComponentBG}
/>
Expand Down
200 changes: 200 additions & 0 deletions src/components/ReportActionItem/TripRoomPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import React, {useMemo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {FlatList, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import Icon from '@components/Icon';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithoutFeedback} from '@components/Pressable';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import * as TripReservationUtils from '@libs/TripReservationUtils';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import variables from '@styles/variables';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {ReportAction} from '@src/types/onyx';
import type {Reservation} from '@src/types/onyx/Transaction';

type TripRoomPreviewProps = {
/** All the data of the action */
action: ReportAction;

/** The associated chatReport */
chatReportID: string;

/** Extra styles to pass to View wrapper */
containerStyles?: StyleProp<ViewStyle>;

/** Popover context menu anchor, used for showing context menu */
contextMenuAnchor?: ContextMenuAnchor;

/** Callback for updating context menu active state, used for showing context menu */
checkIfContextMenuActive?: () => void;

/** Whether the corresponding report action item is hovered */
isHovered?: boolean;
};

type ReservationViewProps = {
reservation: Reservation;
};

function ReservationView({reservation}: ReservationViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();

const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type);
const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName;

const titleComponent =
reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}>
<Text style={styles.labelStrong}>{reservation.start.shortName}</Text>
<Icon
src={Expensicons.ArrowRightLong}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
fill={theme.icon}
/>
<Text style={styles.labelStrong}>{reservation.end.shortName}</Text>
</View>
) : (
<Text
numberOfLines={1}
style={styles.labelStrong}
>
{title}
</Text>
);

return (
<MenuItemWithTopDescription
description={translate(`travel.${reservation.type}`)}
descriptionTextStyle={styles.textMicro}
titleComponent={titleComponent}
titleContainerStyle={styles.gap1}
secondaryIcon={reservationIcon}
secondaryIconFill={theme.icon}
wrapperStyle={[styles.taskDescriptionMenuItem, styles.p0]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
interactive={false}
iconHeight={variables.iconSizeSmall}
iconWidth={variables.iconSizeSmall}
iconStyles={[StyleUtils.getTripReservationIconContainer(true), styles.mr3]}
isSmallAvatarSubscriptMenu
/>
);
}

const renderItem = ({item}: {item: Reservation}) => <ReservationView reservation={item} />;

function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`);

const tripTransactions = ReportUtils.getTripTransactions(chatReport?.iouReportID, 'reportID');
const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions);
const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : '';
const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport);

const displayAmount = useMemo(() => {
if (totalDisplaySpend) {
return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency);
}

// If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere")
let displayAmountValue = '';
const actionMessage = action.message?.[0]?.text ?? '';
const splits = actionMessage.split(' ');

splits.forEach((split) => {
if (!/\d/.test(split)) {
return;
}

displayAmountValue = split;
});

return displayAmountValue;
}, [action.message, iouReport?.currency, totalDisplaySpend]);

return (
<OfflineWithFeedback
pendingAction={action?.pendingAction}
shouldDisableOpacity={!!(action.pendingAction ?? action.isOptimisticAction)}
needsOffscreenAlphaCompositing
>
<View style={[styles.chatItemMessage, containerStyles]}>
<PressableWithoutFeedback
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)}
shouldUseHapticsOnLongPress
style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox, styles.cursorDefault]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('iou.viewDetails')}
>
<View style={[styles.moneyRequestPreviewBox, styles.p4, styles.gap5, isHovered ? styles.reportPreviewBoxHoverBorder : undefined]}>
<View style={styles.expenseAndReportPreviewTextContainer}>
<View style={styles.reportPreviewAmountSubtitleContainer}>
<View style={styles.flexRow}>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<Text style={[styles.textLabelSupporting, styles.lh16]}>
{translate('travel.trip')}{dateInfo}
</Text>
</View>
</View>
</View>
<View style={styles.reportPreviewAmountSubtitleContainer}>
<View style={styles.flexRow}>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<Text style={styles.textHeadlineH2}>{displayAmount}</Text>
</View>
</View>
<View style={styles.flexRow}>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<Text style={[styles.textLabelSupporting, styles.textNormal, styles.lh20]}>{chatReport?.reportName}</Text>
</View>
</View>
</View>
</View>
<FlatList
data={reservations}
style={styles.gap3}
renderItem={renderItem}
/>
<Button
medium
success
text={translate('travel.viewTrip')}
onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chatReportID))}
/>
</View>
</PressableWithoutFeedback>
</View>
</OfflineWithFeedback>
);
}

TripRoomPreview.displayName = 'TripRoomPreview';

export default TripRoomPreview;
13 changes: 13 additions & 0 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,9 +562,14 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry<ReportAction>, key:
return false;
}

if (isTripPreview(reportAction)) {
return true;
}

// All other actions are displayed except thread parents, deleted, or non-pending actions
const isDeleted = isDeletedAction(reportAction);
const isPending = !!reportAction.pendingAction;

return !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction);
}

Expand Down Expand Up @@ -1239,6 +1244,13 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry<ReportAction
return currentUserAccountID === reportAction?.actorAccountID;
}

/**
* Check if the report action is the trip preview
*/
function isTripPreview(reportAction: OnyxEntry<ReportAction>): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.TRIPPREVIEW;
}

export {
extractLinksFromMessageHtml,
getDismissedViolationMessageText,
Expand Down Expand Up @@ -1309,6 +1321,7 @@ export {
isLinkedTransactionHeld,
wasActionTakenByCurrentUser,
isResolvedActionTrackExpense,
isTripPreview,
};

export type {LastVisibleMessage};
4 changes: 2 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6772,9 +6772,9 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxInputOrEntry<R
return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport);
}

function getTripTransactions(tripRoomReportID: string | undefined): Transaction[] {
function getTripTransactions(tripRoomReportID: string | undefined, reportFieldToCompare: 'parentReportID' | 'reportID' = 'parentReportID'): Transaction[] {
const tripTransactionReportIDs = Object.values(allReports ?? {})
.filter((report) => report && report?.parentReportID === tripRoomReportID)
.filter((report) => report && report?.[reportFieldToCompare] === tripRoomReportID)
.map((report) => report?.reportID);
return tripTransactionReportIDs.flatMap((reportID) => TransactionUtils.getAllReportTransactions(reportID));
}
Expand Down
5 changes: 4 additions & 1 deletion src/pages/home/report/ContextMenu/ContextMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ const ContextMenuActions: ContextMenuAction[] = [
successTextTranslateKey: 'reportActionContextMenu.copied',
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !ReportActionsUtils.isReportActionAttachment(reportAction) && !ReportActionsUtils.isMessageDeleted(reportAction),
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
!ReportActionsUtils.isReportActionAttachment(reportAction) &&
!ReportActionsUtils.isMessageDeleted(reportAction) &&
!ReportActionsUtils.isTripPreview(reportAction),

// If return value is true, we switch the `text` and `icon` on
// `ContextMenuItem` with `successText` and `successIcon` which will fall back to
Expand Down
12 changes: 12 additions & 0 deletions src/pages/home/report/ReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import TaskAction from '@components/ReportActionItem/TaskAction';
import TaskPreview from '@components/ReportActionItem/TaskPreview';
import TaskView from '@components/ReportActionItem/TaskView';
import TripDetailsView from '@components/ReportActionItem/TripDetailsView';
import TripRoomPreview from '@components/ReportActionItem/TripRoomPreview';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import SpacerView from '@components/SpacerView';
import Text from '@components/Text';
Expand Down Expand Up @@ -541,6 +542,17 @@ function ReportActionItem({
isWhisper={isWhisper}
/>
);
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.TRIPPREVIEW) {
children = (
<TripRoomPreview
action={action}
chatReportID={action.originalMessage.linkedReportID}
isHovered={hovered}
contextMenuAnchor={popoverAnchorRef.current}
containerStyles={displayAsGroup ? [] : [styles.mt2]}
checkIfContextMenuActive={toggleContextMenuFromActiveReportAction}
/>
);
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
children = ReportUtils.isClosedExpenseReportWithNoExpenses(iouReport) ? (
<RenderHTML html={`<comment>${translate('parentReportAction.deletedReport')}</comment>`} />
Expand Down
9 changes: 0 additions & 9 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4993,15 +4993,6 @@ const styles = (theme: ThemeColors) =>
flex: 1,
},

tripReservationIconContainer: {
width: variables.avatarSizeNormal,
height: variables.avatarSizeNormal,
backgroundColor: theme.border,
borderRadius: variables.componentBorderRadiusXLarge,
alignItems: 'center',
justifyContent: 'center',
},

textLineThrough: {
textDecorationLine: 'line-through',
},
Expand Down
9 changes: 9 additions & 0 deletions src/styles/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,15 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
...StyleSheet.flatten(descriptionTextStyle),
opacity: styles.opacitySemiTransparent.opacity,
}),

getTripReservationIconContainer: (isSmallIcon: boolean): StyleProp<ViewStyle> => ({
width: isSmallIcon ? variables.avatarSizeSmallNormal : variables.avatarSizeNormal,
height: isSmallIcon ? variables.avatarSizeSmallNormal : variables.avatarSizeNormal,
borderRadius: isSmallIcon ? variables.avatarSizeSmallNormal : variables.componentBorderRadiusXLarge,
backgroundColor: theme.border,
alignItems: 'center',
justifyContent: 'center',
}),
});

type StyleUtilsType = ReturnType<typeof createStyleUtils>;
Expand Down

0 comments on commit f6085d2

Please sign in to comment.