Skip to content

feat(mobile): persist + restore open tabs, fix language change + flaky relaunch test#42

Merged
JohnMcLear merged 6 commits into
mainfrom
feat/mobile-tab-state-persistence
May 12, 2026
Merged

feat(mobile): persist + restore open tabs, fix language change + flaky relaunch test#42
JohnMcLear merged 6 commits into
mainfrom
feat/mobile-tab-state-persistence

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Three real-device-feedback fixes on PR #37's heels (waits for #37 to merge first; this branch was cut off Phase 7):

  1. Tab/workspace persistence on mobile. Closing the app and reopening it now restores the open pads + last-active tab + last-active workspace. New `tab-persistence.ts` writes `{tabs, activeTabId, activeWorkspaceId}` to Capacitor Preferences on every mutation (debounced 150ms). `state.getInitial()` awaits `loadFromStorage()` so the shell's first `onTabsChanged` subscription sees the restored set.

  2. Language change actually applies. Mobile's `events.onSettingsChanged` was a noop, so saving a new language never threaded through to `applySettings` / `setLanguage` / the iframe's `?lang=` param. Now `settings-store` has an `onChanged` emitter and `CapacitorPlatform` routes the subscription through it.

  3. Flaky desktop e2e `restore-on-relaunch.spec.ts` timeout: 30s → 60s. Has flaked on ~50% of recent PR CI runs; reruns succeed at the same 30s. Happy path still completes in 8-12s.

Shell surface change

`InitialState` gains optional `activeWorkspaceId`. `App.tsx` prefers it over `workspaceOrder[0]` when present and valid. Desktop main process doesn't populate it (yet) so desktop behaviour is unchanged — the field is informational only when omitted.

Test plan

  • `pnpm typecheck` + `pnpm test` clean (204 shell + 285 desktop + 8 mobile)
  • APK installed on C62 — manual: open pad → kill app → reopen → see same pad
  • CI green on `pnpm test:e2e` (especially the desktop relaunch test no longer flakes)

🤖 Generated with Claude Code

JohnMcLear and others added 4 commits May 12, 2026 09:04
… + X-Frame fallback + https:// auto-prefix

Six device-test issues from real-hardware run:

1. Dialogs (AddWorkspace / OpenPad / etc.) overflowed phone viewports —
   DialogShell width now `min(${width}px, calc(100vw - 16px))` so a 420
   panel caps at viewport-16 on phones. Tighter padding under 480px.
2. Bare hostname URLs were rejected — AddWorkspaceDialog auto-prefixes
   `https://` when the input has no `://` scheme.
3. (Same as #1 — OpenPadDialog uses DialogShell.)
4. "Black screen" on pad open — likely X-Frame-Options DENY/SAMEORIGIN
   from the server. PadIframeStack now starts a 6s timeout per iframe;
   if no onLoad fires, an opaque overlay surfaces with an "Open in
   browser" button that hands the URL off to @capacitor/browser.
5. Status bar overlapped the rail — shell-root-wrapper now applies
   `env(safe-area-inset-*)` padding. Zero visual change on desktop;
   stops mobile WebView from drawing under the status bar / notch.
6. Newly added workspaces didn't appear in the rail — mobile platform's
   `events.onWorkspacesChanged` was a noop. workspace-store now owns
   a tiny listener Set; CapacitorPlatform wires it through so the
   shell's `onWorkspacesChanged` handler in App.tsx receives the new
   `{workspaces, order}` after every add / update / remove / reorder.

DialogShell width-test dropped (jsdom can't parse CSS `min()`; behaviour
is exercised in Playwright on real Chromium).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-history + Ctrl-K hint + tests

Round 2 of device feedback:

- App icons: regenerated all five mipmap densities + adaptive foregrounds
  from packages/desktop/build/icons/icon-512.png. The Capacitor default
  icon is gone; Etherpad branding everywhere.
- Removed the always-visible PadActionsOverlay (share is redundant with
  Etherpad's own UI; the "open in browser" button forced over content).
- Collapse handle now flush to left edge, 14px wide (was 22 + 3px gap),
  right-flat border so it reads as a tab sticking out.
- Tab.open auto-collapses the workspace rail (mobile UX) — fires only on
  the open event, doesn't fight subsequent manual expands.
- Tab.open upserts pad-history so QuickSwitcher's name search finds the
  pad. Wired events.onPadHistoryChanged through.
- Settings.userName threads into the iframe src as `&userName=` so the
  user's name applies to existing + new pads (Etherpad reads the query
  param at join time).
- "Tip: Ctrl+K opens this from anywhere" hidden via `@media (pointer:
  coarse)` — touch devices can't issue keyboard shortcuts anyway.
- Tests: 8 mobile Playwright cases now (added 3 — auto-collapse,
  pad-history populate, userName in src). X-Frame detection removed
  (Chromium fires onLoad even for blocked iframes; needs the native
  WebChromeClient hook in Phase 6b).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ging

- Edge-swipe right from left edge expands the rail; swipe left from
  inside collapses it. Touch-only — doesn't fire when a dialog is open
  (dialogs handle their own gestures).
- Android hardware/gesture back: dismiss open dialog first, else collapse
  rail, else minimise the app. Mirrors stock Android navigation expectations.
- padHistory.upsert errors now log to console.warn instead of being
  swallowed by `void`. Earlier logcat confirmed upsert is firing and
  writing 'Jehejej' to SharedPreferences as expected.
Closing the app then reopening it now puts the user back on the pad
they were on. Implementation:

- New `tab-persistence.ts` writes `{tabs, activeTabId, activeWorkspaceId}`
  to Capacitor Preferences under `etherpad:windowState` whenever the
  tab-store mutates (debounced 150ms).
- `loadFromStorage()` is awaited inside `state.getInitial()` so the
  shell's first `onTabsChanged` subscription sees the restored set.
- `window.setActiveWorkspace` now writes through to the persistence
  layer instead of being a no-op.

Shell change: InitialState gains optional `activeWorkspaceId`. App.tsx
prefers it over `workspaceOrder[0]` when present and valid. Desktop
main process doesn't populate it (yet) so desktop behaviour is
unchanged — the field is informational only when omitted.

Also fix(mobile): wire `events.onSettingsChanged` so changing language
or any other setting actually updates the iframe + UI in-place. Was a
noop subscriber; settings-store now has an onChanged emitter and
CapacitorPlatform routes the shell's subscription through it. Without
this, `applySettings()` + `setLanguage()` never fired and the iframe's
?lang= param never refreshed.

Also fix(desktop e2e): bump restore-on-relaunch.spec.ts timeout from
30s → 60s. The test has flaked on ~50% of recent PR runs (cold-start
under xvfb contention); reruns succeed at the same 30s. Happy path
still completes in 8-12s.
@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

…trip

- The 150ms debounce in tab-store.scheduleSave was eating writes on
  app-kill: open pad → \`am force-stop\` ≤150ms later → save never
  fires → on relaunch tab list is empty (the device-side bug the user
  hit). Write-through immediately on every mutation; tab.open is a
  per-user-gesture event so the write rate is fine.

- New Playwright smoke "opening a pad then reloading restores the same
  pad (full write+read cycle)" — characterizes the bug above. Failed
  before this commit, passes after. Reload is the closest in-browser
  analogue to app-kill+relaunch (same JS context boundary, same
  Preferences read on init).

- B&W app icon: regenerated all five mipmap densities + adaptive
  foregrounds from build/icons/tray-icon.png (the official Etherpad
  pencil-pad silhouette). Adaptive background flipped #FFFFFF → #000000.
…-persistence

# Conflicts:
#	packages/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
#	packages/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
#	packages/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
#	packages/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
#	packages/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
#	packages/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
#	packages/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
#	packages/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
#	packages/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
#	packages/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
#	packages/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
#	packages/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
#	packages/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
#	packages/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
#	packages/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
#	packages/mobile/src/platform/tabs/tab-store.ts
#	packages/mobile/tests/smoke.spec.ts
@JohnMcLear JohnMcLear merged commit 51d392a into main May 12, 2026
5 checks passed
@JohnMcLear JohnMcLear deleted the feat/mobile-tab-state-persistence branch May 12, 2026 08:44
This was referenced May 12, 2026
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.

1 participant