diff --git a/app/client/web/src/calling/Calling.tsx b/app/client/web/src/calling/Calling.tsx
index 5df66458..f67108ef 100644
--- a/app/client/web/src/calling/Calling.tsx
+++ b/app/client/web/src/calling/Calling.tsx
@@ -1,13 +1,14 @@
import React, { useEffect, useRef, useState } from 'react';
import classes from './Calling.module.css'
-import { useCalling, type Ring } from './useCalling.hook';
-import { Card } from '../card/Card';
+import { type Card } from 'databag-client-sdk'
+import { useCalling } from './useCalling.hook';
+import { Card as Contact } from '../card/Card';
import { Loader, Image, Text, ActionIcon } from '@mantine/core'
import { IconEyeX, IconPhone, IconPhoneOff, IconMicrophone, IconMicrophoneOff, IconVideo, IconVideoOff } from '@tabler/icons-react'
import { modals } from '@mantine/modals'
import { Colors } from '../constants/Colors'
-export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
+export function Calling({ callCard }: { callCard: { card: null|Card }}) {
const [connecting, setConnecting] = useState(false);
const [ending, setEnding] = useState(false);
const [applyingVideo, setApplyingVideo] = useState(false);
@@ -80,11 +81,11 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
}
}
- const call = async (cardId: string) => {
+ const call = async (card: Card) => {
if (!connecting) {
setConnecting(true);
try {
- await actions.call(cardId);
+ await actions.call(card);
} catch (err) {
console.log(err);
showError();
@@ -93,7 +94,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
}
}
- const accept = async (ring: Ring) => {
+ const accept = async (ring: { callId: string, card: Card }) => {
if (!accepting) {
setAccepting(ring.callId);
try {
@@ -106,7 +107,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
}
}
- const ignore = async (ring: Ring) => {
+ const ignore = async (ring: { callId: string, card: Card }) => {
if (!ignoring) {
setIgnoring(ring.callId);
try {
@@ -119,7 +120,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
}
}
- const decline = async (ring: Ring) => {
+ const decline = async (ring: { callId: string, card: Card }) => {
if (!declining) {
setDeclining(ring.callId);
try {
@@ -133,8 +134,8 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
}
useEffect(() => {
- if (callCard?.cardId) {
- call(callCard.cardId);
+ if (callCard?.card) {
+ call(callCard.card);
}
}, [callCard]);
@@ -162,7 +163,7 @@ export function Calling({ callCard }: { callCard: { cardId: null|string }}) {
return (
-
+
)
});
diff --git a/app/client/web/src/calling/useCalling.hook.ts b/app/client/web/src/calling/useCalling.hook.ts
index 03bd2caa..dcca9085 100644
--- a/app/client/web/src/calling/useCalling.hook.ts
+++ b/app/client/web/src/calling/useCalling.hook.ts
@@ -1,33 +1,17 @@
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';
-
-export type Ring = {
- callId: string,
- card: Card,
-}
+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 as null|MediaStream);
- const localAudio = useRef(null as null|MediaStreamTrack);
- const localVideo = useRef(null as null|MediaStreamTrack);
- const remoteStream = useRef(null as null|MediaStream);
- const updatingPeer = useRef(false);
- const peerUpdate = useRef([] as {type: string, data?: any}[]);
const [state, setState] = useState({
strings: display.state.strings,
- ringing: [] as { cardId: string, callId: string }[],
- calls: [] as Ring[],
- cards: [] as Card[],
+ calls: [] as { callId: string, card: Card }[],
calling: null as null | Card,
- failed: false,
- loaded: false,
localStream: null as null|MediaStream,
remoteStream: null as null|MediaStream,
localVideo: false,
@@ -35,6 +19,7 @@ export function useCalling() {
audioEnabled: false,
videoEnabled: false,
connected: false,
+ failed: false,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -43,337 +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 getAudioStream = async (audioId: null|string) => {
- try {
- if (audioId) {
- return await navigator.mediaDevices.getUserMedia({ video: false, audio: { deviceId: audioId } });
- }
- }
- catch (err) {
- console.log(err);
- }
- return await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
- }
-
- const getVideoStream = async (videoId: null|string) => {
- try {
- if (videoId) {
- return await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoId }, audio: false });
- }
- }
- catch (err) {
- console.log(err);
- }
- return await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
- }
-
- const linkStatus = async (status: string) => {
- if (call.current) {
- const { policy, peer, link } = call.current;
- if (status === 'connected') {
- localVideo.current = null;
- localStream.current = null;
- remoteStream.current = new MediaStream();
- updateState({ localStream: localStream.current, remoteStream: remoteStream.current,
- audioEnabled: false, videoEnabled: false, localVideo: false, remoteVideo: false, connected: true });
-
- try {
- const audioStream = await getAudioStream(null);
- const audioTrack = audioStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'audio');
- if (audioTrack) {
- localAudio.current = audioTrack;
- }
- if (localAudio.current) {
- localAudio.current.enabled = true;
- await updatePeer('local_track', { track: audioTrack, stream: audioStream });
- updateState({ audioEnabled: true });
- }
- } catch (err) {
- console.log(err);
- }
- } else if (status === 'closed') {
- updatePeer('close');
- }
- }
- }
-
- const linkMessage = async (message: any) => {
- if (call.current) {
- const { peer, link, policy, candidates } = 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) {
- for (const candidate of candidates) {
- await peer.addIceCandidate(candidate);
- };
- call.current.candidates = [];
- }
- } 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: RTCIceCandidate) => {
- 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();
- await peer.setLocalDescription(description);
- await link.sendMessage({ description });
- } catch (err) {
- console.log(err);
- }
- }
- }
-
- const peerTrack = async (track: MediaStreamTrack, stream: MediaStream) => {
- if (call.current && localStream.current) {
- try {
- const { peer } = call.current;
- peer.addTrack(track, stream);
- } 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() || { type: '' };
- if (type === 'negotiate') {
- await peerNegotiate();
- } else if (type === 'candidate') {
- await peerCandidate(data);
- } else if (type === 'message') {
- await linkMessage(data);
- } else if (type === 'remote_track') {
- if (remoteStream.current) {
- remoteStream.current.addTrack(data);
- if (data.kind === 'video') {
- updateState({ remoteVideo: true });
- }
- }
- } else if (type === 'local_track') {
- await peerTrack(data.track, data.stream);
- } else if (type === 'close' && call.current) {
- peerUpdate.current = [];
- const { peer, link } = call.current;
- call.current = null;
- try {
- peer.close();
- link.close();
- } catch (err) {
- console.log(err);
- }
- if (localVideo.current) {
- localVideo.current.stop();
- localVideo.current = null;
- }
- if (localAudio.current) {
- localAudio.current.stop();
- localAudio.current = null;
- }
- localStream.current = null;
- remoteStream.current = null,
- updateState({ calling: null, failed: false, localStream: null, remoteStream: null, localVideo: false, remoteVideo: false });
- }
- }
- 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');
- }
- await updatePeer('close');
- },
- 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 = [] as RTCIceCandidate[];
- call.current = { policy, peer, link, candidates };
- link.setStatusListener(linkStatus);
- link.setMessageListener((msg: any) => 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 = [] as RTCIceCandidate[];
- call.current = { policy, peer, link, candidates };
- link.setStatusListener(linkStatus);
- link.setMessageListener((msg: any) => updatePeer('message', msg));
- updateState({ calling: card, connected: false });
- },
- enableAudio: async () => {
- if (!call.current) {
- throw new Error('cannot unmute audio');
- }
- if (!localAudio.current) {
- const audioStream = await getAudioStream(null);
- const audioTrack = audioStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'audio');
- if (!audioTrack) {
- throw new Error('no available audio track');
- }
- localAudio.current = audioTrack;
- localStream.current = audioStream;
- updatePeer('local_track', { track: audioTrack, stream: audioStream });
- updateState({ localAudio: true, localStream: audioStream, audioEnabled: true });
- } else {
- 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 (!call.current) {
- throw new Error('cannot start video');
- }
- if (!localVideo.current) {
- const videoStream = await getVideoStream(null);
- const videoTrack = videoStream.getTracks().find((track: MediaStreamTrack) => track.kind === 'video');
- if (videoTrack) {
- localVideo.current = videoTrack;
- localStream.current = videoStream;
- updatePeer('local_track', { track: videoTrack, stream: videoStream });
- updateState({ localVideo: true, localStream: videoStream });
- }
- }
- if (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 }
+ const actions = ring.actions;
+ return { state, actions };
}
diff --git a/app/client/web/src/contacts/Contacts.tsx b/app/client/web/src/contacts/Contacts.tsx
index d05ecb6c..cec3e93b 100644
--- a/app/client/web/src/contacts/Contacts.tsx
+++ b/app/client/web/src/contacts/Contacts.tsx
@@ -3,7 +3,8 @@ import { useContacts } from './useContacts.hook'
import { Text, ActionIcon, TextInput, Button } from '@mantine/core'
import { IconUserCheck, IconCancel, IconRefresh, IconSearch, IconUserPlus, IconSortAscending, IconSortDescending, IconMessage2, IconPhone } from '@tabler/icons-react'
import classes from './Contacts.module.css'
-import { Card } from '../card/Card'
+import { type Card } from 'databag-client-sdk';
+import { Card as Contact } from '../card/Card'
import { ProfileParams } from '../profile/Profile'
import { Colors } from '../constants/Colors'
import { modals } from '@mantine/modals'
@@ -37,7 +38,7 @@ function Action({ icon, color, strings, select }: { icon: ReactNode; color: stri
)
}
-export function Contacts({ openRegistry, openContact, textContact, callContact }: { openRegistry: ()=>void; openContact: (params: ProfileParams)=>void, textContact: (cardId: string)=>void, callContact: (cardId: string)=>void }) {
+export function Contacts({ openRegistry, openContact, textContact, callContact }: { openRegistry: ()=>void; openContact: (params: ProfileParams)=>void, textContact: (cardId: string)=>void, callContact: (card: Card)=>void }) {
const { state, actions } = useContacts()
const cards = state.filtered.map((card, idx) => {
@@ -47,7 +48,7 @@ export function Contacts({ openRegistry, openContact, textContact, callContact }
const phone =
const text =
return [
- callContact(card.cardId)} strings={state.strings} />,
+ callContact(card)} strings={state.strings} />,
textContact(card.cardId)} strings={state.strings} />,
]
} else if (status === 'offsync') {
@@ -75,7 +76,7 @@ export function Contacts({ openRegistry, openContact, textContact, callContact }
}
return (
-
+
)
})
diff --git a/app/client/web/src/context/useRingContext.hook.ts b/app/client/web/src/context/useRingContext.hook.ts
index 064837c3..aa757b70 100644
--- a/app/client/web/src/context/useRingContext.hook.ts
+++ b/app/client/web/src/context/useRingContext.hook.ts
@@ -18,11 +18,11 @@ export function useRingContext() {
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({
- ringing: [] as { cardId: string, callId: string }[],
calls: [] as { callId: string, cardId: string}[],
- cards: [] as Card[],
calling: null as null | Card,
localStream: null as null|MediaStream,
remoteStream: null as null|MediaStream,
@@ -40,11 +40,10 @@ export function useRingContext() {
}
useEffect(() => {
- const calls = state.ringing
- .map(ring => ({ callId: ring.callId, card: state.cards.find(card => ring.cardId === card.cardId) }) )
+ 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 });
- }, [state.ringing, state.cards]);
+ }, [ringing, cards]);
const getAudioStream = async (audioId: null|string) => {
try {
@@ -74,10 +73,10 @@ export function useRingContext() {
if (call.current) {
const { peer, link } = call.current;
if (status === 'connected') {
- await updatePeer('open');
+ updateState({ connected: true });
await actions.enableAudio();
} else if (status === 'closed') {
- await updatePeer('close');
+ await cleanup;
}
}
}
@@ -141,11 +140,6 @@ export function useRingContext() {
updateState({ localVideo: true, localStream: localStream.current })
}
break;
- case 'open':
- updateState({ connected: true });
- case 'close':
- await cleanup();
- break;
default:
console.log('unknown event');
break;
@@ -229,18 +223,18 @@ export function useRingContext() {
useEffect(() => {
if (app.state.session) {
- const setRinging = (ringing: { cardId: string, callId: string }[]) => {
- updateState({ ringing });
+ const setRing = (ringing: { cardId: string, callId: string }[]) => {
+ setRinging(ringing);
}
const setContacts = (cards: Card[]) => {
- updateState({ cards });
+ setCards(cards);
}
const ring = app.state.session.getRing();
ring.addRingingListener(setRinging);
const contact = app.state.session.getContact();
contact.addCardListener(setContacts);
return () => {
- ring.removeRingingListener(setRinging);
+ ring.removeRingingListener(setRing);
contact.removeCardListener(setContacts);
cleanup();
}
@@ -275,18 +269,14 @@ export function useRingContext() {
throw err;
}
},
- call: async (cardId: string) => {
+ call: async (card: Card) => {
if (connecting.current || closing.current || call.current) {
throw new Error('not ready make calls');
}
try {
connecting.current = true;
- 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 link = await contact.callCard(card.cardId);
await setup(link, card);
connecting.current = false;
} catch (err) {
diff --git a/app/client/web/src/session/Session.tsx b/app/client/web/src/session/Session.tsx
index 9b212560..e34dda11 100644
--- a/app/client/web/src/session/Session.tsx
+++ b/app/client/web/src/session/Session.tsx
@@ -14,7 +14,7 @@ import { Profile, ProfileParams } from '../profile/Profile'
import { Details } from '../details/Details';
import { Content } from '../content/Content'
import { Conversation } from '../conversation/Conversation'
-import { Focus } from 'databag-client-sdk'
+import { Focus, Card } from 'databag-client-sdk'
import { useDisclosure } from '@mantine/hooks'
import { IconAlertCircle } from '@tabler/icons-react'
import { Calling } from '../calling/Calling';
@@ -29,7 +29,7 @@ export function Session() {
const [details, { open: openDetails, close: closeDetails }] = useDisclosure(false)
const [profile, { open: openProfile, close: closeProfile }] = useDisclosure(false)
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 textContact = (cardId: string) => {
console.log("MESSAGE: ", cardId);
@@ -38,8 +38,8 @@ export function Session() {
setTab('content');
}
- const callContact = (cardId: string) => {
- setCallCard({ cardId });
+ const callContact = (card: Card) => {
+ setCallCard({ card });
}
return (