diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index 24fb8d5..ca347eb 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -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 diff --git a/meshchatx/src/backend/telephone_manager.py b/meshchatx/src/backend/telephone_manager.py index 231f10c..1165168 100644 --- a/meshchatx/src/backend/telephone_manager.py +++ b/meshchatx/src/backend/telephone_manager.py @@ -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: diff --git a/meshchatx/src/backend/voicemail_manager.py b/meshchatx/src/backend/voicemail_manager.py index 629da48..927fb96 100644 --- a/meshchatx/src/backend/voicemail_manager.py +++ b/meshchatx/src/backend/voicemail_manager.py @@ -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): diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index d5dddd5..09f5213 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -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; diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue index 7ec0418..88cc70e 100644 --- a/meshchatx/src/frontend/components/call/CallPage.vue +++ b/meshchatx/src/frontend/components/call/CallPage.vue @@ -191,7 +191,9 @@ {{ $t("call.recording_voicemail") }} {{ $t("call.incoming_call") }} @@ -200,17 +202,25 @@ class="text-gray-700 dark:text-zinc-300 font-bold text-sm flex items-center gap-2" > Busy... - Rejected - Calling... Available - Ringing... - Connecting... + Connecting...
- {{ formatDestinationHash(entry.remote_identity_hash) }} + {{ formatDestinationHash(entry.remote_destination_hash || entry.remote_identity_hash) }}
@@ -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()); diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue index f00fd8b..920aaff 100644 --- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue +++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue @@ -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" > -
-
+
+

Raw LXMF Message

-
{{ JSON.stringify(rawMessageData, null, 2) }}
+
{{
+                        JSON.stringify(rawMessageData, null, 2)
+                    }}