diff --git a/app/client/mobile/src/context/useRingContext.hook.ts b/app/client/mobile/src/context/useRingContext.hook.ts
index 9ca009b1..82817be1 100644
--- a/app/client/mobile/src/context/useRingContext.hook.ts
+++ b/app/client/mobile/src/context/useRingContext.hook.ts
@@ -34,7 +34,7 @@ export function useRingContext() {
const peerUpdate = useRef([] as {type: string, data?: any}[]);
const connecting = useRef(false);
const passive = useRef(false);
- const passiveTrack = useRef([] as MediaStreamTrack);
+ const passiveTracks = useRef([] as MediaStreamTrack[]);
const closing = useRef(false);
const [ringing, setRinging] = useState([] as { cardId: string, callId: string }[]);
const [cards, setCards] = useState([] as Card[]);
@@ -131,9 +131,10 @@ export function useRingContext() {
if (remoteStream.current) {
remoteStream.current.addTrack(data);
passive.current = false;
- passiveTrack.current.forEach(track => {
+ passiveTracks.current.forEach(track => {
peer.addTrack(track, sourceStream.current);
});
+ passiveTracks.current = [];
if (data.kind === 'video') {
InCallManager.setForceSpeakerphoneOn(true);
updateState({ remoteVideo: true });
@@ -142,7 +143,7 @@ export function useRingContext() {
break;
case 'local_track':
if (passive.current) {
- passiveTrack.push(data);
+ passiveTracks.push(data);
} else {
peer.addTrack(data, sourceStream.current);
}
@@ -167,7 +168,7 @@ export function useRingContext() {
const setup = async (link: Link, card: Card, polite: boolean) => {
passive.current = polite;
- passiveTrack.current = [];
+ passiveTracks.current = [];
remoteStream.current = new MediaStream();
localStream.current = new MediaStream();
sourceStream.current = await mediaDevices.getUserMedia({
diff --git a/app/client/web/src/call/Call.module.css b/app/client/web/src/call/Call.module.css
new file mode 100644
index 00000000..52e8604b
--- /dev/null
+++ b/app/client/web/src/call/Call.module.css
@@ -0,0 +1,66 @@
+.active {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+}
+.inactive {
+ display: none;
+}
+.call {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.controls {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ position: absolute;
+ bottom: 10%;
+ padding: 8;
+ gap: 12;
+ border-radius: 8;
+ opacity: 0.7;
+}
+.full {
+ width: 100%;
+ height: 100%;
+}
+.box {
+ position: absolute;
+ top: 10%;
+ right: 10%;
+ width: 20%;
+ height: 20%;
+}
+.titleView {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ paddingTop: 92;
+}
+.titleName {
+ font-size: 24;
+ padding-bottom: 32;
+}
+.titleImage {
+ width: 80%;
+ height: auto;
+ aspect-ratio: 1;
+ border-radius: 8;
+}
+.duration {
+ padding-top: 16;
+ font-size: 20;
+}
+.logoView {
+ height: 100%;
+ width: auto;
+ aspect-ratio: 1;
+}
diff --git a/app/client/web/src/call/Call.tsx b/app/client/web/src/call/Call.tsx
new file mode 100644
index 00000000..7bb8d52c
--- /dev/null
+++ b/app/client/web/src/call/Call.tsx
@@ -0,0 +1,82 @@
+import React, { useEffect, useState } from 'react';
+import { useCall } from './useCall.hook';
+import classes from './Call.module.css'
+import { Card as Contact } from '../card/Card';
+import { Colors } from '../constants/Colors';
+import { modals } from '@mantine/modals'
+
+export function Call() {
+ const { state, actions } = useCall();
+ const [ending, setEnding] = useState(false);
+ const [applyingAudio, setApplyingAudio] = useState(false);
+ const [applyingVideo, setApplyingVideo] = useState(false);
+ const [accepting, setAccepting] = useState(null as null|string);
+ const [ignoring, setIgnoring] = useState(null as null|string);
+ const [declining, setDeclining] = useState(null as null|string);
+
+ const showError = () => {
+ modals.openConfirmModal({
+ title: state.strings.operationFailed,
+ withCloseButton: true,
+ overlayProps: {
+ backgroundOpacity: 0.55,
+ blur: 3,
+ },
+ children: {state.strings.tryAgain},
+ cancelProps: { display: 'none' },
+ confirmProps: { display: 'none' },
+ })
+ }
+
+ const toggleAudio = async () => {
+ if (!applyingAudio) {
+ setApplyingAudio(true);
+ try {
+ if (state.audioEnabled) {
+ await actions.disableAudio();
+ } else if (!state.audioEnabled) {
+ await actions.enableAudio();
+ }
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setApplyingAudio(false);
+ }
+ }
+
+ const toggleVideo = async () => {
+ if (!applyingVideo) {
+ setApplyingVideo(true);
+ try {
+ if (state.videoEnabled) {
+ await actions.disableVideo();
+ } else if (!state.videoEnabled) {
+ await actions.enableVideo();
+ }
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setApplyingVideo(false);
+ }
+ }
+
+ const end = async () => {
+ if (!ending) {
+ setEnding(true);
+ try {
+ await actions.end();
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setEnding(false);
+ }
+ }
+
+ return (
+
+ );
+}
+
diff --git a/app/client/web/src/call/useCall.hook.ts b/app/client/web/src/call/useCall.hook.ts
new file mode 100644
index 00000000..dd9a6be6
--- /dev/null
+++ b/app/client/web/src/call/useCall.hook.ts
@@ -0,0 +1,63 @@
+import { useState, useContext, useEffect, useRef } from 'react'
+import { RingContext } from '../context/RingContext'
+import { DisplayContext } from '../context/DisplayContext'
+import { ContextType } from '../context/ContextType'
+import { Card } from 'databag-client-sdk';
+
+export function useCall() {
+ const ring = useContext(RingContext) as ContextType;
+ const display = useContext(DisplayContext) as ContextType;
+ const offsetTime = useRef(0);
+ const offset = useRef(false);
+
+ const [state, setState] = useState({
+ strings: display.state.strings,
+ calls: [] as { callId: string, card: Card }[],
+ calling: null as null | Card,
+ localStream: null as null|MediaStream,
+ remoteStream: null as null|MediaStream,
+ remoteVideo: false,
+ localVideo: false,
+ audioEnabled: false,
+ videoEnabled: false,
+ connected: false,
+ duration: 0,
+ failed: false,
+ width: 0,
+ height: 0,
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const updateState = (value: any) => {
+ setState((s) => ({ ...s, ...value }))
+ }
+
+ useEffect(() => {
+ const { width, height, strings } = display.state;
+ updateState({ width, height, strings });
+ }, [display.state]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (offset.current) {
+ const now = new Date();
+ const duration = Math.floor((now.getTime() / 1000) - offsetTime.current);
+ updateState({ duration });
+ }
+ }, 1000);
+ return () => {
+ clearInterval(interval);
+ }
+ }, []);
+
+ useEffect(() => {
+ const { calls, calling, fullscreen, localStream, remoteStream, remoteVideo, localVideo, audioEnabled, videoEnabled, connected, connectedTime, failed } = ring.state;
+ offsetTime.current = connectedTime;
+ offset.current = connected;
+ const duration = connected ? Math.floor(((new Date()).getTime() / 1000) - connectedTime) : 0;
+ updateState({ calls, calling, fullscreen, duration, localStream, remoteStream, remoteVideo, localVideo, audioEnabled, videoEnabled, connected, failed });
+ }, [ring.state]);
+
+ 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 cec3e93b..8b5f5ae1 100644
--- a/app/client/web/src/contacts/Contacts.tsx
+++ b/app/client/web/src/contacts/Contacts.tsx
@@ -38,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: (card: Card)=>void }) {
+export function Contacts({ openRegistry, openContact, textContact }: { openRegistry: ()=>void; openContact: (params: ProfileParams)=>void, textContact: (cardId: string)=>void }) {
const { state, actions } = useContacts()
const cards = state.filtered.map((card, idx) => {
@@ -48,7 +48,7 @@ export function Contacts({ openRegistry, openContact, textContact, callContact }
const phone =
const text =
return [
- callContact(card)} strings={state.strings} />,
+ actions.call(card)} strings={state.strings} />,
textContact(card.cardId)} strings={state.strings} />,
]
} else if (status === 'offsync') {
diff --git a/app/client/web/src/contacts/useContacts.hook.ts b/app/client/web/src/contacts/useContacts.hook.ts
index bb3e0f26..47bc8141 100644
--- a/app/client/web/src/contacts/useContacts.hook.ts
+++ b/app/client/web/src/contacts/useContacts.hook.ts
@@ -1,5 +1,6 @@
import { useState, useContext, useEffect } from 'react'
import { AppContext } from '../context/AppContext'
+import { RingContext } from '../context/RingContext'
import { DisplayContext } from '../context/DisplayContext'
import { ContextType } from '../context/ContextType'
import { Card } from 'databag-client-sdk'
@@ -7,6 +8,7 @@ import { Card } from 'databag-client-sdk'
export function useContacts() {
const app = useContext(AppContext) as ContextType
const display = useContext(DisplayContext) as ContextType
+ const ring = useContext(RingContext) as ContextType
const [state, setState] = useState({
strings: display.state.strings,
cards: [] as Card[],
@@ -64,6 +66,9 @@ export function useContacts() {
}, [state.sortAsc, state.filter, state.cards])
const actions = {
+ call: async (card: Card) => {
+ await ring.actions.call(card);
+ },
toggleSort: () => {
const sortAsc = !state.sortAsc
updateState({ sortAsc })
diff --git a/app/client/web/src/context/useRingContext.hook.ts b/app/client/web/src/context/useRingContext.hook.ts
index aa757b70..addc06ef 100644
--- a/app/client/web/src/context/useRingContext.hook.ts
+++ b/app/client/web/src/context/useRingContext.hook.ts
@@ -18,6 +18,8 @@ export function useRingContext() {
const peerUpdate = useRef([] as {type: string, data?: any}[]);
const connecting = useRef(false);
const closing = useRef(false);
+ const passive = useRef(false);
+ const passiveTracks = useRef([] as { track: MediaStreamTrack, stream: MediaStream }[]);
const [ringing, setRinging] = useState([] as { cardId: string, callId: string }[]);
const [cards, setCards] = useState([] as Card[]);
@@ -32,6 +34,7 @@ export function useRingContext() {
videoEnabled: false,
connected: false,
failed: false,
+ fullscreen: false,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -124,13 +127,22 @@ export function useRingContext() {
case 'remote_track':
if (remoteStream.current) {
remoteStream.current.addTrack(data);
+ passive.current = false;
+ passiveTracks.current.forEach(data => {
+ peer.addTrack(data.track, data.stream);
+ });
+ passiveTracks.current = [];
if (data.kind === 'video') {
updateState({ remoteVideo: true });
}
}
break;
case 'local_track':
- peer.addTrack(data.track, data.stream);
+ if (passive.current) {
+ passiveTracks.current.push(data);
+ } else {
+ peer.addTrack(data.track, data.stream);
+ }
if (data.track.kind === 'audio') {
localAudio.current = data.track;
}
@@ -153,7 +165,8 @@ export function useRingContext() {
}
}
- const setup = async (link: Link, card: Card) => {
+ const setup = async (link: Link, card: Card, polite: boolean) => {
+ passive.current = polite;
localAudio.current = null;
localVideo.current = null;
localStream.current = null;
@@ -242,6 +255,9 @@ export function useRingContext() {
}, [app.state.session]);
const actions = {
+ setFullscreen: (fullscreen: boolean) => {
+ updateState({ fullscreen });
+ },
ignore: async (callId: string, card: Card) => {
const ring = app.state.session.getRing();
await ring.ignore(card.cardId, callId);
@@ -262,7 +278,7 @@ export function useRingContext() {
const { cardId, node } = card;
const ring = app.state.session.getRing();
const link = await ring.accept(cardId, callId, node);
- await setup(link, card);
+ await setup(link, card, true);
connecting.current = false;
} catch (err) {
connecting.current = false;
@@ -277,7 +293,7 @@ export function useRingContext() {
connecting.current = true;
const contact = app.state.session.getContact();
const link = await contact.callCard(card.cardId);
- await setup(link, card);
+ await setup(link, card, false);
connecting.current = false;
} catch (err) {
connecting.current = false;
diff --git a/app/client/web/src/ring/Ring.module.css b/app/client/web/src/ring/Ring.module.css
new file mode 100644
index 00000000..50d83d6a
--- /dev/null
+++ b/app/client/web/src/ring/Ring.module.css
@@ -0,0 +1,57 @@
+active {
+ width: 100%;
+ height: 64px;
+ max-width: 500px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+inactive {
+ display: none;
+}
+ring {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ border-radius: 16px;
+ padding-left: 16px;
+ padding-right: 8px;
+}
+card {
+ padding: 8px;
+ width: 100%;
+}
+circleIcon {
+}
+flipIcon {
+ transform: rotate(135deg);
+}
+end {
+}
+name {
+ flex-grow: 1;
+ flex-shrink: 1;
+ display: flex;
+ flex-direction: row;
+ padding-left: 8px;
+}
+nameSet {
+ font-size: 20px;
+}
+nameUnset {
+ font-size: 20px;
+ font-style: italic;
+}
+status {
+ width: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+duration {
+ color: Colors.primary;
+ font-size: 20px;
+}
diff --git a/app/client/web/src/ring/Ring.tsx b/app/client/web/src/ring/Ring.tsx
new file mode 100644
index 00000000..72092bbe
--- /dev/null
+++ b/app/client/web/src/ring/Ring.tsx
@@ -0,0 +1,118 @@
+import React, { useEffect, useState } from 'react';
+import { useRing } from './useRing.hook';
+import classes from './Ring.module.css';
+import { Card as Contact } from '../card/Card';
+import { Colors } from '../constants/Colors';
+import { modals } from '@mantine/modals'
+import { Loader, Image, Text, ActionIcon } from '@mantine/core'
+import { IconEyeX, IconPhone, IconPhoneOff, IconMicrophone, IconMicrophoneOff } from '@tabler/icons-react'
+
+export function Ring() {
+ const { state, actions } = useRing();
+ const [ending, setEnding] = useState(false);
+ const [applyingAudio, setApplyingAudio] = useState(false);
+ const [accepting, setAccepting] = useState(null as null|string);
+ const [ignoring, setIgnoring] = useState(null as null|string);
+ const [declining, setDeclining] = useState(null as null|string);
+
+ const showError = () => {
+ modals.openConfirmModal({
+ title: state.strings.operationFailed,
+ withCloseButton: true,
+ overlayProps: {
+ backgroundOpacity: 0.55,
+ blur: 3,
+ },
+ children: {state.strings.tryAgain},
+ cancelProps: { display: 'none' },
+ confirmProps: { display: 'none' },
+ })
+ }
+
+ const toggleAudio = async () => {
+ if (!applyingAudio) {
+ setApplyingAudio(true);
+ try {
+ if (state.audioEnabled) {
+ await actions.disableAudio();
+ } else if (!state.audioEnabled) {
+ await actions.enableAudio();
+ }
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setApplyingAudio(false);
+ }
+ }
+
+ const end = async () => {
+ if (!ending) {
+ setEnding(true);
+ try {
+ await actions.end();
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setEnding(false);
+ }
+ }
+
+ const accept = async (callId, card) => {
+ if (!accepting) {
+ setAccepting(callId);
+ try {
+ await actions.accept(callId, card);
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setAccepting(null);
+ }
+ }
+
+ const ignore = async (callId, card) => {
+ if (!ignoring) {
+ setIgnoring(callId);
+ try {
+ await actions.ignore(callId, card);
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setIgnoring(null);
+ }
+ }
+
+ const decline = async (callId, card) => {
+ if (!declining) {
+ setDeclining(callId);
+ try {
+ await actions.decline(callId, card);
+ } catch (err) {
+ console.log(err);
+ showError();
+ }
+ setDeclining(null);
+ }
+ }
+
+ const calls = state.calls.map((ring, index) => {
+ const { name, handle, node, imageUrl } = ring.card;
+ const ignoreButton = ignore(ring)} color={Colors.pending}>
+ const declineButton = decline(ring)} color={Colors.offsync}>
+ const acceptButton = accept(ring)} color={Colors.primary}>
+
+ return (
+
+
+
+ )
+ });
+
+ return (
+
+ );
+}
+
diff --git a/app/client/web/src/ring/useRing.hook.ts b/app/client/web/src/ring/useRing.hook.ts
new file mode 100644
index 00000000..47aca1e2
--- /dev/null
+++ b/app/client/web/src/ring/useRing.hook.ts
@@ -0,0 +1,53 @@
+import { useState, useContext, useEffect, useRef } from 'react'
+import { RingContext } from '../context/RingContext'
+import { DisplayContext } from '../context/DisplayContext'
+import { ContextType } from '../context/ContextType'
+import { Card } from 'databag-client-sdk';
+
+export function useRing() {
+ const ring = useContext(RingContext) as ContextType;
+ const display = useContext(DisplayContext) as ContextType;
+ const offsetTime = useRef(0);
+ const offset = useRef(false);
+
+ const [state, setState] = useState({
+ strings: display.state.strings,
+ calls: [] as { callId: string, card: Card }[],
+ calling: null as null | Card,
+ remoteVideo: false,
+ localVideo: false,
+ audioEnabled: false,
+ connected: false,
+ duration: 0,
+ failed: false,
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const updateState = (value: any) => {
+ setState((s) => ({ ...s, ...value }))
+ }
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (offset.current) {
+ const now = new Date();
+ const duration = Math.floor((now.getTime() / 1000) - offsetTime.current);
+ updateState({ duration });
+ }
+ }, 1000);
+ return () => {
+ clearInterval(interval);
+ }
+ }, []);
+
+ useEffect(() => {
+ const { calls, calling, localVideo, remoteVideo, audioEnabled, connected, connectedTime, failed } = ring.state;
+ offsetTime.current = connectedTime;
+ offset.current = connected;
+ const duration = connected ? Math.floor(((new Date()).getTime() / 1000) - connectedTime) : 0;
+ updateState({ calls, calling, duration, localVideo, remoteVideo, audioEnabled, connected, failed });
+ }, [ring.state]);
+
+ const actions = ring.actions;
+ return { state, actions };
+}
diff --git a/app/client/web/src/session/Session.module.css b/app/client/web/src/session/Session.module.css
index 1bdfda4a..2145b571 100644
--- a/app/client/web/src/session/Session.module.css
+++ b/app/client/web/src/session/Session.module.css
@@ -26,13 +26,21 @@
}
}
- .show {
+ .body {
display: flex;
width: 100%;
height: calc(100% - 48px);
position: absolute;
top: 0;
left: 0;
+ flex-direction: column;
+ }
+
+ .show {
+ display: flex;
+ flex-grow: 1;
+ height: 10%;
+ position: relative;
}
.hide {
@@ -63,10 +71,18 @@
.right {
height: 100%;
- display: flex;
flex-grow: 1;
min-width: 0;
background: var(--mantine-color-surface-3);
+ display: flex;
+ flex-direction: column;
+
+ .conversation {
+ display: flex;
+ height: 0;
+ flex-grow: 1;
+ min-width: 0;
+ }
}
}
diff --git a/app/client/web/src/session/Session.tsx b/app/client/web/src/session/Session.tsx
index e34dda11..5147ff9b 100644
--- a/app/client/web/src/session/Session.tsx
+++ b/app/client/web/src/session/Session.tsx
@@ -17,7 +17,8 @@ import { Conversation } from '../conversation/Conversation'
import { Focus, Card } from 'databag-client-sdk'
import { useDisclosure } from '@mantine/hooks'
import { IconAlertCircle } from '@tabler/icons-react'
-import { Calling } from '../calling/Calling';
+import { Ring } from '../ring/Ring';
+import { Call } from '../call/Call';
export function Session() {
const { state } = useSession();
@@ -29,7 +30,6 @@ 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({ card: null} as {card: null|Card});
const textContact = (cardId: string) => {
console.log("MESSAGE: ", cardId);
@@ -38,63 +38,61 @@ export function Session() {
setTab('content');
}
- const callContact = (card: Card) => {
- setCallCard({ card });
- }
-
return (
{state.layout === 'small' && (
<>
-
-
-
-
- {state.focus && (
+
+
+
-
+
- )}
- {details && (
+ {state.focus && (
+
+
+
+ )}
+ {details && (
+
+
+
+ )}
+
+
-
-
-
- {
- setProfileParams(params)
- openProfile()
- }}
- />
-
- {registry && (
+
- {
setProfileParams(params)
openProfile()
}}
/>
- )}
- {profile && (
-
- )}
+ {registry && (
+
+ {
+ setProfileParams(params)
+ openProfile()
+ }}
+ />
+
+ )}
+ {profile && (
+
+ )}
+
{tab === 'content' && (
@@ -140,11 +138,15 @@ export function Session() {
-
{state.focus && }
+
+
+
+ {state.focus && }
+
+
{
@@ -189,7 +191,7 @@ export function Session() {
)}
-
+
)