feat(call): enhance call management features by adding voicemail functionality, updating call status handling, and improving UI elements for call interactions
This commit is contained in:
@@ -287,15 +287,21 @@
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-zinc-800 dark:border-zinc-600 dark:text-zinc-200 dark:focus:ring-blue-400 dark:focus:border-blue-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2 dark:border-zinc-900">
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden">
|
||||
<div>{{ $t("app.identity_hash") }}</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
||||
<div
|
||||
class="text-sm text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
:title="config.identity_hash"
|
||||
>
|
||||
{{ config.identity_hash }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 dark:border-zinc-900">
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden">
|
||||
<div>{{ $t("app.lxmf_address") }}</div>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
||||
<div
|
||||
class="text-sm text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
:title="config.lxmf_address_hash"
|
||||
>
|
||||
{{ config.lxmf_address_hash }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,7 +371,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<CallOverlay v-if="activeCall || isCallEnded" :active-call="activeCall || lastCall" :is-ended="isCallEnded" />
|
||||
<CallOverlay
|
||||
v-if="activeCall || isCallEnded || wasDeclined"
|
||||
:active-call="activeCall || lastCall"
|
||||
:is-ended="isCallEnded"
|
||||
:was-declined="wasDeclined"
|
||||
@hangup="onOverlayHangup"
|
||||
/>
|
||||
<Toast />
|
||||
|
||||
<!-- identity switching overlay -->
|
||||
@@ -429,7 +441,6 @@ export default {
|
||||
|
||||
isShowingMyIdentitySection: true,
|
||||
isShowingAnnounceSection: true,
|
||||
isShowingCallsSection: true,
|
||||
|
||||
isSidebarOpen: false,
|
||||
|
||||
@@ -439,10 +450,10 @@ export default {
|
||||
config: null,
|
||||
appInfo: null,
|
||||
|
||||
isTelephoneCallActive: false,
|
||||
activeCall: null,
|
||||
propagationNodeStatus: null,
|
||||
isCallEnded: false,
|
||||
wasDeclined: false,
|
||||
lastCall: null,
|
||||
endedTimeout: null,
|
||||
ringtonePlayer: null,
|
||||
@@ -765,7 +776,6 @@ export default {
|
||||
|
||||
// update ui
|
||||
this.activeCall = response.data.active_call;
|
||||
this.isTelephoneCallActive = this.activeCall != null;
|
||||
|
||||
// Stop ringtone if not ringing anymore
|
||||
if (this.activeCall?.status !== 4) {
|
||||
@@ -775,16 +785,23 @@ export default {
|
||||
// If call just ended, show ended state for a few seconds
|
||||
if (oldCall != null && this.activeCall == null) {
|
||||
this.lastCall = oldCall;
|
||||
this.isCallEnded = true;
|
||||
|
||||
if (this.wasDeclined) {
|
||||
// Already set by hangupCall
|
||||
} else {
|
||||
this.isCallEnded = true;
|
||||
}
|
||||
|
||||
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
||||
this.endedTimeout = setTimeout(() => {
|
||||
this.isCallEnded = false;
|
||||
this.wasDeclined = false;
|
||||
this.lastCall = null;
|
||||
}, 5000);
|
||||
} else if (this.activeCall != null) {
|
||||
// if a new call starts, clear ended state
|
||||
this.isCallEnded = false;
|
||||
this.wasDeclined = false;
|
||||
this.lastCall = null;
|
||||
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
||||
}
|
||||
@@ -792,20 +809,9 @@ export default {
|
||||
// do nothing on error
|
||||
}
|
||||
},
|
||||
async hangupTelephoneCall() {
|
||||
// confirm user wants to hang up call
|
||||
if (!(await DialogUtils.confirm("Are you sure you want to hang up the current telephone call?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// hangup call
|
||||
await axios.get(`/api/v1/telephone/hangup`);
|
||||
|
||||
// reload status
|
||||
await this.updateTelephoneStatus();
|
||||
} catch {
|
||||
// ignore error hanging up call
|
||||
onOverlayHangup() {
|
||||
if (this.activeCall && this.activeCall.is_incoming && this.activeCall.status === 4) {
|
||||
this.wasDeclined = true;
|
||||
}
|
||||
},
|
||||
onAppNameClick() {
|
||||
|
||||
@@ -7,16 +7,21 @@
|
||||
<!-- 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="flex-1 flex items-center space-x-2">
|
||||
<div class="size-2 rounded-full" :class="isEnded ? 'bg-red-500' : 'bg-green-500 animate-pulse'"></div>
|
||||
<div
|
||||
class="size-2 rounded-full"
|
||||
:class="isEnded || wasDeclined ? 'bg-red-500' : 'bg-green-500 animate-pulse'"
|
||||
></div>
|
||||
<span class="text-[10px] font-bold text-gray-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
{{
|
||||
isEnded
|
||||
? "Call Ended"
|
||||
: activeCall.is_voicemail
|
||||
? "Recording Voicemail"
|
||||
: activeCall.status === 6
|
||||
? "Active Call"
|
||||
: "Call Status"
|
||||
wasDeclined
|
||||
? $t("call.call_declined")
|
||||
: isEnded
|
||||
? $t("call.call_ended")
|
||||
: activeCall.is_voicemail
|
||||
? $t("call.recording_voicemail")
|
||||
: activeCall.status === 6
|
||||
? $t("call.active_call")
|
||||
: $t("call.call_status")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
@@ -38,19 +43,33 @@
|
||||
<div class="flex flex-col items-center mb-4">
|
||||
<div
|
||||
class="p-4 rounded-full mb-3"
|
||||
:class="isEnded ? 'bg-red-100 dark:bg-red-900/30' : 'bg-blue-100 dark:bg-blue-900/30'"
|
||||
:class="
|
||||
isEnded || wasDeclined ? 'bg-red-100 dark:bg-red-900/30' : 'bg-blue-100 dark:bg-blue-900/30'
|
||||
"
|
||||
>
|
||||
<LxmfUserIcon
|
||||
v-if="activeCall.remote_icon"
|
||||
:icon-name="activeCall.remote_icon.icon_name"
|
||||
:icon-foreground-colour="activeCall.remote_icon.foreground_colour"
|
||||
:icon-background-colour="activeCall.remote_icon.background_colour"
|
||||
class="size-8"
|
||||
/>
|
||||
<MaterialDesignIcon
|
||||
v-else
|
||||
icon-name="account"
|
||||
class="size-8"
|
||||
:class="isEnded ? 'text-red-600 dark:text-red-400' : 'text-blue-600 dark:text-blue-400'"
|
||||
:class="
|
||||
isEnded || wasDeclined
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-blue-600 dark:text-blue-400'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center w-full">
|
||||
<div class="text-center w-full min-w-0">
|
||||
<div class="font-bold text-gray-900 dark:text-white truncate px-2">
|
||||
{{ activeCall.remote_identity_name || "Unknown" }}
|
||||
{{ activeCall.remote_identity_name || $t("call.unknown") }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono">
|
||||
<div class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono truncate px-4">
|
||||
{{
|
||||
activeCall.remote_identity_hash
|
||||
? formatDestinationHash(activeCall.remote_identity_hash)
|
||||
@@ -65,23 +84,26 @@
|
||||
<div
|
||||
class="text-sm font-medium"
|
||||
:class="[
|
||||
isEnded
|
||||
isEnded || wasDeclined
|
||||
? 'text-red-600 dark:text-red-400 animate-pulse'
|
||||
: activeCall.status === 6
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-gray-600 dark:text-zinc-400',
|
||||
]"
|
||||
>
|
||||
<span v-if="isEnded">Call Ended</span>
|
||||
<span v-else-if="activeCall.is_incoming && activeCall.status === 4">Incoming Call...</span>
|
||||
<span v-else-if="activeCall.status === 0">Busy</span>
|
||||
<span v-else-if="activeCall.status === 1">Rejected</span>
|
||||
<span v-else-if="activeCall.status === 2">Calling...</span>
|
||||
<span v-else-if="activeCall.status === 3">Available</span>
|
||||
<span v-else-if="activeCall.status === 4">Ringing...</span>
|
||||
<span v-else-if="activeCall.status === 5">Connecting...</span>
|
||||
<span v-else-if="activeCall.status === 6">Connected</span>
|
||||
<span v-else>Status: {{ activeCall.status }}</span>
|
||||
<span v-if="wasDeclined">{{ $t("call.call_declined") }}</span>
|
||||
<span v-else-if="isEnded">{{ $t("call.call_ended") }}</span>
|
||||
<span v-else-if="activeCall.is_incoming && activeCall.status === 4">{{
|
||||
$t("call.incoming_call")
|
||||
}}</span>
|
||||
<span v-else-if="activeCall.status === 0">{{ $t("call.busy") }}</span>
|
||||
<span v-else-if="activeCall.status === 1">{{ $t("call.rejected") }}</span>
|
||||
<span v-else-if="activeCall.status === 2">{{ $t("call.calling") }}</span>
|
||||
<span v-else-if="activeCall.status === 3">{{ $t("call.available") }}</span>
|
||||
<span v-else-if="activeCall.status === 4">{{ $t("call.ringing") }}</span>
|
||||
<span v-else-if="activeCall.status === 5">{{ $t("call.connecting") }}</span>
|
||||
<span v-else-if="activeCall.status === 6">{{ $t("call.connected") }}</span>
|
||||
<span v-else>{{ $t("call.status") }}: {{ activeCall.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -135,18 +157,33 @@
|
||||
<!-- Hangup -->
|
||||
<button
|
||||
type="button"
|
||||
:title="activeCall.is_incoming && activeCall.status === 4 ? 'Decline' : 'Hangup'"
|
||||
:title="
|
||||
activeCall.is_incoming && activeCall.status === 4
|
||||
? $t('call.decline_call')
|
||||
: $t('call.hangup_call')
|
||||
"
|
||||
class="p-3 rounded-full bg-red-600 text-white hover:bg-red-700 shadow-lg shadow-red-600/30 transition-all duration-200"
|
||||
@click="hangupCall"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone-hangup" class="size-6 rotate-[135deg]" />
|
||||
</button>
|
||||
|
||||
<!-- Send to Voicemail (if incoming) -->
|
||||
<button
|
||||
v-if="activeCall.is_incoming && activeCall.status === 4"
|
||||
type="button"
|
||||
:title="$t('call.send_to_voicemail')"
|
||||
class="p-3 rounded-full bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-600/30 transition-all duration-200"
|
||||
@click="sendToVoicemail"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="voicemail" class="size-6" />
|
||||
</button>
|
||||
|
||||
<!-- Answer (if incoming) -->
|
||||
<button
|
||||
v-if="activeCall.is_incoming && activeCall.status === 4"
|
||||
type="button"
|
||||
title="Answer"
|
||||
:title="$t('call.answer_call')"
|
||||
class="p-3 rounded-full bg-green-600 text-white hover:bg-green-700 shadow-lg shadow-green-600/30 animate-bounce"
|
||||
@click="answerCall"
|
||||
>
|
||||
@@ -160,10 +197,17 @@
|
||||
v-show="isMinimized && !isEnded"
|
||||
class="px-4 py-2 flex items-center justify-between bg-white dark:bg-zinc-900"
|
||||
>
|
||||
<div class="flex items-center space-x-2 overflow-hidden mr-2">
|
||||
<MaterialDesignIcon icon-name="account" class="size-5 text-blue-500" />
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate">
|
||||
{{ activeCall.remote_identity_name || "Unknown" }}
|
||||
<div class="flex items-center space-x-2 overflow-hidden mr-2 min-w-0">
|
||||
<LxmfUserIcon
|
||||
v-if="activeCall.remote_icon"
|
||||
:icon-name="activeCall.remote_icon.icon_name"
|
||||
:icon-foreground-colour="activeCall.remote_icon.foreground_colour"
|
||||
:icon-background-colour="activeCall.remote_icon.background_colour"
|
||||
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>
|
||||
<div class="flex items-center space-x-1">
|
||||
@@ -192,12 +236,13 @@
|
||||
|
||||
<script>
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import LxmfUserIcon from "../LxmfUserIcon.vue";
|
||||
import Utils from "../../js/Utils";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
|
||||
export default {
|
||||
name: "CallOverlay",
|
||||
components: { MaterialDesignIcon },
|
||||
components: { MaterialDesignIcon, LxmfUserIcon },
|
||||
props: {
|
||||
activeCall: {
|
||||
type: Object,
|
||||
@@ -207,6 +252,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
wasDeclined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -237,11 +286,20 @@ export default {
|
||||
},
|
||||
async hangupCall() {
|
||||
try {
|
||||
this.$emit("hangup");
|
||||
await window.axios.get("/api/v1/telephone/hangup");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to hangup call");
|
||||
}
|
||||
},
|
||||
async sendToVoicemail() {
|
||||
try {
|
||||
await window.axios.get("/api/v1/telephone/send-to-voicemail");
|
||||
ToastUtils.success("Call sent to voicemail");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to send call to voicemail");
|
||||
}
|
||||
},
|
||||
async toggleMicrophone() {
|
||||
try {
|
||||
const endpoint = this.isMicMuted
|
||||
|
||||
@@ -67,22 +67,33 @@
|
||||
class="mx-auto bg-gray-300 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-4 rounded-full"
|
||||
:class="{ 'animate-pulse': activeCall && activeCall.status === 4 }"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="account" class="size-12" />
|
||||
<LxmfUserIcon
|
||||
v-if="(activeCall || lastCall)?.remote_icon"
|
||||
:icon-name="(activeCall || lastCall).remote_icon.icon_name"
|
||||
:icon-foreground-colour="
|
||||
(activeCall || lastCall).remote_icon.foreground_colour
|
||||
"
|
||||
:icon-background-colour="
|
||||
(activeCall || lastCall).remote_icon.background_colour
|
||||
"
|
||||
class="size-12"
|
||||
/>
|
||||
<MaterialDesignIcon v-else icon-name="account" class="size-12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- name -->
|
||||
<div class="text-xl font-semibold text-gray-500 dark:text-zinc-100">
|
||||
<div class="text-xl font-semibold text-gray-500 dark:text-zinc-100 truncate px-4">
|
||||
<span v-if="(activeCall || lastCall)?.remote_identity_name != null">{{
|
||||
(activeCall || lastCall).remote_identity_name
|
||||
}}</span>
|
||||
<span v-else>Unknown</span>
|
||||
<span v-else>{{ $t("call.unknown") }}</span>
|
||||
</div>
|
||||
|
||||
<!-- identity hash -->
|
||||
<div
|
||||
v-if="(activeCall || lastCall)?.remote_identity_hash != null"
|
||||
class="text-gray-500 dark:text-zinc-100 opacity-60 text-sm"
|
||||
class="text-gray-500 dark:text-zinc-100 opacity-60 text-sm truncate px-8 font-mono"
|
||||
>
|
||||
{{
|
||||
(activeCall || lastCall).remote_identity_hash
|
||||
@@ -94,14 +105,21 @@
|
||||
|
||||
<!-- call status -->
|
||||
<div class="text-gray-500 dark:text-zinc-100 mb-4 mt-2">
|
||||
<template v-if="isCallEnded">
|
||||
<span class="text-red-500 font-bold animate-pulse">Call Ended</span>
|
||||
<template v-if="wasDeclined">
|
||||
<span class="text-red-500 font-bold animate-pulse">{{
|
||||
$t("call.call_declined")
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else-if="isCallEnded">
|
||||
<span class="text-red-500 font-bold animate-pulse">{{
|
||||
$t("call.call_ended")
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else-if="activeCall">
|
||||
<span
|
||||
v-if="activeCall.is_incoming && activeCall.status === 4"
|
||||
class="animate-bounce inline-block"
|
||||
>Incoming Call...</span
|
||||
>{{ $t("call.incoming_call") }}</span
|
||||
>
|
||||
<span v-else>
|
||||
<span v-if="activeCall.status === 0">Busy...</span>
|
||||
@@ -195,21 +213,33 @@
|
||||
<!-- answer call -->
|
||||
<button
|
||||
v-if="activeCall.is_incoming && activeCall.status === 4"
|
||||
title="Answer Call"
|
||||
: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"
|
||||
@click="answerCall"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone" class="size-6" />
|
||||
<span>Accept</span>
|
||||
<span>{{ $t("call.accept") }}</span>
|
||||
</button>
|
||||
|
||||
<!-- send to voicemail -->
|
||||
<button
|
||||
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"
|
||||
@click="sendToVoicemail"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="voicemail" class="size-6" />
|
||||
<span>{{ $t("call.send_to_voicemail") }}</span>
|
||||
</button>
|
||||
|
||||
<!-- hangup/decline call -->
|
||||
<button
|
||||
:title="
|
||||
activeCall.is_incoming && activeCall.status === 4
|
||||
? 'Decline Call'
|
||||
: 'Hangup Call'
|
||||
? $t('call.decline_call')
|
||||
: $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"
|
||||
@@ -217,7 +247,9 @@
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone-hangup" class="size-6 rotate-[135deg]" />
|
||||
<span>{{
|
||||
activeCall.is_incoming && activeCall.status === 4 ? "Decline" : "Hangup"
|
||||
activeCall.is_incoming && activeCall.status === 4
|
||||
? $t("call.decline")
|
||||
: $t("call.hangup")
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -293,19 +325,36 @@
|
||||
class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-zinc-800/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div :class="entry.is_incoming ? 'text-blue-500' : 'text-green-500'">
|
||||
<MaterialDesignIcon
|
||||
:icon-name="entry.is_incoming ? 'phone-incoming' : 'phone-outgoing'"
|
||||
class="size-5"
|
||||
<div class="shrink-0">
|
||||
<LxmfUserIcon
|
||||
v-if="entry.remote_icon"
|
||||
:icon-name="entry.remote_icon.icon_name"
|
||||
:icon-foreground-colour="entry.remote_icon.foreground_colour"
|
||||
:icon-background-colour="entry.remote_icon.background_colour"
|
||||
class="size-8"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
entry.is_incoming
|
||||
? 'text-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'text-green-500 bg-green-50 dark:bg-green-900/20'
|
||||
"
|
||||
class="size-8 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="entry.is_incoming ? 'phone-incoming' : 'phone-outgoing'"
|
||||
class="size-5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">
|
||||
{{ entry.remote_identity_name || "Unknown" }}
|
||||
{{ entry.remote_identity_name || $t("call.unknown") }}
|
||||
</p>
|
||||
<span
|
||||
class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono ml-2"
|
||||
class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono ml-2 shrink-0"
|
||||
>
|
||||
{{ entry.timestamp ? formatDateTime(entry.timestamp * 1000) : "" }}
|
||||
</span>
|
||||
@@ -322,6 +371,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-400 dark:text-zinc-600 font-mono truncate mt-0.5"
|
||||
:title="entry.remote_identity_hash"
|
||||
>
|
||||
{{ entry.remote_identity_hash }}
|
||||
</div>
|
||||
@@ -381,32 +431,51 @@
|
||||
:class="{ 'bg-blue-50/50 dark:bg-blue-900/10': !voicemail.is_read }"
|
||||
>
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- Play/Pause Button -->
|
||||
<button
|
||||
class="shrink-0 size-10 rounded-full flex items-center justify-center transition-all"
|
||||
:class="
|
||||
playingVoicemailId === voicemail.id
|
||||
? 'bg-red-500 text-white animate-pulse'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
"
|
||||
@click="playVoicemail(voicemail)"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="playingVoicemailId === voicemail.id ? 'stop' : 'play'"
|
||||
class="size-6"
|
||||
<!-- Icon / Play/Pause Button -->
|
||||
<div class="relative shrink-0">
|
||||
<LxmfUserIcon
|
||||
v-if="voicemail.remote_icon"
|
||||
:icon-name="voicemail.remote_icon.icon_name"
|
||||
:icon-foreground-colour="voicemail.remote_icon.foreground_colour"
|
||||
:icon-background-colour="voicemail.remote_icon.background_colour"
|
||||
class="size-10"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="size-10 rounded-full bg-gray-200 dark:bg-zinc-800 flex items-center justify-center text-gray-400"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="account" class="size-6" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute inset-0 size-10 rounded-full flex items-center justify-center transition-all opacity-0 hover:opacity-100 bg-black/20 text-white"
|
||||
:class="{
|
||||
'opacity-100 bg-red-500/80 animate-pulse':
|
||||
playingVoicemailId === voicemail.id,
|
||||
}"
|
||||
@click="playVoicemail(voicemail)"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="playingVoicemailId === voicemail.id ? 'stop' : 'play'"
|
||||
class="size-6"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<p class="text-sm font-bold text-gray-900 dark:text-white truncate">
|
||||
{{ voicemail.remote_identity_name || "Unknown" }}
|
||||
<div class="flex items-center min-w-0 mr-2">
|
||||
<p class="text-sm font-bold text-gray-900 dark:text-white truncate">
|
||||
{{ voicemail.remote_identity_name || $t("call.unknown") }}
|
||||
</p>
|
||||
<span
|
||||
v-if="!voicemail.is_read"
|
||||
class="ml-2 size-2 inline-block rounded-full bg-blue-500"
|
||||
class="ml-2 shrink-0 size-2 inline-block rounded-full bg-blue-500"
|
||||
></span>
|
||||
</p>
|
||||
<span class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono">
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono shrink-0"
|
||||
>
|
||||
{{ formatDateTime(voicemail.timestamp * 1000) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -418,9 +487,11 @@
|
||||
<MaterialDesignIcon icon-name="clock-outline" class="size-3" />
|
||||
{{ formatDuration(voicemail.duration_seconds) }}
|
||||
</span>
|
||||
<span class="opacity-60 font-mono text-[10px]">{{
|
||||
formatDestinationHash(voicemail.remote_identity_hash)
|
||||
}}</span>
|
||||
<span
|
||||
class="opacity-60 font-mono text-[10px] truncate"
|
||||
:title="voicemail.remote_identity_hash"
|
||||
>{{ formatDestinationHash(voicemail.remote_identity_hash) }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -478,7 +549,9 @@
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
:class="config.custom_ringtone_enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-zinc-700'"
|
||||
:class="
|
||||
config.custom_ringtone_enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-zinc-700'
|
||||
"
|
||||
@click="
|
||||
config.custom_ringtone_enabled = !config.custom_ringtone_enabled;
|
||||
updateConfig({ custom_ringtone_enabled: config.custom_ringtone_enabled });
|
||||
@@ -519,29 +592,37 @@
|
||||
v-for="ringtone in ringtones"
|
||||
:key="ringtone.id"
|
||||
class="group p-4 rounded-xl border border-gray-100 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-800/30 flex items-center gap-4 transition-all hover:shadow-md"
|
||||
:class="{ 'ring-2 ring-blue-500/20 bg-blue-50/20 dark:bg-blue-900/10': ringtone.is_primary }"
|
||||
:class="{
|
||||
'ring-2 ring-blue-500/20 bg-blue-50/20 dark:bg-blue-900/10':
|
||||
ringtone.is_primary,
|
||||
}"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-if="editingRingtoneId === ringtone.id" class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="editingRingtoneId === ringtone.id"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="editingRingtoneName"
|
||||
class="text-sm bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded px-2 py-1 flex-1"
|
||||
class="text-sm bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded px-2 py-1 flex-1 min-w-0"
|
||||
@keyup.enter="saveRingtoneName"
|
||||
@blur="saveRingtoneName"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
<div v-else class="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
class="text-sm font-medium text-gray-900 dark:text-white truncate"
|
||||
>
|
||||
{{ ringtone.display_name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="ringtone.is_primary"
|
||||
class="text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 rounded"
|
||||
class="shrink-0 text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 rounded"
|
||||
>
|
||||
Primary
|
||||
</span>
|
||||
<button
|
||||
class="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-blue-500 transition-opacity"
|
||||
class="shrink-0 opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-blue-500 transition-opacity"
|
||||
@click="startEditingRingtone(ringtone)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="pencil" class="size-3" />
|
||||
@@ -555,11 +636,19 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="p-2 rounded-lg hover:bg-white dark:hover:bg-zinc-800 text-gray-500 dark:text-gray-400 transition-colors"
|
||||
:title="isPlayingRingtone && playingRingtoneId === ringtone.id ? 'Stop' : 'Preview'"
|
||||
:title="
|
||||
isPlayingRingtone && playingRingtoneId === ringtone.id
|
||||
? 'Stop'
|
||||
: 'Preview'
|
||||
"
|
||||
@click="playRingtonePreview(ringtone)"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="isPlayingRingtone && playingRingtoneId === ringtone.id ? 'stop' : 'play'"
|
||||
:icon-name="
|
||||
isPlayingRingtone && playingRingtoneId === ringtone.id
|
||||
? 'stop'
|
||||
: 'play'
|
||||
"
|
||||
class="size-5"
|
||||
/>
|
||||
</button>
|
||||
@@ -581,8 +670,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-gray-100 dark:border-zinc-800 rounded-2xl bg-gray-50/30 dark:bg-zinc-900/20">
|
||||
<MaterialDesignIcon icon-name="music-off" class="size-8 text-gray-300 dark:text-zinc-700 mb-2" />
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-gray-100 dark:border-zinc-800 rounded-2xl bg-gray-50/30 dark:bg-zinc-900/20"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="music-off"
|
||||
class="size-8 text-gray-300 dark:text-zinc-700 mb-2"
|
||||
/>
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("call.no_custom_ringtone_uploaded") }}
|
||||
</div>
|
||||
@@ -789,11 +884,12 @@
|
||||
<script>
|
||||
import Utils from "../../js/Utils";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import LxmfUserIcon from "../LxmfUserIcon.vue";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
|
||||
export default {
|
||||
name: "CallPage",
|
||||
components: { MaterialDesignIcon },
|
||||
components: { MaterialDesignIcon, LxmfUserIcon },
|
||||
data() {
|
||||
return {
|
||||
config: null,
|
||||
@@ -804,6 +900,7 @@ export default {
|
||||
isShowingStats: false,
|
||||
callHistory: [],
|
||||
isCallEnded: false,
|
||||
wasDeclined: false,
|
||||
lastCall: null,
|
||||
endedTimeout: null,
|
||||
activeTab: "phone",
|
||||
@@ -839,22 +936,22 @@ export default {
|
||||
return this.activeCall?.is_speaker_muted ?? false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getConfig();
|
||||
this.getAudioProfiles();
|
||||
this.getStatus();
|
||||
this.getHistory();
|
||||
this.getVoicemails();
|
||||
this.getVoicemailStatus();
|
||||
this.getRingtones();
|
||||
this.getRingtoneStatus();
|
||||
mounted() {
|
||||
this.getConfig();
|
||||
this.getAudioProfiles();
|
||||
this.getStatus();
|
||||
this.getHistory();
|
||||
this.getVoicemails();
|
||||
this.getVoicemailStatus();
|
||||
this.getRingtones();
|
||||
this.getRingtoneStatus();
|
||||
|
||||
// poll for status
|
||||
this.statusInterval = setInterval(() => {
|
||||
this.getStatus();
|
||||
this.getVoicemailStatus();
|
||||
this.getRingtoneStatus();
|
||||
}, 1000);
|
||||
// poll for status
|
||||
this.statusInterval = setInterval(() => {
|
||||
this.getStatus();
|
||||
this.getVoicemailStatus();
|
||||
this.getRingtoneStatus();
|
||||
}, 1000);
|
||||
|
||||
// poll for history/voicemails less frequently
|
||||
this.historyInterval = setInterval(() => {
|
||||
@@ -1219,11 +1316,22 @@ export default {
|
||||
},
|
||||
async hangupCall() {
|
||||
try {
|
||||
if (this.activeCall && this.activeCall.is_incoming && this.activeCall.status === 4) {
|
||||
this.wasDeclined = true;
|
||||
}
|
||||
await window.axios.get("/api/v1/telephone/hangup");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to hangup call");
|
||||
}
|
||||
},
|
||||
async sendToVoicemail() {
|
||||
try {
|
||||
await window.axios.get("/api/v1/telephone/send-to-voicemail");
|
||||
ToastUtils.success("Call sent to voicemail");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to send call to voicemail");
|
||||
}
|
||||
},
|
||||
async switchAudioProfile(audioProfileId) {
|
||||
try {
|
||||
await window.axios.get(`/api/v1/telephone/switch-audio-profile/${audioProfileId}`);
|
||||
|
||||
@@ -547,6 +547,8 @@
|
||||
"settings": "Einstellungen",
|
||||
"unknown": "Unbekannt",
|
||||
"call_ended": "Anruf beendet",
|
||||
"call_declined": "Anruf abgelehnt",
|
||||
"send_to_voicemail": "An Sprachnachricht senden",
|
||||
"incoming_call": "Eingehender Anruf...",
|
||||
"busy": "Besetzt...",
|
||||
"rejected": "Abgelehnt...",
|
||||
|
||||
@@ -571,6 +571,8 @@
|
||||
"settings": "Settings",
|
||||
"unknown": "Unknown",
|
||||
"call_ended": "Call Ended",
|
||||
"call_declined": "Call Declined",
|
||||
"send_to_voicemail": "Send to Voicemail",
|
||||
"incoming_call": "Incoming Call...",
|
||||
"busy": "Busy...",
|
||||
"rejected": "Rejected...",
|
||||
|
||||
@@ -547,6 +547,8 @@
|
||||
"settings": "Настройки",
|
||||
"unknown": "Неизвестно",
|
||||
"call_ended": "Звонок завершен",
|
||||
"call_declined": "Звонок отклонен",
|
||||
"send_to_voicemail": "На голосовую почту",
|
||||
"incoming_call": "Входящий звонок...",
|
||||
"busy": "Занято...",
|
||||
"rejected": "Отклонено...",
|
||||
|
||||
Reference in New Issue
Block a user