import { CometChat } from "@cometchat/chat-sdk-react-native"; import React, { JSX } from "react"; import { Modal, View } from "react-native"; import { ChatConfigurator } from "../shared/framework/ChatConfigurator"; import { CometChatUIEventHandler } from "../shared/events/CometChatUIEventHandler/CometChatUIEventHandler"; import { CometChatUIKit } from "../shared/CometChatUiKit/CometChatUIKit"; import { AdditionalAuxiliaryHeaderOptionsParams, AdditionalParams } from "../shared/base/Types"; import { CallContstatnts, MessageCategoryConstants, MessageTypeConstants, } from "../shared/constants/UIKitConstants"; import { DataSource } from "../shared/framework/DataSource"; import { DataSourceDecorator } from "../shared/framework/DataSourceDecorator"; import { CometChatMessageTemplate } from "../shared/modals"; import { getCometChatTranslation, getCurrentLanguage } from "../shared/resources/CometChatLocalizeNew/LocalizationManager"; import { permissionUtil } from "../shared/utils/PermissionUtil"; import { CallUIEvents } from "./CallEvents"; import { CallingConfiguration } from "./CallingConfiguration"; import { CallingPackage } from "./CallingPackage"; import { CallUtils } from "./CallUtils"; import { CometChatMeetCallBubble } from "./CometChatCallBubble"; import { CometChatCallButtons } from "./CometChatCallButtons"; import { CometChatOngoingCall } from "./CometChatOngoingCall"; import { Icon } from "../shared/icons/Icon"; import { CometChatCallActionBubble } from "./CometChatCallBubble/CometChatCallBubble"; import { CometChatTheme } from "../theme/type"; import { LocalizedDateHelper, localizedDateHelperInstance } from "../shared/helper/LocalizedDateHelper"; const CometChatCalls = CallingPackage.CometChatCalls; const t = getCometChatTranslation(); /** * CallingExtensionDecorator extends the DataSourceDecorator to add calling-specific * configurations and templates. * * @class CallingExtensionDecorator * @extends {DataSourceDecorator} */ export class CallingExtensionDecorator extends DataSourceDecorator { /** * Optional calling configuration. */ configuration?: CallingConfiguration; /** * The logged in CometChat user. */ loggedInUser!: CometChat.User; /** * Creates an instance of CallingExtensionDecorator. * * @param {object} props - The properties object. * @param {DataSource} props.dataSource - The original data source. * @param {CallingConfiguration} [props.configuration] - Optional calling configuration. */ constructor(props: { dataSource: DataSource; configuration?: CallingConfiguration }) { super(props.dataSource); CometChat.getLoggedinUser() .then((user: CometChat.User | null) => { this.loggedInUser = user!; }) .catch((err: CometChat.CometChatException) => { console.log("unable to get logged in user."); }); if (props.configuration) { this.configuration = props.configuration; } } /** * Returns a unique identifier for this decorator. * * @returns {string} The identifier. */ getId(): string { return "call"; } /** * Determines whether a message is deleted. * * @param {CometChat.BaseMessage} message - The message to check. * @returns {boolean} True if the message is deleted; otherwise false. */ isDeletedMessage(message: CometChat.BaseMessage): boolean { return message.getDeletedBy() != null; } /** * Retrieves all supported message types including call types. * * @returns An array of message type strings. */ getAllMessageTypes() { let types: string[] = super.getAllMessageTypes(); types.push(CallContstatnts.audioCall); types.push(CallContstatnts.videoCall); types.push(MessageTypeConstants.meeting); return types; } /** * Retrieves all supported message categories including call-related categories. * * @returns An array of message category strings. */ getAllMessageCategories() { let categories = super.getAllMessageCategories(); categories.push(MessageCategoryConstants.call); categories.push(MessageCategoryConstants.custom); return categories; } /** * Renders the call bubble view for one-to-one call messages. * * @param {object} param0 - The props object. * @param {any} param0.message - The call message. * @param {CometChatTheme} param0.theme - The theme. * @returns {JSX.Element|null} The rendered call bubble view. */ UserCallBubbleView = ({ message, theme }: { message: any; theme: CometChatTheme }) => { if (this.isDeletedMessage(message)) return null; const callStatus = CallUtils.getCallStatus(message, this.loggedInUser); return ( } /> ); }; /** * Returns the audio call message template for one-to-one chats. * * @param {CometChatTheme} theme - The theme to use for the template. * @returns {CometChatMessageTemplate} The audio call message template. */ getUserAudioCallTemplate = (theme: CometChatTheme) => { return new CometChatMessageTemplate({ category: MessageCategoryConstants.call, type: MessageTypeConstants.audio, BubbleView: (message) => { return this.UserCallBubbleView({ message, theme, }); }, }); }; /** * Returns the video call message template for one-to-one chats. * * @param {CometChatTheme} theme - The theme to use for the template. * @returns {CometChatMessageTemplate} The video call message template. */ getUserVideoCallTemplates = (theme: CometChatTheme) => { return new CometChatMessageTemplate({ category: MessageCategoryConstants.call, type: MessageTypeConstants.video, BubbleView: (message) => { return this.UserCallBubbleView({ message, theme, }); }, }); }; /** * Renders the group call bubble view. * * @param {object} props - The props for the group call bubble view. * @param {CometChat.CustomMessage} props.message - The call message. * @param {CometChatTheme} props.theme - The theme. * @param {string} props.alignment - Alignment for the bubble. * @returns {JSX.Element} The rendered group call bubble view. */ GroupCallBubbleView = (props: { message: CometChat.CustomMessage; theme: CometChatTheme; alignment: string; }) => { let loggedInUser = CometChatUIKit.loggedInUser; const { message, theme, alignment } = props; if (this.isDeletedMessage(message)) return ChatConfigurator.dataSource.getDeleteMessageBubble(message, theme); const isSentByMe = message.getSender().getUid() === loggedInUser!.getUid(); const _style = isSentByMe ? theme.messageListStyles.outgoingMessageBubbleStyles?.meetCallBubbleStyles : theme.messageListStyles.incomingMessageBubbleStyles?.meetCallBubbleStyles; const sentAt = message?.getSentAt() ? message.getSentAt() * 1000 : Date.now(); const customData = message.getCustomData() as any; const callType = (message.getCustomData() as any).callType; // Extract session ID with cross-platform compatibility const sessionId = customData.sessionId || customData.sessionID; const BubbleIcon = (() => { if (isSentByMe) { if (callType === "audio") { return ; } return ; } else { if (callType === "audio") { return ; } return ; } })(); return ( this.startDirectCall( sessionId, message, theme, callType ) } style={_style ?? {}} subTitleText={ localizedDateHelperInstance.getFormattedDate(sentAt, LocalizedDateHelper.patterns.callBubble, getCurrentLanguage()) } /> ); }; /** * Initiates a direct call by checking permissions and showing the ongoing call screen. * * @param sessionId - The call session ID. * @param message - The call message. * @param [theme] - The theme. * @param [callType] - The type of call (audio/video). * @returns */ async startDirectCall( sessionId: string, message: CometChat.BaseMessage, theme?: CometChatTheme, callType?: string ) { // Validate session ID if (!sessionId || sessionId === 'undefined' || sessionId === 'null') { console.error('[CallingExtensionDecorator] Invalid session ID provided:', sessionId); return; } // Request only the necessary permissions based on call type. try { const resources: ("mic" | "camera")[] = callType === "audio" ? ["mic"] : ["mic", "camera"]; const allowed = await permissionUtil.startResourceBasedTask(resources); if (!allowed) { console.warn( `[CallingExtensionDecorator] Permission check failed for resources: ${resources.join(",")}. Aborting join.` ); return; } } catch (e) { console.error("[CallingExtensionDecorator] Unexpected error while requesting permissions", e); return; } const callSettingsBuilder = ( this.configuration?.groupCallSettingsBuilder ? this.configuration?.groupCallSettingsBuilder( undefined, message.getReceiver() as CometChat.Group, callType === "audio" ) : new CometChatCalls.CallSettingsBuilder().setIsAudioOnlyCall(callType === "audio") ).setCallEventListener( new CometChatCalls.OngoingCallListener({ onCallEndButtonPressed: () => { CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccShowOngoingCall, { child: null, }); }, onError: (error: CometChat.CometChatException) => { console.error("[CallingExtensionDecorator] OngoingCallListener onError while joining", error); CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccShowOngoingCall, { child: null, }); }, }) ); const ongoingCallScreen = ( { console.error("[CallingExtensionDecorator] CometChatOngoingCall onError for session", sessionId, e); CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccShowOngoingCall, { child: null, }); }} /> ); CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccShowOngoingCall, { child: ongoingCallScreen, }); } /** * Renders the call buttons in the auxiliary header app bar. * * @param [user] - The user for one-to-one chats. * @param [group] - The group for group chats. * @param [additionalParams] - Additional parameters. * @returns The rendered call buttons or null if conditions are not met. */ getAuxiliaryHeaderAppbarOptions( user?: CometChat.User, group?: CometChat.Group, additionalAuxiliaryHeaderOptionsParams?: AdditionalAuxiliaryHeaderOptionsParams ) { // For one-to-one chats: if a user exists and is blocked, don't render the call buttons. if (user && !group && user.getBlockedByMe()) { return null; } return ( ); } /** * Returns the group call template to render group call messages. * * @param {CometChatTheme} theme - The theme. * @returns {CometChatMessageTemplate} The group call message template. */ getGroupCallTemplate = (theme: CometChatTheme) => { return new CometChatMessageTemplate({ category: MessageCategoryConstants.custom, type: MessageTypeConstants.meeting, ContentView: (message, alignment) => this.GroupCallBubbleView({ message: message as CometChat.CustomMessage, alignment, theme }), StatusInfoView: (message, alignment) => { if (message.getDeletedAt()) { return null; } return <>; }, // Pass theme explicitly so common options (e.g. Message Privately) validate group context correctly. options: (loggedInUser, messageObject, _theme, group) => { return super.getCommonOptions(loggedInUser, messageObject, _theme, group); }, }); }; /** * Retrieves all message templates including call-related templates. * * @param {CometChatTheme} theme - The theme. * @param {AdditionalParams} [additionalParams] - Additional parameters. * @returns {CometChatMessageTemplate[]} An array of message templates. */ getAllMessageTemplates( theme: CometChatTheme, additionalParams?: AdditionalParams ): CometChatMessageTemplate[] { let templates = super.getAllMessageTemplates(theme, additionalParams); templates.push( this.getUserAudioCallTemplate(theme), this.getUserVideoCallTemplates(theme), this.getGroupCallTemplate(theme) ); return templates; } /** * Retrieves the last conversation message for display in the conversation list. * * @param {CometChat.Conversation} conversation - The conversation object. * @param {CometChatTheme} [theme] - The theme. * @returns {string | JSX.Element} The last conversation message. */ getLastConversationMessage(conversation: CometChat.Conversation, theme?: CometChatTheme): string | JSX.Element { if (conversation.getLastMessage()["category"] != "call") return super.getLastConversationMessage(conversation, theme); let lastMesssageString = ""; if (conversation.getLastMessage()["type"] == "audio") lastMesssageString = t("AUDIO_CALL"); if (conversation.getLastMessage()["type"] == "video") lastMesssageString = t("VIDEO_CALL"); return lastMesssageString; } }