mirror of
https://github.com/balzack/databag.git
synced 2025-04-23 01:55:17 +00:00
refactored calls into ring context
This commit is contained in:
parent
45b20948dc
commit
56b701905d
@ -2250,7 +2250,7 @@ SPEC CHECKSUMS:
|
||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
||||
Yoga: a9ef4f5c2cd79ad812110525ef61048be6a582a4
|
||||
Yoga: b05994d1933f507b0a28ceaa4fdb968dc18da178
|
||||
|
||||
PODFILE CHECKSUM: 9cf7373afef7b881c911fda82ff1f94eacee3e98
|
||||
|
||||
|
@ -7,13 +7,13 @@ import {BlurView} from '@react-native-community/blur';
|
||||
import { Confirm } from '../confirm/Confirm';
|
||||
import { ActivityIndicator } from 'react-native-paper';
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { Colors } from '../constants/Colors';
|
||||
import { type Card } from 'databag-client-sdk';
|
||||
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";
|
||||
|
||||
export function Calling({ callCard }: { callCard: string }) {
|
||||
export function Calling({ callCard }: { callCard: null|Card }) {
|
||||
const { state, actions } = useCalling();
|
||||
const [alert, setAlert] = useState(false);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
@ -42,9 +42,9 @@ export function Calling({ callCard }: { callCard: string }) {
|
||||
if (!applyingVideo) {
|
||||
setApplyingVideo(true);
|
||||
try {
|
||||
if (state.video && state.videoEnabled) {
|
||||
if (state.videoEnabled) {
|
||||
await actions.disableVideo();
|
||||
} else if (state.video && !state.videoEnabled) {
|
||||
} else if (!state.videoEnabled) {
|
||||
await actions.enableVideo();
|
||||
}
|
||||
} catch (err) {
|
||||
@ -59,9 +59,9 @@ export function Calling({ callCard }: { callCard: string }) {
|
||||
if (!applyingAudio) {
|
||||
setApplyingAudio(true);
|
||||
try {
|
||||
if (state.audio && state.audioEnabled) {
|
||||
if (state.audioEnabled) {
|
||||
await actions.disableAudio();
|
||||
} else if (state.audio && !state.audioEnabled) {
|
||||
} else if (!state.audioEnabled) {
|
||||
await actions.enableAudio();
|
||||
}
|
||||
} catch (err) {
|
||||
@ -85,11 +85,11 @@ export function Calling({ callCard }: { callCard: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const call = async (cardId: string) => {
|
||||
const call = async (card) => {
|
||||
if (!connecting) {
|
||||
setConnecting(true);
|
||||
try {
|
||||
await actions.call(cardId);
|
||||
await actions.call(card);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setAlert(true);
|
||||
@ -149,9 +149,9 @@ export function Calling({ callCard }: { callCard: string }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { cardId } = callCard;
|
||||
if (cardId) {
|
||||
call(cardId);
|
||||
const { card } = callCard;
|
||||
if (card) {
|
||||
call(card);
|
||||
}
|
||||
}, [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)} />
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
});
|
||||
@ -199,27 +199,14 @@ export function Calling({ callCard }: { callCard: string }) {
|
||||
<View style={{ ...styles.container, backgroundColor: surface.base }}>
|
||||
<View style={{ ...styles.frame, top: frameOffset, width: frameWidth > 400 ? 400 : frameWidth, height: frameHeight > 400 ? 400 : frameHeight }}>
|
||||
<Image
|
||||
style={{ ...styles.image, opacity: state.loaded ? 1 : 0 }}
|
||||
style={styles.image}
|
||||
resizeMode="contain"
|
||||
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>
|
||||
)}
|
||||
{ state.calling && state.loaded && (
|
||||
{ state.calling && (
|
||||
<View style={{ ...styles.overlap, top: 64 }}>
|
||||
<View style={{backgroundColor: surface.title, borderRadius: 16 }}>
|
||||
{ state.calling.name && (
|
||||
@ -231,45 +218,37 @@ export function Calling({ callCard }: { callCard: string }) {
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{ state.calling && state.loaded && state.remote && (
|
||||
{ state.calling && state.remoteStream && state.remoteVideo && (
|
||||
<View style={{ ...styles.canvas, backgroundColor: surface.base }}>
|
||||
<RTCView
|
||||
style={styles.full}
|
||||
mirror={true}
|
||||
objectFit={'contain'}
|
||||
streamURL={state.remote.toURL()}
|
||||
/>
|
||||
<RTCView
|
||||
style={styles.full}
|
||||
mirror={true}
|
||||
objectFit={'contain'}
|
||||
streamURL={state.remoteStream.toURL()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{ state.calling && state.loaded && state.local && !state.remote && (
|
||||
{ state.calling && state.localStream && state.localVideo && (
|
||||
<View style={{ ...styles.canvas, backgroundColor: surface.base }}>
|
||||
<RTCView
|
||||
style={styles.full}
|
||||
mirror={true}
|
||||
objectFit={'contain'}
|
||||
streamURL={state.local.toURL()}
|
||||
/>
|
||||
<RTCView
|
||||
style={ state.remoteVideo ? styles.box : styles.full}
|
||||
mirror={true}
|
||||
objectFit={'contain'}
|
||||
streamURL={state.localStream.toURL()}
|
||||
zOrder={2}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{ state.calling && state.loaded && state.local && state.remote && (
|
||||
<RTCView
|
||||
style={{ ...styles.box, top: frameOffset }}
|
||||
mirror={true}
|
||||
objectFit={'contain'}
|
||||
streamURL={state.local.toURL()}
|
||||
zOrder={2}
|
||||
/>
|
||||
)}
|
||||
{ state.calling && state.loaded && (
|
||||
{ state.calling && (
|
||||
<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 }}>
|
||||
<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" 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.audioEnabled ? 'microphone' : 'microphone-off'} loading={applyingAudio} compact="true" mode="contained" size={32} onPress={toggleAudio} />
|
||||
<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} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{ state.calling && state.loaded && !state.connected && (
|
||||
{ state.calling && !state.connected && (
|
||||
<View style={{ ...styles.overlap, bottom: 24 }}>
|
||||
<View style={{backgroundColor: surface.title, borderRadius: 16 }}>
|
||||
<Text style={styles.connecting}>{ state.strings.connecting }</Text>
|
||||
|
@ -1,48 +1,25 @@
|
||||
import { useState, useContext, useEffect, useRef } from 'react'
|
||||
import { DisplayContext } from '../context/DisplayContext';
|
||||
import { AppContext } from '../context/AppContext'
|
||||
import { RingContext } from '../context/RingContext'
|
||||
import { DisplayContext } from '../context/DisplayContext'
|
||||
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';
|
||||
import { Card } from 'databag-client-sdk';
|
||||
|
||||
export function useCalling() {
|
||||
const app = useContext(AppContext) as ContextType;
|
||||
const ring = useContext(RingContext) 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({
|
||||
strings: {},
|
||||
ringing: [],
|
||||
calls: [],
|
||||
cards: [],
|
||||
strings: display.state.strings,
|
||||
calls: [] as { callId: string, card: Card }[],
|
||||
calling: null as null | Card,
|
||||
failed: false,
|
||||
loaded: false,
|
||||
panelOffset: 0,
|
||||
local: null,
|
||||
remote: null,
|
||||
audio: null,
|
||||
localStream: null as null|MediaStream,
|
||||
remoteStream: null as null|MediaStream,
|
||||
localVideo: false,
|
||||
remoteVideo: false,
|
||||
audioEnabled: false,
|
||||
video: null,
|
||||
videoEnabled: false,
|
||||
videoAdded: false,
|
||||
connected: false,
|
||||
failed: false,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -51,317 +28,10 @@ export function useCalling() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const calls = state.ringing
|
||||
.map(ring => ({ callId: ring.callId, card: state.cards.find(card => ring.cardId === card.cardId) }) )
|
||||
.filter(ring => (ring.card && !ring.card.blocked));
|
||||
updateState({ calls });
|
||||
}, [state.ringing, state.cards]);
|
||||
const { calls, calling, localStream, remoteStream, localVideo, remoteVideo, audioEnabled, videoEnabled, connected, failed } = ring.state;
|
||||
updateState({ calls, calling, localStream, remoteStream, localVideo, remoteVideo, audioEnabled, videoEnabled, connected, failed });
|
||||
}, [ring.state]);
|
||||
|
||||
useEffect(() => {
|
||||
const { strings } = display.state;
|
||||
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 }
|
||||
const actions = ring.actions;
|
||||
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} />;
|
||||
}
|
||||
|
||||
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 {state, actions} = useContacts();
|
||||
const [alert, setAlert] = useState(false);
|
||||
@ -74,7 +74,7 @@ export function Contacts({openRegistry, openContact, callContact, textContact}:
|
||||
key="call"
|
||||
icon="phone-outline"
|
||||
color={Colors.connected}
|
||||
select={()=>callContact(item.cardId)}
|
||||
select={()=>callContact(item)}
|
||||
/>,
|
||||
<Action
|
||||
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 {SafeAreaView, Pressable, View, useColorScheme} from 'react-native';
|
||||
import {RingContextProvider} from '../context/RingContext';
|
||||
import {styles} from './Session.styled';
|
||||
import {IconButton, Surface, Text, Icon} from 'react-native-paper';
|
||||
import {Settings} from '../settings/Settings';
|
||||
@ -12,7 +13,7 @@ import {Identity} from '../identity/Identity';
|
||||
import {Conversation} from '../conversation/Conversation';
|
||||
import {useSession} from './useSession.hook';
|
||||
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 {createDrawerNavigator} from '@react-navigation/drawer';
|
||||
import {createNativeStackNavigator} from '@react-navigation/native-stack';
|
||||
@ -33,7 +34,7 @@ export function Session() {
|
||||
const scheme = useColorScheme();
|
||||
const [tab, setTab] = useState('content');
|
||||
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 [disconnected, setDisconnected] = useState(false);
|
||||
const [showDisconnected, setShowDisconnected] = useState(false);
|
||||
@ -42,8 +43,8 @@ export function Session() {
|
||||
setTextCard({ cardId });
|
||||
}
|
||||
|
||||
const callContact = (cardId: null|string) => {
|
||||
setCallCard({ cardId });
|
||||
const callContact = (card: null|Card) => {
|
||||
setCallCard({ card });
|
||||
}
|
||||
|
||||
const sessionNav = {strings: state.strings, callContact, callCard, textContact, textCard};
|
||||
@ -77,126 +78,128 @@ export function Session() {
|
||||
}, [state.appState, state.sdkState]);
|
||||
|
||||
return (
|
||||
<View style={styles.session}>
|
||||
{state.layout !== 'large' && (
|
||||
<Surface elevation={3}>
|
||||
<SafeAreaView style={styles.full}>
|
||||
<View style={styles.screen}>
|
||||
<View
|
||||
style={{
|
||||
...styles.body,
|
||||
...showContent,
|
||||
}}>
|
||||
<ContentTab textCard={textCard} scheme={scheme} contentTab={contentTab} />
|
||||
<RingContextProvider>
|
||||
<View style={styles.session}>
|
||||
{state.layout !== 'large' && (
|
||||
<Surface elevation={3}>
|
||||
<SafeAreaView style={styles.full}>
|
||||
<View style={styles.screen}>
|
||||
<View
|
||||
style={{
|
||||
...styles.body,
|
||||
...showContent,
|
||||
}}>
|
||||
<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
|
||||
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>
|
||||
</SafeAreaView>
|
||||
</Surface>
|
||||
</View>
|
||||
)}
|
||||
<Calling callCard={callCard} />
|
||||
</View>
|
||||
)}
|
||||
{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>
|
||||
</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({
|
||||
guid: '',
|
||||
} as ContactParams);
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user