feat(call): improve call handling by adding remote destination hash tracking, improving initiation status checks, and refining ringtone management in the frontend
This commit is contained in:
@@ -1650,6 +1650,13 @@ class ReticulumMeshChat:
|
||||
ctx = context or self.current_context
|
||||
if not ctx:
|
||||
return
|
||||
|
||||
if ctx.telephone_manager and ctx.telephone_manager.initiation_status:
|
||||
print(
|
||||
"on_incoming_telephone_call: Ignoring as we are currently initiating an outgoing call."
|
||||
)
|
||||
return
|
||||
|
||||
caller_hash = caller_identity.hash.hex()
|
||||
|
||||
# Check if caller is blocked
|
||||
@@ -3872,6 +3879,9 @@ class ReticulumMeshChat:
|
||||
if telephone_active_call is not None:
|
||||
# remote_identity is already fetched and checked for None above
|
||||
remote_hash = remote_identity.hash.hex()
|
||||
remote_destination_hash = RNS.Destination.hash(
|
||||
remote_identity, "lxmf", "delivery"
|
||||
).hex()
|
||||
remote_name = None
|
||||
if self.telephone_manager.get_name_for_identity_hash:
|
||||
remote_name = self.telephone_manager.get_name_for_identity_hash(
|
||||
@@ -3896,6 +3906,7 @@ class ReticulumMeshChat:
|
||||
active_call = {
|
||||
"hash": telephone_active_call.hash.hex(),
|
||||
"remote_identity_hash": remote_hash,
|
||||
"remote_destination_hash": remote_destination_hash,
|
||||
"remote_identity_name": remote_name,
|
||||
"remote_icon": dict(remote_icon) if remote_icon else None,
|
||||
"custom_image": custom_image,
|
||||
@@ -4068,6 +4079,7 @@ class ReticulumMeshChat:
|
||||
remote_identity_hash,
|
||||
)
|
||||
if lxmf_hash:
|
||||
d["remote_destination_hash"] = lxmf_hash
|
||||
icon = self.database.misc.get_user_icon(lxmf_hash)
|
||||
if icon:
|
||||
d["remote_icon"] = dict(icon)
|
||||
@@ -4238,6 +4250,7 @@ class ReticulumMeshChat:
|
||||
remote_identity_hash,
|
||||
)
|
||||
if lxmf_hash:
|
||||
d["remote_destination_hash"] = lxmf_hash
|
||||
icon = self.database.misc.get_user_icon(lxmf_hash)
|
||||
if icon:
|
||||
d["remote_icon"] = dict(icon)
|
||||
@@ -4296,7 +4309,9 @@ class ReticulumMeshChat:
|
||||
try:
|
||||
voicemail_id = int(voicemail_id)
|
||||
except (ValueError, TypeError):
|
||||
return web.json_response({"message": "Invalid voicemail ID"}, status=400)
|
||||
return web.json_response(
|
||||
{"message": "Invalid voicemail ID"}, status=400
|
||||
)
|
||||
|
||||
if not self.voicemail_manager:
|
||||
return web.json_response(
|
||||
@@ -9536,7 +9551,15 @@ class ReticulumMeshChat:
|
||||
"Received an announce from "
|
||||
+ RNS.prettyhexrep(destination_hash)
|
||||
+ " for [lxst.telephony]"
|
||||
+ (f" ({display_name})" if (display_name := parse_lxmf_display_name(base64.b64encode(app_data).decode() if app_data else None, None)) else "")
|
||||
+ (
|
||||
f" ({display_name})"
|
||||
if (
|
||||
display_name := parse_lxmf_display_name(
|
||||
base64.b64encode(app_data).decode() if app_data else None, None
|
||||
)
|
||||
)
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
# track announce timestamp
|
||||
|
||||
@@ -110,7 +110,7 @@ class TelephoneManager:
|
||||
self.telephone.hangup()
|
||||
except Exception as e:
|
||||
RNS.log(f"TelephoneManager: Error during hangup: {e}", RNS.LOG_ERROR)
|
||||
|
||||
|
||||
# Always clear initiation status on hangup to prevent "Dialing..." hang
|
||||
self._update_initiation_status(None, None)
|
||||
|
||||
@@ -140,7 +140,7 @@ class TelephoneManager:
|
||||
# Update start time to when it was actually established for duration calculation
|
||||
self.call_start_time = time.time()
|
||||
self.call_was_established = True
|
||||
|
||||
|
||||
# Clear initiation status as soon as call is established
|
||||
self._update_initiation_status(None, None)
|
||||
|
||||
@@ -296,21 +296,43 @@ class TelephoneManager:
|
||||
self._update_initiation_status("Dialing...")
|
||||
self.call_start_time = time.time()
|
||||
self.call_is_incoming = False
|
||||
|
||||
|
||||
# Use a thread for the blocking LXST call, but monitor status for early exit
|
||||
# if established elsewhere or timed out/hung up
|
||||
call_task = asyncio.create_task(asyncio.to_thread(self.telephone.call, destination_identity))
|
||||
|
||||
call_task = asyncio.create_task(
|
||||
asyncio.to_thread(self.telephone.call, destination_identity)
|
||||
)
|
||||
|
||||
start_wait = time.time()
|
||||
# LXST telephone.call usually returns on establishment or timeout.
|
||||
# We wait for it, but if status becomes established or ended, we can stop waiting.
|
||||
while not call_task.done():
|
||||
if self.telephone.call_status in [6, 0, 1]: # Established, Busy, Rejected
|
||||
if self.telephone.call_status in [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
]: # Established, Busy, Rejected
|
||||
break
|
||||
if self.telephone.call_status == 3 and (time.time() - start_wait > 1.0): # Available (ended/timeout)
|
||||
if self.telephone.call_status == 3 and (
|
||||
time.time() - start_wait > 1.0
|
||||
): # Available (ended/timeout)
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# If the task finished but we're still ringing or connecting,
|
||||
# wait a bit more for establishment or definitive failure
|
||||
if self.telephone.call_status in [4, 5]: # Ringing, Connecting
|
||||
wait_until = time.time() + timeout_seconds
|
||||
while time.time() < wait_until:
|
||||
if self.telephone.call_status in [
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
]: # Established, Busy, Rejected, Ended
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
return self.telephone.active_call
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -550,14 +550,14 @@ class VoicemailManager:
|
||||
"1",
|
||||
temp_path,
|
||||
]
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, check=False
|
||||
) # noqa: S603
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False) # noqa: S603
|
||||
|
||||
if result.returncode == 0 and os.path.exists(temp_path):
|
||||
os.remove(filepath)
|
||||
os.rename(temp_path, filepath)
|
||||
RNS.log(f"Voicemail: Fixed recording format for {filepath}", RNS.LOG_DEBUG)
|
||||
RNS.log(
|
||||
f"Voicemail: Fixed recording format for {filepath}", RNS.LOG_DEBUG
|
||||
)
|
||||
else:
|
||||
RNS.log(
|
||||
f"Voicemail: ffmpeg failed to fix {filepath}: {result.stderr}",
|
||||
@@ -581,7 +581,7 @@ class VoicemailManager:
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
f"anullsrc=r=48000:cl=mono",
|
||||
"anullsrc=r=48000:cl=mono",
|
||||
"-t",
|
||||
str(max(1, seconds)),
|
||||
"-c:a",
|
||||
@@ -598,7 +598,10 @@ class VoicemailManager:
|
||||
RNS.LOG_ERROR,
|
||||
)
|
||||
except Exception as e:
|
||||
RNS.log(f"Voicemail: Error creating silence file for {filepath}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(
|
||||
f"Voicemail: Error creating silence file for {filepath}: {e}",
|
||||
RNS.LOG_ERROR,
|
||||
)
|
||||
return False
|
||||
|
||||
def start_greeting_recording(self):
|
||||
|
||||
@@ -724,6 +724,10 @@ export default {
|
||||
if (this.config?.do_not_disturb_enabled) {
|
||||
break;
|
||||
}
|
||||
// If we are the caller (outgoing initiation), skip playing the incoming ringtone
|
||||
if (this.initiationStatus) {
|
||||
break;
|
||||
}
|
||||
NotificationUtils.showIncomingCallNotification();
|
||||
this.updateTelephoneStatus();
|
||||
this.playRingtone();
|
||||
@@ -1059,13 +1063,32 @@ export default {
|
||||
this.initiationTargetHash = response.data.initiation_target_hash;
|
||||
this.initiationTargetName = response.data.initiation_target_name;
|
||||
|
||||
// Update call ended state if needed
|
||||
const justEnded = oldCall != null && this.activeCall == null;
|
||||
if (justEnded) {
|
||||
this.lastCall = oldCall;
|
||||
this.toneGenerator.playBusyTone();
|
||||
|
||||
// Trigger history refresh
|
||||
GlobalEmitter.emit("telephone-history-updated");
|
||||
|
||||
if (!this.wasDeclined) {
|
||||
this.isCallEnded = true;
|
||||
}
|
||||
|
||||
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
||||
this.endedTimeout = setTimeout(() => {
|
||||
this.isCallEnded = false;
|
||||
this.wasDeclined = false;
|
||||
this.lastCall = null;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle outgoing ringback tone
|
||||
if (this.initiationStatus === "Ringing...") {
|
||||
this.toneGenerator.playRingback();
|
||||
} else if (!this.initiationStatus && !this.activeCall) {
|
||||
// Only stop if we're not ringing or in a call
|
||||
// This might be too aggressive if called during a transition,
|
||||
// but toneGenerator.stop() is safe to call multiple times.
|
||||
} else if (!this.initiationStatus && !this.activeCall && !this.isCallEnded) {
|
||||
// Only stop if we're not ringing, in a call, or just finished a call (busy tone playing)
|
||||
this.toneGenerator.stop();
|
||||
}
|
||||
|
||||
@@ -1092,8 +1115,8 @@ export default {
|
||||
this.isCallWindowOpen = false;
|
||||
}
|
||||
|
||||
// Handle ringtone
|
||||
if (this.activeCall?.status === 4) {
|
||||
// Handle ringtone (only for incoming ringing)
|
||||
if (this.activeCall?.status === 4 && this.activeCall?.is_incoming) {
|
||||
// Call is ringing
|
||||
if (!this.ringtonePlayer && this.config?.custom_ringtone_enabled && !this.isFetchingRingtone) {
|
||||
this.isFetchingRingtone = true;
|
||||
@@ -1138,25 +1161,8 @@ export default {
|
||||
}
|
||||
|
||||
// If call just ended, show ended state for a few seconds
|
||||
if (oldCall != null && this.activeCall == null) {
|
||||
this.lastCall = oldCall;
|
||||
this.toneGenerator.playBusyTone();
|
||||
|
||||
// Trigger history refresh
|
||||
GlobalEmitter.emit("telephone-history-updated");
|
||||
|
||||
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);
|
||||
if (justEnded) {
|
||||
// Handled above
|
||||
} else if (this.activeCall != null) {
|
||||
// if a new call starts, clear ended state
|
||||
this.isCallEnded = false;
|
||||
|
||||
@@ -191,7 +191,9 @@
|
||||
{{ $t("call.recording_voicemail") }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="activeCall && activeCall.is_incoming && activeCall.status === 4"
|
||||
v-else-if="
|
||||
activeCall && activeCall.is_incoming && activeCall.status === 4
|
||||
"
|
||||
class="text-blue-600 dark:text-blue-400 font-bold text-sm animate-bounce"
|
||||
>{{ $t("call.incoming_call") }}</span
|
||||
>
|
||||
@@ -200,17 +202,25 @@
|
||||
class="text-gray-700 dark:text-zinc-300 font-bold text-sm flex items-center gap-2"
|
||||
>
|
||||
<span v-if="activeCall && activeCall.status === 0">Busy...</span>
|
||||
<span v-else-if="activeCall && activeCall.status === 1" class="text-red-500"
|
||||
<span
|
||||
v-else-if="activeCall && activeCall.status === 1"
|
||||
class="text-red-500"
|
||||
>Rejected</span
|
||||
>
|
||||
<span v-else-if="activeCall && activeCall.status === 2" class="animate-pulse"
|
||||
<span
|
||||
v-else-if="activeCall && activeCall.status === 2"
|
||||
class="animate-pulse"
|
||||
>Calling...</span
|
||||
>
|
||||
<span v-else-if="activeCall && activeCall.status === 3">Available</span>
|
||||
<span v-else-if="activeCall && activeCall.status === 4" class="animate-pulse"
|
||||
<span
|
||||
v-else-if="activeCall && activeCall.status === 4"
|
||||
class="animate-pulse"
|
||||
>Ringing...</span
|
||||
>
|
||||
<span v-else-if="activeCall && activeCall.status === 5">Connecting...</span>
|
||||
<span v-else-if="activeCall && activeCall.status === 5"
|
||||
>Connecting...</span
|
||||
>
|
||||
<span
|
||||
v-else-if="activeCall && activeCall.status === 6"
|
||||
class="text-green-500 flex items-center gap-2"
|
||||
@@ -651,10 +661,10 @@
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] font-mono text-gray-400 dark:text-zinc-600 truncate mt-0.5 cursor-pointer hover:text-blue-500 transition-colors"
|
||||
:title="entry.remote_identity_hash"
|
||||
@click.stop="copyHash(entry.remote_identity_hash)"
|
||||
:title="entry.remote_destination_hash || entry.remote_identity_hash"
|
||||
@click.stop="copyHash(entry.remote_destination_hash || entry.remote_identity_hash)"
|
||||
>
|
||||
{{ formatDestinationHash(entry.remote_identity_hash) }}
|
||||
{{ formatDestinationHash(entry.remote_destination_hash || entry.remote_identity_hash) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1239,7 +1249,7 @@
|
||||
type="button"
|
||||
class="text-[10px] flex items-center gap-1 text-gray-500 hover:text-blue-500 font-bold uppercase tracking-wider transition-colors"
|
||||
@click="
|
||||
destinationHash = voicemail.remote_identity_hash;
|
||||
destinationHash = voicemail.remote_destination_hash || voicemail.remote_identity_hash;
|
||||
activeTab = 'phone';
|
||||
call(destinationHash);
|
||||
"
|
||||
@@ -2050,7 +2060,7 @@ export default {
|
||||
) {
|
||||
suggestions.push({
|
||||
name: c.name,
|
||||
hash: c.remote_identity_hash,
|
||||
hash: c.remote_destination_hash || c.remote_identity_hash,
|
||||
type: "contact",
|
||||
icon: "account",
|
||||
});
|
||||
@@ -2069,7 +2079,7 @@ export default {
|
||||
) {
|
||||
suggestions.push({
|
||||
name: h.remote_identity_name || h.remote_identity_hash.substring(0, 8),
|
||||
hash: h.remote_identity_hash,
|
||||
hash: h.remote_destination_hash || h.remote_identity_hash,
|
||||
type: "history",
|
||||
icon: "history",
|
||||
});
|
||||
@@ -2199,6 +2209,7 @@ export default {
|
||||
const response = await window.axios.get("/api/v1/telephone/status");
|
||||
const oldCall = this.activeCall;
|
||||
const newCall = response.data.active_call;
|
||||
const callStatus = response.data.call_status;
|
||||
|
||||
// Sync local mute state from backend
|
||||
if (newCall) {
|
||||
@@ -2213,6 +2224,14 @@ export default {
|
||||
this.initiationTargetHash = response.data.initiation_target_hash;
|
||||
this.initiationTargetName = response.data.initiation_target_name;
|
||||
|
||||
// If no active call and status is idle/busy/rejected/available, clear stale initiation UI
|
||||
const isIdleState = !this.activeCall && ![2, 4, 5].includes(callStatus);
|
||||
if (isIdleState && this.initiationStatus) {
|
||||
this.initiationStatus = null;
|
||||
this.initiationTargetHash = null;
|
||||
this.initiationTargetName = null;
|
||||
}
|
||||
|
||||
if (this.activeCall?.is_voicemail) {
|
||||
this.wasVoicemail = true;
|
||||
}
|
||||
@@ -2260,7 +2279,7 @@ export default {
|
||||
this.editingContact = null;
|
||||
this.contactForm = {
|
||||
name: entry.remote_identity_name || "",
|
||||
remote_identity_hash: entry.remote_identity_hash,
|
||||
remote_identity_hash: entry.remote_destination_hash || entry.remote_identity_hash,
|
||||
preferred_ringtone_id: null,
|
||||
};
|
||||
this.isContactModalOpen = true;
|
||||
@@ -2863,6 +2882,12 @@ export default {
|
||||
}
|
||||
|
||||
let hashToCall = identityHash.trim();
|
||||
// Accept lxmf:// URIs or pasted text; extract first 64-char hex
|
||||
const hexMatch = hashToCall.match(/[0-9a-fA-F]{64}/);
|
||||
if (hexMatch) {
|
||||
hashToCall = hexMatch[0];
|
||||
}
|
||||
hashToCall = hashToCall.toLowerCase();
|
||||
|
||||
// Try to resolve name from contacts
|
||||
const contact = this.contacts.find((c) => c.name.toLowerCase() === hashToCall.toLowerCase());
|
||||
|
||||
@@ -1082,8 +1082,12 @@
|
||||
class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="isRawMessageModalOpen = false"
|
||||
>
|
||||
<div class="w-full max-w-2xl bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
||||
<div class="px-6 py-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-between shrink-0">
|
||||
<div
|
||||
class="w-full max-w-2xl bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
|
||||
>
|
||||
<div
|
||||
class="px-6 py-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-between shrink-0"
|
||||
>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Raw LXMF Message</h3>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1094,7 +1098,9 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto font-mono text-xs bg-gray-50 dark:bg-black/40">
|
||||
<pre class="whitespace-pre-wrap break-all text-gray-800 dark:text-zinc-300">{{ JSON.stringify(rawMessageData, null, 2) }}</pre>
|
||||
<pre class="whitespace-pre-wrap break-all text-gray-800 dark:text-zinc-300">{{
|
||||
JSON.stringify(rawMessageData, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-zinc-800 flex justify-end shrink-0">
|
||||
<button
|
||||
@@ -1607,10 +1613,7 @@ export default {
|
||||
// if content is only the paper message, or it already contains the detected text,
|
||||
// we'll hide the raw content div to avoid double rendering.
|
||||
const trimmedContent = content.trim();
|
||||
if (
|
||||
trimmedContent === items.paperMessage ||
|
||||
trimmedContent.includes("Paper Message detected")
|
||||
) {
|
||||
if (trimmedContent === items.paperMessage || trimmedContent.includes("Paper Message detected")) {
|
||||
items.isOnlyPaperMessage = true;
|
||||
}
|
||||
}
|
||||
@@ -2040,7 +2043,7 @@ export default {
|
||||
...chatItem.lxmf_message,
|
||||
raw_uri: response.data.uri,
|
||||
};
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// if URI is not available (message no longer in router), we show what we have
|
||||
this.rawMessageData = { ...chatItem.lxmf_message };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export default class ToneGenerator {
|
||||
this.oscillator = null;
|
||||
this.gainNode = null;
|
||||
this.timeoutId = null;
|
||||
this.currentTone = null; // 'ringback', 'busy', or null
|
||||
}
|
||||
|
||||
_initAudioContext() {
|
||||
@@ -13,8 +14,10 @@ export default class ToneGenerator {
|
||||
}
|
||||
|
||||
playRingback() {
|
||||
if (this.currentTone === "ringback") return;
|
||||
this._initAudioContext();
|
||||
this.stop();
|
||||
this.currentTone = "ringback";
|
||||
|
||||
const play = () => {
|
||||
const osc1 = this.audioCtx.createOscillator();
|
||||
@@ -57,8 +60,10 @@ export default class ToneGenerator {
|
||||
}
|
||||
|
||||
playBusyTone() {
|
||||
if (this.currentTone === "busy") return;
|
||||
this._initAudioContext();
|
||||
this.stop();
|
||||
this.currentTone = "busy";
|
||||
|
||||
const play = () => {
|
||||
const osc = this.audioCtx.createOscillator();
|
||||
@@ -89,12 +94,13 @@ export default class ToneGenerator {
|
||||
};
|
||||
|
||||
play();
|
||||
|
||||
|
||||
// Auto-stop busy tone after 4 seconds (4 cycles)
|
||||
setTimeout(() => this.stop(), 4000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.currentTone = null;
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
@@ -102,21 +108,40 @@ export default class ToneGenerator {
|
||||
|
||||
if (this.oscillator) {
|
||||
if (Array.isArray(this.oscillator)) {
|
||||
this.oscillator.forEach(osc => {
|
||||
try { osc.stop(); } catch (e) {}
|
||||
try { osc.disconnect(); } catch (e) {}
|
||||
this.oscillator.forEach((osc) => {
|
||||
try {
|
||||
osc.stop();
|
||||
} catch {
|
||||
// Ignore errors if oscillator is already stopped or disconnected
|
||||
}
|
||||
try {
|
||||
osc.disconnect();
|
||||
} catch {
|
||||
// Ignore errors if oscillator is already disconnected
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try { this.oscillator.stop(); } catch (e) {}
|
||||
try { this.oscillator.disconnect(); } catch (e) {}
|
||||
try {
|
||||
this.oscillator.stop();
|
||||
} catch {
|
||||
// Ignore errors if oscillator is already stopped or disconnected
|
||||
}
|
||||
try {
|
||||
this.oscillator.disconnect();
|
||||
} catch {
|
||||
// Ignore errors if oscillator is already disconnected
|
||||
}
|
||||
}
|
||||
this.oscillator = null;
|
||||
}
|
||||
|
||||
if (this.gainNode) {
|
||||
try { this.gainNode.disconnect(); } catch (e) {}
|
||||
try {
|
||||
this.gainNode.disconnect();
|
||||
} catch {
|
||||
// Ignore errors if gain node is already disconnected
|
||||
}
|
||||
this.gainNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user