diff --git a/meshchatx/src/frontend/components/CommandPalette.vue b/meshchatx/src/frontend/components/CommandPalette.vue
index a7ca6de..d085eb5 100644
--- a/meshchatx/src/frontend/components/CommandPalette.vue
+++ b/meshchatx/src/frontend/components/CommandPalette.vue
@@ -257,6 +257,14 @@ export default {
type: "navigation",
route: { name: "documentation" },
},
+ {
+ id: "nav-repository-server",
+ title: "nav_repository_server",
+ description: "nav_repository_server_desc",
+ icon: "package-variant",
+ type: "navigation",
+ route: { name: "repository-server" },
+ },
{
id: "nav-micron-editor",
title: "nav_micron_editor",
diff --git a/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue b/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
index b4c854f..a598429 100644
--- a/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
@@ -500,6 +500,7 @@
v-if="
chatItem.lxmf_message.content &&
!cv.getParsedItems(chatItem)?.isOnlyPaperMessage &&
+ !cv.getParsedItems(chatItem)?.isOnlyMapLink &&
!cv.shouldHideAutoImageCaption(chatItem)
"
class="leading-relaxed break-words [word-break:break-word] min-w-0 markdown-content"
@@ -635,6 +636,39 @@
Ingest Message
+
+
+
+
+ {{
+ cv.getParsedItems(chatItem).mapLink.kind === "ping"
+ ? $t("messages.map_link_ping_title")
+ : $t("messages.map_link_share_title")
+ }}
+
+
+ {{ cv.getParsedItems(chatItem).mapLink.parsed.lat.toFixed(5) }},
+ {{ cv.getParsedItems(chatItem).mapLink.parsed.lon.toFixed(5) }}
+ (z{{ cv.getParsedItems(chatItem).mapLink.parsed.zoom }})
+
+
+
+
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 77e80ff..096cd71 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -45,143 +45,14 @@
@close="close"
/>
-
-
-
-
-
-
-
Telemetry History
-
-
-
-
-
- No telemetry history found for this peer.
-
-
-
-
- {{ item.is_outbound ? "Sent" : "Received" }}
-
- {{
- formatTimeAgo(item.lxmf_message.created_at)
- }}
-
-
-
-
-
-
-
-
-
-
-
- Battery: {{ item.lxmf_message.fields.telemetry.battery.charge_percent }}%
-
-
-
- SNR: {{ item.lxmf_message.fields.telemetry.physical_link.snr }}dB
-
-
-
-
-
-
- Location Request
-
-
-
-
-
-
- Battery Level (%)
- {{ telemetryBatteryHistory[0].y }}% →
- {{ telemetryBatteryHistory[telemetryBatteryHistory.length - 1].y }}%
-
-
-
-
-
-
-
-
-
-
-
+
item.is_outbound && ["failed", "cancelled"].includes(item.lxmf_message?.state)
);
},
- telemetryBatteryHistory() {
- return this.selectedPeerTelemetryItems
- .filter((item) => item.lxmf_message.fields?.telemetry?.battery)
- .map((item) => ({
- x: item.lxmf_message.timestamp,
- y: item.lxmf_message.fields.telemetry.battery.charge_percent,
- }))
- .sort((a, b) => a.x - b.x);
- },
- batterySparklinePath() {
- const history = this.telemetryBatteryHistory;
- if (history.length < 2) return "";
-
- const minX = history[0].x;
- const maxX = history[history.length - 1].x;
- const rangeX = maxX - minX || 1;
-
- return history
- .map((p, i) => {
- const x = ((p.x - minX) / rangeX) * 100;
- const y = 100 - p.y; // SVG y is top-down
- return `${i === 0 ? "M" : "L"} ${x} ${y}`;
- })
- .join(" ");
+ telemetryModalGradientSuffix() {
+ const h = this.selectedPeer?.destination_hash || "default";
+ return h.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 24) || "default";
},
},
watch: {
@@ -2292,6 +2145,9 @@ export default {
if (this.getParsedItems(chatItem)?.isOnlyPaperMessage) {
return false;
}
+ if (this.getParsedItems(chatItem)?.isOnlyMapLink) {
+ return false;
+ }
if (this.shouldHideAutoImageCaption(chatItem)) {
return false;
}
@@ -2652,6 +2508,7 @@ export default {
const items = {
contact: null,
paperMessage: null,
+ mapLink: null,
};
// Parse contact: Contact: ivan
[LXMF: ...] [LXST: ...]
@@ -2694,6 +2551,23 @@ export default {
}
}
+ const mapUri = findMapUriInContent(content);
+ if (mapUri && !items.paperMessage) {
+ const parsed = parseMeshchatMapUri(mapUri);
+ if (parsed) {
+ const kind = mapLinkKindFromMessage(content, parsed);
+ let t = content.trim().replace(mapUri, "").trim();
+ t = t
+ .replace(/^MeshChatX\s+map\s+ping:\s*/i, "")
+ .replace(/^MeshChatX\s+map:\s*/i, "")
+ .trim();
+ items.mapLink = { uri: mapUri, parsed, kind };
+ if (t === "") {
+ items.isOnlyMapLink = true;
+ }
+ }
+ }
+
return items;
},
async addContact(name, hash, lxmf_address = null, lxst_address = null) {
@@ -4486,6 +4360,38 @@ export default {
},
});
},
+ openMapShareFromParsed(parsed) {
+ if (!parsed) {
+ return;
+ }
+ const query = {
+ lat: String(parsed.lat),
+ lon: String(parsed.lon),
+ zoom: String(parsed.zoom),
+ };
+ if (parsed.layers) {
+ query.layers = parsed.layers;
+ }
+ if (parsed.label) {
+ query.label = parsed.label;
+ }
+ this.$router.push({
+ name: "map",
+ query,
+ });
+ },
+ async copyMapShareUri(uri) {
+ if (!uri) {
+ return;
+ }
+ try {
+ await navigator.clipboard.writeText(uri);
+ ToastUtils.success(this.$t("messages.map_link_copied"));
+ } catch (e) {
+ console.error(e);
+ ToastUtils.error(this.$t("messages.failed_copy_address"));
+ }
+ },
isTelemetryOnly(msg) {
return isTelemetryOnlyMessage(msg);
},
diff --git a/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue b/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue
new file mode 100644
index 0000000..4749c7a
--- /dev/null
+++ b/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue
@@ -0,0 +1,491 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("tools.utilities") }}
+
+
+ {{ $t("tools.repository_server.title") }}
+
+
+ {{ $t("tools.repository_server.subtitle") }}
+
+
+
+
+
+
+
+ {{ $t("tools.repository_server.back_tools") }}
+
+
+
+
+
+
+
+ {{ refreshProgressLabel }}
+
+
+
+
+
+
+
+ {{ $t("tools.repository_server.upload_heading") }}
+
+
+
{{ lastUploadError }}
+
+
+
+
{{ $t("tools.repository_server.refresh_partial") }}
+
+ -
+ {{ pkg }}: {{ msg }}
+
+
+
+
+
+
+
+ {{ $t("tools.repository_server.files_heading") }}
+
+ {{ entries.length }}
+
+
+ {{ $t("common.loading") }}
+
+
+ {{ $t("tools.repository_server.empty") }}
+
+
+
+
+ | {{ $t("tools.repository_server.col_name") }} |
+ {{ $t("tools.repository_server.col_source") }} |
+ {{ $t("tools.repository_server.col_size") }} |
+ |
+
+
+
+
+ | {{ row.name }} |
+ {{ row.source }} |
+ {{ formatBytes(row.bytes) }} |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue
index 97d1235..b7ab6b0 100644
--- a/meshchatx/src/frontend/components/tools/ToolsPage.vue
+++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue
@@ -195,8 +195,8 @@ export default {
route: { name: "propagation-nodes" },
icon: "mailbox",
iconBg: "tool-card__icon bg-cyan-50 text-cyan-500 dark:bg-cyan-900/30 dark:text-cyan-200",
- title: "Propagation Nodes",
- description: "Manage preferred and local propagation nodes with live stats and path checks.",
+ titleKey: "tools.propagation_nodes.title",
+ descriptionKey: "tools.propagation_nodes.description",
},
{
name: "forwarder",
@@ -222,6 +222,14 @@ export default {
titleKey: "docs.title",
descriptionKey: "docs.subtitle",
},
+ {
+ name: "repository-server",
+ route: { name: "repository-server" },
+ icon: "package-variant",
+ iconBg: "tool-card__icon bg-sky-50 text-sky-600 dark:bg-sky-900/30 dark:text-sky-200",
+ titleKey: "tools.repository_server.title",
+ descriptionKey: "tools.repository_server.description",
+ },
{
name: "micron-editor",
route: { name: "micron-editor" },