From 93edf8b7f345569a3485fc566b4c632f717e009b Mon Sep 17 00:00:00 2001 From: zfurtak Date: Tue, 23 Jul 2024 15:10:20 +0200 Subject: [PATCH 001/149] Enable name in the filter function --- src/libs/OptionsListUtils.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ebe4ffdbe53a..14fcdee1875c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -179,6 +179,7 @@ type GetOptionsConfig = { includeInvoiceRooms?: boolean; includeDomainEmail?: boolean; action?: IOUAction; + shouldAcceptName?: boolean; }; type GetUserToInviteConfig = { @@ -189,6 +190,7 @@ type GetUserToInviteConfig = { betas: OnyxEntry; reportActions?: ReportActions; showChatPreviewLine?: boolean; + shouldAcceptName?: boolean; }; type MemberForList = { @@ -222,7 +224,7 @@ type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: bo type FilterOptionsConfig = Pick< GetOptionsConfig, - 'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' + 'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' | 'shouldAcceptName' > & {preferChatroomsOverThreads?: boolean; includeChatRoomsByParticipants?: boolean}; type HasText = { @@ -1698,6 +1700,7 @@ function canCreateOptimisticPersonalDetailOption({ * We create a new user option if the following conditions are satisfied: * - There's no matching recent report and personal detail option * - The searchValue is a valid email or phone number + * - If prop shouldAcceptName = true, the searchValue can be also a normal string * - The searchValue isn't the current personal detail login * - We can use chronos or the search value is not the chronos email */ @@ -1709,6 +1712,7 @@ function getUserToInviteOption({ betas, reportActions = {}, showChatPreviewLine = false, + shouldAcceptName = false, }: GetUserToInviteConfig): ReportUtils.OptionData | null { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue))); const isCurrentUserLogin = isCurrentUser({login: searchValue} as PersonalDetails); @@ -1723,7 +1727,7 @@ function getUserToInviteOption({ !searchValue || isCurrentUserLogin || isInSelectedOption || - (!isValidEmail && !isValidPhoneNumber) || + (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude || (isChronosEmail && !Permissions.canUseChronos(betas)) || excludeUnknownUsers @@ -1744,7 +1748,7 @@ function getUserToInviteOption({ showChatPreviewLine, }); userToInvite.isOptimisticAccount = true; - userToInvite.login = searchValue; + userToInvite.login = isValidEmail || isValidPhoneNumber ? searchValue : ''; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing userToInvite.text = userToInvite.text || searchValue; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -2481,6 +2485,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt excludeLogins = [], preferChatroomsOverThreads = false, includeChatRoomsByParticipants = false, + shouldAcceptName = false, } = config ?? {}; if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) { return {...options, recentReports: options.recentReports.slice(0, maxRecentReportsToShow)}; @@ -2586,6 +2591,7 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt betas, selectedOptions: config?.selectedOptions, optionsToExclude, + shouldAcceptName, }); } } From a53c355cc1771fbb7f9952008337a18acaf25f65 Mon Sep 17 00:00:00 2001 From: zfurtak Date: Tue, 23 Jul 2024 15:22:05 +0200 Subject: [PATCH 002/149] Blank commit From fc456f35d01dbd76614cdf98e80100bd9b62e760 Mon Sep 17 00:00:00 2001 From: zfurtak Date: Thu, 25 Jul 2024 16:02:35 +0200 Subject: [PATCH 003/149] Added shouldAcceptName prop From 24221c75513c1559904277c183416274045cc067 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 1 Aug 2024 18:56:32 +0530 Subject: [PATCH 004/149] fix: Chat - File does not appear with strikethrough style when uploaded offline and deleted. Signed-off-by: krishna2323 --- .../BaseAnchorForAttachmentsOnly.tsx | 3 ++- .../AnchorForAttachmentsOnly/types.ts | 3 +++ .../DefaultAttachmentView/index.tsx | 6 ++++-- .../Attachments/AttachmentView/index.tsx | 5 +++++ .../HTMLRenderers/AnchorRenderer.tsx | 9 +++++---- .../HTMLRenderers/ImageRenderer.tsx | 4 ++++ src/components/ThumbnailImage.tsx | 20 +++++++++++++++++++ .../comment/AttachmentCommentFragment.tsx | 4 +--- 8 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 1d273e847d26..e4b69e9e3b68 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -30,7 +30,7 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & onPressOut?: () => void; }; -function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', download, onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { +function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', download, onPressIn, onPressOut, isDeleted}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; @@ -66,6 +66,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow shouldShowDownloadIcon={!!sourceID && !isOffline} shouldShowLoadingSpinnerIcon={isDownloading} isUsedAsChatAttachment + isDeleted={isDeleted} /> )} diff --git a/src/components/AnchorForAttachmentsOnly/types.ts b/src/components/AnchorForAttachmentsOnly/types.ts index a5186d8c0d90..47caffa9b9b9 100644 --- a/src/components/AnchorForAttachmentsOnly/types.ts +++ b/src/components/AnchorForAttachmentsOnly/types.ts @@ -9,6 +9,9 @@ type AnchorForAttachmentsOnlyProps = { /** Any additional styles to apply */ style?: StyleProp; + + /** Any additional styles to apply */ + isDeleted?: boolean; }; export default AnchorForAttachmentsOnlyProps; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index ee594f66aabc..0b3c99219ebb 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -24,9 +24,11 @@ type DefaultAttachmentViewProps = { containerStyles?: StyleProp; icon?: IconAsset; + + isDeleted?: boolean; }; -function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isDeleted}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -40,7 +42,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa /> - {fileName} + {fileName} {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 39c25706bbfe..5e1af81b80a5 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -76,6 +76,9 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /* Flag indicating whether the attachment has been uploaded. */ isUploaded?: boolean; + + /** Any additional styles to apply */ + isDeleted?: boolean; }; function AttachmentView({ @@ -100,6 +103,7 @@ function AttachmentView({ duration, isUsedAsChatAttachment, isUploaded = true, + isDeleted, }: AttachmentViewProps) { const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -286,6 +290,7 @@ function AttachmentView({ shouldShowDownloadIcon={shouldShowDownloadIcon} shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon} containerStyles={containerStyles} + isDeleted={isDeleted} /> ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 7892d8624699..08aa639820f7 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -25,12 +25,15 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const isAttachment = !!htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; const tNodeChild = tnode?.domNode?.children?.[0]; const displayName = tNodeChild && 'data' in tNodeChild && typeof tNodeChild.data === 'string' ? tNodeChild.data : ''; - const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || ''; + const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); + const hasStrikethroughStyle = 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; + const textDecorationLineStyle = hasStrikethroughStyle ? styles.underlineLineThrough : {}; + if (!HTMLEngineUtils.isChildOfComment(tnode)) { // This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click. // We don't have this behaviour in other links in NewDot @@ -51,13 +54,11 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { ); } - const hasStrikethroughStyle = 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; - const textDecorationLineStyle = hasStrikethroughStyle ? styles.underlineLineThrough : {}; - return ( ); diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 04d0200ea228..18e297e0fe43 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -52,6 +52,8 @@ type ThumbnailImageProps = { /** The object position of image */ objectPosition?: ImageObjectPosition; + + isDeleted?: boolean; }; type UpdateImageSizeParams = { @@ -71,6 +73,7 @@ function ThumbnailImage({ fallbackIconColor, fallbackIconBackground, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL, + isDeleted, }: ThumbnailImageProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -110,6 +113,23 @@ function ThumbnailImage({ const sizeStyles = shouldDynamicallyResize ? [thumbnailDimensionsStyles] : [styles.w100, styles.h100]; + if (isDeleted) { + const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.sidebarHover); + + return ( + + + + + + ); + } + if (failedToLoad || previewSourceURL === '') { const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.border); diff --git a/src/pages/home/report/comment/AttachmentCommentFragment.tsx b/src/pages/home/report/comment/AttachmentCommentFragment.tsx index 7d2d81b86e02..f9f86f0a9cd0 100644 --- a/src/pages/home/report/comment/AttachmentCommentFragment.tsx +++ b/src/pages/home/report/comment/AttachmentCommentFragment.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import RenderCommentHTML from './RenderCommentHTML'; @@ -14,8 +13,7 @@ type AttachmentCommentFragmentProps = { function AttachmentCommentFragment({addExtraMargin, html, source, styleAsDeleted}: AttachmentCommentFragmentProps) { const styles = useThemeStyles(); - const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); - const htmlContent = styleAsDeleted && isUploading ? `${html}` : html; + const htmlContent = styleAsDeleted ? `${html}` : html; return ( From 05b2f2dcb03cb99da0ba872867f259dcac8aa313 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Aug 2024 02:28:44 +0530 Subject: [PATCH 005/149] updated design. Signed-off-by: krishna2323 --- .../BaseAnchorForAttachmentsOnly.tsx | 2 +- src/components/ThumbnailImage.tsx | 49 +++++++++++++------ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index e4b69e9e3b68..b77646e485da 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -66,7 +66,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow shouldShowDownloadIcon={!!sourceID && !isOffline} shouldShowLoadingSpinnerIcon={isDownloading} isUsedAsChatAttachment - isDeleted={isDeleted} + isDeleted={!!isDeleted} /> )} diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 18e297e0fe43..9ed9c6c8d46c 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -113,22 +113,22 @@ function ThumbnailImage({ const sizeStyles = shouldDynamicallyResize ? [thumbnailDimensionsStyles] : [styles.w100, styles.h100]; - if (isDeleted) { - const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.sidebarHover); - - return ( - - - - - - ); - } + // if (isDeleted) { + // const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.sidebarHover); + + // return ( + // + // + // + // + // + // ); + // } if (failedToLoad || previewSourceURL === '') { const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.border); @@ -149,6 +149,23 @@ function ThumbnailImage({ return ( + {isDeleted && ( + <> + + + + + + + + + + )} Date: Sun, 25 Aug 2024 02:17:47 +0530 Subject: [PATCH 006/149] add deleted indicator for video renderer. Signed-off-by: krishna2323 --- .../HTMLRenderers/VideoRenderer.tsx | 3 + src/components/ThumbnailImage.tsx | 37 +---------- .../VideoPlayerThumbnail.tsx | 62 +++++++++++-------- src/components/VideoPlayerPreview/index.tsx | 8 ++- 4 files changed, 47 insertions(+), 63 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index e0df7e7081c5..bde61badba8f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -24,6 +24,8 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]); const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]); const currentReportIDValue = useCurrentReportID(); + const parentStyle = tnode.parent?.styles?.nativeTextRet ?? {}; + const isDeleted = 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; return ( @@ -36,6 +38,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { thumbnailUrl={thumbnailUrl} videoDimensions={{width, height}} videoDuration={duration} + isDeleted={isDeleted} onShowModalPress={() => { const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID ?? '-1', CONST.ATTACHMENT_TYPE.REPORT, sourceURL); Navigation.navigate(route); diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 9ed9c6c8d46c..52f20902406f 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -9,6 +9,7 @@ import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; +import AttachmentDeletedIndicator from './AttachmentDeletedIndicator'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import type {ImageObjectPosition} from './Image/types'; @@ -53,6 +54,7 @@ type ThumbnailImageProps = { /** The object position of image */ objectPosition?: ImageObjectPosition; + /** Whether the image is deleted */ isDeleted?: boolean; }; @@ -113,23 +115,6 @@ function ThumbnailImage({ const sizeStyles = shouldDynamicallyResize ? [thumbnailDimensionsStyles] : [styles.w100, styles.h100]; - // if (isDeleted) { - // const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.sidebarHover); - - // return ( - // - // - // - // - // - // ); - // } - if (failedToLoad || previewSourceURL === '') { const fallbackColor = StyleUtils.getBackgroundColorStyle(fallbackIconBackground ?? theme.border); @@ -149,23 +134,7 @@ function ThumbnailImage({ return ( - {isDeleted && ( - <> - - - - - - - - - - )} + {isDeleted && } )} - - {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => ( - DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - onLongPress={(event) => - showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)) - } - shouldUseHapticsOnLongPress - > - - - - - )} - + {!isDeleted ? ( + + {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => ( + DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={(event) => + showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)) + } + shouldUseHapticsOnLongPress + > + + + + + )} + + ) : ( + + )} ); } diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index 2ce65f08fc20..a56fedd8fb13 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -38,9 +38,12 @@ type VideoPlayerPreviewProps = { /** Callback executed when modal is pressed. */ onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; + + /** Whether the image is deleted */ + isDeleted?: boolean; }; -function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress}: VideoPlayerPreviewProps) { +function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDimensions, videoDuration, onShowModalPress, isDeleted}: VideoPlayerPreviewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {currentlyPlayingURL, currentlyPlayingURLReportID, updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -71,11 +74,12 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi return ( - {shouldUseNarrowLayout || isThumbnail ? ( + {shouldUseNarrowLayout || isThumbnail || isDeleted ? ( ) : ( From 98aa1548408956d9c902186c5316544179a22613 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 25 Aug 2024 03:02:58 +0530 Subject: [PATCH 007/149] add styles object for deleted indicator. Signed-off-by: krishna2323 --- src/components/AnchorForAttachmentsOnly/types.ts | 2 +- .../AttachmentView/DefaultAttachmentView/index.tsx | 1 + src/components/Attachments/AttachmentView/index.tsx | 2 +- .../VideoPlayerPreview/VideoPlayerThumbnail.tsx | 2 +- src/components/VideoPlayerPreview/index.tsx | 2 +- src/styles/index.ts | 11 +++++++++++ 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/types.ts b/src/components/AnchorForAttachmentsOnly/types.ts index 47caffa9b9b9..89932a95aa04 100644 --- a/src/components/AnchorForAttachmentsOnly/types.ts +++ b/src/components/AnchorForAttachmentsOnly/types.ts @@ -10,7 +10,7 @@ type AnchorForAttachmentsOnlyProps = { /** Any additional styles to apply */ style?: StyleProp; - /** Any additional styles to apply */ + /** Whether the attachment is deleted */ isDeleted?: boolean; }; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index 0b3c99219ebb..e9138fbcdd61 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -25,6 +25,7 @@ type DefaultAttachmentViewProps = { icon?: IconAsset; + /** Whether the attachment is deleted */ isDeleted?: boolean; }; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 5e1af81b80a5..1a63c9bfd6b8 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -77,7 +77,7 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /* Flag indicating whether the attachment has been uploaded. */ isUploaded?: boolean; - /** Any additional styles to apply */ + /** Whether the attachment is deleted */ isDeleted?: boolean; }; diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index 96bcf8b5896e..fd9861340cff 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -24,7 +24,7 @@ type VideoPlayerThumbnailProps = { /** Accessibility label for the thumbnail. */ accessibilityLabel: string; - /** Whether the image is deleted */ + /** Whether the video is deleted */ isDeleted?: boolean; }; diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index a56fedd8fb13..10d3e753f38a 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -39,7 +39,7 @@ type VideoPlayerPreviewProps = { /** Callback executed when modal is pressed. */ onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; - /** Whether the image is deleted */ + /** Whether the video is deleted */ isDeleted?: boolean; }; diff --git a/src/styles/index.ts b/src/styles/index.ts index 9f93c799abb5..8db89c98b2e6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1111,6 +1111,17 @@ const styles = (theme: ThemeColors) => height: 25, }, + deletedIndicator: { + zIndex: 20, + width: '100%', + height: '100%', + overflow: 'hidden', + }, + + deletedIndicatorOverlay: { + opacity: 0.8, + }, + // Actions actionAvatar: { borderRadius: 20, From 01003146a058a2a1048164f67889947ec1a14ba6 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 25 Aug 2024 03:10:05 +0530 Subject: [PATCH 008/149] add AttachmentDeletedIndicator.tsx Signed-off-by: krishna2323 --- src/components/AttachmentDeletedIndicator.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/components/AttachmentDeletedIndicator.tsx diff --git a/src/components/AttachmentDeletedIndicator.tsx b/src/components/AttachmentDeletedIndicator.tsx new file mode 100644 index 000000000000..1866e5357daf --- /dev/null +++ b/src/components/AttachmentDeletedIndicator.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; + +type AttachmentOfflineIndicatorProps = { + /** Any additional styles to apply */ + containerStyles?: StyleProp; +}; + +function AttachmentDeletedIndicator({containerStyles}: AttachmentOfflineIndicatorProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + if (!isOffline) { + return null; + } + + return ( + <> + + + + + + ); +} + +AttachmentDeletedIndicator.displayName = 'AttachmentDeletedIndicator'; + +export default AttachmentDeletedIndicator; From d2bf2d5cb6af4335b04f9d922361b3cda206be5d Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 26 Aug 2024 14:18:31 +0200 Subject: [PATCH 009/149] create new route for attendee screen --- src/ROUTES.ts | 5 +++++ src/SCREENS.ts | 1 + .../Navigation/AppNavigator/ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 7 +++++++ .../iou/request/step/withWritableReportOrNotFound.tsx | 3 ++- 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index dd87e5a9996f..b085572bd03c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -396,6 +396,11 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, + MONEY_REQUEST_ATTENDEE: { + route: ':action/:iouType/attendees/:transactionID/:reportID', + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo), + }, SETTINGS_CATEGORIES_ROOT: { route: 'settings/:policyID/categories', getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cc4360d7695d..925988cbede5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -203,6 +203,7 @@ const SCREENS = { EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', RECEIPT: 'Money_Request_Receipt', STATE_SELECTOR: 'Money_Request_State_Selector', + ATTENDEE: 'Money_Request_Attendee', }, TRANSACTION_DUPLICATE: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 4694a2e73d5c..4d55ed65d12e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -96,6 +96,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/AddDebitCardPage').default, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePaymentsPage').default, [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default, + [SCREENS.MONEY_REQUEST.ATTENDEE]: () => require('../../../../pages/iou/request/step/AttendeeSelectionPage').default, }); const TravelModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6b4d7eca95c1..138a37ed20ae 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -913,6 +913,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true}, [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route, + [SCREENS.MONEY_REQUEST.ATTENDEE]: ROUTES.MONEY_REQUEST_ATTENDEE.route, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT, [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b689f36d8a35..689e86e66e3c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -935,6 +935,13 @@ type MoneyRequestNavigatorParamList = { backTo?: Routes; currency?: string; }; + [SCREENS.MONEY_REQUEST.ATTENDEE]: { + action: IOUAction; + iouType: Exclude; + transactionID: string; + reportID: string; + backTo: Routes; + }; }; type NewTaskNavigatorParamList = { diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 8df530f3c81c..faba177dd8c6 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -43,7 +43,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT | typeof SCREENS.MONEY_REQUEST.STEP_SCAN | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM - | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO; + | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO + | typeof SCREENS.MONEY_REQUEST.ATTENDEE; type Route = RouteProp; From a355c431b92e3d06bcc71b993ab5a95c3a032142 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 26 Aug 2024 16:09:32 +0200 Subject: [PATCH 010/149] create attendee screen + selector --- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/OptionsListUtils.ts | 12 +- .../request/MoneyRequestAttendeeSelector.tsx | 367 ++++++++++++++++++ .../request/step/AttendeeSelectionPage.tsx | 56 +++ src/types/onyx/IOU.ts | 8 +- 6 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 src/pages/iou/request/MoneyRequestAttendeeSelector.tsx create mode 100644 src/pages/iou/request/step/AttendeeSelectionPage.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index a378df670367..1c3bd187f1a9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -831,6 +831,7 @@ export default { atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses.', splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.', invalidMerchant: 'Please enter a correct merchant.', + atLeastOneAttendee: 'At least one attendee must be selected', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`, enableWallet: 'Enable wallet', @@ -881,6 +882,7 @@ export default { bookingPendingDescription: "This booking is pending because it hasn't been paid yet.", bookingArchived: 'This booking is archived', bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.', + attendees: 'Attendees', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index a106666bf2ca..91baf30bfdeb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -827,6 +827,7 @@ export default { atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes.', splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.', invalidMerchant: 'Por favor, introduce un comerciante correcto.', + atLeastOneAttendee: 'At least one attendee must be selected', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`, enableWallet: 'Habilitar billetera', @@ -885,6 +886,7 @@ export default { bookingPendingDescription: 'Esta reserva está pendiente porque aún no se ha pagado.', bookingArchived: 'Esta reserva está archivada', bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.', + attendees: 'Attendees', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3f64ec6131be..ea2254a311ad 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -219,7 +219,10 @@ type Options = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; -type FilterOptionsConfig = Pick & { +type FilterOptionsConfig = Pick< + GetOptionsConfig, + 'sortByReportTypeInSearch' | 'canInviteUser' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' | 'shouldAcceptName' +> & { preferChatroomsOverThreads?: boolean; preferPolicyExpenseChat?: boolean; }; @@ -425,7 +428,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login)); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login) || participant.text); return { keyForList: String(detail?.accountID), @@ -2505,6 +2508,10 @@ function shouldUseBoldText(report: ReportUtils.OptionData): boolean { return report.isUnread === true && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE; } +function getAttendeeOptions() { + return getEmptyOptions(); +} + export { getAvatarsForAccountIDs, isCurrentUser, @@ -2550,6 +2557,7 @@ export { getCurrentUserSearchTerms, getEmptyOptions, shouldUseBoldText, + getAttendeeOptions, }; export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx new file mode 100644 index 000000000000..0cec9a8eacc3 --- /dev/null +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -0,0 +1,367 @@ +import lodashIsEqual from 'lodash/isEqual'; +import lodashPick from 'lodash/pick'; +import lodashReject from 'lodash/reject'; +import React, {memo, useCallback, useEffect, useMemo} from 'react'; +import type {GestureResponderEvent} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import EmptySelectionListContent from '@components/EmptySelectionListContent'; +import FormHelpMessage from '@components/FormHelpMessage'; +import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; +import SelectionList from '@components/SelectionList'; +import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; +import usePolicy from '@hooks/usePolicy'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; +import * as Report from '@userActions/Report'; +import type {IOUAction, IOURequestType, IOUType} from '@src/CONST'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Attendee, Participant} from '@src/types/onyx/IOU'; + +type MoneyRequestParticipantsSelectorProps = { + /** Callback to request parent modal to go to next step, which should be split */ + onFinish: (value?: string) => void; + + /** Callback to add participants in MoneyRequestModal */ + onParticipantsAdded: (value: Participant[]) => void; + + /** Selected participants from MoneyRequestModal with login */ + participants?: Participant[] | typeof CONST.EMPTY_ARRAY; + + /** The type of IOU report, i.e. split, request, send, track */ + iouType: IOUType; + + /** The expense type, ie. manual, scan, distance */ + iouRequestType: IOURequestType; + + /** The action of the IOU, i.e. create, split, move */ + action: IOUAction; +}; + +function MoneyRequestAttendeeSelector({participants = CONST.EMPTY_ARRAY, onFinish, onParticipantsAdded, iouType, iouRequestType, action}: MoneyRequestParticipantsSelectorProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const referralContentType = iouType === CONST.IOU.TYPE.PAY ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.PAY_SOMEONE : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SUBMIT_EXPENSE; + const {isOffline} = useNetwork(); + const personalDetails = usePersonalDetails(); + const {isDismissed} = useDismissedReferralBanners({referralContentType}); + const {canUseP2PDistanceRequests} = usePermissions(); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const policy = usePolicy(activePolicyID); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); + const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); + const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + + const isPaidGroupPolicy = useMemo(() => PolicyUtils.isPaidGroupPolicy(policy), [policy]); + const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; + const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action); + + useEffect(() => { + Report.searchInServer(debouncedSearchTerm.trim()); + }, [debouncedSearchTerm]); + + const defaultOptions = useMemo(() => { + if (!areOptionsInitialized || !didScreenTransitionEnd) { + OptionsListUtils.getEmptyOptions(); + } + + const optionList = OptionsListUtils.getFilteredOptions( + options.reports, + options.personalDetails, + betas, + '', + participants as Attendee[], + CONST.EXPENSIFY_EMAILS, + + // If we are using this component in the "Submit expense" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to submit an expense from their admin on their own Workspace Chat. + (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.SPLIT) && action !== CONST.IOU.ACTION.SUBMIT, + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, + false, + {}, + [], + false, + {}, + [], + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, + false, + false, + 0, + undefined, + undefined, + undefined, + undefined, + undefined, + iouType === CONST.IOU.TYPE.INVOICE, + action, + isPaidGroupPolicy, + ); + + return optionList; + }, [ + action, + areOptionsInitialized, + betas, + canUseP2PDistanceRequests, + didScreenTransitionEnd, + iouRequestType, + iouType, + isCategorizeOrShareAction, + options.personalDetails, + options.reports, + participants, + isPaidGroupPolicy, + ]); + + const chatOptions = useMemo(() => { + if (!areOptionsInitialized) { + return { + userToInvite: null, + recentReports: [], + personalDetails: [], + currentUserOption: null, + headerMessage: '', + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; + } + + const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { + selectedOptions: participants as Attendee[], + excludeLogins: CONST.EXPENSIFY_EMAILS, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + preferPolicyExpenseChat: isPaidGroupPolicy, + shouldAcceptName: true, + }); + return newOptions; + }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, participants, isPaidGroupPolicy]); + + /** + * Returns the sections needed for the OptionsSelector + * @returns {Array} + */ + const [sections, header] = useMemo(() => { + const newSections: OptionsListUtils.CategorySection[] = []; + if (!areOptionsInitialized || !didScreenTransitionEnd) { + return [newSections, '']; + } + + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( + debouncedSearchTerm, + participants.map((participant) => ({...participant, reportID: participant.reportID ?? '-1'})), + chatOptions.recentReports, + chatOptions.personalDetails, + personalDetails, + true, + ); + console.log('formatResults', formatResults); + newSections.push(formatResults.section); + + newSections.push({ + title: translate('common.recents'), + data: chatOptions.recentReports, + shouldShow: chatOptions.recentReports.length > 0, + }); + + newSections.push({ + title: translate('common.contacts'), + data: chatOptions.personalDetails, + shouldShow: chatOptions.personalDetails.length > 0, + }); + + if ( + chatOptions.userToInvite && + !OptionsListUtils.isCurrentUser({...chatOptions.userToInvite, accountID: chatOptions.userToInvite?.accountID ?? -1, status: chatOptions.userToInvite?.status ?? undefined}) + ) { + newSections.push({ + title: undefined, + data: [chatOptions.userToInvite].map((participant) => { + const isPolicyExpenseChat = participant?.isPolicyExpenseChat ?? false; + return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + }), + shouldShow: true, + }); + } + + const headerMessage = OptionsListUtils.getHeaderMessage( + (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0, + !!chatOptions?.userToInvite, + debouncedSearchTerm.trim(), + participants.some((participant) => OptionsListUtils.getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), + ); + + return [newSections, headerMessage]; + }, [ + areOptionsInitialized, + didScreenTransitionEnd, + debouncedSearchTerm, + participants, + chatOptions.recentReports, + chatOptions.personalDetails, + chatOptions.userToInvite, + personalDetails, + translate, + cleanSearchTerm, + ]); + + /** + * Removes a selected option from list if already selected. If not already selected add this option to the list. + * @param {Object} option + */ + const addParticipantToSelection = useCallback( + (option: Participant) => { + const isOptionSelected = (selectedOption: Participant) => { + if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { + return true; + } + + if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { + return true; + } + + return false; + }; + const isOptionInList = participants.some(isOptionSelected); + let newSelectedOptions: Participant[]; + + if (isOptionInList) { + newSelectedOptions = lodashReject(participants, isOptionSelected); + } else { + newSelectedOptions = [ + ...participants, + { + accountID: option.accountID, + login: option.login, + isPolicyExpenseChat: option.isPolicyExpenseChat, + reportID: option.reportID, + text: option.text, + selected: true, + searchText: option.searchText, + iouType, + }, + ]; + } + + onParticipantsAdded(newSelectedOptions); + }, + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes + [participants, onParticipantsAdded], + ); + + const shouldShowErrorMessage = participants.length < 1; + + const handleConfirmSelection = useCallback( + (_keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { + if (shouldShowErrorMessage || (!participants.length && !option)) { + return; + } + + onFinish(CONST.IOU.TYPE.SPLIT); + }, + [shouldShowErrorMessage, onFinish, participants], + ); + + const showLoadingPlaceholder = useMemo(() => !areOptionsInitialized || !didScreenTransitionEnd, [areOptionsInitialized, didScreenTransitionEnd]); + + const optionLength = useMemo(() => { + if (!areOptionsInitialized) { + return 0; + } + return sections.reduce((acc, section) => acc + section.data.length, 0); + }, [areOptionsInitialized, sections]); + + const shouldShowListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]); + + const footerContent = useMemo(() => { + if (isDismissed && !shouldShowErrorMessage && !participants.length) { + return; + } + + return ( + <> + {shouldShowErrorMessage && ( + + )} + + {!isCategorizeOrShareAction && ( +