import TUICore, { TUIConstants } from '@tencentcloud/tui-core-lite'; import { i18next } from '@tencentcloud/uikit-base-component-react'; import { ContactStoreRaw as ContactStore, LoginStoreRaw as LoginStore, reportStoreUsageData, StoreName, } from 'tuikit-atomicx-react/chat'; import { Dialog } from '../baseComp/Dialog'; import { useOfflinePushInfo } from '../hooks/useOfflinePushInfo'; import { safeJSONParse } from './json'; import type { StartCallParams, CallMessagePayload } from '../types/call'; import type { CustomMessageInfo, MessageInfo } from 'tuikit-atomicx-react/chat'; function isCallMessage(message: MessageInfo) { try { const payloadData = safeJSONParse((message as CustomMessageInfo).messagePayload?.customData, {} as any); if (payloadData.businessID === 1 && payloadData.data) { const payloadDataData = safeJSONParse(payloadData.data, {} as any); if (payloadDataData.businessID === 'av_call' || payloadDataData.businessID === 'rtc_call') { return true; } } } catch { return false; } return false; } function isCreateGroupMessage(message: MessageInfo) { const payloadData = safeJSONParse((message as CustomMessageInfo).messagePayload?.customData, {} as any); return payloadData?.businessID === 'group_create'; } function startCall(params: StartCallParams) { reportStoreUsageData({ storeName: StoreName.ChatEvokeCall }); const result = TUICore.getService(TUIConstants.TUICalling.SERVICE.NAME); if (!result) { console.error('[Call] CallKit not integrated'); Dialog.open({ title: i18next.t('CallKit.tip'), confirmText: i18next.t('CallKit.got_it'), cancelText: '', content: (
{i18next.t('CallKit.callkit_not_integrated_content')}
{i18next.t('CallKit.step_integrate_callkit')}
{i18next.t('CallKit.view_integration_doc')}
{i18next.t('CallKit.step_claim_free_trial')}
{i18next.t('CallKit.go_console_claim')}
), }); return; } // Inject offlinePushInfo for CALL scene if configured // useOfflinePushInfo is a hook-named factory without real React hooks inside, // so it is safe to call in this plain utility function. // eslint-disable-next-line react-hooks/rules-of-hooks const { createCallOfflinePushInfo } = useOfflinePushInfo(); const callOfflinePushInfo = createCallOfflinePushInfo(); TUICore.callService({ serviceName: TUIConstants.TUICalling.SERVICE.NAME, method: TUIConstants.TUICalling.SERVICE.METHOD.START_CALL, params: { ...params, version: 'v3', // Merge offlinePushInfo: caller's value takes precedence ...(callOfflinePushInfo && !params.offlinePushInfo ? { offlinePushInfo: callOfflinePushInfo } : {}), }, }); // offlinePushInfo doc: https://cloud.tencent.com/document/product/269/105713 } function parseCallMessage(message: MessageInfo): CallMessagePayload | undefined { const dataContent = safeJSONParse((message as CustomMessageInfo).messagePayload?.customData, undefined); if (!dataContent) { return undefined; } const callInfo = safeJSONParse(dataContent.data, undefined as any); if (!callInfo) { return undefined; } const customPayload = (message as CustomMessageInfo).messagePayload; const result: CallMessagePayload = { description: customPayload?.description ?? '', extension: customPayload?.extensionInfo ?? '', data: { businessID: dataContent.businessID, timeout: dataContent.timeout, data: { businessID: callInfo.businessID, call_end: callInfo.call_end, call_type: callInfo.call_type, data: { cmd: callInfo.data.cmd, inviter: callInfo.data.inviter, message: callInfo.data.message, room_id: callInfo.data.room_id, str_room_id: callInfo.data.str_room_id, }, platform: callInfo.platform, room_id: callInfo.room_id, userData: callInfo.userData, version: callInfo.version, }, inviteID: dataContent.inviteID, groupID: dataContent.groupID, actionType: dataContent.actionType, inviter: dataContent.inviter, inviteeList: dataContent.inviteeList, }, }; return result; } const MAX_REASONABLE_CALL_DURATION_SECONDS = 24 * 60 * 60; const EPOCH_SECONDS_THRESHOLD = 1_000_000_000; const EPOCH_MILLISECONDS_THRESHOLD = 1_000_000_000_000; function normalizeCallDurationSeconds(value: unknown): number { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds <= 0) { return 0; } if (seconds >= EPOCH_MILLISECONDS_THRESHOLD || seconds >= EPOCH_SECONDS_THRESHOLD) { return 0; } if (seconds > MAX_REASONABLE_CALL_DURATION_SECONDS) { return 0; } return seconds; } function formatTime(seconds: number): string { if (!seconds || seconds <= 0) { return '00:00'; } const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } function substringByLength(str: string, maxLength = 12): string { if (!str || str.length <= maxLength) { return str; } return `${str.substring(0, maxLength)}...`; } function getUserRemarkByUserID(userID: string): string { const friend = ContactStore.getState().friendList.find(f => f.userID === userID); return friend?.friendRemark || ''; } /** * userID - showName */ const userShowNameMap = new Map(); /** * userID - 1 */ const requestedUserMap = new Map(); function parseCallMessageText(message: MessageInfo): string { const callMessagePayload = parseCallMessage(message); if (!callMessagePayload || callMessagePayload.data.businessID !== 1) { return ''; } const { data } = callMessagePayload; const objectData = data.data; const userID: string = message.from?.userID ?? ''; const remark = getUserRemarkByUserID(userID); const myUserID = LoginStore.getState().loginUserInfo?.userID ?? ''; let messageSender = remark || message.from?.nameCard || message.from?.nickname || userID; messageSender = substringByLength(messageSender); switch (data.actionType) { case 1: { if (objectData?.data?.cmd === 'audioCall' || objectData?.data?.cmd === 'videoCall') { if (data.groupID) { return i18next.t('CallMessage.start_call', { messageSender }); } } if (objectData?.data?.cmd === 'hangup') { if (data.groupID) { return i18next.t('CallMessage.call_ended'); } return `${i18next.t('CallMessage.call_duration')}: ${formatTime(normalizeCallDurationSeconds(objectData?.call_end))}`; } if (objectData?.data?.cmd === 'switchToAudio') { return i18next.t('CallMessage.switch_to_audio_call'); } if (objectData?.data?.cmd === 'switchToVideo') { return i18next.t('CallMessage.switch_to_video_call'); } // when CDM is abnormal, the default return value is start call return i18next.t('CallMessage.start_call', { messageSender: '' }); } case 2: if (data.groupID) { return `${messageSender} ${i18next.t('CallMessage.cancel_call')}`; } if (data.inviter === myUserID) { return i18next.t('CallMessage.canceled'); } return i18next.t('CallMessage.canceled_by_recipient'); case 3: if (objectData?.data?.cmd === 'switchToAudio') { return i18next.t('CallMessage.switch_to_audio_call'); } if (objectData?.data?.cmd === 'switchToVideo') { return i18next.t('CallMessage.switch_to_video_call'); } if (data.groupID) { return `${messageSender} ${i18next.t('CallMessage.answered')}`; } return i18next.t('CallMessage.answered'); case 4: if (data.groupID) { return `${messageSender} ${i18next.t('CallMessage.decline_call')}`; } if ((objectData as any)?.line_busy === 'line_busy' || objectData?.data?.message === 'lineBusy') { if (data.inviter === myUserID) { return i18next.t('CallMessage.line_busy'); } return i18next.t('CallMessage.missed_due_to_busy'); } if (data.inviter === myUserID) { return i18next.t('CallMessage.declined_by_recipient'); } return i18next.t('CallMessage.declined'); case 5: if (objectData?.data?.cmd === 'switchToAudio') { return i18next.t('CallMessage.switch_to_audio_call'); } if (objectData?.data?.cmd === 'switchToVideo') { return i18next.t('CallMessage.switch_to_video_call'); } if (data.groupID) { // group call initiator timeout if ((userID as any) === data.inviter) { handleCallKitTimeoutSignaling(data.inviteeList); let inviteeList = ''; data.inviteeList?.forEach((inviteeUserID: string) => { const showName = userShowNameMap.get(inviteeUserID) || inviteeUserID; inviteeList += `${substringByLength(showName)}、`; }); inviteeList = inviteeList.substring(0, inviteeList.lastIndexOf('、')); return `${inviteeList} ${i18next.t('CallMessage.no_answer')}`; } // group call recipient timeout return `${messageSender} ${i18next.t('CallMessage.no_answer')}`; } if (data.inviter === myUserID) { return i18next.t('CallMessage.no_answer_from_recipient'); } return i18next.t('CallMessage.no_answer_timeout'); default: return ''; } } function handleCallKitTimeoutSignaling(inviteeList: string[] = []) { if (inviteeList.length === 0) { return; } const userIDList: string[] = []; inviteeList.forEach((userID: string) => { const friend = ContactStore.getState().friendList.find(f => f.userID === userID); const remark = friend?.friendRemark; if (remark) { userShowNameMap.set(userID, remark); } else if (!requestedUserMap.has(userID)) { userIDList.push(userID); requestedUserMap.set(userID, 1); } }); // note: request once for each callKit timeout signaling, do not use rate limiting processing for now if (userIDList.length > 0) { ContactStore.getContactInfo(userIDList).then((contactList) => { contactList.forEach((contact) => { const showName = contact.nickname || contact.userID; userShowNameMap.set(contact.userID, showName); }); }).catch(() => { // capture exceptions when requests fail to avoid blocking code execution process }); } } export { startCall, isCallMessage, isCreateGroupMessage, parseCallMessage, parseCallMessageText, };