feat(mesh): show federated Archipelago nodes on the Mesh Map
Peers that opt in via a new "Share Location" toggle in Settings (server.set-location RPC) get plotted on other trusted peers' Mesh Map with a distinct Archy-logo marker, separate from raw LoRa radio peers. Location is persisted locally, carried in NodeStateSnapshot, and propagated through federation sync/delta like other node state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e3baaa5de3
commit
177b8a4338
@@ -130,6 +130,7 @@ async function refreshFederationNodes() {
|
||||
})
|
||||
}
|
||||
fedNodesByDid.value = next
|
||||
mesh.updateFederatedPositions(res.nodes)
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
async function refreshSelfOnion() {
|
||||
|
||||
@@ -68,6 +68,60 @@ const copiedOnion = ref(false)
|
||||
const copiedDid = ref(false)
|
||||
let copiedTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// Location sharing — opt-in only, off by default. Lets this node's own
|
||||
// position appear on OTHER trusted federation peers' Mesh Map (with the
|
||||
// Archy logo marker), the same way a mesh radio peer's position shows up.
|
||||
// Backed by the store's serverInfo (already synced live over the WS), not
|
||||
// a separate fetch, so it stays in sync with any other tab/session too.
|
||||
const shareLocation = computed(() => !!store.serverInfo?.['share-location'])
|
||||
const savedLat = computed(() => store.serverInfo?.lat ?? null)
|
||||
const savedLon = computed(() => store.serverInfo?.lon ?? null)
|
||||
const locationSaving = ref(false)
|
||||
const locationError = ref('')
|
||||
|
||||
async function useCurrentLocation() {
|
||||
locationError.value = ''
|
||||
if (!navigator.geolocation) {
|
||||
locationError.value = 'Geolocation not supported by this browser'
|
||||
return
|
||||
}
|
||||
locationSaving.value = true
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async (pos) => {
|
||||
await saveLocation(pos.coords.latitude, pos.coords.longitude, shareLocation.value)
|
||||
locationSaving.value = false
|
||||
},
|
||||
(err) => {
|
||||
locationError.value = err.code === 1 ? 'Location permission denied' : err.message
|
||||
locationSaving.value = false
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 15000 },
|
||||
)
|
||||
}
|
||||
|
||||
async function toggleShareLocation() {
|
||||
const next = !shareLocation.value
|
||||
await saveLocation(savedLat.value, savedLon.value, next)
|
||||
}
|
||||
|
||||
async function saveLocation(lat: number | null, lon: number | null, share: boolean) {
|
||||
try {
|
||||
locationSaving.value = true
|
||||
await rpcClient.call({ method: 'server.set-location', params: { lat, lon, share } })
|
||||
// Optimistic update — the next WS state push confirms it, but no need
|
||||
// to wait for that round-trip to reflect the change in the toggle/coords.
|
||||
if (store.serverInfo) {
|
||||
store.serverInfo.lat = lat
|
||||
store.serverInfo.lon = lon
|
||||
store.serverInfo['share-location'] = share
|
||||
}
|
||||
} catch (e) {
|
||||
locationError.value = e instanceof Error ? e.message : 'Failed to save location'
|
||||
} finally {
|
||||
locationSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// mDNS hostname — HTTPS (even self-signed) is required for mic/camera access
|
||||
// (getUserMedia refuses plain HTTP outside localhost); surface both so users
|
||||
// know where to go for features that need it, without forcing HTTPS on anyone.
|
||||
@@ -260,6 +314,41 @@ init()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Sharing Card — opt-in, off by default. Puts this node on
|
||||
OTHER trusted peers' Mesh Map with the Archy logo marker. -->
|
||||
<div class="bg-black/20 rounded-xl px-5 py-4 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg class="w-5 h-5 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">Share Location</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 mb-1">
|
||||
<p class="text-sm text-white/70">Show this node on trusted peers' Mesh Map</p>
|
||||
<button
|
||||
class="glass-toggle"
|
||||
:class="{ active: shareLocation }"
|
||||
role="switch"
|
||||
:aria-checked="shareLocation"
|
||||
:disabled="locationSaving"
|
||||
@click="toggleShareLocation"
|
||||
>
|
||||
<span class="glass-toggle-knob" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-white/50 mt-2">
|
||||
<span v-if="savedLat !== null && savedLon !== null">{{ savedLat.toFixed(3) }}, {{ savedLon.toFixed(3) }}</span>
|
||||
<span v-else class="text-white/30">No location set</span>
|
||||
<button
|
||||
class="ml-auto glass-button px-3 py-1 text-xs"
|
||||
:disabled="locationSaving"
|
||||
@click="useCurrentLocation"
|
||||
>{{ locationSaving ? 'Locating…' : 'Use current location' }}</button>
|
||||
</div>
|
||||
<p v-if="locationError" class="mt-2 text-xs text-yellow-300">{{ locationError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Release Notes Modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
|
||||
Reference in New Issue
Block a user