Browse Source

FEATURE | Calls history

master
Vitalik 8 months ago
parent
commit
9d87502a41
  1. 10
      .env.stage
  2. 2
      android/app/build.gradle
  3. 1
      ios/Podfile
  4. 8
      ios/Podfile.lock
  5. 21
      ios/taskme2.xcodeproj/project.pbxproj
  6. 15
      package-lock.json
  7. 1
      package.json
  8. 5
      src/App.tsx
  9. 18
      src/api/calls/requests.interfaces.ts
  10. 17
      src/api/calls/requests.ts
  11. 13
      src/modules/calls/atoms/call-card-info.atom.tsx
  12. 2
      src/modules/calls/components/call-background.component.tsx
  13. 14
      src/modules/calls/components/call-row-card.component.tsx
  14. 5
      src/modules/calls/configs/ice-servers.config.ts
  15. 6
      src/modules/calls/core/accept-call.ts
  16. 19
      src/modules/calls/core/call-events-listener.ts
  17. 1
      src/modules/calls/core/start-call.ts
  18. 37
      src/modules/calls/core/stop-call.ts
  19. 1
      src/modules/calls/enums/call-types.enum.ts
  20. 2
      src/modules/calls/hooks/index.ts
  21. 127
      src/modules/calls/hooks/use-call-data.hook.ts
  22. 47
      src/modules/calls/hooks/use-call-streams.hook.ts
  23. 67
      src/modules/calls/hooks/use-calls-history.hook.ts
  24. 4
      src/modules/calls/screens/call/atoms/calling.atom.tsx
  25. 46
      src/modules/calls/screens/call/index.tsx
  26. 37
      src/modules/calls/smart-components/call-swipable-row-card.smart-component.tsx
  27. 113
      src/modules/calls/smart-components/calls-list.smart-component.tsx
  28. 55
      src/modules/calls/widgets/incoming-call.widget.tsx
  29. 5
      src/modules/contacts/screens/contact-detail.screen.tsx
  30. 17
      src/modules/contacts/screens/contacts.screen.tsx
  31. 1
      src/modules/root/index.tsx
  32. 1
      src/services/system/real-time.service.ts
  33. 6
      src/shared/enums/call-status.enum.ts
  34. 29
      src/shared/enums/index.ts
  35. 5
      src/shared/events/index.ts
  36. 17
      src/shared/interfaces/call.inteface.ts
  37. 27
      src/shared/interfaces/index.ts
  38. 3
      tsconfig.json

10
.env.stage

@ -1,7 +1,3 @@
# API_URL=https://taskme-api.work-jetup.site API_URL=https://taskme-api.work-jetup.site
# SOCKET_URL=https://taskme-api.work-jetup.site SOCKET_URL=https://taskme-api.work-jetup.site
# ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 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

2
android/app/build.gradle

@ -25,7 +25,7 @@ android {
applicationId "com.app.task_me" applicationId "com.app.task_me"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 221 versionCode 222
versionName "2.3" versionName "2.3"
resValue "string", "build_config_package", "com.app.task_me" resValue "string", "build_config_package", "com.app.task_me"
} }

1
ios/Podfile

@ -31,7 +31,6 @@ target 'taskme2' do
pod 'react-native-sqlite-storage', :path => '../node_modules/react-native-sqlite-storage' pod 'react-native-sqlite-storage', :path => '../node_modules/react-native-sqlite-storage'
pod 'react-native-config/Extension', :path => '../node_modules/react-native-config' 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 change depending on the env values.
flags = get_default_flags() flags = get_default_flags()

8
ios/Podfile.lock

@ -560,8 +560,6 @@ PODS:
- React-perflogger (= 0.72.10) - React-perflogger (= 0.72.10)
- ReactNativeExceptionHandler (2.10.10): - ReactNativeExceptionHandler (2.10.10):
- React-Core - React-Core
- ReactNativeIncallManager (4.2.0):
- React-Core
- rn-fetch-blob (0.12.0): - rn-fetch-blob (0.12.0):
- React-Core - React-Core
- RNAudioRecorderPlayer (3.6.6): - RNAudioRecorderPlayer (3.6.6):
@ -718,7 +716,6 @@ DEPENDENCIES:
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactNativeExceptionHandler (from `../node_modules/react-native-exception-handler`) - 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`) - rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
- RNAudioRecorderPlayer (from `../node_modules/react-native-audio-recorder-player`) - RNAudioRecorderPlayer (from `../node_modules/react-native-audio-recorder-player`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
@ -883,8 +880,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon" :path: "../node_modules/react-native/ReactCommon"
ReactNativeExceptionHandler: ReactNativeExceptionHandler:
:path: "../node_modules/react-native-exception-handler" :path: "../node_modules/react-native-exception-handler"
ReactNativeIncallManager:
:path: "../node_modules/react-native-incall-manager"
rn-fetch-blob: rn-fetch-blob:
:path: "../node_modules/rn-fetch-blob" :path: "../node_modules/rn-fetch-blob"
RNAudioRecorderPlayer: RNAudioRecorderPlayer:
@ -1005,7 +1000,6 @@ SPEC CHECKSUMS:
React-utils: 372b83030a74347331636909278bf0a60ec30d59 React-utils: 372b83030a74347331636909278bf0a60ec30d59
ReactCommon: 38824bfffaf4c51fbe03a2730b4fd874ef34d67b ReactCommon: 38824bfffaf4c51fbe03a2730b4fd874ef34d67b
ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60 ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60
ReactNativeIncallManager: bfc9c67358cd524882a7c4116dcb311ac2293d4b
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNAudioRecorderPlayer: f790fc1afb118552ae6285d60adde52ee6b5d9ef RNAudioRecorderPlayer: f790fc1afb118552ae6285d60adde52ee6b5d9ef
RNCAsyncStorage: 7deab901e27d1f989a83e8be6ce91b673772c848 RNCAsyncStorage: 7deab901e27d1f989a83e8be6ce91b673772c848
@ -1034,6 +1028,6 @@ SPEC CHECKSUMS:
Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3 Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: 12feabb02fdabf7d6322d9ef0cd90fefc37bdafa PODFILE CHECKSUM: f9f0683c1738b3e88b8940a4c7885412b08fb771
COCOAPODS: 1.15.2 COCOAPODS: 1.15.2

21
ios/taskme2.xcodeproj/project.pbxproj

@ -710,7 +710,10 @@
"-DFOLLY_MOBILE=1", "-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
); );
OTHER_LDFLAGS = "$(inherited) "; OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
@ -726,7 +729,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = taskme2/taskme2Stage.Release.entitlements; CODE_SIGN_ENTITLEMENTS = taskme2/taskme2Stage.Release.entitlements;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
INFOPLIST_FILE = taskme2/Info.plist; INFOPLIST_FILE = taskme2/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Task me ;)"; INFOPLIST_KEY_CFBundleDisplayName = "Task me ;)";
@ -905,7 +908,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = taskme2/taskme2.entitlements; CODE_SIGN_ENTITLEMENTS = taskme2/taskme2.entitlements;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = taskme2/Info.plist; INFOPLIST_FILE = taskme2/Info.plist;
@ -942,7 +945,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = taskme2/taskme2.entitlements; CODE_SIGN_ENTITLEMENTS = taskme2/taskme2.entitlements;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
INFOPLIST_FILE = taskme2/Info.plist; INFOPLIST_FILE = taskme2/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Task me ;)"; INFOPLIST_KEY_CFBundleDisplayName = "Task me ;)";
@ -1037,7 +1040,10 @@
"-DFOLLY_MOBILE=1", "-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
); );
OTHER_LDFLAGS = "$(inherited) "; OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
}; };
@ -1107,7 +1113,10 @@
"-DFOLLY_MOBILE=1", "-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
); );
OTHER_LDFLAGS = "$(inherited) "; OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;

15
package-lock.json generated

@ -62,7 +62,6 @@
"react-native-html-to-pdf": "^0.12.0", "react-native-html-to-pdf": "^0.12.0",
"react-native-image-crop-picker": "^0.40.0", "react-native-image-crop-picker": "^0.40.0",
"react-native-image-picker": "^5.6.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-keyboard-aware-scroll-view": "^0.9.5",
"react-native-masked-text": "^1.13.0", "react-native-masked-text": "^1.13.0",
"react-native-modal": "^13.0.1", "react-native-modal": "^13.0.1",
@ -12350,14 +12349,6 @@
"react-native": "*" "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": { "node_modules/react-native-iphone-x-helper": {
"version": "1.3.1", "version": "1.3.1",
"license": "MIT", "license": "MIT",
@ -22184,12 +22175,6 @@
"version": "5.7.0", "version": "5.7.0",
"requires": {} "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": { "react-native-iphone-x-helper": {
"version": "1.3.1", "version": "1.3.1",
"requires": {} "requires": {}

1
package.json

@ -72,7 +72,6 @@
"react-native-html-to-pdf": "^0.12.0", "react-native-html-to-pdf": "^0.12.0",
"react-native-image-crop-picker": "^0.40.0", "react-native-image-crop-picker": "^0.40.0",
"react-native-image-picker": "^5.6.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-keyboard-aware-scroll-view": "^0.9.5",
"react-native-masked-text": "^1.13.0", "react-native-masked-text": "^1.13.0",
"react-native-modal": "^13.0.1", "react-native-modal": "^13.0.1",

5
src/App.tsx

@ -2,7 +2,10 @@ import React, { FC, useEffect } from 'react'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { Navigation } from './modules/root' import { Navigation } from './modules/root'
import store from './store' import store from './store'
import './services/system/reactron.service' if (__DEV__) {
require('./services/system/reactron.service')
}
import { ThemeProvider } from './shared/themes' import { ThemeProvider } from './shared/themes'
// import Orientation from 'react-native-orientation-locker' // import Orientation from 'react-native-orientation-locker'
import { AppState, LogBox } from 'react-native' import { AppState, LogBox } from 'react-native'

18
src/api/calls/requests.interfaces.ts

@ -1,3 +1,5 @@
import { ICall } from '@/shared'
export interface IStartCallPayload { export interface IStartCallPayload {
targetUserId: number targetUserId: number
rtcMessage: any rtcMessage: any
@ -10,5 +12,19 @@ export interface IAnswerCallPayload {
export interface IIceCandidatePayload { export interface IIceCandidatePayload {
targetUserId: number targetUserId: number
rtcMessage: any
candidates: any[]
}
export interface ICancelCallPayload {
callId: number
}
export interface IFinishCallPayload {
callId: number
}
export interface ICallsListResponse {
items: ICall[]
count: number
} }

17
src/api/calls/requests.ts

@ -1,6 +1,8 @@
import api from '../http.service' import api from '../http.service'
import { import {
IAnswerCallPayload, IAnswerCallPayload,
ICancelCallPayload,
IFinishCallPayload,
IIceCandidatePayload, IIceCandidatePayload,
IStartCallPayload, IStartCallPayload,
} from './requests.interfaces' } from './requests.interfaces'
@ -16,3 +18,18 @@ export const answerCallReq = (payload: IAnswerCallPayload) => {
export const iceCandidateReq = (payload: IIceCandidatePayload) => { export const iceCandidateReq = (payload: IIceCandidatePayload) => {
return api.post('calls/iceCandidate', payload, {}, '') 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, '')
}

13
src/modules/calls/atoms/call-card-info.atom.tsx

@ -7,7 +7,7 @@ import { CallTypesEnum } from '../enums'
interface IProps { interface IProps {
callerFullName: string callerFullName: string
dateTime: Date dateTime: string
callStatus: CallTypesEnum callStatus: CallTypesEnum
isMissed: boolean isMissed: boolean
} }
@ -23,16 +23,9 @@ export const CallCardInfo: FC<IProps> = ({
const callStatuses = { const callStatuses = {
incoming: 'Вихідний', incoming: 'Вихідний',
outgoing: 'Вхідний', outgoing: 'Вхідний',
skipped: 'Пропущений',
} }
const date = dateTime
.toISOString()
.toString()
.substr(0, 10)
.replace(/\-/g, '.')
const time = dateTime.toISOString().toString().substr(11, 5)
return ( return (
<View style={styles.wrapper}> <View style={styles.wrapper}>
<Txt <Txt
@ -49,7 +42,7 @@ export const CallCardInfo: FC<IProps> = ({
<Txt <Txt
style={ style={
styles.infoText styles.infoText
}>{`${callStatuses[callStatus]} ${date} ${time}`}</Txt> }>{`${callStatuses[callStatus]} ${dateTime}`}</Txt>
<IconComponent <IconComponent
style={[ style={[

2
src/modules/calls/components/call-background.component.tsx

@ -92,7 +92,7 @@ const createStyles = (theme: PartialTheme) =>
left: 0, left: 0,
}, },
content: { content: {
paddingBottom: 100, paddingBottom: 60,
justifyContent: 'flex-start', justifyContent: 'flex-start',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(0,0,0,.3)', backgroundColor: 'rgba(0,0,0,.3)',

14
src/modules/calls/components/call-row-card.component.tsx

@ -7,18 +7,16 @@ import { CallCardInfo } from '../atoms'
import { CallTypesEnum } from '../enums' import { CallTypesEnum } from '../enums'
interface IProps { interface IProps {
id: string
callerFullName: string callerFullName: string
imageUrl?: string imageUrl?: string
callStatus: CallTypesEnum callStatus: CallTypesEnum
dateTime: Date dateTime: string
isMissed: boolean isMissed: boolean
onPressCard: (id: string) => void onPressCard: () => void
onPressCall: (id: string) => void onPressCall: () => void
} }
export const CallRowCard: FC<IProps> = ({ export const CallRowCard: FC<IProps> = ({
id,
imageUrl, imageUrl,
callerFullName, callerFullName,
callStatus, callStatus,
@ -32,7 +30,8 @@ export const CallRowCard: FC<IProps> = ({
return ( return (
<TouchableOpacity <TouchableOpacity
style={styles.container} style={styles.container}
onPress={() => onPressCard(id)}> activeOpacity={1}
onPress={() => onPressCard()}>
<View style={styles.mainContent}> <View style={styles.mainContent}>
<Avatar <Avatar
containerStyle={styles.avatar} containerStyle={styles.avatar}
@ -52,7 +51,7 @@ export const CallRowCard: FC<IProps> = ({
<TouchableOpacity <TouchableOpacity
style={styles.callBtn} style={styles.callBtn}
onPress={() => onPressCall(id)}> onPress={() => onPressCall()}>
<IconComponent <IconComponent
name={'phone-1'} name={'phone-1'}
size={$size(23, 22)} size={$size(23, 22)}
@ -72,6 +71,7 @@ const createStyles = (theme: PartialTheme) =>
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: theme.$border, borderBottomColor: theme.$border,
justifyContent: 'space-between', justifyContent: 'space-between',
backgroundColor: theme.$layoutBg,
}, },
mainContent: { mainContent: {
flexDirection: 'row', flexDirection: 'row',

5
src/modules/calls/configs/ice-servers.config.ts

@ -8,4 +8,9 @@ export const iceServers = [
{ {
urls: 'stun:stun2.l.google.com:19302', urls: 'stun:stun2.l.google.com:19302',
}, },
{
urls: 'turn:relay1.expressturn.com:3478',
username: 'efFBS0EV3YVRQY4HFQ',
credential: 'ssU85viEgPpENkG7',
},
] ]

6
src/modules/calls/core/accept-call.ts

@ -1,4 +1,3 @@
import inCallManager from 'react-native-incall-manager'
import { CallDataStore, CallMod } from '../hooks' import { CallDataStore, CallMod } from '../hooks'
import { answerCallReq } from '@/api/calls/requests' import { answerCallReq } from '@/api/calls/requests'
import { RTCSessionDescription } from 'react-native-webrtc' import { RTCSessionDescription } from 'react-native-webrtc'
@ -20,10 +19,9 @@ export class AcceptCall {
this.setRemoteDescription() this.setRemoteDescription()
await this.sendAnswer() await this.sendAnswer()
this.proccessIceCandidates() this.proccessIceCandidates()
inCallManager.start({ media: 'video' }) // inCallManager.start({ media: 'video' })
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
@ -31,7 +29,7 @@ export class AcceptCall {
private prepare() { private prepare() {
this.callDataStore.changeMod(CallMod.Speaking) this.callDataStore.changeMod(CallMod.Speaking)
inCallManager.stopRingtone() // inCallManager.stopRingtone()
} }
private setRemoteDescription() { private setRemoteDescription() {

19
src/modules/calls/core/call-events-listener.ts

@ -1,7 +1,7 @@
import { socketEvents } from '@/shared' import { socketEvents } from '@/shared'
import { CallMod, callDataStoreHelper, useCallDataStore } from '../hooks' import { CallMod, callDataStoreHelper, useCallDataStore } from '../hooks'
import { RTCSessionDescription } from 'react-native-webrtc' import { RTCSessionDescription } from 'react-native-webrtc'
import inCallManager from 'react-native-incall-manager' // import inCallManager from 'react-native-incall-manager'
class CallEventsListenter { class CallEventsListenter {
constructor() { constructor() {
@ -21,9 +21,24 @@ class CallEventsListenter {
new RTCSessionDescription(data.rtcMessage), new RTCSessionDescription(data.rtcMessage),
) )
useCallDataStore.getState().changeMod(CallMod.Speaking) 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() export const callEventsListener = new CallEventsListenter()

1
src/modules/calls/core/start-call.ts

@ -50,5 +50,6 @@ export class StartCall {
title: 'Невідомий користувач', title: 'Невідомий користувач',
} }
this.getCallFromStore().put(from.title, from.avatarImageUrl) this.getCallFromStore().put(from.title, from.avatarImageUrl)
this.callDataStore.setCallId(data.call.id)
} }
} }

37
src/modules/calls/core/stop-call.ts

@ -1,8 +1,12 @@
import inCallManager from 'react-native-incall-manager' import { finishCallReq } from '@/api/calls/requests'
import { CallDataStore } from '../hooks' import { CallDataStore, ICallStreamsStore } from '../hooks'
import { MediaStream } from 'react-native-webrtc'
export class StopCall { export class StopCall {
constructor(private readonly getCallDataStore: () => CallDataStore) {} constructor(
private readonly getCallDataStore: () => CallDataStore,
private readonly getCallStreamsStore: () => ICallStreamsStore,
) {}
private get peerConnection() { private get peerConnection() {
return this.getCallDataStore().peerConnection return this.getCallDataStore().peerConnection
@ -12,8 +16,31 @@ export class StopCall {
return this.getCallDataStore() return this.getCallDataStore()
} }
public stop() { public async stop() {
await finishCallReq({
callId: this.callDataStore.callId,
}).catch(console.log)
// inCallManager.stop()
this.peerConnection.close() 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) {}
} }
} }

1
src/modules/calls/enums/call-types.enum.ts

@ -1,4 +1,5 @@
export enum CallTypesEnum { export enum CallTypesEnum {
INCOMING = 'incoming', INCOMING = 'incoming',
OUTGOING = 'outgoing', OUTGOING = 'outgoing',
SKIP = 'skipped',
} }

2
src/modules/calls/hooks/index.ts

@ -1,2 +1,4 @@
export * from './use-call-data.hook' export * from './use-call-data.hook'
export * from './use-call-from.hook' export * from './use-call-from.hook'
export * from './use-call-streams.hook'
export * from './use-calls-history.hook'

127
src/modules/calls/hooks/use-call-data.hook.ts

@ -1,13 +1,19 @@
import { MediaStream, RTCPeerConnection } from 'react-native-webrtc' import { MediaStream, RTCPeerConnection } from 'react-native-webrtc'
import { create } from 'zustand' import { create } from 'zustand'
import { iceServers } from '../configs' import { iceServers } from '../configs'
import { iceCandidateReq } from '@/api/calls/requests' import {
cancelCallReq,
finishCallReq,
iceCandidateReq,
} from '@/api/calls/requests'
import { AcceptCall, StartCall } from '../core' import { AcceptCall, StartCall } from '../core'
import { useCallFromStore } from './use-call-from.hook' import { useCallFromStore } from './use-call-from.hook'
import { NavigationService } from '@/services/system' import { NavigationService } from '@/services/system'
import { RouteKey } from '@/shared' 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 { StopCall } from '../core/stop-call'
import { callsStreamsCtr, initCallsMediaDevices } from './use-call-streams.hook'
import { Alert } from 'react-native'
export enum CallMod { export enum CallMod {
Outgoing, Outgoing,
@ -22,21 +28,21 @@ export interface CallDataStore {
targetUserId?: number targetUserId?: number
callId?: number callId?: number
remoteRTCMessage?: any remoteRTCMessage?: any
remoteStream?: any
icecandidates: any[] icecandidates: any[]
connectedStatus: RTCIceConnectionState connectedStatus: RTCIceConnectionState
changeMod: (mod: CallMod) => void changeMod: (mod: CallMod) => void
startCall: (targetUserId: number) => void startCall: (targetUserId: number, peerConnection: RTCPeerConnection) => void
incomeCall: ( incomeCall: (
targetUserId: number, targetUserId: number,
remoteRTCMessage: any, remoteRTCMessage: any,
callId: number, callId: number,
peerConnection: RTCPeerConnection,
) => void ) => void
setRemoteStream: (stream: any) => void
addIcecanidate: (item: any) => void addIcecanidate: (item: any) => void
cleanIcecandidates: () => void cleanIcecandidates: () => void
setConnectedStatus: (connectedStatus: RTCIceConnectionState) => void setConnectedStatus: (connectedStatus: RTCIceConnectionState) => void
setCallId: (callId: number) => void
} }
export const useCallDataStore = create<CallDataStore>()(set => ({ export const useCallDataStore = create<CallDataStore>()(set => ({
@ -53,26 +59,28 @@ export const useCallDataStore = create<CallDataStore>()(set => ({
set({ mod }) set({ mod })
}, },
startCall(targetUserId) { setCallId(callId: number) {
set({ callId })
},
startCall(targetUserId, peerConnection) {
set({ set({
targetUserId, targetUserId,
peerConnection: createPeerConnection(), peerConnection,
mod: CallMod.Outgoing, mod: CallMod.Outgoing,
}) })
}, },
incomeCall(targetUserId, remoteRTCMessage, callId) { incomeCall(targetUserId, remoteRTCMessage, callId, peerConnection) {
set({ set({
targetUserId, targetUserId,
callId, callId,
remoteRTCMessage, remoteRTCMessage,
mod: CallMod.Incoming, mod: CallMod.Incoming,
peerConnection: createPeerConnection(), peerConnection,
}) })
}, },
setRemoteStream(stream) {
set({ remoteStream: stream })
},
addIcecanidate(item) { addIcecanidate(item) {
set(prev => ({ icecandidates: [...prev.icecandidates, item] })) set(prev => ({ icecandidates: [...prev.icecandidates, item] }))
}, },
@ -87,90 +95,101 @@ export const useCallDataStore = create<CallDataStore>()(set => ({
export const callDataStoreHelper = { export const callDataStoreHelper = {
peerConnection: () => useCallDataStore.getState().peerConnection, 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 () => { processAccept: async () => {
new AcceptCall(useCallDataStore.getState).accept() new AcceptCall(useCallDataStore.getState).accept()
}, },
processStart: async (targetUserId: number) => { processStart: async (targetUserId: number) => {
InCallManager.start({ media: 'audio', ringback: '_BUNDLE_' }) // InCallManager.start({ media: 'audio', ringback: '_BUNDLE_' })
useCallDataStore.getState().startCall(targetUserId) try {
NavigationService.navigate(RouteKey.Call, {}) const peerConnection = await createPeerConnection()
useCallDataStore.getState().startCall(targetUserId, peerConnection)
setTimeout(() => { NavigationService.navigate(RouteKey.Call, {})
new StartCall( new StartCall(
useCallDataStore.getState, useCallDataStore.getState,
useCallFromStore.getState, useCallFromStore.getState,
).start() ).start()
}, 999) } catch (e) {
console.log(e)
}
}, },
stop: () => { stop: () => {
new StopCall(useCallDataStore.getState).stop() new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}, },
finishCall: () => { finishCall: async () => {
const state = useCallDataStore.getState() useCallDataStore.setState(useCallDataStore.getInitialState())
if ( NavigationService.goBack()
state.peerConnection.iceConnectionState === 'disconnected' || },
state.peerConnection.iceConnectionState === 'failed' cancel: async () => {
) { await cancelCallReq({
new StopCall(useCallDataStore.getState).stop() callId: useCallDataStore.getState().callId,
} }).catch(console.log)
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
useCallDataStore.setState(useCallDataStore.getInitialState()) useCallDataStore.setState(useCallDataStore.getInitialState())
NavigationService.goBack() NavigationService.goBack()
}, },
} }
function createPeerConnection() { async function createPeerConnection() {
const peerConnection = new RTCPeerConnection({ const peerConnection = new RTCPeerConnection({
iceServers: iceServers, iceServers: iceServers,
}) })
await initCallsMediaDevices(peerConnection)
peerConnection.addEventListener('track', event => { peerConnection.addEventListener('track', event => {
try { try {
const existremoteStream = useCallDataStore.getState().remoteStream const existremoteStream = callsStreamsCtr().remoteStream
const remoteMediaStream = existremoteStream || new MediaStream() const remoteMediaStream = existremoteStream || new MediaStream()
remoteMediaStream.addTrack(event.track, remoteMediaStream) remoteMediaStream.addTrack(event.track)
useCallDataStore.getState().setRemoteStream(remoteMediaStream) callsStreamsCtr().setRemoteStream(remoteMediaStream)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
}) })
peerConnection.addEventListener('icecandidate', event => { peerConnection.addEventListener('icecandidate', event => {
if (!event.candidate) { if (event.candidate !== null) {
return 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 => { peerConnection.addEventListener('iceconnectionstatechange', event => {
console.log( console.log(peerConnection.iceConnectionState)
'iceconnectionstatechange',
peerConnection.iceConnectionState,
)
useCallDataStore useCallDataStore
.getState() .getState()
.setConnectedStatus(peerConnection.iceConnectionState) .setConnectedStatus(peerConnection.iceConnectionState)
if (peerConnection.iceConnectionState === 'closed') {
useCallDataStore.getState().changeMod(CallMod.Finished)
}
if ( 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 => { peerConnection.addEventListener('onicegatheringstatechange', event => {})
// You can ignore some candidate errors.
// Connections can still be made even when errors occur.
console.log(event)
})
return peerConnection return peerConnection
} }

47
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<ICallStreamsStore>()(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')
}
}

67
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<ICallListItem>({
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,
}
}

4
src/modules/calls/screens/call/atoms/calling.atom.tsx

@ -14,7 +14,7 @@ export const CallingAtom: FC<IProps> = ({ localStream, remoteStream }) => {
flex: 1, flex: 1,
backgroundColor: '#000', backgroundColor: '#000',
}}> }}>
{/* {localStream ? ( {localStream ? (
<RTCView <RTCView
objectFit={'cover'} objectFit={'cover'}
style={{ style={{
@ -24,7 +24,7 @@ export const CallingAtom: FC<IProps> = ({ localStream, remoteStream }) => {
}} }}
streamURL={localStream.toURL()} streamURL={localStream.toURL()}
/> />
) : null} */} ) : null}
{remoteStream ? ( {remoteStream ? (
<RTCView <RTCView
objectFit={'cover'} objectFit={'cover'}

46
src/modules/calls/screens/call/index.tsx

@ -1,45 +1,30 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { CallingAtom, OutgoingcallAtom } from './atoms' import { CallingAtom, OutgoingcallAtom } from './atoms'
import { Button, useSocketListener } from '@/shared' import { Button } from '@/shared'
import { mediaDevices, RTCSessionDescription } from 'react-native-webrtc'
import { startCallReq } from '@/api/calls/requests'
import { import {
CallMod, CallMod,
callDataStoreHelper, callDataStoreHelper,
useCallDataStore, useCallDataStore,
useCallFromStore, useCallFromStore,
useCallsStream,
} from '../../hooks' } from '../../hooks'
import { CallBackground, CallBtn } from '../../components' import { CallBackground, CallBtn } from '../../components'
import { Dimensions, Modal, StatusBar, StyleSheet, View } from 'react-native' import {
Alert,
Dimensions,
Modal,
StatusBar,
StyleSheet,
View,
} from 'react-native'
export const CallScreen = () => { export const CallScreen = () => {
const mod = useCallDataStore(s => s.mod) const mod = useCallDataStore(s => s.mod)
const remoteStream = useCallDataStore(s => s.remoteStream) const { remoteStream, localStream } = useCallsStream()
const [localStream, setlocalStream] = useState(null)
const { title, avatarImageUrl } = useCallFromStore() const { title, avatarImageUrl } = useCallFromStore()
const connectedStatus = useCallDataStore(s => s.connectedStatus) const connectedStatus = useCallDataStore(s => s.connectedStatus)
const initMediaDevices = async () => { const stopCall = () => {
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 = () => {
callDataStoreHelper.stop() callDataStoreHelper.stop()
} }
@ -62,7 +47,7 @@ export const CallScreen = () => {
iconName="phone-2" iconName="phone-2"
bgColor="#DE253B" bgColor="#DE253B"
isAnimated={false} isAnimated={false}
onPress={() => callDataStoreHelper.stop()} onPress={stopCall}
/> />
</View> </View>
</CallBackground> </CallBackground>
@ -70,7 +55,7 @@ export const CallScreen = () => {
[CallMod.Outgoing]: ( [CallMod.Outgoing]: (
<OutgoingcallAtom <OutgoingcallAtom
title={title} title={title}
onPressCancel={cancelCall} onPressCancel={callDataStoreHelper.cancel}
avatarImageUrl={avatarImageUrl} avatarImageUrl={avatarImageUrl}
/> />
), ),
@ -103,6 +88,9 @@ export const CallScreen = () => {
height: Dimensions.get('screen').height, height: Dimensions.get('screen').height,
}}> }}>
{templates[mod]} {templates[mod]}
{/* <Txt style={{ color: 'red', fontSize: 50 }}>
{mod} {connectedStatus}
</Txt> */}
</Modal> </Modal>
<StatusBar hidden /> <StatusBar hidden />
</> </>

37
src/modules/calls/smart-components/call-swipable-row-card.smart-component.tsx

@ -1,4 +1,4 @@
import { getTheme, SquareButton } from '@/shared' import { getTheme, RouteKey, SquareButton, useNav } from '@/shared'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import React, { FC } from 'react' import React, { FC } from 'react'
@ -6,21 +6,38 @@ import { StyleSheet } from 'react-native'
import { Swipeable } from 'react-native-gesture-handler' import { Swipeable } from 'react-native-gesture-handler'
import { CallRowCard } from '../components' import { CallRowCard } from '../components'
import { CallTypesEnum } from '../enums' import { CallTypesEnum } from '../enums'
import { deleteCallHistoryReq } from '@/api/calls/requests'
import { callDataStoreHelper } from '../hooks'
interface IProps { interface IProps {
id: string id: number
callerFullName: string callerFullName: string
imageUrl?: string imageUrl?: string
callStatus: CallTypesEnum callStatus: CallTypesEnum
dateTime: Date dateTime: string
isMissed: boolean isMissed: boolean
onDelete: (id: string) => void onDeleted?: () => void
onPressCard: (id: string) => void targetUserId: number
onPressCall: (id: string) => void
} }
export const CallSwipableRowCardSmart: FC<IProps> = props => { export const CallSwipableRowCardSmart: FC<IProps> = props => {
const { theme } = useTheme(createStyles) const { theme } = useTheme(createStyles)
const nav = useNav()
const handlePressDelete = async () => {
await deleteCallHistoryReq(props.id)
if (props.onDeleted) {
props.onDeleted()
}
}
const handlePressCard = () => {
nav.navigate(RouteKey.ContactDetail, { contactId: props.id })
}
const handlePressCall = () => {
callDataStoreHelper.processStart(props.targetUserId)
}
const btnToRender = () => ( const btnToRender = () => (
<SquareButton <SquareButton
@ -28,13 +45,17 @@ export const CallSwipableRowCardSmart: FC<IProps> = props => {
bgColor={theme.calls.deleteBtn.$bg} bgColor={theme.calls.deleteBtn.$bg}
txtColor={theme.calls.deleteBtn.$text} txtColor={theme.calls.deleteBtn.$text}
iconName={'bin'} iconName={'bin'}
onPress={() => props.onDelete(props.id)} onPress={handlePressDelete}
/> />
) )
return ( return (
<Swipeable overshootLeft={false} renderRightActions={btnToRender}> <Swipeable overshootLeft={false} renderRightActions={btnToRender}>
<CallRowCard {...props} /> <CallRowCard
{...props}
onPressCall={handlePressCall}
onPressCard={handlePressCard}
/>
</Swipeable> </Swipeable>
) )
} }

113
src/modules/calls/smart-components/calls-list.smart-component.tsx

@ -1,64 +1,91 @@
import { $size, Txt } from '@/shared' import { $size, Loading, NotFound, Txt } from '@/shared'
import { SearchForm } from '@/shared/components/forms' import { SearchForm } from '@/shared/components/forms'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { PartialTheme } from '@/shared/themes/interfaces' import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
import React, { FC, useState } from 'react' import { StyleSheet, FlatList } from 'react-native'
import { ScrollView, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { CallSwipableRowCardSmart } from '.' import { CallSwipableRowCardSmart } from '.'
import { getCallsConfig } from '../configs/calls.config' import { ICallListItem, useCallHistory } from '../hooks'
export const CallSmartList: FC = () => { export const CallSmartList: FC = () => {
const { styles, theme } = useTheme(createStyles) const { styles } = useTheme(createStyles)
const [searchString, setSearchString] = useState<string>() const [searchString, setSearchString] = useState<string>()
const { list } = useCallHistory()
const { isLoading, isLoadingNext } = list
const timmer = useRef(null)
useEffect(() => {
if (searchString !== undefined) {
clearTimeout(timmer.current)
const callsData = getCallsConfig() timmer.current = setTimeout(() => {
list.setLoadParams({ searchString })
}, 400)
const itemToRender = ({ item }) => ( return () => {
clearTimeout(timmer.current)
}
}
}, [searchString])
const itemToRender = ({ item }: { item: ICallListItem }) => (
<CallSwipableRowCardSmart <CallSwipableRowCardSmart
id={item.id} id={item.callId}
callStatus={item.callType} callStatus={item.type}
imageUrl={null} imageUrl={item.avatarUrl}
callerFullName={`${item.firstName} ${item.lastName}`} callerFullName={item.title}
dateTime={item.dateTime} dateTime={item.date}
isMissed={item.isMissed} isMissed={item.isMissed}
onDelete={() => {}} onDeleted={list.resetFlatList}
onPressCard={() => {}} targetUserId={item.targetUserId}
onPressCall={() => {}}
/> />
) )
const keyExtractor = useCallback(
(item: ICallListItem) => String(item.callId),
[],
)
return ( return (
<ScrollView> <FlatList
<Txt style={styles.listContainer}
style={{ contentContainerStyle={styles.containerInner}
color: theme.$textPrimary, data={list.items}
textAlign: 'center', renderItem={itemToRender}
fontWeight: '500', showsVerticalScrollIndicator={false}
alignSelf: 'center', showsHorizontalScrollIndicator={false}
}}> initialNumToRender={10}
Буде реалізовано в наступній версії додатку. onEndReached={list.loadMore}
</Txt> keyExtractor={keyExtractor}
refreshing={!searchString && list.isLoading}
{/* <SearchForm onRefresh={list.resetFlatList}
placeholder="Знайти виклик" ListFooterComponent={() => {
searchValue={searchString} if (!isLoading && isLoadingNext) {
onChange={setSearchString} return <Loading />
/> }
if (isLoading) {
<FlatList return null
style={styles.listContainer} }
data={callsData} if (list.isEmpty) {
renderItem={itemToRender} return <NotFound text="Виклики не знайдені" />
/> */} }
</ScrollView> }}
ListHeaderComponent={
<SearchForm
placeholder="Знайти виклик"
searchValue={searchString}
onChange={setSearchString}
/>
}
/>
) )
} }
const createStyles = (theme: PartialTheme) => const createStyles = () =>
StyleSheet.create({ StyleSheet.create({
listContainer: { listContainer: {
marginBottom: $size(25, 23), // marginBottom: $size(25, 23),
flex: 1,
},
containerInner: {
paddingBottom: 20,
}, },
}) })

55
src/modules/calls/widgets/incoming-call.widget.tsx

@ -13,7 +13,7 @@ import {
useCallDataStore, useCallDataStore,
useCallFromStore, useCallFromStore,
} from '../hooks' } from '../hooks'
import inCallManager from 'react-native-incall-manager' // import inCallManager from 'react-native-incall-manager'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import { CallBackground, CallBtn } from '../components' import { CallBackground, CallBtn } from '../components'
import { NavigationService } from '@/services/system' import { NavigationService } from '@/services/system'
@ -28,19 +28,18 @@ export const IncomingCallWidget = () => {
'call/new', 'call/new',
data => { data => {
try { try {
inCallManager.startRingtone( // inCallManager.startRingtone(
'_DEFAULT_', // '_DEFAULT_',
[1000, 400, 300], // [1000, 400, 300],
null, // null,
30, // 30,
) // )
setVisible(true) setVisible(true)
useCallFromStore useCallFromStore
.getState() .getState()
.put(data.from.title, data.from.avatarImageUrl) .put(data.from.title, data.from.avatarImageUrl)
useCallDataStore
.getState() callDataStoreHelper.proccesIncome(data)
.incomeCall(data.callerId, data.rtcMessage, data.callId)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
@ -48,21 +47,25 @@ export const IncomingCallWidget = () => {
[setVisible], [setVisible],
) )
useSocketListener('call/canceled', data => {
setVisible(false)
callDataStoreHelper.stop()
})
useSocketListener('call/ICEcandidate', data => { useSocketListener('call/ICEcandidate', data => {
try { try {
const message = data.rtcMessage const candidates = data.candidates
const peerConnection = callDataStoreHelper.peerConnection() const peerConnection = callDataStoreHelper.peerConnection()
const candidate = new RTCIceCandidate({
candidate: message.candidate, candidates.map(it => {
sdpMid: message.id, const item = new RTCIceCandidate(it)
sdpMLineIndex: message.label,
if (peerConnection) {
peerConnection.addIceCandidate(item)
} else {
useCallDataStore.getState().addIcecanidate(item)
}
}) })
if (peerConnection) {
peerConnection.addIceCandidate(candidate)
} else {
useCallDataStore.getState().addIcecanidate(candidate)
}
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
@ -70,12 +73,10 @@ export const IncomingCallWidget = () => {
const decline = () => {} const decline = () => {}
const accept = () => { const accept = async () => {
inCallManager.stopRingtone() // inCallManager.stopRingtone()
callDataStoreHelper.processAccept() await callDataStoreHelper.processAccept()
setTimeout(() => { NavigationService.navigate(RouteKey.Call, {})
NavigationService.navigate(RouteKey.Call, {})
}, 1000)
setVisible(false) setVisible(false)
} }

5
src/modules/contacts/screens/contact-detail.screen.tsx

@ -21,6 +21,7 @@ import { selectId } from '@/store/account'
import { simpleDispatch } from '@/store/store-helpers' import { simpleDispatch } from '@/store/store-helpers'
import { SelectChat } from '@/store/chats' import { SelectChat } from '@/store/chats'
import { chatManager } from '@/managers' import { chatManager } from '@/managers'
import { callDataStoreHelper } from '@/modules/calls/hooks'
interface IProps extends IRouteParams { interface IProps extends IRouteParams {
route: { route: {
@ -124,9 +125,7 @@ export const ContactDetailScreen: FC<IProps> = ({ navigation, route }) => {
<ContactsSpeakings <ContactsSpeakings
contactId={contactId} contactId={contactId}
onCall={() => onCall={() =>
Alert.alert( callDataStoreHelper.processStart(contact.userId)
'ця функція буде реалізована в наступній версії додатку',
)
} }
onMessage={onPressMessage} onMessage={onPressMessage}
disabled={contact?.userId === accountId} disabled={contact?.userId === accountId}

17
src/modules/contacts/screens/contacts.screen.tsx

@ -1,21 +1,18 @@
import { $size, IRouteParams, ScreenLayout, SwitchButtons } from '@/shared' import { $size, ScreenLayout, SwitchButtons } from '@/shared'
import React, { FC, useEffect, useState } from 'react' import React, { FC, useState } from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { ContactsSmartList } from '../smart-component' import { ContactsSmartList } from '../smart-component'
import { TabView, SceneMap } from 'react-native-tab-view' import { TabView, SceneMap } from 'react-native-tab-view'
import { CallSmartList } from '@/modules/calls/smart-components/calls-list.smart-component' import { CallSmartList } from '@/modules/calls/smart-components/calls-list.smart-component'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { callDataStoreHelper } from '@/modules/calls/hooks'
interface IProps extends IRouteParams {}
type TRoutes = { type TRoutes = {
key: string key: string
title: string title: string
}[] }[]
export const ContactScreen: FC<IProps> = () => { export const ContactScreen: FC = () => {
const { styles } = useTheme(createStyles) const { styles } = useTheme(createStyles)
const [index, setIndex] = useState<number>(0) const [index, setIndex] = useState<number>(0)
@ -26,15 +23,9 @@ export const ContactScreen: FC<IProps> = () => {
const renderScene = SceneMap({ const renderScene = SceneMap({
contacts: ContactsSmartList, contacts: ContactsSmartList,
calls: () => <CallSmartList />, calls: CallSmartList,
}) })
useEffect(() => {
setTimeout(() => {
callDataStoreHelper.processStart(40)
}, 1000)
}, [])
const renderTabBar = () => ( const renderTabBar = () => (
<SwitchButtons <SwitchButtons
style={styles.switchContainer} style={styles.switchContainer}

1
src/modules/root/index.tsx

@ -54,7 +54,6 @@ export const Navigation: FC = () => {
// const status = 'online' // const status = 'online'
useSharedFiles() useSharedFiles()
useAppSocketListener() useAppSocketListener()
useAppBadge() useAppBadge()

1
src/services/system/real-time.service.ts

@ -122,6 +122,7 @@ export class SocketIo {
this._onSocketSendEvent('call/answered') this._onSocketSendEvent('call/answered')
this._onSocketSendEvent('call/new') this._onSocketSendEvent('call/new')
this._onSocketSendEvent('call/ICEcandidate') this._onSocketSendEvent('call/ICEcandidate')
this._onSocketSendEvent('call/canceled')
this._on('error/join-user', async () => { this._on('error/join-user', async () => {
await authService.refreshSession() await authService.refreshSession()

6
src/shared/enums/call-status.enum.ts

@ -0,0 +1,6 @@
export enum CallStatus {
New = 'n',
InProccess = 'i',
Canceled = 'c',
Finished = 'f',
}

29
src/shared/enums/index.ts

@ -1,18 +1,19 @@
export * from './route-key.enum' export * from './actions-queue-type.enum'
export * from './navigation-module-key.enum' export * from './btns-type.enum'
export * from './exception-keys.enum' export * from './call-status.enum'
export * from './storage-keys.enum'
export * from './user.enum'
export * from './taxonomies.enum'
export * from './task-status.enum'
export * from './task-actions.enum'
export * from './chat-bg.enum' export * from './chat-bg.enum'
export * from './task-events.enum'
export * from './notification.enum'
export * from './chat.enums' export * from './chat.enums'
export * from './btns-type.enum'
export * from './actions-queue-type.enum'
export * from './entity-type.enum' export * from './entity-type.enum'
export * from './permissions.enum'
export * from './error-message.enum' export * from './error-message.enum'
export * from './file-type.enum' export * from './exception-keys.enum'
export * from './file-type.enum'
export * from './navigation-module-key.enum'
export * from './notification.enum'
export * from './permissions.enum'
export * from './route-key.enum'
export * from './storage-keys.enum'
export * from './task-actions.enum'
export * from './task-events.enum'
export * from './task-status.enum'
export * from './taxonomies.enum'
export * from './user.enum'

5
src/shared/events/index.ts

@ -235,7 +235,10 @@ export type SocketEvents = {
rtcMessage: any rtcMessage: any
} }
'call/ICEcandidate': { 'call/ICEcandidate': {
rtcMessage: any candidates: any[]
}
'call/canceled': {
callId: number
} }
} }

17
src/shared/interfaces/call.inteface.ts

@ -0,0 +1,17 @@
import { CallStatus } from '../enums'
import { IUser } from './user.interfaces'
export interface ICall {
id: number
usersIds: number[]
initiatorUserId: number
finishedAt: string
startAt: string
status: CallStatus
createdAt: string
updatedAt: string
users?: IUser[]
}

27
src/shared/interfaces/index.ts

@ -1,15 +1,18 @@
export * from './routing.interfaces' export * from './call.inteface'
export * from './token-pair.interfaces' export * from './chats.interfaces'
export * from './user.interfaces'
export * from './styles.interfaces'
export * from './taxonomy.interfaces'
export * from './contact.interfaces'
export * from './pagination.interfaces'
export * from './tasks.interfaces'
export * from './comments.interfaces' export * from './comments.interfaces'
export * from './configs.interfaces'
export * from './contact.interfaces'
export * from './drawer.interface'
export * from './entities'
export * from './factories.interfaces' export * from './factories.interfaces'
export * from './options.interfaces'
export * from './notification.interfaces'
export * from './chats.interfaces'
export * from './media.interfaces' export * from './media.interfaces'
export * from './configs.interfaces' export * from './notification.interfaces'
export * from './options.interfaces'
export * from './pagination.interfaces'
export * from './routing.interfaces'
export * from './styles.interfaces'
export * from './tasks.interfaces'
export * from './taxonomy.interfaces'
export * from './token-pair.interfaces'
export * from './user.interfaces'

3
tsconfig.json

@ -63,7 +63,8 @@
"node_modules", "node_modules",
"babel.config.js", "babel.config.js",
"metro.config.js", "metro.config.js",
"jest.config.js" "jest.config.js",
"taskme"
] ]
} }

Loading…
Cancel
Save