# 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 `.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).