mirror of
https://github.com/balzack/databag.git
synced 2025-05-05 07:55:15 +00:00
refactored calls into ring context
This commit is contained in:
parent
45b20948dc
commit
56b701905d
@ -2250,7 +2250,7 @@ SPEC CHECKSUMS:
|
|||||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||||
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
||||||
Yoga: a9ef4f5c2cd79ad812110525ef61048be6a582a4
|
Yoga: b05994d1933f507b0a28ceaa4fdb968dc18da178
|
||||||
|
|
||||||
PODFILE CHECKSUM: 9cf7373afef7b881c911fda82ff1f94eacee3e98
|
PODFILE CHECKSUM: 9cf7373afef7b881c911fda82ff1f94eacee3e98
|
||||||
|
|
||||||
|
@ -7,13 +7,13 @@ import {BlurView} from '@react-native-community/blur';
|
|||||||
import { Confirm } from '../confirm/Confirm';
|
import { Confirm } from '../confirm/Confirm';
|
||||||
import { ActivityIndicator } from 'react-native-paper';
|
import { ActivityIndicator } from 'react-native-paper';
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
|
||||||
import { Colors } from '../constants/Colors';
|
import { Colors } from '../constants/Colors';
|
||||||
|
import { type Card } from 'databag-client-sdk';
|
||||||
import { RTCView } from 'react-native-webrtc';
|
import { RTCView } from 'react-native-webrtc';
|
||||||
import { Card } from '../card/Card';
|
import { Card as Contact } from '../card/Card';
|
||||||
import { activateKeepAwake, deactivateKeepAwake} from "@sayem314/react-native-keep-awake";
|
import { activateKeepAwake, deactivateKeepAwake} from "@sayem314/react-native-keep-awake";
|
||||||
|
|
||||||
export function Calling({ callCard }: { callCard: string }) {
|
export function Calling({ callCard }: { callCard: null|Card }) {
|
||||||
const { state, actions } = useCalling();
|
const { state, actions } = useCalling();
|
||||||
const [alert, setAlert] = useState(false);
|
const [alert, setAlert] = useState(false);
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
@ -42,9 +42,9 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
if (!applyingVideo) {
|
if (!applyingVideo) {
|
||||||
setApplyingVideo(true);
|
setApplyingVideo(true);
|
||||||
try {
|
try {
|
||||||
if (state.video && state.videoEnabled) {
|
if (state.videoEnabled) {
|
||||||
await actions.disableVideo();
|
await actions.disableVideo();
|
||||||
} else if (state.video && !state.videoEnabled) {
|
} else if (!state.videoEnabled) {
|
||||||
await actions.enableVideo();
|
await actions.enableVideo();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -59,9 +59,9 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
if (!applyingAudio) {
|
if (!applyingAudio) {
|
||||||
setApplyingAudio(true);
|
setApplyingAudio(true);
|
||||||
try {
|
try {
|
||||||
if (state.audio && state.audioEnabled) {
|
if (state.audioEnabled) {
|
||||||
await actions.disableAudio();
|
await actions.disableAudio();
|
||||||
} else if (state.audio && !state.audioEnabled) {
|
} else if (!state.audioEnabled) {
|
||||||
await actions.enableAudio();
|
await actions.enableAudio();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -85,11 +85,11 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = async (cardId: string) => {
|
const call = async (card) => {
|
||||||
if (!connecting) {
|
if (!connecting) {
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
try {
|
try {
|
||||||
await actions.call(cardId);
|
await actions.call(card);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
setAlert(true);
|
setAlert(true);
|
||||||
@ -149,9 +149,9 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { cardId } = callCard;
|
const { card } = callCard;
|
||||||
if (cardId) {
|
if (card) {
|
||||||
call(cardId);
|
call(card);
|
||||||
}
|
}
|
||||||
}, [callCard]);
|
}, [callCard]);
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
const acceptButton = <IconButton key="accept" style={styles.circleIcon} iconColor="white" containerColor={Colors.primary} icon="phone-outline" compact="true" mode="contained" size={24} loading={accepting===callId} onPress={()=>accept(callId, card)} />
|
const acceptButton = <IconButton key="accept" style={styles.circleIcon} iconColor="white" containerColor={Colors.primary} icon="phone-outline" compact="true" mode="contained" size={24} loading={accepting===callId} onPress={()=>accept(callId, card)} />
|
||||||
return (
|
return (
|
||||||
<Surface mode="flat" key={index}>
|
<Surface mode="flat" key={index}>
|
||||||
<Card containerStyle={styles.card} placeholder={''} imageUrl={imageUrl} name={name} node={node} handle={handle} actions={[ignoreButton, declineButton, acceptButton]} />
|
<Contact containerStyle={styles.card} placeholder={''} imageUrl={imageUrl} name={name} node={node} handle={handle} actions={[ignoreButton, declineButton, acceptButton]} />
|
||||||
</Surface>
|
</Surface>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -199,27 +199,14 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
<View style={{ ...styles.container, backgroundColor: surface.base }}>
|
<View style={{ ...styles.container, backgroundColor: surface.base }}>
|
||||||
<View style={{ ...styles.frame, top: frameOffset, width: frameWidth > 400 ? 400 : frameWidth, height: frameHeight > 400 ? 400 : frameHeight }}>
|
<View style={{ ...styles.frame, top: frameOffset, width: frameWidth > 400 ? 400 : frameWidth, height: frameHeight > 400 ? 400 : frameHeight }}>
|
||||||
<Image
|
<Image
|
||||||
style={{ ...styles.image, opacity: state.loaded ? 1 : 0 }}
|
style={styles.image}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
source={{ uri: state.calling.imageUrl }}
|
source={{ uri: state.calling.imageUrl }}
|
||||||
onLayout={actions.loaded}
|
|
||||||
/>
|
/>
|
||||||
{ state.loaded && (
|
|
||||||
<LinearGradient style={{...styles.overlap, width: '100%', height: 16, top: 0, borderRadius: 8}} start={{x: 0, y: 0}} end={{x: 0, y: 1}} colors={[surface.base, surface.blend]} />
|
|
||||||
)}
|
|
||||||
{ state.loaded && (
|
|
||||||
<LinearGradient style={{...styles.overlap, width: '100%', height: 16, bottom: 0, borderRadius: 8}} start={{x: 0, y: 0.2}} end={{x: 0, y: 1}} colors={[surface.blend, surface.base]} />
|
|
||||||
)}
|
|
||||||
{ state.loaded && (
|
|
||||||
<LinearGradient style={{...styles.overlap, height: '100%', width: 16, right: 0}} start={{x: 0, y: 0}} end={{x: 1, y: 0}} colors={[surface.blend, surface.base]} />
|
|
||||||
)}
|
|
||||||
{ state.loaded && (
|
|
||||||
<LinearGradient style={{...styles.overlap, height: '100%', width: 16, left: 0}} start={{x: 1, y: 0}} end={{x: 0, y: 0}} colors={[surface.blend, surface.base]} />
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{ state.calling && state.loaded && (
|
{ state.calling && (
|
||||||
<View style={{ ...styles.overlap, top: 64 }}>
|
<View style={{ ...styles.overlap, top: 64 }}>
|
||||||
<View style={{backgroundColor: surface.title, borderRadius: 16 }}>
|
<View style={{backgroundColor: surface.title, borderRadius: 16 }}>
|
||||||
{ state.calling.name && (
|
{ state.calling.name && (
|
||||||
@ -231,45 +218,37 @@ export function Calling({ callCard }: { callCard: string }) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{ state.calling && state.loaded && state.remote && (
|
{ state.calling && state.remoteStream && state.remoteVideo && (
|
||||||
<View style={{ ...styles.canvas, backgroundColor: surface.base }}>
|
<View style={{ ...styles.canvas, backgroundColor: surface.base }}>
|
||||||
<RTCView
|
<RTCView
|
||||||
style={styles.full}
|
style={styles.full}
|
||||||
mirror={true}
|
mirror={true}
|
||||||
objectFit={'contain'}
|
objectFit={'contain'}
|
||||||
streamURL={state.remote.toURL()}
|
streamURL={state.remoteStream.toURL()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{ state.calling && state.loaded && state.local && !state.remote && (
|
{ state.calling && state.localStream && state.localVideo && (
|
||||||
<View style={{ ...styles.canvas, backgroundColor: surface.base }}>
|
<View style={{ ...styles.canvas, backgroundColor: surface.base }}>
|
||||||
<RTCView
|
<RTCView
|
||||||
style={styles.full}
|
style={ state.remoteVideo ? styles.box : styles.full}
|
||||||
mirror={true}
|
mirror={true}
|
||||||
objectFit={'contain'}
|
objectFit={'contain'}
|
||||||
streamURL={state.local.toURL()}
|
streamURL={state.localStream.toURL()}
|
||||||
/>
|
zOrder={2}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{ state.calling && state.loaded && state.local && state.remote && (
|
{ state.calling && (
|
||||||
<RTCView
|
|
||||||
style={{ ...styles.box, top: frameOffset }}
|
|
||||||
mirror={true}
|
|
||||||
objectFit={'contain'}
|
|
||||||
streamURL={state.local.toURL()}
|
|
||||||
zOrder={2}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ state.calling && state.loaded && (
|
|
||||||
<View style={{ ...styles.overlap, bottom: 64 }}>
|
<View style={{ ...styles.overlap, bottom: 64 }}>
|
||||||
<View style={{ paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16, gap: 16, display: 'flex', flexDirection: 'row', borderRadius: 16, backgroundColor: surface.control }}>
|
<View style={{ paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16, gap: 16, display: 'flex', flexDirection: 'row', borderRadius: 16, backgroundColor: surface.control }}>
|
||||||
<IconButton style={styles.closeIcon} iconColor="white" containerColor={Colors.primary} icon={state.audioEnabled ? 'microphone' : 'microphone-off'} loading={applyingAudio} disabled={!state.audio} compact="true" mode="contained" size={32} onPress={toggleAudio} />
|
<IconButton style={styles.closeIcon} iconColor="white" disabled={!state.connected} containerColor={Colors.primary} icon={state.audioEnabled ? 'microphone' : 'microphone-off'} loading={applyingAudio} compact="true" mode="contained" size={32} onPress={toggleAudio} />
|
||||||
<IconButton style={styles.closeIcon} iconColor="white" containerColor={Colors.primary} icon={state.videoEnabled ? 'video-outline' : 'video-off-outline'} loading={applyingVideo} disabled={!state.video} compact="true" mode="contained" size={32} onPress={toggleVideo} />
|
<IconButton style={styles.closeIcon} iconColor="white" disabled={!state.connected} containerColor={Colors.primary} icon={state.videoEnabled ? 'video-outline' : 'video-off-outline'} loading={applyingVideo} compact="true" mode="contained" size={32} onPress={toggleVideo} />
|
||||||
<IconButton style={styles.closeIcon} iconColor="white" containerColor={Colors.danger} icon="phone-hangup-outline" compact="true" mode="contained" size={32} onPress={end} />
|
<IconButton style={styles.closeIcon} iconColor="white" containerColor={Colors.danger} icon="phone-hangup-outline" compact="true" mode="contained" size={32} onPress={end} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{ state.calling && state.loaded && !state.connected && (
|
{ state.calling && !state.connected && (
|
||||||
<View style={{ ...styles.overlap, bottom: 24 }}>
|
<View style={{ ...styles.overlap, bottom: 24 }}>
|
||||||
<View style={{backgroundColor: surface.title, borderRadius: 16 }}>
|
<View style={{backgroundColor: surface.title, borderRadius: 16 }}>
|
||||||
<Text style={styles.connecting}>{ state.strings.connecting }</Text>
|
<Text style={styles.connecting}>{ state.strings.connecting }</Text>
|
||||||
|
@ -1,48 +1,25 @@
|
|||||||
import { useState, useContext, useEffect, useRef } from 'react'
|
import { useState, useContext, useEffect, useRef } from 'react'
|
||||||
import { DisplayContext } from '../context/DisplayContext';
|
import { RingContext } from '../context/RingContext'
|
||||||
import { AppContext } from '../context/AppContext'
|
import { DisplayContext } from '../context/DisplayContext'
|
||||||
import { ContextType } from '../context/ContextType'
|
import { ContextType } from '../context/ContextType'
|
||||||
import { Link, type Card } from 'databag-client-sdk';
|
import { Card } from 'databag-client-sdk';
|
||||||
import InCallManager from 'react-native-incall-manager';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ScreenCapturePickerView,
|
|
||||||
RTCPeerConnection,
|
|
||||||
RTCIceCandidate,
|
|
||||||
RTCSessionDescription,
|
|
||||||
RTCView,
|
|
||||||
MediaStream,
|
|
||||||
MediaiStreamTrack,
|
|
||||||
mediaDevices,
|
|
||||||
registerGlobals
|
|
||||||
} from 'react-native-webrtc';
|
|
||||||
|
|
||||||
export function useCalling() {
|
export function useCalling() {
|
||||||
const app = useContext(AppContext) as ContextType;
|
const ring = useContext(RingContext) as ContextType;
|
||||||
const display = useContext(DisplayContext) as ContextType;
|
const display = useContext(DisplayContext) as ContextType;
|
||||||
const call = useRef(null as { policy: string, peer: RTCPeerConnection, link: Link, candidates: RTCIceCandidate[] } | null);
|
|
||||||
const localStream = useRef(null);
|
|
||||||
const remoteStream = useRef(null);
|
|
||||||
const updatingPeer = useRef(false);
|
|
||||||
const peerUpdate = useRef([]);
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
strings: {},
|
strings: display.state.strings,
|
||||||
ringing: [],
|
calls: [] as { callId: string, card: Card }[],
|
||||||
calls: [],
|
|
||||||
cards: [],
|
|
||||||
calling: null as null | Card,
|
calling: null as null | Card,
|
||||||
failed: false,
|
localStream: null as null|MediaStream,
|
||||||
loaded: false,
|
remoteStream: null as null|MediaStream,
|
||||||
panelOffset: 0,
|
localVideo: false,
|
||||||
local: null,
|
remoteVideo: false,
|
||||||
remote: null,
|
|
||||||
audio: null,
|
|
||||||
audioEnabled: false,
|
audioEnabled: false,
|
||||||
video: null,
|
|
||||||
videoEnabled: false,
|
videoEnabled: false,
|
||||||
videoAdded: false,
|
|
||||||
connected: false,
|
connected: false,
|
||||||
|
failed: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -51,317 +28,10 @@ export function useCalling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const calls = state.ringing
|
const { calls, calling, localStream, remoteStream, localVideo, remoteVideo, audioEnabled, videoEnabled, connected, failed } = ring.state;
|
||||||
.map(ring => ({ callId: ring.callId, card: state.cards.find(card => ring.cardId === card.cardId) }) )
|
updateState({ calls, calling, localStream, remoteStream, localVideo, remoteVideo, audioEnabled, videoEnabled, connected, failed });
|
||||||
.filter(ring => (ring.card && !ring.card.blocked));
|
}, [ring.state]);
|
||||||
updateState({ calls });
|
|
||||||
}, [state.ringing, state.cards]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const actions = ring.actions;
|
||||||
const { strings } = display.state;
|
return { state, actions };
|
||||||
updateState({ strings });
|
|
||||||
}, [display.state]);
|
|
||||||
|
|
||||||
const constraints = {
|
|
||||||
mandatory: {
|
|
||||||
OfferToReceiveAudio: true,
|
|
||||||
OfferToReceiveVideo: false,
|
|
||||||
VoiceActivityDetection: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const linkStatus = async (status: string) => {
|
|
||||||
if (call.current) {
|
|
||||||
const { policy, peer, link } = call.current;
|
|
||||||
if (status === 'connected') {
|
|
||||||
try {
|
|
||||||
remoteStream.current = new MediaStream();
|
|
||||||
localStream.current = await mediaDevices.getUserMedia({
|
|
||||||
audio: true,
|
|
||||||
video: {
|
|
||||||
frameRate: 30,
|
|
||||||
facingMode: 'user'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const audio = localStream.current.getTracks().find(track => track.kind === 'audio');
|
|
||||||
const video = localStream.current.getTracks().find(track => track.kind === 'video');
|
|
||||||
if (audio) {
|
|
||||||
audio.enabled = true;
|
|
||||||
await updatePeer('local_track', audio);
|
|
||||||
}
|
|
||||||
if (video) {
|
|
||||||
video.enabled = false;
|
|
||||||
}
|
|
||||||
InCallManager.start({media: 'audio'});
|
|
||||||
updateState({ audio, video, audioAdded: true, audioEnabled: true, videoAdded: false, videoEnabled: false, connected: true });
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
updateState({ failed: true });
|
|
||||||
}
|
|
||||||
} else if (status === 'closed') {
|
|
||||||
updatePeer('close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkMessage = async (message: any) => {
|
|
||||||
if (call.current) {
|
|
||||||
const { peer, link, policy } = call.current;
|
|
||||||
try {
|
|
||||||
if (message.description) {
|
|
||||||
const offer = new RTCSessionDescription(message.description);
|
|
||||||
await peer.setRemoteDescription(offer);
|
|
||||||
if (message.description.type === 'offer') {
|
|
||||||
const description = await peer.createAnswer();
|
|
||||||
await peer.setLocalDescription(description);
|
|
||||||
link.sendMessage({ description });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.current) {
|
|
||||||
const { candidates } = call.current;
|
|
||||||
call.current.candidates = [];
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
await peer.addIceCandidate(candidate);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (message.candidate) {
|
|
||||||
const candidate = new RTCIceCandidate(message.candidate);
|
|
||||||
if (peer.remoteDescription == null) {
|
|
||||||
candidates.push(candidate);
|
|
||||||
} else {
|
|
||||||
await peer.addIceCandidate(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
updateState({ failed: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const peerCandidate = async (candidate) => {
|
|
||||||
if (call.current && candidate) {
|
|
||||||
const { link } = call.current;
|
|
||||||
await link.sendMessage({ candidate });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const peerNegotiate = async () => {
|
|
||||||
if (call.current) {
|
|
||||||
try {
|
|
||||||
const { peer, link } = call.current;
|
|
||||||
const description = await peer.createOffer(constraints);
|
|
||||||
await peer.setLocalDescription(description);
|
|
||||||
await link.sendMessage({ description });
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const peerTrack = async (track) => {
|
|
||||||
if (call.current) {
|
|
||||||
try {
|
|
||||||
const { peer } = call.current;
|
|
||||||
peer.addTrack(track, localStream.current);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePeer = async (type: string, data: any) => {
|
|
||||||
peerUpdate.current.push({ type, data });
|
|
||||||
|
|
||||||
if (!updatingPeer.current) {
|
|
||||||
updatingPeer.current = true;
|
|
||||||
while (peerUpdate.current.length > 0) {
|
|
||||||
const { type, data } = peerUpdate.current.shift();
|
|
||||||
if (type === 'negotiate') {
|
|
||||||
await peerNegotiate();
|
|
||||||
} else if (type === 'candidate') {
|
|
||||||
await peerCandidate(data);
|
|
||||||
} else if (type === 'message') {
|
|
||||||
await linkMessage(data);
|
|
||||||
} else if (type === 'remote_track') {
|
|
||||||
remoteStream.current.addTrack(data, remoteStream.current);
|
|
||||||
if (data.kind === 'video') {
|
|
||||||
InCallManager.setForceSpeakerphoneOn(true);
|
|
||||||
updateState({ remote: remoteStream.current });
|
|
||||||
}
|
|
||||||
} else if (type === 'local_track') {
|
|
||||||
await peerTrack(data);
|
|
||||||
if (data.kind === 'video') {
|
|
||||||
InCallManager.setForceSpeakerphoneOn(true);
|
|
||||||
}
|
|
||||||
} else if (type === 'close' && call.current) {
|
|
||||||
peerUpdate.current = [];
|
|
||||||
const { peer, link } = call.current;
|
|
||||||
call.current = null;
|
|
||||||
try {
|
|
||||||
InCallManager.stop();
|
|
||||||
peer.close();
|
|
||||||
link.close();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
localStream.current = null;
|
|
||||||
remoteStream.current = null,
|
|
||||||
updateState({ calling: null, failed: false, audio: null, video: null, local: null, remote: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updatingPeer.current = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transmit = (ice: { urls: string; username: string; credential: string }[]) => {
|
|
||||||
const peerConnection = new RTCPeerConnection({ iceServers: ice });
|
|
||||||
peerConnection.addEventListener( 'connectionstatechange', event => {
|
|
||||||
console.log("CONNECTION STATE", event);
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener( 'icecandidate', event => {
|
|
||||||
updatePeer('candidate', event.candidate);
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener( 'icecandidateerror', event => {
|
|
||||||
console.log("ICE ERROR");
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener( 'iceconnectionstatechange', event => {
|
|
||||||
console.log("ICE STATE CHANGE", event);
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener( 'negotiationneeded', event => {
|
|
||||||
updatePeer('negotiate');
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener( 'signalingstatechange', event => {
|
|
||||||
console.log("ICE SIGNALING", event);
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener( 'track', event => {
|
|
||||||
updatePeer('remote_track', event.track);
|
|
||||||
});
|
|
||||||
return peerConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (app.state.session) {
|
|
||||||
const setRinging = (ringing: { cardId: string, callId: string }[]) => {
|
|
||||||
updateState({ ringing });
|
|
||||||
}
|
|
||||||
const setContacts = (cards: Card[]) => {
|
|
||||||
updateState({ cards });
|
|
||||||
}
|
|
||||||
const ring = app.state.session.getRing();
|
|
||||||
ring.addRingingListener(setRinging);
|
|
||||||
const contact = app.state.session.getContact();
|
|
||||||
contact.addCardListener(setContacts);
|
|
||||||
return () => {
|
|
||||||
ring.removeRingingListener(setRinging);
|
|
||||||
contact.removeCardListener(setContacts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [app.state.session]);
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
ignore: async (callId: string, card: Card) => {
|
|
||||||
const ring = app.state.session.getRing();
|
|
||||||
await ring.ignore(card.cardId, callId);
|
|
||||||
},
|
|
||||||
decline: async (callId: string, card: Card) => {
|
|
||||||
const ring = app.state.session.getRing();
|
|
||||||
await ring.decline(card.cardId, callId);
|
|
||||||
},
|
|
||||||
end: async () => {
|
|
||||||
if (!call.current) {
|
|
||||||
throw new Error('no active call');
|
|
||||||
}
|
|
||||||
const { link, peer } = call.current;
|
|
||||||
try {
|
|
||||||
peer.close();
|
|
||||||
link.close();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
call.current = null;
|
|
||||||
localStream.current = null;
|
|
||||||
remoteStream.current = null;
|
|
||||||
updateState({ calling: null, audio: null, video: null, local: null, remote: null });
|
|
||||||
},
|
|
||||||
accept: async (callId: string, card: Card) => {
|
|
||||||
if (call.current) {
|
|
||||||
throw new Error('active call in progress');
|
|
||||||
}
|
|
||||||
const { cardId, node } = card;
|
|
||||||
const ring = app.state.session.getRing();
|
|
||||||
const link = await ring.accept(cardId, callId, node);
|
|
||||||
const ice = link.getIce();
|
|
||||||
const peer = transmit(ice);
|
|
||||||
const policy = 'impolite';
|
|
||||||
const candidates = [];
|
|
||||||
call.current = { policy, peer, link, candidates };
|
|
||||||
link.setStatusListener(linkStatus);
|
|
||||||
link.setMessageListener((msg) => updatePeer('message', msg));
|
|
||||||
updateState({ calling: card, connected: false });
|
|
||||||
},
|
|
||||||
call: async (cardId: string) => {
|
|
||||||
if (call.current) {
|
|
||||||
throw new Error('active call in proegress');
|
|
||||||
}
|
|
||||||
const card = state.cards.find(contact => contact.cardId === cardId);
|
|
||||||
if (!card) {
|
|
||||||
throw new Error('calling contact not found');
|
|
||||||
}
|
|
||||||
const contact = app.state.session.getContact();
|
|
||||||
const link = await contact.callCard(cardId);
|
|
||||||
const ice = link.getIce();
|
|
||||||
const peer = transmit(ice);
|
|
||||||
const policy = 'polite';
|
|
||||||
const candidates = [];
|
|
||||||
call.current = { policy, peer, link, candidates };
|
|
||||||
link.setStatusListener(linkStatus);
|
|
||||||
link.setMessageListener((msg) => updatePeer('message', msg));
|
|
||||||
updateState({ calling: card, connected: false });
|
|
||||||
},
|
|
||||||
loaded: (e) => {
|
|
||||||
const { width, height } = e.nativeEvent.layout;
|
|
||||||
if (width > (height + 80)) {
|
|
||||||
updateState({ panelOffset: 0, loaded: true });
|
|
||||||
} else {
|
|
||||||
updateState({ panelOffset: ((height - width) - 80) / 2, loaded: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enableAudio: async () => {
|
|
||||||
if (!call.current || !state.audio || !state.audioAdded) {
|
|
||||||
throw new Error('cannot unmute audio');
|
|
||||||
}
|
|
||||||
state.audio.enabled = true;
|
|
||||||
updateState({ audioEnabled: true });
|
|
||||||
},
|
|
||||||
disableAudio: () => {
|
|
||||||
if (!call.current || !state.audio || !state.audioAdded) {
|
|
||||||
throw new Error('cannot mute audio');
|
|
||||||
}
|
|
||||||
state.audio.enabled = false;
|
|
||||||
updateState({ audioEnabled: false });
|
|
||||||
},
|
|
||||||
enableVideo: () => {
|
|
||||||
if (!call.current || !state.video) {
|
|
||||||
throw new Error('cannot start video');
|
|
||||||
}
|
|
||||||
state.video.enabled = true;
|
|
||||||
if (!state.videoAdded) {
|
|
||||||
const local = new MediaStream();
|
|
||||||
local.addTrack(state.video, local);
|
|
||||||
updateState({ local });
|
|
||||||
updatePeer('local_track', state.video);
|
|
||||||
}
|
|
||||||
updateState({ videoAdded: true, videoEnabled: true });
|
|
||||||
},
|
|
||||||
disableVideo: () => {
|
|
||||||
if (!call.current || !state.video) {
|
|
||||||
throw new Error('cannot stop video');
|
|
||||||
}
|
|
||||||
state.video.enabled = false;
|
|
||||||
updateState({ videoEnabled: false });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return { state, actions }
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ function Action({icon, color, select}: {icon: string; color: string; select: ()
|
|||||||
return <IconButton style={styles.icon} loading={loading} iconColor={color} mode="contained" icon={icon} onPress={onPress} />;
|
return <IconButton style={styles.icon} loading={loading} iconColor={color} mode="contained" icon={icon} onPress={onPress} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Contacts({openRegistry, openContact, callContact, textContact}: {openRegistry: () => void; openContact: (params: ContactParams) => void, callContact: (cardId: null|string)=>void, textContact: (cardId: null|string)=>void}) {
|
export function Contacts({openRegistry, openContact, callContact, textContact}: {openRegistry: () => void; openContact: (params: ContactParams) => void, callContact: (card: null|Card)=>void, textContact: (cardId: null|string)=>void}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {state, actions} = useContacts();
|
const {state, actions} = useContacts();
|
||||||
const [alert, setAlert] = useState(false);
|
const [alert, setAlert] = useState(false);
|
||||||
@ -74,7 +74,7 @@ export function Contacts({openRegistry, openContact, callContact, textContact}:
|
|||||||
key="call"
|
key="call"
|
||||||
icon="phone-outline"
|
icon="phone-outline"
|
||||||
color={Colors.connected}
|
color={Colors.connected}
|
||||||
select={()=>callContact(item.cardId)}
|
select={()=>callContact(item)}
|
||||||
/>,
|
/>,
|
||||||
<Action
|
<Action
|
||||||
key="text"
|
key="text"
|
||||||
|
9
app/client/mobile/src/context/RingContext.tsx
Normal file
9
app/client/mobile/src/context/RingContext.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { ReactNode, createContext } from 'react'
|
||||||
|
import { useRingContext } from './useRingContext.hook'
|
||||||
|
|
||||||
|
export const RingContext = createContext({})
|
||||||
|
|
||||||
|
export function RingContextProvider({ children }: { children: ReactNode }) {
|
||||||
|
const { state, actions } = useRingContext()
|
||||||
|
return <RingContext.Provider value={{ state, actions }}>{children}</RingContext.Provider>
|
||||||
|
}
|
357
app/client/mobile/src/context/useRingContext.hook.ts
Normal file
357
app/client/mobile/src/context/useRingContext.hook.ts
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
import { useState, useContext, useEffect, useRef } from 'react'
|
||||||
|
import { DisplayContext } from '../context/DisplayContext';
|
||||||
|
import { AppContext } from '../context/AppContext'
|
||||||
|
import { ContextType } from '../context/ContextType'
|
||||||
|
import { Link, type Card } from 'databag-client-sdk';
|
||||||
|
import InCallManager from 'react-native-incall-manager';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ScreenCapturePickerView,
|
||||||
|
RTCPeerConnection,
|
||||||
|
RTCIceCandidate,
|
||||||
|
RTCSessionDescription,
|
||||||
|
RTCView,
|
||||||
|
MediaStream,
|
||||||
|
MediaiStreamTrack,
|
||||||
|
mediaDevices,
|
||||||
|
registerGlobals
|
||||||
|
} from 'react-native-webrtc';
|
||||||
|
|
||||||
|
const CLOSE_POLL_MS = 100;
|
||||||
|
|
||||||
|
export function useRingContext() {
|
||||||
|
const app = useContext(AppContext) as ContextType;
|
||||||
|
const display = useContext(DisplayContext) as ContextType;
|
||||||
|
const call = useRef(null as { peer: RTCPeerConnection, link: Link, candidates: RTCIceCandidate[] } | null);
|
||||||
|
const sourceStream = useRef(null as null|MediaStream);
|
||||||
|
const localStream = useRef(null as null|MediaStream);
|
||||||
|
const localAudio = useRef(null as null|MediaStreamTrack);
|
||||||
|
const localVideo = useRef(null as null|MediaStreamTrack);
|
||||||
|
const localAudioAdded = useRef(false);
|
||||||
|
const localVideoAdded = useRef(false);
|
||||||
|
const remoteStream = useRef(null as null|MediaStream);
|
||||||
|
const updatingPeer = useRef(false);
|
||||||
|
const peerUpdate = useRef([] as {type: string, data?: any}[]);
|
||||||
|
const connecting = useRef(false);
|
||||||
|
const closing = useRef(false);
|
||||||
|
const [ringing, setRinging] = useState([] as { cardId: string, callId: string }[]);
|
||||||
|
const [cards, setCards] = useState([] as Card[]);
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
calls: [] as { callId: string, cardId: string}[],
|
||||||
|
calling: null as null | Card,
|
||||||
|
localStream: null as null|MediaStream,
|
||||||
|
remoteStream: null as null|MediaStream,
|
||||||
|
localVideo: false,
|
||||||
|
remoteVideo: false,
|
||||||
|
audioEnabled: false,
|
||||||
|
videoEnabled: false,
|
||||||
|
connected: false,
|
||||||
|
failed: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const updateState = (value: any) => {
|
||||||
|
setState((s) => ({ ...s, ...value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const calls = ringing.map(ring => ({ callId: ring.callId, card: cards.find(card => ring.cardId === card.cardId) }))
|
||||||
|
.filter(ring => (ring.card && !ring.card.blocked));
|
||||||
|
updateState({ calls });
|
||||||
|
}, [ringing, cards]);
|
||||||
|
|
||||||
|
const constraints = {
|
||||||
|
mandatory: {
|
||||||
|
OfferToReceiveAudio: true,
|
||||||
|
OfferToReceiveVideo: false,
|
||||||
|
VoiceActivityDetection: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkStatus = async (status: string) => {
|
||||||
|
if (call.current) {
|
||||||
|
const { peer, link } = call.current;
|
||||||
|
if (status === 'connected') {
|
||||||
|
updateState({ connected: true });
|
||||||
|
await actions.enableAudio();
|
||||||
|
} else if (status === 'closed') {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePeer = async (type: string, data?: any) => {
|
||||||
|
peerUpdate.current.push({ type, data });
|
||||||
|
|
||||||
|
if (!updatingPeer.current) {
|
||||||
|
updatingPeer.current = true;
|
||||||
|
while (!closing.current && call.current && peerUpdate.current.length > 0) {
|
||||||
|
const { peer, link, candidates } = call.current;
|
||||||
|
const { type, data } = peerUpdate.current.shift() || { type: '' };
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'negotiate':
|
||||||
|
const description = await peer.createOffer();
|
||||||
|
await peer.setLocalDescription(description);
|
||||||
|
await link.sendMessage({ description });
|
||||||
|
break;
|
||||||
|
case 'candidate':
|
||||||
|
await link.sendMessage({ data });
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
if (data.description) {
|
||||||
|
const offer = new RTCSessionDescription(data.description);
|
||||||
|
await peer.setRemoteDescription(offer);
|
||||||
|
if (data.description.type === 'offer') {
|
||||||
|
const description = await peer.createAnswer();
|
||||||
|
await peer.setLocalDescription(description);
|
||||||
|
link.sendMessage({ description });
|
||||||
|
}
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
await peer.addIceCandidate(candidate);
|
||||||
|
};
|
||||||
|
call.current.candidates = [];
|
||||||
|
} else if (data.candidate) {
|
||||||
|
const candidate = new RTCIceCandidate(data.candidate);
|
||||||
|
if (peer.remoteDescription == null) {
|
||||||
|
candidates.push(candidate);
|
||||||
|
} else {
|
||||||
|
await peer.addIceCandidate(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'remote_track':
|
||||||
|
if (remoteStream.current) {
|
||||||
|
remoteStream.current.addTrack(data);
|
||||||
|
if (data.kind === 'video') {
|
||||||
|
InCallManager.setForceSpeakerphoneOn(true);
|
||||||
|
updateState({ remoteVideo: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'local_track':
|
||||||
|
peer.addTrack(data, sourceStream.current);
|
||||||
|
if (data.kind === 'video') {
|
||||||
|
InCallManager.setForceSpeakerphoneOn(true);
|
||||||
|
updateState({ localVideo: true })
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('unknown event');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
updateState({ failed: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatingPeer.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = async (link: Link, card: Card) => {
|
||||||
|
|
||||||
|
remoteStream.current = new MediaStream();
|
||||||
|
localStream.current = new MediaStream();
|
||||||
|
sourceStream.current = await mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
frameRate: 30,
|
||||||
|
facingMode: 'user'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
InCallManager.start({media: 'audio'});
|
||||||
|
localAudio.current = sourceStream.current.getTracks().find(track => track.kind === 'audio');
|
||||||
|
localVideo.current = sourceStream.current.getTracks().find(track => track.kind === 'video');
|
||||||
|
if (localAudio.current) {
|
||||||
|
localAudio.current.enabled = false;
|
||||||
|
}
|
||||||
|
if (localVideo.current) {
|
||||||
|
localVideo.current.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ice = link.getIce();
|
||||||
|
const peer = transmit(ice);
|
||||||
|
const candidates = [] as RTCIceCandidate[];
|
||||||
|
call.current = { peer, link, candidates };
|
||||||
|
link.setStatusListener(linkStatus);
|
||||||
|
link.setMessageListener((msg: any) => updatePeer('message', msg));
|
||||||
|
updateState({ calling: card, failed: false, connected: false,
|
||||||
|
audioEnabled: false, videoEnabled: false, localVideo: false, remoteVideo: false,
|
||||||
|
localStream: localStream.current, remoteStream: remoteStream.current });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
closing.current = true;
|
||||||
|
while (updatingPeer.current || connecting.current) {
|
||||||
|
await new Promise((r) => setTimeout(r, CLOSE_POLL_MS));
|
||||||
|
}
|
||||||
|
if (call.current) {
|
||||||
|
const { peer, link } = call.current;
|
||||||
|
peer.close();
|
||||||
|
link.close();
|
||||||
|
call.current = null;
|
||||||
|
}
|
||||||
|
if (localVideo.current) {
|
||||||
|
localVideo.current.stop();
|
||||||
|
localVideo.current = null;
|
||||||
|
localVideoAdded.current = false;
|
||||||
|
}
|
||||||
|
if (localAudio.current) {
|
||||||
|
localAudio.current.stop();
|
||||||
|
localAudio.current = null;
|
||||||
|
localAudioAdded.current = false;
|
||||||
|
}
|
||||||
|
localStream.current = null;
|
||||||
|
remoteStream.current = null;
|
||||||
|
sourceStream.current = null;
|
||||||
|
peerUpdate.current = [];
|
||||||
|
InCallManager.stop();
|
||||||
|
updateState({ calling: null, failed: false, localStream: null, remoteStream: null, localVideo: false, remoteVideo: false });
|
||||||
|
closing.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transmit = (ice: { urls: string; username: string; credential: string }[]) => {
|
||||||
|
const peerConnection = new RTCPeerConnection({ iceServers: ice });
|
||||||
|
peerConnection.addEventListener( 'connectionstatechange', event => {
|
||||||
|
console.log("CONNECTION STATE", event);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener( 'icecandidate', event => {
|
||||||
|
updatePeer('candidate', event.candidate);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener( 'icecandidateerror', event => {
|
||||||
|
console.log("ICE ERROR");
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener( 'iceconnectionstatechange', event => {
|
||||||
|
console.log("ICE STATE CHANGE", event);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener( 'negotiationneeded', event => {
|
||||||
|
updatePeer('negotiate');
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener( 'signalingstatechange', event => {
|
||||||
|
console.log("ICE SIGNALING", event);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener( 'track', event => {
|
||||||
|
updatePeer('remote_track', event.track);
|
||||||
|
});
|
||||||
|
return peerConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (app.state.session) {
|
||||||
|
const setRing = (ringing: { cardId: string, callId: string }[]) => {
|
||||||
|
setRinging(ringing);
|
||||||
|
}
|
||||||
|
const setContacts = (cards: Card[]) => {
|
||||||
|
setCards(cards);
|
||||||
|
}
|
||||||
|
const ring = app.state.session.getRing();
|
||||||
|
ring.addRingingListener(setRinging);
|
||||||
|
const contact = app.state.session.getContact();
|
||||||
|
contact.addCardListener(setContacts);
|
||||||
|
return () => {
|
||||||
|
ring.removeRingingListener(setRing);
|
||||||
|
contact.removeCardListener(setContacts);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [app.state.session]);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
ignore: async (callId: string, card: Card) => {
|
||||||
|
const ring = app.state.session.getRing();
|
||||||
|
await ring.ignore(card.cardId, callId);
|
||||||
|
},
|
||||||
|
decline: async (callId: string, card: Card) => {
|
||||||
|
const ring = app.state.session.getRing();
|
||||||
|
await ring.decline(card.cardId, callId);
|
||||||
|
},
|
||||||
|
end: async () => {
|
||||||
|
await cleanup();
|
||||||
|
},
|
||||||
|
accept: async (callId: string, card: Card) => {
|
||||||
|
if (connecting.current || closing.current || call.current) {
|
||||||
|
throw new Error('not ready to accept calls');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connecting.current = true;
|
||||||
|
const { cardId, node } = card;
|
||||||
|
const ring = app.state.session.getRing();
|
||||||
|
const link = await ring.accept(cardId, callId, node);
|
||||||
|
await setup(link, card);
|
||||||
|
connecting.current = false;
|
||||||
|
} catch (err) {
|
||||||
|
connecting.current = false;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
call: async (card: Card) => {
|
||||||
|
if (connecting.current || closing.current || call.current) {
|
||||||
|
throw new Error('not ready make calls');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connecting.current = true;
|
||||||
|
const contact = app.state.session.getContact();
|
||||||
|
const link = await contact.callCard(card.cardId);
|
||||||
|
await setup(link, card);
|
||||||
|
connecting.current = false;
|
||||||
|
} catch (err) {
|
||||||
|
connecting.current = false;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enableAudio: async () => {
|
||||||
|
if (connecting.current || closing.current || !call.current) {
|
||||||
|
throw new Error('cannot unmute audio');
|
||||||
|
}
|
||||||
|
if (!localAudio.current) {
|
||||||
|
throw new Error('audio not available');
|
||||||
|
} else {
|
||||||
|
if (!localAudioAdded.current) {
|
||||||
|
localAudioAdded.current = true;
|
||||||
|
updatePeer('local_track', localAudio.current);
|
||||||
|
}
|
||||||
|
localAudio.current.enabled = true;
|
||||||
|
}
|
||||||
|
updateState({ audioEnabled: true });
|
||||||
|
},
|
||||||
|
disableAudio: async () => {
|
||||||
|
if (!call.current) {
|
||||||
|
throw new Error('cannot mute audio');
|
||||||
|
}
|
||||||
|
if (localAudio.current) {
|
||||||
|
localAudio.current.enabled = false;
|
||||||
|
}
|
||||||
|
updateState({ audioEnabled: false });
|
||||||
|
},
|
||||||
|
enableVideo: async () => {
|
||||||
|
if (connecting.current || closing.current || !call.current) {
|
||||||
|
throw new Error('cannot start video');
|
||||||
|
}
|
||||||
|
if (!localVideo.current) {
|
||||||
|
throw new Error('video not available');
|
||||||
|
} else {
|
||||||
|
if (!localVideoAdded.current) {
|
||||||
|
localVideoAdded.current = true;
|
||||||
|
localStream.current.addTrack(localVideo.current, localStream.current);
|
||||||
|
updatePeer('local_track', localVideo.current);
|
||||||
|
}
|
||||||
|
localVideo.current.enabled = true;
|
||||||
|
}
|
||||||
|
updateState({ videoEnabled: true });
|
||||||
|
},
|
||||||
|
disableVideo: async () => {
|
||||||
|
if (!call.current) {
|
||||||
|
throw new Error('cannot stop video');
|
||||||
|
}
|
||||||
|
if (localVideo.current) {
|
||||||
|
localVideo.current.enabled = false;
|
||||||
|
}
|
||||||
|
updateState({ videoEnabled: false });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return { state, actions }
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import React, {useState, useCallback, useEffect} from 'react';
|
import React, {useState, useCallback, useEffect} from 'react';
|
||||||
import {SafeAreaView, Pressable, View, useColorScheme} from 'react-native';
|
import {SafeAreaView, Pressable, View, useColorScheme} from 'react-native';
|
||||||
|
import {RingContextProvider} from '../context/RingContext';
|
||||||
import {styles} from './Session.styled';
|
import {styles} from './Session.styled';
|
||||||
import {IconButton, Surface, Text, Icon} from 'react-native-paper';
|
import {IconButton, Surface, Text, Icon} from 'react-native-paper';
|
||||||
import {Settings} from '../settings/Settings';
|
import {Settings} from '../settings/Settings';
|
||||||
@ -12,7 +13,7 @@ import {Identity} from '../identity/Identity';
|
|||||||
import {Conversation} from '../conversation/Conversation';
|
import {Conversation} from '../conversation/Conversation';
|
||||||
import {useSession} from './useSession.hook';
|
import {useSession} from './useSession.hook';
|
||||||
import {TransitionPresets} from '@react-navigation/stack';
|
import {TransitionPresets} from '@react-navigation/stack';
|
||||||
import {Focus} from 'databag-client-sdk';
|
import {Focus, Card} from 'databag-client-sdk';
|
||||||
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native';
|
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native';
|
||||||
import {createDrawerNavigator} from '@react-navigation/drawer';
|
import {createDrawerNavigator} from '@react-navigation/drawer';
|
||||||
import {createNativeStackNavigator} from '@react-navigation/native-stack';
|
import {createNativeStackNavigator} from '@react-navigation/native-stack';
|
||||||
@ -33,7 +34,7 @@ export function Session() {
|
|||||||
const scheme = useColorScheme();
|
const scheme = useColorScheme();
|
||||||
const [tab, setTab] = useState('content');
|
const [tab, setTab] = useState('content');
|
||||||
const [textCard, setTextCard] = useState({ cardId: null} as {cardId: null|string});
|
const [textCard, setTextCard] = useState({ cardId: null} as {cardId: null|string});
|
||||||
const [callCard, setCallCard] = useState({ cardId: null} as {cardId: null|string});
|
const [callCard, setCallCard] = useState({ card: null} as {card: null|Card});
|
||||||
const [dismissed, setDismissed] = useState(false);
|
const [dismissed, setDismissed] = useState(false);
|
||||||
const [disconnected, setDisconnected] = useState(false);
|
const [disconnected, setDisconnected] = useState(false);
|
||||||
const [showDisconnected, setShowDisconnected] = useState(false);
|
const [showDisconnected, setShowDisconnected] = useState(false);
|
||||||
@ -42,8 +43,8 @@ export function Session() {
|
|||||||
setTextCard({ cardId });
|
setTextCard({ cardId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const callContact = (cardId: null|string) => {
|
const callContact = (card: null|Card) => {
|
||||||
setCallCard({ cardId });
|
setCallCard({ card });
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionNav = {strings: state.strings, callContact, callCard, textContact, textCard};
|
const sessionNav = {strings: state.strings, callContact, callCard, textContact, textCard};
|
||||||
@ -77,126 +78,128 @@ export function Session() {
|
|||||||
}, [state.appState, state.sdkState]);
|
}, [state.appState, state.sdkState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.session}>
|
<RingContextProvider>
|
||||||
{state.layout !== 'large' && (
|
<View style={styles.session}>
|
||||||
<Surface elevation={3}>
|
{state.layout !== 'large' && (
|
||||||
<SafeAreaView style={styles.full}>
|
<Surface elevation={3}>
|
||||||
<View style={styles.screen}>
|
<SafeAreaView style={styles.full}>
|
||||||
<View
|
<View style={styles.screen}>
|
||||||
style={{
|
<View
|
||||||
...styles.body,
|
style={{
|
||||||
...showContent,
|
...styles.body,
|
||||||
}}>
|
...showContent,
|
||||||
<ContentTab textCard={textCard} scheme={scheme} contentTab={contentTab} />
|
}}>
|
||||||
|
<ContentTab textCard={textCard} scheme={scheme} contentTab={contentTab} />
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...styles.body,
|
||||||
|
...showContact,
|
||||||
|
}}>
|
||||||
|
<ContactTab textContact={textContact} callContact={callContact} scheme={scheme} />
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...styles.body,
|
||||||
|
...showSettings,
|
||||||
|
}}>
|
||||||
|
<Surface elevation={0}>
|
||||||
|
<Settings showLogout={true} />
|
||||||
|
</Surface>
|
||||||
|
</View>
|
||||||
|
<View style={styles.tabs}>
|
||||||
|
{tab === 'content' && (
|
||||||
|
<IconButton
|
||||||
|
style={styles.activeTab}
|
||||||
|
mode="contained"
|
||||||
|
icon={'comment-multiple'}
|
||||||
|
size={28}
|
||||||
|
onPress={() => {
|
||||||
|
setTab('content');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab !== 'content' && (
|
||||||
|
<IconButton
|
||||||
|
style={styles.idleTab}
|
||||||
|
mode="contained"
|
||||||
|
icon={'comment-multiple-outline'}
|
||||||
|
size={28}
|
||||||
|
onPress={() => {
|
||||||
|
setTab('content');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab === 'contacts' && (
|
||||||
|
<IconButton
|
||||||
|
style={styles.activeTab}
|
||||||
|
mode="contained"
|
||||||
|
icon={'contacts'}
|
||||||
|
size={28}
|
||||||
|
onPress={() => {
|
||||||
|
setTab('contacts');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab !== 'contacts' && (
|
||||||
|
<IconButton
|
||||||
|
style={styles.idleTab}
|
||||||
|
mode="contained"
|
||||||
|
icon={'contacts-outline'}
|
||||||
|
size={28}
|
||||||
|
onPress={() => {
|
||||||
|
setTab('contacts');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab === 'settings' && (
|
||||||
|
<IconButton
|
||||||
|
style={styles.activeTab}
|
||||||
|
mode="contained"
|
||||||
|
icon={'cog'}
|
||||||
|
size={28}
|
||||||
|
onPress={() => {
|
||||||
|
setTab('settings');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab !== 'settings' && (
|
||||||
|
<IconButton
|
||||||
|
style={styles.idleTab}
|
||||||
|
mode="contained"
|
||||||
|
icon={'cog-outline'}
|
||||||
|
size={28}
|
||||||
|
onPress={() => {
|
||||||
|
setTab('settings');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
</SafeAreaView>
|
||||||
style={{
|
|
||||||
...styles.body,
|
|
||||||
...showContact,
|
|
||||||
}}>
|
|
||||||
<ContactTab textContact={textContact} callContact={callContact} scheme={scheme} />
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
...styles.body,
|
|
||||||
...showSettings,
|
|
||||||
}}>
|
|
||||||
<Surface elevation={0}>
|
|
||||||
<Settings showLogout={true} />
|
|
||||||
</Surface>
|
|
||||||
</View>
|
|
||||||
<View style={styles.tabs}>
|
|
||||||
{tab === 'content' && (
|
|
||||||
<IconButton
|
|
||||||
style={styles.activeTab}
|
|
||||||
mode="contained"
|
|
||||||
icon={'comment-multiple'}
|
|
||||||
size={28}
|
|
||||||
onPress={() => {
|
|
||||||
setTab('content');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab !== 'content' && (
|
|
||||||
<IconButton
|
|
||||||
style={styles.idleTab}
|
|
||||||
mode="contained"
|
|
||||||
icon={'comment-multiple-outline'}
|
|
||||||
size={28}
|
|
||||||
onPress={() => {
|
|
||||||
setTab('content');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab === 'contacts' && (
|
|
||||||
<IconButton
|
|
||||||
style={styles.activeTab}
|
|
||||||
mode="contained"
|
|
||||||
icon={'contacts'}
|
|
||||||
size={28}
|
|
||||||
onPress={() => {
|
|
||||||
setTab('contacts');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab !== 'contacts' && (
|
|
||||||
<IconButton
|
|
||||||
style={styles.idleTab}
|
|
||||||
mode="contained"
|
|
||||||
icon={'contacts-outline'}
|
|
||||||
size={28}
|
|
||||||
onPress={() => {
|
|
||||||
setTab('contacts');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab === 'settings' && (
|
|
||||||
<IconButton
|
|
||||||
style={styles.activeTab}
|
|
||||||
mode="contained"
|
|
||||||
icon={'cog'}
|
|
||||||
size={28}
|
|
||||||
onPress={() => {
|
|
||||||
setTab('settings');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab !== 'settings' && (
|
|
||||||
<IconButton
|
|
||||||
style={styles.idleTab}
|
|
||||||
mode="contained"
|
|
||||||
icon={'cog-outline'}
|
|
||||||
size={28}
|
|
||||||
onPress={() => {
|
|
||||||
setTab('settings');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
</Surface>
|
|
||||||
)}
|
|
||||||
{state.layout === 'large' && (
|
|
||||||
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
|
|
||||||
<View style={styles.container}>
|
|
||||||
<DetailsScreen nav={sessionNav} />
|
|
||||||
</View>
|
|
||||||
</NavigationContainer>
|
|
||||||
)}
|
|
||||||
{ disconnected && showDisconnected && !dismissed && (
|
|
||||||
<View style={styles.alert}>
|
|
||||||
<Surface elevation={5} style={styles.alertArea}>
|
|
||||||
<Icon color={Colors.offsync} size={20} source="alert-circle-outline" />
|
|
||||||
<Text style={styles.alertLabel}>{ state.strings.disconnected }</Text>
|
|
||||||
<Pressable onPress={dismiss}>
|
|
||||||
<Icon color={Colors.offsync} size={20} source="close" />
|
|
||||||
</Pressable>
|
|
||||||
</Surface>
|
</Surface>
|
||||||
</View>
|
)}
|
||||||
)}
|
{state.layout === 'large' && (
|
||||||
<Calling callCard={callCard} />
|
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
</View>
|
<View style={styles.container}>
|
||||||
|
<DetailsScreen nav={sessionNav} />
|
||||||
|
</View>
|
||||||
|
</NavigationContainer>
|
||||||
|
)}
|
||||||
|
{ disconnected && showDisconnected && !dismissed && (
|
||||||
|
<View style={styles.alert}>
|
||||||
|
<Surface elevation={5} style={styles.alertArea}>
|
||||||
|
<Icon color={Colors.offsync} size={20} source="alert-circle-outline" />
|
||||||
|
<Text style={styles.alertLabel}>{ state.strings.disconnected }</Text>
|
||||||
|
<Pressable onPress={dismiss}>
|
||||||
|
<Icon color={Colors.offsync} size={20} source="close" />
|
||||||
|
</Pressable>
|
||||||
|
</Surface>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Calling callCard={callCard} />
|
||||||
|
</View>
|
||||||
|
</RingContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +234,7 @@ function ContentTab({scheme, textCard, contentTab}: {scheme: string, textCard: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContactTab({scheme, textContact, callContact}: {scheme: string, textContact: (cardId: string)=>void, callContact: (cardId: string)=>void}) {
|
function ContactTab({scheme, textContact, callContact}: {scheme: string, textContact: (cardId: string)=>void, callContact: (card: Card)=>void}) {
|
||||||
const [contactParams, setContactParams] = useState({
|
const [contactParams, setContactParams] = useState({
|
||||||
guid: '',
|
guid: '',
|
||||||
} as ContactParams);
|
} as ContactParams);
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user