rendering ring indicator

This commit is contained in:
Roland Osborne 2025-02-06 14:21:03 -08:00
parent a717a6d88e
commit cda9f6078a
3 changed files with 92 additions and 30 deletions

View File

@ -35,6 +35,7 @@ export function useRingContext() {
connected: false,
failed: false,
fullscreen: false,
connectedTime: 0,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -76,8 +77,14 @@ export function useRingContext() {
if (call.current) {
const { peer, link } = call.current;
if (status === 'connected') {
updateState({ connected: true });
await actions.enableAudio();
const connectedTime = Math.floor((new Date()).getTime() / 1000);
updateState({ connected: true, connectedTime });
try {
await actions.enableAudio();
} catch (err) {
console.log('failed to enable audio on connection');
console.log(err);
}
} else if (status === 'closed') {
await cleanup;
}
@ -177,7 +184,7 @@ export function useRingContext() {
call.current = { peer, link, candidates };
link.setStatusListener(linkStatus);
link.setMessageListener((msg: any) => updatePeer('message', msg));
updateState({ calling: card, failed: false, connected: false,
updateState({ calling: card, failed: false, connected: false, connectedTime: 0,
audioEnabled: false, videoEnabled: false, localVideo: false, remoteVideo: false,
localStream: localStream.current, remoteStream: remoteStream.current });
}
@ -204,7 +211,7 @@ export function useRingContext() {
localStream.current = null;
remoteStream.current = null,
peerUpdate.current = [];
updateState({ calling: null, failed: false, localStream: null, remoteStream: null, localVideo: false, remoteVideo: false });
updateState({ calling: null, connected: false, connectedTime: 0, failed: false, localStream: null, remoteStream: null, localVideo: false, remoteVideo: false });
closing.current = false;
}

View File

@ -1,57 +1,68 @@
active {
.active {
width: 100%;
height: 64px;
max-width: 500px;
display: flex;
align-items: center;
justify-content: center;
}
inactive {
.inactive {
display: none;
}
ring {
.ring {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
height: 48px;
max-width: 400px;
border-radius: 16px;
padding-left: 16px;
padding-right: 8px;
padding-left: 32px;
padding-right: 32px;
gap: 8px;
background-color: var(--mantine-color-surface-4);
}
card {
.card {
padding: 8px;
width: 100%;
height: 100%;
}
circleIcon {
}
flipIcon {
.off {
margin-top: 16px;
transform: rotate(135deg);
}
end {
.space {
position: relative;
padding: 8px;
}
name {
.circleIcon {
}
.flipIcon {
transform: rotate(135deg);
}
.end {
}
.name {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: row;
padding-left: 8px;
}
nameSet {
.nameSet {
font-size: 20px;
}
nameUnset {
.nameUnset {
font-size: 20px;
font-style: italic;
}
status {
.status {
width: 64px;
display: flex;
align-items: center;
justify-content: center;
}
duration {
.duration {
color: Colors.primary;
font-size: 20px;
}

View File

@ -5,7 +5,7 @@ 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'
import { IconEyeX, IconPhone, IconPhoneOff, IconArrowsMaximize, IconMicrophone, IconMicrophoneOff } from '@tabler/icons-react'
export function Ring() {
const { state, actions } = useRing();
@ -99,20 +99,64 @@ export function Ring() {
}
const calls = state.calls.map((ring, index) => {
const { name, handle, node, imageUrl } = ring.card;
const ignoreButton = <ActionIcon key="ignore" variant="subtle" loading={ignoring===ring.callId} onClick={()=>ignore(ring)} color={Colors.pending}><IconEyeX /></ActionIcon>
const declineButton = <div key="decline" className={classes.space}><ActionIcon variant="subtle" loading={declining===ring.callId} onClick={()=>decline(ring)} color={Colors.offsync}><IconPhone className={classes.off} /></ActionIcon></div>
const acceptButton = <ActionIcon key="accept" variant="subtle" loading={accepting===ring.callId} onClick={()=>accept(ring)} color={Colors.primary}><IconPhone /></ActionIcon>
const { callId, card } = ring;
const { name, handle, node, imageUrl } = card;
const ignoreButton = <ActionIcon key="ignore" variant="subtle" loading={ignoring===ring.callId} onClick={()=>ignore(callId, card)} color={Colors.pending}><IconEyeX /></ActionIcon>
const declineButton = <div key="decline" className={classes.space}><ActionIcon variant="subtle" loading={declining===ring.callId} onClick={()=>decline(callId, card)} color={Colors.offsync}><IconPhone className={classes.off} /></ActionIcon></div>
const acceptButton = <ActionIcon key="accept" variant="subtle" loading={accepting===ring.callId} onClick={()=>accept(callId, card)} color={Colors.primary}><IconPhone /></ActionIcon>
return (
<div key={index} className={classes.caller}>
<Contact className={classes.card} placeholder={''} imageUrl={imageUrl} name={name} node={node} handle={handle} actions={[ignoreButton, declineButton, acceptButton]} />
</div>
<Contact className={classes.card} placeholder={''} imageUrl={imageUrl} name={name} node={node} handle={handle} actions={[ignoreButton, declineButton, acceptButton]} />
)
});
return (
<div style={{ width: '100%', height: 14, backgroundColor: 'yellow' }} />
<div className={(accepting || state.calling || state.calls.length > 0) ? classes.active : classes.inactive}>
{ state.calls.length > 0 && !accepting && !state.calling && (
<div className={classes.ring}>
{ calls[0] }
</div>
)}
{ accepting && !state.calling && (
<div className={classes.ring}>
<Loader size={32} />
</div>
)}
{ state.calling && (
<div className={classes.ring}>
<ActionIcon variant="subtle" loading={applyingAudio} disabled={!state.connected} className={classes.circleIcon} color={Colors.primary}>
{ state.audioEnabled && (
<IconMicrophone />
)}
{ !state.audioEnabled && (
<IconMicrophoneOff />
)}
</ActionIcon>
<ActionIcon variant="subtle" disabled={!state.connected} className={classes.circleIcon} color={Colors.confirmed}>
<IconArrowsMaximize />
</ActionIcon>
<div className={classes.name}>
{ state.calling.name && (
<Text className={classes.nameSet}>{ state.calling.name }</Text>
)}
{ !state.calling.name && (
<Text className={classs.nameUnset}>{ state.strings.name }</Text>
)}
</div>
<div className={classes.status}>
{ state.connected && (
<Text className={classes.duration}>{ `${Math.floor(state.duration/60)}:${(state.duration % 60).toString().padStart(2, '0')}` }</Text>
)}
{ !state.connected && (
<Loader size={18} />
)}
</div>
<div className={classes.end}>
<ActionIcon variant="subtle" loading={ending} onClick={end} color={Colors.offsync}><IconPhone className={classes.off} /></ActionIcon>
</div>
</div>
)}
</div>
);
}