Browse Source

FEATURE | Call ket

master
Vitalik 8 months ago
parent
commit
f45016365c
  1. 1
      index.js
  2. 11
      ios/taskme2.xcodeproj/project.pbxproj
  3. 2
      ios/taskme2.xcodeproj/xcshareddata/xcschemes/taskme2.xcscheme
  4. 45
      ios/taskme2/AppDelegate.mm
  5. 1
      ios/taskme2/Info.plist
  6. 17
      src/App.tsx
  7. 4
      src/api/calls/requests.ts
  8. 2
      src/config/index.ts
  9. 44
      src/modules/calls/core/call-events-listener.ts
  10. 113
      src/modules/calls/hooks/use-call-data.hook.ts
  11. 1
      src/modules/calls/hooks/use-call-streams.hook.ts
  12. 22
      src/modules/calls/screens/call/index.tsx
  13. 22
      src/modules/calls/services/call-root.service.ts
  14. 10
      src/modules/calls/services/call-utility.service.ts
  15. 68
      src/modules/calls/services/call.service.ts
  16. 78
      src/modules/calls/services/callkeep.service.ts
  17. 77
      src/modules/calls/services/calls-events.service.ts
  18. 6
      src/modules/calls/services/index.ts
  19. 66
      src/modules/calls/services/peer-connection.service.ts
  20. 6
      src/modules/calls/smart-components/call-swipable-row-card.smart-component.tsx
  21. 40
      src/modules/calls/widgets/incoming-call.widget.tsx
  22. 17
      src/modules/contacts/screens/contact-detail.screen.tsx
  23. 1
      src/modules/root/hooks/use-app-socket-listener.hook.ts
  24. 1
      src/modules/root/index.tsx
  25. 1
      src/services/domain/actions-queue.service.ts
  26. 27
      src/services/system/callkeep.service.ts
  27. 3
      src/services/system/index.ts
  28. 71
      src/services/system/voip-notification.service.ts
  29. 11
      src/shared/db/index.ts
  30. 2
      src/shared/events/index.ts
  31. 2
      src/shared/helpers/chat-message-preview-text.helper.ts

1
index.js

@ -16,6 +16,7 @@ import '@/services/system/skeleton-data.service'
const errorHandler = (e, isFatal) => { const errorHandler = (e, isFatal) => {
if (isFatal) { if (isFatal) {
console.log(e)
Alert.alert( Alert.alert(
'Unexpected error occurred', 'Unexpected error occurred',
` `

11
ios/taskme2.xcodeproj/project.pbxproj

@ -334,7 +334,7 @@
CreatedOnToolsVersion = 14.3.1; CreatedOnToolsVersion = 14.3.1;
}; };
13B07F861A680F5B00A75B9A = { 13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1120; LastSwiftMigration = 1520;
}; };
}; };
}; };
@ -734,7 +734,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 = 20; CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -854,6 +854,7 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "taskme2-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@ -1013,7 +1014,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 = 20; CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
@ -1134,6 +1135,7 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "taskme2-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -1150,7 +1152,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 = 20; CURRENT_PROJECT_VERSION = 22;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1270,6 +1272,7 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "taskme2-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

2
ios/taskme2.xcodeproj/xcshareddata/xcschemes/taskme2.xcscheme

@ -59,7 +59,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"

45
ios/taskme2/AppDelegate.mm

@ -17,7 +17,7 @@
// They will be passed down to the ViewController used by React Native. // They will be passed down to the ViewController used by React Native.
self.initialProps = @{}; self.initialProps = @{};
NSLog(@"setup testtttt");
return [super application:application didFinishLaunchingWithOptions:launchOptions]; return [super application:application didFinishLaunchingWithOptions:launchOptions];
} }
@ -43,57 +43,46 @@ NSLog(@"setup testtttt");
// --- Handle updated push credentials // --- Handle updated push credentials
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type { - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
// Register VoIP push token (a property of PKPushCredentials) with server // Register VoIP push token (a property of PKPushCredentials) with server
NSString *token = [credentials.token description]; NSLog(@"pushRegistry didUpdatePushCredentials");
token = [token stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; [RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:type];
token = [token stringByReplacingOccurrencesOfString:@" " withString:@""]; }
NSLog(@"VoIP token: %@", token);
NSDictionary *params = @{@"token": token};
[[NSNotificationCenter defaultCenter] postNotificationName:@"VoipTokenReceived" object:nil userInfo:params];
[RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];
}
- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type // --- Handle invalidation of push token
{ - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type {
// --- The system calls this method when a previously provided push token is no longer valid for use. No action is necessary on your part to reregister the push type. Instead, use this method to notify your server not to send push notifications using the matching push token. NSLog(@"pushRegistry didInvalidatePushTokenForType");
} }
// --- Handle incoming pushes // --- Handle incoming pushes
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
// --- NOTE: apple forced us to invoke callkit ASAP when we receive voip push
// --- see: react-native-callkeep
// --- Retrieve information from your voip push payload
NSString *uuid = payload.dictionaryPayload[@"uuid"]; NSString *uuid = payload.dictionaryPayload[@"uuid"];
NSString *callerName = [NSString stringWithFormat:@"%@ (Connecting...)", payload.dictionaryPayload[@"callerName"]]; NSString *callerName = payload.dictionaryPayload[@"callerName"];
NSString *handle = payload.dictionaryPayload[@"handle"]; NSString *handle = payload.dictionaryPayload[@"handle"];
// --- this is optional, only required if you want to call `completion()` on the js side NSLog(@"Payload uuid: %@", uuid);
[RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];
// --- this is optional, only required if you want to call `completion()` on the js side
// [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];
// --- Process the received push // --- Process the received push
[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type]; [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
// --- You should make sure to report to callkit BEFORE execute `completion()`
[RNCallKeep reportNewIncomingCall: uuid [RNCallKeep reportNewIncomingCall: uuid
handle: handle handle: handle
handleType: @"generic" handleType: @"generic"
hasVideo: NO hasVideo: NO
localizedCallerName: callerName localizedCallerName: callerName
supportsHolding: YES supportsHolding: NO
supportsDTMF: YES supportsDTMF: NO
supportsGrouping: YES supportsGrouping: NO
supportsUngrouping: YES supportsUngrouping: NO
fromPushKit: YES fromPushKit: YES
payload: nil payload: nil
withCompletionHandler: completion]; withCompletionHandler: completion];
// --- You don't need to call it if you stored `completion()` and will call it on the js side.
completion(); completion();
} }

1
ios/taskme2/Info.plist

@ -79,6 +79,7 @@
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>voip</string> <string>voip</string>
</array> </array>

17
src/App.tsx

@ -7,7 +7,6 @@ if (__DEV__) {
} }
import { ThemeProvider } from './shared/themes' import { ThemeProvider } from './shared/themes'
// import Orientation from 'react-native-orientation-locker'
import { AppState, LogBox, Platform } from 'react-native' import { AppState, LogBox, Platform } from 'react-native'
import { appService } from './services/app.service' import { appService } from './services/app.service'
import 'react-native-gesture-handler' import 'react-native-gesture-handler'
@ -18,17 +17,17 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { appEvents } from './shared' import { appEvents } from './shared'
import Toast from 'react-native-toast-message' import Toast from 'react-native-toast-message'
import { toastConfig } from './config/toast.config' import { toastConfig } from './config/toast.config'
import './services/system/callkeep.service'
import { VoipNotificationsService } from './services/system' import { VoipNotificationsService } from './services/system'
import { callKeepService } from './modules/calls/services/callkeep.service'
import './modules/calls/services/calls-events.service'
LogBox.ignoreLogs(['Warning: ...', 'Require cycle: ...']) // Ignore log notification by message LogBox.ignoreLogs(['Warning: ...', 'Require cycle: ...'])
LogBox.ignoreAllLogs() //Ignore all log notifications LogBox.ignoreAllLogs()
const App: FC = () => { const App: FC = () => {
useEffect(() => { useEffect(() => {
appService.initApp() appService.initApp()
// Orientation.lockToPortrait()
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -48,6 +47,14 @@ const App: FC = () => {
} }
}, []) }, [])
useEffect(() => {
// console.log(callsEventsService)
callKeepService.register()
return () => {
callKeepService.unregister()
}
}, [])
useEffect(() => { useEffect(() => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
VoipNotificationsService.getInstance().initListeners() VoipNotificationsService.getInstance().initListeners()

4
src/api/calls/requests.ts

@ -33,3 +33,7 @@ export const getCallsListReq = (params: any) => {
export const deleteCallHistoryReq = (callId: number) => { export const deleteCallHistoryReq = (callId: number) => {
return api.delete(`calls/history/${callId}`, null, '') return api.delete(`calls/history/${callId}`, null, '')
} }
export const getIncomeDataReq = (callId: string) => {
return api.post(`calls/income/${callId}`, {}, {}, '')
}

2
src/config/index.ts

@ -9,8 +9,6 @@ export const dynamicConfig = {
googlePlayUrl: googlePlayUrl:
'https://play.google.com/store/apps/details?id=com.app.task_me', 'https://play.google.com/store/apps/details?id=com.app.task_me',
} }
console.log('config', Config)
export const config = { export const config = {
...dynamicConfig, ...dynamicConfig,
fonts, fonts,

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

@ -1,44 +0,0 @@
import { socketEvents } from '@/shared'
import { CallMod, callDataStoreHelper, useCallDataStore } from '../hooks'
import { RTCSessionDescription } from 'react-native-webrtc'
// import inCallManager from 'react-native-incall-manager'
class CallEventsListenter {
constructor() {
this.init()
}
private init() {
this.initCallAnswered()
}
private initCallAnswered() {
socketEvents.on('call/answered', data => {
console.log('ANSWERED')
callDataStoreHelper
.peerConnection()
.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
useCallDataStore.getState().changeMod(CallMod.Speaking)
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()

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

@ -1,19 +1,5 @@
import { MediaStream, RTCPeerConnection } from 'react-native-webrtc' import { RTCPeerConnection } from 'react-native-webrtc'
import { create } from 'zustand' import { create } from 'zustand'
import { iceServers } from '../configs'
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 { 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,
@ -95,101 +81,4 @@ 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 () => {
new AcceptCall(useCallDataStore.getState).accept()
},
processStart: async (targetUserId: number) => {
// 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()
} catch (e) {
console.log(e)
}
},
stop: () => {
new StopCall(useCallDataStore.getState, callsStreamsCtr).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()
},
}
async function createPeerConnection() {
const peerConnection = new RTCPeerConnection({
iceServers: iceServers,
})
await initCallsMediaDevices(peerConnection)
peerConnection.addEventListener('track', event => {
try {
const existremoteStream = callsStreamsCtr().remoteStream
const remoteMediaStream = existremoteStream || new MediaStream()
remoteMediaStream.addTrack(event.track)
callsStreamsCtr().setRemoteStream(remoteMediaStream)
} catch (e) {
console.log(e)
}
})
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate !== null) {
iceCandidateReq({
targetUserId: useCallDataStore.getState().targetUserId,
candidates: [event.candidate],
})
}
})
peerConnection.addEventListener('iceconnectionstatechange', event => {
console.log(peerConnection.iceConnectionState)
useCallDataStore
.getState()
.setConnectedStatus(peerConnection.iceConnectionState)
if (peerConnection.iceConnectionState === 'closed') {
useCallDataStore.getState().changeMod(CallMod.Finished)
}
if (
peerConnection.iceConnectionState === 'disconnected' ||
peerConnection.iceConnectionState === 'failed'
) {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}
})
peerConnection.addEventListener('onicegatheringstatechange', event => {})
return peerConnection
} }

1
src/modules/calls/hooks/use-call-streams.hook.ts

@ -1,6 +1,5 @@
import { MediaStream, mediaDevices } from 'react-native-webrtc' import { MediaStream, mediaDevices } from 'react-native-webrtc'
import { create } from 'zustand' import { create } from 'zustand'
import { callDataStoreHelper } from './use-call-data.hook'
export interface ICallStreamsStore { export interface ICallStreamsStore {
localStream: MediaStream localStream: MediaStream

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

@ -1,22 +1,15 @@
import React, { useEffect, useRef, useState } from 'react' import React from 'react'
import { CallingAtom, OutgoingcallAtom } from './atoms' import { CallingAtom, OutgoingcallAtom } from './atoms'
import { Button } from '@/shared' import { Button } from '@/shared'
import { import {
CallMod, CallMod,
callDataStoreHelper,
useCallDataStore, useCallDataStore,
useCallFromStore, useCallFromStore,
useCallsStream, useCallsStream,
} from '../../hooks' } from '../../hooks'
import { CallBackground, CallBtn } from '../../components' import { CallBackground, CallBtn } from '../../components'
import { import { Dimensions, Modal, StatusBar, StyleSheet, View } from 'react-native'
Alert, import { callService } from '../../services'
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)
@ -25,7 +18,7 @@ export const CallScreen = () => {
const connectedStatus = useCallDataStore(s => s.connectedStatus) const connectedStatus = useCallDataStore(s => s.connectedStatus)
const stopCall = () => { const stopCall = () => {
callDataStoreHelper.stop() callService.stop()
} }
const templates = { const templates = {
@ -55,7 +48,7 @@ export const CallScreen = () => {
[CallMod.Outgoing]: ( [CallMod.Outgoing]: (
<OutgoingcallAtom <OutgoingcallAtom
title={title} title={title}
onPressCancel={callDataStoreHelper.cancel} onPressCancel={callService.cancel}
avatarImageUrl={avatarImageUrl} avatarImageUrl={avatarImageUrl}
/> />
), ),
@ -66,10 +59,7 @@ export const CallScreen = () => {
needOverlay={false} needOverlay={false}
showAvatar={false}> showAvatar={false}>
<View style={styles.row}> <View style={styles.row}>
<Button <Button title="Вийти" onPress={callService.finishCall} />
title="Вийти"
onPress={() => callDataStoreHelper.finishCall()}
/>
</View> </View>
</CallBackground> </CallBackground>
), ),

22
src/modules/calls/services/call-root.service.ts

@ -0,0 +1,22 @@
import {
CallDataStore,
CallFromStore,
ICallStreamsStore,
useCallDataStore,
useCallFromStore,
useCallsStream,
} from '../hooks'
export class CallRoot {
protected get store(): CallDataStore {
return useCallDataStore.getState()
}
protected get storeFrom(): CallFromStore {
return useCallFromStore.getState()
}
protected get streamsStore(): ICallStreamsStore {
return useCallsStream.getState()
}
}

10
src/modules/calls/services/call-utility.service.ts

@ -0,0 +1,10 @@
import { CallDataStore, useCallDataStore } from '../hooks'
import { CallRoot } from './call-root.service'
class CallUtilityService extends CallRoot {
public peerConnection() {
return this.store.peerConnection
}
}
export const callUtilityService = new CallUtilityService()

68
src/modules/calls/services/call.service.ts

@ -0,0 +1,68 @@
import { NavigationService } from '@/services/system'
import { AcceptCall, StartCall } from '../core'
import { callsStreamsCtr, useCallDataStore, useCallFromStore } from '../hooks'
import { CallRoot } from './call-root.service'
import { peerConnectionService } from './peer-connection.service'
import { RouteKey } from '@/shared'
import { StopCall } from '../core/stop-call'
import { cancelCallReq } from '@/api/calls/requests'
import { RTCSessionDescription } from 'react-native-webrtc'
class CallService extends CallRoot {
public async proccesIncome(data: any) {
const peerConnection = await peerConnectionService.createIns()
useCallDataStore
.getState()
.incomeCall(
data.callerId,
data.rtcMessage,
data.callId,
peerConnection,
)
peerConnection.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
}
public async processAccept() {
new AcceptCall(useCallDataStore.getState).accept()
}
public async processStart(targetUserId: number) {
try {
const peerConnection = await peerConnectionService.createIns()
useCallDataStore.getState().startCall(targetUserId, peerConnection)
NavigationService.navigate(RouteKey.Call, {})
new StartCall(
useCallDataStore.getState,
useCallFromStore.getState,
).start()
} catch (e) {
console.log(e)
}
}
public async stop() {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}
public async finishCall() {
useCallDataStore.setState(useCallDataStore.getInitialState())
NavigationService.goBack()
}
public async cancel() {
await cancelCallReq({
callId: useCallDataStore.getState().callId,
}).catch(console.log)
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
useCallDataStore.setState(useCallDataStore.getInitialState())
NavigationService.goBack()
}
}
export const callService = new CallService()

78
src/modules/calls/services/callkeep.service.ts

@ -0,0 +1,78 @@
import { getIncomeDataReq } from '@/api/calls/requests'
import RNCallKeep from 'react-native-callkeep'
class CallKeepService {
public register() {
this.init()
this.initListeners()
}
public unregister() {
this.removeListeners()
}
private init() {
RNCallKeep.setup({
ios: {
appName: 'TaskMe',
includesCallsInRecents: false,
maximumCallGroups: '1',
supportsVideo: false,
},
android: {
alertTitle: 'Permissions required',
alertDescription:
'This application needs to access your phone accounts',
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'phone_account_icon',
additionalPermissions: [],
foregroundService: {
channelId: 'com.company.my',
channelName: 'Foreground service for my app',
notificationTitle: 'My app is running on background',
notificationIcon:
'Path to the resource icon of the notification',
},
},
})
}
private initListeners() {
RNCallKeep.addEventListener('answerCall', this.onAnswer.bind(this))
RNCallKeep.addEventListener('endCall', this.onEnd.bind(this))
RNCallKeep.addEventListener(
'didPerformSetMutedCallAction',
this.changeMuted.bind(this),
)
}
private removeListeners() {
RNCallKeep.clearInitialEvents()
RNCallKeep.removeEventListener('answerCall')
RNCallKeep.removeEventListener('endCall')
RNCallKeep.removeEventListener('didPerformSetMutedCallAction')
}
private async onAnswer(data: any) {
try {
console.log('ON ANSWER', data)
getIncomeDataReq(data.callUUID)
} catch (e) {
console.log('Error on answer', e)
}
}
private async onEnd() {
console.log('onend')
}
private async changeMuted() {
console.log('changemuted')
}
public async stop() {
RNCallKeep.endCall('')
}
}
export const callKeepService = new CallKeepService()

77
src/modules/calls/services/calls-events.service.ts

@ -0,0 +1,77 @@
import { RouteKey, socketEvents } from '@/shared'
import { CallRoot } from './call-root.service'
import { CallMod } from '../hooks'
import { callService } from './call.service'
import { NavigationService } from '@/services/system'
import { Alert } from 'react-native'
import { RTCIceCandidate, RTCSessionDescription } from 'react-native-webrtc'
class CallsEventsService extends CallRoot {
constructor() {
super()
this.init()
}
private init() {
socketEvents.on('call/answered', data => {
this.store.peerConnection.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
this.store.changeMod(CallMod.Speaking)
this.proccessIceCandidates()
})
socketEvents.on('call/new', this.proccessNewCall.bind(this))
socketEvents.on(
'call/ICEcandidate',
this.proccessIceCandidateEvent.bind(this),
)
}
private proccessIceCandidates() {
const existIcecandidates = this.store.icecandidates
if (existIcecandidates.length) {
existIcecandidates.map(candidate =>
this.store.peerConnection.addIceCandidate(candidate),
)
this.store.cleanIcecandidates()
}
}
private async proccessNewCall(data) {
console.log('procces new call', data)
try {
this.storeFrom.put(data.from.title, data.from.avatarImageUrl)
await callService.proccesIncome(data)
setTimeout(() => {
callService.processAccept()
NavigationService.navigate(RouteKey.Call, {})
}, 100)
} catch (e) {
console.log('e', e)
}
}
private proccessIceCandidateEvent(data) {
try {
const candidates = data.candidates
const peerConnection = this.store.peerConnection
candidates.map(it => {
const item = new RTCIceCandidate(it)
if (peerConnection) {
peerConnection.addIceCandidate(item)
} else {
this.store.addIcecanidate(item)
}
})
} catch (e) {
console.log(e)
}
}
}
export const callsEventsService = new CallsEventsService()

6
src/modules/calls/services/index.ts

@ -0,0 +1,6 @@
export * from './call-root.service'
export * from './call-utility.service'
export * from './call.service'
export * from './callkeep.service'
export * from './calls-events.service'
export * from './peer-connection.service'

66
src/modules/calls/services/peer-connection.service.ts

@ -0,0 +1,66 @@
import { MediaStream, RTCPeerConnection } from 'react-native-webrtc'
import { iceServers } from '../configs'
import { CallRoot } from './call-root.service'
import { iceCandidateReq } from '@/api/calls/requests'
import {
CallMod,
callsStreamsCtr,
initCallsMediaDevices,
useCallDataStore,
} from '../hooks'
import { StopCall } from '../core/stop-call'
class PeerConnectionService extends CallRoot {
public async createIns() {
const peerConnection = new RTCPeerConnection({
iceServers: iceServers,
})
await initCallsMediaDevices(peerConnection)
await this.addListeners(peerConnection)
return peerConnection
}
private async addListeners(peerConnection: RTCPeerConnection) {
peerConnection.addEventListener('track', event => {
try {
const existremoteStream = this.streamsStore.remoteStream
const remoteMediaStream = existremoteStream || new MediaStream()
remoteMediaStream.addTrack(event.track)
this.streamsStore.setRemoteStream(remoteMediaStream)
} catch (e) {
console.log(e)
}
})
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate === null) {
return
}
iceCandidateReq({
targetUserId: this.store.targetUserId,
candidates: [event.candidate],
})
})
peerConnection.addEventListener('iceconnectionstatechange', event => {
this.store.setConnectedStatus(peerConnection.iceConnectionState)
if (peerConnection.iceConnectionState === 'closed') {
this.store.changeMod(CallMod.Finished)
}
if (
peerConnection.iceConnectionState === 'disconnected' ||
peerConnection.iceConnectionState === 'failed'
) {
new StopCall(useCallDataStore.getState, callsStreamsCtr).stop()
}
})
}
}
export const peerConnectionService = new PeerConnectionService()

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

@ -1,4 +1,4 @@
import { getTheme, RouteKey, SquareButton, useNav } from '@/shared' import { 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'
@ -7,7 +7,7 @@ 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 { deleteCallHistoryReq } from '@/api/calls/requests'
import { callDataStoreHelper } from '../hooks' import { callService } from '../services'
interface IProps { interface IProps {
id: number id: number
@ -36,7 +36,7 @@ export const CallSwipableRowCardSmart: FC<IProps> = props => {
} }
const handlePressCall = () => { const handlePressCall = () => {
callDataStoreHelper.processStart(props.targetUserId) callService.processStart(props.targetUserId)
} }
const btnToRender = () => ( const btnToRender = () => (

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

@ -1,55 +1,25 @@
import { import { RouteKey, useSocketListener, useTheme } from '@/shared'
$size,
RouteKey,
Txt,
useNav,
useSocketListener,
useTheme,
} from '@/shared'
import React, { useState } from 'react' import React, { useState } from 'react'
import { Alert, Dimensions, Modal, StyleSheet, View } from 'react-native' import { Dimensions, Modal, StyleSheet, View } from 'react-native'
import { import {
callDataStoreHelper, callDataStoreHelper,
useCallDataStore, useCallDataStore,
useCallFromStore, useCallFromStore,
} from '../hooks' } from '../hooks'
// 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'
import { RTCIceCandidate } from 'react-native-webrtc' import { RTCIceCandidate } from 'react-native-webrtc'
import { callService } from '../services'
export const IncomingCallWidget = () => { export const IncomingCallWidget = () => {
const [isVisible, setVisible] = useState(false) const [isVisible, setVisible] = useState(false)
const { title, avatarImageUrl } = useCallFromStore() const { title, avatarImageUrl } = useCallFromStore()
const { styles } = useTheme(createStyles) const { styles } = useTheme(createStyles)
useSocketListener(
'call/new',
data => {
try {
// inCallManager.startRingtone(
// '_DEFAULT_',
// [1000, 400, 300],
// null,
// 30,
// )
setVisible(true)
useCallFromStore
.getState()
.put(data.from.title, data.from.avatarImageUrl)
callDataStoreHelper.proccesIncome(data)
} catch (e) {
console.log(e)
}
},
[setVisible],
)
useSocketListener('call/canceled', data => { useSocketListener('call/canceled', data => {
setVisible(false) setVisible(false)
callDataStoreHelper.stop() callService.stop()
}) })
useSocketListener('call/ICEcandidate', data => { useSocketListener('call/ICEcandidate', data => {
@ -75,7 +45,7 @@ export const IncomingCallWidget = () => {
const accept = async () => { const accept = async () => {
// inCallManager.stopRingtone() // inCallManager.stopRingtone()
await callDataStoreHelper.processAccept() await callService.processAccept()
NavigationService.navigate(RouteKey.Call, {}) NavigationService.navigate(RouteKey.Call, {})
setVisible(false) setVisible(false)
} }

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

@ -21,7 +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' import { callService } from '@/modules/calls/services'
interface IProps extends IRouteParams { interface IProps extends IRouteParams {
route: { route: {
@ -59,10 +59,13 @@ export const ContactDetailScreen: FC<IProps> = ({ navigation, route }) => {
const getSubText = () => { const getSubText = () => {
if (contact?.factoryName) { if (contact?.factoryName) {
if (contact.position) if (contact.position) {
return `${contact.factoryName}, ${contact.position}` return `${contact.factoryName}, ${contact.position}`
}
return contact.factoryName return contact.factoryName
} else return contact?.position ? contact.position : '' } else {
return contact?.position ? contact.position : ''
}
} }
const fieldsToRender = useMemo( const fieldsToRender = useMemo(
@ -73,8 +76,12 @@ export const ContactDetailScreen: FC<IProps> = ({ navigation, route }) => {
key={`${it.value}---${i}`} key={`${it.value}---${i}`}
disabled={type === ContactDetailFieldType.OTHER} disabled={type === ContactDetailFieldType.OTHER}
onPress={() => { onPress={() => {
if (contact?.status !== EUserStatus.Deleted && it.value) if (
contact?.status !== EUserStatus.Deleted &&
it.value
) {
onPress(type, it.value) onPress(type, it.value)
}
}} }}
needCopy needCopy
/> />
@ -125,7 +132,7 @@ export const ContactDetailScreen: FC<IProps> = ({ navigation, route }) => {
<ContactsSpeakings <ContactsSpeakings
contactId={contactId} contactId={contactId}
onCall={() => onCall={() =>
callDataStoreHelper.processStart(contact.userId) callService.processStart(contact.userId)
} }
onMessage={onPressMessage} onMessage={onPressMessage}
disabled={contact?.userId === accountId} disabled={contact?.userId === accountId}

1
src/modules/root/hooks/use-app-socket-listener.hook.ts

@ -10,7 +10,6 @@ export const useAppSocketListener = () => {
const onStopSession = async () => await authService.stopSession() const onStopSession = async () => await authService.stopSession()
const onChangedUserPermissions = async () => { const onChangedUserPermissions = async () => {
console.log('permission changed')
await permissionsService.loadPermissionsForUsers() await permissionsService.loadPermissionsForUsers()
} }

1
src/modules/root/index.tsx

@ -97,7 +97,6 @@ export const Navigation: FC = () => {
<SelectTaxonomiesModalSmart /> <SelectTaxonomiesModalSmart />
<RecordAudioModalSmart /> <RecordAudioModalSmart />
<ChatSendImgModal /> <ChatSendImgModal />
<IncomingCallWidget />
</> </>
) )
} }

1
src/services/domain/actions-queue.service.ts

@ -225,7 +225,6 @@ class ActionsQueueService {
let hasNext = false let hasNext = false
const action = await this.dbAQRepository.getFirst() const action = await this.dbAQRepository.getFirst()
console.log('action to execute', action)
try { try {
if (!action) return if (!action) return

27
src/services/system/callkeep.service.ts

@ -1,27 +0,0 @@
import RNCallKeep from 'react-native-callkeep'
const options = {
ios: {
appName: 'TaskMe',
},
android: {
alertTitle: 'Permissions required',
alertDescription:
'This application needs to access your phone accounts',
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'phone_account_icon',
additionalPermissions: [],
// Required to get audio in background when using Android 11
foregroundService: {
channelId: 'com.company.my',
channelName: 'Foreground service for my app',
notificationTitle: 'My app is running on background',
notificationIcon: 'Path to the resource icon of the notification',
},
},
}
RNCallKeep.setup(options).then(accepted => {
console.log('accepted', accepted)
})

3
src/services/system/index.ts

@ -1,5 +1,5 @@
export * from './app-info.service' export * from './app-info.service'
export * from './callkeep.service'
export * from './camera-roll.service' export * from './camera-roll.service'
export * from './converter.service' export * from './converter.service'
export * from './device-info.service' export * from './device-info.service'
@ -9,7 +9,6 @@ export * from './media-permissions.service'
export * from './media.service' export * from './media.service'
export * from './navigation.service' export * from './navigation.service'
export * from './notification.service' export * from './notification.service'
export * from './reactron.service'
export * from './real-time.service' export * from './real-time.service'
export * from './skeleton-data.service' export * from './skeleton-data.service'
export * from './storage.service' export * from './storage.service'

71
src/services/system/voip-notification.service.ts

@ -1,10 +1,6 @@
import { saveUserDeviceReq } from '@/api' import { saveUserDeviceReq } from '@/api'
import axios from 'axios'
import { Platform } from 'react-native'
import VoipPushNotification from 'react-native-voip-push-notification' import VoipPushNotification from 'react-native-voip-push-notification'
import { DeviceInfoService } from './device-info.service' import { DeviceInfoService } from './device-info.service'
import RNCallKeep from 'react-native-callkeep'
export class VoipNotificationsService { export class VoipNotificationsService {
static instance: VoipNotificationsService static instance: VoipNotificationsService
@ -16,70 +12,43 @@ export class VoipNotificationsService {
return this.instance return this.instance
} }
private oneSignalId: string private voipToken: string
public initListeners() { public initListeners() {
VoipPushNotification.addEventListener( try {
'register', VoipPushNotification.addEventListener(
this.handleRegister.bind(this), 'register',
) this.handleRegister.bind(this),
VoipPushNotification.addEventListener( )
'notification',
this.handleNotification.bind(this),
)
VoipPushNotification.registerVoipToken() VoipPushNotification.registerVoipToken()
} catch (e) {
console.log(e)
}
} }
public removeListeners() { public removeListeners() {
VoipPushNotification.removeEventListener('register') VoipPushNotification.removeEventListener('register')
VoipPushNotification.removeEventListener('notification')
} }
private async handleRegister(token: string) { private async handleRegister(token: string) {
try { try {
console.log('voip token', token) console.log('voip token', token)
this.sendTokenToOneSignal(token) this.voipToken = token
} catch (e) { } catch (e) {
console.log('Handle register', e) console.log('Handle register', e)
} }
} }
private async handleNotification(notification) {
console.log('app', notification)
RNCallKeep.displayIncomingCall('sda', 'Vutaku', 'Yatsenkoo', 'number')
VoipPushNotification.onVoipNotificationCompleted(notification.uuid)
}
private async sendTokenToOneSignal(token: string) {
const { data } = await axios.post(
'https://onesignal.com/api/v1/players',
{
app_id: '42e07e75-a735-4351-af0b-912a47831a5a',
identifier: token,
device_type: Platform.OS === 'ios' ? 0 : 1,
test_type: 1,
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
if (data?.id) {
this.oneSignalId = data.id
}
}
public async saveUser() { public async saveUser() {
const deviceUuid = await DeviceInfoService.getDeviceUniqueId() setTimeout(async () => {
const deviceUuid = await DeviceInfoService.getDeviceUniqueId()
saveUserDeviceReq({
deviceUuid, saveUserDeviceReq({
notificationUserId: this.oneSignalId, deviceUuid,
isVoip: true, notificationUserId: this.voipToken,
}) isVoip: true,
})
}, 150)
} }
} }

11
src/shared/db/index.ts

@ -34,7 +34,7 @@ const migrations = [
versionMigrations, versionMigrations,
taskMigrations, taskMigrations,
chatMigrations, chatMigrations,
chatMessageMigrations chatMessageMigrations,
] ]
const runMigrations = async () => { const runMigrations = async () => {
@ -43,9 +43,7 @@ const runMigrations = async () => {
db.executeSql( db.executeSql(
item.raw, item.raw,
[], [],
(tx, results) => { (tx, results) => {},
console.log('Migrations success - ' + item.name)
},
error => { error => {
Alert.alert('Error on migrations', JSON.stringify(error)) Alert.alert('Error on migrations', JSON.stringify(error))
console.log(error) console.log(error)
@ -86,8 +84,3 @@ runMigrations()
// dropTable(DbTableKey.Tasks) // dropTable(DbTableKey.Tasks)
// dropTable(DbTableKey.Chats) // dropTable(DbTableKey.Chats)
// dropTable(DbTableKey.ChatMessages) // dropTable(DbTableKey.ChatMessages)

2
src/shared/events/index.ts

@ -224,7 +224,7 @@ export type SocketEvents = {
'call/new': { 'call/new': {
callerId: number callerId: number
rtcMessage: any rtcMessage: any
callId: number callId: string
from: { from: {
type: 'personal' type: 'personal'
title: string title: string

2
src/shared/helpers/chat-message-preview-text.helper.ts

@ -3,7 +3,7 @@ import { MessageType } from '../enums'
const regexToMatch = /@\[([^[]*)]\(([^(^)]*)\)/g const regexToMatch = /@\[([^[]*)]\(([^(^)]*)\)/g
const parseMentionsMessage = (message: string, regex: RegExp): string => { const parseMentionsMessage = (message: string, regex: RegExp): string => {
const subst = ` @$1 ` const subst = ` @$1 `
const result = message.replace(regex, subst) const result = message?.replace(regex, subst)
return result return result
} }

Loading…
Cancel
Save