diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg
new file mode 100644
index 000000000000..9c0711fcaedc
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/CONST.ts b/src/CONST.ts
index e2a1e79ccbb3..86c5bb8d2a0c 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1262,6 +1262,7 @@ const CONST = {
ATTACHMENT_TYPE: {
REPORT: 'r',
NOTE: 'n',
+ SEARCH: 's',
},
IMAGE_HIGH_RESOLUTION_THRESHOLD: 7000,
@@ -5379,6 +5380,13 @@ const CONST = {
APPROVED: 'approved',
PAID: 'paid',
},
+ CHAT: {
+ ALL: 'all',
+ UNREAD: 'unread',
+ SENT: 'sent',
+ ATTACHMENTS: 'attachments',
+ LINKS: 'links',
+ },
},
CHAT_TYPES: {
LINK: 'link',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index f67298828d7d..7801f5cf6053 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -57,8 +57,13 @@ const ROUTES = {
SEARCH_ADVANCED_FILTERS_IS: 'search/filters/is',
SEARCH_REPORT: {
- route: 'search/view/:reportID',
- getRoute: (reportID: string) => `search/view/${reportID}` as const,
+ route: 'search/view/:reportID/:reportActionID?',
+ getRoute: (reportID: string, reportActionID?: string) => {
+ if (reportActionID) {
+ return `search/view/${reportID}/${reportActionID}` as const;
+ }
+ return `search/view/${reportID}` as const;
+ },
},
TRANSACTION_HOLD_REASON_RHP: 'search/hold',
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index 14b7ac6f2313..c327d7fa6093 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -246,13 +246,14 @@ function AttachmentModal({
}
if (typeof sourceURL === 'string') {
- fileDownload(sourceURL, file?.name ?? '');
+ const fileName = type === CONST.ATTACHMENT_TYPE.SEARCH ? FileUtils.getFileName(`${sourceURL}`) : file?.name;
+ fileDownload(sourceURL, fileName ?? '');
}
// At ios, if the keyboard is open while opening the attachment, then after downloading
// the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
Keyboard.dismiss();
- }, [isAuthTokenRequiredState, sourceState, file]);
+ }, [isAuthTokenRequiredState, sourceState, file, type]);
/**
* Execute the onConfirm callback and close the modal.
@@ -460,7 +461,7 @@ function AttachmentModal({
let headerTitleNew = headerTitle;
let shouldShowDownloadButton = false;
let shouldShowThreeDotsButton = false;
- if (!isEmptyObject(report)) {
+ if (!isEmptyObject(report) || type === CONST.ATTACHMENT_TYPE.SEARCH) {
headerTitleNew = translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment');
shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isReceiptAttachment && !isOffline && !isLocalSource;
shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen && threeDotsMenuItems.length !== 0;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
index 771d2631379e..99699b9ef3c6 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
@@ -90,10 +90,8 @@ function ImageRenderer({tnode}: ImageRendererProps) {
return;
}
- if (reportID) {
- const route = ROUTES.ATTACHMENTS?.getRoute(reportID, type, source, accountID);
- Navigation.navigate(route);
- }
+ const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID);
+ Navigation.navigate(route);
}}
onLongPress={(event) =>
showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs))
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
index e0df7e7081c5..ce822af14cb8 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
+import {AttachmentContext} from '@components/AttachmentContext';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import VideoPlayerPreview from '@components/VideoPlayerPreview';
import useCurrentReportID from '@hooks/useCurrentReportID';
@@ -28,19 +29,26 @@ function VideoRenderer({tnode, key}: VideoRendererProps) {
return (
{({report}) => (
- {
- const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID ?? '-1', CONST.ATTACHMENT_TYPE.REPORT, sourceURL);
- Navigation.navigate(route);
- }}
- />
+
+ {({accountID, type}) => (
+ {
+ if (!sourceURL || !type) {
+ return;
+ }
+ const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID ?? '-1', type, sourceURL, accountID);
+ Navigation.navigate(route);
+ }}
+ />
+ )}
+
)}
);
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 0616794a8e3a..bc12bc6c135b 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -56,6 +56,7 @@ import CheckmarkCircle from '@assets/images/simple-illustrations/simple-illustra
import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg';
import Coins from '@assets/images/simple-illustrations/simple-illustration__coins.svg';
import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg';
+import CommentBubblesBlue from '@assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg';
import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg';
import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg';
import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg';
@@ -182,6 +183,7 @@ export {
SmartScan,
Hourglass,
CommentBubbles,
+ CommentBubblesBlue,
TrashCan,
TeleScope,
Profile,
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index 76fd7d3524cb..f336740a8558 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -10,7 +10,7 @@ import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/typ
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
-import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
@@ -95,7 +95,7 @@ type SearchPageHeaderProps = {
onSelectDeleteOption?: (itemsToDelete: string[]) => void;
setOfflineModalOpen?: () => void;
setDownloadErrorModalOpen?: () => void;
- data?: TransactionListItemType[] | ReportListItemType[];
+ data?: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[];
};
type SearchHeaderOptionValue = DeepValueOf | undefined;
@@ -111,6 +111,8 @@ function getHeaderContent(type: SearchDataTypes): HeaderContent {
return {icon: Illustrations.EnvelopeReceipt, titleText: 'workspace.common.invoices'};
case CONST.SEARCH.DATA_TYPES.TRIP:
return {icon: Illustrations.Luggage, titleText: 'travel.trips'};
+ case CONST.SEARCH.DATA_TYPES.CHAT:
+ return {icon: Illustrations.CommentBubblesBlue, titleText: 'common.chats'};
case CONST.SEARCH.DATA_TYPES.EXPENSE:
default:
return {icon: Illustrations.MoneyReceipts, titleText: 'common.expenses'};
@@ -135,6 +137,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
.filter(
(item) =>
!SearchUtils.isTransactionListItemType(item) &&
+ !SearchUtils.isReportActionListItemType(item) &&
item.reportID &&
item.transactions.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected),
)
diff --git a/src/components/Search/SearchStatusBar.tsx b/src/components/Search/SearchStatusBar.tsx
index 7c1ffeff1818..0d2b8120609f 100644
--- a/src/components/Search/SearchStatusBar.tsx
+++ b/src/components/Search/SearchStatusBar.tsx
@@ -13,11 +13,12 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type IconAsset from '@src/types/utils/IconAsset';
-import type {ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryString, SearchStatus, TripSearchStatus} from './types';
+import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryString, SearchStatus, TripSearchStatus} from './types';
type SearchStatusBarProps = {
type: SearchDataTypes;
status: SearchStatus;
+ resetOffset: () => void;
};
const expenseOptions: Array<{key: ExpenseSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
@@ -107,19 +108,54 @@ const tripOptions: Array<{key: TripSearchStatus; icon: IconAsset; text: Translat
},
];
+const chatOptions: Array<{key: ChatSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
+ {
+ key: CONST.SEARCH.STATUS.CHAT.ALL,
+ icon: Expensicons.All,
+ text: 'common.all',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.ALL),
+ },
+ {
+ key: CONST.SEARCH.STATUS.CHAT.UNREAD,
+ icon: Expensicons.ChatBubbleUnread,
+ text: 'common.unread',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.UNREAD),
+ },
+ {
+ key: CONST.SEARCH.STATUS.CHAT.SENT,
+ icon: Expensicons.Send,
+ text: 'common.sent',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.SENT),
+ },
+ {
+ key: CONST.SEARCH.STATUS.CHAT.ATTACHMENTS,
+ icon: Expensicons.Document,
+ text: 'common.attachments',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.ATTACHMENTS),
+ },
+ {
+ key: CONST.SEARCH.STATUS.CHAT.LINKS,
+ icon: Expensicons.Paperclip,
+ text: 'common.links',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.CHAT.LINKS),
+ },
+];
+
function getOptions(type: SearchDataTypes) {
switch (type) {
case CONST.SEARCH.DATA_TYPES.INVOICE:
return invoiceOptions;
case CONST.SEARCH.DATA_TYPES.TRIP:
return tripOptions;
+ case CONST.SEARCH.DATA_TYPES.CHAT:
+ return chatOptions;
case CONST.SEARCH.DATA_TYPES.EXPENSE:
default:
return expenseOptions;
}
}
-function SearchStatusBar({type, status}: SearchStatusBarProps) {
+function SearchStatusBar({type, status, resetOffset}: SearchStatusBarProps) {
const {singleExecution} = useSingleExecution();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -134,7 +170,10 @@ function SearchStatusBar({type, status}: SearchStatusBarProps) {
showsHorizontalScrollIndicator={false}
>
{options.map((item, index) => {
- const onPress = singleExecution(() => Navigation.setParams({q: item.query}));
+ const onPress = singleExecution(() => {
+ resetOffset();
+ Navigation.setParams({q: item.query});
+ });
const isActive = status === item.key;
const isFirstItem = index === 0;
const isLastItem = index === options.length - 1;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 8c4530b08b64..4a98d651e2b2 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -6,7 +6,7 @@ import {useOnyx} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import DecisionModal from '@components/DecisionModal';
import SearchTableHeader from '@components/SelectionList/SearchTableHeader';
-import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import SelectionListWithModal from '@components/SelectionListWithModal';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import SearchStatusSkeleton from '@components/Skeletons/SearchStatusSkeleton';
@@ -54,7 +54,10 @@ function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, se
return {...item, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple};
}
-function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) {
+function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType | ReportActionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) {
+ if (SearchUtils.isReportActionListItemType(item)) {
+ return item;
+ }
return SearchUtils.isTransactionListItemType(item)
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple)
: {
@@ -142,8 +145,8 @@ function Search({queryJSON}: SearchProps) {
};
const getItemHeight = useCallback(
- (item: TransactionListItemType | ReportListItemType) => {
- if (SearchUtils.isTransactionListItemType(item)) {
+ (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
+ if (SearchUtils.isTransactionListItemType(item) || SearchUtils.isReportActionListItemType(item)) {
return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding;
}
@@ -161,6 +164,8 @@ function Search({queryJSON}: SearchProps) {
[isLargeScreenWidth],
);
+ const resetOffset = () => setOffset(0);
+
const getItemHeightMemoized = memoize(getItemHeight, {
transformKey: ([item]) => {
// List items are displayed differently on "L"arge and "N"arrow screens so the height will differ
@@ -205,6 +210,7 @@ function Search({queryJSON}: SearchProps) {
) : (
@@ -219,9 +225,9 @@ function Search({queryJSON}: SearchProps) {
return null;
}
- const ListItem = SearchUtils.getListItem(status);
- const data = SearchUtils.getSections(status, searchResults.data, searchResults.search);
- const sortedData = SearchUtils.getSortedSections(status, data, sortBy, sortOrder);
+ const ListItem = SearchUtils.getListItem(type, status);
+ const data = SearchUtils.getSections(type, status, searchResults.data, searchResults.search);
+ const sortedData = SearchUtils.getSortedSections(type, status, data, sortBy, sortOrder);
const sortedSelectedData = sortedData.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple));
const shouldShowEmptyState = !isDataLoaded || data.length === 0;
@@ -236,13 +242,17 @@ function Search({queryJSON}: SearchProps) {
>
);
}
- const toggleTransaction = (item: TransactionListItemType | ReportListItemType) => {
+ const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
+ if (SearchUtils.isReportActionListItemType(item)) {
+ return;
+ }
if (SearchUtils.isTransactionListItemType(item)) {
if (!item.keyForList) {
return;
@@ -269,7 +279,7 @@ function Search({queryJSON}: SearchProps) {
});
};
- const openReport = (item: TransactionListItemType | ReportListItemType) => {
+ const openReport = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
let reportID = SearchUtils.isTransactionListItemType(item) && !item.isFromOneTransactionReport ? item.transactionThreadReportID : item.reportID;
if (!reportID) {
@@ -282,6 +292,12 @@ function Search({queryJSON}: SearchProps) {
SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
}
+ if (SearchUtils.isReportActionListItemType(item)) {
+ const reportActionID = item.reportActionID;
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(reportID, reportActionID));
+ return;
+ }
+
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(reportID));
};
@@ -332,10 +348,11 @@ function Search({queryJSON}: SearchProps) {
-
+
sections={[{data: sortedSelectedData, isDisabled: false}]}
- turnOnSelectionModeOnLongPress
+ turnOnSelectionModeOnLongPress={type !== CONST.SEARCH.DATA_TYPES.CHAT}
onTurnOnSelectionMode={(item) => item && toggleTransaction(item)}
onCheckboxPress={toggleTransaction}
onSelectAll={toggleAllTransactions}
@@ -352,7 +369,7 @@ function Search({queryJSON}: SearchProps) {
/>
)
}
- canSelectMultiple={canSelectMultiple}
+ canSelectMultiple={type !== CONST.SEARCH.DATA_TYPES.CHAT && canSelectMultiple}
customListHeaderHeight={searchHeaderHeight}
// To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling,
// we have configured a larger windowSize and a longer delay between batch renders.
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 9f2aca1ff957..b48afb75d0de 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -28,7 +28,8 @@ type SearchColumnType = ValueOf;
type ExpenseSearchStatus = ValueOf;
type InvoiceSearchStatus = ValueOf;
type TripSearchStatus = ValueOf;
-type SearchStatus = ExpenseSearchStatus | InvoiceSearchStatus | TripSearchStatus;
+type ChatSearchStatus = ValueOf;
+type SearchStatus = ExpenseSearchStatus | InvoiceSearchStatus | TripSearchStatus | ChatSearchStatus;
type SearchContext = {
currentSearchHash: number;
@@ -88,4 +89,5 @@ export type {
ExpenseSearchStatus,
InvoiceSearchStatus,
TripSearchStatus,
+ ChatSearchStatus,
};
diff --git a/src/components/SelectionList/ChatListItem.tsx b/src/components/SelectionList/ChatListItem.tsx
new file mode 100644
index 000000000000..1a27e0ecbfcf
--- /dev/null
+++ b/src/components/SelectionList/ChatListItem.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import {View} from 'react-native';
+import {AttachmentContext} from '@components/AttachmentContext';
+import MultipleAvatars from '@components/MultipleAvatars';
+import TextWithTooltip from '@components/TextWithTooltip';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import ReportActionItemDate from '@pages/home/report/ReportActionItemDate';
+import ReportActionItemFragment from '@pages/home/report/ReportActionItemFragment';
+import CONST from '@src/CONST';
+import BaseListItem from './BaseListItem';
+import type {ChatListItemProps, ListItem, ReportActionListItemType} from './types';
+
+function ChatListItem({
+ item,
+ isFocused,
+ showTooltip,
+ isDisabled,
+ canSelectMultiple,
+ onSelectRow,
+ onDismissError,
+ onFocus,
+ onLongPressRow,
+ shouldSyncFocus,
+}: ChatListItemProps) {
+ const reportActionItem = item as unknown as ReportActionListItemType;
+ const from = reportActionItem.from;
+ const icons = [
+ {
+ type: CONST.ICON_TYPE_AVATAR,
+ source: from.avatar,
+ name: reportActionItem.formattedFrom,
+ id: from.accountID,
+ },
+ ];
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const StyleUtils = useStyleUtils();
+
+ const attachmentContextValue = {type: CONST.ATTACHMENT_TYPE.SEARCH};
+
+ const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
+ const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
+
+ return (
+
+ {(hovered) => (
+
+
+
+
+
+
+
+
+
+
+ {reportActionItem.message.map((fragment, index) => (
+
+ ))}
+
+
+
+ )}
+
+ );
+}
+
+ChatListItem.displayName = 'ChatListItem';
+
+export default ChatListItem;
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index cb1914824a20..f54532a7f318 100644
--- a/src/components/SelectionList/SearchTableHeader.tsx
+++ b/src/components/SelectionList/SearchTableHeader.tsx
@@ -85,6 +85,13 @@ const expenseHeaders: SearchColumnConfig[] = [
},
];
+const SearchColumns = {
+ [CONST.SEARCH.DATA_TYPES.EXPENSE]: expenseHeaders,
+ [CONST.SEARCH.DATA_TYPES.INVOICE]: expenseHeaders,
+ [CONST.SEARCH.DATA_TYPES.TRIP]: expenseHeaders,
+ [CONST.SEARCH.DATA_TYPES.CHAT]: null,
+};
+
type SearchTableHeaderProps = {
data: OnyxTypes.SearchResults['data'];
metadata: OnyxTypes.SearchResults['search'];
@@ -102,6 +109,10 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shou
const {translate} = useLocalize();
const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth;
+ if (SearchColumns[metadata.type] === null) {
+ return;
+ }
+
if (displayNarrowVersion) {
return;
}
@@ -109,7 +120,7 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shou
return (
- {expenseHeaders.map(({columnName, translationKey, shouldShow, isColumnSortable}) => {
+ {SearchColumns[metadata.type]?.map(({columnName, translationKey, shouldShow, isColumnSortable}) => {
if (!shouldShow(data, metadata)) {
return null;
}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index c484a59fee78..ea0fb35932dd 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -5,10 +5,11 @@ import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import type CursorStyles from '@styles/utils/cursor/types';
import type CONST from '@src/CONST';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
-import type {SearchPersonalDetails, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
+import type {SearchPersonalDetails, SearchReport, SearchReportAction, SearchTransaction} from '@src/types/onyx/SearchResults';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type IconAsset from '@src/types/utils/IconAsset';
+import type ChatListItem from './ChatListItem';
import type InviteMemberListItem from './InviteMemberListItem';
import type RadioListItem from './RadioListItem';
import type ReportListItem from './Search/ReportListItem';
@@ -206,6 +207,21 @@ type TransactionListItemType = ListItem &
keyForList: string;
};
+type ReportActionListItemType = ListItem &
+ SearchReportAction & {
+ /** The personal details of the user posting comment */
+ from: SearchPersonalDetails;
+
+ /** final and formatted "from" value used for displaying and sorting */
+ formattedFrom: string;
+
+ /** final "date" value used for sorting */
+ date: string;
+
+ /** Key used internally by React */
+ keyForList: string;
+ };
+
type ReportListItemType = ListItem &
SearchReport & {
/** The personal details of the user requesting money */
@@ -277,7 +293,16 @@ type TransactionListItemProps = ListItemProps;
type ReportListItemProps = ListItemProps;
-type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem | typeof TransactionListItem | typeof ReportListItem;
+type ChatListItemProps = ListItemProps;
+
+type ValidListItem =
+ | typeof RadioListItem
+ | typeof UserListItem
+ | typeof TableListItem
+ | typeof InviteMemberListItem
+ | typeof TransactionListItem
+ | typeof ReportListItem
+ | typeof ChatListItem;
type Section = {
/** Title of the section */
@@ -556,4 +581,6 @@ export type {
TransactionListItemType,
UserListItemProps,
ValidListItem,
+ ReportActionListItemType,
+ ChatListItemProps,
};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c2902dc3d8a5..2c163aeee794 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -147,6 +147,7 @@ export default {
buttonConfirm: 'Got it',
name: 'Name',
attachment: 'Attachment',
+ attachments: 'Attachments',
center: 'Center',
from: 'From',
to: 'To',
@@ -388,6 +389,10 @@ export default {
importSpreadsheet: 'Import spreadsheet',
offlinePrompt: "You can't take this action right now.",
outstanding: 'Outstanding',
+ chats: 'Chats',
+ unread: 'Unread',
+ sent: 'Sent',
+ links: 'Links',
days: 'days',
},
location: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 9ce98e12b135..c2b0ae8a041f 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -137,6 +137,7 @@ export default {
buttonConfirm: 'Ok, entendido',
name: 'Nombre',
attachment: 'Archivo adjunto',
+ attachments: 'Archivos adjuntos',
from: 'De',
to: 'A',
in: 'En',
@@ -378,6 +379,10 @@ export default {
import: 'Importar',
offlinePrompt: 'No puedes realizar esta acción ahora mismo.',
outstanding: 'Pendiente',
+ chats: 'Chats',
+ unread: 'No leído',
+ sent: 'Enviado',
+ links: 'Enlaces',
days: 'días',
},
connectionComplete: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index c8c2c0f0e41d..a9258029cba7 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1332,6 +1332,7 @@ type AuthScreensParamList = CentralPaneScreensParamList &
type SearchReportParamList = {
[SCREENS.SEARCH.REPORT_RHP]: {
reportID: string;
+ reportActionID?: string;
};
};
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index e0a12e967bfa..e0832b6a1512 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -1,8 +1,9 @@
import type {ValueOf} from 'type-fest';
import type {ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SearchStatus, SortOrder} from '@components/Search/types';
+import ChatListItem from '@components/SelectionList/ChatListItem';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
-import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -76,10 +77,32 @@ function getTransactionItemCommonFormattedProperties(
};
}
+type ReportKey = `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`;
+
+type TransactionKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
+
+type ReportActionKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`;
+
+function isReportEntry(key: string): key is ReportKey {
+ return key.startsWith(ONYXKEYS.COLLECTION.REPORT);
+}
+
+function isTransactionEntry(key: string): key is TransactionKey {
+ return key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION);
+}
+
+function isReportActionEntry(key: string): key is ReportActionKey {
+ return key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+}
+
function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean {
- return Object.values(data).some((item) => {
- const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
- return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
+ return Object.keys(data).some((key) => {
+ if (isTransactionEntry(key)) {
+ const item = data[key];
+ const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
+ return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
+ }
+ return false;
});
}
@@ -89,11 +112,16 @@ function isReportListItemType(item: ListItem): item is ReportListItemType {
return 'transactions' in item;
}
-function isTransactionListItemType(item: TransactionListItemType | ReportListItemType): item is TransactionListItemType {
+function isTransactionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is TransactionListItemType {
const transactionListItem = item as TransactionListItemType;
return transactionListItem.transactionID !== undefined;
}
+function isReportActionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is ReportActionListItemType {
+ const reportActionListItem = item as ReportActionListItemType;
+ return reportActionListItem.reportActionID !== undefined;
+}
+
function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | OnyxTypes.SearchResults['data']): boolean {
if (Array.isArray(data)) {
return data.some((item: TransactionListItemType | ReportListItemType) => {
@@ -110,14 +138,23 @@ function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] |
});
}
- for (const [key, transactionItem] of Object.entries(data)) {
- if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
- const item = transactionItem as SearchTransaction;
+ for (const key in data) {
+ if (isTransactionEntry(key)) {
+ const item = data[key];
const date = TransactionUtils.getCreated(item);
if (DateUtils.doesDateBelongToAPastYear(date)) {
return true;
}
+ } else if (isReportActionEntry(key)) {
+ const item = data[key];
+ for (const action of Object.values(item)) {
+ const date = action.created;
+
+ if (DateUtils.doesDateBelongToAPastYear(date)) {
+ return true;
+ }
+ }
}
}
return false;
@@ -128,9 +165,10 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
const doesDataContainAPastYearTransaction = shouldShowYear(data);
- return Object.entries(data)
- .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION))
- .map(([, transactionItem]) => {
+ return Object.keys(data)
+ .filter(isTransactionEntry)
+ .map((key) => {
+ const transactionItem = data[key];
const from = data.personalDetailsList?.[transactionItem.accountID];
const to = data.personalDetailsList?.[transactionItem.managerID];
@@ -155,6 +193,26 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
});
}
+function getReportActionsSections(data: OnyxTypes.SearchResults['data']): ReportActionListItemType[] {
+ const reportActionItems: ReportActionListItemType[] = [];
+ for (const key in data) {
+ if (isReportActionEntry(key)) {
+ const reportActions = data[key];
+ for (const reportAction of Object.values(reportActions)) {
+ const from = data.personalDetailsList?.[reportAction.accountID];
+ reportActionItems.push({
+ ...reportAction,
+ from,
+ formattedFrom: from?.displayName ?? from?.login ?? '',
+ date: reportAction.created,
+ keyForList: reportAction.reportActionID,
+ });
+ }
+ }
+ }
+ return reportActionItems;
+}
+
function getIOUReportName(data: OnyxTypes.SearchResults['data'], reportItem: SearchReport) {
const payerPersonalDetails = data.personalDetailsList?.[reportItem.managerID ?? 0];
const payerName = payerPersonalDetails?.displayName ?? payerPersonalDetails?.login ?? translateLocal('common.hidden');
@@ -183,7 +241,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
const reportIDToTransactions: Record = {};
for (const key in data) {
- if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) {
+ if (isReportEntry(key)) {
const reportItem = {...data[key]};
const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`;
const transactions = reportIDToTransactions[reportKey]?.transactions ?? [];
@@ -192,12 +250,12 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
reportIDToTransactions[reportKey] = {
...reportItem,
keyForList: reportItem.reportID,
- from: data.personalDetailsList?.[reportItem.accountID],
- to: data.personalDetailsList?.[reportItem.managerID],
+ from: data.personalDetailsList?.[reportItem.accountID ?? -1],
+ to: data.personalDetailsList?.[reportItem.managerID ?? -1],
transactions,
reportName: isIOUReport ? getIOUReportName(data, reportItem) : reportItem.reportName,
};
- } else if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
+ } else if (isTransactionEntry(key)) {
const transactionItem = {...data[key]};
const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`;
@@ -233,15 +291,24 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
return Object.values(reportIDToTransactions);
}
-function getListItem(status: SearchStatus): ListItemType {
+function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType {
+ if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return ChatListItem;
+ }
return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItem : ReportListItem;
}
-function getSections(status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
+function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
+ if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return getReportActionsSections(data);
+ }
return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getTransactionsSections(data, metadata) : getReportSections(data, metadata);
}
-function getSortedSections(status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
+function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
+ if (type === CONST.SEARCH.DATA_TYPES.CHAT) {
+ return data;
+ }
return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder) : getSortedReportData(data as ReportListItemType[]);
}
@@ -612,6 +679,7 @@ export {
isReportListItemType,
isSearchResultsEmpty,
isTransactionListItemType,
+ isReportActionListItemType,
normalizeQuery,
shouldShowYear,
buildCannedSearchQuery,
diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx
index 88b2a38b3603..1f259c96d625 100644
--- a/src/pages/Search/EmptySearchView.tsx
+++ b/src/pages/Search/EmptySearchView.tsx
@@ -32,6 +32,7 @@ function EmptySearchView({type}: EmptySearchViewProps) {
buttonText: translate('search.searchResults.emptyTripResults.buttonText'),
buttonAction: () => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS),
};
+ case CONST.SEARCH.DATA_TYPES.CHAT:
case CONST.SEARCH.DATA_TYPES.EXPENSE:
case CONST.SEARCH.DATA_TYPES.INVOICE:
default:
diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx
index 810e7140060f..a83fd364fa4a 100644
--- a/src/pages/Search/SearchTypeMenu.tsx
+++ b/src/pages/Search/SearchTypeMenu.tsx
@@ -42,6 +42,12 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
icon: Expensicons.Receipt,
route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}),
},
+ {
+ title: translate('common.chats'),
+ type: CONST.SEARCH.DATA_TYPES.CHAT,
+ icon: Expensicons.ChatBubbles,
+ route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.STATUS.TRIP.ALL)}),
+ },
{
title: translate('workspace.common.invoices'),
type: CONST.SEARCH.DATA_TYPES.INVOICE,
diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx
index 7140cd2d45c4..369d5cef6ee4 100644
--- a/src/pages/home/report/ReportAttachments.tsx
+++ b/src/pages/home/report/ReportAttachments.tsx
@@ -6,6 +6,7 @@ import type {Attachment} from '@components/Attachments/types';
import ComposerFocusManager from '@libs/ComposerFocusManager';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -55,7 +56,8 @@ function ReportAttachments({route}: ReportAttachmentsProps) {
ComposerFocusManager.setReadyToFocus();
}}
onCarouselAttachmentChange={onCarouselAttachmentChange}
- shouldShowNotFoundPage={!isLoadingApp && !report?.reportID}
+ shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID}
+ isAuthTokenRequired={type === CONST.ATTACHMENT_TYPE.SEARCH}
/>
);
}
diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts
index e67b5b30f1d6..98e3aa66fa00 100644
--- a/src/types/onyx/SearchResults.ts
+++ b/src/types/onyx/SearchResults.ts
@@ -1,18 +1,29 @@
import type {ValueOf} from 'type-fest';
import type {SearchStatus} from '@components/Search/types';
+import type ChatListItem from '@components/SelectionList/ChatListItem';
import type ReportListItem from '@components/SelectionList/Search/ReportListItem';
import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
-import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import type CONST from '@src/CONST';
+import type ONYXKEYS from '@src/ONYXKEYS';
+import type ReportActionName from './ReportActionName';
/** Types of search data */
type SearchDataTypes = ValueOf;
/** Model of search result list item */
-type ListItemType = T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL ? typeof TransactionListItem : typeof ReportListItem;
+type ListItemType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT
+ ? typeof ChatListItem
+ : T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL
+ ? typeof TransactionListItem
+ : typeof ReportListItem;
/** Model of search list item data type */
-type ListItemDataType = T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItemType[] : ReportListItemType[];
+type ListItemDataType = C extends typeof CONST.SEARCH.DATA_TYPES.CHAT
+ ? ReportActionListItemType[]
+ : T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL
+ ? TransactionListItemType[]
+ : ReportListItemType[];
/** Model of columns to show for search results */
type ColumnsToShow = {
@@ -98,6 +109,39 @@ type SearchReport = {
action?: SearchTransactionAction;
};
+/** Model of report action search result */
+type SearchReportAction = {
+ /** The report action sender ID */
+ accountID: number;
+
+ /** The name (or type) of the action */
+ actionName: ReportActionName;
+
+ /** The report action created date */
+ created: string;
+
+ /** report action message */
+ message: Array<{
+ /** The type of the action item fragment. Used to render a corresponding component */
+ type: string;
+
+ /** The text content of the fragment. */
+ text: string;
+
+ /** The html content of the fragment. */
+ html: string;
+
+ /** Collection of accountIDs of users mentioned in message */
+ whisperedTo?: number[];
+ }>;
+
+ /** The ID of the report action */
+ reportActionID: string;
+
+ /** The ID of the report */
+ reportID: string;
+};
+
/** Model of transaction search result */
type SearchTransaction = {
/** The ID of the transaction */
@@ -212,13 +256,23 @@ type SearchTransaction = {
/** Types of searchable transactions */
type SearchTransactionType = ValueOf;
+/**
+ * A utility type that creates a record where all keys are strings that start with a specified prefix.
+ */
+type PrefixedRecord = {
+ [Key in `${Prefix}${string}`]: ValueType;
+};
+
/** Model of search results */
type SearchResults = {
/** Current search results state */
search: SearchResultsInfo;
/** Search results data */
- data: Record> & Record;
+ data: PrefixedRecord &
+ Record> &
+ PrefixedRecord> &
+ PrefixedRecord;
/** Whether search data is being fetched from server */
isLoading?: boolean;
@@ -226,4 +280,4 @@ type SearchResults = {
export default SearchResults;
-export type {ListItemType, ListItemDataType, SearchTransaction, SearchTransactionType, SearchTransactionAction, SearchPersonalDetails, SearchDataTypes, SearchReport};
+export type {ListItemType, ListItemDataType, SearchTransaction, SearchTransactionType, SearchTransactionAction, SearchPersonalDetails, SearchDataTypes, SearchReport, SearchReportAction};