Vitalik
9 months ago
36 changed files with 11434 additions and 2813 deletions
@ -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 |
@ -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'], |
||||||
} |
} |
||||||
|
@ -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, |
||||||
}; |
} |
||||||
|
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
export * from './call-background.component' |
||||||
|
export * from './call-btn.component' |
||||||
export * from './call-row-card.component' |
export * from './call-row-card.component' |
||||||
|
@ -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 @@ |
|||||||
|
export * from './calls.config' |
||||||
|
export * from './ice-servers.config' |
@ -0,0 +1,2 @@ |
|||||||
|
export * from './use-call-data.hook' |
||||||
|
export * from './use-call-from.hook' |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
export * from './calling.atom' |
||||||
|
export * from './income-call.atom' |
||||||
|
export * from './outgoing-call.atom' |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
export const useCallSockets = () => {} |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
export * from './incoming-call.widget' |
Loading…
Reference in new issue