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>
<div
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="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded }"
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 || wasDeclined }"
>
<!-- 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">
@@ -105,6 +105,12 @@
<span v-else-if="activeCall.status === 6">{{ $t("call.connected") }}</span>
<span v-else>{{ $t("call.status") }}: {{ activeCall.status }}</span>
</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>
<!-- Stats (only when connected and not minimized) -->
@@ -123,11 +129,11 @@
</div>
<!-- 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 -->
<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="
isMicMuted
@@ -142,7 +148,7 @@
<!-- Mute Speaker -->
<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="
isSpeakerMuted
@@ -206,9 +212,17 @@
class="size-5 shrink-0"
/>
<MaterialDesignIcon v-else icon-name="account" class="size-5 text-blue-500 shrink-0" />
<span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">
{{ activeCall.remote_identity_name || $t("call.unknown") }}
</span>
<div class="flex flex-col min-w-0">
<span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">
{{ activeCall.remote_identity_name || $t("call.unknown") }}
</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 class="flex items-center space-x-1">
<button
@@ -260,6 +274,7 @@ export default {
data() {
return {
isMinimized: false,
elapsedTimeInterval: null,
};
},
computed: {
@@ -269,6 +284,23 @@ export default {
isSpeakerMuted() {
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: {
formatDestinationHash(hash) {

View File

@@ -135,6 +135,12 @@
</span>
</template>
</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 -->
<div v-if="activeCall && activeCall.status === 6" class="mb-4">
@@ -209,16 +215,16 @@
</div>
<!-- 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 -->
<button
v-if="activeCall.is_incoming && activeCall.status === 4"
:title="$t('call.answer_call')"
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"
>
<MaterialDesignIcon icon-name="phone" class="size-6" />
<MaterialDesignIcon icon-name="phone" class="size-5" />
<span>{{ $t("call.accept") }}</span>
</button>
@@ -227,10 +233,10 @@
v-if="activeCall.is_incoming && activeCall.status === 4"
:title="$t('call.send_to_voicemail')"
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"
>
<MaterialDesignIcon icon-name="voicemail" class="size-6" />
<MaterialDesignIcon icon-name="voicemail" class="size-5" />
<span>{{ $t("call.send_to_voicemail") }}</span>
</button>
@@ -242,10 +248,10 @@
: $t('call.hangup_call')
"
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"
>
<MaterialDesignIcon icon-name="phone-hangup" class="size-6 rotate-[135deg]" />
<MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
<span>{{
activeCall.is_incoming && activeCall.status === 4
? $t("call.decline")
@@ -926,6 +932,7 @@ export default {
ringtones: [],
editingRingtoneId: null,
editingRingtoneName: "",
elapsedTimeInterval: null,
};
},
computed: {
@@ -935,6 +942,13 @@ export default {
isSpeakerMuted() {
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.getConfig();
@@ -959,6 +973,11 @@ export default {
this.getVoicemails();
}, 10000);
// update elapsed time every second
this.elapsedTimeInterval = setInterval(() => {
this.$forceUpdate();
}, 1000);
// autofill destination hash from query string
const urlParams = new URLSearchParams(window.location.search);
const destinationHash = urlParams.get("destination_hash");
@@ -969,6 +988,7 @@ export default {
beforeUnmount() {
if (this.statusInterval) clearInterval(this.statusInterval);
if (this.historyInterval) clearInterval(this.historyInterval);
if (this.elapsedTimeInterval) clearInterval(this.elapsedTimeInterval);
if (this.endedTimeout) clearTimeout(this.endedTimeout);
if (this.audioPlayer) {
this.audioPlayer.pause();