-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[VIP-Travel] Create Trip Room Preview #38808
Changes from 50 commits
1279f0f
370d8a8
102ffb8
489720b
5d16a01
7036238
20caa2f
b8b20ee
a9c5370
b4b3730
71419d4
8b25ebc
18c8503
9eb79a2
56e19f7
993f684
f88bec0
3cbb84c
c53d977
f8d3f34
e8390c5
6eef035
01a7e2d
3a85f3d
05a7613
fd6418c
8219de2
3262a35
5e1d9b9
658d003
1776dcc
70204a0
4720a06
cc1a8e3
d753bb4
faeadcc
ca0b2e7
bfc8b82
19ce931
af036c8
545b1d8
24e9d8f
ad04386
deeb5c1
7ea02b9
9cbffe5
9b12b07
bc12893
cddd0ae
6c6078b
109530a
6b81aa4
2ca5697
9259a3a
26230ca
d5c0016
89ed40b
7f88ab2
d723732
fad5159
7e89f2d
6ed35e5
89c4d18
7118a42
4b60448
42b5f3f
46f7bbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
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 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 a message is a whisper */ | ||
isWhisper?: boolean; | ||
|
||
/** 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 {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}`)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a default here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
descriptionTextStyle={styles.textMicro} | ||
titleComponent={titleComponent} | ||
titleContainerStyle={styles.tripReservationTitleGap} | ||
secondaryIcon={reservationIcon} | ||
shouldShowRightIcon={false} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few of these |
||
wrapperStyle={[styles.taskDescriptionMenuItem, styles.p0]} | ||
shouldGreyOutWhenDisabled={false} | ||
numberOfLinesTitle={0} | ||
interactive={false} | ||
shouldStackHorizontally={false} | ||
hoverAndPressStyle={false} | ||
iconHeight={variables.iconSizeSmall} | ||
iconWidth={variables.iconSizeSmall} | ||
iconStyles={[styles.tripReservationIconContainer(true), styles.mr3]} | ||
secondaryIconFill={theme.icon} | ||
isSmallAvatarSubscriptMenu | ||
/> | ||
); | ||
} | ||
|
||
const renderItem = ({item}: {item: Reservation}) => <ReservationView reservation={item} />; | ||
|
||
function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, isWhisper = 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 || isWhisper ? styles.reportPreviewBoxHoverBorder : undefined]}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we using the whisper pattern here? Could you show me the case for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, we don't have this case in the app, I removed that |
||
<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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -563,7 +563,12 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry<ReportAction>, key: | |
// 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); | ||
|
||
// @TODO: isTripRoomPreview condition has been added to make the TripRoomPreview component visible. | ||
// Remove it when the reportAction.message array is not empty for this type, then !isDeleted will be true. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What issue will fix this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I described this issue here: https://swmansion.slack.com/archives/C05S5EV2JTX/p1716989867708889?thread_ts=1716242660.491669&cid=C05S5EV2JTX. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there any conditions when TripRoomPreview should be visible and when not? If it should be always visible, I'll refactor this code and remove the comment cc: @stitesExpensify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that it should always be visible |
||
const isTripRoomPreview = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.TRIPPREVIEW; | ||
|
||
return isTripRoomPreview || !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4971,13 +4971,17 @@ const styles = (theme: ThemeColors) => | |
flex: 1, | ||
}, | ||
|
||
tripReservationIconContainer: { | ||
width: variables.avatarSizeNormal, | ||
height: variables.avatarSizeNormal, | ||
tripReservationIconContainer: (isSmallIcon: boolean) => ({ | ||
width: isSmallIcon ? variables.avatarSizeSmallNormal : variables.avatarSizeNormal, | ||
height: isSmallIcon ? variables.avatarSizeSmallNormal : variables.avatarSizeNormal, | ||
borderRadius: isSmallIcon ? variables.avatarSizeSmallNormal : variables.componentBorderRadiusXLarge, | ||
backgroundColor: theme.border, | ||
borderRadius: variables.componentBorderRadiusXLarge, | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
}), | ||
|
||
tripReservationTitleGap: { | ||
gap: 2, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you share a quick screenshot of where this is used again? I vaguely recall a quick conversation where we decided that maybe it's better to use 4. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sweet, thanks! |
||
}, | ||
|
||
textLineThrough: { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this style should go in StyleUtils, I don't think we use many functions in the styles file. WDYT?