feat(App): implement unread conversations count update and update telephony settings in the UI

This commit is contained in:
Ivan
2026-05-06 00:21:37 -05:00
parent 151aa4c55f
commit ef040a9e90
7 changed files with 123 additions and 34 deletions
+31 -1
View File
@@ -685,6 +685,7 @@ export default {
ElectronUtils,
reloadInterval: null,
appInfoInterval: null,
unreadCountInterval: null,
isShowingMyIdentitySection: true,
isShowingAnnounceSection: true,
@@ -915,6 +916,10 @@ export default {
this.appInfoInterval = setInterval(() => {
this.getAppInfo();
}, 15000);
this.unreadCountInterval = setInterval(() => {
this.updateUnreadConversationsCount();
}, 5000);
this.updateUnreadConversationsCount();
},
stopShell() {
if (!this.shellRunning) {
@@ -925,6 +930,8 @@ export default {
this.reloadInterval = null;
clearInterval(this.appInfoInterval);
this.appInfoInterval = null;
clearInterval(this.unreadCountInterval);
this.unreadCountInterval = null;
WebSocketConnection.off("message", this.onWebsocketMessage);
WebSocketConnection.off("disconnected", this.onWsShellDisconnected);
WebSocketConnection.off("connected", this.onWsShellConnected);
@@ -1084,6 +1091,21 @@ export default {
onShowTutorialShell() {
this.$refs.tutorialModal?.show();
},
updateUnreadConversationsCount() {
if (this._unreadCountTimeout) {
clearTimeout(this._unreadCountTimeout);
}
this._unreadCountTimeout = setTimeout(async () => {
try {
const response = await window.api.get("/api/v1/notifications", {
params: { unread: true, limit: 1 },
});
GlobalState.unreadConversationsCount = response.data?.lxmf_total_unread_count ?? 0;
} catch (e) {
console.error("Failed to update unread conversations count", e);
}
}, 300);
},
onConfigUpdatedExternally(newConfig) {
if (!newConfig || typeof newConfig !== "object") {
return;
@@ -1144,7 +1166,9 @@ export default {
if (this.initiationStatus) {
break;
}
NotificationUtils.showIncomingCallNotification();
NotificationUtils.showIncomingCallNotification(
json.remote_identity_name || json.remote_identity_hash
);
this.updateTelephoneStatus();
this.playRingtone();
break;
@@ -1181,11 +1205,13 @@ export default {
this.stopRingtone();
this.ringtonePlayer = null;
this.toneGenerator.stop();
NotificationUtils.cancelIncomingCallNotification();
this.updateTelephoneStatus();
break;
}
case "telephone_call_ended": {
this.stopRingtone();
NotificationUtils.cancelIncomingCallNotification();
this.ringtonePlayer = null;
if (this.config?.telephone_tone_generator_enabled) {
this.toneGenerator.setVolume(this.config.telephone_tone_generator_volume);
@@ -1202,6 +1228,10 @@ export default {
break;
}
// Update sidebar unread count so the badge appears
// immediately even when not on the Messages page.
this.updateUnreadConversationsCount();
// show notification for new messages if window is not focussed
// only for incoming messages from people (with content)
if (
+1 -1
View File
@@ -2,7 +2,7 @@
<template>
<div
class="fixed max-sm:bottom-[calc(5.75rem+env(safe-area-inset-bottom,0px))] bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
class="fixed max-sm:bottom-[calc(1rem+env(safe-area-inset-bottom,0px))] bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
>
<TransitionGroup name="toast">
<div
@@ -135,7 +135,7 @@
</div>
</div>
<div class="relative z-10 space-y-1 mb-8">
<div class="relative z-10 space-y-1 mb-8 flex flex-col items-center text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white truncate max-w-[280px]">
{{
(activeCall || lastCall)?.remote_identity_name ||
@@ -528,34 +528,36 @@
class="pt-2 flex flex-col items-stretch gap-4 lg:flex-row lg:items-start lg:justify-between"
>
<div class="flex min-w-0 flex-1 flex-col gap-2">
<Toggle
id="dnd-toggle"
:model-value="config?.do_not_disturb_enabled"
:label="$t('call.do_not_disturb')"
@update:model-value="toggleDoNotDisturb"
/>
<Toggle
id="contacts-only-toggle"
:model-value="config?.telephone_allow_calls_from_contacts_only"
:label="$t('call.allow_calls_from_contacts_only')"
@update:model-value="toggleAllowCallsFromContactsOnly"
/>
<Toggle
id="telephone-announce-toggle"
:model-value="config?.telephone_announce_enabled"
label="Announce Telephone Presence (LXST)"
@update:model-value="toggleTelephoneAnnounceEnabled"
/>
<div class="flex flex-col gap-1">
<div v-if="config?.telephone_enabled" class="flex flex-col gap-2">
<Toggle
id="web-audio-toggle"
:model-value="config?.telephone_web_audio_enabled"
label="Web Audio Bridge"
@update:model-value="onToggleWebAudio"
id="dnd-toggle"
:model-value="config?.do_not_disturb_enabled"
:label="$t('call.do_not_disturb')"
@update:model-value="toggleDoNotDisturb"
/>
<div class="text-xs text-gray-500 dark:text-zinc-400 px-1">
Web audio bridge allows web/electron to hook into LXST backend for
passing microphone and audio streams to active telephone calls.
<Toggle
id="contacts-only-toggle"
:model-value="config?.telephone_allow_calls_from_contacts_only"
:label="$t('call.allow_calls_from_contacts_only')"
@update:model-value="toggleAllowCallsFromContactsOnly"
/>
<Toggle
id="telephone-announce-toggle"
:model-value="config?.telephone_announce_enabled"
label="Announce Telephone Presence (LXST)"
@update:model-value="toggleTelephoneAnnounceEnabled"
/>
<div class="flex flex-col gap-1">
<Toggle
id="web-audio-toggle"
:model-value="config?.telephone_web_audio_enabled"
label="Web Audio Bridge"
@update:model-value="onToggleWebAudio"
/>
<div class="text-xs text-gray-500 dark:text-zinc-400 px-1">
Web audio bridge allows web/electron to hook into LXST backend for
passing microphone and audio streams to active telephone calls.
</div>
</div>
</div>
</div>
@@ -2715,6 +2717,11 @@ export default {
this.stopWebAudio();
return;
}
// Do not start web audio during voicemail
if (this.activeCall?.is_voicemail) {
this.stopWebAudio();
return;
}
if (this.activeCall && webAudioStatus.enabled) {
this.audioFrameMs = webAudioStatus.frame_ms || 60;
await this.startWebAudio();
@@ -1937,7 +1937,7 @@ export default {
interface_discovery_whitelist: "",
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
autoconnect_discovered_interfaces: null,
default_bootstrap_only: true,
network_identity: "",
},
@@ -293,7 +293,9 @@ export default {
}
const st = this.iface._stats;
if (!st || typeof st !== "object") {
return null;
// If stats are missing, the interface is likely detached or
// not yet initialised; show down rather than unknown.
return false;
}
if ("status" in st) {
const s = st.status;
@@ -752,7 +752,7 @@ export default {
interface_discovery_whitelist: "",
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
autoconnect_discovered_interfaces: null,
default_bootstrap_only: true,
network_identity: "",
},
@@ -1242,7 +1242,7 @@ export default {
discovery.autoconnect_discovered_interfaces !== null &&
discovery.autoconnect_discovered_interfaces !== ""
? Number(discovery.autoconnect_discovered_interfaces)
: 0;
: null;
this.discoveryConfig.default_bootstrap_only = this.parseBool(discovery.default_bootstrap_only ?? true);
this.discoveryConfig.network_identity = discovery.network_identity ?? "";
} catch (e) {
@@ -1266,7 +1266,7 @@ export default {
autoconnect_discovered_interfaces:
this.discoveryConfig.autoconnect_discovered_interfaces === null ||
this.discoveryConfig.autoconnect_discovered_interfaces === ""
? 0
? null
: Number(this.discoveryConfig.autoconnect_discovered_interfaces),
default_bootstrap_only: this.discoveryConfig.default_bootstrap_only,
network_identity: this.discoveryConfig.network_identity || null,
@@ -671,6 +671,36 @@
</div>
</section>
<!-- Telephony Settings -->
<section
v-show="matchesSearch(...sectionKeywords.telephony)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
<div>
<div class="settings-section__eyebrow">Telephony</div>
<h2>Telephone (LXST)</h2>
<p>Enable or disable the integrated voice calling system.</p>
</div>
</header>
<div class="settings-section__body space-y-4">
<label class="setting-toggle">
<Toggle
id="telephone-enabled-toggle"
v-model="config.telephone_enabled"
@update:model-value="onTelephoneEnabledChange"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">Enable Telephone (LXST)</span>
<span class="setting-toggle__description">
Allow incoming and outgoing voice calls over the mesh network.
</span>
<span class="setting-toggle__hint">Disabling will end any active calls.</span>
</span>
</label>
</div>
</section>
<!-- Desktop / Electron Settings -->
<section
v-if="ElectronUtils.isElectron()"
@@ -2669,6 +2699,16 @@ export default {
visualiserShowDisabledInterfaces: false,
visualiserShowDiscoveredInterfaces: false,
sectionKeywords: {
telephony: [
"Telephony",
"Telephone",
"LXST",
"Enable Telephone",
"voice",
"calling",
"call",
"mesh network",
],
strangerProtection: [
"Security",
"app.stranger_protection",
@@ -3739,6 +3779,16 @@ export default {
);
}, 1000);
},
async onTelephoneEnabledChange(value) {
this.config.telephone_enabled = value;
try {
const newConfig = await patchServerConfig({ telephone_enabled: value }, window.api);
this.config = newConfig;
ToastUtils.success(value ? "Telephone enabled" : "Telephone disabled");
} catch {
ToastUtils.error("Failed to update telephone setting");
}
},
async onDesktopOpenCallsInSeparateWindowChange(value) {
this.config.desktop_open_calls_in_separate_window = value;
await this.updateConfig(