feat(call): implement elapsed time display for active calls and improve UI responsiveness with updated button styles

This commit is contained in:
2026-01-01 20:17:56 -06:00
parent 3794e5806b
commit bf94ceebbb
2 changed files with 67 additions and 15 deletions

View File

@@ -1,8 +1,8 @@
<template> <template>
<div <div
v-if="activeCall" v-if="activeCall"
class="fixed bottom-4 right-4 z-[100] w-72 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300" class="fixed bottom-4 right-4 z-[100] w-80 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
:class="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded }" :class="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded || wasDeclined }"
> >
<!-- Header --> <!-- Header -->
<div class="p-3 flex items-center bg-gray-50 dark:bg-zinc-800/50 border-b border-gray-100 dark:border-zinc-800"> <div class="p-3 flex items-center bg-gray-50 dark:bg-zinc-800/50 border-b border-gray-100 dark:border-zinc-800">
@@ -105,6 +105,12 @@
<span v-else-if="activeCall.status === 6">{{ $t("call.connected") }}</span> <span v-else-if="activeCall.status === 6">{{ $t("call.connected") }}</span>
<span v-else>{{ $t("call.status") }}: {{ activeCall.status }}</span> <span v-else>{{ $t("call.status") }}: {{ activeCall.status }}</span>
</div> </div>
<div
v-if="activeCall.status === 6 && !isEnded && elapsedTime"
class="text-xs text-gray-500 dark:text-zinc-400 mt-1 font-mono"
>
{{ elapsedTime }}
</div>
</div> </div>
<!-- Stats (only when connected and not minimized) --> <!-- Stats (only when connected and not minimized) -->
@@ -123,11 +129,11 @@
</div> </div>
<!-- Controls --> <!-- Controls -->
<div v-if="!isEnded" class="flex justify-center space-x-3"> <div v-if="!isEnded && !wasDeclined" class="flex flex-wrap justify-center gap-3">
<!-- Mute Mic --> <!-- Mute Mic -->
<button <button
type="button" type="button"
:title="isMicMuted ? 'Unmute Mic' : 'Mute Mic'" :title="isMicMuted ? $t('call.unmute_mic') : $t('call.mute_mic')"
class="p-3 rounded-full transition-all duration-200" class="p-3 rounded-full transition-all duration-200"
:class=" :class="
isMicMuted isMicMuted
@@ -142,7 +148,7 @@
<!-- Mute Speaker --> <!-- Mute Speaker -->
<button <button
type="button" type="button"
:title="isSpeakerMuted ? 'Unmute Speaker' : 'Mute Speaker'" :title="isSpeakerMuted ? $t('call.unmute_speaker') : $t('call.mute_speaker')"
class="p-3 rounded-full transition-all duration-200" class="p-3 rounded-full transition-all duration-200"
:class=" :class="
isSpeakerMuted isSpeakerMuted
@@ -206,9 +212,17 @@
class="size-5 shrink-0" class="size-5 shrink-0"
/> />
<MaterialDesignIcon v-else icon-name="account" class="size-5 text-blue-500 shrink-0" /> <MaterialDesignIcon v-else icon-name="account" class="size-5 text-blue-500 shrink-0" />
<div class="flex flex-col min-w-0">
<span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block"> <span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">
{{ activeCall.remote_identity_name || $t("call.unknown") }} {{ activeCall.remote_identity_name || $t("call.unknown") }}
</span> </span>
<span
v-if="activeCall.status === 6 && elapsedTime"
class="text-[10px] text-gray-500 dark:text-zinc-400 font-mono"
>
{{ elapsedTime }}
</span>
</div>
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<button <button
@@ -260,6 +274,7 @@ export default {
data() { data() {
return { return {
isMinimized: false, isMinimized: false,
elapsedTimeInterval: null,
}; };
}, },
computed: { computed: {
@@ -269,6 +284,23 @@ export default {
isSpeakerMuted() { isSpeakerMuted() {
return this.activeCall?.is_speaker_muted ?? false; return this.activeCall?.is_speaker_muted ?? false;
}, },
elapsedTime() {
if (!this.activeCall?.call_start_time) {
return null;
}
const elapsed = Math.floor(Date.now() / 1000 - this.activeCall.call_start_time);
return Utils.formatMinutesSeconds(elapsed);
},
},
mounted() {
this.elapsedTimeInterval = setInterval(() => {
this.$forceUpdate();
}, 1000);
},
beforeUnmount() {
if (this.elapsedTimeInterval) {
clearInterval(this.elapsedTimeInterval);
}
}, },
methods: { methods: {
formatDestinationHash(hash) { formatDestinationHash(hash) {

View File

@@ -135,6 +135,12 @@
</span> </span>
</template> </template>
</div> </div>
<div
v-if="activeCall && activeCall.status === 6 && !isCallEnded && elapsedTime"
class="text-gray-500 dark:text-zinc-400 mb-4 text-center font-mono text-lg"
>
{{ elapsedTime }}
</div>
<!-- settings during connected call --> <!-- settings during connected call -->
<div v-if="activeCall && activeCall.status === 6" class="mb-4"> <div v-if="activeCall && activeCall.status === 6" class="mb-4">
@@ -209,16 +215,16 @@
</div> </div>
<!-- actions --> <!-- actions -->
<div v-if="activeCall" class="mx-auto space-x-4"> <div v-if="activeCall" class="flex flex-wrap justify-center gap-4 mt-6">
<!-- answer call --> <!-- answer call -->
<button <button
v-if="activeCall.is_incoming && activeCall.status === 4" v-if="activeCall.is_incoming && activeCall.status === 4"
:title="$t('call.answer_call')" :title="$t('call.answer_call')"
type="button" type="button"
class="inline-flex items-center gap-x-2 rounded-2xl bg-green-600 px-6 py-4 text-lg font-bold text-white shadow-xl hover:bg-green-500 transition-all duration-200 animate-bounce" class="inline-flex items-center gap-x-2 rounded-2xl bg-green-600 px-5 py-3 text-base font-bold text-white shadow-xl hover:bg-green-500 transition-all duration-200 animate-bounce"
@click="answerCall" @click="answerCall"
> >
<MaterialDesignIcon icon-name="phone" class="size-6" /> <MaterialDesignIcon icon-name="phone" class="size-5" />
<span>{{ $t("call.accept") }}</span> <span>{{ $t("call.accept") }}</span>
</button> </button>
@@ -227,10 +233,10 @@
v-if="activeCall.is_incoming && activeCall.status === 4" v-if="activeCall.is_incoming && activeCall.status === 4"
:title="$t('call.send_to_voicemail')" :title="$t('call.send_to_voicemail')"
type="button" type="button"
class="inline-flex items-center gap-x-2 rounded-2xl bg-blue-600 px-6 py-4 text-lg font-bold text-white shadow-xl hover:bg-blue-500 transition-all duration-200" class="inline-flex items-center gap-x-2 rounded-2xl bg-blue-600 px-5 py-3 text-base font-bold text-white shadow-xl hover:bg-blue-500 transition-all duration-200"
@click="sendToVoicemail" @click="sendToVoicemail"
> >
<MaterialDesignIcon icon-name="voicemail" class="size-6" /> <MaterialDesignIcon icon-name="voicemail" class="size-5" />
<span>{{ $t("call.send_to_voicemail") }}</span> <span>{{ $t("call.send_to_voicemail") }}</span>
</button> </button>
@@ -242,10 +248,10 @@
: $t('call.hangup_call') : $t('call.hangup_call')
" "
type="button" type="button"
class="inline-flex items-center gap-x-2 rounded-2xl bg-red-600 px-6 py-4 text-lg font-bold text-white shadow-xl hover:bg-red-500 transition-all duration-200" class="inline-flex items-center gap-x-2 rounded-2xl bg-red-600 px-5 py-3 text-base font-bold text-white shadow-xl hover:bg-red-500 transition-all duration-200"
@click="hangupCall" @click="hangupCall"
> >
<MaterialDesignIcon icon-name="phone-hangup" class="size-6 rotate-[135deg]" /> <MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
<span>{{ <span>{{
activeCall.is_incoming && activeCall.status === 4 activeCall.is_incoming && activeCall.status === 4
? $t("call.decline") ? $t("call.decline")
@@ -926,6 +932,7 @@ export default {
ringtones: [], ringtones: [],
editingRingtoneId: null, editingRingtoneId: null,
editingRingtoneName: "", editingRingtoneName: "",
elapsedTimeInterval: null,
}; };
}, },
computed: { computed: {
@@ -935,6 +942,13 @@ export default {
isSpeakerMuted() { isSpeakerMuted() {
return this.activeCall?.is_speaker_muted ?? false; return this.activeCall?.is_speaker_muted ?? false;
}, },
elapsedTime() {
if (!this.activeCall?.call_start_time) {
return null;
}
const elapsed = Math.floor(Date.now() / 1000 - this.activeCall.call_start_time);
return Utils.formatMinutesSeconds(elapsed);
},
}, },
mounted() { mounted() {
this.getConfig(); this.getConfig();
@@ -959,6 +973,11 @@ export default {
this.getVoicemails(); this.getVoicemails();
}, 10000); }, 10000);
// update elapsed time every second
this.elapsedTimeInterval = setInterval(() => {
this.$forceUpdate();
}, 1000);
// autofill destination hash from query string // autofill destination hash from query string
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const destinationHash = urlParams.get("destination_hash"); const destinationHash = urlParams.get("destination_hash");
@@ -969,6 +988,7 @@ export default {
beforeUnmount() { beforeUnmount() {
if (this.statusInterval) clearInterval(this.statusInterval); if (this.statusInterval) clearInterval(this.statusInterval);
if (this.historyInterval) clearInterval(this.historyInterval); if (this.historyInterval) clearInterval(this.historyInterval);
if (this.elapsedTimeInterval) clearInterval(this.elapsedTimeInterval);
if (this.endedTimeout) clearTimeout(this.endedTimeout); if (this.endedTimeout) clearTimeout(this.endedTimeout);
if (this.audioPlayer) { if (this.audioPlayer) {
this.audioPlayer.pause(); this.audioPlayer.pause();