feat(call): implement elapsed time display for active calls and improve UI responsiveness with updated button styles
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user