Skip to content

Tested against iOS and Android#112

Open
kushaldas wants to merge 7 commits intomainfrom
mobile
Open

Tested against iOS and Android#112
kushaldas wants to merge 7 commits intomainfrom
mobile

Conversation

@kushaldas
Copy link
Copy Markdown
Member

Yet to try login in Android. But, from now on we must update any UI for mobile too.

kushaldas and others added 5 commits April 19, 2026 20:20
Adds a viewport-branched mobile UI (< 720 px) without touching the
desktop layout. Desktop  1024 px renders identical output  all
mobile work lives behind the new DesktopShell / MobileShell split in
App.vue and a data-platform attribute on <html>.

New:
- src/stores/platform.ts  breakpoint + iOS/Android detection
- src/components/shell/{DesktopShell,MobileShell}.vue
- src/components/mobile/{MobileTabBar,MobileAppBar,
MobileIconButton,FolderDrawer,ComposeSheet}.vue
- src/views/MobileThreadView.vue  /mail/thread/:id detail
- src/views/OnboardingView.vue  zero-account first-run
Completes the Tauri mobile scaffold from the previous commit. The app now
builds and runs on an Android emulator and the iOS simulator, showing the
onboarding screen.

- Split src-tauri into lib.rs + main.rs: `run()` is the mobile_entry_point,
main.rs delegates. Declared `chithi_lib` [lib] with staticlib/cdylib/rlib
crate-types so `cargo build --lib --target *-android/*-apple-ios*` works.

- Move data-dir resolution into the Tauri .setup() closure and use
`app.path().app_data_dir()` on mobile. The old `dirs::data_local_dir()`
path is not writable inside the Android app sandbox and was panicking at
boot with EACCES.

- Add `openssl = { vendored }` for `cfg(target_os = "android")` so the IMAP
/ reqwest / lettre TLS stack can cross-compile without a system OpenSSL.
(iOS uses Security.framework via native-tls, so no change needed there.)

- vite binds to 0.0.0.0 when TAURI_DEV_HOST is unset so that
`adb reverse tcp:1420 tcp:1420` can reach it from the emulator. IPv6-only
localhost binding was silently breaking the mobile dev loop.

- Initialized src-tauri/gen/{android,apple} via `pnpm tauri {android,ios}
init`.

- Pin @rolldown/binding-darwin-arm64 so pnpm installs the native binding
rolldown needs on macOS arm64 (the optional dep was being skipped).
…s screens

Implements PATCHES-MOBILE sections 6-15 on top of the mobile chrome scaffold:

- MessageListItem gets a `mode="mobile"` branch with 2-line comfortable
rows, per-account corner dots, and swipe-to-archive/delete gestures;
MessageList forwards the mode and wires the archive/delete handlers.
- New MobileThreadView -- app bar with back/archive/trash/overflow,
per-account chip in the subject block, reused MessageReader body,
and a sticky Reply / Reply-all / Forward action dock.
- ContactsView mobile branch: large-title bar, account filter chips,
sticky letter headers, per-account corner dots, edge index rail.
- CalendarView mobile branch: Day / Week / Month views with
calendar-colored events, now-indicator, month cells and today list.
- SettingsView mobile branch: large-title bar, sectioned cards
(Accounts / General / Privacy / About), dashed "Add account"
button, and the edit-account modal restyled as a bottom sheet.
- uiStore.openCompose now takes { replyTo, kind } so the thread view
can launch Reply / Reply-all / Forward with the right seed state.
- Status-bar theming on both platforms (iOS Info.plist
UIStatusBarStyle; Android themes.xml + colors.xml).

Fixes surfaced while verifying on device:

- SettingsView.vue was missing its `</style>` closing tag at EOF, so
the SFC parser reported "Element is missing end tag" at the <style>
line (not a parser bug, just an unclosed block).
- The Add/Edit and Delete Teleports lived inside the `v-else` desktop
branch, so they never mounted on mobile -- pulled up to the template
root so both chromes share them.
- The router guard redirected `/settings?addAccount=<provider>` back
to `/onboarding` on first run (zero accounts), making the provider
tap look like a no-op. Whitelisted the hand-off query and taught
SettingsView to auto-open the new-account form with the matching
provider preselected.
…works on iOS

tauri-plugin-shell::open spawns a subprocess via the ::open crate, which
on iOS has no /usr/bin/open to spawn and fails with "No such file or
directory (os error 2)". The shell plugin is also explicitly deprecated
since 2.1.0 in favour of tauri-plugin-opener, which bridges to
UIApplication.open(_:) on iOS and Intent.ACTION_VIEW on Android.

Swap:

- src-tauri/Cargo.toml, src-tauri/src/lib.rs: tauri-plugin-shell ->
tauri-plugin-opener
- src-tauri/capabilities/default.json: replace "shell:allow-open" with
"opener:allow-open-url" + "opener:allow-default-urls" (the opener
plugin denies by default; the default-urls preset scopes it to
http/https/mailto/tel which is what the OAuth flows need)
- package.json: @tauri-apps/plugin-shell -> @tauri-apps/plugin-opener
- src/views/SettingsView.vue: import openUrl from plugin-opener, swap
the three OAuth entry points (Google, Microsoft, JMAP OIDC) to call
it. Renamed a clashing local openUrl variable to verificationUrl.

Also quiet an unrelated noise surfaced by verifying the flow:
db::accounts was warn!-logging on every startup for OAuth-only accounts
because it unconditionally tried to read an IMAP/SMTP password from the
keyring under the non-.oauth service and those entries never exist for
JMAP+OIDC or Microsoft 365 accounts. keyring::get_password now returns
Result<Option<String>> so the caller can distinguish "no entry" (debug)
from a real keyring failure (warn)
CI's rustfmt breaks the retrieval-error format! across multiple lines;
ours didn't. Match CI so the fmt job passes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a mobile-first UI shell and onboarding flow, and adjusts the Tauri backend/project setup to support mobile (iOS/Android) builds and platform detection.

Changes:

  • Add mobile UI chrome (tab bar, app bar, drawer, compose sheet scaffold) and a pushed mobile thread reader route.
  • Add onboarding route + router guard redirecting first-run users without accounts.
  • Refactor Tauri entrypoint into a library (chithi_lib::run) and add mobile-support plugins/capabilities (os/opener) plus generated iOS/Android project files.

Reviewed changes

Copilot reviewed 70 out of 112 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
vite.config.ts Change dev server host binding (mobile testing related).
src/views/OnboardingView.vue New onboarding provider picker UI.
src/views/MailView.vue Add mobile mail layout + app bar + FAB compose.
src/stores/ui.ts Add mobile UI state (compose sheet, drawer, compose context).
src/stores/platform.ts New platform/breakpoint store for mobile vs desktop shells.
src/router/index.ts Add onboarding + mobile thread route + first-run guard.
src/main.ts Initialize platform store.
src/components/shell/MobileShell.vue New mobile shell wrapper + tab bar/drawer/sheet composition.
src/components/shell/DesktopShell.vue New desktop shell wrapper extracted from App.vue.
src/components/mobile/* New mobile components (tab bar, app bar, icon button, drawer, compose sheet).
src/components/mail/MessageList.vue Add mobile mode + swipe archive/delete wiring.
src/assets/styles/main.css Add mobile CSS tokens and mobile overrides.
src/App.vue Switch to platform-driven DesktopShell/MobileShell.
src-tauri/src/main.rs Delegate to library entrypoint for mobile compatibility.
src-tauri/src/lib.rs New unified Tauri run() with mobile plugins + mobile-safe data dir.
src-tauri/src/keyring.rs Make missing keyring entry non-fatal via Option return.
src-tauri/src/db/accounts.rs Update keyring password retrieval handling.
src-tauri/capabilities/default.json Update permissions for opener + os platform detection.
src-tauri/Cargo.toml / Cargo.lock Add lib crate type + new plugins + android vendored OpenSSL.
src-tauri/gen/* Add generated iOS/Android project scaffolding and schemas/capabilities updates.
pnpm-lock.yaml / package.json Add new Tauri plugins and additional dev dependency.
Files not reviewed (2)
  • pnpm-lock.yaml: Language not supported
  • src-tauri/gen/apple/chithi.xcodeproj/project.xcworkspace/contents.xcworkspacedata: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +24
if (hideTabBarRoutes.has(String(route.name))) return false;
// The reader detail route uses `/mail/thread/:id` (name: "reader")
if (route.path.startsWith("/mail/thread/")) return false;
return true;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment says the thread detail route name is "reader", but the router registers it as name: "mobile-reader". Update the comment to match the actual route name to avoid confusion during future routing changes.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +39
const archive = foldersStore.folders.find(
(f) => f.path.toLowerCase() === "archive" || f.name.toLowerCase() === "archive",
);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mobile swipe-archive looks for an Archive folder by matching path/name to the literal string "archive". The backend already normalizes special folders via folder_type (e.g., folder_type === "archive"), which is more reliable across providers/locales and avoids missing archive folders like "All Mail"/"Archiv". Consider using folder_type (and traversing children if needed) for this lookup.

Copilot uses AI. Check for mistakes.
Comment thread src/router/index.ts
Comment on lines +61 to +66
// Redirect to onboarding if no accounts are configured. The guard only
// runs on the main window — standalone compose/reader windows have
// their own param-driven initialization and should pass through.
router.beforeEach(async (to) => {
if (to.name === "onboarding") return true;
if (to.name === "compose" || to.name === "reader") return true;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR metadata discrepancy: the title says "Tested against iOS and Android", but the description says Android login hasn’t been tried yet. Please either update the title/description to be consistent or add a brief note of what was actually validated on Android (e.g., build runs, navigation, login, sync).

Copilot uses AI. Check for mistakes.
Comment thread vite.config.ts
port: 1420,
strictPort: true,
host: host || false,
host: host || "0.0.0.0",
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.host now defaults to "0.0.0.0" when TAURI_DEV_HOST is unset, which binds Vite to all interfaces and can unintentionally expose the dev server on the local network. Consider keeping the previous default (false/localhost) and only using 0.0.0.0 when explicitly requested (env var/CLI flag) for mobile testing.

Copilot uses AI. Check for mistakes.
Comment thread src/stores/platform.ts
Comment on lines +29 to +33
const mod = await import("@tauri-apps/plugin-os");
const p = mod.platform();
if (p === "ios") kind.value = "ios";
else if (p === "android") kind.value = "android";
else kind.value = "desktop";
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tauri-apps/plugin-os APIs (including platform()) are async; const p = mod.platform(); will be a Promise, so the comparisons will never match and kind will fall back to desktop. Await the call (or import the named platform function and await platform()), then compare the resolved string.

Copilot uses AI. Check for mistakes.
Comment thread package.json
},
"packageManager": "pnpm@10.27.0",
"devDependencies": {
"@rolldown/binding-darwin-arm64": "1.0.0-rc.13",
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding @rolldown/binding-darwin-arm64 as a normal devDependency is platform-specific and will likely break installs/CI on non-macOS-arm64 environments. If a binding is needed, it should be an optional dependency handled by the bundler, or gated via optionalDependencies/postinstall rather than being required for every platform.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +67
<div class="folder-drawer" :class="{ open: drawerOpen }" aria-hidden="false">
<div class="scrim" @click="onScrimClick" />
<aside class="pane" role="dialog" aria-label="Folders">
<header class="brand">
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drawer container is always marked aria-hidden="false", even when closed. This makes the hidden dialog content reachable to assistive tech. Bind aria-hidden to !drawerOpen (or use v-if/inert) and consider adding aria-modal="true" on the dialog element when open.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +49
<nav class="mobile-tab-bar" role="tablist" aria-label="Primary">
<button
v-for="t in tabs"
:key="t.id"
class="mobile-tab"
:class="{ active: active === t.id }"
role="tab"
:aria-selected="active === t.id"
:aria-label="t.label"
@click="onTabClick(t)"
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MobileTabBar uses role="tablist"/role="tab", but these roles imply a tabpanel relationship and specific keyboard interactions that aren’t implemented here (this is primary navigation). Consider using semantic navigation (e.g., <nav> with buttons/links) and aria-current="page" for the active item instead of tab roles.

Copilot uses AI. Check for mistakes.
Three issues were stacking up: on Android the token poll would bail the
moment the system backgrounded the app for Chrome; on both mobile targets
the tokens that *did* arrive were silently dropped before sync; and on
the iOS simulator the verification URL never opened at all.

- oauth.rs: the device-code poll now treats any reqwest `.send()` error
as transient and keeps retrying until the overall device-code
deadline. `.send()` cannot return HTTP status errors (those flow
through `resp.status()` below), so the only thing it can hand us are
network/body errors -- and on mobile those are virtually always the
OS tearing down an in-flight socket while the app is backgrounded.
Logs the error kind flags at INFO so future non-transient cases are
identifiable.

- commands/oauth.rs + MainActivity.kt + app/build.gradle.kts: new
`open_oauth_url` command. On Android it JNIs through
`webview.with_webview(...).jni_handle().exec(...)` into a Kotlin
`openCustomTab` on MainActivity that fires an
`androidx.browser.customtabs.CustomTabsIntent`. The tab overlays the
task instead of replacing it with Chrome, so the app stays foreground
and the poll loop never gets suspended. Requires
`androidx.browser:browser:1.8.0` and the `jni` crate under the
Android target.

- oauth.rs: Android file-backed fallback for `store_tokens` /
`load_tokens` / `delete_tokens` under `app_data_dir/oauth_tokens/`.
keyring-rs v3 has no Android backend and silently falls back to its
non-persistent mock -- every `Entry::new` gets a fresh in-memory
slot, so the temp->real account-id migration in
`commands::accounts::add_account` was erasing the tokens it had just
been handed. The Android app sandbox scopes that directory to the
app UID; we can swap in EncryptedSharedPreferences later without
changing callers. Wired via `oauth::init_token_store(&data_dir)`
from the setup hook in lib.rs.

- SettingsView.vue: dispatch the verification URL through
`api.openOauthUrl` only on Android (Custom Tabs path); iOS and desktop
stay on the JS `@tauri-apps/plugin-opener` `openUrl`, which already
goes through UIApplication / OS defaults correctly. The Rust
free-function equivalent shells out to `uiopen` on iOS, which does
not exist on the simulator -- that's what was making the iOS retry
silently drop every poll.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants