Browse Source

FEATURE | Clls

master
Vitalik 9 months ago
parent
commit
c963264efa
  1. 8
      .env.stage
  2. 67
      .eslintrc.js
  3. 18
      .prettierrc.js
  4. 2
      android/app/build.gradle
  5. 1
      ios/Podfile
  6. 18
      ios/Podfile.lock
  7. 6
      ios/taskme2.xcodeproj/project.pbxproj
  8. 13216
      package-lock.json
  9. 3
      package.json
  10. 14
      src/api/calls/requests.interfaces.ts
  11. 18
      src/api/calls/requests.ts
  12. 4
      src/config/index.ts
  13. 108
      src/modules/calls/components/call-background.component.tsx
  14. 85
      src/modules/calls/components/call-btn.component.tsx
  15. 2
      src/modules/calls/components/index.ts
  16. 11
      src/modules/calls/configs/ice-servers.config.ts
  17. 2
      src/modules/calls/configs/index.ts
  18. 2
      src/modules/calls/hooks/index.ts
  19. 167
      src/modules/calls/hooks/use-call-data.hook.ts
  20. 21
      src/modules/calls/hooks/use-call-from.hook.ts
  21. 108
      src/modules/calls/screens/call/atoms/calling.atom.tsx
  22. 26
      src/modules/calls/screens/call/atoms/income-call.atom.tsx
  23. 3
      src/modules/calls/screens/call/atoms/index.ts
  24. 11
      src/modules/calls/screens/call/atoms/outgoing-call.atom.tsx
  25. 45
      src/modules/calls/screens/call/atoms/temporary.atom.tsx
  26. 1
      src/modules/calls/screens/call/hooks/use-call-sockets.hook.ts
  27. 102
      src/modules/calls/screens/call/index.tsx
  28. 128
      src/modules/calls/widgets/incoming-call.widget.tsx
  29. 1
      src/modules/calls/widgets/index.ts
  30. 2
      src/modules/root/index.tsx
  31. 3
      src/modules/root/navigation-groups/tab-bar.group.tsx
  32. 3
      src/modules/root/navigation-groups/users.group.tsx
  33. 11
      src/services/system/real-time.service.ts
  34. 11
      src/shared/components/elements/txt.component.tsx
  35. 2
      src/shared/enums/route-key.enum.ts
  36. 17
      src/shared/events/index.ts

8
.env.stage

@ -1,3 +1,7 @@
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
API_URL=http://localhost:3000
SOCKET_URL=http://localhost:3000
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27

67
.eslintrc.js

@ -1,10 +1,69 @@
module.exports = { module.exports = {
root: true, root: true,
extends: '@react-native-community', parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint', 'react', 'react-native'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-native/all',
'@react-native',
],
rules: { rules: {
'no-console': 'off',
'react-native/no-inline-styles': 0,
'react-native/split-platform-components': 2,
'react-native/no-raw-text': 0,
'react-native/no-single-element-style-arrays': 2,
'react-native/no-unused-styles': 'off',
'react/jsx-key': 'off',
'react/no-children-prop': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-raw-text': 'off',
'@typescript-eslint/no-empty-interface': 2,
'@typescript-eslint/no-empty-function': 1,
'react-hooks/exhaustive-deps': 0,
'prettier/prettier': 1,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/ban-types': [
'off',
{
extendDefaults: true,
types: {
'{}': false,
},
},
],
semi: 0, semi: 0,
curly: 0,
'no-shadow': 'off',
'react-hooks/exhaustive-deps': 'off',
}, },
overrides: [
{
files: ['*.ts', '*.mts', '*.cts', '*.tsx'],
rules: {
'no-undef': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'react-native/no-color-literals': 'off',
'react-native/sort-styles': 'off',
'no-mixed-spaces-and-tabs': 'off',
'@typescript-eslint/no-explicit-any': 0,
'react/no-unstable-nested-components': 0,
'react-hooks/rules-of-hooks': 0,
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 0,
},
},
],
settings: {
react: {
version: 'detect',
},
},
ignorePatterns: ['.eslintrc.js'],
} }

18
.prettierrc.js

@ -1,10 +1,10 @@
module.exports = { module.exports = {
bracketSpacing: true, bracketSpacing: true,
jsxBracketSameLine: true, bracketSameLine: true,
singleQuote: true, singleQuote: true,
trailingComma: 'all', trailingComma: 'all',
arrowParens: 'avoid', arrowParens: 'avoid',
tabWidth: 4, tabWidth: 4,
useTabs: true, useTabs: true,
semi: false, semi: false,
}; }

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 217 versionCode 221
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,6 +31,7 @@ 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()

18
ios/Podfile.lock

@ -77,7 +77,7 @@ PODS:
- hermes-engine (0.72.10): - hermes-engine (0.72.10):
- hermes-engine/Pre-built (= 0.72.10) - hermes-engine/Pre-built (= 0.72.10)
- hermes-engine/Pre-built (0.72.10) - hermes-engine/Pre-built (0.72.10)
- JitsiWebRTC (111.0.2) - JitsiWebRTC (118.0.0)
- libevent (2.1.12) - libevent (2.1.12)
- libwebp (1.3.2): - libwebp (1.3.2):
- libwebp/demux (= 1.3.2) - libwebp/demux (= 1.3.2)
@ -442,8 +442,8 @@ PODS:
- react-native-video/Video (= 5.2.1) - react-native-video/Video (= 5.2.1)
- react-native-video/Video (5.2.1): - react-native-video/Video (5.2.1):
- React-Core - React-Core
- react-native-webrtc (111.0.6): - react-native-webrtc (118.0.2):
- JitsiWebRTC (~> 111.0.0) - JitsiWebRTC (~> 118.0.0)
- React-Core - React-Core
- react-native-webview (13.8.1): - react-native-webview (13.8.1):
- RCT-Folly (= 2021.07.22.00) - RCT-Folly (= 2021.07.22.00)
@ -560,6 +560,8 @@ 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):
@ -716,6 +718,7 @@ 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`)"
@ -880,6 +883,8 @@ 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:
@ -944,7 +949,7 @@ SPEC CHECKSUMS:
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 90e4033deb00bee33330a9f15eff0f874bd82f6d hermes-engine: 90e4033deb00bee33330a9f15eff0f874bd82f6d
JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b JitsiWebRTC: 3a41671ef65a51d7204323814b055a2690b921c7
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135 OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135
@ -980,7 +985,7 @@ SPEC CHECKSUMS:
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253
react-native-webrtc: 255a1172fd31525b952b36aef7b8e9a41de325e5 react-native-webrtc: a0a8a1730b6cc5a5bda8a6e2166a74c9b78029e2
react-native-webview: bdc091de8cf7f8397653e30182efcd9f772e03b3 react-native-webview: bdc091de8cf7f8397653e30182efcd9f772e03b3
React-NativeModulesApple: c3e696ff867e4bc212266cbdf7e862e48a0166fd React-NativeModulesApple: c3e696ff867e4bc212266cbdf7e862e48a0166fd
React-perflogger: 43287389ea08993c300897a46f95cfac04bb6c1a React-perflogger: 43287389ea08993c300897a46f95cfac04bb6c1a
@ -1000,6 +1005,7 @@ 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
@ -1028,6 +1034,6 @@ SPEC CHECKSUMS:
Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3 Yoga: d0003f849d2b5224c072cef6568b540d8bb15cd3
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: f9f0683c1738b3e88b8940a4c7885412b08fb771 PODFILE CHECKSUM: 12feabb02fdabf7d6322d9ef0cd90fefc37bdafa
COCOAPODS: 1.15.2 COCOAPODS: 1.15.2

6
ios/taskme2.xcodeproj/project.pbxproj

@ -726,7 +726,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 = 14; CURRENT_PROJECT_VERSION = 17;
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 +905,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 = 14; CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = taskme2/Info.plist; INFOPLIST_FILE = taskme2/Info.plist;
@ -942,7 +942,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 = 14; CURRENT_PROJECT_VERSION = 17;
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 ;)";

13216
package-lock.json generated

File diff suppressed because it is too large Load Diff

3
package.json

@ -72,6 +72,7 @@
"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",
@ -97,7 +98,7 @@
"react-native-video": "^5.2.1", "react-native-video": "^5.2.1",
"react-native-video-controls": "^2.8.1", "react-native-video-controls": "^2.8.1",
"react-native-view-pdf": "^0.14.0", "react-native-view-pdf": "^0.14.0",
"react-native-webrtc": "^111.0.1", "react-native-webrtc": "^118.0.2",
"react-native-webview": "^13.8.1", "react-native-webview": "^13.8.1",
"react-native-wheel-pick": "^1.2.2", "react-native-wheel-pick": "^1.2.2",
"react-redux": "^8.1.2", "react-redux": "^8.1.2",

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

@ -0,0 +1,14 @@
export interface IStartCallPayload {
targetUserId: number
rtcMessage: any
}
export interface IAnswerCallPayload {
callId: number
rtcMessage: any
}
export interface IIceCandidatePayload {
targetUserId: number
rtcMessage: any
}

18
src/api/calls/requests.ts

@ -0,0 +1,18 @@
import api from '../http.service'
import {
IAnswerCallPayload,
IIceCandidatePayload,
IStartCallPayload,
} from './requests.interfaces'
export const startCallReq = (payload: IStartCallPayload) => {
return api.post('calls/start', payload, {}, '')
}
export const answerCallReq = (payload: IAnswerCallPayload) => {
return api.post('calls/answer', payload, {}, '')
}
export const iceCandidateReq = (payload: IIceCandidatePayload) => {
return api.post('calls/iceCandidate', payload, {}, '')
}

4
src/config/index.ts

@ -2,8 +2,8 @@ import Config from 'react-native-config'
import { fonts } from './fonts' import { fonts } from './fonts'
export const dynamicConfig = { export const dynamicConfig = {
baseUrl: Config.API_URL, baseUrl: 'http://192.168.0.150:3000', // Config.API_URL,
socketUrl: Config.SOCKET_URL, socketUrl: 'http://192.168.0.150:3000', //Config.SOCKET_URL,
oneSignalKey: Config.ONE_SIGNAL_KEY, oneSignalKey: Config.ONE_SIGNAL_KEY,
appStoreUrl: 'https://apps.apple.com/ua/app/task-me/id1482240685?l=uk', appStoreUrl: 'https://apps.apple.com/ua/app/task-me/id1482240685?l=uk',
googlePlayUrl: googlePlayUrl:

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

@ -0,0 +1,108 @@
import { $size, Txt, useTheme } from '@/shared'
import { PartialTheme } from '@/shared/themes/interfaces'
import React, { FC, PropsWithChildren } from 'react'
import { Image, StyleSheet } from 'react-native'
import { View } from 'react-native'
interface IProps {
avatarImageUrl: string
title: string
subtitle?: string
}
export const CallBackground: FC<PropsWithChildren<IProps>> = ({
avatarImageUrl,
title,
subtitle,
children,
}) => {
const { styles } = useTheme(createStyles)
const renderImg = () => {
if (avatarImageUrl) {
return (
<Image
source={{
uri: avatarImageUrl,
}}
resizeMode="cover"
style={styles.imgBg}
/>
)
}
}
console.log('avatarImageUrl', avatarImageUrl)
return (
<View style={styles.container}>
{renderImg()}
<View style={styles.txtPreviewContent}>
{avatarImageUrl ? (
<View style={styles.txtPreviewBlock}>
<Txt style={styles.txtPreview}>{title[0]}</Txt>
</View>
) : null}
</View>
<View style={styles.content}>
<Txt style={styles.title}>{title}</Txt>
{subtitle ? (
<Txt style={styles.subtitle}>{subtitle}</Txt>
) : null}
{children}
</View>
</View>
)
}
const createStyles = (theme: PartialTheme) =>
StyleSheet.create({
container: {
flex: 1,
position: 'relative',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#414143',
},
imgBg: {
position: 'absolute',
top: 0,
height: '100%',
width: '100%',
left: 0,
},
content: {
paddingBottom: 100,
justifyContent: 'flex-start',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,.3)',
paddingTop: 20,
width: '100%',
},
title: {
fontSize: $size(30),
color: '#fff',
marginBottom: $size(30),
},
subtitle: {
color: '#fff',
marginBottom: $size(30),
},
txtPreviewBlock: {
width: $size(130),
height: $size(130),
borderRadius: 100,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#DE253B',
},
txtPreviewContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
txtPreview: {
fontSize: $size(42),
color: '#fff',
},
})

85
src/modules/calls/components/call-btn.component.tsx

@ -0,0 +1,85 @@
import { $size, IconComponent } from '@/shared'
import React, { FC, useEffect, useState } from 'react'
import {
Animated,
Easing,
StyleSheet,
TouchableOpacity,
ViewStyle,
} from 'react-native'
interface IProps {
iconName: string
onPress: () => void
bgColor: string
style?: ViewStyle
isAnimated?: boolean
}
export const CallBtn: FC<IProps> = ({
iconName,
onPress,
bgColor = 'rgba(255,255,255,.2)',
style,
isAnimated = true,
}) => {
const [animation] = useState(new Animated.Value(0))
useEffect(() => {
const startAnimation = () => {
Animated.sequence([
Animated.timing(animation, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: false,
}),
Animated.timing(animation, {
toValue: 0,
duration: 1000,
easing: Easing.linear,
useNativeDriver: false,
}),
]).start(() => startAnimation())
}
if (isAnimated) {
startAnimation()
return () => {
animation.stopAnimation()
}
}
}, [isAnimated])
const animatedStyle = {
transform: [
{
scale: animation.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.2],
}),
},
],
}
return (
<Animated.View style={animatedStyle}>
<TouchableOpacity
onPress={onPress}
style={[styles.btn, { backgroundColor: bgColor }, style]}>
<IconComponent name={iconName} size={20} color="#fff" />
</TouchableOpacity>
</Animated.View>
)
}
const styles = StyleSheet.create({
btn: {
width: $size(50),
height: $size(50),
borderRadius: 100,
justifyContent: 'center',
alignItems: 'center',
},
})

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

@ -1 +1,3 @@
export * from './call-background.component'
export * from './call-btn.component'
export * from './call-row-card.component' export * from './call-row-card.component'

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

@ -0,0 +1,11 @@
export const iceServers = [
{
urls: 'stun:stun.l.google.com:19302',
},
{
urls: 'stun:stun1.l.google.com:19302',
},
{
urls: 'stun:stun2.l.google.com:19302',
},
]

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

@ -0,0 +1,2 @@
export * from './calls.config'
export * from './ice-servers.config'

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

@ -0,0 +1,2 @@
export * from './use-call-data.hook'
export * from './use-call-from.hook'

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

@ -0,0 +1,167 @@
import {
MediaStream,
RTCPeerConnection,
RTCSessionDescription,
} from 'react-native-webrtc'
import { create } from 'zustand'
import { iceServers } from '../configs'
import inCallManager from 'react-native-incall-manager'
import { answerCallReq, iceCandidateReq } from '@/api/calls/requests'
import { Alert } from 'react-native'
export enum CallMod {
Temporary,
Incoming,
Outgoing,
Speaking,
}
interface CallDataStore {
peerConnection: RTCPeerConnection
mod: CallMod
targetUserId?: number
callId?: number
remoteRTCMessage?: any
remoteStream?: any
icecandidates: any[]
changeMod: (mod: CallMod) => void
startCall: (targetUserId: number) => void
incomeCall: (
targetUserId: number,
remoteRTCMessage: any,
callId: number,
) => void
setRemoteStream: (stream: any) => void
addIcecanidate: (item: any) => void
cleanIcecandidates: () => void
}
export const useCallDataStore = create<CallDataStore>()(set => ({
peerConnection: null,
mod: null,
targetUserId: null,
callId: null,
remoteRTCMessage: null,
icecandidates: [],
changeMod: mod => {
set({ mod })
},
startCall(targetUserId) {
set({
targetUserId,
peerConnection: createPeerConnection(),
mod: CallMod.Outgoing,
})
},
incomeCall(targetUserId, remoteRTCMessage, callId) {
set({
targetUserId,
callId,
remoteRTCMessage,
mod: CallMod.Incoming,
peerConnection: createPeerConnection(),
})
},
setRemoteStream(stream) {
set({ remoteStream: stream })
},
addIcecanidate(item) {
set(prev => ({ icecandidates: [...prev.icecandidates, item] }))
},
cleanIcecandidates() {
set({ icecandidates: [] })
},
}))
export const callDataStoreHelper = {
peerConnection: () => useCallDataStore.getState().peerConnection,
processAccept: async () => {
try {
useCallDataStore.getState().changeMod(CallMod.Speaking)
inCallManager.stopRingtone()
callDataStoreHelper
.peerConnection()
.setRemoteDescription(
new RTCSessionDescription(
useCallDataStore.getState().remoteRTCMessage,
),
)
const sessionDescription = await callDataStoreHelper
.peerConnection()
.createAnswer()
await callDataStoreHelper
.peerConnection()
.setLocalDescription(sessionDescription)
await answerCallReq({
callId: useCallDataStore.getState().callId,
rtcMessage: sessionDescription,
})
const existIcecandidates = useCallDataStore.getState().icecandidates
if (existIcecandidates.length) {
existIcecandidates.map(candidate =>
callDataStoreHelper
.peerConnection()
.addIceCandidate(candidate),
)
useCallDataStore.getState().cleanIcecandidates()
}
} catch (e) {
console.log(e)
}
},
}
function createPeerConnection() {
const peerConnection = new RTCPeerConnection({
iceServers: iceServers,
})
peerConnection.addEventListener('track', event => {
try {
console.log('accepted track', event.track.id)
const existremoteStream = useCallDataStore.getState().remoteStream
const remoteMediaStream = existremoteStream || new MediaStream()
remoteMediaStream.addTrack(event.track, remoteMediaStream)
useCallDataStore.getState().setRemoteStream(remoteMediaStream)
Alert.alert('track cyka')
} catch (e) {
console.log(e)
}
})
peerConnection.addEventListener('icecandidate', event => {
if (!event.candidate) {
return
}
iceCandidateReq({
targetUserId: useCallDataStore.getState().targetUserId,
rtcMessage: {
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate,
},
})
})
peerConnection.addEventListener('iceconnectionstatechange', event => {
console.log(peerConnection.iceConnectionState)
})
peerConnection.addEventListener('icecandidateerror', event => {
// You can ignore some candidate errors.
// Connections can still be made even when errors occur.
console.log(event)
})
return peerConnection
}

21
src/modules/calls/hooks/use-call-from.hook.ts

@ -0,0 +1,21 @@
import { create } from 'zustand'
interface CallFromStore {
title: string
avatarImageUrl?: string
put(title: string, avatarImageUrl?: string): void
clear(): void
}
export const useCallFromStore = create<CallFromStore>()(set => ({
title: '',
avatarImageUrl: null,
put(title, avatarImageUrl) {
set({ title, avatarImageUrl })
},
clear() {
set({ title: null, avatarImageUrl: null })
},
}))

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

@ -0,0 +1,108 @@
import { Txt } from '@/shared'
import React, { FC } from 'react'
import { View } from 'react-native'
import { RTCView } from 'react-native-webrtc'
interface IProps {
localStream: any
remoteStream: any
}
export const CallingAtom: FC<IProps> = ({ localStream, remoteStream }) => {
return (
<View
style={{
flex: 1,
backgroundColor: '#050A0E',
paddingHorizontal: 12,
paddingVertical: 12,
}}>
{localStream ? (
<RTCView
objectFit={'cover'}
style={{
backgroundColor: '#050A0E',
width: 100,
height: 100,
}}
streamURL={localStream.toURL()}
/>
) : null}
{remoteStream ? (
<RTCView
objectFit={'cover'}
style={{
flex: 1,
backgroundColor: 'blue',
marginTop: 8,
}}
streamURL={remoteStream.toURL()}
/>
) : null}
{/* <View
style={{
marginVertical: 12,
flexDirection: 'row',
justifyContent: 'space-evenly',
}}>
<IconContainer
backgroundColor={'red'}
onPress={() => {
leave()
}}
Icon={() => {
return <CallEnd height={26} width={26} fill="#FFF" />
}}
/>
<IconContainer
style={{
borderWidth: 1.5,
borderColor: '#2B3034',
}}
backgroundColor={!localMicOn ? '#fff' : 'transparent'}
onPress={() => {
toggleMic()
}}
Icon={() => {
return localMicOn ? (
<MicOn height={24} width={24} fill="#FFF" />
) : (
<MicOff height={28} width={28} fill="#1D2939" />
)
}}
/>
<IconContainer
style={{
borderWidth: 1.5,
borderColor: '#2B3034',
}}
backgroundColor={!localWebcamOn ? '#fff' : 'transparent'}
onPress={() => {
toggleCamera()
}}
Icon={() => {
return localWebcamOn ? (
<VideoOn height={24} width={24} fill="#FFF" />
) : (
<VideoOff height={36} width={36} fill="#1D2939" />
)
}}
/>
<IconContainer
style={{
borderWidth: 1.5,
borderColor: '#2B3034',
}}
backgroundColor={'transparent'}
onPress={() => {
switchCamera()
}}
Icon={() => {
return (
<CameraSwitch height={24} width={24} fill="#FFF" />
)
}}
/>
</View> */}
</View>
)
}

26
src/modules/calls/screens/call/atoms/income-call.atom.tsx

@ -0,0 +1,26 @@
import { Button, Txt } from '@/shared'
import React, { FC } from 'react'
import { StyleSheet, View } from 'react-native'
interface IProps {
onPressAnswer: () => void
}
export const IncomingCallAtom: FC<IProps> = ({ onPressAnswer }) => {
return (
<View style={styles.container}>
<Txt>Incoming call</Txt>
<Button title="Answer" onPress={onPressAnswer} />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
paddingHorizontal: 20,
},
})

3
src/modules/calls/screens/call/atoms/index.ts

@ -0,0 +1,3 @@
export * from './calling.atom'
export * from './income-call.atom'
export * from './outgoing-call.atom'

11
src/modules/calls/screens/call/atoms/outgoing-call.atom.tsx

@ -0,0 +1,11 @@
import { Txt } from '@/shared'
import React from 'react'
import { View } from 'react-native'
export const OutgoingCallAtom = () => {
return (
<View>
<Txt>Outgoing call</Txt>
</View>
)
}

45
src/modules/calls/screens/call/atoms/temporary.atom.tsx

@ -0,0 +1,45 @@
import { Button, Txt } from '@/shared'
import { selectAccount } from '@/store/account'
import React, { FC } from 'react'
import { StyleSheet, TextInput, View } from 'react-native'
import { useSelector } from 'react-redux'
interface Props {
id: string
onChange: (id: string) => void
onPressCall: () => void
}
export const TemporaryAtom: FC<Props> = ({ id, onChange, onPressCall }) => {
const account = useSelector(selectAccount)
return (
<View style={styles.container}>
<TextInput
style={styles.input}
value={id}
onChangeText={id => onChange(id)}
/>
<Button title="Call" onPress={onPressCall} />
<Txt>My id: {String(account?.id)}</Txt>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
paddingHorizontal: 20,
},
input: {
borderWidth: 1,
width: 200,
height: 60,
marginBottom: 20,
},
button: {},
})

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

@ -0,0 +1 @@
export const useCallSockets = () => {}

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

@ -0,0 +1,102 @@
import React, { useEffect, useRef, useState } from 'react'
import { CallingAtom, OutgoingCallAtom } from './atoms'
import { useSocketListener } from '@/shared'
import { mediaDevices, RTCSessionDescription } from 'react-native-webrtc'
import { startCallReq } from '@/api/calls/requests'
import { CallMod, callDataStoreHelper, useCallDataStore } from '../../hooks'
export const CallScreen = () => {
const mod = useCallDataStore(s => s.mod)
const changeMod = useCallDataStore(s => s.changeMod)
const remoteStream = useCallDataStore(s => s.remoteStream)
const [localStream, setlocalStream] = useState(null)
console.log('remoteStream', remoteStream)
useSocketListener(
'call/answered',
data => {
console.log('ansewerd', data.rtcMessage)
callDataStoreHelper
.peerConnection()
.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
changeMod(CallMod.Speaking)
},
[],
)
const initMediaDevices = async () => {
const stream = await mediaDevices.getUserMedia({
audio: true,
video: {
frameRate: 30,
facingMode: 'user',
},
})
setlocalStream(stream)
stream.getTracks().forEach(track => {
console.log('sendedn track', track.id)
callDataStoreHelper
.peerConnection()
.addTrack(track as any, stream as any)
console.log('Track was sent')
})
}
useEffect(() => {
initMediaDevices()
if (!mod) {
useCallDataStore.getState().startCall(40)
setTimeout(() => {
processCall()
}, 1000)
}
}, [])
// useEffect(() => {
// if (mod === CallMod.Speaking) {
// initMediaDevices()
// setTimeout(() => {
// initMediaDevices()
// }, 1000)
// }
// }, [mod])
async function processCall() {
const sessionDescription = await callDataStoreHelper
.peerConnection()
.createOffer({
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
VoiceActivityDetection: true,
},
})
await callDataStoreHelper
.peerConnection()
.setLocalDescription(sessionDescription)
await startCallReq({
targetUserId: useCallDataStore.getState().targetUserId,
rtcMessage: sessionDescription,
})
}
const templates = {
[CallMod.Speaking]: (
<CallingAtom
localStream={localStream}
remoteStream={remoteStream}
/>
),
[CallMod.Outgoing]: <OutgoingCallAtom />,
[CallMod.Temporary]: null,
}
return templates[mod]
}

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

@ -0,0 +1,128 @@
import {
$size,
RouteKey,
Txt,
useNav,
useSocketListener,
useTheme,
} from '@/shared'
import React, { useState } from 'react'
import { Alert, Dimensions, Modal, StyleSheet, View } from 'react-native'
import {
callDataStoreHelper,
useCallDataStore,
useCallFromStore,
} from '../hooks'
import inCallManager from 'react-native-incall-manager'
import { PartialTheme } from '@/shared/themes/interfaces'
import { CallBackground, CallBtn } from '../components'
import { NavigationService } from '@/services/system'
import { RTCIceCandidate } from 'react-native-webrtc'
export const IncomingCallWidget = () => {
const [isVisible, setVisible] = useState(false)
const { title, avatarImageUrl } = useCallFromStore()
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)
useCallDataStore
.getState()
.incomeCall(data.callerId, data.rtcMessage, data.callId)
} catch (e) {
console.log(e)
}
},
[setVisible],
)
useSocketListener('call/ICEcandidate', data => {
try {
const message = data.rtcMessage
const peerConnection = callDataStoreHelper.peerConnection()
const candidate = new RTCIceCandidate({
candidate: message.candidate,
sdpMid: message.id,
sdpMLineIndex: message.label,
})
if (peerConnection) {
peerConnection.addIceCandidate(candidate)
} else {
useCallDataStore.getState().addIcecanidate(candidate)
}
} catch (e) {
console.log(e)
}
})
const decline = () => {}
const accept = () => {
inCallManager.stopRingtone()
callDataStoreHelper.processAccept()
setTimeout(() => {
NavigationService.navigate(RouteKey.Call, {})
}, 1000)
setVisible(false)
}
return (
<Modal
presentationStyle="overFullScreen"
visible={isVisible}
statusBarTranslucent={true}
animationType="none"
onRequestClose={() => setVisible(false)}
style={{
backgroundColor: 'black',
margin: 0,
height: Dimensions.get('screen').height,
}}>
<View style={styles.container}>
<CallBackground
title={title}
avatarImageUrl={avatarImageUrl}
subtitle="Телефоннє">
<View style={styles.row}>
<CallBtn
iconName="phone-2"
bgColor="#DE253B"
onPress={decline}
/>
<CallBtn
iconName="phone-2"
bgColor="#38B362"
onPress={accept}
/>
</View>
</CallBackground>
</View>
</Modal>
)
}
const createStyles = (theme: PartialTheme) =>
StyleSheet.create({
container: {
flex: 1,
},
row: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
width: '100%',
},
})

1
src/modules/calls/widgets/index.ts

@ -0,0 +1 @@
export * from './incoming-call.widget'

2
src/modules/root/index.tsx

@ -39,6 +39,7 @@ import { StyleSheet } from 'react-native'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import { useAppBadge, useAppSocketListener, useNetConnect } from './hooks' import { useAppBadge, useAppSocketListener, useNetConnect } from './hooks'
import { PreviewFileModal, LoaderModal } from '@/shared/components/modals' import { PreviewFileModal, LoaderModal } from '@/shared/components/modals'
import { IncomingCallWidget } from '../calls/widgets'
export const Navigation: FC = () => { export const Navigation: FC = () => {
const activeModule = useSelector(selectActiveNavigationModule) const activeModule = useSelector(selectActiveNavigationModule)
@ -96,6 +97,7 @@ export const Navigation: FC = () => {
<SelectTaxonomiesModalSmart /> <SelectTaxonomiesModalSmart />
<RecordAudioModalSmart /> <RecordAudioModalSmart />
<ChatSendImgModal /> <ChatSendImgModal />
<IncomingCallWidget />
</> </>
) )
} }

3
src/modules/root/navigation-groups/tab-bar.group.tsx

@ -11,6 +11,7 @@ import { SettingsScreen } from '@/modules/settings'
import { AddUpdateTaskScreen } from '@/modules/tasks' import { AddUpdateTaskScreen } from '@/modules/tasks'
import { Comments } from '@/modules/comments/screens/comments.screen' import { Comments } from '@/modules/comments/screens/comments.screen'
import { HomeGroup } from './home.group' import { HomeGroup } from './home.group'
import { CallScreen } from '@/modules/calls/screens/call'
const Tab = createBottomTabNavigator() const Tab = createBottomTabNavigator()
@ -39,7 +40,7 @@ export const TabNavigator: FC = () => {
name={RouteKey.AddTask} name={RouteKey.AddTask}
component={AddUpdateTaskScreen} component={AddUpdateTaskScreen}
/> />
<Tab.Screen name={RouteKey.Contacts} component={ContactScreen} /> <Tab.Screen name={RouteKey.Contacts} component={CallScreen} />
<Tab.Screen name={RouteKey.Settings} component={SettingsScreen} /> <Tab.Screen name={RouteKey.Settings} component={SettingsScreen} />
</Tab.Navigator> </Tab.Navigator>
) )

3
src/modules/root/navigation-groups/users.group.tsx

@ -27,6 +27,7 @@ import {
GroupChatDetailScreen, GroupChatDetailScreen,
SendSharedFilesScreen, SendSharedFilesScreen,
} from '@/modules/chats' } from '@/modules/chats'
import { CallScreen } from '@/modules/calls/screens/call'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
@ -138,5 +139,7 @@ export const UsersGroup: FC = () => (
{/* --- Executors --- */} {/* --- Executors --- */}
<Stack.Screen name={RouteKey.Executors} component={ExecutorsScreen} /> <Stack.Screen name={RouteKey.Executors} component={ExecutorsScreen} />
<Stack.Screen name={RouteKey.Call} component={CallScreen} />
</Stack.Navigator> </Stack.Navigator>
) )

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

@ -3,6 +3,7 @@ import { socketEvents } from '@/shared/events'
import io from 'socket.io-client' import io from 'socket.io-client'
import { authService } from '../domain' import { authService } from '../domain'
import { GlobalContainerService } from './global-container.service' import { GlobalContainerService } from './global-container.service'
import { Alert } from 'react-native'
const store = () => GlobalContainerService.get('store') const store = () => GlobalContainerService.get('store')
@ -17,17 +18,19 @@ export class SocketIo {
secure: true, secure: true,
reconnectionAttempts: 10, reconnectionAttempts: 10,
auth(cb) { auth(cb) {
const token = store()?.getState()?.auth?.accessToken
console.log('token', token)
cb({ accessToken: store()?.getState()?.auth?.accessToken }) cb({ accessToken: store()?.getState()?.auth?.accessToken })
}, },
}) })
// this._on('connect', () => this.emit('join-user'))
// this.initSockets() // this.initSockets()
this._on('connect', () => { this._on('connect', () => {
console.log('CONNECT') console.log('CONNECT')
this.initSockets() this.initSockets()
}) })
} catch (e) { } catch (e) {
console.log('ERROR') console.log('----------------------------------- ERROR', e)
} }
} }
@ -116,6 +119,10 @@ export class SocketIo {
this._onSocketSendEvent('version') this._onSocketSendEvent('version')
this._onSocketSendEvent('user/change-permissions') this._onSocketSendEvent('user/change-permissions')
this._onSocketSendEvent('call/answered')
this._onSocketSendEvent('call/new')
this._onSocketSendEvent('call/ICEcandidate')
this._on('error/join-user', async () => { this._on('error/join-user', async () => {
await authService.refreshSession() await authService.refreshSession()
this.emit('join-user') this.emit('join-user')

11
src/shared/components/elements/txt.component.tsx

@ -1,9 +1,9 @@
import React from 'react' import React, { FC, PropsWithChildren } from 'react'
import _ from 'lodash' import _ from 'lodash'
import { Text, TextProps, TextStyle } from 'react-native' import { Text, TextProps, TextStyle } from 'react-native'
import { config } from '@/config' import { config } from '@/config'
import {getFont} from '@/shared/helpers'; import { getFont } from '@/shared/helpers'
interface TxtProps extends TextProps { interface TxtProps extends TextProps {
/** /**
@ -15,14 +15,9 @@ interface TxtProps extends TextProps {
* Default is 400 * Default is 400
*/ */
weight?: '400' | '500' | '600' | '700' weight?: '400' | '500' | '600' | '700'
/**
* Text value
*/
children?: string | number
} }
export const Txt = (props: TxtProps) => { export const Txt: FC<PropsWithChildren<TxtProps>> = props => {
const customStyle: TextStyle = { const customStyle: TextStyle = {
fontFamily: getFont( fontFamily: getFont(
_.defaultTo(props.font, 'Gilroy'), _.defaultTo(props.font, 'Gilroy'),

2
src/shared/enums/route-key.enum.ts

@ -44,4 +44,6 @@ export enum RouteKey {
ConfirmCode = 'ConfirmCode', ConfirmCode = 'ConfirmCode',
NoInternetConnection = 'NoInternetConnection', NoInternetConnection = 'NoInternetConnection',
Call = 'call',
} }

17
src/shared/events/index.ts

@ -220,6 +220,23 @@ export type SocketEvents = {
version: { type: EntityType; entityId: number } version: { type: EntityType; entityId: number }
'chat/update-message': { message: IChatMessage } 'chat/update-message': { message: IChatMessage }
'call/new': {
callerId: number
rtcMessage: any
callId: number
from: {
type: 'personal'
title: string
avatarImageUrl?: string
}
}
'call/answered': {
rtcMessage: any
}
'call/ICEcandidate': {
rtcMessage: any
}
} }
export const appEvents = new Events<AppEvents>() export const appEvents = new Events<AppEvents>()

Loading…
Cancel
Save