From 8fac1134e288fdd300f8b2f1423ddd943bfb36a2 Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Sun, 4 Jan 2026 14:59:20 -0600 Subject: [PATCH] feat(call): improve call initiation and status handling with new properties for target hash and name; improve UI modals for tutorial and changelog visibility based on URL parameters --- meshchatx/src/frontend/components/App.vue | 23 +- .../frontend/components/ChangelogModal.vue | 58 +- .../frontend/components/CommandPalette.vue | 121 +- meshchatx/src/frontend/components/Toast.vue | 7 +- .../src/frontend/components/TutorialModal.vue | 1211 +++++++---------- .../components/archives/ArchivesPage.vue | 26 +- .../frontend/components/call/CallOverlay.vue | 62 +- .../src/frontend/components/call/CallPage.vue | 21 +- .../components/forwarder/ForwarderPage.vue | 241 ++-- .../interfaces/AddInterfacePage.vue | 7 +- .../components/interfaces/InterfacesPage.vue | 217 +-- .../src/frontend/components/map/MapPage.vue | 36 +- .../messages/ConversationViewer.vue | 142 +- .../micron-editor/MicronEditorPage.vue | 421 +++++- .../nomadnetwork/NomadNetworkPage.vue | 75 +- .../components/tools/PaperMessagePage.vue | 380 +++--- .../frontend/components/tools/RNPathPage.vue | 194 ++- .../components/tools/RNodeFlasherPage.vue | 1033 +++++++++++++- .../frontend/components/tools/ToolsPage.vue | 366 ++--- 19 files changed, 3195 insertions(+), 1446 deletions(-) diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index 935a7f4..1eb77ed 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -448,6 +448,8 @@ :was-declined="wasDeclined" :voicemail-status="voicemailStatus" :initiation-status="initiationStatus" + :initiation-target-hash="initiationTargetHash" + :initiation-target-name="initiationTargetName" @hangup="onOverlayHangup" @toggle-mic="onToggleMic" @toggle-speaker="onToggleSpeaker" @@ -564,6 +566,7 @@ export default { isFetchingRingtone: false, initiationStatus: null, initiationTargetHash: null, + initiationTargetName: null, isCallWindowOpen: false, }; }, @@ -732,6 +735,7 @@ export default { case "telephone_initiation_status": { this.initiationStatus = json.status; this.initiationTargetHash = json.target_hash; + this.initiationTargetName = json.target_name; break; } case "new_voicemail": { @@ -788,8 +792,22 @@ export default { const response = await window.axios.get(`/api/v1/app/info`); this.appInfo = response.data.app_info; - // check if we should show tutorial or changelog (only on first load) - if (!this.hasCheckedForModals) { + // check URL params for modal triggers + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has("show-guide")) { + this.$refs.tutorialModal.show(); + // remove param from URL + urlParams.delete("show-guide"); + const newUrl = window.location.pathname + (urlParams.toString() ? `?${urlParams.toString()}` : ""); + window.history.replaceState({}, "", newUrl); + } else if (urlParams.has("changelog")) { + this.$refs.changelogModal.show(); + // remove param from URL + urlParams.delete("changelog"); + const newUrl = window.location.pathname + (urlParams.toString() ? `?${urlParams.toString()}` : ""); + window.history.replaceState({}, "", newUrl); + } else if (!this.hasCheckedForModals) { + // check if we should show tutorial or changelog (only on first load) this.hasCheckedForModals = true; if (this.appInfo && !this.appInfo.tutorial_seen) { this.$refs.tutorialModal.show(); @@ -1020,6 +1038,7 @@ export default { this.voicemailStatus = response.data.voicemail; this.initiationStatus = response.data.initiation_status; this.initiationTargetHash = response.data.initiation_target_hash; + this.initiationTargetName = response.data.initiation_target_name; // Handle power management for calls if (ElectronUtils.isElectron()) { diff --git a/meshchatx/src/frontend/components/ChangelogModal.vue b/meshchatx/src/frontend/components/ChangelogModal.vue index 2d3ccf6..c2276f8 100644 --- a/meshchatx/src/frontend/components/ChangelogModal.vue +++ b/meshchatx/src/frontend/components/ChangelogModal.vue @@ -18,26 +18,21 @@ {{ $t("app.changelog_title", "What's New") }} - v{{ version }} - + - mdi-close - + @@ -50,15 +45,7 @@
{{ error }}
- - Retry - +
- + @@ -117,14 +98,11 @@ {{ $t("app.changelog_title", "What's New") }}
- v{{ version }} - + Full release history
@@ -137,15 +115,7 @@
{{ error }}
- - Retry - +
diff --git a/meshchatx/src/frontend/components/CommandPalette.vue b/meshchatx/src/frontend/components/CommandPalette.vue index 3ea8723..60bd508 100644 --- a/meshchatx/src/frontend/components/CommandPalette.vue +++ b/meshchatx/src/frontend/components/CommandPalette.vue @@ -114,6 +114,7 @@ import MaterialDesignIcon from "./MaterialDesignIcon.vue"; import LxmfUserIcon from "./LxmfUserIcon.vue"; import GlobalEmitter from "../js/GlobalEmitter"; +import ToastUtils from "../js/ToastUtils"; export default { name: "CommandPalette", @@ -174,6 +175,94 @@ export default { type: "navigation", route: { name: "settings" }, }, + { + id: "nav-ping", + title: "nav_ping", + description: "nav_ping_desc", + icon: "radar", + type: "navigation", + route: { name: "ping" }, + }, + { + id: "nav-rnprobe", + title: "nav_rnprobe", + description: "nav_rnprobe_desc", + icon: "radar", + type: "navigation", + route: { name: "rnprobe" }, + }, + { + id: "nav-rncp", + title: "nav_rncp", + description: "nav_rncp_desc", + icon: "swap-horizontal", + type: "navigation", + route: { name: "rncp" }, + }, + { + id: "nav-rnstatus", + title: "nav_rnstatus", + description: "nav_rnstatus_desc", + icon: "chart-line", + type: "navigation", + route: { name: "rnstatus" }, + }, + { + id: "nav-rnpath", + title: "nav_rnpath", + description: "nav_rnpath_desc", + icon: "route", + type: "navigation", + route: { name: "rnpath" }, + }, + { + id: "nav-translator", + title: "nav_translator", + description: "nav_translator_desc", + icon: "translate", + type: "navigation", + route: { name: "translator" }, + }, + { + id: "nav-forwarder", + title: "nav_forwarder", + description: "nav_forwarder_desc", + icon: "email-send-outline", + type: "navigation", + route: { name: "forwarder" }, + }, + { + id: "nav-documentation", + title: "nav_documentation", + description: "nav_documentation_desc", + icon: "book-open-variant", + type: "navigation", + route: { name: "documentation" }, + }, + { + id: "nav-micron-editor", + title: "nav_micron_editor", + description: "nav_micron_editor_desc", + icon: "code-tags", + type: "navigation", + route: { name: "micron-editor" }, + }, + { + id: "nav-rnode-flasher", + title: "nav_rnode_flasher", + description: "nav_rnode_flasher_desc", + icon: "flash", + type: "navigation", + route: { name: "rnode-flasher" }, + }, + { + id: "nav-debug-logs", + title: "nav_debug_logs", + description: "nav_debug_logs_desc", + icon: "console", + type: "navigation", + route: { name: "debug-logs" }, + }, { id: "action-sync", title: "action_sync", @@ -198,6 +287,22 @@ export default { type: "action", action: "toggle-orbit", }, + { + id: "action-getting-started", + title: "action_getting_started", + description: "action_getting_started_desc", + icon: "help-circle", + type: "action", + action: "show-tutorial", + }, + { + id: "action-changelog", + title: "action_changelog", + description: "action_changelog_desc", + icon: "history", + type: "action", + action: "show-changelog", + }, ], }; }, @@ -340,7 +445,7 @@ export default { } else if (result.type === "peer") { this.$router.push({ name: "messages", params: { destinationHash: result.peer.destination_hash } }); } else if (result.type === "contact") { - this.$router.push({ name: "call", query: { destination_hash: result.contact.remote_identity_hash } }); + this.dialContact(result.contact.remote_identity_hash); } else if (result.type === "action") { if (result.action === "sync") { GlobalEmitter.emit("sync-propagation-node"); @@ -352,9 +457,23 @@ export default { }); } else if (result.action === "toggle-orbit") { GlobalEmitter.emit("toggle-orbit"); + } else if (result.action === "show-tutorial") { + GlobalEmitter.emit("show-tutorial"); + } else if (result.action === "show-changelog") { + GlobalEmitter.emit("show-changelog"); } } }, + async dialContact(hash) { + try { + await window.axios.get(`/api/v1/telephone/call/${hash}`); + if (this.$route.name !== "call") { + this.$router.push({ name: "call" }); + } + } catch (e) { + ToastUtils.error(e.response?.data?.message || "Failed to initiate call"); + } + }, }, }; diff --git a/meshchatx/src/frontend/components/Toast.vue b/meshchatx/src/frontend/components/Toast.vue index 1cc77f9..2c19876 100644 --- a/meshchatx/src/frontend/components/Toast.vue +++ b/meshchatx/src/frontend/components/Toast.vue @@ -60,12 +60,13 @@ export default { }; }, mounted() { - GlobalEmitter.on("toast", (toast) => { + this.toastHandler = (toast) => { this.add(toast); - }); + }; + GlobalEmitter.on("toast", this.toastHandler); }, beforeUnmount() { - GlobalEmitter.off("toast"); + GlobalEmitter.off("toast", this.toastHandler); }, methods: { add(toast) { diff --git a/meshchatx/src/frontend/components/TutorialModal.vue b/meshchatx/src/frontend/components/TutorialModal.vue index e1702ac..6023cfe 100644 --- a/meshchatx/src/frontend/components/TutorialModal.vue +++ b/meshchatx/src/frontend/components/TutorialModal.vue @@ -9,7 +9,23 @@ persistent @update:model-value="onVisibleUpdate" > - + + +
+ + +
+

- Welcome to MeshChatX + {{ $t("tutorial.welcome") }} MeshChatX

- The future of off-grid communication. Secure, decentralized, and unstoppable. + {{ $t("tutorial.welcome_desc") }}

@@ -47,9 +63,11 @@ >
-
Security & Performance
-
- Massive improvements in speed, security, and crash recovery. +
+ {{ $t("tutorial.security") }} +
+
+ {{ $t("tutorial.security_desc") }}
@@ -58,9 +76,9 @@ >
-
Maps
-
- OpenLayers w/ MBTiles support and custom API endpoints. +
{{ $t("tutorial.maps") }}
+
+ {{ $t("tutorial.maps_desc") }}
@@ -69,9 +87,11 @@ >
-
Full LXST Voice
-
- Voicemail, ringtones, phonebook, and contact sharing. +
+ {{ $t("tutorial.voice") }} +
+
+ {{ $t("tutorial.voice_desc") }}
@@ -80,9 +100,11 @@ >
-
Advanced Tools
-
- Micron Editor, Paper Messages, RNS tools, Docs. +
+ {{ $t("tutorial.tools") }} +
+
+ {{ $t("tutorial.tools_desc") }}
@@ -91,9 +113,24 @@ >
-
Crawler & Archiver
-
- Automated network crawling and page archiving. +
+ {{ $t("tutorial.archiver") }} +
+
+ {{ $t("tutorial.archiver_desc") }} +
+
+
+
+ +
+
+ {{ $t("tutorial.banishment") }} +
+
+ {{ $t("tutorial.banishment_desc") }}
@@ -103,10 +140,10 @@
- Command Palette + Keybindings + {{ $t("tutorial.palette") }}
-
- Navigate everything instantly and customize shortcuts. +
+ {{ $t("tutorial.palette_desc") }}
@@ -115,256 +152,108 @@ >
-
i18n Support
-
Available in English, German, and Russian.
+
{{ $t("tutorial.i18n") }}
+
+ {{ $t("tutorial.i18n_desc") }} +
+
+ + {{ + $t("tutorial.more_features") + }} +
-

Connect to the Mesh

-

- To send messages, you need to connect to a Reticulum interface. +

+ {{ $t("tutorial.connect") }} +

+

+ {{ $t("tutorial.connect_desc") }}

-
+
-
+
- Suggested Public Relays + {{ + $t("tutorial.suggested_networks") + }}
-
+
- {{ + {{ iface.name }} - {{ iface.target_host }}:{{ iface.target_port }}
-
+
- Online + {{ $t("tutorial.online") }} - Use + {{ $t("tutorial.use") }} +
-
+
- -
OR
- - - - Manual Configuration -
- -
-
+

+ {{ $t("tutorial.custom_interfaces_desc") }} +

+
+ {{ $t("tutorial.open_interfaces") }} +
-

Learn & Create

+

+ {{ $t("tutorial.learn_create") }} +

- Discover how to use MeshChatX to its full potential. + {{ $t("tutorial.learn_create_desc") }}

@@ -374,27 +263,27 @@ >
-
Documentation
-
- Read the official MeshChatX and Reticulum documentation. +
+ {{ $t("tutorial.documentation") }} +
+
+ {{ $t("tutorial.documentation_desc") }}
@@ -404,20 +293,81 @@ >
-
Micron Editor
-
- Take a look at the Micron Editor for a guide on creating mesh-native pages. +
+ {{ $t("tutorial.micron_editor") }}
- Open Micron Editor + {{ $t("tutorial.micron_editor_desc") }} +
+ +
+
+ +
+
+ +
+
+ {{ $t("tutorial.paper_messages") }} +
+
+ {{ $t("tutorial.paper_messages_desc") }} +
+
+
+ +
+ +
+
+ {{ $t("tutorial.send_messages") }} +
+
+ {{ $t("tutorial.send_messages_desc") }} +
+
+
+ +
+ +
+
+ {{ $t("tutorial.explore_nodes") }} +
+
+ {{ $t("tutorial.explore_nodes_desc") }} +
+
+
+ +
+ +
+
+ {{ $t("tutorial.voice_calls") }} +
+
+ {{ $t("tutorial.voice_calls_desc") }} +
+
@@ -434,20 +384,19 @@
-

Ready to Roll!

+

+ {{ $t("tutorial.ready") }} +

- Everything is set up. You need to restart the application for the changes to take - effect. + {{ $t("tutorial.ready_desc") }}

- If you're running in Docker, make sure your container auto-restarts or start it - manually after it stops. + {{ $t("tutorial.docker_note") }}
@@ -456,51 +405,65 @@ - Back + {{ $t("tutorial.back") }} +
- Skip + {{ $t("tutorial.skip") }} + - Next + {{ $t("tutorial.next") }} + - Restart & Start Chatting + {{ $t("tutorial.restart_start") }} +
-
+
+ +
+ + +
+

- Welcome to MeshChatX + {{ $t("tutorial.welcome") }} MeshChatX

- The future of off-grid communication. Secure, decentralized, and unstoppable. + {{ $t("tutorial.welcome_desc") }}

@@ -543,11 +506,10 @@
- Security & Performance + {{ $t("tutorial.security") }}
-
- Massive improvements in speed, security, and integrity with built-in crash - recovery. +
+ {{ $t("tutorial.security_desc_page") }}
@@ -556,10 +518,11 @@ >
-
Maps
-
- OpenLayers support with offline MBTiles and custom API endpoints for online - maps. +
+ {{ $t("tutorial.maps") }} +
+
+ {{ $t("tutorial.maps_desc_page") }}
@@ -568,10 +531,11 @@ >
-
Full LXST Voice
-
- Crystal clear voice calls over mesh. Voicemail, custom ringtones, and phonebook - discovery. +
+ {{ $t("tutorial.voice") }} +
+
+ {{ $t("tutorial.voice_desc_page") }}
@@ -580,9 +544,11 @@ >
-
Advanced Tools
-
- Micron editor, paper messages, RNS tools, and integrated documentation. +
+ {{ $t("tutorial.tools") }} +
+
+ {{ $t("tutorial.tools_desc_page") }}
@@ -592,10 +558,23 @@
- Crawler & Archiver + {{ $t("tutorial.archiver") }}
-
- Automated network crawling and page archiving tools for offline browsing. +
+ {{ $t("tutorial.archiver_desc_page") }} +
+
+
+
+ +
+
+ {{ $t("tutorial.banishment") }} +
+
+ {{ $t("tutorial.banishment_desc") }}
@@ -605,11 +584,10 @@
- Command Palette + Keybindings + {{ $t("tutorial.palette") }}
-
- Navigate the entire application and customize your workflow with instant - shortcuts. +
+ {{ $t("tutorial.palette_desc_page") }}
@@ -618,310 +596,114 @@ >
-
i18n Support
-
- Full internationalization support for English, German, and Russian languages. +
+ {{ $t("tutorial.i18n") }} +
+
+ {{ $t("tutorial.i18n_desc_page") }}
+
+ + {{ + $t("tutorial.more_features") + }} +
-
-
-

Connect to the Mesh

-

- To send messages and make calls, you need to connect to a Reticulum interface. +

+
+

+ {{ $t("tutorial.connect") }} +

+

+ {{ $t("tutorial.connect_desc_page") }}

-
+
-
- - Suggested Public Relays +
+ + {{ + $t("tutorial.suggested_relays") + }}
-
+
- {{ iface.name }} - + {{ iface.name }} + + {{ iface.target_host }}:{{ iface.target_port }}
-
+
- Online + {{ $t("tutorial.online") }} - Use + {{ $t("tutorial.use") }} +
-
- +
+
-
-
-
-
-
-
- Custom Setup -
-
- -
-

- Already have a private relay or hardware RNode? Add it manually to connect to - your own mesh. -

- - Manual Configuration - -
-
- -
-
-

Custom Interface

- Back to suggested -
- -
- - - - - - - - - - - - - - - - - - Add & Connect - -

- Additional configuration like frequencies, encryption, and modes can be adjusted - in the full Interface settings later. +

+

+ {{ $t("tutorial.custom_interfaces") }} +

+

+ {{ $t("tutorial.custom_interfaces_desc_page") }}

+
-
-

Learn & Create

+

+ {{ $t("tutorial.learn_create") }} +

- Discover how to use MeshChatX to its full potential and create your own content. + {{ $t("tutorial.learn_create_desc_page") }}

@@ -932,32 +714,26 @@
- Documentation + {{ $t("tutorial.documentation") }}
-

- Comprehensive guides for MeshChatX and the underlying Reticulum Network Stack. +

+ {{ $t("tutorial.documentation_desc_page") }}

@@ -968,21 +744,80 @@
- Micron Editor + {{ $t("tutorial.micron_editor") }}
-

- Learn how to create mesh-native pages and interactive content using the Micron - markup language. +

+ {{ $t("tutorial.micron_editor_desc_page") }}

- Open Micron Editor + {{ $t("tutorial.open_micron_editor") }} + +
+
+
+ +
+
+ +
+
+ {{ $t("tutorial.paper_messages") }} +
+
+ {{ $t("tutorial.paper_messages_desc") }} +
+
+
+ +
+ +
+
+ {{ $t("tutorial.send_messages") }} +
+
+ {{ $t("tutorial.send_messages_desc") }} +
+
+
+ +
+ +
+
+ {{ $t("tutorial.explore_nodes") }} +
+
+ {{ $t("tutorial.explore_nodes_desc") }} +
+
+
+ +
+ +
+
+ {{ $t("tutorial.voice_calls") }} +
+
+ {{ $t("tutorial.voice_calls_desc") }} +
@@ -999,10 +834,11 @@
-

Ready to Roll!

+

+ {{ $t("tutorial.ready") }} +

- MeshChatX is now configured. You need to restart the application to finalize the - connection. + {{ $t("tutorial.ready_desc_page") }}

-
Restart Required
+
{{ $t("tutorial.restart_required") }}
- If you're running in Docker, ensure your container auto-restarts. Native apps will - relaunch automatically. + {{ $t("tutorial.restart_desc_page") }}
@@ -1022,45 +857,43 @@
- Back + {{ $t("tutorial.back") }} +
- Skip Setup + {{ $t("tutorial.skip_setup") }} + - Continue + {{ $t("tutorial.continue") }} + - Restart & Start Chatting + {{ $t("tutorial.restart_start") }} +
@@ -1074,9 +907,15 @@ import ToastUtils from "../js/ToastUtils"; import DialogUtils from "../js/DialogUtils"; import ElectronUtils from "../js/ElectronUtils"; import GlobalState from "../js/GlobalState"; +import LanguageSelector from "./LanguageSelector.vue"; +import MaterialDesignIcon from "./MaterialDesignIcon.vue"; export default { name: "TutorialModal", + components: { + LanguageSelector, + MaterialDesignIcon, + }, data() { return { visible: false, @@ -1085,32 +924,7 @@ export default { logoUrl, communityInterfaces: [], loadingInterfaces: false, - showCustomForm: false, - comports: [], - newInterface: { - name: "", - type: "TCPClientInterface", - target_host: "", - target_port: 4242, - listen_ip: "0.0.0.0", - listen_port: 4242, - device: "", - port: "", - speed: 115200, - }, - interfaceTypes: [ - { title: "TCP Client", value: "TCPClientInterface" }, - { title: "TCP Server", value: "TCPServerInterface" }, - { title: "UDP Interface", value: "UDPInterface" }, - { title: "RNode Interface", value: "RNodeInterface" }, - { title: "RNode Multi", value: "RNodeMultiInterface" }, - { title: "Serial Interface", value: "SerialInterface" }, - { title: "KISS Interface", value: "KISSInterface" }, - { title: "AX.25 KISS", value: "AX25KISSInterface" }, - { title: "I2P Interface", value: "I2PInterface" }, - { title: "Auto Interface", value: "AutoInterface" }, - { title: "Pipe Interface", value: "PipeInterface" }, - ], + interfaceAddedViaTutorial: false, }; }, computed: { @@ -1120,27 +934,43 @@ export default { isMobile() { return window.innerWidth < 640; }, + config() { + return GlobalState.config; + }, }, mounted() { if (this.isPage) { this.loadCommunityInterfaces(); - this.loadComports(); } }, methods: { + async toggleTheme() { + const newTheme = this.config.theme === "dark" ? "light" : "dark"; + try { + await window.axios.patch("/api/v1/config", { + theme: newTheme, + }); + GlobalState.config.theme = newTheme; + } catch (e) { + console.error("Failed to update theme:", e); + } + }, + async onLanguageChange(langCode) { + try { + await window.axios.patch("/api/v1/config", { + language: langCode, + }); + this.$i18n.locale = langCode; + GlobalState.config.language = langCode; + } catch (e) { + console.error("Failed to update language:", e); + } + }, async show() { this.visible = true; this.currentStep = 1; + this.interfaceAddedViaTutorial = false; await this.loadCommunityInterfaces(); - await this.loadComports(); - }, - async loadComports() { - try { - const response = await window.axios.get("/api/v1/comports"); - this.comports = response.data.comports; - } catch (e) { - console.error("Failed to load comports:", e); - } }, async loadCommunityInterfaces() { this.loadingInterfaces = true; @@ -1164,6 +994,8 @@ export default { }); ToastUtils.success(`Added interface: ${iface.name}`); + this.interfaceAddedViaTutorial = true; + // track change GlobalState.hasPendingInterfaceChanges = true; GlobalState.modifiedInterfaceNames.add(iface.name); @@ -1174,63 +1006,20 @@ export default { ToastUtils.error(e.response?.data?.message || "Failed to add interface"); } }, - showCustomInterfacePrompt() { - this.showCustomForm = true; - }, - async addCustomInterface() { - if (!this.newInterface.name) { - ToastUtils.error("Please enter an interface name"); - return; + gotoAddInterface() { + if (!this.isPage) { + this.visible = false; } - - try { - const payload = { - name: this.newInterface.name, - type: this.newInterface.type, - enabled: true, - }; - - if (this.newInterface.type === "TCPClientInterface") { - payload.target_host = this.newInterface.target_host; - payload.target_port = parseInt(this.newInterface.target_port); - } else if ( - this.newInterface.type === "TCPServerInterface" || - this.newInterface.type === "UDPInterface" - ) { - payload.listen_ip = this.newInterface.listen_ip; - payload.listen_port = parseInt(this.newInterface.listen_port); - if (this.newInterface.type === "UDPInterface") { - payload.forward_ip = "255.255.255.255"; - payload.forward_port = parseInt(this.newInterface.listen_port); - } - } else if ( - [ - "RNodeInterface", - "RNodeMultiInterface", - "SerialInterface", - "KISSInterface", - "AX25KISSInterface", - ].includes(this.newInterface.type) - ) { - payload.port = this.newInterface.port; - if (["SerialInterface", "KISSInterface", "AX25KISSInterface"].includes(this.newInterface.type)) { - payload.speed = parseInt(this.newInterface.speed); - } - } else if (this.newInterface.type === "PipeInterface") { - payload.command = this.newInterface.command; - } - - await window.axios.post("/api/v1/reticulum/interfaces/add", payload); - ToastUtils.success(`Added interface: ${this.newInterface.name}`); - - // track change - GlobalState.hasPendingInterfaceChanges = true; - GlobalState.modifiedInterfaceNames.add(this.newInterface.name); - - this.nextStep(); - } catch (e) { - console.error(e); - ToastUtils.error(e.response?.data?.message || "Failed to add interface"); + if (this.$router) { + this.$router.push({ path: "/interfaces/add" }); + } + }, + gotoRoute(routeName) { + if (!this.isPage) { + this.visible = false; + } + if (this.$router) { + this.$router.push({ name: routeName }); } }, nextStep() { @@ -1239,11 +1028,7 @@ export default { } }, async skipTutorial() { - if ( - await DialogUtils.confirm( - "Are you sure you want to skip the setup? You'll need to manually add interfaces later." - ) - ) { + if (await DialogUtils.confirm(this.$t("tutorial.skip_confirm"))) { await this.markSeen(); this.visible = false; } @@ -1260,7 +1045,9 @@ export default { if (ElectronUtils.isElectron()) { ElectronUtils.relaunch(); } else { - ToastUtils.info("Restart the application/container to apply changes."); + if (this.interfaceAddedViaTutorial) { + ToastUtils.info("Restart the application/container to apply changes."); + } this.visible = false; } }, diff --git a/meshchatx/src/frontend/components/archives/ArchivesPage.vue b/meshchatx/src/frontend/components/archives/ArchivesPage.vue index c97ed16..9c4d915 100644 --- a/meshchatx/src/frontend/components/archives/ArchivesPage.vue +++ b/meshchatx/src/frontend/components/archives/ArchivesPage.vue @@ -3,7 +3,7 @@
@@ -140,6 +140,23 @@
+ + + -
-
- - -
-
- {{ $t("forwarder.active_rules") }} -
-
- {{ $t("forwarder.no_rules") }} -
-
-
-
-
+
+ - {{ rule.is_active ? $t("forwarder.active") : $t("forwarder.disabled") }} -
- ID: {{ rule.id }} -
-
- {{ rule.name }} +
-
- - - {{ $t("forwarder.forwarding_to", { hash: rule.forward_to_hash }) }} - -
-
- - - {{ $t("forwarder.source_filter_display", { hash: rule.source_filter_hash }) }} - -
+ + +
+
+ +
-
+
-
+ + +
+
+ {{ $t("forwarder.active_rules") }} +
+
+ {{ $t("forwarder.no_rules") }} +
+
+
+
+
+ {{ rule.is_active ? $t("forwarder.active") : $t("forwarder.disabled") }} +
+ ID: {{ rule.id }} +
+
+ {{ rule.name }} +
+
+
+ + + {{ $t("forwarder.forwarding_to", { hash: rule.forward_to_hash }) }} + +
+
+ + + {{ $t("forwarder.source_filter_display", { hash: rule.source_filter_hash }) }} + +
+
+
+
+ + +
+
+
diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue index 46b5358..2b32841 100644 --- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue +++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue @@ -141,10 +141,7 @@ Need help? - Reticulum Docs: Configuring Interfaces @@ -982,7 +979,7 @@ This setting requires Transport Mode to be enabled. Reticulum Docs: Interface Modes diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue index 4e71e24..a0519cc 100644 --- a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue +++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue @@ -2,54 +2,57 @@
-
-
-
- -
-
{{ $t("interfaces.restart_required") }}
-
{{ $t("interfaces.restart_description") }}
-
-
- -
- -
-
-
-
- {{ $t("interfaces.manage") }} +
+ +
+
{{ $t("interfaces.restart_required") }}
+
{{ $t("interfaces.restart_description") }}
-
- {{ $t("interfaces.title") }} -
-
{{ $t("interfaces.description") }}
-
- - - {{ $t("interfaces.add_interface") }} - - - - + -->
+
+
+
+ +
+
+ + + +
+
+ +
-
-
- -
-
- - - -
-
- -
+ +
+ +
{{ $t("interfaces.no_interfaces_found") }}
+
{{ $t("interfaces.no_interfaces_description") }}
-
-
- -
{{ $t("interfaces.no_interfaces_found") }}
-
{{ $t("interfaces.no_interfaces_description") }}
-
- -
- +
+ +
diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue index ae39c2f..0062bc9 100644 --- a/meshchatx/src/frontend/components/map/MapPage.vue +++ b/meshchatx/src/frontend/components/map/MapPage.vue @@ -76,14 +76,16 @@
-
+
-
+
-
+
-
+
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue index b72ed04..7b8f3f5 100644 --- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue +++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue @@ -954,7 +954,7 @@
-
+
+ +
+
+
+ +
+
+
+ {{ suggestion.name }} +
+
+ {{ formatDestinationHash(suggestion.hash) }} +
+
+
+ Contact +
+
+
@@ -1092,6 +1138,8 @@ export default { isSendingMessage: false, autoScrollOnNewMessage: true, composeAddress: "", + isComposeInputFocused: false, + selectedComposeSuggestionIndex: -1, isShareContactModalOpen: false, contacts: [], @@ -1146,6 +1194,48 @@ export default { latestConversations() { return this.conversations.slice(0, 4); }, + composeSuggestions() { + if (!this.isComposeInputFocused) return []; + + const search = this.composeAddress.toLowerCase().trim(); + const suggestions = []; + const seenHashes = new Set(); + + // 1. Check contacts + this.contacts.forEach((c) => { + const hash = c.remote_identity_hash; + if (!seenHashes.has(hash)) { + if (!search || c.name.toLowerCase().includes(search) || hash.toLowerCase().includes(search)) { + suggestions.push({ + name: c.name, + hash: hash, + type: "contact", + icon: "account", + }); + seenHashes.add(hash); + } + } + }); + + // 2. Check recent conversations + this.conversations.forEach((c) => { + const hash = c.destination_hash; + if (!seenHashes.has(hash)) { + const name = c.custom_display_name ?? c.display_name; + if (!search || name.toLowerCase().includes(search) || hash.toLowerCase().includes(search)) { + suggestions.push({ + name: name, + hash: hash, + type: "recent", + icon: "history", + }); + seenHashes.add(hash); + } + } + }); + + return suggestions.slice(0, 10); + }, canSendMessage() { // can send if message text is present const messageText = this.newMessageText.trim(); @@ -1249,8 +1339,19 @@ export default { // check translator this.checkTranslator(); + + // fetch contacts for suggestions + this.fetchContacts(); }, methods: { + async fetchContacts() { + try { + const response = await window.axios.get("/api/v1/telephone/contacts"); + this.contacts = response.data; + } catch (e) { + console.log("Failed to fetch contacts:", e); + } + }, async checkTranslator() { if (!this.config?.translator_enabled) { this.hasTranslator = false; @@ -1575,8 +1676,47 @@ export default { await this.handleComposeAddress(destinationHash); }, onComposeEnterPressed() { + if ( + this.selectedComposeSuggestionIndex >= 0 && + this.selectedComposeSuggestionIndex < this.composeSuggestions.length + ) { + const suggestion = this.composeSuggestions[this.selectedComposeSuggestionIndex]; + this.selectComposeSuggestion(suggestion); + } else { + this.onComposeSubmit(); + } + }, + handleComposeInputUp() { + if (this.composeSuggestions.length > 0) { + if (this.selectedComposeSuggestionIndex > 0) { + this.selectedComposeSuggestionIndex--; + } else { + this.selectedComposeSuggestionIndex = this.composeSuggestions.length - 1; + } + } + }, + handleComposeInputDown() { + if (this.composeSuggestions.length > 0) { + if (this.selectedComposeSuggestionIndex < this.composeSuggestions.length - 1) { + this.selectedComposeSuggestionIndex++; + } else { + this.selectedComposeSuggestionIndex = 0; + } + } + }, + selectComposeSuggestion(suggestion) { + this.composeAddress = suggestion.hash; + this.isComposeInputFocused = false; + this.selectedComposeSuggestionIndex = -1; this.onComposeSubmit(); }, + onComposeInputBlur() { + // Delay blur to allow mousedown on suggestions + setTimeout(() => { + this.isComposeInputFocused = false; + this.selectedComposeSuggestionIndex = -1; + }, 200); + }, async handleComposeAddress(destinationHash) { if (destinationHash.startsWith("lxmf@")) { destinationHash = destinationHash.replace("lxmf@", ""); diff --git a/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue b/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue index 4f6f44d..3d6464e 100644 --- a/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue +++ b/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue @@ -8,11 +8,21 @@
-

- {{ $t("tools.micron_editor.title") }} -

+
+

+ {{ $t("tools.micron_editor.title") }} +

+ +
-
- - - -
-
-

- - Ingest Paper Message -

-
-
-

- Paste an LXMF URI to decode and add it to your conversations. -

-
- +
+
+ + +
+
+ + +
+
+ + +
- -
-
-
+ - -
-
-
-

Generated QR Code

-
-
-
-
- -
+ +
+
+

+ + Ingest Paper Message +

- -
-
-
+
+
+ + +
+
+
+

Generated QR Code

+
+
+
+
+
-
- - + class="flex-1 font-mono text-[10px] break-all text-gray-600 dark:text-zinc-300 bg-white dark:bg-zinc-900 p-2 rounded-lg border border-gray-200 dark:border-zinc-700 max-h-20 overflow-y-auto" + > + {{ generatedUri }} +
+ +
+
+ +
+ + +
-
- + -
-
- +
+
+ +
+

No QR Code Generated

+

+ Fill out the message details and click generate to create a signed paper message. +

-

No QR Code Generated

-

- Fill out the message details and click generate to create a signed paper message. -

diff --git a/meshchatx/src/frontend/components/tools/RNPathPage.vue b/meshchatx/src/frontend/components/tools/RNPathPage.vue index 588a699..423b6ba 100644 --- a/meshchatx/src/frontend/components/tools/RNPathPage.vue +++ b/meshchatx/src/frontend/components/tools/RNPathPage.vue @@ -45,17 +45,75 @@
+ +
+
+ + +
+ +
+ Hops: + +
+
+
+ Total + {{ totalItems }} +
+
+ Responsive + {{ + responsiveItems + }} +
+
+ Unresponsive + {{ + unresponsiveItems + }} +
+
+
+
- No paths currently known. + No paths found matching your criteria.
-
+
{{ path.hash }} @@ -64,11 +122,28 @@ > {{ path.hops }} {{ path.hops === 1 ? "hop" : "hops" }} + + {{ getStateText(path.state) }} +
via {{ path.via }} on {{ path.interface }}
-
Expires: {{ formatDate(path.expires) }}
+
+
+ Last Updated: + {{ path.timestamp ? formatDate(path.timestamp) : "Unknown" }} +
+
+ Expires: {{ formatDate(path.expires) }} +
+
+ Announce Hash: {{ path.announce_hash }} +
+
+ + +
+
+ + Page {{ currentPage }} of {{ totalPages }} + +
+
+ Show: + +
+
@@ -208,8 +316,40 @@ export default { rateTable: [], requestHash: "", dropViaHash: "", + // Pagination & Filtering + searchQuery: "", + filterInterface: "", + filterHops: null, + currentPage: 1, + itemsPerPage: 50, + totalItems: 0, + responsiveItems: 0, + unresponsiveItems: 0, + interfaces: [], }; }, + computed: { + totalPages() { + return Math.ceil(this.totalItems / this.itemsPerPage); + }, + }, + watch: { + searchQuery() { + this.currentPage = 1; + this.refreshTable(); + }, + filterInterface() { + this.currentPage = 1; + this.refreshTable(); + }, + filterHops() { + this.currentPage = 1; + this.refreshTable(); + }, + currentPage() { + this.refreshTable(); + }, + }, mounted() { this.refreshAll(); }, @@ -217,12 +357,17 @@ export default { async refreshAll() { this.isLoading = true; try { - const [pathRes, rateRes] = await Promise.all([ - window.axios.get("/api/v1/rnpath/table"), + const [pathRes, rateRes, ifaceRes] = await Promise.all([ + this.fetchPathTable(), window.axios.get("/api/v1/rnpath/rates"), + window.axios.get("/api/v1/reticulum/interfaces"), ]); - this.pathTable = pathRes.data.table; + this.pathTable = pathRes.table; + this.totalItems = pathRes.total; + this.responsiveItems = pathRes.responsive; + this.unresponsiveItems = pathRes.unresponsive; this.rateTable = rateRes.data.rates; + this.interfaces = Object.keys(ifaceRes.data.interfaces); } catch (e) { console.error(e); ToastUtils.error("Failed to fetch path data"); @@ -230,6 +375,41 @@ export default { this.isLoading = false; } }, + async refreshTable() { + this.isLoading = true; + try { + const res = await this.fetchPathTable(); + this.pathTable = res.table; + this.totalItems = res.total; + this.responsiveItems = res.responsive; + this.unresponsiveItems = res.unresponsive; + } catch (e) { + console.error(e); + } finally { + this.isLoading = false; + } + }, + async fetchPathTable() { + const params = { + page: this.currentPage, + limit: this.itemsPerPage, + search: this.searchQuery || undefined, + interface: this.filterInterface || undefined, + hops: this.filterHops !== null ? this.filterHops : undefined, + }; + const res = await window.axios.get("/api/v1/rnpath/table", { params }); + return res.data; + }, + getStateColor(state) { + if (state === 2) return "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300"; + if (state === 1) return "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300"; + return "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400"; + }, + getStateText(state) { + if (state === 2) return "RESPONSIVE"; + if (state === 1) return "UNRESPONSIVE"; + return "UNKNOWN"; + }, async dropPath(hash) { if (!(await DialogUtils.confirm(`Are you sure you want to drop the path to ${hash}?`))) { return; diff --git a/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue b/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue index e06bc66..eaf4eb1 100644 --- a/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue +++ b/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue @@ -10,14 +10,21 @@
+ - + +
+
+ + +
+
+
+ +
+

+ 2. {{ $t("tools.rnode_flasher.select_firmware") }} +

+
+ +
+ +
+
+ {{ $t("tools.rnode_flasher.download_recommended") }} +
+
+ {{ recommendedFirmwareFilename }} +
+ +
+ + +
+ + +
+ +
+ {{ flashError }} +
+ + + +
+ +
{{ flashingStatus }}
+
+
+
+
+
+ + +
+ +
+
+
+

+ 3. {{ $t("tools.rnode_flasher.step_provision") }} +

+ +
+

+ {{ $t("tools.rnode_flasher.provision_description") }} +

+ +
+ + {{ $t("tools.rnode_flasher.provisioning_wait") }} +
+
+ +
+
+

+ 4. {{ $t("tools.rnode_flasher.step_set_hash") }} +

+ +
+

+ {{ $t("tools.rnode_flasher.set_hash_description") }} +

+ +
+ + {{ $t("tools.rnode_flasher.setting_hash_wait") }} +
+
+
+ + +
+
+ {{ $t("tools.rnode_flasher.advanced_tools") }} +
+
+ + + + + +
+ +
+ +
+
+
+ + +
+ +
+
+ +

+ {{ $t("tools.rnode_flasher.configure_bluetooth") }} +

+
+
+
+ + + +
+
+ {{ $t("tools.rnode_flasher.bluetooth_restart_warning") }} +
+
+
+ + +
+
+ +

+ {{ $t("tools.rnode_flasher.configure_tnc") }} +

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+ + {{ $t("tools.rnode_flasher.find_device_issue") }} +
+ +
+ + diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue index 389ceae..e68e4d5 100644 --- a/meshchatx/src/frontend/components/tools/ToolsPage.vue +++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue @@ -2,193 +2,199 @@
-
-
-
- {{ $t("tools.utilities") }} +
+
+
+
+ {{ $t("tools.utilities") }} +
+
+ {{ $t("tools.power_tools") }} +
+
+ {{ $t("tools.diagnostics_description") }} +
-
{{ $t("tools.power_tools") }}
-
- {{ $t("tools.diagnostics_description") }} -
-
-
- -
- -
-
-
{{ $t("tools.ping.title") }}
-
- {{ $t("tools.ping.description") }} +
+ +
+
-
- - - - -
- -
-
-
{{ $t("tools.rnprobe.title") }}
-
- {{ $t("tools.rnprobe.description") }} +
+
{{ $t("tools.ping.title") }}
+
+ {{ $t("tools.ping.description") }} +
-
- - - - -
- -
-
-
{{ $t("tools.rncp.title") }}
-
- {{ $t("tools.rncp.description") }} -
-
- -
- - -
- -
-
-
{{ $t("tools.rnstatus.title") }}
-
- {{ $t("tools.rnstatus.description") }} -
-
- -
- - -
- -
-
-
{{ $t("tools.rnpath.title") }}
-
- {{ $t("tools.rnpath.description") }} -
-
- -
- - -
- -
-
-
{{ $t("tools.translator.title") }}
-
- {{ $t("tools.translator.description") }} -
-
- -
- - -
- -
-
-
{{ $t("tools.forwarder.title") }}
-
- {{ $t("tools.forwarder.description") }} -
-
- -
- - -
- -
-
-
{{ $t("docs.title") }}
-
- {{ $t("docs.subtitle") }} -
-
- -
- - -
- -
-
-
{{ $t("tools.micron_editor.title") }}
-
- {{ $t("tools.micron_editor.description") }} -
-
- -
- - -
- -
-
-
{{ $t("tools.paper_message.title") }}
-
- {{ $t("tools.paper_message.description") }} -
-
- -
- - -
- RNode -
-
-
{{ $t("tools.rnode_flasher.title") }}
-
- {{ $t("tools.rnode_flasher.description") }} -
-
-
- - - -
-
+ - -
- -
-
-
Debug Logs
-
- View and export internal system logs for troubleshooting. + +
+
-
- - +
+
{{ $t("tools.rnprobe.title") }}
+
+ {{ $t("tools.rnprobe.description") }} +
+
+ + + + +
+ +
+
+
{{ $t("tools.rncp.title") }}
+
+ {{ $t("tools.rncp.description") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("tools.rnstatus.title") }}
+
+ {{ $t("tools.rnstatus.description") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("tools.rnpath.title") }}
+
+ {{ $t("tools.rnpath.description") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("tools.translator.title") }}
+
+ {{ $t("tools.translator.description") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("tools.forwarder.title") }}
+
+ {{ $t("tools.forwarder.description") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("docs.title") }}
+
+ {{ $t("docs.subtitle") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("tools.micron_editor.title") }}
+
+ {{ $t("tools.micron_editor.description") }} +
+
+ +
+ + +
+ +
+
+
{{ $t("tools.paper_message.title") }}
+
+ {{ $t("tools.paper_message.description") }} +
+
+ +
+ + +
+ RNode +
+
+
{{ $t("tools.rnode_flasher.title") }}
+
+ {{ $t("tools.rnode_flasher.description") }} +
+
+
+ + + + +
+
+ + +
+ +
+
+
Debug Logs
+
+ View and export internal system logs for troubleshooting. +
+
+ +
+