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 @@
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") }}
+
+
-
- Custom Interface
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Interface
-
-
- More advanced options are available in the full Interface settings after setup.
-
-
+ {{ $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") }}
-
MeshChatX Docs
-
+ Reticulum Docs
+ {{ $t("tutorial.reticulum_docs") }}
+
@@ -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.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") }}
+
+
@@ -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")
+ }}
-
+
+
+
+
+
+
+
+ {{ $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 @@
+
+
+
+
+
+
+
@@ -25,11 +25,13 @@
? $t("call.call_declined")
: isEnded
? $t("call.call_ended")
- : activeCall.is_voicemail
+ : activeCall && activeCall.is_voicemail
? $t("call.recording_voicemail")
- : activeCall.status === 6
+ : activeCall && activeCall.status === 6
? $t("call.active_call")
- : $t("call.call_status")
+ : initiationStatus
+ ? $t("call.initiation")
+ : $t("call.call_status")
}}
- {{ activeCall.remote_identity_name || $t("call.unknown") }}
+ {{
+ (activeCall ? activeCall.remote_identity_name : initiationTargetName) || $t("call.unknown")
+ }}
In contacts
{{
- activeCall.remote_identity_hash
- ? formatDestinationHash(activeCall.remote_identity_hash)
+ (activeCall ? activeCall.remote_identity_hash : initiationTargetHash)
+ ? formatDestinationHash(
+ activeCall ? activeCall.remote_identity_hash : initiationTargetHash
+ )
: ""
}}
@@ -242,18 +252,24 @@
>
- {{ activeCall.remote_identity_name || $t("call.unknown") }}
+ {{
+ (activeCall ? activeCall.remote_identity_name : initiationTargetName) || $t("call.unknown")
+ }}
{{ elapsedTime }}
@@ -297,7 +313,7 @@ export default {
props: {
activeCall: {
type: Object,
- required: true,
+ default: null,
},
isEnded: {
type: Boolean,
@@ -315,6 +331,14 @@ export default {
type: String,
default: null,
},
+ initiationTargetHash: {
+ type: String,
+ default: null,
+ },
+ initiationTargetName: {
+ type: String,
+ default: null,
+ },
},
emits: ["hangup", "toggle-mic", "toggle-speaker"],
data() {
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index eb10c63..acfa7bb 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -141,16 +141,24 @@
- {{ (activeCall || lastCall)?.remote_identity_name || $t("call.unknown") }}
+ {{
+ (activeCall || lastCall)?.remote_identity_name ||
+ initiationTargetName ||
+ $t("call.unknown")
+ }}
- {{ formatDestinationHash((activeCall || lastCall).remote_identity_hash) }}
+ {{
+ formatDestinationHash(
+ (activeCall || lastCall)?.remote_identity_hash || initiationTargetHash
+ )
+ }}
@@ -2001,6 +2009,7 @@ export default {
localSpeakerMuted: false,
initiationStatus: null,
initiationTargetHash: null,
+ initiationTargetName: null,
};
},
computed: {
@@ -2202,6 +2211,7 @@ export default {
this.activeCall = newCall;
this.initiationStatus = response.data.initiation_status;
this.initiationTargetHash = response.data.initiation_target_hash;
+ this.initiationTargetName = response.data.initiation_target_name;
if (this.activeCall?.is_voicemail) {
this.wasVoicemail = true;
@@ -2862,6 +2872,9 @@ export default {
// Provide immediate feedback
this.destinationHash = hashToCall;
+ const targetContact = this.contacts.find((c) => c.remote_identity_hash === hashToCall);
+ this.initiationTargetHash = hashToCall;
+ this.initiationTargetName = targetContact ? targetContact.name : null;
this.activeTab = "phone";
this.initiationStatus = "Initiating...";
this.isCallEnded = false;
diff --git a/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue b/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue
index d3c5d95..01f8b19 100644
--- a/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue
+++ b/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue
@@ -2,131 +2,148 @@
-
-
-
- {{ $t("tools.utilities") }}
+
+
+
+
+ {{ $t("tools.utilities") }}
+
+
{{ $t("forwarder.title") }}
+
+ {{ $t("forwarder.description") }}
+
-
{{ $t("forwarder.title") }}
-
- {{ $t("forwarder.description") }}
-
-
-
-
-
{{ $t("forwarder.add_rule") }}
-
-
-
{{
- $t("forwarder.name")
- }}
-
+
+
+
+ {{ $t("forwarder.add_rule") }}
-
- {{
- $t("forwarder.forward_to_hash")
- }}
-
-
-
- {{
- $t("forwarder.source_filter")
- }}
-
-
-
-
-
-
- {{ $t("forwarder.add_button") }}
-
-
-
-
-
-
-
- {{ $t("forwarder.active_rules") }}
-
-
- {{ $t("forwarder.no_rules") }}
-
-
-
-
-
+
+ {{ $t("forwarder.name") }}
- {{ 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.forward_to_hash") }}
+
+
+
+ {{ $t("forwarder.source_filter") }}
+
-
+
-
-
-
-
+
+ {{ $t("forwarder.add_button") }}
+
+
+
+
+ {{ $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.restart_now") }}
-
-
-
-
-
-
-
- {{ $t("interfaces.manage") }}
+
+
+
+
{{ $t("interfaces.restart_required") }}
+
{{ $t("interfaces.restart_description") }}
-
- {{ $t("interfaces.title") }}
-
-
{{ $t("interfaces.description") }}
-
-
-
- {{ $t("interfaces.add_interface") }}
-
-
-
- {{ $t("interfaces.import") }}
-
-
-
- {{ $t("interfaces.export_all") }}
-
-
+ -->
+
+
+
+
+
+
+
+ {{ $t("interfaces.all") }}
+
+
+ {{ $t("app.enabled") }}
+
+
+ {{ $t("app.disabled") }}
+
+
+
+
+ {{ $t("interfaces.all_types") }}
+
+ {{ type }}
+
+
+
-
-
-
-
-
-
- {{ $t("interfaces.all") }}
-
-
- {{ $t("app.enabled") }}
-
-
- {{ $t("app.disabled") }}
-
-
-
-
- {{ $t("interfaces.all_types") }}
- {{ type }}
-
-
+
+
+
+
{{ $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 @@
-
-
+
+
+
+
+
+
+
+
+
+ {{ 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") }}
-
+
>Recommendations and Requirements
+
+While micron can output formatted text to even the most basic terminal, there's a few capabilities your terminal ${b}*must${b}* support to display micron output correctly, and some that, while not strictly necessary, make the experience a lot better.
+
+Formatting such as ${b}_underline${b}_, ${b}!bold${b}! or ${b}*italics${b}* will be displayed if your terminal supports it.
+
+If you are having trouble getting micron output to display correctly, try using ${b}*gnome-terminal${b}* or ${b}*alacritty${b}*, which should work with all formatting options out of the box. Most other terminals will work fine as well, but you might have to change some settings to get certain formatting to display correctly.
+
+>>>Encoding
+
+All micron sources are intepreted as UTF-8, and micron assumes it can output UTF-8 characters to the terminal. If your terminal does not support UTF-8, output will be faulty.
+
+>>>Colors
+
+Shading and coloring text and backgrounds is integral to micron output, and while micron will attempt to gracefully degrade output even to 1-bit terminals, you will get the best output with terminals supporting at least 256 colors. True-color support is recommended.
+
+>>>Terminal Font
+
+While any unicode capable font can be used with micron, it's highly recommended to use a ${b}*"Nerd Font"${b}* (see https://www.nerdfonts.com/), which will add a lot of extra glyphs and icons to your output.
+
+> A Few Demo Outputs
+
+${b}F222${b}Bddd
+
+${b}cWith micron, you can control layout and presentation
+${b}a
+
+${b}${b}
+
+${b}B33f
+
+You can change background ...
+
+${b}${b}
+
+${b}B393
+
+${b}r${b}F320... and foreground colors${b}f
+${b}a
+
+${b}b
+
+If you want to make a break, horizontal dividers can be inserted. They can be plain, like the one below this text, or you can style them with unicode characters and glyphs, like the wavy divider in the beginning of this document.
+
+-
+
+${b}cText can be ${b}_underlined${b}_, ${b}!bold${b}! or ${b}*italic${b}*.
+
+You can also ${b}_${b}*${b}!B5d5${b}F222combine${b}f${b}b${b}_ ${b}_${b}Ff00f${b}Ff80o${b}Ffd0r${b}F9f0m${b}F0f2a${b}F0fdt${b}F07ft${b}F43fi${b}F70fn${b}Fe0fg${b}${b} for some fabulous effects.
+${b}a
+
+
+>>>Sections and Headings
+
+You can define an arbitrary number of sections and sub sections, each with their own named headings. Text inside sections will be automatically indented.
+
+-
+
+If you place a divider inside a section, it will adhere to the section indents.
+
+>>>>>
+If no heading text is defined, the section will appear as a sub-section without a header. This can be useful for creating indented blocks of text, like this one.
+
+>Micron tags
+
+Tags are used to format text with micron. Some tags can appear anywhere in text, and some must appear at the beginning of a line. If you need to write text that contains a sequence that would be interpreted as a tag, you can escape it with the character \\.
+
+In the following sections, the different tags will be introduced. Any styling set within micron can be reset to the default style by using the special \\${b}\\${b} tag anywhere in the markup, which will immediately remove any formatting previously specified.
+
+>>Alignment
+
+To control text alignment use the tag \\${b}c to center text, \\${b}l to left-align, \\${b}r to right-align, and \\${b}a to return to the default alignment of the document. Alignment tags must appear at the beginning of a line. Here is an example:
+
+${b}Faaa
+${b}=
+${b}cThis line will be centered.
+So will this.
+${b}aThe alignment has now been returned to default.
+${b}rThis will be aligned to the right
+${b}${b}
+${b}=
+${b}${b}
+
+The above markup produces the following output:
+
+${b}Faaa${b}B333
+
+${b}cThis line will be centered.
+So will this.
+
+${b}aThe alignment has now been returned to default.
+
+${b}rThis will be aligned to the right
+
+${b}${b}
+
+
+>>Formatting
+
+Text can be formatted as ${b}!bold${b}! by using the \\${b}! tag, ${b}_underline${b}_ by using the \\${b}_ tag and ${b}*italic${b}* by using the \\${b}* tag.
+
+Here's an example of formatting text:
+
+${b}Faaa
+${b}=
+We shall soon see ${b}!bold${b}! paragraphs of text decorated with ${b}_underlines${b}_ and ${b}*italics${b}*. Some even dare ${b}!${b}*${b}_combine${b}${b} them!
+${b}=
+${b}${b}
+
+The above markup produces the following output:
+
+${b}Faaa${b}B333
+
+We shall soon see ${b}!bold${b}! paragraphs of text decorated with ${b}_underlines${b}_ and ${b}*italics${b}*. Some even dare ${b}!${b}*${b}_combine${b}!${b}*${b}_ them!
+
+${b}${b}
+
+
+>>Sections
+
+To create sections and subsections, use the > tag. This tag must be placed at the beginning of a line. To specify a sub-section of any level, use any number of > tags. If text is placed after a > tag, it will be used as a heading.
+
+Here is an example of sections:
+
+${b}Faaa
+${b}=
+>High Level Stuff
+This is a section. It contains this text.
+
+>>Another Level
+This is a sub section.
+
+>>>Going deeper
+A sub sub section. We could continue, but you get the point.
+
+>>>>
+Wait! It's worth noting that we can also create sections without headings. They look like this.
+${b}=
+${b}${b}
+
+The above markup produces the following output:
+
+${b}Faaa${b}B333
+>High Level Stuff
+This is a section. It contains this text.
+
+>>Another Level
+This is a sub section.
+
+>>>Going deeper
+A sub sub section. We could continue, but you get the point.
+
+>>>>
+Wait! It's worth noting that we can also create sections without headings. They look like this.
+${b}${b}
+
+
+>Colors
+
+Foreground colors can be specified with the \\${b}F tag, followed by three hexadecimal characters. To return to the default foreground color, use the \\${b}f tag. Background color is specified in the same way, but by using the \\${b}B and \\${b}b tags.
+
+Here's a few examples:
+
+${b}Faaa
+${b}=
+You can use ${b}B5d5${b}F222 color ${b}f${b}b ${b}Ff00f${b}Ff80o${b}Ffd0r${b}F9f0m${b}F0f2a${b}F0fdt${b}F07ft${b}F43fi${b}F70fn${b}Fe0fg${b}f for some fabulous effects.
+${b}=
+${b}${b}
+
+The above markup produces the following output:
+
+${b}Faaa${b}B333
+
+You can use ${b}B5d5${b}F222 color ${b}f${b}B333 ${b}Ff00f${b}Ff80o${b}Ffd0r${b}F9f0m${b}F0f2a${b}F0fdt${b}F07ft${b}F43fi${b}F70fn${b}Fe0fg${b}f for some fabulous effects.
+
+${b}${b}
+
+
+>Links
+
+Links to pages, files or other resources can be created with the \\${b}[ tag, which should always be terminated with a closing ]. You can create links with and without labels, it is up to you to control the formatting of links with other tags. Although not strictly necessary, it is good practice to at least format links with underlining.
+
+Here's a few examples:
+
+${b}Faaa
+${b}=
+Here is a link without any label: ${b}[72914442a3689add83a09a767963f57c:/page/index.mu]
+
+This is a ${b}[labeled link${b}72914442a3689add83a09a767963f57c:/page/index.mu] to the same page, but it's hard to see if you don't know it
+
+Here is ${b}F00a${b}_${b}[a more visible link${b}72914442a3689add83a09a767963f57c:/page/index.mu]${b}_${b}f
+${b}=
+${b}${b}
+
+The above markup produces the following output:
+
+${b}Faaa${b}B333
+
+Here is a link without any label: ${b}[72914442a3689add83a09a767963f57c:/page/index.mu]
+
+This is a ${b}[labeled link${b}72914442a3689add83a09a767963f57c:/page/index.mu] to the same page, but it's hard to see if you don't know it
+
+Here is ${b}F00f${b}_${b}[a more visible link${b}72914442a3689add83a09a767963f57c:/page/index.mu]${b}_${b}f
+
+${b}${b}
+
+When links like these are displayed in the built-in browser, clicking on them or activating them using the keyboard will cause the browser to load the specified URL.
+
+>Fields & Requests
+
+Nomad Network let's you use simple input fields for submitting data to node-side applications. Submitted data, along with other session variables will be available to the node-side script / program as environment variables.
+
+>>Request Links
+
+Links can contain request variables and a list of fields to submit to the node-side application. You can include all fields on the page, only specific ones, and any number of request variables. To simply submit all fields on a page to a specified node-side page, create a link like this:
+
+${b}Faaa
+${b}=
+${b}[Submit Fields${b}:/page/fields.mu${b}*]
+${b}=
+${b}${b}
+
+Note the ${b}!*${b}! following the extra ${b}!\\${b}${b}! at the end of the path. This ${b}!*${b}! denotes ${b}*all fields${b}*. You can also specify a list of fields to include:
+
+${b}Faaa
+${b}=
+${b}[Submit Fields${b}:/page/fields.mu${b}username|auth_token]
+${b}=
+${b}${b}
+
+If you want to include pre-set variables, you can do it like this:
+
+${b}Faaa
+${b}=
+${b}[Query the System${b}:/page/fields.mu${b}username|auth_token|action=view|amount=64]
+${b}=
+${b}${b}
+
+>> Fields
+
+Here's an example of creating a field. We'll create a field named ${b}!user_input${b}! and fill it with the text ${b}!Pre-defined data${b}!. Note that we are using background color tags to make the field more visible to the user:
+
+${b}Faaa
+${b}=
+A simple input field: ${b}B444${b}${b}b
+${b}=
+${b}${b}
+
+You must always set a field ${b}*name${b}*, but you can of course omit the pre-defined value of the field:
+
+${b}Faaa
+${b}=
+An empty input field: ${b}B444${b}${b}b
+${b}=
+${b}${b}
+
+You can set the size of the field like this:
+
+${b}Faaa
+${b}=
+A sized input field: ${b}B444${b}<16|with_size${b}>${b}b
+${b}=
+${b}${b}
+
+It is possible to mask fields, for example for use with passwords and similar:
+
+${b}Faaa
+${b}=
+A masked input field: ${b}B444${b}${b}b
+${b}=
+${b}${b}
+
+And you can of course control all parameters at the same time:
+
+${b}Faaa
+${b}=
+Full control: ${b}B444${b}${b}b
+${b}=
+${b}${b}
+
+Collecting the above markup produces the following output:
+
+${b}Faaa${b}B333
+
+A simple input field: ${b}B444${b}${b}B333
+
+An empty input field: ${b}B444${b}${b}B333
+
+A sized input field: ${b}B444${b}<16|with_size${b}>${b}B333
+
+A masked input field: ${b}B444${b}${b}B333
+
+Full control: ${b}B444${b}${b}B333
+${b}b
+>>> Checkboxes
+
+In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options.
+
+${b}Faaa
+${b}=
+${b}|field_name|value${b}>${b}b Label Text${b}
+${b}=
+When the checkbox is checked, it's field will be set to the provided value. If there are multiple checkboxes that share the same field name, the checked values will be concatenated when they are sent to the node by a comma.
+${b}${b}
+
+${b}B444${b}|sign_up|1${b}>${b}b Sign me up${b}
+
+You can also pre-check both checkboxes and radio groups by appending a |* after the field value.
+
+${b}B444${b}|checkbox|1|*${b}>${b}b Pre-checked checkbox${b}
+
+>>> Radio groups
+
+Radio groups are another input that lets the user chose from a set of options. Unlike checkboxes, radio buttons with the same field name are mutually exclusive.
+
+Example:
+
+${b}=
+${b}B900${b}<^|color|Red${b}>${b}b Red
+
+${b}B090${b}<^|color|Green${b}>${b}b Green
+
+${b}B009${b}<^|color|Blue${b}>${b}b Blue
+${b}=
+
+will render:
+
+${b}B900${b}<^|color|Red${b}>${b}b Red
+
+${b}B090${b}<^|color|Green${b}>${b}b Green
+
+${b}B009${b}<^|color|Blue${b}>${b}b Blue
+
+In this example, when the data is submitted, ${b}B444${b} field_color${b}b will be set to whichever value from the list was selected.
+
+${b}${b}
+
+>Comments
+
+You can insert comments that will not be displayed in the output by starting a line with the # character.
+
+Here's an example:
+
+${b}Faaa
+${b}=
+# This line will not be displayed
+This line will
+${b}=
+${b}${b}
+
+The above markup produces the following output:
+
+${b}Faaa${b}B333
+
+# This line will not be displayed
+This line will
+
+${b}${b}
+
+
+>Literals
+
+To display literal content, for example source-code, or blocks of text that should not be interpreted by micron, you can use literal blocks, specified by the \\${b}= tag. Below is the source code of this entire document, presented as a literal block.
+
+-
+
${b}=
`;
},
diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
index 9a65372..82ca359 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
@@ -561,21 +561,19 @@ export default {
// handle success for archived versions first (before path check)
if (nomadnetPageDownload.status === "success" && nomadnetPageDownload.is_archived_version) {
this.nodePagePath = responsePagePath;
+ this.nodePagePathUrlInput = responsePagePath;
this.isShowingArchivedVersion = true;
this.archivedAt = nomadnetPageDownload.archived_at;
this.nodePageContent = nomadnetPageDownload.page_content;
this.nodePageProgress = 100;
this.isLoadingNodePage = false;
this.currentPageDownloadId = null;
- this.renderPageContent(nomadnetPageDownload.page_path, nomadnetPageDownload.page_content);
+ this.fetchArchives();
return;
}
// ignore response if it's for a different page than currently requested/viewed
- if (responsePagePath !== this.nodePagePath) {
- console.log(
- `ignoring nomadnet page download response for ${responsePagePath} as current page is ${this.nodePagePath}`
- );
+ if (this.nodePagePath && responsePagePath !== this.nodePagePath) {
return;
}
@@ -593,22 +591,18 @@ export default {
const nomadnetPageDownloadCallback =
this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
- // handle success
- if (nomadnetPageDownload.status === "success") {
- if (nomadnetPageDownloadCallback && nomadnetPageDownloadCallback.onSuccessCallback) {
- nomadnetPageDownloadCallback.onSuccessCallback(nomadnetPageDownload.page_content);
- delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
- this.currentPageDownloadId = null;
- return;
- }
- }
-
// if no callback found for other statuses, return
if (!nomadnetPageDownloadCallback) {
- console.log(
- "did not find nomadnet page download callback for key: " +
- getNomadnetPageDownloadCallbackKey
- );
+ return;
+ }
+
+ // handle success
+ if (nomadnetPageDownload.status === "success") {
+ if (nomadnetPageDownloadCallback.onSuccessCallback) {
+ nomadnetPageDownloadCallback.onSuccessCallback(nomadnetPageDownload.page_content);
+ }
+ delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
+ this.currentPageDownloadId = null;
return;
}
@@ -703,10 +697,14 @@ export default {
break;
}
case "nomadnet.page.archives": {
+ const currentRelativePath = this.nodePagePath?.includes(":")
+ ? this.nodePagePath.split(":").slice(1).join(":")
+ : this.nodePagePath;
+
if (
this.selectedNode &&
json.destination_hash === this.selectedNode.destination_hash &&
- json.page_path === this.nodePagePath
+ (json.page_path === this.nodePagePath || json.page_path === currentRelativePath)
) {
this.pageArchives = json.archives;
this.isLoadingArchives = false;
@@ -714,10 +712,14 @@ export default {
break;
}
case "nomadnet.page.archive.added": {
+ const currentRelativePath = this.nodePagePath?.includes(":")
+ ? this.nodePagePath.split(":").slice(1).join(":")
+ : this.nodePagePath;
+
if (
this.selectedNode &&
json.destination_hash === this.selectedNode.destination_hash &&
- json.page_path === this.nodePagePath
+ (json.page_path === this.nodePagePath || json.page_path === currentRelativePath)
) {
ToastUtils.success(this.$t("nomadnet.page_archived_successfully"));
this.fetchArchives();
@@ -883,6 +885,7 @@ export default {
this.archivedAt = null;
this.nodePagePath = `${destinationHash}:${pagePath}`;
this.nodePageContent = null;
+ this.pageArchives = [];
this.nodePageProgress = 0;
// update url bar
@@ -905,8 +908,8 @@ export default {
// if page is cache, we can just return it now
if (cachedNodePageContent != null) {
this.nodePageContent = cachedNodePageContent;
- this.renderPageContent(pagePath, cachedNodePageContent);
this.isLoadingNodePage = false;
+ this.fetchArchives();
return;
}
}
@@ -918,7 +921,6 @@ export default {
(pageContent) => {
// do nothing if callback is for a previous request
if (seq !== this.nodePageRequestSequence) {
- console.log("ignoring page content callback for previous page request");
return;
}
@@ -929,17 +931,18 @@ export default {
const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
this.nodePageCache[nodePagePathCacheKey] = this.nodePageContent;
- // update page content
- this.renderPageContent(pagePath, pageContent);
+ // update status
this.isLoadingNodePage = false;
// update node path
this.getNodePath(destinationHash);
+
+ // check if this page has archives
+ this.fetchArchives();
},
(failureReason) => {
// do nothing if callback is for a previous request
if (seq !== this.nodePageRequestSequence) {
- console.log("ignoring failure callback for previous page request");
return;
}
@@ -953,7 +956,6 @@ export default {
(progress) => {
// do nothing if callback is for a previous request
if (seq !== this.nodePageRequestSequence) {
- console.log("ignoring progress callback for previous page request");
return;
}
@@ -1329,11 +1331,15 @@ export default {
fetchArchives() {
if (!this.selectedNode || !this.nodePagePath) return;
this.isLoadingArchives = true;
+
+ const parsed = this.parseNomadnetworkUrl(this.nodePagePath);
+ if (!parsed) return;
+
WebSocketConnection.send(
JSON.stringify({
type: "nomadnet.page.archives.get",
destination_hash: this.selectedNode.destination_hash,
- page_path: this.nodePagePath,
+ page_path: parsed.path,
})
);
},
@@ -1343,6 +1349,13 @@ export default {
this.isShowingArchivedVersion = false;
this.archivedAt = null;
this.nodePageProgress = 0;
+
+ const archive = this.pageArchives.find((a) => a.id === archiveId);
+ if (archive) {
+ this.nodePagePath = `${archive.destination_hash}:${archive.page_path}`;
+ this.nodePagePathUrlInput = this.nodePagePath;
+ }
+
WebSocketConnection.send(
JSON.stringify({
type: "nomadnet.page.archive.load",
@@ -1354,11 +1367,15 @@ export default {
manualArchive() {
if (!this.selectedNode || !this.nodePagePath || !this.nodePageContent) return;
ToastUtils.info(this.$t("nomadnet.archiving_page"));
+
+ const parsed = this.parseNomadnetworkUrl(this.nodePagePath);
+ if (!parsed) return;
+
WebSocketConnection.send(
JSON.stringify({
type: "nomadnet.page.archive.add",
destination_hash: this.selectedNode.destination_hash,
- page_path: this.nodePagePath,
+ page_path: parsed.path,
content: this.nodePageContent,
})
);
diff --git a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
index 74aaefd..b1c2428 100644
--- a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
+++ b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
@@ -2,216 +2,218 @@
-
-
-
-
-
-
-
-
-
- Paper Message Generator
-
-
- Generate signed LXMF messages for physical delivery or offline transfer.
-
+
+
+
+
+
+
+
+
+
+
+ Paper Message Generator
+
+
+ Generate signed LXMF messages for physical delivery or offline transfer.
+
+
-
-
-
-
-
-
-
-
-
- Recipient Address
-
-
+
+
+
+
+
-
-
- Subject (Optional)
-
-
-
-
-
- Message Content
-
-
-
-
-
-
- Generating...
-
-
-
- Generate Paper Message
-
-
-
-
-
-
-
-
-
-
- Paste an LXMF URI to decode and add it to your conversations.
-
-
-
+
+
+
+ Recipient Address
+
+
+
+
+
+ Subject (Optional)
+
+
+
+
+
+ Message Content
+
+
+
-
+
+
+ Generating...
+
+
+
+ Generate Paper Message
+
-
- Read LXM
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+ Paste an LXMF URI to decode and add it to your conversations.
+
+
+
+
- LXMF URI
-
-
-
- {{ generatedUri }}
-
-
-
-
+
+
+
+
+ Read LXM
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
- Print
-
-
-
+
+ LXMF URI
+
+
- Sending...
-
-
-
- Send
-
-
+ 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 }}
+
+
+
+
+
+
+
+
+
+
+ Print
+
+
+
+
+ Sending...
+
+
+
+ Send
+
+
+
-
-
+
-
-
-
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ All Interfaces
+
+ {{ iface }}
+
+
+
+ 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:
+
+ 20
+ 50
+ 100
+ 250
+
+
+
@@ -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 @@
+
+
+ {{ showAdvanced ? "Simple" : "Advanced" }}
+
- Open in new tab
+ Original
-
-
+
+
+
+
+
+
+
+
+
+
+
+ 1. {{ $t("tools.rnode_flasher.select_device") }}
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.product") }}
+
+
+ {{ $t("tools.rnode_flasher.select_product") }}
+
+
+ {{ product.name }}
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.model") }}
+
+ {{ $t("tools.rnode_flasher.select_model") }}
+
+
+ {{ model.name }}
+
+
+
+
+
+
+
+ {{
+ isEnteringDfuMode
+ ? $t("tools.rnode_flasher.entering_dfu_mode")
+ : $t("tools.rnode_flasher.enter_dfu_mode")
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ 2. {{ $t("tools.rnode_flasher.select_firmware") }}
+
+
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.download_recommended") }}
+
+
+ {{ recommendedFirmwareFilename }}
+
+
+
+
+ {{
+ isDownloadingFirmware
+ ? $t("tools.rnode_flasher.downloading")
+ : $t("tools.rnode_flasher.download_recommended")
+ }}
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.select_firmware") }}
+
+
+
+
+ {{ flashError }}
+
+
+
+
+
+ {{
+ isFlashing
+ ? $t("tools.rnode_flasher.flashing", { percentage: flashingProgress })
+ : $t("tools.rnode_flasher.flash_now")
+ }}
+
+
+
+
+
{{ flashingStatus }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3. {{ $t("tools.rnode_flasher.step_provision") }}
+
+
+
+
+ {{ $t("tools.rnode_flasher.provision_description") }}
+
+
+ {{ $t("tools.rnode_flasher.provision") }}
+
+
+
+ {{ $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.set_firmware_hash") }}
+
+
+
+ {{ $t("tools.rnode_flasher.setting_hash_wait") }}
+
+
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.advanced_tools") }}
+
+
+
+
+ {{ $t("tools.rnode_flasher.detect_rnode") }}
+
+
+
+ {{ $t("tools.rnode_flasher.reboot_rnode") }}
+
+
+
+ {{ $t("tools.rnode_flasher.read_display") }}
+
+
+
+ {{ $t("tools.rnode_flasher.dump_eeprom") }}
+
+
+
+ {{ $t("tools.rnode_flasher.wipe_eeprom") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.configure_bluetooth") }}
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.enable") }}
+
+
+ {{ $t("tools.rnode_flasher.disable") }}
+
+
+ {{ $t("tools.rnode_flasher.start_pairing") }}
+
+
+
+ {{ $t("tools.rnode_flasher.bluetooth_restart_warning") }}
+
+
+
+
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.configure_tnc") }}
+
+
+
+
+
+ {{
+ $t("tools.rnode_flasher.frequency")
+ }}
+
+
+
+ {{
+ $t("tools.rnode_flasher.tx_power")
+ }}
+
+
+
+ {{
+ $t("tools.rnode_flasher.bandwidth")
+ }}
+
+
+ {{ bw / 1000 }} KHz
+
+
+
+
+ {{
+ $t("tools.rnode_flasher.spreading_factor")
+ }}
+
+
+ {{ sf }}
+
+
+
+
+
+
+ {{ $t("tools.rnode_flasher.enable") }}
+
+
+ {{ $t("tools.rnode_flasher.disable") }}
+
+
+
+
+
+
+
+
+
+
+ {{ $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") }}
-