feat(mesh): native-unicast DMs, contact import/remove, reachability, contact search

- DMs now use native meshcore unicast (CMD_SEND_TXT_MSG) instead of @DM2 channel
  broadcasts: private (E2E-encrypted to the recipient pubkey by firmware), off the
  public channel, and decodable by stock clients. Plain text (split, not MC-chunked)
  to non-archipelago contacts; typed envelopes to archy peers.
- !ai replies now DM the asker privately (RadioDm) instead of broadcasting on ch0.
- Auto contact-import: a heard advert (PUSH_CONTACT_ADVERT/0x80, 32-byte pubkey) is
  added via CMD_ADD_UPDATE_CONTACT (0x09) so contacts appear without a flood advert.
- clear-all now DELETES firmware contacts via CMD_REMOVE_CONTACT (0x0F) instead of
  blocklisting; blocking filter removed entirely. Wiped contacts return when reachable.
- Contact reachability: MeshPeer carries last_advert + reachable (path-based); UI shows
  a reachability dot.
- Peers list: contact search box (filter by name/DID/npub/pubkey) with a clear button.
- send_message routes stock contacts as plain native text (fixes garbled envelopes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-18 08:08:52 -04:00
co-authored by Claude Opus 4.8
parent 9f2edf6b7a
commit f0fdc23cc9
15 changed files with 578 additions and 95 deletions
@@ -62,6 +62,32 @@ function toggleAllowed(pubkey: string) {
apply({ allowed_contacts: allowedContacts.value })
}
// Manually pasting a raw ed25519 pubkey (hex) — for an allowed asker that
// isn't in the contact list yet (e.g. a phone/meshcore device).
const newPubkey = ref('')
const pubkeyError = ref('')
// Allowlisted keys that aren't one of our known contacts (manually added).
const extraAllowed = computed(() =>
allowedContacts.value.filter(
(k) => !contactOptions.value.some((c) => c.pubkey.toLowerCase() === k.toLowerCase()),
),
)
function addPubkey() {
const pk = newPubkey.value.trim().toLowerCase()
pubkeyError.value = ''
if (!/^[0-9a-f]{64}$/.test(pk)) {
pubkeyError.value = 'Enter a 64-character hex ed25519 public key.'
return
}
if (allowedContacts.value.some((k) => k.toLowerCase() === pk)) {
newPubkey.value = ''
return
}
allowedContacts.value = [...allowedContacts.value, pk]
newPubkey.value = ''
apply({ allowed_contacts: allowedContacts.value })
}
onMounted(() => {
mesh.fetchAssistantStatus()
})
@@ -206,7 +232,28 @@ function onPolicy() {
<input type="checkbox" :checked="isAllowed(c.pubkey)" @change="toggleAllowed(c.pubkey)" />
<span class="mesh-assistant-allow-name">{{ c.name }}</span>
</label>
<!-- Manually-added pubkeys not in the contact list -->
<label
v-for="pk in extraAllowed"
:key="pk"
class="mesh-assistant-allow-row"
>
<input type="checkbox" checked @change="toggleAllowed(pk)" />
<span class="mesh-assistant-allow-name" :title="pk">{{ pk.slice(0, 10) }} (added)</span>
</label>
</div>
<!-- Add an arbitrary pubkey directly -->
<div class="mesh-assistant-addkey">
<input
v-model="newPubkey"
class="mesh-bitcoin-input mesh-bitcoin-input-sm"
placeholder="Paste an ed25519 pubkey (64 hex) to allow"
@keyup.enter="addPubkey"
/>
<button type="button" class="glass-button mesh-bitcoin-input-sm" @click="addPubkey">Add</button>
</div>
<p v-if="pubkeyError" class="text-xs mt-1" style="color:#f87171">{{ pubkeyError }}</p>
</div>
<p class="text-xs text-white/50 mt-2">