Vitalik
9 months ago
36 changed files with 11434 additions and 2813 deletions
@ -1,3 +1,7 @@
@@ -1,3 +1,7 @@
|
||||
API_URL=https://taskme-api.work-jetup.site |
||||
SOCKET_URL=https://taskme-api.work-jetup.site |
||||
# API_URL=https://taskme-api.work-jetup.site |
||||
# SOCKET_URL=https://taskme-api.work-jetup.site |
||||
# ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 |
||||
|
||||
API_URL=http://localhost:3000 |
||||
SOCKET_URL=http://localhost:3000 |
||||
ONE_SIGNAL_KEY=8b9066f5-8c3f-49f7-bef4-c5ab621f9d27 |
@ -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'], |
||||
} |
||||
|
@ -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, |
||||
} |
||||
|
@ -0,0 +1,14 @@
@@ -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 |
||||
} |
@ -0,0 +1,18 @@
@@ -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, {}, '') |
||||
} |
@ -0,0 +1,108 @@
@@ -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', |
||||
}, |
||||
}) |
@ -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', |
||||
}, |
||||
}) |
@ -1 +1,3 @@
@@ -1 +1,3 @@
|
||||
export * from './call-background.component' |
||||
export * from './call-btn.component' |
||||
export * from './call-row-card.component' |
||||
|
@ -0,0 +1,11 @@
@@ -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', |
||||
}, |
||||
] |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './calls.config' |
||||
export * from './ice-servers.config' |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './use-call-data.hook' |
||||
export * from './use-call-from.hook' |
@ -0,0 +1,167 @@
@@ -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 |
||||
} |
@ -0,0 +1,21 @@
@@ -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 }) |
||||
}, |
||||
})) |
@ -0,0 +1,108 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,26 @@
@@ -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, |
||||
}, |
||||
}) |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from './calling.atom' |
||||
export * from './income-call.atom' |
||||
export * from './outgoing-call.atom' |
@ -0,0 +1,11 @@
@@ -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> |
||||
) |
||||
} |
@ -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: {}, |
||||
}) |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export const useCallSockets = () => {} |
@ -0,0 +1,102 @@
@@ -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] |
||||
} |
@ -0,0 +1,128 @@
@@ -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%', |
||||
}, |
||||
}) |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './incoming-call.widget' |
Loading…
Reference in new issue