Files
multica/apps/mobile
Naiyuan Qing 6b2097ccbb feat(inbox): archived notifications sub-view (MUL-3736) (#5518)
Adds an "Archived" sub-view to the Inbox, reachable from an entry at the
bottom of the main list, with per-row unarchive. Mirrors chat's archived
sub-view so the two surfaces share one mental model.

Backend:
- GET /api/inbox/archived and POST /api/inbox/{id}/unarchive. Kept off the
  existing GET /api/inbox so installed clients keep their contract and the
  unbounded archive never rides along with the main list.
- The archived query excludes any issue that still has an active row. Archiving
  is issue-level, so a new notification on an archived issue leaves old archived
  rows beside a fresh active one — without the guard the issue renders in BOTH
  lists. The exclusion lives in SQL so neither list depends on the other's cache.
- Unarchive is issue-level (mirroring archive) and leaves `read` untouched, so a
  restored unread item raises the unread badge again.
- v1 ships no pagination: LIMIT 200, newest-first, so truncation drops the
  oldest rows and never hides a group's newest one.
- inbox:unarchived event, fanned out to the recipient like the other personal
  inbox events.
- Two CONCURRENTLY-built indexes; inbox_item previously had none covering
  workspace/archived/created_at.

Frontend:
- Separate TanStack cache per list; every inbox event invalidates the workspace
  prefix, since any of them can move an item across the boundary.
- View persisted as ?view=archived, so refresh, back/forward, and the mobile
  detail-back all return to the list the user was in.
- Batch actions stay main-view only — they archive from the MAIN inbox, so
  offering them over the archived list would do the opposite of what it reads.
- Mobile subscribes to inbox:unarchived (its list gains the restored row); its
  own archived view remains follow-up.

Known debt: no pagination, so an archive past ~200 rows is truncated silently
in the UI; the entry's count is the deduplicated count of the rows returned.

Verified: pnpm typecheck/test/lint (0 errors), go build/vet, Go inbox suite
against a real Postgres, migrations up+down, and EXPLAIN confirming both new
indexes serve the query.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-16 14:58:42 +08:00
..

Multica Mobile (iOS)

Expo + React Native iOS client for Multica. Independent from web/desktop — shares only types from @multica/core/. See CLAUDE.md for the locked tech-stack baseline and import rules.

Just want to use it on your phone? (no development)

Multica isn't on the App Store yet — until that changes, anyone who wants it on their iPhone builds from source. One command:

pnpm ios:mobile:device:prod:release

This connects to the same backend as multica.ai, so your existing account just works.

Prerequisites: Mac with Xcode, a free Apple ID added under Xcode → Settings → Accounts, iPhone connected via USB with Developer Mode enabled. Walk through Expo's Set up your environment (pick Development build → iOS Device) if any of that is missing.

Xcode signs the build with the "Personal Team" your Apple ID automatically owns — created silently the first time you signed into Xcode, no setup needed. The first build downloads CocoaPods + compiles React Native from source — expect 1020 minutes. Subsequent builds reuse Xcode's cache.

If Xcode rejects signing with "No matching provisioning profiles found" — rare, happens if someone has claimed the default bundle id ai.multica.mobile on Apple's developer portal. Pick any reverse-domain you own and re-run:

export EXPO_BUNDLE_IDENTIFIER_PROD=com.yourname.multica
pnpm ios:mobile:device:prod:release

7-day signing limit: a free Apple ID signs builds for 7 days. After that, plug back into the Mac and re-run the command to re-sign. An Apple Developer Program account ($99/yr) extends this to 1 year.

Everything below is for app developers — you can ignore the rest if you only wanted a personal install.

Scripts

Command What it does Backend
pnpm dev:mobile Metro only (reuse existing install) local (.env.development.local)
pnpm dev:mobile:staging Metro only (reuse existing install) staging (.env.staging)
pnpm dev:mobile:prod Metro only (reuse existing install) production (.env.production)
pnpm ios:mobile Full rebuild + install on iOS Simulator, Debug local
pnpm ios:mobile:staging Full rebuild + install on iOS Simulator, Debug staging
pnpm ios:mobile:prod Full rebuild + install on iOS Simulator, Debug production
pnpm ios:mobile:device Full rebuild + install on USB iPhone, Debug local
pnpm ios:mobile:device:staging Full rebuild + install on USB iPhone, Debug staging
pnpm ios:mobile:device:staging:release Full rebuild + install on USB iPhone, Release (standalone) staging
pnpm ios:mobile:device:prod Full rebuild + install on USB iPhone, Debug production
pnpm ios:mobile:device:prod:release Full rebuild + install on USB iPhone, Release (standalone) production

dev:* runs Metro only — assumes the matching variant is already installed. ios:mobile* does a full native rebuild + install.

Bundle id and display name switch on APP_ENV (see app.config.ts), so Dev / Staging / Production variants can coexist on the same device or simulator.

First-time setup

.env.staging is committed (public staging URL). .env.development.local is gitignored — copy the template once:

cp apps/mobile/.env.example apps/mobile/.env.development.local
# then edit EXPO_PUBLIC_API_URL inside it to your Mac's LAN IP, e.g. http://192.168.1.42:8080

If your Apple ID isn't on the Multica Apple Developer team yet, also uncomment and set EXPO_BUNDLE_IDENTIFIER_DEV to a reverse-domain you own (e.g. com.yourname.multica.dev). This only overrides the dev variant — staging / production bundle ids are intentionally not overridable so variants can coexist.

Build it onto your iPhone

Two paths, depending on what you want to do:

Day-to-day development (Mac in front of you)

pnpm ios:mobile:device:staging

Produces a Debug build with expo-dev-launcher embedded. Every launch the app probes Metro on your Mac and pulls fresh JS — perfect for hot-reload, painful when the Mac is asleep or you're on a different WiFi.

Standalone / "just use it" (walk away from the Mac)

pnpm ios:mobile:device:staging:release

Produces a Release build. No expo-dev-launcher, no Metro probe, no "Downloading…" screen. Splash → app, exactly like an App Store install. Trade-off: every JS change requires re-running this command.

Both paths share the same prerequisites: Mac with Xcode, free Apple ID added under Xcode → Settings → Accounts, iPhone connected via USB with Developer Mode enabled. Follow Expo's Set up your environment — pick Development build → iOS Device — if any of that is missing.

First build of either variant downloads CocoaPods + compiles React Native from source — expect 10-20 minutes. Subsequent builds reuse Xcode's DerivedData cache.

Try it in the iOS Simulator (no iPhone needed)

pnpm ios:mobile:staging

Boots the simulator, builds, installs the dev-client. Faster to iterate than a device build because no signing / provisioning step. Same dev:mobile:staging Metro flow afterward.

7-day signing limit (device only)

A free Apple ID signs builds for 7 days only, Debug and Release both. After that the app refuses to launch on the iPhone. Plug back into the Mac and re-run the corresponding ios:mobile:device* script to re-sign. Simulator builds are unaffected. The only workaround for the device limit is an Apple Developer Program account ($99/yr), which extends to 1 year.

Pointing at a different backend

Edit EXPO_PUBLIC_API_URL in .env.staging, .env.production, or .env.development.local (whichever variant you're running). Then:

  • For an installed Debug build: restart Metro (pnpm dev:mobile:staging) so the next JS bundle picks up the new value.
  • For an installed Release build: re-run the ios:mobile:device:staging:release command — the value is baked into the embedded bundle at build time.

For local backend testing, use your Mac's LAN IP (ipconfig getifaddr en0), not localhost.