Skip to content

Commit d04376a

Browse files
committed
feat(mobile): Start screen setting — Home, or where you left off
Settings › Appearance › Start screen (phones): "Where I left off" keeps the cold-launch landing as it was; "Home" lands on Home on every cold launch. Switching to another app and back is not a launch and never moves the user. Stored like the gesture prefs and mirrored natively in bootstrap.ts so it is back in localStorage before main.tsx reads it. The Play review of 1.1.15 asked for exactly this; same setting as iPhone 1.9.7 (zennotesiphone 45a72f8).
1 parent c1b5d3c commit d04376a

5 files changed

Lines changed: 139 additions & 4 deletions

File tree

src/bootstrap.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Preferences } from '@capacitor/preferences'
2-
import { GESTURES_KEY, HIDE_STATUS_BAR_KEY, LAYOUT_MODE_KEY } from './viewport'
2+
import { GESTURES_KEY, HIDE_STATUS_BAR_KEY, LAYOUT_MODE_KEY, START_SCREEN_KEY } from './viewport'
33

44
const WEB_PREFERENCES_KEY = 'zen:prefs:v2'
55
const NATIVE_PREFERENCES_KEY = 'zn-app-preferences-v2'
@@ -10,11 +10,15 @@ const NATIVE_LAYOUT_MODE_KEY = 'zn-layout-mode'
1010
const NATIVE_HIDE_STATUS_BAR_KEY = 'zn-hide-status-bar'
1111
// The swipe-gesture assignments (#24) ride along too.
1212
const NATIVE_GESTURES_KEY = 'zn-gestures'
13+
// And the Start screen choice (start-screen.ts): read at cold launch, so it
14+
// must be back in localStorage before main.tsx evaluates.
15+
const NATIVE_START_SCREEN_KEY = 'zn-start-screen'
1316
const MIRRORED_KEYS: Record<string, string> = {
1417
[WEB_PREFERENCES_KEY]: NATIVE_PREFERENCES_KEY,
1518
[LAYOUT_MODE_KEY]: NATIVE_LAYOUT_MODE_KEY,
1619
[HIDE_STATUS_BAR_KEY]: NATIVE_HIDE_STATUS_BAR_KEY,
17-
[GESTURES_KEY]: NATIVE_GESTURES_KEY
20+
[GESTURES_KEY]: NATIVE_GESTURES_KEY,
21+
[START_SCREEN_KEY]: NATIVE_START_SCREEN_KEY
1822
}
1923

2024
let persistenceQueue = Promise.resolve()
@@ -84,6 +88,14 @@ async function restoreNativePreferences(): Promise<void> {
8488
} else if (webGestures) {
8589
await Preferences.set({ key: NATIVE_GESTURES_KEY, value: webGestures })
8690
}
91+
92+
const nativeStartScreen = await Preferences.get({ key: NATIVE_START_SCREEN_KEY })
93+
const webStartScreen = localStorage.getItem(START_SCREEN_KEY)
94+
if (nativeStartScreen.value) {
95+
localStorage.setItem(START_SCREEN_KEY, nativeStartScreen.value)
96+
} else if (webStartScreen) {
97+
await Preferences.set({ key: NATIVE_START_SCREEN_KEY, value: webStartScreen })
98+
}
8799
} catch {
88100
// Continue with WebView storage when native preferences are unavailable.
89101
}

src/ui-mobile/MobileShell.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { installNoteRowGestures, NOTE_ROW_SELECTOR } from './note-row-gestures'
4646
import { NoteActionSheet } from './note-actions'
4747
import { installEditorKeyboardScroll } from './editor-keyboard-scroll'
4848
import { installEditorNativeTyping } from './editor-native-typing'
49+
import { getStartScreen, setStartScreen, type StartScreen } from './start-screen'
4950
import { useYouTubeLiteEmbeds } from './youtube-embed-shim'
5051
import { VaultsSheet, promptNewVault } from './MobileDrawer'
5152
import {
@@ -673,8 +674,9 @@ function usePhoneLayoutBoot(): void {
673674
// activeTab (or no snapshot, or an unreadable one) means Home;
674675
// anything else keeps the restored note/view on screen. `null` — the
675676
// witness read somehow still in flight — falls back to Home, the
676-
// pre-#2 behavior, rather than guessing a note.
677-
if (persistedHome !== false) goHome()
677+
// pre-#2 behavior, rather than guessing a note. The Start screen
678+
// setting (start-screen.ts) overrides all of that with Home.
679+
if (persistedHome !== false || getStartScreen() === 'home') goHome()
678680
// First run only: land IN the seeded welcome note (reading mode — no
679681
// keyboard) instead of on a Home screen with nothing to do. Home stays
680682
// one Back tap away. The pane mode is set through the store before the
@@ -2739,6 +2741,50 @@ function SettingsLayoutRow(): React.JSX.Element {
27392741
)
27402742
}
27412743

2744+
const START_SCREEN_CHOICES: Array<{ value: StartScreen; label: string }> = [
2745+
{ value: 'last', label: 'Where I left off' },
2746+
{ value: 'home', label: 'Home' }
2747+
]
2748+
2749+
/**
2750+
* Settings → Appearance → Start screen (island beside Layout / Swipe
2751+
* gestures, phones only — the landing logic is usePhoneLayoutBoot's). A
2752+
* user asked for Home instead of the last note after quitting the app.
2753+
*/
2754+
function SettingsStartScreenRow(): React.JSX.Element {
2755+
const [value, setValue] = useState<StartScreen>(() => getStartScreen())
2756+
const choose = (next: StartScreen): void => {
2757+
setStartScreen(next)
2758+
setValue(next)
2759+
}
2760+
return (
2761+
<div className="zn-settings-layout">
2762+
<div className="zn-settings-layout-text">
2763+
<div className="zn-settings-layout-title">Start screen</div>
2764+
<div className="zn-settings-layout-desc">
2765+
{value === 'home'
2766+
? 'Opening the app lands on Home. Switching to another app and back keeps your place.'
2767+
: 'Opening the app returns to the note or view you left. Choose Home to start fresh every time.'}
2768+
</div>
2769+
</div>
2770+
<div className="zn-settings-layout-seg" role="radiogroup" aria-label="Start screen">
2771+
{START_SCREEN_CHOICES.map((choice) => (
2772+
<button
2773+
key={choice.value}
2774+
type="button"
2775+
role="radio"
2776+
aria-checked={value === choice.value}
2777+
className={value === choice.value ? 'is-active' : ''}
2778+
onClick={() => choose(choice.value)}
2779+
>
2780+
{choice.label}
2781+
</button>
2782+
))}
2783+
</div>
2784+
</div>
2785+
)
2786+
}
2787+
27422788
function useLayoutSettingsRow(): void {
27432789
useEffect(() => {
27442790
let container: HTMLElement | null = null
@@ -2777,6 +2823,7 @@ function useLayoutSettingsRow(): void {
27772823
<>
27782824
<SettingsLayoutRow />
27792825
<SettingsSystemBarsRow />
2826+
{isPhoneWidth() && <SettingsStartScreenRow />}
27802827
{isPhoneWidth() && <SettingsGesturesRow />}
27812828
</>
27822829
)

src/ui-mobile/start-screen.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import assert from 'node:assert/strict'
2+
import test from 'node:test'
3+
4+
// The module reads localStorage lazily (inside the functions), so a stub
5+
// installed before the first call is all node needs.
6+
const store = new Map<string, string>()
7+
;(globalThis as { localStorage?: unknown }).localStorage = {
8+
getItem: (key: string) => store.get(key) ?? null,
9+
setItem: (key: string, value: string) => void store.set(key, value),
10+
removeItem: (key: string) => void store.delete(key)
11+
}
12+
13+
const { DEFAULT_START_SCREEN, getStartScreen, setStartScreen } = await import('./start-screen.ts')
14+
const { START_SCREEN_KEY } = await import('../viewport.ts')
15+
16+
test('nothing stored means where the user left off', () => {
17+
store.delete(START_SCREEN_KEY)
18+
assert.equal(getStartScreen(), 'last')
19+
assert.equal(DEFAULT_START_SCREEN, 'last')
20+
})
21+
22+
test('an unknown stored value falls back to the default', () => {
23+
store.set(START_SCREEN_KEY, 'jetpack')
24+
assert.equal(getStartScreen(), 'last')
25+
})
26+
27+
test('home persists; the default removes the key', () => {
28+
setStartScreen('home')
29+
assert.equal(store.get(START_SCREEN_KEY), 'home')
30+
assert.equal(getStartScreen(), 'home')
31+
setStartScreen('last')
32+
assert.equal(store.has(START_SCREEN_KEY), false)
33+
assert.equal(getStartScreen(), 'last')
34+
})

src/ui-mobile/start-screen.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Where a cold launch lands (user request relayed by Adib, 2026-09-08: "I
3+
* wish there was a feature to set the home screen as the start screen
4+
* instead of the last note when turning the app off and on again").
5+
*
6+
* 'last' (the default) keeps the #2 behaviour: the note or view that was
7+
* open when iOS killed the app comes back, and Home comes back if that is
8+
* where the user left. 'home' lands on Home on every cold launch. Switching
9+
* away and back is not a launch and never moves the user — the landing
10+
* logic in usePhoneLayoutBoot runs once per process.
11+
*
12+
* Read lazily, like gestures.ts; the Settings card writes through
13+
* setStartScreen and the next launch picks it up. The default removes the
14+
* key so a fresh install and a reset look identical.
15+
*/
16+
import { START_SCREEN_KEY } from '../viewport.ts'
17+
18+
export type StartScreen = 'last' | 'home'
19+
20+
export const DEFAULT_START_SCREEN: StartScreen = 'last'
21+
22+
export function getStartScreen(): StartScreen {
23+
try {
24+
return localStorage.getItem(START_SCREEN_KEY) === 'home' ? 'home' : DEFAULT_START_SCREEN
25+
} catch {
26+
return DEFAULT_START_SCREEN
27+
}
28+
}
29+
30+
export function setStartScreen(next: StartScreen): void {
31+
try {
32+
if (next === DEFAULT_START_SCREEN) localStorage.removeItem(START_SCREEN_KEY)
33+
else localStorage.setItem(START_SCREEN_KEY, next)
34+
} catch {
35+
// Storage unavailable: the choice applies to this session only.
36+
}
37+
}

src/viewport.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export const HIDE_STATUS_BAR_KEY = 'zn:hide-status-bar'
9494
* ui-mobile/gestures.ts. Mirrored natively for the same reason as above. */
9595
export const GESTURES_KEY = 'zn:gestures'
9696

97+
/** localStorage key for where a cold launch lands ('home'; absent = where
98+
* the user left off), see ui-mobile/start-screen.ts. Mirrored natively
99+
* like the other shell prefs. */
100+
export const START_SCREEN_KEY = 'zn:start-screen'
101+
97102

98103
/** Publish the current decision to CSS. Safe to call repeatedly. */
99104
export function syncPhoneClass(): void {

0 commit comments

Comments
 (0)