mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
docs: cover in-chat share tokens and cards
user-guide gains two sections: how a received card behaves and what each button state means, and how the "+" button shares something. architecture gains "Share Tokens & Cards" — the three wire formats with the byte arithmetic that explains why contacts use a compact token rather than the URI, why parsing has to run before HTML escaping, the per-page hooks the DM iframe needs, and two traps worth not rediscovering: never compose an i18n key (a composed key hides a missing translation from i18n_check.py — that is how share.card.private_channel shipped missing), and cards belong in .message-content, never .message-meta. whatsnew describes both halves for users, in the release voice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e4a98a427e
commit
3e3364bfc2
3 files changed
+94
No files matched your search
@@ -13,6 +13,7 @@ Technical documentation for mc-webui, covering system architecture, project stru
|
||||
- [WebSocket API](#websocket-api)
|
||||
- [Versioning & Releases](#versioning--releases)
|
||||
- [Offline Support](#offline-support)
|
||||
- [Share Tokens & Cards](#share-tokens--cards)
|
||||
|
||||
---
|
||||
|
||||
@@ -511,3 +512,58 @@ Numbering is SemVer read through an operator's eyes: **MAJOR** when a deploy nee
|
||||
## Offline Support
|
||||
|
||||
The application works completely offline without internet connection. Vendor libraries (Bootstrap, Bootstrap Icons, Socket.IO, Emoji Picker) are bundled locally. A Service Worker provides hybrid caching to ensure functionality without connectivity.
|
||||
|
||||
---
|
||||
|
||||
## Share Tokens & Cards
|
||||
|
||||
In-chat sharing of contacts, channels and positions. `app/static/js/share-tokens.js` owns the formats (pure logic, no DOM), `message-utils.js` renders the cards, `share-composer.js` provides the `+` button and pickers.
|
||||
|
||||
### The formats are fixed by interop
|
||||
|
||||
Canonical spec: `docs/qr_codes.md` in the MeshCore Core repo. These are what the official app emits, so they are not open to redesign.
|
||||
|
||||
| Kind | Wire format | Bytes |
|
||||
|---|---|---|
|
||||
| Contact | `<64-hex-pubkey:type:name>` — a compact token, **not** a URI | ~68 + name |
|
||||
| Channel | `meshcore://channel/add?name=&secret=®ion_scope=` | ~96 |
|
||||
| Location | bare `lat,lon`, six decimals | ~20 |
|
||||
|
||||
`type`: 1 = Companion, 2 = Repeater, 3 = Room Server, 4 = Sensor. `secret` is 32 hex characters. `region_scope` is optional (official app v1.47.0+), carried as `#pl` and displayed without the hash.
|
||||
|
||||
**Why contacts do not use the URI form.** `meshcore://contact/add?name=…&public_key=…&type=1` costs **153 bytes** with a name like `MarWoj 💡📱 mobile`, and the channel packet budget is **135** (DM 150). The compact token is 91. The URI form is still parsed — QR codes and "biz cards" carry it — but never emitted.
|
||||
|
||||
### Parsing must precede HTML escaping
|
||||
|
||||
`processMessageContent()` is the single rendering hook shared by channel chat and DMs, but **escaping is its step 0**, and a token cannot survive the passes that follow:
|
||||
|
||||
- escaping rewrites a channel URI's `&` as `&`
|
||||
- `processChannelLinks` turns a `#channel` name inside the URI into a link
|
||||
- `processMentions` consumes an `@` in a contact name
|
||||
- the contact token's closing `>` collides with quote syntax
|
||||
|
||||
So tokens are lifted out **before** escaping and replaced with `\uE000<n>\uE001` placeholders — private-use-area characters that no pass can match and `escapeHtml()` passes through untouched. The finished card markup is spliced back in as the last step, and is therefore trusted HTML that must not be escaped again: every value inside a card is escaped by its builder.
|
||||
|
||||
Cards render into `.message-content` / `.dm-content`, never `.message-meta`, which `refreshMessagesMeta()` rewrites in place after render. Those containers are `white-space: pre-wrap`, so a card resets it and takes a definite width — with only a `max-width`, the shrink-to-fit message bubble makes the inner flex row wrap labels mid-phrase.
|
||||
|
||||
### Per-page environment
|
||||
|
||||
Card state resolution and actions come from `window.MCShareHooks`, installed separately by `app.js` and `dm.js`, because the DM panel is an iframe with no access to the parent's contact caches, channel list or map modal. Every hook is optional: without them a card still renders and falls back to the "not known yet" state, which is safe because the add endpoints are idempotent.
|
||||
|
||||
Hooks: `lookupContact(pubkey)`, `lookupChannel({name, secret})`, `selfPosition()`, `openMap(label, lat, lon)`, `onContactsChanged()`, `onChannelJoined(channel, name)`, `applyChannelScope(channel, scope)`.
|
||||
|
||||
The DM panel carries its own `#shareMapModal` rather than reaching for the parent's map, since stacking a Bootstrap modal over the DM modal across a frame boundary needs the backdrop z-index workaround.
|
||||
|
||||
### Targets
|
||||
|
||||
Contacts are added with `POST /api/contacts/manual-add` and `target: 'cache'` — DB only, no device write, works while disconnected, and reports `updated` / `renamed`. Rename-on-known-key falls out of `Database.upsert_contact`, whose upsert overwrites a non-empty name and keeps `source='device'` sticky. Channels use `POST /api/channels/join`, which needs the secret under the name `key` (the URI calls it `secret`).
|
||||
|
||||
A shared `region_scope` names a region, but scopes are stored by region id, so the name must already exist in the local registry — and the registry starts empty. A region's 16-byte scope key cannot be derived from its name, so an unknown scope raises a toast rather than being invented.
|
||||
|
||||
### Guarding against false positives
|
||||
|
||||
Bare coordinates are the risky format. The pattern requires three or more decimal places on both numbers, validates ranges, rejects `0,0` (the firmware's "unset" marker), refuses matches abutting a word character, and skips anything inside a URL span. Without that, "1,5 osoby" and "wersja 2,10" become location cards and a Google Maps link gets torn in half.
|
||||
|
||||
### i18n note
|
||||
|
||||
Never compose an i18n key (`t('share.type.' + n)`). `scripts/i18n_check.py` cannot resolve it, so a missing translation passes the check and only surfaces in the browser — which is exactly how `share.card.private_channel` shipped missing during development. Spell the key as a literal on every branch.
|
||||
@@ -161,6 +161,18 @@ Archives are created automatically at midnight (00:00 UTC) each day. The live vi
|
||||
|
||||
**On phones and tablets:** pressing Enter inserts a new line instead of sending — tap the **Send** button to publish. This prevents a mistapped Enter on the on-screen keyboard from firing off a half-typed message. On desktop, Enter still sends.
|
||||
|
||||
### Sharing a Contact, Channel or Location
|
||||
|
||||
The **+** button next to the emoji button attaches something to the message you are writing. It offers three things:
|
||||
|
||||
- **Share Contact** — a searchable list of everyone you know, on the device and in the cache alike. **Me** is the first entry, so sharing your own details is one tap. On a busy mesh the list is capped at 150 rows; type to narrow it down.
|
||||
- **Share Channel** — your channels, with each one's region scope shown. The scope travels with the channel, because a recipient transmitting under a different scope would not be heard.
|
||||
- **Share Location** — your own position as a button, or tap the map to pick any other point.
|
||||
|
||||
What gets inserted is plain text in the format other MeshCore clients understand, so the recipient sees a proper card whichever app they use. A preview of that card appears above the input while the text is valid, and you can type a comment around it before sending. If you edit the inserted text and break it, the preview disappears — that is your warning that it will arrive as raw text rather than a card.
|
||||
|
||||
These payloads are long: a shared contact costs around 90 of the message's bytes. The byte counter under the input accounts for it, and anything that cannot fit in one message is refused rather than cut short, since a truncated public key or channel key is worthless.
|
||||
|
||||
### Replying to Users
|
||||
|
||||
Click the reply button on any message to insert `@[UserName]` into the text field, then type your reply.
|
||||
@@ -221,6 +233,30 @@ URLs ending in `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp` are displayed as:
|
||||
|
||||
**Example:** Sending `https://example.com/photo.jpg` shows a thumbnail of the image that can be clicked to view full-size.
|
||||
|
||||
### Shared Contacts, Channels and Locations
|
||||
|
||||
MeshCore clients pass contacts, channels and positions around as ordinary text inside chat messages. mc-webui recognises all three and shows them as cards with a button, instead of the raw text the message actually contains.
|
||||
|
||||
| Shared | Card shows | Button |
|
||||
|---|---|---|
|
||||
| Contact | Name, node type, shortened public key | **Add Contact** |
|
||||
| Channel | Name, whether it is a hashtag or private channel, its region scope | **Add Channel** |
|
||||
| Location | Coordinates, plus distance and bearing from your own position | **View on map** |
|
||||
|
||||
The button always states what will actually happen, worked out before you see it:
|
||||
|
||||
- **Add Contact** — the public key is new to you.
|
||||
- **Update name to X** — you already hold this public key under a different name. The key is the identity, so this renames the contact you have; it never creates a second one.
|
||||
- **Already in contacts** — you have it, with this name, on the device. Nothing to do.
|
||||
- **Push to device** — you have it in the cache. See below.
|
||||
- **Already joined** / **Name used by a different key** — for channels. The second one is a refusal: something else already occupies that name with a different key, and joining would shadow it.
|
||||
|
||||
**Contacts are added to the cache, not to the device.** Shared contacts arrive unsolicited, and device contact memory is limited, so mc-webui does not spend a slot without you deciding to. The contact appears under Contact Management like any contact learned from an advert, and the card then offers **Push to device** so you can promote it on the spot rather than going looking for it. Channels are different: they go straight to the device, because without the channel key in the firmware there is nothing to decrypt messages with.
|
||||
|
||||
A location card also offers a copy button and links to Google Maps, OpenStreetMap, and Apple Maps on Apple devices. Distance and bearing appear only once your own position is set (Settings → Device).
|
||||
|
||||
Coordinates are only recognised when they are unambiguous — both numbers need at least three decimal places, and they must be in valid ranges — so ordinary text like "1,5 km" or "version 2,10" is left alone, as are coordinates inside a link.
|
||||
|
||||
**Note:** All content enhancements work in both channel messages and Direct Messages (DM).
|
||||
|
||||
---
|
||||
|
||||
@@ -12,6 +12,8 @@ For deep technical notes, see [architecture.md](architecture.md). For the full g
|
||||
|
||||
### Features
|
||||
|
||||
- **Contacts, channels and locations shared in chat now look like something you can use.** Other MeshCore clients let you drop a contact, a channel or your position into a conversation, and the mesh carries it as ordinary message text — which is why, until now, it arrived here as a wall of hex: `<e56c230c…3ae729:1:MarWoj mobile>`, unreadable and useless. Those messages now appear as cards with a name, a type and a button, in channel chat and in direct messages alike, including every such message already sitting in your history. The button says what will actually happen before you press it: **Add Contact** when the key is new to you, **Update name to X** when you already hold that key under a different name — the key is the identity, so this renames what you have rather than creating a duplicate — **Already in contacts** when there is nothing to do, and for channels a plain refusal when the name you were sent is already occupied by a different key, instead of quietly shadowing the channel you already had. A shared contact goes into your cache rather than onto the device: it arrived unsolicited, device contact memory is limited, and the card offers **Push to device** right there if you do want to spend a slot. Channels go straight to the device, since without the key in the firmware there is nothing to decrypt with. A shared position becomes a card with **View on map**, a copy button, links out to Google Maps and OpenStreetMap, and — something the official app does not show you — how far away it is and in which direction. Coordinates are only recognised when they are genuinely coordinates, so "1,5 km" in a sentence is still just text.
|
||||
- **You can share those things yourself, with the new + button beside the emoji button.** It offers three: a contact, a channel, or a location. Your own details are the first entry of the contact list rather than a separate menu item, so sharing yourself is one tap, and the same applies to your own position in the location picker — which otherwise lets you tap the map for any point you like. The contact list covers everyone you know, cache included, with a search box because a busy mesh gives you well over a thousand of them. Sharing a channel takes its region scope along, which matters: a recipient transmitting under the wrong scope simply would not be heard. What lands in the message box is the same plain text every other MeshCore client uses, so the card shows up properly whatever app is on the other end, and you can write a comment around it. While the text is valid you get a preview of the card the recipient will see; break the text by editing it and the preview disappears, which is your warning that it will arrive as raw text. These payloads are bulky — a shared contact eats roughly 90 of the message's bytes — so the counter under the box accounts for it, and anything that will not fit in one message is refused rather than trimmed, because half a public key is worth nothing.
|
||||
- **Repeater settings now tell you when a setting simply isn't there, instead of calling it an error.** Repeaters on the mesh do not all run the same firmware or the same hardware, so a setting that exists on one is genuinely absent on another. Until now the panel had only two answers — a value, or a red error — so a perfectly healthy repeater that had never heard of a setting looked broken, and the section reported a failed read. Such a field now gets a grey **not supported** badge with a short explanation, stays locked, and is left out of anything you Apply. The section itself no longer counts it as a failure. There is one honest caveat, and it comes from the firmware rather than from mc-webui: when a repeater rejects a setting *after* having already stored it — which is what newer firmware does with RX boosted gain on radios that cannot do it — "not supported" says nothing about what the device now holds. Press **Refresh** to see the truth.
|
||||
- **Three settings added for repeaters that support them.** **Advertisement** gains two more hop limits alongside the existing one: a separate cap for flooded adverts, and a cap for flood packets carrying no region scope. Both have existed in the firmware for a while but were never exposed here. **Advanced** gains **channel activity detection** (firmware v1.17 or newer): the radio listens before transmitting and holds off if the channel is busy. On older repeaters it appears as *not supported*, as described above.
|
||||
- **A repeater can be powered off from the Actions tab** (firmware v1.17 or newer). This is deliberately not the same kind of button as **Reboot**. A reboot brings the repeater back on its own in a few seconds; a power-off does not, and nothing on the mesh can undo it — the node stays off until somebody physically gets to it, which for a repeater on a mast is a trip rather than a click. So it is not confirmed with a Yes/No box: you have to type the repeater's name to unlock the button, and the server checks that name again before sending anything.
|
||||
|
||||
Reference in new issue
Block a user