200 lines
12 KiB
Markdown
200 lines
12 KiB
Markdown
# GO 3.0 — Native Mobile Qortal Client
|
||
|
||
**GO 3.0** (Qortal GO) is the native Android port of [Qortal Hub](https://github.com/Qortal/Qortal-Hub)
|
||
(this repo tracks Hub with the mobile shell on branch `feature/chat-v2-merge`). It wraps the Hub's
|
||
React UI in a Capacitor/Android shell and adds a **native Reticulum (RNS) stack on the phone**, giving
|
||
mobile users the Hub 2.0 P2P feature set: presence, voice calls, and direct file transfer over
|
||
Reticulum — plus a mobile-first UI, OS notifications, biometric login, and battery-conscious
|
||
background behavior.
|
||
|
||
**Repository:** https://gitea.qortal.link/simon/GO-3.0
|
||
**Current APK:** GO 3.0 **v0.0.6** (`versionName` 0.0.6, `versionCode` 9) — shown in-app as **GO version** (Core & settings).
|
||
|
||
> **For Hub developers**
|
||
>
|
||
> 1. **Chat GIF picker** — copy `src/components/Chat/gifs/` and wire it as described in
|
||
> [`HUB_GIF_PICKER.md`](HUB_GIF_PICKER.md). Same QDN pointers GO already sends; Hub
|
||
> should display and send them the same way.
|
||
> 2. **Voice + file-transfer interop** — two small Hub-side changes, still required.
|
||
> See [Required Hub changes](#required-hub-changes).
|
||
>
|
||
> Everything else is wire-compatible with the existing Hub.
|
||
|
||
---
|
||
|
||
## Repository layout
|
||
|
||
| Path | What it is |
|
||
|---|---|
|
||
| `src/` | Shared React UI (same codebase as desktop Hub, with mobile components added) |
|
||
| `src/bridge/capacitorBridge.ts` | The mobile equivalent of Electron's main-process bridges: installs `window.presence` / `window.call` / `window.groupCall`, presence overlay coordinator, Q-Chat file transfer sender logic |
|
||
| `src/plugins/Reticulum.ts` | Capacitor plugin typings for the native Reticulum bridge |
|
||
| `src/utils/qchatFileApi.ts` | Unified desktop/mobile Q-Chat file transfer API (desktop → `electronAPI`, mobile → Capacitor) |
|
||
| `src/components/Chat/gifs/` | QDN GIF library picker (catalog, cache, masonry, most-used). Hub-portable — see [`HUB_GIF_PICKER.md`](HUB_GIF_PICKER.md) |
|
||
| `src/components/Mobile/` | Mobile shell: top bar, tab bar, tabs overlay with folders, etc. |
|
||
| `src/appUpdate/` | In-app APK updates from Q-Share (`GO 3.0` on QDN) — Android only |
|
||
| `android/` | Capacitor Android project |
|
||
| `android/app/src/main/java/.../ReticulumPlugin.kt` | Native plugin: Kotlin RNS transport (reticulum-kt) + Chaquopy host for the Python bridge + file staging helpers |
|
||
| `android/app/src/main/java/.../QortalForegroundService.java` | Foreground service: persistent status notification, alert/call notifications |
|
||
| `android/app/src/main/python/presence_bridge.py` | The full Reticulum protocol bridge (presence, calls, group audio, file transfer) — **shared with desktop**, runs under Chaquopy on Android |
|
||
| `electron/resources/presence_bridge.py` | The **desktop copy** of the same bridge — kept in sync; contains the file-transfer fixes the Hub needs to ship |
|
||
| `electron/` | Desktop Electron shell (unchanged upstream behavior, plus the bridge fix) |
|
||
|
||
## Building the Android app
|
||
|
||
Prerequisites: Node 20+, Android SDK (API 34), JDK 17.
|
||
|
||
```bash
|
||
npm install
|
||
npx vite build && npx cap copy android
|
||
cd android && ./gradlew assembleDebug
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
|
||
The Python bridge (`presence_bridge.py`) and vendored RNS ship inside the APK via Chaquopy; no
|
||
on-device Python setup is needed.
|
||
|
||
## Mobile architecture in one paragraph
|
||
|
||
The WebView runs the same renderer as desktop Hub. Where desktop talks to Electron's main process
|
||
(`window.electronAPI`, IPC to `reticulum-daemon.ts`), mobile installs equivalent bridges from
|
||
`capacitorBridge.ts` that talk to `ReticulumPlugin.kt`, which hosts the *identical*
|
||
`presence_bridge.py` in an embedded CPython (Chaquopy). Commands flow as JSON frames
|
||
(`presenceCommand` with request/response correlation); events stream back over a single `presence`
|
||
listener. The result: the phone speaks the exact same RNS wire protocol as the Hub — same presence
|
||
envelopes, same call signaling, same `QGAU` audio framing, same `QGCCTL1` link auth, same file
|
||
transfer Resources.
|
||
|
||
## Feature status (mobile ↔ Hub interop)
|
||
|
||
| Feature | Status |
|
||
|---|---|
|
||
| Presence (announce/heartbeat over RNS overlay) | ✅ Works both ways |
|
||
| Q-Chat (blockchain chat) | ✅ Works |
|
||
| Voice calls **Hub → mobile** | ✅ Works — two-way audio over verified RNS link |
|
||
| Voice calls **mobile → Hub** | ⚠️ Blocked by a Hub-side ordering precondition — see [Hub change #1](#1-voice-calls-admit-verified-link-auth-joins) |
|
||
| Q-Chat file transfer **mobile → Hub** | ✅ Works (mobile sender has the robustness fixes) |
|
||
| Q-Chat file transfer **Hub → mobile**, large files | ⚠️ Reliable only after the Hub ships [Hub change #2](#2-file-transfer-ship-the-updated-presence_bridgepy) |
|
||
| OS notifications (PM summary + sender avatar), biometric login, mobile tabs/folders | ✅ Mobile-only features |
|
||
| Chat GIF library (QDN `gif-` IMAGE pointers) | ✅ Works on GO. Hub: copy picker + send/render — [`HUB_GIF_PICKER.md`](HUB_GIF_PICKER.md) |
|
||
| In-app APK updates from Q-Share | ✅ Mobile-only (checks `GO 3.0` on QDN) |
|
||
| GO version in Core & settings | ✅ Matches `versionName` / this APK |
|
||
|
||
---
|
||
|
||
## Since the last Gitea push (v0.0.1 → v0.0.6)
|
||
|
||
Gitea `main` was last updated **2026-08-09** at `0bb0c74a` (v0.0.1 rebrand). Field
|
||
builds **v0.0.2–v0.0.6** were published to Q-Share. Details are in
|
||
[`CHANGELOG.md`](CHANGELOG.md).
|
||
|
||
| Version | What landed |
|
||
|---|---|
|
||
| **v0.0.2** | Chat reply/draft fixes, Q-Manager file save, SSH-tunneled local nodes, group overlays / calendar |
|
||
| **v0.0.3** | Compact composer (staged grow, fade-in send, emoji panel in the keyboard slot), unlock/stability fixes, first in-app updater |
|
||
| **v0.0.4** | Updater finishes after the Android install permission; large Q-Share saves no longer crash; QDN download progress notifications |
|
||
| **v0.0.5** | Chat GIF library (search, categories, most-used, masonry, queued cache); settings label is **GO version** and matches the APK |
|
||
| **v0.0.6** | First-session chat sync, faster unlock (HD off the UI thread), QDN downloads on a remote core, login runtime overlay |
|
||
|
||
---
|
||
|
||
## Required Hub changes
|
||
|
||
### 1. Voice calls: admit verified link-auth joins
|
||
|
||
*(See `MOBILE_VOICE_HUB_DEV_NOTE.md` in this repo for the full write-up.)*
|
||
|
||
**Symptom:** Hub → mobile calls connect with clean two-way audio. Mobile → Hub calls stay on
|
||
"connecting…" on the Hub and drop after a few seconds — even though the phone sends the exact same
|
||
signed link auth the Hub accepts in the other direction.
|
||
|
||
**Root cause (traced in `electron/src/group-call.ts`):** it is *not* a protocol or signature
|
||
problem — the `GC_JOIN` verifies. It's an ordering precondition:
|
||
|
||
- The audio-link owner is chosen by address ordering (`isLocalAddressReticulumAudioLinkOwner`),
|
||
not by who placed the call.
|
||
- Inbound link audio is dropped unless the link is marked verified (`audio-unverified-address`).
|
||
- `applyVerifiedReticulumLinkAuthJoin` only marks a link verified **if the sender is already in
|
||
`room.participants`** at that instant.
|
||
|
||
When the phone initiates, its verified link-auth `GC_JOIN` arrives *before* the Hub has registered
|
||
the caller as a participant, so the Hub closes the link (`link-auth-no-participant`), drops the
|
||
audio, and never leaves "connecting". In the Hub-initiated direction the participant state is
|
||
already in place, so the identical handshake succeeds.
|
||
|
||
**The change:** in `applyVerifiedReticulumLinkAuthJoin`, when the signed `GC_JOIN` **verifies** and
|
||
the sender is a valid Qortal member of the call, **admit the participant from the verified join**
|
||
instead of bailing with `link-auth-no-participant`. The verified join is itself authenticated proof
|
||
of membership (Ed25519 over `buildGcJoinSignedFields`, already checked), so this removes the race
|
||
without weakening security. No changes to wire format, audio framing, signing, key delivery, or the
|
||
link-owner rule; Hub-initiated calls are unaffected.
|
||
|
||
### 2. File transfer: ship the updated `presence_bridge.py`
|
||
|
||
**Symptom:** large transfers (e.g. ~80 MB Hub → mobile) stall at exactly the first window of
|
||
parallel chunks (8 × 1 MB) and the retry restarts from zero.
|
||
|
||
**Root cause:** the per-chunk `QCHAT_FILE_CHUNK_ACK` is a single link packet with no retry. Under
|
||
congestion a lost ACK caused:
|
||
|
||
1. the sender's 90 s ACK timeout to **fail the whole transfer** — and the un-ACKed chunk was never
|
||
re-queued (`next_chunk_index` had already advanced → permanent hole in the file);
|
||
2. dead links were never replaced, so the 8-link pool only shrank;
|
||
3. re-accepting restarted the download from byte 0 (and could never fill the holes anyway).
|
||
|
||
**The change (already implemented in this repo's `electron/resources/presence_bridge.py`,
|
||
mirrored in the Android copy):**
|
||
|
||
- **`retry_chunks` queue** on the send root — an ACK timeout or mid-chunk link failure re-queues
|
||
the chunk (consumed with priority by `_start_qchat_file_resource_for_state`) and emits
|
||
`retrying` instead of `failed`; only the suspect link is torn down.
|
||
- **Receiver link reopen** — `on_qchat_file_link_closed` opens a replacement link while a receive
|
||
is pending (the receiver is the link initiator; bounded by
|
||
`_QCHAT_FILE_RECEIVER_MAX_LINK_REOPENS = 64`).
|
||
- **Resume** — the receiver persists a `<savePath>.part.meta` ledger (transferId, size, sha256,
|
||
completed chunks) per chunk; a re-accept preloads it and resumes instead of restarting. The meta
|
||
is removed on completion, and both `.part` and meta are discarded on a final hash mismatch.
|
||
|
||
**What to do:** include this file in the next Hub release — nothing else changes. No new packet
|
||
types; fully wire-compatible with old peers. The sender-side re-queue is the critical half, so
|
||
Hub → mobile transfers of large files only become reliable once the Hub ships it.
|
||
|
||
---
|
||
|
||
## Mobile-specific work in this fork (overview)
|
||
|
||
- **Q-Chat file transfer on mobile:** native file staging (`qchatFilePrepare` copies content://
|
||
URIs to app cache + SHA-256), auto save paths, export of completed downloads to public
|
||
Downloads (MediaStore), an **Open** button on received files, renderer-side Ed25519 verification
|
||
of downloader link auth, pending-send persistence across app restarts.
|
||
- **Notifications:** single in-place-updating direct-message summary notification
|
||
("You got N messages from X and Y") with the sender's avatar (fetched in the WebView and passed
|
||
as a data URL — node MIME quirks and OEM skin differences handled), MIUI-aware status
|
||
notification branding, in-app Qortino notifications with auto-dismiss countdown.
|
||
- **Performance/battery:** deduped + rate-limited foreground service updates (was ~7 native
|
||
calls/s, now ≤0.2/s), in-memory cache for hot secure-storage keys (−73% bridge reads),
|
||
visibility-aware polling (Reticulum status, balance, dashboard), WASM bcrypt wallet KDF
|
||
(verified byte-identical to bcryptjs), lazy i18n locales (EN + active language only), avatar
|
||
fetch outcome caching.
|
||
- **Mobile UX:** tab bar + tabs overlay with folders, app library list layouts, biometric unlock
|
||
flow (no keyboard pop on autofill), blocked-accounts flows for public/private nodes, Android
|
||
back-button handling, DM voice call UI, drag-to-reorder accounts (long-press lift with
|
||
neighbour slide animation, portrait + landscape, phones + tablets).
|
||
- **Payments:** `qortal://pay?…` / `qort:` deep links and payment QR codes open the app straight
|
||
to a pre-filled Send screen; a de-Googled in-app QR scanner (`getUserMedia` + `jsQR`, no Play
|
||
Services) reads payment and address QRs. See [`docs/PAYMENT_QR_CODES.md`](docs/PAYMENT_QR_CODES.md).
|
||
- **Chat GIF library:** emoji button also opens a QDN GIF picker (`gif-` IMAGE resources).
|
||
Search, tag chips, most-used, masonry layout, queued/cached loads. Sent as a QDN pointer,
|
||
not a mesh file. Hub port: [`HUB_GIF_PICKER.md`](HUB_GIF_PICKER.md).
|
||
- **In-app updates:** watches Q-Share publications under the `GO 3.0` name, shows notes +
|
||
progress, and hands the APK to Android's installer.
|
||
- **GO version:** Core & settings shows the same `versionName` as the installed APK.
|
||
|
||
See [`CHANGELOG.md`](CHANGELOG.md) for a version-by-version summary of changes.
|
||
|
||
## Upstream
|
||
|
||
Forked from [Qortal/Qortal-Hub](https://gitea.qortal.link/Qortal/Qortal-Hub) — see that repo for
|
||
the desktop Hub documentation, i18n guidelines (`docs/i18n_languages.md`), and development docs
|
||
(`docs/development.md`). License: GPL-3.0 (unchanged).
|