import React, { useCallback, useEffect, useReducer, useRef, useState, useMemo } from 'react'; import { View, TextInput, TouchableOpacity, Text, Image, ImageBackground, FlatList, ImageSourcePropType } from 'react-native'; import { CometChat } from '@cometchat/chat-sdk-react-native'; import { CometChatAvatar, CometChatStatusIndicator, CometChatBadge, CometChatDate, useTheme } from '@cometchat/chat-uikit-react-native'; import { Icon } from '../shared/icons/Icon'; import { GroupTypeConstants, ReceiverTypeConstants, MessageCategoryConstants, MessageTypeConstants } from '@cometchat/chat-uikit-react-native/src/shared/constants/UIKitConstants'; import { CometChatSearchScope, CometChatSearchFilter, States } from './SearchConstants'; import { Skeleton } from './Skeleton'; import { SearchStyle, getSearchStyleLight, getSearchStyleDark } from './style'; import { deepMerge } from '../shared/helper/helperFunctions'; import { DeepPartial } from '../shared/helper/types'; import { getCometChatTranslation } from '../shared/resources/CometChatLocalizeNew/LocalizationManager'; import { CommonUtils } from '../shared/utils/CommonUtils'; import { stripMarkdown } from '../shared/utils/MarkdownUtils'; import { ExtensionConstants } from '../extensions/ExtensionConstants'; import { getExtensionData } from '../extensions/ExtensionModerator'; // ERROR HANDLER function useCometChatErrorHandler(onError?: (error: CometChat.CometChatException) => void) { return useCallback( (error: any, source?: string) => { console.error(`CometChat Error${source ? ` in ${source}` : ''}:`, error); if (onError && error instanceof CometChat.CometChatException) { onError(error); } }, [onError] ); } // SEARCH UTILITIES function hasValidConversationSearchCriteria(searchKeyword: string, filters: CometChatSearchFilter[]): boolean { if (searchKeyword && searchKeyword.trim() !== "") { return true; } const validFilters = [ CometChatSearchFilter.Unread, CometChatSearchFilter.Groups, CometChatSearchFilter.Conversations ]; if (filters.length === 0) { return false; } return filters.every(filter => validFilters.includes(filter)); } function hasValidMessageSearchCriteria(searchKeyword: string, filters: CometChatSearchFilter[]): boolean { if (searchKeyword && searchKeyword.trim() !== "") { return true; } const validMessageFilters = [ CometChatSearchFilter.Photos, CometChatSearchFilter.Videos, CometChatSearchFilter.Documents, CometChatSearchFilter.Audio, CometChatSearchFilter.Links ]; if (filters.length === 0) { return false; } return filters.some(filter => validMessageFilters.includes(filter)); } interface SearchState { searchText: string; conversations: CometChat.Conversation[]; messages: CometChat.BaseMessage[]; fetchState: States; activeFilters: CometChatSearchFilter[]; } type SearchAction = | { type: "setSearchText"; searchText: string } | { type: "setResults"; conversations?: CometChat.Conversation[]; messages?: CometChat.BaseMessage[] } | { type: "clearResults" } | { type: "setFetchState"; fetchState: States } | { type: "setActiveFilter"; filterId: CometChatSearchFilter } | { type: "resetActiveFilters" }; const initialState: SearchState = { searchText: "", conversations: [], messages: [], fetchState: States.loaded, activeFilters: [], }; function searchStateReducer(state: SearchState, action: SearchAction): SearchState { switch (action.type) { case "setSearchText": return { ...state, searchText: action.searchText }; case "setResults": return { ...state, conversations: action.conversations ?? state.conversations, messages: action.messages ?? state.messages, fetchState: States.loaded, }; case "clearResults": return { ...state, conversations: [], messages: [] }; case "setFetchState": return { ...state, fetchState: action.fetchState }; case "setActiveFilter": { const newActiveFilters = [...state.activeFilters]; const filterIndex = newActiveFilters.findIndex(f => f === action.filterId); // Define filter pairs const filterPairs = { conversationPair: [CometChatSearchFilter.Unread, CometChatSearchFilter.Groups], photosVideosPair: [CometChatSearchFilter.Photos, CometChatSearchFilter.Videos], audioDocumentsPair: [CometChatSearchFilter.Audio, CometChatSearchFilter.Documents], linkStandalone: [CometChatSearchFilter.Links] }; if (filterIndex >= 0) { // If filter is already active, remove it newActiveFilters.splice(filterIndex, 1); } else { // Clear incompatible filters before adding new one // If selecting Links (standalone), clear all other filters if (action.filterId === CometChatSearchFilter.Links) { newActiveFilters.length = 0; // Clear all filters } // If selecting conversation pair filter, clear other pairs else if (filterPairs.conversationPair.includes(action.filterId)) { // Remove all non-conversation filters const filtersToRemove = [ ...filterPairs.photosVideosPair, ...filterPairs.audioDocumentsPair, ...filterPairs.linkStandalone ]; filtersToRemove.forEach(filter => { const idx = newActiveFilters.findIndex(f => f === filter); if (idx >= 0) newActiveFilters.splice(idx, 1); }); } // If selecting photos/videos pair filter, clear other pairs else if (filterPairs.photosVideosPair.includes(action.filterId)) { const filtersToRemove = [ ...filterPairs.conversationPair, ...filterPairs.audioDocumentsPair, ...filterPairs.linkStandalone ]; filtersToRemove.forEach(filter => { const idx = newActiveFilters.findIndex(f => f === filter); if (idx >= 0) newActiveFilters.splice(idx, 1); }); } // If selecting audio/documents pair filter, clear other pairs else if (filterPairs.audioDocumentsPair.includes(action.filterId)) { const filtersToRemove = [ ...filterPairs.conversationPair, ...filterPairs.photosVideosPair, ...filterPairs.linkStandalone ]; filtersToRemove.forEach(filter => { const idx = newActiveFilters.findIndex(f => f === filter); if (idx >= 0) newActiveFilters.splice(idx, 1); }); } newActiveFilters.push(action.filterId); } return { ...state, activeFilters: newActiveFilters }; } case "resetActiveFilters": return { ...state, activeFilters: [] }; default: return state; } } /** * Props for the CometChatSearch component */ interface CometChatSearchProps { /** * Callback triggered when the back button is clicked * Use this to handle navigation when user clicks the back button */ onBack?: () => void; /** * Whether to hide the back button */ hideBackButton?: boolean; /** * Callback triggered when a conversation is clicked in search results * @param conversation - The conversation that was clicked * @param searchKeyword - The keyword that was used in the search */ onConversationClicked?: (conversation: CometChat.Conversation, searchKeyword?: string) => void; /** * Callback triggered when a message is clicked in search results * @param message - The message that was clicked * @param searchKeyword - The keyword that was used in the search */ onMessageClicked?: (message: CometChat.BaseMessage, searchKeyword?: string) => void; /** * Array of search filters to display in the filter bar */ searchFilters?: Array; /** * Filter that should be active by default when the component loads */ initialSearchFilter?: CometChatSearchFilter; /** * Scopes to search in (Conversations, Messages, or both) */ searchIn?: Array; /** * Request builder for conversations search */ conversationsRequestBuilder?: CometChat.ConversationsRequestBuilder; /** * Request builder for messages search */ messagesRequestBuilder?: CometChat.MessagesRequestBuilder; /** * Custom error handler for search operations */ onError?: (error: CometChat.CometChatException) => void; /** * User ID to search within specific user's messages */ uid?: string; /** * Group ID to search within specific group's messages */ guid?: string; /** * Search placeholder text */ searchPlaceholder?: string; /** * Custom styles for the component */ style?: DeepPartial; /** * Custom loading view component that will be shown during search operations */ loadingView?: () => React.ReactElement; /** * Custom empty state view component that will be shown when no results are found */ emptyView?: () => React.ReactElement; /** * Custom error state view component that will be shown when search fails */ errorView?: () => React.ReactElement; /** * Custom view component for conversation items (both unread and group conversations) * @param conversation - The conversation object to render * @param searchKeyword - The search keyword used */ conversationItemView?: (conversation: CometChat.Conversation, searchKeyword?: string) => React.ReactElement; /** * Custom view component for text message items * @param message - The text message object to render * @param searchKeyword - The search keyword used */ textMessageItemView?: (message: CometChat.BaseMessage, searchKeyword?: string) => React.ReactElement; /** * Custom view component for image message items * @param message - The image message object to render * @param searchKeyword - The search keyword used */ imageMessageItemView?: (message: CometChat.BaseMessage, searchKeyword?: string) => React.ReactElement; /** * Custom view component for audio message items * @param message - The audio message object to render * @param searchKeyword - The search keyword used */ audioMessageItemView?: (message: CometChat.BaseMessage, searchKeyword?: string) => React.ReactElement; /** * Custom view component for video message items * @param message - The video message object to render * @param searchKeyword - The search keyword used */ videoMessageItemView?: (message: CometChat.BaseMessage, searchKeyword?: string) => React.ReactElement; /** * Custom view component for document/file message items * @param message - The document message object to render * @param searchKeyword - The search keyword used */ documentMessageItemView?: (message: CometChat.BaseMessage, searchKeyword?: string) => React.ReactElement; /** * Custom view component for link message items (text messages with link previews) * @param message - The link message object to render * @param searchKeyword - The search keyword used */ linkMessageItemView?: (message: CometChat.BaseMessage, searchKeyword?: string) => React.ReactElement; } const t = getCometChatTranslation(); interface LinkPreviewImageProps { uri: string; fallbackUri?: string; mergedStyles: any; theme: any; } const LinkFallbackIcon = ({ mergedStyles, theme }: { mergedStyles: any; theme: any }) => ( ); const LinkPreviewImage: React.FC = ({ uri, fallbackUri, mergedStyles, theme }) => { const [imageError, setImageError] = useState(false); const [fallbackError, setFallbackError] = useState(false); if (imageError && (!fallbackUri || fallbackError)) { return ; } return ( imageError ? setFallbackError(true) : setImageError(true)} /> ); }; // Helper function to check for extension-generated thumbnails const checkThumbnail = (message: CometChat.MediaMessage): { uri: string } => { let image: { uri: string } = { uri: "" }; const thumbnailData = getExtensionData(message, ExtensionConstants.thumbnailGeneration); if (thumbnailData == undefined) { // Fallback to attachment thumbnail for videos if (message.getType() === "video") { const attachment = message.getAttachment(); const thumbnailUrl = attachment && ((attachment as any).thumbnail || (attachment as any).url); image = thumbnailUrl ? { uri: thumbnailUrl } : image; } } else { // Extension-generated thumbnail exists const attachmentData = thumbnailData["attachments"]; if (attachmentData && attachmentData.length) { const dataObj = attachmentData[0]; if (!dataObj["error"]) { const imageLink = dataObj?.["data"]?.["thumbnails"]?.["url_small"]; if (imageLink) { image = { uri: dataObj["data"]["thumbnails"]["url_small"] }; } } } } return image; }; interface VideoThumbnailProps { thumbnailUrl: ImageSourcePropType; mergedStyles: any; } const VideoThumbnail = React.memo(({ thumbnailUrl, mergedStyles, }: VideoThumbnailProps) => { const [imageSource, setImageSource] = useState(thumbnailUrl); useEffect(() => { if (thumbnailUrl && typeof thumbnailUrl === "object" && "uri" in thumbnailUrl) { CommonUtils.prefetchThumbnail((thumbnailUrl as any).uri).then((success) => { if (success) { // console.log("success", thumbnailUrl); setImageSource(thumbnailUrl); } }).catch((error) => { console.log("error", error); }); } }, [thumbnailUrl]); return ( ); }, /** Custom comparison function */ (prev, next) => { const prevUri = (prev.thumbnailUrl as any)?.uri; const nextUri = (next.thumbnailUrl as any)?.uri; return prevUri === nextUri; } ); // Memoized Conversation Item Component interface ConversationItemProps { conversation: CometChat.Conversation; searchText: string; onPress: (conversation: CometChat.Conversation, searchText: string) => void; mergedStyles: any; theme: any; conversationItemView?: (conversation: CometChat.Conversation, searchKeyword?: string) => React.ReactElement; } const ConversationItem = React.memo(( { conversation, searchText, onPress, mergedStyles, theme, conversationItemView } ) => { const getStatusIndicator = () => { const withObj = conversation.getConversationWith(); if (withObj instanceof CometChat.Group) { if (withObj.getType() === GroupTypeConstants.password) return "password"; if (withObj.getType() === GroupTypeConstants.private) return "private"; } else if (withObj instanceof CometChat.User) { if (withObj.getStatus() === 'online' && !withObj.getHasBlockedMe() && !withObj.getBlockedByMe()) { return "online"; } return "offline"; } return undefined; }; const renderLeadingView = () => { const withObj = conversation.getConversationWith(); const avatarURL = withObj instanceof CometChat.User ? withObj.getAvatar() : withObj.getIcon(); const name = withObj.getName(); return ( ); }; const renderTrailingView = () => { const timestamp = conversation.getLastMessage()?.getSentAt(); if (!timestamp) return null; return ( ); }; const formatMentionsInText = (rawText: any, message?: CometChat.BaseMessage) => { if (typeof rawText !== 'string') return rawText; // Strip markdown syntax for clean search result preview while // preserving mention tokens for processing below. let text = stripMarkdown(rawText).replace(/<@all:(.*?)>/g, '@$1'); try { const mentionedUsers: CometChat.User[] = (message && (message).getMentionedUsers && (message).getMentionedUsers()) || []; if (mentionedUsers && mentionedUsers.length > 0) { mentionedUsers.forEach((u: CometChat.User) => { const uid = u.getUid(); const name = u.getName(); if (uid && name) { const uidRegex = new RegExp(`<@uid:${uid}>`, 'g'); text = text.replace(uidRegex, `@${name}`); } }); } } catch (e) { console.log(e) } return text; }; if (conversationItemView) { return ( onPress(conversation, searchText)}> {conversationItemView(conversation, searchText)} ); } return ( onPress(conversation, searchText)} > {renderLeadingView()} {conversation.getConversationWith().getName()} {formatMentionsInText((conversation.getLastMessage())?.getText?.(), conversation.getLastMessage())} {renderTrailingView()} ); }, (prevProps, nextProps) => { // Only re-render if conversation ID or search text changes return prevProps.conversation.getConversationId() === nextProps.conversation.getConversationId() && prevProps.searchText === nextProps.searchText; }); ConversationItem.displayName = 'ConversationItem'; /** * CometChatSearch component for searching conversations and messages in CometChat */ export const CometChatSearch: React.FC = ({ onBack = () => { }, hideBackButton = false, onConversationClicked, onMessageClicked, searchFilters = [ CometChatSearchFilter.Unread, CometChatSearchFilter.Groups, CometChatSearchFilter.Photos, CometChatSearchFilter.Videos, CometChatSearchFilter.Links, CometChatSearchFilter.Documents, CometChatSearchFilter.Audio ], initialSearchFilter, searchIn = [], conversationsRequestBuilder, messagesRequestBuilder, onError, uid, guid, searchPlaceholder = t("SEARCH_PLACEHOLDER") || "Search...", style, loadingView, emptyView, errorView, conversationItemView, textMessageItemView, imageMessageItemView, audioMessageItemView, videoMessageItemView, documentMessageItemView, linkMessageItemView }) => { const [searchState, dispatch] = useReducer(searchStateReducer, initialState); const [searchValue, setSearchValue] = useState(""); const [loggedInUser, setLoggedInUser] = useState(null); const theme = useTheme(); const errorHandler = useCometChatErrorHandler(onError); const timeoutIdRef = useRef(null); const searchInputRef = useRef(null); // Merge theme styles with provided style overrides const mergedStyles = useMemo(() => { const baseStyles = getSearchStyleLight(theme.color, theme.spacing, theme.typography); return deepMerge(baseStyles, style ?? {}); }, [theme, style]); // Initialize active filters with initial filter if provided useEffect(() => { if (searchState.activeFilters.length === 0 && initialSearchFilter) { dispatch({ type: "setActiveFilter", filterId: initialSearchFilter }); } }, [initialSearchFilter, searchState.activeFilters.length]); // Get logged in user useEffect(() => { const getLoggedInUser = async () => { try { const user = await CometChat.getLoggedinUser(); setLoggedInUser(user); } catch (error) { errorHandler(error as CometChat.CometChatException, "getLoggedInUser"); } }; getLoggedInUser(); }, [errorHandler]); // Handle search input changes with debouncing const handleSearch = useCallback((text: string) => { setSearchValue(text); const newSearchText = text.trim(); if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } timeoutIdRef.current = setTimeout(() => { dispatch({ type: "setSearchText", searchText: newSearchText }); timeoutIdRef.current = null; }, 500); }, []); // Clear search const handleClearSearch = useCallback(() => { setSearchValue(""); dispatch({ type: "setSearchText", searchText: "" }); dispatch({ type: "clearResults" }); }, []); // Toggle filter const toggleFilter = useCallback((filterId: CometChatSearchFilter) => { dispatch({ type: "setActiveFilter", filterId }); }, []); // Auto-focus the search input when component mounts useEffect(() => { const timer = setTimeout(() => { if (searchInputRef.current) { searchInputRef.current.focus(); } }, 100); return () => clearTimeout(timer); }, []); // Cleanup timeout on unmount useEffect(() => { return () => { if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } }; }, []); const renderHeader = () => ( {!hideBackButton && ( )} ); const getVisibleFilters = useCallback(() => { const conversationFilters = [ CometChatSearchFilter.Conversations, CometChatSearchFilter.Unread, CometChatSearchFilter.Groups ]; const messageFilters = [ CometChatSearchFilter.Messages, CometChatSearchFilter.Photos, CometChatSearchFilter.Videos, CometChatSearchFilter.Documents, CometChatSearchFilter.Audio, CometChatSearchFilter.Links ]; // Define filter pairs const filterPairs = { conversationPair: [CometChatSearchFilter.Unread, CometChatSearchFilter.Groups], photosVideosPair: [CometChatSearchFilter.Photos, CometChatSearchFilter.Videos], audioDocumentsPair: [CometChatSearchFilter.Audio, CometChatSearchFilter.Documents], linkStandalone: [CometChatSearchFilter.Links] }; // If searchIn is empty, search in both conversations and messages const effectiveSearchIn = searchIn.length === 0 ? [CometChatSearchScope.Conversations, CometChatSearchScope.Messages] : searchIn; let availableFilters: CometChatSearchFilter[] = []; // Add filters based on searchIn scope if (effectiveSearchIn.includes(CometChatSearchScope.Conversations)) { availableFilters.push(...conversationFilters); } if (effectiveSearchIn.includes(CometChatSearchScope.Messages)) { availableFilters.push(...messageFilters); } // Filter the searchFilters prop to only include available filters let filteredAvailableFilters = searchFilters.filter(filter => availableFilters.includes(filter)); // Filter out conversation filters if uid or guid is present if (uid || guid) { filteredAvailableFilters = filteredAvailableFilters.filter(filter => !conversationFilters.includes(filter)); } // If no filters are selected, return filtered available filters if (searchState.activeFilters.length === 0) { return filteredAvailableFilters; } // Handle pairing logic const activeFilters = searchState.activeFilters; // Check which pair is active const hasConversationPair = activeFilters.some(filter => filterPairs.conversationPair.includes(filter)); const hasPhotosVideosPair = activeFilters.some(filter => filterPairs.photosVideosPair.includes(filter)); const hasAudioDocumentsPair = activeFilters.some(filter => filterPairs.audioDocumentsPair.includes(filter)); const hasLinkStandalone = activeFilters.includes(CometChatSearchFilter.Links); // If conversation pair is active, show only conversation filters if (hasConversationPair && effectiveSearchIn.includes(CometChatSearchScope.Conversations) && !uid && !guid) { return filterPairs.conversationPair.filter(filter => filteredAvailableFilters.includes(filter)); } // If photos/videos pair is active, show only photos and videos if (hasPhotosVideosPair) { return filterPairs.photosVideosPair.filter(filter => filteredAvailableFilters.includes(filter)); } // If audio/documents pair is active, show only audio and documents if (hasAudioDocumentsPair) { return filterPairs.audioDocumentsPair.filter(filter => filteredAvailableFilters.includes(filter)); } // If links is active (standalone), show only links if (hasLinkStandalone) { return filterPairs.linkStandalone.filter(filter => filteredAvailableFilters.includes(filter)); } // Fallback: return filtered available filters return filteredAvailableFilters; }, [searchState.activeFilters, searchFilters, searchIn, uid, guid]); const getFilterText = (filterId: CometChatSearchFilter): string => { const filterTextMap = { [CometChatSearchFilter.Audio]: t("SEARCH_FILTER_AUDIO") || "Audio", [CometChatSearchFilter.Conversations]: t("SEARCH_FILTER_CONVERSATIONS") || "Conversations", [CometChatSearchFilter.Documents]: t("SEARCH_FILTER_DOCUMENTS") || "Documents", [CometChatSearchFilter.Groups]: t("SEARCH_FILTER_GROUPS") || "Groups", [CometChatSearchFilter.Links]: t("SEARCH_FILTER_LINKS") || "Links", [CometChatSearchFilter.Messages]: t("SEARCH_FILTER_MESSAGES") || "Messages", [CometChatSearchFilter.Photos]: t("SEARCH_FILTER_PHOTOS") || "Photos", [CometChatSearchFilter.Unread]: t("SEARCH_FILTER_UNREAD") || "Unread", [CometChatSearchFilter.Videos]: t("SEARCH_FILTER_VIDEOS") || "Videos", }; return filterTextMap[filterId] || ""; }; const getFilterIcon = (filterId: CometChatSearchFilter) => { const filterIconMap = { [CometChatSearchFilter.Audio]: "mic-fill" as const, [CometChatSearchFilter.Documents]: "documents" as const, [CometChatSearchFilter.Groups]: "group" as const, [CometChatSearchFilter.Links]: "link" as const, [CometChatSearchFilter.Messages]: "chat" as const, [CometChatSearchFilter.Photos]: "photo" as const, [CometChatSearchFilter.Unread]: "unread" as const, [CometChatSearchFilter.Videos]: "videocam" as const, [CometChatSearchFilter.Conversations]: "chat" as const, }; return filterIconMap[filterId]; }; const renderFilters = () => { const visibleFilters = getVisibleFilters(); if (visibleFilters.length === 0) { return null; } return ( {visibleFilters.map((filterId) => { const isActive = searchState.activeFilters.includes(filterId); const iconName = getFilterIcon(filterId); return ( toggleFilter(filterId)} > {iconName && ( )} {getFilterText(filterId)} ); })} ); }; // ============================================================================ // CONVERSATIONS SEARCH LOGIC (moved from useCometChatSearchConversationsList.ts) // ============================================================================ const [conversationState, setConversationState] = useState({ conversationList: [] as CometChat.Conversation[], fetchState: States.loaded, hasMoreResults: false, }); const conversationRequestRef = useRef(null); const lastConversationSearchKeyword = useRef(searchState.searchText); const lastConversationActiveFilters = useRef(searchState.activeFilters); const isConversationMoreResultsLoading = useRef(false); // ============================================================================ // MESSAGES SEARCH LOGIC (moved from useCometChatSearchMessagesList.ts) // ============================================================================ const [messageState, setMessageState] = useState({ messageList: [] as CometChat.BaseMessage[], fetchState: States.loaded, hasMoreResults: false, }); const messageRequestRef = useRef(null); const lastMessageSearchKeyword = useRef(searchState.searchText); const lastMessageActiveFilters = useRef(searchState.activeFilters); const isMessageMoreResultsLoading = useRef(false); // ============================================================================ // CONVERSATION SEARCH FUNCTIONS // ============================================================================ const buildConversationsRequest = useCallback(() => { let builder = conversationsRequestBuilder ? Object.assign(Object.create(Object.getPrototypeOf(conversationsRequestBuilder)), conversationsRequestBuilder) : new CometChat.ConversationsRequestBuilder(); if (searchState.searchText && searchState.searchText.trim() !== "") { builder = builder.setSearchKeyword(searchState.searchText); } const limit = (guid || uid || searchState.activeFilters.length > 0) ? 30 : 3; builder = builder.setLimit(limit); // Apply filters - both can be active simultaneously for unread group conversations if (searchState.activeFilters.includes(CometChatSearchFilter.Unread)) { builder = builder.setUnread(true); } if (searchState.activeFilters.includes(CometChatSearchFilter.Groups)) { builder = builder.setConversationType(ReceiverTypeConstants.group); } return builder.build(); }, [conversationsRequestBuilder, searchState.searchText, searchState.activeFilters, guid, uid]); const loadMoreConversations = useCallback(async () => { if (isConversationMoreResultsLoading.current || !conversationRequestRef.current || !conversationState.hasMoreResults) { return; } try { isConversationMoreResultsLoading.current = true; const moreConversations = await conversationRequestRef.current.fetchNext(); if (moreConversations.length > 0) { setConversationState(prev => ({ ...prev, conversationList: [...prev.conversationList, ...moreConversations], hasMoreResults: moreConversations.length === 3 || moreConversations.length === 30 // Check if more results might be available })); } else { setConversationState(prev => ({ ...prev, hasMoreResults: false })); } } catch (error) { errorHandler(error, "loadMoreConversations"); } finally { isConversationMoreResultsLoading.current = false; } }, [errorHandler, conversationState.hasMoreResults]); const searchConversations = useCallback(async () => { try { setConversationState(prev => ({ ...prev, fetchState: States.loading })); if (!hasValidConversationSearchCriteria(searchState.searchText, searchState.activeFilters)) { setConversationState(prev => ({ ...prev, conversationList: [], fetchState: States.loaded, hasMoreResults: false })); return; } conversationRequestRef.current = buildConversationsRequest(); const conversations = await conversationRequestRef.current!.fetchNext(); if (conversations.length > 0) { const limit = (guid || uid || searchState.activeFilters.length > 0) ? 30 : 3; setConversationState({ conversationList: conversations, fetchState: States.loaded, hasMoreResults: conversations.length >= limit, }); } else { setConversationState({ conversationList: [], fetchState: States.loaded, hasMoreResults: false, }); } } catch (error) { errorHandler(error, "searchConversations"); setConversationState(prev => ({ ...prev, fetchState: States.error })); } }, [searchState.searchText, searchState.activeFilters, buildConversationsRequest, guid, uid, errorHandler]); // ============================================================================ // MESSAGE SEARCH FUNCTIONS // ============================================================================ const buildMessagesRequest = useCallback(() => { let builder = messagesRequestBuilder ? Object.assign(Object.create(Object.getPrototypeOf(messagesRequestBuilder)), messagesRequestBuilder) : new CometChat.MessagesRequestBuilder(); builder = builder.hideDeletedMessages(true); const limit = (guid || uid || searchState.activeFilters.length > 0) ? 30 : 3; if (!messagesRequestBuilder) { builder = builder .setCategories([MessageCategoryConstants.message, MessageCategoryConstants.custom]) .setTypes([MessageTypeConstants.text, MessageTypeConstants.image, MessageTypeConstants.video, MessageTypeConstants.audio, MessageTypeConstants.file]) .setLimit(limit); } if (searchState.searchText && searchState.searchText.trim() !== "") { builder = builder.setSearchKeyword(searchState.searchText); } if (uid) { builder = builder.setUID(uid); } else if (guid) { builder = builder.setGUID(guid); } if (searchState.activeFilters && searchState.activeFilters.length > 0) { if (searchState.activeFilters.includes(CometChatSearchFilter.Links)) { builder = builder.hasLinks(true); } const attachmentTypeMap = { [CometChatSearchFilter.Photos]: CometChat.AttachmentType.IMAGE, [CometChatSearchFilter.Videos]: CometChat.AttachmentType.VIDEO, [CometChatSearchFilter.Documents]: CometChat.AttachmentType.FILE, [CometChatSearchFilter.Audio]: CometChat.AttachmentType.AUDIO, }; // Collect all attachment types for active filters const activeAttachmentTypes: CometChat.AttachmentType[] = []; for (const [filter, attachmentType] of Object.entries(attachmentTypeMap)) { if (searchState.activeFilters.includes(filter as CometChatSearchFilter)) { activeAttachmentTypes.push(attachmentType); } } // Set all active attachment types (supports multiple types for paired filters) if (activeAttachmentTypes.length > 0) { builder = builder.setAttachmentTypes(activeAttachmentTypes); } } return builder.build(); }, [messagesRequestBuilder, searchState.activeFilters, searchState.searchText, uid, guid]); const loadMoreMessages = useCallback(async () => { if (isMessageMoreResultsLoading.current || !messageRequestRef.current || !messageState.hasMoreResults) { return; } try { isMessageMoreResultsLoading.current = true; const moreMessages = await messageRequestRef.current.fetchPrevious(); if (moreMessages.length > 0) { const filteredMoreMessages = moreMessages.filter(msg => { const isDeleted = (msg as any).getDeletedAt && (msg as any).getDeletedAt() > 0; const isActionMessage = msg.getCategory() === MessageCategoryConstants.action; return !isDeleted && !isActionMessage; }); setMessageState(prev => ({ ...prev, messageList: [...prev.messageList, ...filteredMoreMessages], hasMoreResults: moreMessages.length === 3 || moreMessages.length === 30 // Check if more results might be available })); } else { setMessageState(prev => ({ ...prev, hasMoreResults: false })); } } catch (error) { errorHandler(error, "loadMoreMessages"); } finally { isMessageMoreResultsLoading.current = false; } }, [errorHandler, messageState.hasMoreResults]); const searchMessages = useCallback(async () => { try { setMessageState(prev => ({ ...prev, fetchState: States.loading })); if (!hasValidMessageSearchCriteria(searchState.searchText, searchState.activeFilters)) { setMessageState(prev => ({ ...prev, messageList: [], fetchState: States.loaded, hasMoreResults: false })); return; } messageRequestRef.current = buildMessagesRequest(); const messages = await messageRequestRef.current!.fetchPrevious(); if (messages.length > 0) { const reversedList = messages.reverse(); const filteredMessages = reversedList.filter(msg => { const isDeleted = (msg as any).getDeletedAt && (msg as any).getDeletedAt() > 0; const isActionMessage = msg.getCategory() === MessageCategoryConstants.action; return !isDeleted && !isActionMessage; }); const limit = (guid || uid || searchState.activeFilters.length > 0) ? 30 : 3; setMessageState({ messageList: filteredMessages, fetchState: States.loaded, hasMoreResults: messages.length >= limit, }); } else { setMessageState({ messageList: [], fetchState: States.loaded, hasMoreResults: false, }); } } catch (error) { errorHandler(error, "searchMessages"); setMessageState(prev => ({ ...prev, fetchState: States.error })); } }, [searchState.searchText, searchState.activeFilters, buildMessagesRequest, guid, uid, errorHandler]); // ============================================================================ // SEARCH EFFECTS - Trigger searches when search text or filters change // ============================================================================ useEffect(() => { const hasKeywordChanged = searchState.searchText !== lastConversationSearchKeyword.current; const haveFiltersChanged = JSON.stringify(searchState.activeFilters) !== JSON.stringify(lastConversationActiveFilters.current); if (hasKeywordChanged || haveFiltersChanged) { lastConversationSearchKeyword.current = searchState.searchText; lastConversationActiveFilters.current = [...searchState.activeFilters]; searchConversations(); } }, [searchState.searchText, searchState.activeFilters, searchConversations]); useEffect(() => { const hasKeywordChanged = searchState.searchText !== lastMessageSearchKeyword.current; const haveFiltersChanged = JSON.stringify(searchState.activeFilters) !== JSON.stringify(lastMessageActiveFilters.current); if (hasKeywordChanged || haveFiltersChanged) { lastMessageSearchKeyword.current = searchState.searchText; lastMessageActiveFilters.current = [...searchState.activeFilters]; searchMessages(); } }, [searchState.searchText, searchState.activeFilters, searchMessages]); const shouldRenderConversations = useCallback(() => { // If searchIn is empty, search in both conversations and messages const effectiveSearchIn = searchIn.length === 0 ? [CometChatSearchScope.Conversations, CometChatSearchScope.Messages] : searchIn; // Don't render conversations if searchIn doesn't include conversations scope if (!effectiveSearchIn.includes(CometChatSearchScope.Conversations)) { return false; } // Don't render conversations if uid or guid is present (searching within specific user/group) if (uid || guid) { return false; } // Define conversation filters const conversationFilters = [CometChatSearchFilter.Unread, CometChatSearchFilter.Groups]; if (searchState.searchText && searchState.searchText.trim() !== "" && searchState.activeFilters.length === 0) { return true; } if (searchState.activeFilters.length > 0) { return searchState.activeFilters.some(filter => conversationFilters.includes(filter)); } return false; }, [searchState.activeFilters, searchState.searchText, uid, guid, searchIn]); const shouldRenderMessages = useCallback(() => { // If searchIn is empty, search in both conversations and messages const effectiveSearchIn = searchIn.length === 0 ? [CometChatSearchScope.Conversations, CometChatSearchScope.Messages] : searchIn; // Don't render messages if searchIn doesn't include messages scope if (!effectiveSearchIn.includes(CometChatSearchScope.Messages)) { return false; } // Always render messages if uid or guid is present (searching within specific user/group) if (uid || guid) { return true; } // Define message filters const messageFilters = [ CometChatSearchFilter.Photos, CometChatSearchFilter.Videos, CometChatSearchFilter.Documents, CometChatSearchFilter.Audio, CometChatSearchFilter.Links ]; // Define conversation filters const conversationFilters = [CometChatSearchFilter.Unread, CometChatSearchFilter.Groups]; if (searchState.searchText && searchState.searchText.trim() !== "" && searchState.activeFilters.length === 0) { return true; } if (searchState.activeFilters.length > 0) { // Show messages if any message filter is selected and no conversation filter is selected return searchState.activeFilters.some(filter => messageFilters.includes(filter)) && !searchState.activeFilters.some(filter => conversationFilters.includes(filter)); } return false; }, [searchState.activeFilters, searchState.searchText, uid, guid, searchIn]); // Handle scroll-based fetching for messages const handleScrollEnd = useCallback((event: any) => { if (!shouldRenderMessages() || !messageState.hasMoreResults) return; const contentOffsetY = event.nativeEvent.contentOffset.y; const contentHeight = event.nativeEvent.contentSize.height; const layoutHeight = event.nativeEvent.layoutMeasurement.height; if (contentOffsetY + layoutHeight >= contentHeight - 10) { loadMoreMessages(); } }, [shouldRenderMessages, messageState.hasMoreResults, loadMoreMessages]); // Get status indicator for conversation const getStatusIndicator = (conv: CometChat.Conversation) => { const withObj = conv.getConversationWith(); if (withObj instanceof CometChat.Group) { if (withObj.getType() === GroupTypeConstants.password) return "password"; if (withObj.getType() === GroupTypeConstants.private) return "private"; } else if (withObj instanceof CometChat.User) { if (withObj.getStatus() === 'online' && !withObj.getHasBlockedMe() && !withObj.getBlockedByMe()) { return "online"; } return "offline"; } return undefined; }; // Render conversation avatar const renderConversationLeadingView = (conversation: CometChat.Conversation) => { const withObj = conversation.getConversationWith(); const avatarURL = withObj instanceof CometChat.User ? withObj.getAvatar() : withObj.getIcon(); const name = withObj.getName(); return ( ); }; // Render conversation trailing view (date and badge) const renderConversationTrailingView = (conversation: CometChat.Conversation) => { const timestamp = conversation.getLastMessage()?.getSentAt(); if (!timestamp) return null; return ( ); }; // Get file type icon based on file extension const getFileTypeIcon = (fileName: string): 'audio-file-type' | 'image-file-type' | 'video-file-type' | 'pdf-file-type' | 'presentation-file-type' | 'spreadsheet-file-type' | 'text-file-type' | 'zip-file-type' | 'document-file-type' | 'unknown-file-type' => { if (!fileName) return 'unknown-file-type'; const extension = fileName.toLowerCase().split('.').pop() || ''; // Audio files if (['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac'].includes(extension)) { return 'audio-file-type'; } // Image files if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) { return 'image-file-type'; } // Video files if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) { return 'video-file-type'; } // PDF files if (extension === 'pdf') { return 'pdf-file-type'; } // Presentation files if (['ppt', 'pptx', 'key'].includes(extension)) { return 'presentation-file-type'; } // Spreadsheet files if (['xls', 'xlsx', 'csv', 'numbers'].includes(extension)) { return 'spreadsheet-file-type'; } // Text/Document files if (['txt', 'rtf', 'doc', 'docx', 'pages'].includes(extension)) { return 'text-file-type'; } // ZIP/Archive files if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) { return 'zip-file-type'; } // Default document type for other files return 'document-file-type'; }; // Format mentions in raw message text: convert <@all:alias> to @alias // and <@uid:UID> to @Name when message provides mentioned users. // Also strips markdown syntax for clean search result previews. const formatMentionsInText = (rawText: any, message?: CometChat.BaseMessage) => { if (typeof rawText !== 'string') return rawText; // Strip markdown syntax while preserving mention tokens for processing below. let text = stripMarkdown(rawText).replace(/<@all:(.*?)>/g, '@$1'); try { const mentionedUsers: CometChat.User[] = (message && (message).getMentionedUsers && (message).getMentionedUsers()) || []; if (mentionedUsers && mentionedUsers.length > 0) { mentionedUsers.forEach((u: CometChat.User) => { const uid = u.getUid(); const name = u.getName(); if (uid && name) { const uidRegex = new RegExp(`<@uid:${uid}>`, 'g'); text = text.replace(uidRegex, `@${name}`); } }); } } catch (e) { console.log(e) } return text; }; // Render message leading view (for audio/file icons) const renderMessageLeadingView = (message: CometChat.BaseMessage) => { const messageType = message.getType(); switch (messageType) { case 'audio': { return ( ); } case 'file': { const fileMessage = message as CometChat.MediaMessage; const attachment = fileMessage.getAttachment(); const fileName = attachment?.getName?.() || ''; const fileTypeIcon = getFileTypeIcon(fileName); return ( ); } case 'text': { // Check if this is a text message with link metadata (link preview) const textMessage = message as CometChat.TextMessage; const metadata = textMessage.getMetadata(); const linkPreview = metadata && (metadata as any)['@injected'] && (metadata as any)['@injected']['extensions'] && (metadata as any)['@injected']['extensions']['link-preview']; if (linkPreview && linkPreview.links && linkPreview.links.length > 0) { const firstLink = linkPreview.links[0]; const thumbnailUrl = firstLink.image || firstLink.favicon; if (thumbnailUrl) { return ( ); } else { // Fallback to link icon if no thumbnail return ; } } // Show link icon for text messages in Links filter that lack preview metadata if (searchState.activeFilters.includes(CometChatSearchFilter.Links)) { return ; } return null; } default: return null; } }; // Render message trailing view (content preview or date) const renderMessageTrailingView = (message: CometChat.BaseMessage) => { const timestamp = message.getSentAt(); if (!timestamp) return null; const messageType = message.getType(); // Handle different message types switch (messageType) { case 'image': { const imageMessage = message as CometChat.MediaMessage; const attachment = imageMessage.getAttachment(); const imageUrl = attachment && (attachment as any).url; if (imageUrl) { return ( ); } break; } case 'video': { const videoMessage = message as CometChat.MediaMessage; const thumbnailImage = checkThumbnail(videoMessage); if (thumbnailImage.uri) { return ( ); } break; } default: // For text messages and other types, show timestamp return ( ); } // Fallback to timestamp return ( ); }; // Memoized trailing view component const MemoizedTrailingView = React.memo(({ message }: { message: CometChat.BaseMessage }) => renderMessageTrailingView(message), (prev, next) => prev.message.getId() === next.message.getId() ); // Render conversations section const renderConversationsSection = () => { const { conversationList, fetchState } = conversationState; if (fetchState === States.loading && conversationList.length === 0) { return loadingView ? loadingView() : ; } if (fetchState === States.empty || conversationList.length === 0) { return null; // Don't show empty conversations section } if (fetchState === States.error) { if (errorView) { return errorView(); } return ( {t("SEARCH_ERROR_LOADING_CONVERSATIONS") || "Error Loading Conversations"} {t("SEARCH_TRY_AGAIN") || "Please try again later"} ); } return ( {t("CHATS") || "Chats"} item.getConversationId()} scrollEnabled={false} initialNumToRender={10} maxToRenderPerBatch={5} windowSize={5} removeClippedSubviews={true} updateCellsBatchingPeriod={50} renderItem={({ item: conversation }) => { // Use custom conversation item view if provided if (conversationItemView) { return ( onConversationClicked?.(conversation, searchState.searchText)} > {conversationItemView(conversation, searchState.searchText)} ); } // Default conversation item rendering return ( onConversationClicked?.(conversation, searchState.searchText)} > {renderConversationLeadingView(conversation)} {conversation.getConversationWith().getName()} {formatMentionsInText((conversation.getLastMessage())?.getText?.(), conversation.getLastMessage())} {renderConversationTrailingView(conversation)} ); }} ListFooterComponent={ conversationState.hasMoreResults ? ( {isConversationMoreResultsLoading.current ? t("SEARCH_LOADING") || 'Loading...' : t("SEARCH_SEE_MORE") || 'See More'} ) : null } /> ); }; // Helper to format date for headers const getDateHeaderDate = useCallback((timestamp: number) => { return new Date(timestamp * 1000).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); }, []); // Render messages section const renderMessagesSection = () => { const { messageList, fetchState } = messageState; if (fetchState === States.loading && messageList.length === 0) { return loadingView ? loadingView() : ; } if (fetchState === States.empty || messageList.length === 0) { return null; // Don't show empty messages section } if (fetchState === States.error) { if (errorView) { return errorView(); } return ( {t("SEARCH_ERROR_LOADING_MESSAGES") || "Error Loading M essages"} {t("SEARCH_TRY_AGAIN") || "Please try again later"} ); } return ( {t("MESSAGES") || "Messages"} `${item.getId()}-${index}`} initialNumToRender={10} maxToRenderPerBatch={5} windowSize={5} removeClippedSubviews={true} updateCellsBatchingPeriod={50} renderItem={({ item: message, index }) => { const messageType = message.getType(); // Date header logic for media items let dateHeader: React.ReactNode = null; if (['image', 'video'].includes(messageType)) { const sentAt = message.getSentAt(); const currentDateStr = getDateHeaderDate(sentAt); const prevMessage = index > 0 ? messageList[index - 1] : null; const prevDateStr = prevMessage ? getDateHeaderDate(prevMessage.getSentAt()) : null; if (currentDateStr !== prevDateStr) { dateHeader = ( ); } } // Check if message is a text message with link preview const isLinkMessage = messageType === 'text' && (() => { const textMessage = message as CometChat.TextMessage; const metadata = textMessage.getMetadata(); return metadata && (metadata as any)['@injected'] && (metadata as any)['@injected']['extensions'] && (metadata as any)['@injected']['extensions']['link-preview']; })(); // Determine which custom view to use based on message type let customView: (() => React.ReactElement) | undefined; if (isLinkMessage && linkMessageItemView) { customView = () => linkMessageItemView(message, searchState.searchText); } else if (messageType === 'text' && textMessageItemView) { customView = () => textMessageItemView(message, searchState.searchText); } else if (messageType === 'image' && imageMessageItemView) { customView = () => imageMessageItemView(message, searchState.searchText); } else if (messageType === 'video' && videoMessageItemView) { customView = () => videoMessageItemView(message, searchState.searchText); } else if (messageType === 'audio' && audioMessageItemView) { customView = () => audioMessageItemView(message, searchState.searchText); } else if (messageType === 'file' && documentMessageItemView) { customView = () => documentMessageItemView(message, searchState.searchText); } // Use custom view if available if (customView) { return ( {dateHeader} onMessageClicked?.(message, searchState.searchText)} > {customView()} ); } // Default message item rendering return ( {dateHeader} onMessageClicked?.(message, searchState.searchText)} > {renderMessageLeadingView(message)} {message.getSender().getName()} {(() => { const messageType = message.getType(); // For media messages (image, video, file, audio), show file name if (['image', 'video', 'file', 'audio'].includes(messageType)) { const mediaMessage = message as CometChat.MediaMessage; const attachment = mediaMessage.getAttachment?.(); return attachment?.getName?.() || messageType; } // For text messages, show the text content if (messageType === 'text') { const raw = (message as CometChat.TextMessage).getText?.(); const formatted = formatMentionsInText(raw, message); return formatted || messageType || 'Message'; } // For custom messages, try to localize the type const customTypeKey = `CUSTOM_MESSAGE_${messageType.toUpperCase()}`; const localizedType = t(customTypeKey); // If localization exists and is different from key, use it if (localizedType && localizedType !== customTypeKey) { return localizedType; } // Handle specific extension types mapping to existing keys if (messageType === 'extension_poll') return t("CUSTOM_MESSAGE_POLL") || "Poll"; if (messageType === 'extension_sticker') return t("CUSTOM_MESSAGE_STICKER") || "Sticker"; if (messageType === 'extension_whiteboard') return t("CUSTOM_MESSAGE_WHITEBOARD") || "Whiteboard"; if (messageType === 'extension_document') return t("CUSTOM_MESSAGE_DOCUMENT") || "Document"; if (messageType === 'meeting') return t("meeting") || "Meeting"; return messageType || 'Message'; })()} ); }} /> ); }; const renderResults = () => { const conversationsRendered = shouldRenderConversations(); const messagesRendered = shouldRenderMessages(); // Initial empty state - no search text and no filters if ((!searchState.searchText || searchState.searchText.trim() === "") && searchState.activeFilters.length === 0) { if (emptyView) { return emptyView(); } return ( {t("SEARCH_NO_RESULTS_TITLE") || "No Results"} {t("SEARCH_NO_RESULTS_SUBTITLE") || "Start typing to search for messages and conversations"} ); } // Show loading shimmer if we have search/filters active and both sections are loading if ((searchState.searchText.trim() !== "" || searchState.activeFilters.length > 0) && (conversationState.fetchState === States.loading || messageState.fetchState === States.loading) && conversationState.conversationList.length === 0 && messageState.messageList.length === 0) { return loadingView ? loadingView() : ; } // Check if we have search text or filters but no actual results if ((searchState.searchText.trim() !== "" || searchState.activeFilters.length > 0) && conversationState.conversationList.length === 0 && messageState.messageList.length === 0 && conversationState.fetchState !== States.loading && messageState.fetchState !== States.loading && conversationState.fetchState !== States.error && messageState.fetchState !== States.error) { // No results found state if (emptyView) { return emptyView(); } return ( {t("SEARCH_NO_RESULTS_FOUND_TITLE") || "No Results Found"} {t("SEARCH_NO_RESULTS_FOUND_SUBTITLE") || "We couldn't find any matches. Please try a different search keyword."} ); } // Show search results with actual item lists - USING SINGLE FLATLIST FOR VIRTUALIZATION // Create combined data array with section headers type ListItem = | { type: 'conversation-header' } | { type: 'conversation'; data: CometChat.Conversation } | { type: 'conversation-footer' } | { type: 'message-header' } | { type: 'message'; data: CometChat.BaseMessage; index: number } | { type: 'message-footer' }; const combinedData: ListItem[] = []; // Add conversations section if needed if (conversationsRendered && conversationState.conversationList.length > 0) { combinedData.push({ type: 'conversation-header' }); conversationState.conversationList.forEach(conv => { combinedData.push({ type: 'conversation', data: conv }); }); if (conversationState.hasMoreResults) { combinedData.push({ type: 'conversation-footer' }); } } // Add messages section if needed if (messagesRendered && messageState.messageList.length > 0) { combinedData.push({ type: 'message-header' }); messageState.messageList.forEach((msg, index) => { combinedData.push({ type: 'message', data: msg, index }); }); } const keyExtractor = (item: ListItem, index: number) => { if (item.type === 'conversation') { return `conversation-${item.data.getConversationId()}`; } else if (item.type === 'message') { return `message-${item.data.getId()}-${index}`; } return `${item.type}-${index}`; }; const renderCombinedItem = ({ item }: { item: ListItem }) => { switch (item.type) { case 'conversation-header': return {t("CHATS") || "Chats"}; case 'conversation': return ( {})} mergedStyles={mergedStyles} theme={theme} conversationItemView={conversationItemView} /> ); case 'conversation-footer': return ( {isConversationMoreResultsLoading.current ? t("SEARCH_LOADING") || 'Loading...' : t("SEARCH_SEE_MORE") || 'See More'} ); case 'message-header': return {t("MESSAGES") || "Messages"}; case 'message': { const message = item.data; const messageIndex = item.index; const messageType = message.getType(); // Date header logic for media items let dateHeader: React.ReactNode = null; if (['image', 'video'].includes(messageType)) { const sentAt = message.getSentAt(); const currentDateStr = getDateHeaderDate(sentAt); const prevItem = messageIndex > 0 ? messageState.messageList[messageIndex - 1] : null; const prevDateStr = prevItem ? getDateHeaderDate(prevItem.getSentAt()) : null; if (currentDateStr !== prevDateStr) { dateHeader = ( ); } } // Check if message is a text message with link preview const isLinkMessage = messageType === 'text' && (() => { const textMessage = message as CometChat.TextMessage; const metadata = textMessage.getMetadata(); return metadata && (metadata as any)['@injected'] && (metadata as any)['@injected']['extensions'] && (metadata as any)['@injected']['extensions']['link-preview']; })(); // Determine which custom view to use based on message type let customView: (() => React.ReactElement) | undefined; if (isLinkMessage && linkMessageItemView) { customView = () => linkMessageItemView(message, searchState.searchText); } else if (messageType === 'text' && textMessageItemView) { customView = () => textMessageItemView(message, searchState.searchText); } else if (messageType === 'image' && imageMessageItemView) { customView = () => imageMessageItemView(message, searchState.searchText); } else if (messageType === 'video' && videoMessageItemView) { customView = () => videoMessageItemView(message, searchState.searchText); } else if (messageType === 'audio' && audioMessageItemView) { customView = () => audioMessageItemView(message, searchState.searchText); } else if (messageType === 'file' && documentMessageItemView) { customView = () => documentMessageItemView(message, searchState.searchText); } // Use custom view if available if (customView) { return ( {dateHeader} onMessageClicked?.(message, searchState.searchText)} > {customView()} ); } // Default message item rendering return ( {dateHeader} onMessageClicked?.(message, searchState.searchText)} > {renderMessageLeadingView(message)} {message.getSender().getName()} {(() => { const msgType = message.getType(); // For media messages (image, video, file, audio), show file name if (['image', 'video', 'file', 'audio'].includes(msgType)) { const mediaMessage = message as CometChat.MediaMessage; const attachment = mediaMessage.getAttachment?.(); return attachment?.getName?.() || msgType; } // For text messages, show the text content if (msgType === 'text') { const raw = (message as CometChat.TextMessage).getText?.(); const formatted = formatMentionsInText(raw, message); return formatted || msgType || 'Message'; } // For custom messages, try to localize the type const customTypeKey = `CUSTOM_MESSAGE_${msgType.toUpperCase()}`; const localizedType = t(customTypeKey); // If localization exists and is different from key, use it if (localizedType && localizedType !== customTypeKey) { return localizedType; } // Handle specific extension types mapping to existing keys if (msgType === 'extension_poll') return t("CUSTOM_MESSAGE_POLL") || "Poll"; if (msgType === 'extension_sticker') return t("CUSTOM_MESSAGE_STICKER") || "Sticker"; if (msgType === 'extension_whiteboard') return t("CUSTOM_MESSAGE_WHITEBOARD") || "Whiteboard"; if (msgType === 'extension_document') return t("CUSTOM_MESSAGE_DOCUMENT") || "Document"; if (msgType === 'meeting') return t("meeting") || "Meeting"; return msgType || 'Message'; })()} ); } default: return null; } }; return ( { // Load more messages if we're at the end and messages section is visible if (messagesRendered && messageState.hasMoreResults) { loadMoreMessages(); } }} onEndReachedThreshold={0.5} /> ); }; return ( {renderHeader()} {renderFilters()} {renderResults()} ); }; export default CometChatSearch;