From cb72691a77d55dcba0342faa7ffdc44c11be89bd Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 17 Apr 2026 23:28:11 -0500 Subject: [PATCH] feat(router): add new route for reticulum configuration editor and update command palette navigation --- meshchatx/src/frontend/components/App.vue | 78 +- .../frontend/components/CommandPalette.vue | 8 + meshchatx/src/frontend/components/Toast.vue | 18 +- .../src/frontend/components/TutorialModal.vue | 1073 +++++++++-------- .../frontend/components/about/AboutPage.vue | 61 +- .../src/frontend/components/call/CallPage.vue | 45 +- .../components/contacts/ContactsPage.vue | 93 +- .../src/frontend/components/forms/Toggle.vue | 10 +- .../interfaces/AddInterfacePage.vue | 145 ++- .../components/interfaces/InterfacesPage.vue | 142 ++- .../src/frontend/components/map/MapPage.vue | 59 +- .../components/messages/AddAudioButton.vue | 5 +- .../messages/ConversationDropDownMenu.vue | 91 +- .../messages/ConversationMessageEntry.vue | 74 +- .../messages/ConversationViewer.vue | 507 +++++++- .../components/messages/InViewAnimatedImg.vue | 65 + .../components/messages/MessagesPage.vue | 236 ++++ .../components/messages/MessagesSidebar.vue | 107 +- .../nomadnetwork/NomadNetworkPage.vue | 33 +- .../nomadnetwork/NomadNetworkSidebar.vue | 66 +- .../PropagationNodesPage.vue | 85 +- .../components/settings/IdentitiesPage.vue | 187 +-- .../components/settings/SettingsPage.vue | 272 ++++- .../components/stickers/StickerEditor.vue | 625 ++++++++++ .../stickers/StickerPacksManager.vue | 317 +++++ .../components/stickers/StickerView.vue | 212 ++++ .../components/tools/PaperMessagePage.vue | 142 ++- .../tools/ReticulumConfigEditorPage.vue | 262 ++++ .../frontend/components/tools/ToolsPage.vue | 11 +- meshchatx/src/frontend/js/inViewObserver.js | 37 + .../js/settings/incomingDeliveryLimit.js | 52 + .../js/settings/settingsMaintenanceClient.js | 21 + meshchatx/src/frontend/js/tgsDecode.js | 20 + meshchatx/src/frontend/main.js | 5 + 34 files changed, 4249 insertions(+), 915 deletions(-) create mode 100644 meshchatx/src/frontend/components/messages/InViewAnimatedImg.vue create mode 100644 meshchatx/src/frontend/components/stickers/StickerEditor.vue create mode 100644 meshchatx/src/frontend/components/stickers/StickerPacksManager.vue create mode 100644 meshchatx/src/frontend/components/stickers/StickerView.vue create mode 100644 meshchatx/src/frontend/components/tools/ReticulumConfigEditorPage.vue create mode 100644 meshchatx/src/frontend/js/inViewObserver.js create mode 100644 meshchatx/src/frontend/js/settings/incomingDeliveryLimit.js create mode 100644 meshchatx/src/frontend/js/tgsDecode.js diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index d40aee6..5a5d520 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -39,26 +39,26 @@ -
+ +
+ +
+
+
+

{{ $t("messages.scan_qr") }}

+ +
+
+ +
+ {{ ingestScannerError || $t("messages.scanner_hint") }}
@@ -191,6 +312,12 @@ export default { isIngestModalOpen: false, ingestUri: "", + isIngestScannerModalOpen: false, + ingestScannerError: null, + ingestScannerStream: null, + ingestScannerAnimationFrame: null, + isMobileComposeModalOpen: false, + mobileComposeAddress: "", }; }, computed: { @@ -210,6 +337,13 @@ export default { messagesSidebarOnRight() { return this.messagesSidebarPosition === "right"; }, + cameraSupported() { + return ( + typeof window !== "undefined" && + typeof window.BarcodeDetector !== "undefined" && + navigator?.mediaDevices?.getUserMedia + ); + }, }, watch: { conversations() { @@ -220,6 +354,7 @@ export default { }, destinationHash(newHash) { if (newHash) { + this.isMobileComposeModalOpen = false; this.onComposeNewMessage(newHash); } }, @@ -230,6 +365,7 @@ export default { clearTimeout(this.peersRefreshTimeout); this.conversationsAbortController?.abort(); this.announcesAbortController?.abort(); + this.stopIngestScanner(); // stop listening for websocket messages WebSocketConnection.off("message", this.onWebsocketMessage); @@ -847,6 +983,89 @@ export default { this.ingestUri = ""; this.isIngestModalOpen = true; }, + async openIngestScannerModal() { + this.ingestScannerError = null; + this.isIngestScannerModalOpen = true; + await this.$nextTick(); + await this.startIngestScanner(); + }, + closeIngestScannerModal() { + this.isIngestScannerModalOpen = false; + this.stopIngestScanner(); + }, + async startIngestScanner() { + if (!this.cameraSupported) { + this.ingestScannerError = this.$t("messages.camera_not_supported"); + return; + } + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: "environment" }, + audio: false, + }); + this.ingestScannerStream = stream; + const video = this.$refs.ingestScannerVideo; + if (!video) { + this.ingestScannerError = this.$t("messages.camera_failed"); + this.stopIngestScanner(); + return; + } + video.srcObject = stream; + await video.play(); + this.detectIngestQrLoop(); + } catch (e) { + this.ingestScannerError = this.describeCameraError(e); + } + }, + detectIngestQrLoop() { + if (!this.isIngestScannerModalOpen) return; + const video = this.$refs.ingestScannerVideo; + if (!video || video.readyState < 2) { + this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop()); + return; + } + const detector = new window.BarcodeDetector({ formats: ["qr_code"] }); + detector + .detect(video) + .then((barcodes) => { + const qr = barcodes?.[0]?.rawValue?.trim(); + if (!qr) { + this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop()); + return; + } + if (!/^lxm(a|f)?:\/\//i.test(qr)) { + ToastUtils.error(this.$t("messages.invalid_qr_uri")); + this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop()); + return; + } + this.ingestUri = qr; + this.closeIngestScannerModal(); + this.ingestPaperMessage(); + }) + .catch(() => { + this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop()); + }); + }, + stopIngestScanner() { + if (this.ingestScannerAnimationFrame) { + cancelAnimationFrame(this.ingestScannerAnimationFrame); + this.ingestScannerAnimationFrame = null; + } + if (this.ingestScannerStream) { + this.ingestScannerStream.getTracks().forEach((track) => track.stop()); + this.ingestScannerStream = null; + } + }, + describeCameraError(error) { + const name = error?.name || ""; + if (name === "NotAllowedError" || name === "SecurityError") { + return this.$t("messages.camera_permission_denied"); + } + if (name === "NotFoundError" || name === "DevicesNotFoundError") { + return this.$t("messages.camera_not_found"); + } + return this.$t("messages.camera_failed"); + }, async pasteFromClipboard() { try { this.ingestUri = await navigator.clipboard.readText(); @@ -874,6 +1093,23 @@ export default { const match = hash.match(/popout=([^&]+)/); return match ? decodeURIComponent(match[1]) : null; }, + openMobileCompose() { + this.mobileComposeAddress = ""; + this.isMobileComposeModalOpen = true; + }, + openIngestFromMobileCompose() { + this.isMobileComposeModalOpen = false; + this.openIngestPaperMessageModal(); + }, + async submitMobileCompose() { + const raw = this.mobileComposeAddress.trim(); + if (!raw) { + return; + } + this.isMobileComposeModalOpen = false; + this.mobileComposeAddress = ""; + await this.onComposeNewMessage(raw.replace(/^lxmf@/, "")); + }, }, }; diff --git a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue index 7f5638e..63722c2 100644 --- a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue +++ b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue @@ -10,7 +10,7 @@ ]" > + + + +
@@ -1919,6 +1923,27 @@ export default { font-feature-settings: inherit; } +/* + * Mobile-only: allow horizontal scrolling for micron pages so ASCII art and + * fixed-width content do not get word-wrapped and broken up. Markdown and HTML + * rendered content keep their natural wrap behaviour. + */ +@media (max-width: 640px) { + .nodeContainer { + overflow-x: auto; + } + + .nodeContainer .Mu-mws { + flex-wrap: nowrap; + } + + .nodeContainer pre, + .nodeContainer .mu-parse-fallback, + .nodeContainer .mu-line-parse-fallback { + white-space: pre; + } +} + pre.text-wrap > div { display: flex; white-space: pre; diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue index 9307fc7..b1c559c 100644 --- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue +++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue @@ -7,7 +7,7 @@ class="flex flex-col h-full min-h-0 bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800" >