import { CometChat } from "@cometchat/chat-sdk-react-native"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { Text, TouchableOpacity, View, StyleSheet } from "react-native"; import { CometChatAvatar, CometChatSoundManager } from "../../shared"; import { CallTypeConstants, MessageCategoryConstants, MessageTypeConstants, } from "../../shared/constants/UIKitConstants"; import { CometChatUIEventHandler } from "../../shared/events/CometChatUIEventHandler/CometChatUIEventHandler"; import { CallUIEvents } from "../CallEvents"; import { CallingPackage } from "../CallingPackage"; import { CometChatOngoingCall } from "../CometChatOngoingCall"; import { Icon } from "../../shared/icons/Icon"; import { useTheme } from "../../theme"; import { IncomingCallStyle } from "./style"; import { deepMerge } from "../../shared/helper/helperFunctions"; import { DeepPartial } from "../../shared/helper/types"; import { JSX } from "react"; import { useCometChatTranslation } from "../../shared/resources/CometChatLocalizeNew"; import { SafeAreaView } from "react-native-safe-area-context"; const listnerID = "CALL_LISTENER_" + new Date().getTime(); const CometChatCalls = CallingPackage.CometChatCalls; /** * Props for the CometChatIncomingCall component. * * @interface CometChatIncomingCallInterface */ export interface CometChatIncomingCallInterface { /** The incoming call object, which can be a Call or a CustomMessage */ call: CometChat.Call | CometChat.CustomMessage | any; /** Custom view for the entire call item */ ItemView?: (call: CometChat.Call | CometChat.CustomMessage) => JSX.Element; /** Custom view for the title section of the call item */ TitleView?: (call: CometChat.Call | CometChat.CustomMessage) => JSX.Element; /** Custom view for the subtitle section of the call item */ SubtitleView?: (call: CometChat.Call | CometChat.CustomMessage) => JSX.Element; /** Custom view for the leading section of the call item */ LeadingView?: (call: CometChat.Call | CometChat.CustomMessage) => JSX.Element; /** Custom view for the trailing section of the call item */ TrailingView?: (call: CometChat.Call | CometChat.CustomMessage) => JSX.Element; /** Flag to disable sound for incoming calls */ disableSoundForCalls?: boolean; /** Path or identifier for a custom sound to play for incoming calls */ customSoundForCalls?: string; /** Callback fired when the call is accepted */ onAccept?: (message: CometChat.BaseMessage) => void; /** Callback fired when the call is declined */ onDecline: (message: CometChat.BaseMessage) => void; /** Callback fired when an error occurs */ onError?: (e: CometChat.CometChatException) => void; /** Optional custom call settings builder */ callSettingsBuilder?: typeof CometChatCalls.CallSettingsBuilder; /** Custom style overrides for the incoming call component */ style?: DeepPartial; } /** * CometChatIncomingCall component. * * This component handles incoming calls by playing a sound, offering accept/decline buttons, * and showing an ongoing call screen if accepted. Custom views for various parts of the call UI * can be provided via props. * * @param {CometChatIncomingCallInterface} props - Component configuration props. * @returns {JSX.Element} The rendered incoming call UI. */ export const CometChatIncomingCall = (props: CometChatIncomingCallInterface): JSX.Element => { const { onAccept, onDecline, customSoundForCalls, disableSoundForCalls, ItemView, TitleView, SubtitleView, LeadingView, TrailingView, call, onError, callSettingsBuilder, style, } = props; const theme = useTheme(); const { t } = useCometChatTranslation(); const [showCallScreen, setShowCallScreen] = useState(false); const acceptedCall = useRef(undefined); /** Reference to the call listener */ const callListener = useRef(undefined); /** Reference to the call settings builder instance */ const callSettings = useRef(undefined); // Merge the default and custom styles for incoming calls. const incomingCallStyle = useMemo(() => { return deepMerge(theme.incomingCallStyle, style ?? {}); }, [theme.incomingCallStyle, style]); /** * Ends the call by rejecting it and emitting a call rejected event. */ const endCall = () => { CometChat.rejectCall(call["sessionId"], CometChat.CALL_STATUS.REJECTED).then( (rejectedCall) => { CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccCallRejected, { call: rejectedCall, }); // Notify parent so it can unmount this component. onDecline && onDecline(rejectedCall); CometChatSoundManager.pause(); }, (err) => { onError && onError(err); } ); }; /** * Accepts the incoming call. * * If a custom onAccept callback is provided, it is used instead of the default behavior. */ const acceptCall = () => { CometChatSoundManager.pause(); if (onAccept) { onAccept(call); return; } CometChat.acceptCall(call["sessionId"]).then( (accepted) => { acceptedCall.current = accepted!; setShowCallScreen(true); CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccCallAccepted, { call: accepted, }); }, (err) => { onError && onError(err); } ); }; /** * Checks if the provided call is a default CometChat call (not a custom meeting call). * * @param {CometChat.BaseMessage} ccCall - The call to check. * @returns {Boolean} True if it's a default call; otherwise, false. */ function isDefaultCall(ccCall: CometChat.BaseMessage): Boolean { return ccCall.getCategory() === MessageCategoryConstants.call; } // Set up listeners and call settings on component mount. useEffect(() => { if (call && !disableSoundForCalls && call.getType() !== MessageTypeConstants.meeting) { // Play a custom or default incoming call ringtone. if (customSoundForCalls) { CometChatSoundManager.play("incomingCall", customSoundForCalls); } else { CometChatSoundManager.play("incomingCall"); } } // Add a call listener for call cancellation. CometChat.addCallListener( listnerID, new CometChat.CallListener({ onIncomingCallCancelled: () => { CometChatSoundManager.pause(); }, }) ); // Create an ongoing call listener for managing call events. callListener.current = new CometChatCalls.OngoingCallListener({ onCallEnded: () => { CometChatCalls.endSession(); CometChat.clearActiveCall(); CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccCallEnded, {}); setShowCallScreen(false); acceptedCall.current = undefined; }, onCallEndButtonPressed: () => { if (isDefaultCall(call)) { CometChat.endCall(call.getSessionId()).then((endedCall) => { CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccCallEnded, { call: endedCall, }); }); } }, onUserJoined: (user: CometChat.User) => { console.log("user joined:", user); }, onUserLeft: (user: CometChat.User) => { if (isDefaultCall(call)) { CometChat.endCall(call.getSessionId()) .then((endedCall2) => { CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccCallEnded, { call: endedCall2, }); }) .catch((err) => { console.log("Error on userLeft:", err); }); } }, onError: (error: CometChat.CometChatException) => { CometChatUIEventHandler.emitCallEvent(CallUIEvents.ccCallFailed, { error }); onError && onError(error); }, }); // Initialize call settings using the provided builder or default values. callSettings.current = callSettingsBuilder?.setCallEventListener(callListener.current) ?? new CometChatCalls.CallSettingsBuilder() .enableDefaultLayout(true) .setCallEventListener(callListener.current) .setIsAudioOnlyCall(call["type"] === "audio"); // Cleanup listeners and pause any sounds on unmount. return () => { CometChatUIEventHandler.removeCallListener(listnerID); CometChat.removeCallListener(listnerID); CometChatSoundManager.pause(); }; }, []); /** * If the call is accepted, render the ongoing call screen. */ if (showCallScreen) { return ( ); } /** * Render a custom ItemView if provided. */ if (ItemView) { return ItemView(call); } /** * Render the default incoming call overlay with header and action buttons. */ return ( {/* Top row: LeadingView, Title/Subtitle, TrailingView */} {LeadingView && LeadingView(call)} {TitleView ? ( TitleView(call) ) : ( {call["sender"]?.["name"] ?? t("INCOMING_CALL")} )} {SubtitleView ? ( SubtitleView(call) ) : ( {call?.["type"] === CallTypeConstants.audio ? t("INCOMING_AUDIO_CALL") : t("INCOMING_VIDEO_CALL")} )} {TrailingView ? ( TrailingView(call) ) : ( )} {/* Buttons row */} {t("DECLINE")} {t("ACCEPT")} ); }; const styles = StyleSheet.create({ overlay: { position: "absolute", top: 0, left: 10, right: 10, bottom: 0, zIndex: 99999, }, topRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, bottomRow: { flexDirection: "row", gap: 10, justifyContent: "space-between", alignItems: "center", }, rowInline: { flexDirection: "row", alignItems: "center", }, });