{{
isSyncingPropagationNode
diff --git a/meshchatx/src/frontend/components/LxmfUserIcon.vue b/meshchatx/src/frontend/components/LxmfUserIcon.vue
index d054c88..c174dd8 100644
--- a/meshchatx/src/frontend/components/LxmfUserIcon.vue
+++ b/meshchatx/src/frontend/components/LxmfUserIcon.vue
@@ -3,13 +3,14 @@
v-if="customImage"
class="rounded-full overflow-hidden shrink-0 flex items-center justify-center"
:class="iconClass || 'size-6'"
+ :style="iconStyle"
>
@@ -18,6 +19,7 @@
v-else
class="bg-gray-100 dark:bg-zinc-800 text-gray-400 dark:text-zinc-500 p-[15%] rounded-full shrink-0 flex items-center justify-center border border-gray-200 dark:border-zinc-700"
:class="iconClass || 'size-6'"
+ :style="iconStyle"
>
@@ -51,6 +53,10 @@ export default {
type: String,
default: "",
},
+ iconStyle: {
+ type: Object,
+ default: () => ({}),
+ },
},
computed: {
finalForegroundColor() {
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index fc82b14..bd5f3ca 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -2411,18 +2411,26 @@ export default {
},
async onToggleWebAudio(newVal) {
if (!this.config) return;
+ const previousValue = this.config.telephone_web_audio_enabled;
this.config.telephone_web_audio_enabled = newVal;
try {
+ if (newVal) {
+ const permitted = await this.requestAudioPermission();
+ if (!permitted) {
+ this.config.telephone_web_audio_enabled = false;
+ await this.updateConfig({ telephone_web_audio_enabled: false });
+ return;
+ }
+ }
await this.updateConfig({ telephone_web_audio_enabled: newVal });
if (newVal) {
- await this.requestAudioPermission();
await this.startWebAudio();
} else {
this.stopWebAudio();
}
} catch {
// revert on failure
- this.config.telephone_web_audio_enabled = !newVal;
+ this.config.telephone_web_audio_enabled = previousValue;
}
},
async startWebAudio() {
@@ -2430,7 +2438,18 @@ export default {
return;
}
try {
- const constraints = this.selectedAudioInputId
+ await this.refreshAudioDevices();
+ const hasInputDevices = (this.audioInputDevices || []).length > 0;
+ if (!hasInputDevices) {
+ ToastUtils.error(this.$t("call.no_audio_input_found"));
+ this.config.telephone_web_audio_enabled = false;
+ await this.updateConfig({ telephone_web_audio_enabled: false });
+ return;
+ }
+
+ const validDeviceIds = new Set((this.audioInputDevices || []).map((d) => d.deviceId));
+ const hasSelectedDevice = this.selectedAudioInputId && validDeviceIds.has(this.selectedAudioInputId);
+ const constraints = hasSelectedDevice
? { audio: { deviceId: { exact: this.selectedAudioInputId } } }
: { audio: true };
const stream = await navigator.mediaDevices.getUserMedia(constraints);
@@ -2481,17 +2500,44 @@ export default {
this.refreshAudioDevices();
} catch (err) {
console.error("Web audio failed", err);
- ToastUtils.error(this.$t("call.web_audio_not_available"));
+ const errorKey =
+ err?.name === "NotFoundError" || err?.name === "OverconstrainedError"
+ ? "call.no_audio_input_found"
+ : err?.name === "NotAllowedError"
+ ? "call.microphone_permission_denied"
+ : "call.web_audio_not_available";
+ ToastUtils.error(this.$t(errorKey));
+ this.config.telephone_web_audio_enabled = false;
+ await this.updateConfig({ telephone_web_audio_enabled: false });
this.stopWebAudio();
}
},
async requestAudioPermission() {
try {
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const hasAudioInput = devices.some((d) => d.kind === "audioinput");
+ if (devices.length > 0 && !hasAudioInput) {
+ ToastUtils.error(this.$t("call.no_audio_input_found"));
+ return false;
+ }
+
+ const constraints = this.selectedAudioInputId
+ ? { audio: { deviceId: { exact: this.selectedAudioInputId } } }
+ : { audio: true };
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((t) => t.stop());
await this.refreshAudioDevices();
+ return true;
} catch (e) {
console.error("Permission or device request failed", e);
+ const errorKey =
+ e?.name === "NotFoundError" || e?.name === "OverconstrainedError"
+ ? "call.no_audio_input_found"
+ : e?.name === "NotAllowedError"
+ ? "call.microphone_permission_denied"
+ : "call.web_audio_not_available";
+ ToastUtils.error(this.$t(errorKey));
+ return false;
}
},
async refreshAudioDevices() {
diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
index 1ea0145..51d2ec2 100644
--- a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
+++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
@@ -164,8 +164,8 @@
Recently Heard Announces
- Cards appear/disappear as announces are heard. Connected entries show a green
- pill; disconnected entries are dimmed with a red label.
+ Discovery runs continually; heard announces stay listed. Connected entries show
+ a green pill; disconnected entries are dimmed with a red label.
@@ -193,7 +193,7 @@
v-if="sortedDiscoveredInterfaces.length === 0"
class="text-sm text-gray-500 dark:text-gray-300"
>
- No discovered interfaces yet.
+ {{ discoveredEmptyMessage }}
{
+ const key = this.discoveryKey(iface);
+ const existing =
+ merged.get(key) || this.discoveredInterfaces.find((i) => this.discoveryKey(i) === key);
+ const lastHeard = iface.last_heard ?? existing?.last_heard ?? Math.floor(Date.now() / 1000);
+ merged.set(key, {
+ ...existing,
+ ...iface,
+ last_heard: lastHeard,
+ __isNew: isNew || existing?.__isNew,
+ });
+ };
+
+ this.discoveredInterfaces.forEach((iface) => addOrUpdate(iface, false));
+ incoming.forEach((iface) => addOrUpdate(iface, true));
+
+ this.discoveredInterfaces = Array.from(merged.values());
+ this.discoveredActive = active;
} catch (e) {
console.log(e);
}
},
+ discoveryKey(iface) {
+ return (
+ iface.discovery_hash ||
+ `${iface.reachable_on || iface.target_host || iface.remote || iface.listen_ip || iface.name || "unknown"}:${
+ iface.port || iface.target_port || iface.listen_port || ""
+ }`
+ );
+ },
formatLastHeard(ts) {
const seconds = Math.max(0, Math.floor(Date.now() / 1000 - ts));
if (seconds < 60) return `${seconds}s ago`;
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index f2bb947..993c636 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -35,7 +35,8 @@
:icon-background-colour="
selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.background_colour : ''
"
- icon-class="size-11"
+ icon-class="shrink-0"
+ :icon-style="messageIconStyle"
/>
@@ -956,7 +957,7 @@
{{
@@ -1482,6 +1483,15 @@ export default {
};
},
computed: {
+ messageIconStyle() {
+ const size = Number(this.config?.message_icon_size) || 28;
+ return {
+ width: `${size}px`,
+ height: `${size}px`,
+ minWidth: `${size}px`,
+ minHeight: `${size}px`,
+ };
+ },
isSyncingPropagationNode() {
return [
"path_requested",
diff --git a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue
index 7f12ac1..96f26de 100644
--- a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue
+++ b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue
@@ -290,7 +290,7 @@
-
+
@@ -366,7 +366,8 @@
:icon-background-colour="
conversation.lxmf_user_icon ? conversation.lxmf_user_icon.background_colour : ''
"
- icon-class="size-7"
+ icon-class="shrink-0"
+ :icon-style="messageIconStyle"
/>
@@ -559,7 +560,8 @@
:icon-name="peer.lxmf_user_icon?.icon_name"
:icon-foreground-colour="peer.lxmf_user_icon?.foreground_colour"
:icon-background-colour="peer.lxmf_user_icon?.background_colour"
- icon-class="size-7"
+ icon-class="shrink-0"
+ :icon-style="messageIconStyle"
/>
@@ -795,6 +797,10 @@ export default {
allSelected() {
return this.conversations.length > 0 && this.selectedHashes.size === this.conversations.length;
},
+ messageIconStyle() {
+ const size = GlobalState.config?.message_icon_size || 28;
+ return { width: `${size}px`, height: `${size}px` };
+ },
},
watch: {
foldersExpanded(newVal) {
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index a475e21..e3a0920 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -14,28 +14,20 @@
{{ $t("app.profile") }}
-
- {{ config.display_name }}
+
+
+
+
+
+ {{ $t("app.manage_identity") }}
+
-
{{ $t("app.manage_identity") }}
-
-
-
-
- {{ $t("app.identity_hash") }}
-
-
-
- {{ $t("app.lxmf_address") }}
-
@@ -280,6 +272,22 @@
+
+
+
+
+ {{ $t("maintenance.clear_lxmf_icons") }}
+
+
+ {{ $t("maintenance.clear_lxmf_icons_desc") }}
+
+
+
+
+
+
+
Icon Size
+
+ {{ config.message_icon_size || 28 }}px
+
+
+
+
+
+
+
+
+
-
{{ $t("app.live_preview") }}
+
+
+
+
+
+ Preview Name
+
+
+ Hey there, this is how text and icons will look.
+
+
- {{ $t("app.realtime") }}
+ {{ $t("app.live_preview") }}
@@ -1168,6 +1220,7 @@ import Toggle from "../forms/Toggle.vue";
import ShortcutRecorder from "./ShortcutRecorder.vue";
import KeyboardShortcuts from "../../js/KeyboardShortcuts";
import ElectronUtils from "../../js/ElectronUtils";
+import LxmfUserIcon from "../LxmfUserIcon.vue";
export default {
name: "SettingsPage",
@@ -1175,6 +1228,7 @@ export default {
MaterialDesignIcon,
Toggle,
ShortcutRecorder,
+ LxmfUserIcon,
},
data() {
return {
@@ -1193,10 +1247,13 @@ export default {
lxmf_local_propagation_node_enabled: null,
lxmf_preferred_propagation_node_destination_hash: null,
archives_max_storage_gb: 1,
+ backup_max_count: 5,
banished_effect_enabled: true,
banished_text: "BANISHED",
banished_color: "#dc2626",
blackhole_integration_enabled: true,
+ message_font_size: 14,
+ message_icon_size: 28,
telephone_tone_generator_enabled: true,
telephone_tone_generator_volume: 50,
gitea_base_url: "https://git.quad4.io",
@@ -1256,6 +1313,7 @@ export default {
"app.light_theme",
"app.dark_theme",
"Message Font Size",
+ "Icon Size",
"app.live_preview",
"app.realtime",
],
@@ -1337,6 +1395,20 @@ export default {
}
return this.config;
},
+ previewTextStyle() {
+ const size = this.config?.message_font_size || 14;
+ return { "font-size": `${size}px` };
+ },
+ messageIconPreviewStyle() {
+ const size = Number(this.config?.message_icon_size) || 28;
+ return {
+ width: `${size}px`,
+ height: `${size}px`,
+ minWidth: `${size}px`,
+ minHeight: `${size}px`,
+ transition: "width 120ms linear, height 120ms linear",
+ };
+ },
},
beforeUnmount() {
// stop listening for websocket messages
@@ -1452,6 +1524,28 @@ export default {
);
}, 1000);
},
+ async onDisplayNameChange() {
+ if (this.saveTimeouts.display_name) clearTimeout(this.saveTimeouts.display_name);
+ this.saveTimeouts.display_name = setTimeout(async () => {
+ await this.updateConfig(
+ {
+ display_name: this.config.display_name,
+ },
+ "display_name"
+ );
+ }, 600);
+ },
+ async onMessageIconSizeChange() {
+ if (this.saveTimeouts.message_icon_size) clearTimeout(this.saveTimeouts.message_icon_size);
+ this.saveTimeouts.message_icon_size = setTimeout(async () => {
+ await this.updateConfig(
+ {
+ message_icon_size: this.config.message_icon_size,
+ },
+ "message_icon_size"
+ );
+ }, 1000);
+ },
async onLanguageChange() {
await this.updateConfig(
{
@@ -1780,6 +1874,15 @@ export default {
ToastUtils.error(this.$t("common.error"));
}
},
+ async clearLxmfIcons() {
+ if (!(await DialogUtils.confirm(this.$t("maintenance.clear_confirm")))) return;
+ try {
+ await window.axios.delete("/api/v1/maintenance/lxmf-icons");
+ ToastUtils.success(this.$t("maintenance.lxmf_icons_cleared"));
+ } catch {
+ ToastUtils.error(this.$t("common.error"));
+ }
+ },
async clearArchives() {
if (!(await DialogUtils.confirm(this.$t("maintenance.clear_confirm")))) return;
try {
diff --git a/meshchatx/src/frontend/components/tools/BotsPage.vue b/meshchatx/src/frontend/components/tools/BotsPage.vue
index c1c30b8..a8cb759 100644
--- a/meshchatx/src/frontend/components/tools/BotsPage.vue
+++ b/meshchatx/src/frontend/components/tools/BotsPage.vue
@@ -83,7 +83,7 @@
{{ bot.name }}
- {{ runningMap[bot.id]?.address || "Not running" }}
+ {{ bot.address || runningMap[bot.id]?.address || "Not running" }}
{{ bot.template_id || bot.template }}
@@ -277,7 +277,7 @@ export default {
try {
await window.axios.post("/api/v1/bots/start", {
bot_id: bot.id,
- template_id: bot.template_id,
+ template_id: bot.template_id || bot.template,
name: bot.name,
});
ToastUtils.success(this.$t("bots.bot_started"));