Files
simon 6090038638 GO 3.0 v0.0.7: live chat catch-up, Hub push, Reticulum status.
Checkpoint before merging upstream Hub v3.0.1.
2026-08-19 13:03:15 +00:00

12 KiB
Raw Permalink Blame History

GO 3.0 — Native Mobile Qortal Client

GO 3.0 (Qortal GO) is the native Android port of 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. 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.

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
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.

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
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
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
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.2v0.0.6 were published to Q-Share. Details are in 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

(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 reopenon_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.
  • 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.
  • 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 for a version-by-version summary of changes.

Upstream

Forked from 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).