feat(call): update tone generator functionality with volume control and enable/disable settings; update frontend components to reflect new configurations
This commit is contained in:
@@ -3882,8 +3882,8 @@ class ReticulumMeshChat:
|
||||
remote_destination_hash = RNS.Destination.hash(
|
||||
remote_identity, "lxmf", "delivery"
|
||||
).hex()
|
||||
remote_telephony_hash = (
|
||||
self.get_lxst_telephony_hash_for_identity_hash(remote_hash)
|
||||
remote_telephony_hash = self.get_lxst_telephony_hash_for_identity_hash(
|
||||
remote_hash
|
||||
)
|
||||
remote_name = None
|
||||
if self.telephone_manager.get_name_for_identity_hash:
|
||||
@@ -3915,7 +3915,6 @@ class ReticulumMeshChat:
|
||||
"custom_image": custom_image,
|
||||
"is_incoming": telephone_active_call.is_incoming,
|
||||
"status": self.telephone_manager.telephone.call_status,
|
||||
"remote_destination_hash": remote_destination_hash,
|
||||
"remote_telephony_hash": remote_telephony_hash,
|
||||
"audio_profile_id": self.telephone_manager.telephone.transmit_codec.profile
|
||||
if hasattr(
|
||||
@@ -4145,11 +4144,15 @@ class ReticulumMeshChat:
|
||||
status=503,
|
||||
)
|
||||
|
||||
# check if busy
|
||||
if (
|
||||
self.telephone_manager.telephone.busy
|
||||
or self.telephone_manager.initiation_status
|
||||
):
|
||||
# check if busy, but ignore stale busy when no active call
|
||||
is_busy = self.telephone_manager.telephone.busy
|
||||
if is_busy and not self.telephone_manager.telephone.active_call:
|
||||
# If there's no active call and we're not currently initiating,
|
||||
# we shouldn't be busy.
|
||||
if not self.telephone_manager.initiation_status:
|
||||
is_busy = False
|
||||
|
||||
if is_busy or self.telephone_manager.initiation_status:
|
||||
return web.json_response(
|
||||
{
|
||||
"message": "Telephone is busy",
|
||||
@@ -4713,10 +4716,16 @@ class ReticulumMeshChat:
|
||||
lxmf_hash = self.get_lxmf_destination_hash_for_identity_hash(
|
||||
remote_identity_hash,
|
||||
)
|
||||
tele_hash = self.get_lxst_telephony_hash_for_identity_hash(
|
||||
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)
|
||||
if tele_hash:
|
||||
d["remote_telephony_hash"] = tele_hash
|
||||
contacts.append(d)
|
||||
|
||||
return web.json_response(contacts)
|
||||
@@ -4844,6 +4853,7 @@ class ReticulumMeshChat:
|
||||
|
||||
# fetch custom display names
|
||||
custom_names = {}
|
||||
lxmf_names_for_telephony = {}
|
||||
if other_user_hashes:
|
||||
db_custom_names = self.database.provider.fetchall(
|
||||
f"SELECT destination_hash, display_name FROM custom_destination_display_names WHERE destination_hash IN ({','.join(['?'] * len(other_user_hashes))})", # noqa: S608
|
||||
@@ -4852,6 +4862,27 @@ class ReticulumMeshChat:
|
||||
for row in db_custom_names:
|
||||
custom_names[row["destination_hash"]] = row["display_name"]
|
||||
|
||||
# If we're looking for telephony announces, pre-fetch LXMF announces for the same identities
|
||||
if aspect == "lxst.telephony":
|
||||
identity_hashes = list(
|
||||
set(
|
||||
[
|
||||
r["identity_hash"]
|
||||
for r in results
|
||||
if r.get("identity_hash")
|
||||
]
|
||||
)
|
||||
)
|
||||
if identity_hashes:
|
||||
lxmf_results = self.database.announces.provider.fetchall(
|
||||
f"SELECT identity_hash, app_data FROM announces WHERE aspect = 'lxmf.delivery' AND identity_hash IN ({','.join(['?'] * len(identity_hashes))})", # noqa: S608
|
||||
identity_hashes,
|
||||
)
|
||||
for row in lxmf_results:
|
||||
lxmf_names_for_telephony[row["identity_hash"]] = (
|
||||
parse_lxmf_display_name(row["app_data"])
|
||||
)
|
||||
|
||||
# process all announces
|
||||
all_announces = []
|
||||
for announce in results:
|
||||
@@ -4861,6 +4892,11 @@ class ReticulumMeshChat:
|
||||
|
||||
# parse display name from announce
|
||||
display_name = None
|
||||
is_local = (
|
||||
self.current_context
|
||||
and announce["identity_hash"] == self.current_context.identity_hash
|
||||
)
|
||||
|
||||
if announce["aspect"] == "lxmf.delivery":
|
||||
display_name = parse_lxmf_display_name(announce["app_data"])
|
||||
elif announce["aspect"] == "nomadnetwork.node":
|
||||
@@ -4868,7 +4904,22 @@ class ReticulumMeshChat:
|
||||
announce["app_data"]
|
||||
)
|
||||
elif announce["aspect"] == "lxst.telephony":
|
||||
display_name = announce.get("display_name") or "Anonymous Peer"
|
||||
display_name = parse_lxmf_display_name(announce["app_data"])
|
||||
if not display_name or display_name == "Anonymous Peer":
|
||||
# Try pre-fetched LXMF name
|
||||
display_name = lxmf_names_for_telephony.get(
|
||||
announce["identity_hash"]
|
||||
)
|
||||
|
||||
if not display_name or display_name == "Anonymous Peer":
|
||||
if is_local and self.current_context:
|
||||
display_name = self.current_context.config.display_name.get()
|
||||
else:
|
||||
# try to resolve name from identity hash (checks contacts too)
|
||||
display_name = (
|
||||
self.get_name_for_identity_hash(announce["identity_hash"])
|
||||
or "Anonymous Peer"
|
||||
)
|
||||
|
||||
# get current hops away
|
||||
hops = RNS.Transport.hops_to(
|
||||
@@ -7783,6 +7834,16 @@ class ReticulumMeshChat:
|
||||
else:
|
||||
self.telephone_manager.stop_recording()
|
||||
|
||||
if "telephone_tone_generator_enabled" in data:
|
||||
self.config.telephone_tone_generator_enabled.set(
|
||||
self._parse_bool(data["telephone_tone_generator_enabled"]),
|
||||
)
|
||||
|
||||
if "telephone_tone_generator_volume" in data:
|
||||
self.config.telephone_tone_generator_volume.set(
|
||||
int(data["telephone_tone_generator_volume"]),
|
||||
)
|
||||
|
||||
if "telephone_audio_profile_id" in data:
|
||||
profile_id = int(data["telephone_audio_profile_id"])
|
||||
self.config.telephone_audio_profile_id.set(profile_id)
|
||||
@@ -8609,6 +8670,8 @@ class ReticulumMeshChat:
|
||||
"desktop_open_calls_in_separate_window": ctx.config.desktop_open_calls_in_separate_window.get(),
|
||||
"desktop_hardware_acceleration_enabled": ctx.config.desktop_hardware_acceleration_enabled.get(),
|
||||
"blackhole_integration_enabled": ctx.config.blackhole_integration_enabled.get(),
|
||||
"telephone_tone_generator_enabled": ctx.config.telephone_tone_generator_enabled.get(),
|
||||
"telephone_tone_generator_volume": ctx.config.telephone_tone_generator_volume.get(),
|
||||
}
|
||||
|
||||
# try and get a name for the provided identity hash
|
||||
|
||||
@@ -186,6 +186,16 @@ class ConfigManager:
|
||||
"call_recording_enabled",
|
||||
False,
|
||||
)
|
||||
self.telephone_tone_generator_enabled = self.BoolConfig(
|
||||
self,
|
||||
"telephone_tone_generator_enabled",
|
||||
True,
|
||||
)
|
||||
self.telephone_tone_generator_volume = self.IntConfig(
|
||||
self,
|
||||
"telephone_tone_generator_volume",
|
||||
50,
|
||||
)
|
||||
|
||||
# map config
|
||||
self.map_offline_enabled = self.BoolConfig(self, "map_offline_enabled", False)
|
||||
|
||||
@@ -272,6 +272,8 @@ class TelephoneManager:
|
||||
# Wait for identity to appear
|
||||
start_wait = time.time()
|
||||
while time.time() - start_wait < timeout_seconds:
|
||||
if not self.initiation_status: # Externally cancelled (hangup)
|
||||
return None
|
||||
await asyncio.sleep(0.5)
|
||||
destination_identity = resolve_identity(destination_hash_hex)
|
||||
if destination_identity:
|
||||
@@ -289,6 +291,8 @@ class TelephoneManager:
|
||||
# Wait up to 10s for path discovery
|
||||
path_wait_start = time.time()
|
||||
while time.time() - path_wait_start < min(timeout_seconds, 10):
|
||||
if not self.initiation_status: # Externally cancelled
|
||||
return None
|
||||
if RNS.Transport.has_path(destination_hash):
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
@@ -307,6 +311,8 @@ class TelephoneManager:
|
||||
# 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 not self.initiation_status: # Externally cancelled
|
||||
break
|
||||
if self.telephone.call_status in [
|
||||
6,
|
||||
0,
|
||||
@@ -321,9 +327,14 @@ class TelephoneManager:
|
||||
|
||||
# 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
|
||||
if self.initiation_status and self.telephone.call_status in [
|
||||
4,
|
||||
5,
|
||||
]: # Ringing, Connecting
|
||||
wait_until = time.time() + timeout_seconds
|
||||
while time.time() < wait_until:
|
||||
if not self.initiation_status: # Externally cancelled
|
||||
break
|
||||
if self.telephone.call_status in [
|
||||
6,
|
||||
0,
|
||||
|
||||
@@ -745,7 +745,10 @@ export default {
|
||||
this.initiationTargetName = json.target_name;
|
||||
|
||||
if (this.initiationStatus === "Ringing...") {
|
||||
this.toneGenerator.playRingback();
|
||||
if (this.config?.telephone_tone_generator_enabled) {
|
||||
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
|
||||
this.toneGenerator.playRingback();
|
||||
}
|
||||
} else if (this.initiationStatus === null) {
|
||||
this.toneGenerator.stop();
|
||||
}
|
||||
@@ -768,7 +771,10 @@ export default {
|
||||
case "telephone_call_ended": {
|
||||
this.stopRingtone();
|
||||
this.ringtonePlayer = null;
|
||||
this.toneGenerator.playBusyTone();
|
||||
if (this.config?.telephone_tone_generator_enabled) {
|
||||
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
|
||||
this.toneGenerator.playBusyTone();
|
||||
}
|
||||
this.updateTelephoneStatus();
|
||||
break;
|
||||
}
|
||||
@@ -850,6 +856,7 @@ export default {
|
||||
const response = await window.axios.get(`/api/v1/config`);
|
||||
this.config = response.data.config;
|
||||
GlobalState.config = response.data.config;
|
||||
this.displayName = response.data.config.display_name;
|
||||
} catch (e) {
|
||||
// do nothing if failed to load config
|
||||
console.log(e);
|
||||
@@ -1067,7 +1074,10 @@ export default {
|
||||
const justEnded = oldCall != null && this.activeCall == null;
|
||||
if (justEnded) {
|
||||
this.lastCall = oldCall;
|
||||
this.toneGenerator.playBusyTone();
|
||||
if (this.config?.telephone_tone_generator_enabled) {
|
||||
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
|
||||
this.toneGenerator.playBusyTone();
|
||||
}
|
||||
|
||||
// Trigger history refresh
|
||||
GlobalEmitter.emit("telephone-history-updated");
|
||||
@@ -1086,7 +1096,10 @@ export default {
|
||||
|
||||
// Handle outgoing ringback tone
|
||||
if (this.initiationStatus === "Ringing...") {
|
||||
this.toneGenerator.playRingback();
|
||||
if (this.config?.telephone_tone_generator_enabled) {
|
||||
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
|
||||
this.toneGenerator.playRingback();
|
||||
}
|
||||
} 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();
|
||||
|
||||
@@ -661,10 +661,26 @@
|
||||
</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_telephony_hash || entry.remote_destination_hash || entry.remote_identity_hash"
|
||||
@click.stop="copyHash(entry.remote_telephony_hash || entry.remote_destination_hash || entry.remote_identity_hash)"
|
||||
:title="
|
||||
entry.remote_telephony_hash ||
|
||||
entry.remote_destination_hash ||
|
||||
entry.remote_identity_hash
|
||||
"
|
||||
@click.stop="
|
||||
copyHash(
|
||||
entry.remote_telephony_hash ||
|
||||
entry.remote_destination_hash ||
|
||||
entry.remote_identity_hash
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatDestinationHash(entry.remote_telephony_hash || entry.remote_destination_hash || entry.remote_identity_hash) }}
|
||||
{{
|
||||
formatDestinationHash(
|
||||
entry.remote_telephony_hash ||
|
||||
entry.remote_destination_hash ||
|
||||
entry.remote_identity_hash
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -692,8 +708,12 @@
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-1 bg-blue-600 text-white rounded-lg text-[10px] font-bold hover:bg-blue-500 transition-all shadow-md shadow-blue-500/10 shrink-0"
|
||||
@click="
|
||||
destinationHash = entry.remote_identity_hash;
|
||||
call(destinationHash);
|
||||
destinationHash =
|
||||
entry.remote_telephony_hash ||
|
||||
entry.remote_destination_hash ||
|
||||
entry.remote_identity_hash;
|
||||
activeTab = 'phone';
|
||||
$nextTick(() => call(destinationHash));
|
||||
"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone" class="size-3" />
|
||||
@@ -819,7 +839,7 @@
|
||||
@click="
|
||||
destinationHash = announce.destination_hash;
|
||||
activeTab = 'phone';
|
||||
call(destinationHash);
|
||||
$nextTick(() => call(destinationHash));
|
||||
"
|
||||
>
|
||||
Call
|
||||
@@ -1249,9 +1269,12 @@
|
||||
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_telephony_hash || voicemail.remote_destination_hash || voicemail.remote_identity_hash;
|
||||
destinationHash =
|
||||
voicemail.remote_telephony_hash ||
|
||||
voicemail.remote_destination_hash ||
|
||||
voicemail.remote_identity_hash;
|
||||
activeTab = 'phone';
|
||||
call(destinationHash);
|
||||
$nextTick(() => call(destinationHash));
|
||||
"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone" class="size-3" />
|
||||
@@ -1383,9 +1406,12 @@
|
||||
type="button"
|
||||
class="text-[10px] bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 px-3 py-1 rounded-full font-bold uppercase tracking-wider hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors"
|
||||
@click="
|
||||
destinationHash = contact.remote_identity_hash;
|
||||
destinationHash =
|
||||
contact.remote_telephony_hash ||
|
||||
contact.remote_destination_hash ||
|
||||
contact.remote_identity_hash;
|
||||
activeTab = 'phone';
|
||||
call(destinationHash);
|
||||
$nextTick(() => call(destinationHash));
|
||||
"
|
||||
>
|
||||
Call
|
||||
@@ -1478,6 +1504,73 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tone Generator Settings -->
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center justify-between gap-6 pt-4 border-t border-gray-100 dark:border-zinc-800/50"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Tone Generator
|
||||
</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.telephone_tone_generator_enabled
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-zinc-700'
|
||||
"
|
||||
@click="
|
||||
config.telephone_tone_generator_enabled =
|
||||
!config.telephone_tone_generator_enabled;
|
||||
updateConfig({
|
||||
telephone_tone_generator_enabled:
|
||||
config.telephone_tone_generator_enabled,
|
||||
});
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="
|
||||
config.telephone_tone_generator_enabled
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-400">
|
||||
Play audio feedback during call dialing and disconnection.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="config.telephone_tone_generator_enabled" class="flex-1 md:max-w-xs">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label
|
||||
class="text-xs font-bold text-gray-500 dark:text-zinc-400 uppercase tracking-wider"
|
||||
>
|
||||
Tone Volume
|
||||
</label>
|
||||
<span class="text-xs font-mono text-gray-400"
|
||||
>{{ config.telephone_tone_generator_volume }}%</span
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="config.telephone_tone_generator_volume"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
class="w-full h-1.5 bg-gray-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
@change="
|
||||
updateConfig({
|
||||
telephone_tone_generator_volume:
|
||||
config.telephone_tone_generator_volume,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preferred Ringtone for Non-Contacts -->
|
||||
<div
|
||||
class="p-4 rounded-xl bg-blue-50/50 dark:bg-blue-900/10 border border-blue-100/50 dark:border-blue-900/30"
|
||||
@@ -2188,8 +2281,10 @@ export default {
|
||||
},
|
||||
async updateConfig(config) {
|
||||
try {
|
||||
await window.axios.patch("/api/v1/config", config);
|
||||
await this.getConfig();
|
||||
const response = await window.axios.patch("/api/v1/config", config);
|
||||
if (response.data?.config) {
|
||||
this.config = response.data.config;
|
||||
}
|
||||
ToastUtils.success("Settings saved");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save settings");
|
||||
|
||||
@@ -902,6 +902,8 @@ export default {
|
||||
banished_text: "BANISHED",
|
||||
banished_color: "#dc2626",
|
||||
blackhole_integration_enabled: true,
|
||||
telephone_tone_generator_enabled: true,
|
||||
telephone_tone_generator_volume: 50,
|
||||
},
|
||||
saveTimeouts: {},
|
||||
shortcuts: [],
|
||||
|
||||
@@ -5,6 +5,15 @@ export default class ToneGenerator {
|
||||
this.gainNode = null;
|
||||
this.timeoutId = null;
|
||||
this.currentTone = null; // 'ringback', 'busy', or null
|
||||
this.volume = 0.1; // Default volume (0.0 to 1.0 real gain)
|
||||
}
|
||||
|
||||
setVolume(volumePercent) {
|
||||
// volumePercent is 0-100
|
||||
this.volume = (volumePercent / 100) * 0.2; // Cap at 0.2 real gain for safety
|
||||
if (this.gainNode && this.audioCtx) {
|
||||
this.gainNode.gain.setTargetAtTime(this.volume, this.audioCtx.currentTime, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
_initAudioContext() {
|
||||
@@ -26,7 +35,7 @@ export default class ToneGenerator {
|
||||
|
||||
osc1.frequency.value = 440;
|
||||
osc2.frequency.value = 480;
|
||||
gain.gain.value = 0.1;
|
||||
gain.gain.value = this.volume;
|
||||
|
||||
osc1.connect(gain);
|
||||
osc2.connect(gain);
|
||||
@@ -70,7 +79,7 @@ export default class ToneGenerator {
|
||||
const gain = this.audioCtx.createGain();
|
||||
|
||||
osc.frequency.value = 480;
|
||||
gain.gain.value = 0.1;
|
||||
gain.gain.value = this.volume;
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.audioCtx.destination);
|
||||
|
||||
Reference in New Issue
Block a user