diff --git a/meshchatx/src/frontend/components/LanguageSelector.vue b/meshchatx/src/frontend/components/LanguageSelector.vue index 44b87e9..7ad6d8d 100644 --- a/meshchatx/src/frontend/components/LanguageSelector.vue +++ b/meshchatx/src/frontend/components/LanguageSelector.vue @@ -4,34 +4,37 @@ type="button" class="relative rounded-full p-1.5 sm:p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors" :title="$t('app.language')" - @click="toggleDropdown" + @click.stop="toggleDropdown" > -
-
- + +
+
+ +
-
+
@@ -62,6 +65,7 @@ export default { data() { return { isDropdownOpen: false, + dropdownPosition: { top: 0, left: 0 }, languages: [ { code: "en", name: "English" }, { code: "de", name: "Deutsch" }, @@ -73,10 +77,27 @@ export default { currentLanguage() { return this.$i18n.locale; }, + dropdownStyle() { + return { + top: `${this.dropdownPosition.top}px`, + left: `${this.dropdownPosition.left}px`, + }; + }, }, methods: { - toggleDropdown() { + toggleDropdown(event) { this.isDropdownOpen = !this.isDropdownOpen; + if (this.isDropdownOpen) { + this.updateDropdownPosition(event); + } + }, + updateDropdownPosition(event) { + const button = event.currentTarget; + const rect = button.getBoundingClientRect(); + this.dropdownPosition = { + top: rect.bottom + 8, + left: Math.max(8, rect.right - 192), // 192px is w-48 + }; }, closeDropdown() { this.isDropdownOpen = false; diff --git a/meshchatx/src/frontend/components/LxmfUserIcon.vue b/meshchatx/src/frontend/components/LxmfUserIcon.vue index b3ea2a0..d054c88 100644 --- a/meshchatx/src/frontend/components/LxmfUserIcon.vue +++ b/meshchatx/src/frontend/components/LxmfUserIcon.vue @@ -16,10 +16,10 @@
- +
diff --git a/meshchatx/src/frontend/components/NotificationBell.vue b/meshchatx/src/frontend/components/NotificationBell.vue index 6871ce4..64170e8 100644 --- a/meshchatx/src/frontend/components/NotificationBell.vue +++ b/meshchatx/src/frontend/components/NotificationBell.vue @@ -3,7 +3,7 @@ -
-
-
-

Notifications

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

Notifications

+
-
Loading notifications...
-
- -
No new notifications
-
- -
- +
Loading notifications...
+
+ +
+ +
No new notifications
+
+ +
+ +
-
+
@@ -139,9 +146,17 @@ export default { notifications: [], unreadCount: 0, reloadInterval: null, + dropdownPosition: { top: 0, left: 0 }, }; }, - computed: {}, + computed: { + dropdownStyle() { + return { + top: `${this.dropdownPosition.top}px`, + left: `${this.dropdownPosition.left}px`, + }; + }, + }, beforeUnmount() { if (this.reloadInterval) { clearInterval(this.reloadInterval); @@ -158,13 +173,25 @@ export default { }, 5000); }, methods: { - async toggleDropdown() { + async toggleDropdown(event) { this.isDropdownOpen = !this.isDropdownOpen; if (this.isDropdownOpen) { + this.updateDropdownPosition(event); await this.loadNotifications(); await this.markNotificationsAsViewed(); } }, + updateDropdownPosition(event) { + const button = event.currentTarget; + const rect = button.getBoundingClientRect(); + const isMobile = window.innerWidth < 640; + const dropdownWidth = isMobile ? 320 : 384; // 80 (320px) or 96 (384px) + + this.dropdownPosition = { + top: rect.bottom + 8, + left: Math.max(16, rect.right - dropdownWidth), + }; + }, closeDropdown() { this.isDropdownOpen = false; }, diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue index 0aa12e4..81fe925 100644 --- a/meshchatx/src/frontend/components/about/AboutPage.vue +++ b/meshchatx/src/frontend/components/about/AboutPage.vue @@ -27,36 +27,19 @@
- - - - @@ -92,619 +75,577 @@ }}
-
- - - {{ showAdvanced ? "Hide Advanced" : "Advanced Mode" }} - - -
- -
+
+ +
+
- - Environment Information + + Security & Integrity
-
-
-
Reticulum Config
-
- {{ appInfo.reticulum_config_path }} -
- - Reveal File - -
-
-
Database Path
-
- {{ appInfo.database_path }} -
- - Reveal DB - -
-
+ -
-
- Identity Hash - {{ - config.identity_hash - }} -
-
- LXMF Address - {{ - config.lxmf_address_hash - }} -
-
-
- Python - v{{ appInfo.python_version }} -
-
- LXMF - v{{ appInfo.lxmf_version }} -
-
- RNS - v{{ appInfo.rns_version }} -
-
-
-
- - -
-
-
- - Security & Integrity -
-
- - - {{ + - -
-
- -
-
+ {{ + appInfo.integrity_issues.length === 0 + ? $t("about.secured") + : $t("about.tampering_detected") + }} + +
-
    -
  • - - {{ issue }} -
  • -
-
-
- - {{ $t("about.no_integrity_violations") }} + + {{ $t("common.acknowledge_reset") }} +
- -
+
- - Dependency Chain + + Technical Issues Detected
-
-
-
-
+
  • + + {{ issue }} +
  • + +
    +
    + + {{ $t("about.no_integrity_violations") }} +
    +
    + + +
    +
    + + Environment Information +
    +
    +
    +
    Reticulum Config
    +
    + {{ appInfo.reticulum_config_path }} +
    + +
    +
    +
    Database Path
    +
    + {{ appInfo.database_path }} +
    + +
    +
    +
    +
    + Identity Hash - -
    -
    -
    - MeshChatX -
    -
    - v{{ appInfo.version }} -
    -
    + {{ + config.identity_hash + }}
    -
    -
    -
    + LXMF Address - LXMF -
    -
    -
    - Lightweight Extensible Message Format -
    -
    - v{{ appInfo.lxmf_version }} -
    -
    -
    -
    -
    -
    - RNS -
    -
    -
    - Reticulum Network Stack -
    -
    - v{{ appInfo.rns_version }} -
    -
    + {{ + config.lxmf_address_hash + }}
    - -
    -
    + Python -
    - Core Runtime -
    -
    -
    - LXST Engine - v{{ appInfo.lxst_version }} -
    -
    - Electron - v{{ electronVersion }} -
    -
    - Chrome - v{{ chromeVersion }} -
    -
    - Node.js - v{{ nodeVersion }} -
    -
    -
    - -
    -
    - Backend Stack -
    -
    -
    - {{ name.replace("_", " ") }} - v{{ version }} -
    -
    -
    + v{{ appInfo.python_version }} +
    +
    + LXMF + v{{ appInfo.lxmf_version }} +
    +
    + RNS + v{{ appInfo.rns_version }}
    +
    - -
    -
    -
    - - Database Health & Maintenance -
    -
    - - Refresh - - - Vacuum - - - Recovery - -
    -
    - -
    -
    + +
    +
    + + Dependency Chain +
    +
    +
    +
    - Integrity +
    -
    - {{ databaseHealth.quick_check }} +
    +
    MeshChatX
    +
    + v{{ appInfo.version }} +
    +
    - Journal + LXMF
    -
    - {{ databaseHealth.journal_mode }} +
    +
    + Lightweight Extensible Message Format +
    +
    + v{{ appInfo.lxmf_version }} +
    - Page Count -
    -
    - {{ databaseHealth.page_count }} -
    -
    -
    + class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-purple-500 to-indigo-500" + >
    - Free Space + RNS
    -
    - {{ formatBytes(databaseHealth.estimated_free_bytes) }} +
    +
    + Reticulum Network Stack +
    +
    + v{{ appInfo.rns_version }} +
    -
    - -
    -
    -
    - - Database Backups -
    -
    - Full snapshots of your communications database. -
    -
    - +
    +
    - - Download Backup - -
    - - -
    -
    -
    -
    +
    +
    + LXST Engine + v{{ appInfo.lxst_version }} - - Local Snapshots -
    -
    - Create point-in-time restore points on disk. -
    -
    - - + Electron + v{{ electronVersion }} +
    +
    + Chrome + v{{ chromeVersion }} +
    +
    + Node.js + v{{ nodeVersion }} - Create -
    +
    -
    +
    +
    + Backend Stack +
    +
    -
    - {{ snapshot.name }} - {{ formatBytes(snapshot.size) }} • {{ snapshot.created_at }} -
    - {{ name.replace("_", " ") }} + v{{ version }} - Restore -
    - - -
    -
    - -
    -
    Identity Key Control
    -
    - Critical Security Warning -
    -
    -
    - -
    - - - Export Key File - - - - Copy Base32 Key - -
    - -
    -
    - Restore Identity -
    -
    - - - Upload Key File - - -
    - — or — -
    - - - Paste Base32 - -
    - - -
    - - - Confirm Key Restore - -
    -
    -
    -
    - + + +
    +
    +
    + + Database Health & Maintenance +
    +
    + + + +
    +
    + +
    +
    +
    + Integrity +
    +
    + {{ databaseHealth.quick_check }} +
    +
    +
    +
    + Journal +
    +
    + {{ databaseHealth.journal_mode }} +
    +
    +
    +
    + Page Count +
    +
    + {{ databaseHealth.page_count }} +
    +
    +
    +
    + Free Space +
    +
    + {{ formatBytes(databaseHealth.estimated_free_bytes) }} +
    +
    +
    + +
    + +
    +
    +
    + + Database Backups +
    +
    + Full snapshots of your communications database. +
    +
    + +
    + + +
    +
    +
    +
    + + Local Snapshots +
    +
    + Create point-in-time restore points on disk. +
    +
    +
    + + +
    +
    + +
    +
    +
    + {{ snapshot.name }} + {{ formatBytes(snapshot.size) }} • {{ snapshot.created_at }} +
    + +
    +
    +
    + + +
    +
    + +
    +
    Identity Key Control
    +
    + Critical Security Warning +
    +
    +
    + +
    + + +
    + +
    +
    + Restore Identity +
    +
    + + +
    + — or — +
    + +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    @@ -715,6 +656,7 @@ import Utils from "../../js/Utils"; import ElectronUtils from "../../js/ElectronUtils"; import DialogUtils from "../../js/DialogUtils"; import ToastUtils from "../../js/ToastUtils"; +import GlobalEmitter from "../../js/GlobalEmitter"; export default { name: "AboutPage", components: {}, @@ -758,7 +700,6 @@ export default { electronVersion: null, chromeVersion: null, nodeVersion: null, - showAdvanced: false, showIdentityPaste: false, }; }, @@ -1030,10 +971,10 @@ export default { } }, showChangelog() { - this.$router.push({ name: "changelog" }); + GlobalEmitter.emit("show-changelog"); }, showTutorial() { - this.$router.push({ name: "tutorial" }); + GlobalEmitter.emit("show-tutorial"); }, showReticulumConfigFile() { const reticulumConfigPath = this.appInfo.reticulum_config_path; @@ -1178,3 +1119,10 @@ export default { }, }; + + diff --git a/meshchatx/src/frontend/components/blocked/BlockedPage.vue b/meshchatx/src/frontend/components/blocked/BlockedPage.vue index 80603ad..57a3b6d 100644 --- a/meshchatx/src/frontend/components/blocked/BlockedPage.vue +++ b/meshchatx/src/frontend/components/blocked/BlockedPage.vue @@ -8,8 +8,8 @@
    -

    Blocked

    -

    Manage blocked users and nodes

    +

    Banished

    +

    Manage Banished users and nodes

    @@ -39,7 +39,7 @@
    -

    Loading blocked items...

    +

    Loading banished items...

    -

    No blocked items

    +

    No banished items

    {{ searchQuery - ? "No blocked items match your search." - : "You haven't blocked any users or nodes yet." + ? "No banished items match your search." + : "You haven't banished any users or nodes yet." }}

    @@ -95,6 +95,13 @@ > User + + RNS Blackhole +

    -
    - Blocked {{ formatTimeAgo(item.created_at) }} +
    + Banished {{ formatTimeAgo(item.created_at) }} +
    +
    + Source: {{ item.rns_source }} +
    +
    + "{{ item.rns_reason }}"
    @@ -114,7 +133,7 @@ @click="onUnblock(item)" > - Unblock + Lift Banishment
    @@ -137,17 +156,31 @@ export default { data() { return { blockedItems: [], + reticulumBlackholedItems: [], isLoading: false, searchQuery: "", }; }, computed: { + allBlockedItems() { + // Combine local blocked items and reticulum blackholed items + // Prioritize local items if they overlap + const localHashes = new Set(this.blockedItems.map((i) => i.destination_hash)); + const combined = [...this.blockedItems]; + + for (const item of this.reticulumBlackholedItems) { + if (!localHashes.has(item.destination_hash)) { + combined.push(item); + } + } + return combined; + }, filteredBlockedItems() { if (!this.searchQuery.trim()) { - return this.blockedItems; + return this.allBlockedItems; } const query = this.searchQuery.toLowerCase(); - return this.blockedItems.filter((item) => { + return this.allBlockedItems.filter((item) => { const matchesHash = item.destination_hash.toLowerCase().includes(query); const matchesDisplayName = (item.display_name || "").toLowerCase().includes(query); return matchesHash || matchesDisplayName; @@ -161,74 +194,70 @@ export default { async loadBlockedDestinations() { this.isLoading = true; try { + // Load local blocked destinations const response = await window.axios.get("/api/v1/blocked-destinations"); const blockedHashes = response.data.blocked_destinations || []; - const items = await Promise.all( - blockedHashes.map(async (blocked) => { - let displayName = "Unknown"; - let isNode = false; + // Load Reticulum blackholed identities + let reticulumBlackholed = {}; + try { + const rnsResponse = await window.axios.get("/api/v1/reticulum/blackhole"); + reticulumBlackholed = rnsResponse.data.blackholed_identities || {}; + } catch (e) { + console.error("Failed to load Reticulum blackhole", e); + } - try { - const nodeAnnounceResponse = await window.axios.get("/api/v1/announces", { - params: { - aspect: "nomadnetwork.node", - identity_hash: blocked.destination_hash, - include_blocked: true, - limit: 1, - }, - }); + const processItem = async (hash, data = {}) => { + let displayName = "Unknown"; + let isNode = false; - if (nodeAnnounceResponse.data.announces && nodeAnnounceResponse.data.announces.length > 0) { - const announce = nodeAnnounceResponse.data.announces[0]; - displayName = announce.display_name || "Unknown"; - isNode = true; - } else { - const announceResponse = await window.axios.get("/api/v1/announces", { - params: { - identity_hash: blocked.destination_hash, - include_blocked: true, - limit: 1, - }, - }); + try { + const announceResponse = await window.axios.get("/api/v1/announces", { + params: { + identity_hash: hash, + include_blocked: true, + limit: 1, + }, + }); - if (announceResponse.data.announces && announceResponse.data.announces.length > 0) { - const announce = announceResponse.data.announces[0]; - displayName = announce.display_name || "Unknown"; - isNode = announce.aspect === "nomadnetwork.node"; - } else { - const lxmfResponse = await window.axios.get("/api/v1/announces", { - params: { - destination_hash: blocked.destination_hash, - include_blocked: true, - limit: 1, - }, - }); - - if (lxmfResponse.data.announces && lxmfResponse.data.announces.length > 0) { - const announce = lxmfResponse.data.announces[0]; - displayName = announce.display_name || "Unknown"; - isNode = announce.aspect === "nomadnetwork.node"; - } - } - } - } catch (e) { - console.log(e); + if (announceResponse.data.announces && announceResponse.data.announces.length > 0) { + const announce = announceResponse.data.announces[0]; + displayName = announce.display_name || "Unknown"; + isNode = announce.aspect === "nomadnetwork.node"; } + } catch { + // ignore error + } - return { - destination_hash: blocked.destination_hash, - display_name: displayName, - created_at: blocked.created_at, - is_node: isNode, - }; - }) + return { + destination_hash: hash, + display_name: displayName, + created_at: data.created_at || null, + is_node: isNode, + is_rns_blackholed: !!data.is_rns, + rns_source: data.source || null, + rns_reason: data.reason || null, + rns_until: data.until || null, + }; + }; + + const items = await Promise.all( + blockedHashes.map((blocked) => + processItem(blocked.destination_hash, { created_at: blocked.created_at }) + ) + ); + + const rnsItems = await Promise.all( + Object.entries(reticulumBlackholed).map(([hash, info]) => + processItem(hash, { ...info, is_rns: true }) + ) ); this.blockedItems = items; + this.reticulumBlackholedItems = rnsItems; } catch (e) { console.log(e); - ToastUtils.error("Failed to load blocked destinations"); + ToastUtils.error("Failed to load banished destinations"); } finally { this.isLoading = false; } @@ -236,7 +265,7 @@ export default { async onUnblock(item) { if ( !(await DialogUtils.confirm( - `Are you sure you want to unblock ${item.display_name || item.destination_hash}?` + `Are you sure you want to lift the banishment for ${item.display_name || item.destination_hash}?` )) ) { return; @@ -245,10 +274,10 @@ export default { try { await window.axios.delete(`/api/v1/blocked-destinations/${item.destination_hash}`); await this.loadBlockedDestinations(); - ToastUtils.success("Unblocked successfully"); + ToastUtils.success("Banishment lifted successfully"); } catch (e) { console.log(e); - ToastUtils.error("Failed to unblock"); + ToastUtils.error("Failed to lift banishment"); } }, onSearchInput() {}, diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue index 78585b2..eb10c63 100644 --- a/meshchatx/src/frontend/components/call/CallPage.vue +++ b/meshchatx/src/frontend/components/call/CallPage.vue @@ -2380,15 +2380,15 @@ export default { } }, async blockIdentity(hash) { - if (!confirm(`Are you sure you want to block this identity?`)) return; + if (!confirm(`Are you sure you want to banish this identity?`)) return; try { await window.axios.post("/api/v1/blocked-destinations", { destination_hash: hash, }); - ToastUtils.success("Identity blocked"); + ToastUtils.success("Identity banished"); this.getHistory(); } catch { - ToastUtils.error("Failed to block identity"); + ToastUtils.error("Failed to banish identity"); } }, async getVoicemailStatus() { diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue index 8a75642..46b5358 100644 --- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue +++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue @@ -1588,12 +1588,6 @@ export default { .glass-label { @apply mb-1 text-sm font-semibold text-gray-800 dark:text-gray-200; } -.primary-chip { - @apply inline-flex items-center gap-x-2 rounded-full bg-blue-600/90 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-blue-500 transition; -} -.secondary-chip { - @apply inline-flex items-center gap-x-2 rounded-full border border-gray-300 dark:border-zinc-700 px-3 py-1.5 text-xs font-semibold text-gray-700 dark:text-gray-100 bg-white/80 dark:bg-zinc-900/70 hover:border-blue-400; -} .glass-field { @apply space-y-1; } diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue index 8da7ef5..ae39c2f 100644 --- a/meshchatx/src/frontend/components/map/MapPage.vue +++ b/meshchatx/src/frontend/components/map/MapPage.vue @@ -78,12 +78,12 @@
    @@ -147,9 +147,7 @@ class="absolute top-2 left-4 right-4 sm:left-auto sm:right-4 sm:w-80 z-30" >
    -
    +
    @@ -250,15 +248,15 @@ >
    @@ -922,8 +920,8 @@ Edit Note @@ -938,15 +936,15 @@
    diff --git a/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue b/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue index a062e35..d6cd0e4 100644 --- a/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue +++ b/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue @@ -1,6 +1,6 @@ @@ -321,9 +321,9 @@ export default { try { await window.axios.delete(`/api/v1/blocked-destinations/${identityHash}`); GlobalEmitter.emit("block-status-changed"); - DialogUtils.alert("Node unblocked successfully"); + DialogUtils.alert("Banishment lifted successfully"); } catch (e) { - DialogUtils.alert("Failed to unblock node"); + DialogUtils.alert("Failed to lift banishment"); console.log(e); } }, diff --git a/meshchatx/src/frontend/components/ping/PingPage.vue b/meshchatx/src/frontend/components/ping/PingPage.vue index e7069a9..7f1cca3 100644 --- a/meshchatx/src/frontend/components/ping/PingPage.vue +++ b/meshchatx/src/frontend/components/ping/PingPage.vue @@ -40,28 +40,24 @@
    - - - diff --git a/meshchatx/src/frontend/components/rncp/RNCPPage.vue b/meshchatx/src/frontend/components/rncp/RNCPPage.vue index 81a7f0b..7ba3af8 100644 --- a/meshchatx/src/frontend/components/rncp/RNCPPage.vue +++ b/meshchatx/src/frontend/components/rncp/RNCPPage.vue @@ -15,6 +15,33 @@
    {{ $t("rncp.description") }}
    + +
    +
    + {{ $t("rncp.usage_steps") }} +
    +
    + +

    + +

    + +

    +
    +
    @@ -493,6 +520,10 @@ export default { this.listenDestinationHash = null; this.listenResult = null; }, + renderMarkdown(text) { + if (!text) return ""; + return text.replace(/\*\*(.*?)\*\*/g, "$1"); + }, }, }; diff --git a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue index 92a6b38..8ff7d99 100644 --- a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue +++ b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue @@ -48,11 +48,36 @@
    -
    -
    Active Links: {{ linkCount }}
    +
    +
    +
    Active Links: {{ linkCount }}
    +
    + +
    +
    + Blackhole: {{ blackholeEnabled ? "Publishing" : "Active" }} + {{ blackholeCount }} Identities +
    +
    +
    +
    + +
    +
    Blackhole Sources
    +
    +
    + {{ source }} +
    @@ -189,6 +214,9 @@ export default { linkCount: null, includeLinkStats: false, sorting: "", + blackholeEnabled: null, + blackholeSources: [], + blackholeCount: 0, }; }, watch: { @@ -215,6 +243,9 @@ export default { const response = await window.axios.get("/api/v1/rnstatus", { params }); this.interfaces = response.data.interfaces || []; this.linkCount = response.data.link_count; + this.blackholeEnabled = response.data.blackhole_enabled; + this.blackholeSources = response.data.blackhole_sources || []; + this.blackholeCount = response.data.blackhole_count || 0; } catch (e) { console.error(e); } finally { diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue index 0ee6f4a..61c3340 100644 --- a/meshchatx/src/frontend/components/settings/SettingsPage.vue +++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue @@ -436,6 +436,40 @@
    + +
    +
    +
    +
    RNS Security
    +

    Network Security

    +

    Manage mesh-level security features.

    +
    +
    +
    +
    +
    +
    + {{ $t("app.blackhole_integration_enabled") }} +
    +
    + {{ $t("app.blackhole_integration_description") }} +
    +
    + +
    +
    +
    +
    @@ -494,14 +528,14 @@
    Privacy
    -

    Blocked

    -

    Manage blocked users and nodes

    +

    Banished

    +

    Manage Banished users and nodes

    - Manage Blocked + Manage Banished

    - Blocked users and nodes will not be able to send you messages, and their announces will + Banished users and nodes will not be able to send you messages, and their announces will be ignored.

    @@ -867,6 +901,7 @@ export default { banished_effect_enabled: true, banished_text: "BANISHED", banished_color: "#dc2626", + blackhole_integration_enabled: true, }, saveTimeouts: {}, shortcuts: [], @@ -1319,9 +1354,6 @@ export default { .setting-toggle__hint { @apply text-xs text-gray-500 dark:text-gray-400; } -.primary-chip { - @apply inline-flex items-center gap-x-1 rounded-full bg-blue-600/90 px-4 py-1.5 text-xs font-semibold text-white shadow hover:bg-blue-500 transition; -} .info-callout { @apply rounded-2xl border border-blue-100 dark:border-blue-900/40 bg-blue-50/60 dark:bg-blue-900/20 px-3 py-3 text-blue-900 dark:text-blue-100; } diff --git a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue index 9eaab52..74aaefd 100644 --- a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue +++ b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue @@ -138,8 +138,8 @@

    Generated QR Code

    -
    -
    +
    +
    @@ -181,11 +181,20 @@
    @@ -227,6 +236,7 @@ export default { isGenerating: false, generatedUri: null, ingestUri: "", + isSending: false, }; }, computed: { @@ -284,7 +294,7 @@ export default { try { await QRCode.toCanvas(this.$refs.qrcode, this.generatedUri, { - width: 320, + width: 256, margin: 2, color: { dark: "#000000", @@ -341,6 +351,54 @@ export default { link.click(); } }, + async sendPaperMessage() { + const canvas = this.$refs.qrcode; + if (!canvas || !this.destinationHash || !this.generatedUri) return; + + try { + this.isSending = true; + + // get data url from canvas (format: data:image/png;base64,iVBORw0KG...) + const dataUrl = canvas.toDataURL("image/png"); + + // extract base64 data by removing the prefix + const imageBytes = dataUrl.split(",")[1]; + + // build lxmf message + const lxmf_message = { + destination_hash: this.destinationHash, + content: this.generatedUri, + fields: { + image: { + image_type: "png", + image_bytes: imageBytes, + name: "qrcode.png", + }, + }, + }; + + // send message + const response = await window.axios.post(`/api/v1/lxmf-messages/send`, { + delivery_method: "opportunistic", + lxmf_message: lxmf_message, + }); + + if (response.data.status === "success") { + ToastUtils.success("Paper message sent successfully"); + this.generatedUri = null; + this.destinationHash = ""; + this.content = ""; + this.title = ""; + } else { + ToastUtils.error(response.data.message || "Failed to send paper message"); + } + } catch (err) { + console.error("Failed to send paper message:", err); + ToastUtils.error("Failed to send paper message"); + } finally { + this.isSending = false; + } + }, printQRCode() { const canvas = this.$refs.qrcode; if (!canvas) return; diff --git a/meshchatx/src/frontend/components/tools/RNPathPage.vue b/meshchatx/src/frontend/components/tools/RNPathPage.vue new file mode 100644 index 0000000..588a699 --- /dev/null +++ b/meshchatx/src/frontend/components/tools/RNPathPage.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue index bb8a85d..389ceae 100644 --- a/meshchatx/src/frontend/components/tools/ToolsPage.vue +++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue @@ -70,6 +70,21 @@ + +
    + +
    +
    +
    {{ $t("tools.rnpath.title") }}
    +
    + {{ $t("tools.rnpath.description") }} +
    +
    + +
    +