Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
Vitalik | 1b5fae792e | 8 months ago |
Vitalik | f291915f31 | 8 months ago |
Vitalik | d4716231dc | 9 months ago |
Vitalik | fa04562285 | 9 months ago |
Vitalik | 14bd3a13df | 9 months ago |
Vitalik | 4f86a18a25 | 9 months ago |
Vitalik | cad19c01cf | 9 months ago |
Vitalik | 87a58f33d8 | 9 months ago |
Vitalik | 559d6fe59e | 9 months ago |
Vitalik | c34d436da2 | 9 months ago |
54 changed files with 11824 additions and 2918 deletions
@ -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 |
||||
|
@ -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,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 |
||||
} |
@ -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, '') |
||||
} |
@ -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', |
||||
}, |
||||
}) |
@ -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,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', |
||||
}, |
||||
] |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './calls.config' |
||||
export * from './ice-servers.config' |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -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() |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
export * from './accept-call' |
||||
export * from './start-call' |
@ -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) |
||||
} |
||||
} |
@ -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,4 +1,5 @@
@@ -1,4 +1,5 @@
|
||||
export enum CallTypesEnum { |
||||
INCOMING = 'incoming', |
||||
OUTGOING = 'outgoing', |
||||
SKIP = 'skipped', |
||||
} |
||||
|
@ -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' |
@ -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 |
||||
} |
@ -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 }) |
||||
}, |
||||
})) |
@ -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') |
||||
} |
||||
} |
@ -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, |
||||
} |
||||
} |
@ -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> |
||||
) |
||||
} |
@ -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%', |
||||
}, |
||||
}) |
@ -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,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%', |
||||
}, |
||||
}) |
@ -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,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, |
||||
}, |
||||
}) |
@ -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, |
||||
}, |
||||
}) |
||||
|
@ -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%', |
||||
}, |
||||
}) |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './incoming-call.widget' |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export enum CallStatus { |
||||
New = 'n', |
||||
InProccess = 'i', |
||||
Canceled = 'c', |
||||
Finished = 'f', |
||||
} |
@ -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' |
||||
|
@ -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[] |
||||
} |
@ -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' |
||||
|
Loading…
Reference in new issue