Compare commits

...

10 Commits

  1. 2
      .env.stage
  2. 67
      .eslintrc.js
  3. 18
      .prettierrc.js
  4. 2
      android/app/build.gradle
  5. 10
      ios/Podfile.lock
  6. 21
      ios/taskme2.xcodeproj/project.pbxproj
  7. 12951
      package-lock.json
  8. 2
      package.json
  9. 5
      src/App.tsx
  10. 30
      src/api/calls/requests.interfaces.ts
  11. 35
      src/api/calls/requests.ts
  12. 4
      src/config/index.ts
  13. 13
      src/modules/calls/atoms/call-card-info.atom.tsx
  14. 128
      src/modules/calls/components/call-background.component.tsx
  15. 85
      src/modules/calls/components/call-btn.component.tsx
  16. 14
      src/modules/calls/components/call-row-card.component.tsx
  17. 2
      src/modules/calls/components/index.ts
  18. 16
      src/modules/calls/configs/ice-servers.config.ts
  19. 2
      src/modules/calls/configs/index.ts
  20. 62
      src/modules/calls/core/accept-call.ts
  21. 44
      src/modules/calls/core/call-events-listener.ts
  22. 2
      src/modules/calls/core/index.ts
  23. 55
      src/modules/calls/core/start-call.ts
  24. 46
      src/modules/calls/core/stop-call.ts
  25. 1
      src/modules/calls/enums/call-types.enum.ts
  26. 4
      src/modules/calls/hooks/index.ts
  27. 195
      src/modules/calls/hooks/use-call-data.hook.ts
  28. 21
      src/modules/calls/hooks/use-call-from.hook.ts
  29. 47
      src/modules/calls/hooks/use-call-streams.hook.ts
  30. 67
      src/modules/calls/hooks/use-calls-history.hook.ts
  31. 104
      src/modules/calls/screens/call/atoms/calling.atom.tsx
  32. 46
      src/modules/calls/screens/call/atoms/income-call.atom.tsx
  33. 3
      src/modules/calls/screens/call/atoms/index.ts
  34. 47
      src/modules/calls/screens/call/atoms/outgoing-call.atom.tsx
  35. 45
      src/modules/calls/screens/call/atoms/temporary.atom.tsx
  36. 115
      src/modules/calls/screens/call/index.tsx
  37. 37
      src/modules/calls/smart-components/call-swipable-row-card.smart-component.tsx
  38. 113
      src/modules/calls/smart-components/calls-list.smart-component.tsx
  39. 129
      src/modules/calls/widgets/incoming-call.widget.tsx
  40. 1
      src/modules/calls/widgets/index.ts
  41. 5
      src/modules/contacts/screens/contact-detail.screen.tsx
  42. 8
      src/modules/contacts/screens/contacts.screen.tsx
  43. 4
      src/modules/root/index.tsx
  44. 3
      src/modules/root/navigation-groups/users.group.tsx
  45. 4
      src/services/system/navigation.service.ts
  46. 12
      src/services/system/real-time.service.ts
  47. 11
      src/shared/components/elements/txt.component.tsx
  48. 6
      src/shared/enums/call-status.enum.ts
  49. 29
      src/shared/enums/index.ts
  50. 2
      src/shared/enums/route-key.enum.ts
  51. 20
      src/shared/events/index.ts
  52. 17
      src/shared/interfaces/call.inteface.ts
  53. 27
      src/shared/interfaces/index.ts
  54. 3
      tsconfig.json

2
.env.stage

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
API_URL=https://taskme-api.work-jetup.site
SOCKET_URL=https://taskme-api.work-jetup.site
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27

67
.eslintrc.js

@ -1,10 +1,69 @@ @@ -1,10 +1,69 @@
module.exports = {
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: {
'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,
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 @@ @@ -1,10 +1,10 @@
module.exports = {
bracketSpacing: true,
jsxBracketSameLine: true,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
tabWidth: 4,
useTabs: true,
semi: false,
};
bracketSpacing: true,
bracketSameLine: true,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
tabWidth: 4,
useTabs: true,
semi: false,
}

2
android/app/build.gradle

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

10
ios/Podfile.lock

@ -77,7 +77,7 @@ PODS: @@ -77,7 +77,7 @@ PODS:
- hermes-engine (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)
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
@ -442,8 +442,8 @@ PODS: @@ -442,8 +442,8 @@ PODS:
- react-native-video/Video (= 5.2.1)
- react-native-video/Video (5.2.1):
- React-Core
- react-native-webrtc (111.0.6):
- JitsiWebRTC (~> 111.0.0)
- react-native-webrtc (118.0.2):
- JitsiWebRTC (~> 118.0.0)
- React-Core
- react-native-webview (13.8.1):
- RCT-Folly (= 2021.07.22.00)
@ -944,7 +944,7 @@ SPEC CHECKSUMS: @@ -944,7 +944,7 @@ SPEC CHECKSUMS:
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 90e4033deb00bee33330a9f15eff0f874bd82f6d
JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b
JitsiWebRTC: 3a41671ef65a51d7204323814b055a2690b921c7
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135
@ -980,7 +980,7 @@ SPEC CHECKSUMS: @@ -980,7 +980,7 @@ SPEC CHECKSUMS:
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253
react-native-webrtc: 255a1172fd31525b952b36aef7b8e9a41de325e5
react-native-webrtc: a0a8a1730b6cc5a5bda8a6e2166a74c9b78029e2
react-native-webview: bdc091de8cf7f8397653e30182efcd9f772e03b3
React-NativeModulesApple: c3e696ff867e4bc212266cbdf7e862e48a0166fd
React-perflogger: 43287389ea08993c300897a46f95cfac04bb6c1a

21
ios/taskme2.xcodeproj/project.pbxproj

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

12951
package-lock.json generated

File diff suppressed because it is too large Load Diff

2
package.json

@ -97,7 +97,7 @@ @@ -97,7 +97,7 @@
"react-native-video": "^5.2.1",
"react-native-video-controls": "^2.8.1",
"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-wheel-pick": "^1.2.2",
"react-redux": "^8.1.2",

5
src/App.tsx

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

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

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

35
src/api/calls/requests.ts

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
import api from '../http.service'
import {
IAnswerCallPayload,
ICancelCallPayload,
IFinishCallPayload,
IIceCandidatePayload,
IStartCallPayload,
} from './requests.interfaces'
export const startCallReq = (payload: IStartCallPayload) => {
return api.post<any>('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, {}, '')
}
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, '')
}

4
src/config/index.ts

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

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

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

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

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
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
fullComponent?: JSX.Element
showAvatar?: boolean
needOverlay?: boolean
}
export const CallBackground: FC<PropsWithChildren<IProps>> = ({
avatarImageUrl,
title,
subtitle,
children,
fullComponent,
showAvatar = true,
needOverlay = true,
}) => {
const { styles } = useTheme(createStyles)
const renderImg = () => {
if (fullComponent) {
return <View style={styles.fullCimponent}>{fullComponent}</View>
} else if (avatarImageUrl) {
return (
<Image
source={{
uri: avatarImageUrl,
}}
resizeMode="cover"
style={styles.imgBg}
/>
)
}
}
return (
<View style={styles.container}>
{renderImg()}
<View style={styles.txtPreviewContent}>
{showAvatar && !avatarImageUrl ? (
<View style={styles.txtPreviewBlock}>
<Txt style={styles.txtPreview}>{title[0]}</Txt>
</View>
) : null}
</View>
<View
style={[
styles.content,
{ backgroundColor: needOverlay ? 'rgba(0,0,0,.3)' : null },
]}>
<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,
},
fullCimponent: {
position: 'absolute',
top: 0,
height: '100%',
width: '100%',
left: 0,
},
content: {
paddingBottom: 60,
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 @@ @@ -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',
},
})

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

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

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

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

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

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

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

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

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

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
import { CallDataStore, CallMod } from '../hooks'
import { answerCallReq } from '@/api/calls/requests'
import { RTCSessionDescription } from 'react-native-webrtc'
export class AcceptCall {
constructor(private readonly getCallDataStore: () => CallDataStore) {}
private get peerConnection() {
return this.getCallDataStore().peerConnection
}
private get callDataStore() {
return this.getCallDataStore()
}
public async accept() {
try {
this.prepare()
this.setRemoteDescription()
await this.sendAnswer()
this.proccessIceCandidates()
// inCallManager.start({ media: 'video' })
} catch (e) {
console.log(e)
}
}
private prepare() {
this.callDataStore.changeMod(CallMod.Speaking)
// inCallManager.stopRingtone()
}
private setRemoteDescription() {
this.peerConnection.setRemoteDescription(
new RTCSessionDescription(this.callDataStore.remoteRTCMessage),
)
}
private async sendAnswer() {
const sessionDescription = await this.peerConnection.createAnswer()
await this.peerConnection.setLocalDescription(sessionDescription)
await answerCallReq({
callId: this.callDataStore.callId,
rtcMessage: sessionDescription,
})
}
private proccessIceCandidates() {
const existIcecandidates = this.callDataStore.icecandidates
if (existIcecandidates.length) {
existIcecandidates.map(candidate =>
this.peerConnection.addIceCandidate(candidate),
)
this.callDataStore.cleanIcecandidates()
}
}
}

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

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
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()

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

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
export * from './accept-call'
export * from './start-call'

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

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
import { startCallReq } from '@/api/calls/requests'
import { CallDataStore, CallFromStore } from '../hooks'
export class StartCall {
constructor(
private readonly getCallDataStore: () => CallDataStore,
private readonly getCallFromStore: () => CallFromStore,
) {}
private get callDataStore() {
return this.getCallDataStore()
}
private get peerConnection() {
return this.callDataStore.peerConnection
}
public async start() {
const sessionDescription = await this.createOffer()
await this.peerConnection.setLocalDescription(sessionDescription)
const data = await this.sendStartCallReq(sessionDescription)
this.proccessResponse(data)
}
private async createOffer() {
const sessionDescription = await this.peerConnection.createOffer({
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
VoiceActivityDetection: true,
},
})
return sessionDescription
}
private async sendStartCallReq(sessionDescription: any) {
const result = await startCallReq({
targetUserId: this.callDataStore.targetUserId,
rtcMessage: sessionDescription,
})
return result.data
}
private async proccessResponse(data: any) {
const from = data.from
? data.from
: {
title: 'Невідомий користувач',
}
this.getCallFromStore().put(from.title, from.avatarImageUrl)
this.callDataStore.setCallId(data.call.id)
}
}

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

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
import { finishCallReq } from '@/api/calls/requests'
import { CallDataStore, ICallStreamsStore } from '../hooks'
import { MediaStream } from 'react-native-webrtc'
export class StopCall {
constructor(
private readonly getCallDataStore: () => CallDataStore,
private readonly getCallStreamsStore: () => ICallStreamsStore,
) {}
private get peerConnection() {
return this.getCallDataStore().peerConnection
}
private get callDataStore() {
return this.getCallDataStore()
}
public async stop() {
await finishCallReq({
callId: this.callDataStore.callId,
}).catch(console.log)
// inCallManager.stop()
this.peerConnection.close()
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 @@ @@ -1,4 +1,5 @@
export enum CallTypesEnum {
INCOMING = 'incoming',
OUTGOING = 'outgoing',
SKIP = 'skipped',
}

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

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

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

@ -0,0 +1,195 @@ @@ -0,0 +1,195 @@
import { MediaStream, RTCPeerConnection } from 'react-native-webrtc'
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 {
Outgoing,
Speaking,
Finished,
Incoming,
}
export interface CallDataStore {
peerConnection: RTCPeerConnection
mod: CallMod
targetUserId?: number
callId?: number
remoteRTCMessage?: any
icecandidates: any[]
connectedStatus: RTCIceConnectionState
changeMod: (mod: CallMod) => void
startCall: (targetUserId: number, peerConnection: RTCPeerConnection) => void
incomeCall: (
targetUserId: number,
remoteRTCMessage: any,
callId: number,
peerConnection: RTCPeerConnection,
) => void
addIcecanidate: (item: any) => void
cleanIcecandidates: () => void
setConnectedStatus: (connectedStatus: RTCIceConnectionState) => void
setCallId: (callId: number) => void
}
export const useCallDataStore = create<CallDataStore>()(set => ({
peerConnection: null,
mod: null,
targetUserId: null,
callId: null,
remoteRTCMessage: null,
icecandidates: [],
connectedStatus: null,
changeMod: mod => {
set({ mod })
},
setCallId(callId: number) {
set({ callId })
},
startCall(targetUserId, peerConnection) {
set({
targetUserId,
peerConnection,
mod: CallMod.Outgoing,
})
},
incomeCall(targetUserId, remoteRTCMessage, callId, peerConnection) {
set({
targetUserId,
callId,
remoteRTCMessage,
mod: CallMod.Incoming,
peerConnection,
})
},
addIcecanidate(item) {
set(prev => ({ icecandidates: [...prev.icecandidates, item] }))
},
cleanIcecandidates() {
set({ icecandidates: [] })
},
setConnectedStatus(connectedStatus) {
set({ connectedStatus })
},
}))
export const callDataStoreHelper = {
peerConnection: () => useCallDataStore.getState().peerConnection,
proccesIncome: async (data: any) => {
const peerConnection = await createPeerConnection()
useCallDataStore
.getState()
.incomeCall(
data.callerId,
data.rtcMessage,
data.callId,
peerConnection,
)
peerConnection.setRemoteDescription(
new RTCSessionDescription(data.rtcMessage),
)
},
processAccept: async () => {
new AcceptCall(useCallDataStore.getState).accept()
},
processStart: async (targetUserId: number) => {
// InCallManager.start({ media: 'audio', ringback: '_BUNDLE_' })
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
}

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

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
import { create } from 'zustand'
export 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 })
},
}))

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

@ -0,0 +1,47 @@ @@ -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 @@ @@ -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,
}
}

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

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
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: '#000',
}}>
{localStream ? (
<RTCView
objectFit={'cover'}
style={{
backgroundColor: '#050A0E',
width: 100,
height: 100,
}}
streamURL={localStream.toURL()}
/>
) : null}
{remoteStream ? (
<RTCView
objectFit={'cover'}
style={{
flex: 1,
}}
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>
)
}

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

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
import { CallBackground, CallBtn } from '@/modules/calls/components'
import { Button, Txt } from '@/shared'
import React, { FC } from 'react'
import { StyleSheet, View } from 'react-native'
interface IProps {
onPressAnswer: () => void
title: string
avatarImageUrl: string
}
export const IncomingCallAtom: FC<IProps> = ({
onPressAnswer,
title,
avatarImageUrl,
}) => {
return (
<CallBackground
title={title}
avatarImageUrl={avatarImageUrl}
subtitle="Телефоннє">
<View style={styles.row}>
<CallBtn
iconName="phone-2"
bgColor="#38B362"
onPress={onPressAnswer}
/>
</View>
</CallBackground>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
paddingHorizontal: 20,
},
row: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
width: '100%',
},
})

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

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

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

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
import { CallBackground, CallBtn } from '@/modules/calls/components'
import { Button, Txt } from '@/shared'
import React, { FC } from 'react'
import { StyleSheet, View } from 'react-native'
interface IProps {
onPressCancel: () => void
title: string
avatarImageUrl: string
}
export const OutgoingcallAtom: FC<IProps> = ({
onPressCancel,
title,
avatarImageUrl,
}) => {
return (
<CallBackground
title={title}
avatarImageUrl={avatarImageUrl}
subtitle="Виклик">
<View style={styles.row}>
<CallBtn
iconName="phone-2"
bgColor="#DE253B"
onPress={onPressCancel}
isAnimated={false}
/>
</View>
</CallBackground>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
paddingHorizontal: 20,
},
row: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
width: '100%',
},
})

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

@ -0,0 +1,45 @@ @@ -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: {},
})

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

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
import React, { useEffect, useRef, useState } from 'react'
import { CallingAtom, OutgoingcallAtom } from './atoms'
import { Button } from '@/shared'
import {
CallMod,
callDataStoreHelper,
useCallDataStore,
useCallFromStore,
useCallsStream,
} from '../../hooks'
import { CallBackground, CallBtn } from '../../components'
import {
Alert,
Dimensions,
Modal,
StatusBar,
StyleSheet,
View,
} from 'react-native'
export const CallScreen = () => {
const mod = useCallDataStore(s => s.mod)
const { remoteStream, localStream } = useCallsStream()
const { title, avatarImageUrl } = useCallFromStore()
const connectedStatus = useCallDataStore(s => s.connectedStatus)
const stopCall = () => {
callDataStoreHelper.stop()
}
const templates = {
[CallMod.Speaking]: (
<CallBackground
title={title}
avatarImageUrl={avatarImageUrl}
showAvatar={false}
needOverlay={false}
subtitle={connectedStatus}
fullComponent={
<CallingAtom
localStream={localStream}
remoteStream={remoteStream}
/>
}>
<View style={styles.row}>
<CallBtn
iconName="phone-2"
bgColor="#DE253B"
isAnimated={false}
onPress={stopCall}
/>
</View>
</CallBackground>
),
[CallMod.Outgoing]: (
<OutgoingcallAtom
title={title}
onPressCancel={callDataStoreHelper.cancel}
avatarImageUrl={avatarImageUrl}
/>
),
[CallMod.Finished]: (
<CallBackground
title={'Звінок завершенно'}
avatarImageUrl={avatarImageUrl}
needOverlay={false}
showAvatar={false}>
<View style={styles.row}>
<Button
title="Вийти"
onPress={() => callDataStoreHelper.finishCall()}
/>
</View>
</CallBackground>
),
}
return (
<>
<Modal
presentationStyle="overFullScreen"
visible={true}
statusBarTranslucent={true}
animationType="none"
style={{
backgroundColor: 'black',
margin: 0,
height: Dimensions.get('screen').height,
}}>
{templates[mod]}
{/* <Txt style={{ color: 'red', fontSize: 50 }}>
{mod} {connectedStatus}
</Txt> */}
</Modal>
<StatusBar hidden />
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100,
paddingHorizontal: 20,
},
row: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
width: '100%',
paddingHorizontal: 30,
},
})

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

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

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

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

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

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
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)
callDataStoreHelper.proccesIncome(data)
} catch (e) {
console.log(e)
}
},
[setVisible],
)
useSocketListener('call/canceled', data => {
setVisible(false)
callDataStoreHelper.stop()
})
useSocketListener('call/ICEcandidate', data => {
try {
const candidates = data.candidates
const peerConnection = callDataStoreHelper.peerConnection()
candidates.map(it => {
const item = new RTCIceCandidate(it)
if (peerConnection) {
peerConnection.addIceCandidate(item)
} else {
useCallDataStore.getState().addIcecanidate(item)
}
})
} catch (e) {
console.log(e)
}
})
const decline = () => {}
const accept = async () => {
// inCallManager.stopRingtone()
await callDataStoreHelper.processAccept()
NavigationService.navigate(RouteKey.Call, {})
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 @@ @@ -0,0 +1 @@
export * from './incoming-call.widget'

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

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

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

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { $size, IRouteParams, ScreenLayout, SwitchButtons } from '@/shared'
import { $size, ScreenLayout, SwitchButtons } from '@/shared'
import React, { FC, useState } from 'react'
import { StyleSheet } from 'react-native'
import { ContactsSmartList } from '../smart-component'
@ -7,14 +7,12 @@ import { CallSmartList } from '@/modules/calls/smart-components/calls-list.smart @@ -7,14 +7,12 @@ import { CallSmartList } from '@/modules/calls/smart-components/calls-list.smart
import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook'
interface IProps extends IRouteParams {}
type TRoutes = {
key: string
title: string
}[]
export const ContactScreen: FC<IProps> = () => {
export const ContactScreen: FC = () => {
const { styles } = useTheme(createStyles)
const [index, setIndex] = useState<number>(0)
@ -25,7 +23,7 @@ export const ContactScreen: FC<IProps> = () => { @@ -25,7 +23,7 @@ export const ContactScreen: FC<IProps> = () => {
const renderScene = SceneMap({
contacts: ContactsSmartList,
calls: () => <CallSmartList />,
calls: CallSmartList,
})
const renderTabBar = () => (

4
src/modules/root/index.tsx

@ -39,6 +39,8 @@ import { StyleSheet } from 'react-native' @@ -39,6 +39,8 @@ import { StyleSheet } from 'react-native'
import { PartialTheme } from '@/shared/themes/interfaces'
import { useAppBadge, useAppSocketListener, useNetConnect } from './hooks'
import { PreviewFileModal } from '@/shared/components/modals/preview-file-modal.smart-component'
import { IncomingCallWidget } from '../calls/widgets'
import '@/modules/calls/core/call-events-listener'
export const Navigation: FC = () => {
const activeModule = useSelector(selectActiveNavigationModule)
@ -52,7 +54,6 @@ export const Navigation: FC = () => { @@ -52,7 +54,6 @@ export const Navigation: FC = () => {
// const status = 'online'
useSharedFiles()
useAppSocketListener()
useAppBadge()
@ -96,6 +97,7 @@ export const Navigation: FC = () => { @@ -96,6 +97,7 @@ export const Navigation: FC = () => {
<SelectTaxonomiesModalSmart />
<RecordAudioModalSmart />
<ChatSendImgModal />
<IncomingCallWidget />
</>
)
}

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

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

4
src/services/system/navigation.service.ts

@ -12,8 +12,12 @@ export const navigationRef = React.createRef<any>() @@ -12,8 +12,12 @@ export const navigationRef = React.createRef<any>()
export function navigate(name: string, params: any) {
navigationRef.current?.navigate(name, params)
}
export function goBack() {
navigationRef.current?.goBack()
}
export const NavigationService = {
setModule,
goBack,
navigate,
}

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

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

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

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
import React from 'react'
import React, { FC, PropsWithChildren } from 'react'
import _ from 'lodash'
import { Text, TextProps, TextStyle } from 'react-native'
import { config } from '@/config'
import {getFont} from '@/shared/helpers';
import { getFont } from '@/shared/helpers'
interface TxtProps extends TextProps {
/**
@ -15,14 +15,9 @@ interface TxtProps extends TextProps { @@ -15,14 +15,9 @@ interface TxtProps extends TextProps {
* Default is 400
*/
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 = {
fontFamily: getFont(
_.defaultTo(props.font, 'Gilroy'),

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

@ -0,0 +1,6 @@ @@ -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 @@ @@ -1,18 +1,19 @@
export * from './route-key.enum'
export * from './navigation-module-key.enum'
export * from './exception-keys.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 './actions-queue-type.enum'
export * from './btns-type.enum'
export * from './call-status.enum'
export * from './chat-bg.enum'
export * from './task-events.enum'
export * from './notification.enum'
export * from './chat.enums'
export * from './btns-type.enum'
export * from './actions-queue-type.enum'
export * from './entity-type.enum'
export * from './permissions.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'

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

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

20
src/shared/events/index.ts

@ -209,6 +209,26 @@ export type SocketEvents = { @@ -209,6 +209,26 @@ export type SocketEvents = {
version: { type: EntityType; entityId: number }
'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': {
candidates: any[]
}
'call/canceled': {
callId: number
}
}
export const appEvents = new Events<AppEvents>()

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

@ -0,0 +1,17 @@ @@ -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 @@ @@ -1,15 +1,18 @@
export * from './routing.interfaces'
export * from './token-pair.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 './call.inteface'
export * from './chats.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 './options.interfaces'
export * from './notification.interfaces'
export * from './chats.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 @@ @@ -63,7 +63,8 @@
"node_modules",
"babel.config.js",
"metro.config.js",
"jest.config.js"
"jest.config.js",
"taskme"
]
}

Loading…
Cancel
Save