From 9d87502a41ee3fcdd3dbdc84eac2136d16845a02 Mon Sep 17 00:00:00 2001 From: Vitalik Date: Wed, 13 Mar 2024 19:57:45 +0200 Subject: [PATCH] FEATURE | Calls history --- .env.stage | 10 +- android/app/build.gradle | 2 +- ios/Podfile | 1 - ios/Podfile.lock | 8 +- ios/taskme2.xcodeproj/project.pbxproj | 21 ++- package-lock.json | 15 --- package.json | 1 - src/App.tsx | 5 +- src/api/calls/requests.interfaces.ts | 18 ++- src/api/calls/requests.ts | 17 +++ .../calls/atoms/call-card-info.atom.tsx | 13 +- .../components/call-background.component.tsx | 2 +- .../components/call-row-card.component.tsx | 14 +- .../calls/configs/ice-servers.config.ts | 5 + src/modules/calls/core/accept-call.ts | 6 +- .../calls/core/call-events-listener.ts | 19 ++- src/modules/calls/core/start-call.ts | 1 + src/modules/calls/core/stop-call.ts | 37 ++++- src/modules/calls/enums/call-types.enum.ts | 1 + src/modules/calls/hooks/index.ts | 2 + src/modules/calls/hooks/use-call-data.hook.ts | 127 ++++++++++-------- .../calls/hooks/use-call-streams.hook.ts | 47 +++++++ .../calls/hooks/use-calls-history.hook.ts | 67 +++++++++ .../calls/screens/call/atoms/calling.atom.tsx | 4 +- src/modules/calls/screens/call/index.tsx | 46 +++---- ...call-swipable-row-card.smart-component.tsx | 37 +++-- .../calls-list.smart-component.tsx | 113 ++++++++++------ .../calls/widgets/incoming-call.widget.tsx | 55 ++++---- .../screens/contact-detail.screen.tsx | 5 +- .../contacts/screens/contacts.screen.tsx | 17 +-- src/modules/root/index.tsx | 1 - src/services/system/real-time.service.ts | 1 + src/shared/enums/call-status.enum.ts | 6 + src/shared/enums/index.ts | 29 ++-- src/shared/events/index.ts | 5 +- src/shared/interfaces/call.inteface.ts | 17 +++ src/shared/interfaces/index.ts | 27 ++-- tsconfig.json | 3 +- 38 files changed, 528 insertions(+), 277 deletions(-) create mode 100644 src/modules/calls/hooks/use-call-streams.hook.ts create mode 100644 src/modules/calls/hooks/use-calls-history.hook.ts create mode 100644 src/shared/enums/call-status.enum.ts create mode 100644 src/shared/interfaces/call.inteface.ts diff --git a/.env.stage b/.env.stage index 7c8f12a..427c589 100644 --- a/.env.stage +++ b/.env.stage @@ -1,7 +1,3 @@ -# API_URL=https://taskme-api.work-jetup.site -# SOCKET_URL=https://taskme-api.work-jetup.site -# ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 - -API_URL=http://localhost:3000 -SOCKET_URL=http://localhost:3000 -ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 \ No newline at end of file +API_URL=https://taskme-api.work-jetup.site +SOCKET_URL=https://taskme-api.work-jetup.site +ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 diff --git a/android/app/build.gradle b/android/app/build.gradle index a809356..8a719de 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,7 +25,7 @@ android { applicationId "com.app.task_me" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 221 + versionCode 222 versionName "2.3" resValue "string", "build_config_package", "com.app.task_me" } diff --git a/ios/Podfile b/ios/Podfile index 73a270c..6f1a032 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -31,7 +31,6 @@ target 'taskme2' do pod 'react-native-sqlite-storage', :path => '../node_modules/react-native-sqlite-storage' pod 'react-native-config/Extension', :path => '../node_modules/react-native-config' - pod 'ReactNativeIncallManager', :path => '../node_modules/react-native-incall-manager' # Flags change depending on the env values. flags = get_default_flags() diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c7b2619..dc25a79 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -560,8 +560,6 @@ PODS: - React-perflogger (= 0.72.10) - ReactNativeExceptionHandler (2.10.10): - React-Core - - ReactNativeIncallManager (4.2.0): - - React-Core - rn-fetch-blob (0.12.0): - React-Core - RNAudioRecorderPlayer (3.6.6): @@ -718,7 +716,6 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - ReactNativeExceptionHandler (from `../node_modules/react-native-exception-handler`) - - ReactNativeIncallManager (from `../node_modules/react-native-incall-manager`) - rn-fetch-blob (from `../node_modules/rn-fetch-blob`) - RNAudioRecorderPlayer (from `../node_modules/react-native-audio-recorder-player`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" @@ -883,8 +880,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" ReactNativeExceptionHandler: :path: "../node_modules/react-native-exception-handler" - ReactNativeIncallManager: - :path: "../node_modules/react-native-incall-manager" rn-fetch-blob: :path: "../node_modules/rn-fetch-blob" RNAudioRecorderPlayer: @@ -1005,7 +1000,6 @@ SPEC CHECKSUMS: React-utils: 372b83030a74347331636909278bf0a60ec30d59 ReactCommon: 38824bfffaf4c51fbe03a2730b4fd874ef34d67b ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60 - ReactNativeIncallManager: bfc9c67358cd524882a7c4116dcb311ac2293d4b rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba RNAudioRecorderPlayer: f790fc1afb118552ae6285d60adde52ee6b5d9ef RNCAsyncStorage: 7deab901e27d1f989a83e8be6ce91b673772c848 @@ -1034,6 +1028,6 @@ SPEC CHECKSUMS: Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 12feabb02fdabf7d6322d9ef0cd90fefc37bdafa +PODFILE CHECKSUM: f9f0683c1738b3e88b8940a4c7885412b08fb771 COCOAPODS: 1.15.2 diff --git a/ios/taskme2.xcodeproj/project.pbxproj b/ios/taskme2.xcodeproj/project.pbxproj index c4cf1ed..b8f6fdc 100644 --- a/ios/taskme2.xcodeproj/project.pbxproj +++ b/ios/taskme2.xcodeproj/project.pbxproj @@ -710,7 +710,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -726,7 +729,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = taskme2/taskme2Stage.Release.entitlements; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = HQ3J3TDPR2; INFOPLIST_FILE = taskme2/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Task me ;)"; @@ -905,7 +908,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = taskme2/taskme2.entitlements; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = HQ3J3TDPR2; ENABLE_BITCODE = NO; INFOPLIST_FILE = taskme2/Info.plist; @@ -942,7 +945,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = taskme2/taskme2.entitlements; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = HQ3J3TDPR2; INFOPLIST_FILE = taskme2/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Task me ;)"; @@ -1037,7 +1040,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; @@ -1107,7 +1113,10 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/package-lock.json b/package-lock.json index 8b36e16..7b99a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,6 @@ "react-native-html-to-pdf": "^0.12.0", "react-native-image-crop-picker": "^0.40.0", "react-native-image-picker": "^5.6.0", - "react-native-incall-manager": "^4.2.0", "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-masked-text": "^1.13.0", "react-native-modal": "^13.0.1", @@ -12350,14 +12349,6 @@ "react-native": "*" } }, - "node_modules/react-native-incall-manager": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-native-incall-manager/-/react-native-incall-manager-4.2.0.tgz", - "integrity": "sha512-DC5XRQVAwNgNA6YZ3ILF6ttWXv/MUQ4omzmVDh/uHc0TW0v4f8QIdt6D9GHZhGKb3+qB7XKUxpXVBrLH+9zqfQ==", - "peerDependencies": { - "react-native": ">=0.40.0" - } - }, "node_modules/react-native-iphone-x-helper": { "version": "1.3.1", "license": "MIT", @@ -22184,12 +22175,6 @@ "version": "5.7.0", "requires": {} }, - "react-native-incall-manager": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-native-incall-manager/-/react-native-incall-manager-4.2.0.tgz", - "integrity": "sha512-DC5XRQVAwNgNA6YZ3ILF6ttWXv/MUQ4omzmVDh/uHc0TW0v4f8QIdt6D9GHZhGKb3+qB7XKUxpXVBrLH+9zqfQ==", - "requires": {} - }, "react-native-iphone-x-helper": { "version": "1.3.1", "requires": {} diff --git a/package.json b/package.json index 9ddc849..5d6d0cc 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "react-native-html-to-pdf": "^0.12.0", "react-native-image-crop-picker": "^0.40.0", "react-native-image-picker": "^5.6.0", - "react-native-incall-manager": "^4.2.0", "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-masked-text": "^1.13.0", "react-native-modal": "^13.0.1", diff --git a/src/App.tsx b/src/App.tsx index b5b6945..767e970 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,10 @@ import React, { FC, useEffect } from 'react' import { Provider } from 'react-redux' import { Navigation } from './modules/root' import store from './store' -import './services/system/reactron.service' +if (__DEV__) { + require('./services/system/reactron.service') +} + import { ThemeProvider } from './shared/themes' // import Orientation from 'react-native-orientation-locker' import { AppState, LogBox } from 'react-native' diff --git a/src/api/calls/requests.interfaces.ts b/src/api/calls/requests.interfaces.ts index dfddffe..e8e88bc 100644 --- a/src/api/calls/requests.interfaces.ts +++ b/src/api/calls/requests.interfaces.ts @@ -1,3 +1,5 @@ +import { ICall } from '@/shared' + export interface IStartCallPayload { targetUserId: number rtcMessage: any @@ -10,5 +12,19 @@ export interface IAnswerCallPayload { export interface IIceCandidatePayload { targetUserId: number - rtcMessage: any + + candidates: any[] +} + +export interface ICancelCallPayload { + callId: number +} + +export interface IFinishCallPayload { + callId: number +} + +export interface ICallsListResponse { + items: ICall[] + count: number } diff --git a/src/api/calls/requests.ts b/src/api/calls/requests.ts index b6e90b4..16a8cc7 100644 --- a/src/api/calls/requests.ts +++ b/src/api/calls/requests.ts @@ -1,6 +1,8 @@ import api from '../http.service' import { IAnswerCallPayload, + ICancelCallPayload, + IFinishCallPayload, IIceCandidatePayload, IStartCallPayload, } from './requests.interfaces' @@ -16,3 +18,18 @@ export const answerCallReq = (payload: IAnswerCallPayload) => { export const iceCandidateReq = (payload: IIceCandidatePayload) => { return api.post('calls/iceCandidate', payload, {}, '') } + +export const cancelCallReq = (payload: ICancelCallPayload) => { + return api.post('calls/cancel', payload, {}, '') +} +export const finishCallReq = (payload: IFinishCallPayload) => { + return api.post('calls/finish', payload, {}, '') +} + +export const getCallsListReq = (params: any) => { + return api.get('calls', params, '') +} + +export const deleteCallHistoryReq = (callId: number) => { + return api.delete(`calls/history/${callId}`, null, '') +} diff --git a/src/modules/calls/atoms/call-card-info.atom.tsx b/src/modules/calls/atoms/call-card-info.atom.tsx index 11bffe7..86e3ab5 100644 --- a/src/modules/calls/atoms/call-card-info.atom.tsx +++ b/src/modules/calls/atoms/call-card-info.atom.tsx @@ -7,7 +7,7 @@ import { CallTypesEnum } from '../enums' interface IProps { callerFullName: string - dateTime: Date + dateTime: string callStatus: CallTypesEnum isMissed: boolean } @@ -23,16 +23,9 @@ export const CallCardInfo: FC = ({ const callStatuses = { incoming: 'Вихідний', outgoing: 'Вхідний', + skipped: 'Пропущений', } - const date = dateTime - .toISOString() - .toString() - .substr(0, 10) - .replace(/\-/g, '.') - - const time = dateTime.toISOString().toString().substr(11, 5) - return ( = ({ {`${callStatuses[callStatus]} ${date} ${time}`} + }>{`${callStatuses[callStatus]} ${dateTime}`} left: 0, }, content: { - paddingBottom: 100, + paddingBottom: 60, justifyContent: 'flex-start', alignItems: 'center', backgroundColor: 'rgba(0,0,0,.3)', diff --git a/src/modules/calls/components/call-row-card.component.tsx b/src/modules/calls/components/call-row-card.component.tsx index 85aa7ec..77cd1f9 100644 --- a/src/modules/calls/components/call-row-card.component.tsx +++ b/src/modules/calls/components/call-row-card.component.tsx @@ -7,18 +7,16 @@ import { CallCardInfo } from '../atoms' import { CallTypesEnum } from '../enums' interface IProps { - id: string callerFullName: string imageUrl?: string callStatus: CallTypesEnum - dateTime: Date + dateTime: string isMissed: boolean - onPressCard: (id: string) => void - onPressCall: (id: string) => void + onPressCard: () => void + onPressCall: () => void } export const CallRowCard: FC = ({ - id, imageUrl, callerFullName, callStatus, @@ -32,7 +30,8 @@ export const CallRowCard: FC = ({ return ( onPressCard(id)}> + activeOpacity={1} + onPress={() => onPressCard()}> = ({ onPressCall(id)}> + onPress={() => onPressCall()}> borderBottomWidth: 1, borderBottomColor: theme.$border, justifyContent: 'space-between', + backgroundColor: theme.$layoutBg, }, mainContent: { flexDirection: 'row', diff --git a/src/modules/calls/configs/ice-servers.config.ts b/src/modules/calls/configs/ice-servers.config.ts index cd5f5ea..80ef27a 100644 --- a/src/modules/calls/configs/ice-servers.config.ts +++ b/src/modules/calls/configs/ice-servers.config.ts @@ -8,4 +8,9 @@ export const iceServers = [ { urls: 'stun:stun2.l.google.com:19302', }, + { + urls: 'turn:relay1.expressturn.com:3478', + username: 'efFBS0EV3YVRQY4HFQ', + credential: 'ssU85viEgPpENkG7', + }, ] diff --git a/src/modules/calls/core/accept-call.ts b/src/modules/calls/core/accept-call.ts index e16aef9..e2dff2d 100644 --- a/src/modules/calls/core/accept-call.ts +++ b/src/modules/calls/core/accept-call.ts @@ -1,4 +1,3 @@ -import inCallManager from 'react-native-incall-manager' import { CallDataStore, CallMod } from '../hooks' import { answerCallReq } from '@/api/calls/requests' import { RTCSessionDescription } from 'react-native-webrtc' @@ -20,10 +19,9 @@ export class AcceptCall { this.setRemoteDescription() await this.sendAnswer() - this.proccessIceCandidates() - inCallManager.start({ media: 'video' }) + // inCallManager.start({ media: 'video' }) } catch (e) { console.log(e) } @@ -31,7 +29,7 @@ export class AcceptCall { private prepare() { this.callDataStore.changeMod(CallMod.Speaking) - inCallManager.stopRingtone() + // inCallManager.stopRingtone() } private setRemoteDescription() { diff --git a/src/modules/calls/core/call-events-listener.ts b/src/modules/calls/core/call-events-listener.ts index a693bed..d9e8964 100644 --- a/src/modules/calls/core/call-events-listener.ts +++ b/src/modules/calls/core/call-events-listener.ts @@ -1,7 +1,7 @@ import { socketEvents } from '@/shared' import { CallMod, callDataStoreHelper, useCallDataStore } from '../hooks' import { RTCSessionDescription } from 'react-native-webrtc' -import inCallManager from 'react-native-incall-manager' +// import inCallManager from 'react-native-incall-manager' class CallEventsListenter { constructor() { @@ -21,9 +21,24 @@ class CallEventsListenter { new RTCSessionDescription(data.rtcMessage), ) useCallDataStore.getState().changeMod(CallMod.Speaking) - inCallManager.stopRingback() + + this.proccessIceCandidates() + // inCallManager.stopRingback() }) } + + private proccessIceCandidates() { + const existIcecandidates = useCallDataStore.getState().icecandidates + + if (existIcecandidates.length) { + existIcecandidates.map(candidate => + useCallDataStore + .getState() + .peerConnection.addIceCandidate(candidate), + ) + useCallDataStore.getState().cleanIcecandidates() + } + } } export const callEventsListener = new CallEventsListenter() diff --git a/src/modules/calls/core/start-call.ts b/src/modules/calls/core/start-call.ts index 1c242e7..7d67ac3 100644 --- a/src/modules/calls/core/start-call.ts +++ b/src/modules/calls/core/start-call.ts @@ -50,5 +50,6 @@ export class StartCall { title: 'Невідомий користувач', } this.getCallFromStore().put(from.title, from.avatarImageUrl) + this.callDataStore.setCallId(data.call.id) } } diff --git a/src/modules/calls/core/stop-call.ts b/src/modules/calls/core/stop-call.ts index 366c6f4..63a2e3e 100644 --- a/src/modules/calls/core/stop-call.ts +++ b/src/modules/calls/core/stop-call.ts @@ -1,8 +1,12 @@ -import inCallManager from 'react-native-incall-manager' -import { CallDataStore } from '../hooks' +import { finishCallReq } from '@/api/calls/requests' +import { CallDataStore, ICallStreamsStore } from '../hooks' +import { MediaStream } from 'react-native-webrtc' export class StopCall { - constructor(private readonly getCallDataStore: () => CallDataStore) {} + constructor( + private readonly getCallDataStore: () => CallDataStore, + private readonly getCallStreamsStore: () => ICallStreamsStore, + ) {} private get peerConnection() { return this.getCallDataStore().peerConnection @@ -12,8 +16,31 @@ export class StopCall { return this.getCallDataStore() } - public stop() { + public async stop() { + await finishCallReq({ + callId: this.callDataStore.callId, + }).catch(console.log) + // inCallManager.stop() this.peerConnection.close() - inCallManager.stop() + this.stopStreams() + } + + private stopStreams() { + const streamsStore = this.getCallStreamsStore() + + this.stopStream(streamsStore.localStream) + this.stopStream(streamsStore.remoteStream) + + streamsStore.setLocalStream(null) + streamsStore.setRemoteStream(null) + } + + private stopStream(stream: MediaStream) { + try { + if (!stream) { + return + } + stream.getTracks().forEach(track => track.stop()) + } catch (e) {} } } diff --git a/src/modules/calls/enums/call-types.enum.ts b/src/modules/calls/enums/call-types.enum.ts index a800e92..8b98dc4 100644 --- a/src/modules/calls/enums/call-types.enum.ts +++ b/src/modules/calls/enums/call-types.enum.ts @@ -1,4 +1,5 @@ export enum CallTypesEnum { INCOMING = 'incoming', OUTGOING = 'outgoing', + SKIP = 'skipped', } diff --git a/src/modules/calls/hooks/index.ts b/src/modules/calls/hooks/index.ts index 1f15b31..58b984b 100644 --- a/src/modules/calls/hooks/index.ts +++ b/src/modules/calls/hooks/index.ts @@ -1,2 +1,4 @@ export * from './use-call-data.hook' export * from './use-call-from.hook' +export * from './use-call-streams.hook' +export * from './use-calls-history.hook' diff --git a/src/modules/calls/hooks/use-call-data.hook.ts b/src/modules/calls/hooks/use-call-data.hook.ts index d8e35cf..5b22f06 100644 --- a/src/modules/calls/hooks/use-call-data.hook.ts +++ b/src/modules/calls/hooks/use-call-data.hook.ts @@ -1,13 +1,19 @@ import { MediaStream, RTCPeerConnection } from 'react-native-webrtc' import { create } from 'zustand' import { iceServers } from '../configs' -import { iceCandidateReq } from '@/api/calls/requests' +import { + cancelCallReq, + finishCallReq, + iceCandidateReq, +} from '@/api/calls/requests' import { AcceptCall, StartCall } from '../core' import { useCallFromStore } from './use-call-from.hook' import { NavigationService } from '@/services/system' import { RouteKey } from '@/shared' -import InCallManager from 'react-native-incall-manager' +// import InCallManager from 'react-native-incall-manager' import { StopCall } from '../core/stop-call' +import { callsStreamsCtr, initCallsMediaDevices } from './use-call-streams.hook' +import { Alert } from 'react-native' export enum CallMod { Outgoing, @@ -22,21 +28,21 @@ export interface CallDataStore { targetUserId?: number callId?: number remoteRTCMessage?: any - remoteStream?: any icecandidates: any[] connectedStatus: RTCIceConnectionState changeMod: (mod: CallMod) => void - startCall: (targetUserId: number) => void + startCall: (targetUserId: number, peerConnection: RTCPeerConnection) => void incomeCall: ( targetUserId: number, remoteRTCMessage: any, callId: number, + peerConnection: RTCPeerConnection, ) => void - setRemoteStream: (stream: any) => void addIcecanidate: (item: any) => void cleanIcecandidates: () => void setConnectedStatus: (connectedStatus: RTCIceConnectionState) => void + setCallId: (callId: number) => void } export const useCallDataStore = create()(set => ({ @@ -53,26 +59,28 @@ export const useCallDataStore = create()(set => ({ set({ mod }) }, - startCall(targetUserId) { + setCallId(callId: number) { + set({ callId }) + }, + + startCall(targetUserId, peerConnection) { set({ targetUserId, - peerConnection: createPeerConnection(), + peerConnection, mod: CallMod.Outgoing, }) }, - incomeCall(targetUserId, remoteRTCMessage, callId) { + incomeCall(targetUserId, remoteRTCMessage, callId, peerConnection) { set({ targetUserId, callId, remoteRTCMessage, mod: CallMod.Incoming, - peerConnection: createPeerConnection(), + peerConnection, }) }, - setRemoteStream(stream) { - set({ remoteStream: stream }) - }, + addIcecanidate(item) { set(prev => ({ icecandidates: [...prev.icecandidates, item] })) }, @@ -87,90 +95,101 @@ export const useCallDataStore = create()(set => ({ export const callDataStoreHelper = { peerConnection: () => useCallDataStore.getState().peerConnection, + proccesIncome: async (data: any) => { + const peerConnection = await createPeerConnection() + + useCallDataStore + .getState() + .incomeCall( + data.callerId, + data.rtcMessage, + data.callId, + peerConnection, + ) + + peerConnection.setRemoteDescription( + new RTCSessionDescription(data.rtcMessage), + ) + }, processAccept: async () => { new AcceptCall(useCallDataStore.getState).accept() }, processStart: async (targetUserId: number) => { - InCallManager.start({ media: 'audio', ringback: '_BUNDLE_' }) - useCallDataStore.getState().startCall(targetUserId) - NavigationService.navigate(RouteKey.Call, {}) - - setTimeout(() => { + // InCallManager.start({ media: 'audio', ringback: '_BUNDLE_' }) + try { + const peerConnection = await createPeerConnection() + useCallDataStore.getState().startCall(targetUserId, peerConnection) + NavigationService.navigate(RouteKey.Call, {}) new StartCall( useCallDataStore.getState, useCallFromStore.getState, ).start() - }, 999) + } catch (e) { + console.log(e) + } }, stop: () => { - new StopCall(useCallDataStore.getState).stop() + new StopCall(useCallDataStore.getState, callsStreamsCtr).stop() }, - finishCall: () => { - const state = useCallDataStore.getState() - if ( - state.peerConnection.iceConnectionState === 'disconnected' || - state.peerConnection.iceConnectionState === 'failed' - ) { - new StopCall(useCallDataStore.getState).stop() - } + finishCall: async () => { + useCallDataStore.setState(useCallDataStore.getInitialState()) + NavigationService.goBack() + }, + cancel: async () => { + await cancelCallReq({ + callId: useCallDataStore.getState().callId, + }).catch(console.log) + new StopCall(useCallDataStore.getState, callsStreamsCtr).stop() useCallDataStore.setState(useCallDataStore.getInitialState()) NavigationService.goBack() }, } -function createPeerConnection() { +async function createPeerConnection() { const peerConnection = new RTCPeerConnection({ iceServers: iceServers, }) + await initCallsMediaDevices(peerConnection) peerConnection.addEventListener('track', event => { try { - const existremoteStream = useCallDataStore.getState().remoteStream + const existremoteStream = callsStreamsCtr().remoteStream const remoteMediaStream = existremoteStream || new MediaStream() - remoteMediaStream.addTrack(event.track, remoteMediaStream) - useCallDataStore.getState().setRemoteStream(remoteMediaStream) + remoteMediaStream.addTrack(event.track) + callsStreamsCtr().setRemoteStream(remoteMediaStream) } catch (e) { console.log(e) } }) peerConnection.addEventListener('icecandidate', event => { - if (!event.candidate) { - return + if (event.candidate !== null) { + iceCandidateReq({ + targetUserId: useCallDataStore.getState().targetUserId, + candidates: [event.candidate], + }) } - - iceCandidateReq({ - targetUserId: useCallDataStore.getState().targetUserId, - rtcMessage: { - label: event.candidate.sdpMLineIndex, - id: event.candidate.sdpMid, - candidate: event.candidate.candidate, - }, - }) }) peerConnection.addEventListener('iceconnectionstatechange', event => { - console.log( - 'iceconnectionstatechange', - peerConnection.iceConnectionState, - ) + console.log(peerConnection.iceConnectionState) useCallDataStore .getState() .setConnectedStatus(peerConnection.iceConnectionState) + if (peerConnection.iceConnectionState === 'closed') { + useCallDataStore.getState().changeMod(CallMod.Finished) + } + if ( - peerConnection.iceConnectionState === 'closed' || - peerConnection.iceConnectionState === 'disconnected' + peerConnection.iceConnectionState === 'disconnected' || + peerConnection.iceConnectionState === 'failed' ) { - useCallDataStore.getState().changeMod(CallMod.Finished) + new StopCall(useCallDataStore.getState, callsStreamsCtr).stop() } }) - peerConnection.addEventListener('icecandidateerror', event => { - // You can ignore some candidate errors. - // Connections can still be made even when errors occur. - console.log(event) - }) + peerConnection.addEventListener('onicegatheringstatechange', event => {}) return peerConnection } diff --git a/src/modules/calls/hooks/use-call-streams.hook.ts b/src/modules/calls/hooks/use-call-streams.hook.ts new file mode 100644 index 0000000..d40a784 --- /dev/null +++ b/src/modules/calls/hooks/use-call-streams.hook.ts @@ -0,0 +1,47 @@ +import { MediaStream, mediaDevices } from 'react-native-webrtc' +import { create } from 'zustand' +import { callDataStoreHelper } from './use-call-data.hook' + +export interface ICallStreamsStore { + localStream: MediaStream + remoteStream: MediaStream + + setLocalStream: (localStream: MediaStream) => void + setRemoteStream: (remoteStream: MediaStream) => void +} + +const callsStreamStore = create()(set => ({ + localStream: null, + remoteStream: null, + + setLocalStream(localStream) { + set({ localStream }) + }, + + setRemoteStream(remoteStream) { + set({ remoteStream }) + }, +})) + +export const useCallsStream = callsStreamStore + +export const callsStreamsCtr = () => callsStreamStore.getState() + +export const initCallsMediaDevices = async (peerConnection: any) => { + try { + const stream = await mediaDevices.getUserMedia({ + audio: true, + video: { + frameRate: 30, + facingMode: 'user', + }, + }) + + callsStreamsCtr().setLocalStream(stream) + stream.getTracks().forEach(track => { + peerConnection.addTrack(track as any, stream as any) + }) + } catch (e) { + console.log('eerror') + } +} diff --git a/src/modules/calls/hooks/use-calls-history.hook.ts b/src/modules/calls/hooks/use-calls-history.hook.ts new file mode 100644 index 0000000..275e4a1 --- /dev/null +++ b/src/modules/calls/hooks/use-calls-history.hook.ts @@ -0,0 +1,67 @@ +import { getCallsListReq } from '@/api/calls/requests' +import { CallStatus, ICall, IUser, useFlatList } from '@/shared' +import store from '@/store' +import { selectId } from '@/store/account' +import moment from 'moment' +import { CallTypesEnum } from '../enums' + +export interface ICallListItem { + callId: number + date: string + title: string + avatarUrl?: string + type: CallTypesEnum + isMissed: boolean + targetUserId: number +} + +export const useCallHistory = () => { + const list = useFlatList({ + fetchItems: getCallsListReq, + needInit: false, + limit: 10, + serrializatorItems: transformItems, + clearWhenReload: false, + }) + + function transformItems(items: ICall[]) { + const result: ICallListItem[] = [] + const userId = selectId(store.getState()) + + items.map((it, i) => { + result[i] = transformItem(it, userId) + }) + + return result + } + + function transformItem(it: ICall, userId: number): ICallListItem { + const targetUser = it.users.find(it => it.id !== userId) + const info = targetUser.info + return { + callId: it.id, + title: `${info.firstName} ${info.lastName}`, + date: moment(new Date(it.startAt)).format('DD.MM.YY'), + avatarUrl: info.avatarUrl, + isMissed: it.status !== CallStatus.Finished, + targetUserId: targetUser.id, + type: getType(it, userId), + } + } + + function getType(it: ICall, userId: number) { + if (it.status !== CallStatus.Finished) { + if (userId !== it.initiatorUserId) { + return CallTypesEnum.SKIP + } + } + + return userId === it.initiatorUserId + ? CallTypesEnum.INCOMING + : CallTypesEnum.OUTGOING + } + + return { + list, + } +} diff --git a/src/modules/calls/screens/call/atoms/calling.atom.tsx b/src/modules/calls/screens/call/atoms/calling.atom.tsx index 957a7e6..a59210a 100644 --- a/src/modules/calls/screens/call/atoms/calling.atom.tsx +++ b/src/modules/calls/screens/call/atoms/calling.atom.tsx @@ -14,7 +14,7 @@ export const CallingAtom: FC = ({ localStream, remoteStream }) => { flex: 1, backgroundColor: '#000', }}> - {/* {localStream ? ( + {localStream ? ( = ({ localStream, remoteStream }) => { }} streamURL={localStream.toURL()} /> - ) : null} */} + ) : null} {remoteStream ? ( { const mod = useCallDataStore(s => s.mod) - const remoteStream = useCallDataStore(s => s.remoteStream) - const [localStream, setlocalStream] = useState(null) + const { remoteStream, localStream } = useCallsStream() const { title, avatarImageUrl } = useCallFromStore() const connectedStatus = useCallDataStore(s => s.connectedStatus) - const initMediaDevices = async () => { - const stream = await mediaDevices.getUserMedia({ - audio: true, - video: { - frameRate: 30, - facingMode: 'user', - }, - }) - setlocalStream(stream) - stream.getTracks().forEach(track => { - callDataStoreHelper - .peerConnection() - .addTrack(track as any, stream as any) - }) - } - - useEffect(() => { - initMediaDevices() - }, []) - - const cancelCall = () => { + const stopCall = () => { callDataStoreHelper.stop() } @@ -62,7 +47,7 @@ export const CallScreen = () => { iconName="phone-2" bgColor="#DE253B" isAnimated={false} - onPress={() => callDataStoreHelper.stop()} + onPress={stopCall} /> @@ -70,7 +55,7 @@ export const CallScreen = () => { [CallMod.Outgoing]: ( ), @@ -103,6 +88,9 @@ export const CallScreen = () => { height: Dimensions.get('screen').height, }}> {templates[mod]} + {/* + {mod} {connectedStatus} + */}