Push-style side menu + Obsidian-like sidebar with notes list (Android)

Hi everyone,

I've been working on a couple of UX improvements for the Android app that I think would make Joplin feel significantly more polished, especially for users who switched from Obsidian or use Android gesture navigation. I'd love to hear feedback before submitting a proper PR.


1. Push-content side menu (instead of overlay)

Currently the left sidebar slides over the content. Most modern note-taking apps (Obsidian, Gmail, Notion) push the content to the right instead — the menu and content move together as one unit.

Current behavior: sidebar overlays the note list
Proposed behavior: sidebar pushes the note list to the right

The implementation uses react-native-gesture-handler with direct Animated.Value.setValue() for real-time finger tracking and Animated.spring() on release:

// menuX: -menuWidth (closed) → 0 (open)
// contentX mirrors it: 0 → menuWidth
const contentTranslateX = menuX.interpolate({
    inputRange: [-menuWidth, 0],
    outputRange: [0, menuWidth],
    extrapolate: 'clamp',
});

// Spring config (Material Design feel)
const SPRING_CONFIG = {
    useNativeDriver: true,
    damping: 32,
    stiffness: 300,
    mass: 1,
    overshootClamping: true,
};

The result feels native — the menu literally follows your finger, then snaps open/closed on release based on velocity or distance.


2. Obsidian-style notes list inside the sidebar

Right now the sidebar only shows folders. In Obsidian, tapping a folder reveals its notes inline — you can navigate between notes without ever closing the panel.

Proposed change: show the current folder's notes below the folder tree, updating automatically when you switch folders. Tapping a note opens it without closing the sidebar.

// In side-menu-content.tsx — connected to Redux state
notes: state.notes,           // current folder's notes (already in store)
selectedNoteId: state.selectedNoteIds?.[0] ?? null,

// Render notes below folder tree
{props.notes.length > 0 && props.notes.map(note => (
    <NoteItem
        key={note.id}
        note={note}
        selected={note.id === props.selectedNoteId}
        themeId={props.themeId}
        onPress={note_press}  // opens note, menu stays open
    />
))}

No new data fetching needed — state.notes is already populated by the existing reducer.


3. Configurable swipe edge width — important for gesture navigation users

This is the part I think deserves most attention. On Android phones with gesture navigation (no back button), the system back gesture also lives on the left edge of the screen — the same place Joplin's menu swipe originates. This causes constant false triggers.

The fix: expose the swipe edge zone as a user setting, and add velocity + distance thresholds so slow accidental drags don't open the menu.

New settings in builtInMetadata.ts:

'ui.sideMenuSwipeWidth': {
    value: 80,
    type: SettingItemType.Int,
    public: true,
    appTypes: [AppType.Mobile],
    section: 'appearance',
    label: () => _('Side menu swipe area width (pixels)'),
    description: () => _('Width of the screen edge area that triggers the side menu (25–200px). Increase to 120+ if using Android gesture navigation.'),
    minimum: 25,
    maximum: 200,
    step: 10,
    storage: SettingStorage.File,
    isGlobal: true,
},
'ui.sideMenuSwipeVelocityThreshold': {
    value: 500,
    type: SettingItemType.Int,
    public: true,
    appTypes: [AppType.Mobile],
    section: 'appearance',
    label: () => _('Side menu swipe velocity threshold (px/s)'),
    description: () => _('Minimum swipe speed required to open the side menu. Higher values reduce false triggers.'),
    minimum: 100,
    maximum: 2000,
    step: 50,
    storage: SettingStorage.File,
    isGlobal: true,
},
'ui.sideMenuSwipeDistanceThreshold': {
    value: 60,
    type: SettingItemType.Int,
    public: true,
    appTypes: [AppType.Mobile],
    section: 'appearance',
    label: () => _('Side menu swipe distance threshold (px)'),
    minimum: 20,
    maximum: 300,
    step: 10,
    storage: SettingStorage.File,
    isGlobal: true,
},

Gesture detection logic — the key part that prevents conflicts with Android system gestures:

const panGesture = Gesture.Pan()
    .onStart(() => {
        const fromEdge = !isOpen && touchStartX.current <= swipeEdgeWidth;
        const canClose = isOpen;
        gestureValid.current = fromEdge || canClose;
    })
    .onUpdate((e) => {
        if (!gestureValid.current) return;

        // Cancel if vertical scroll dominates
        const absX = Math.abs(e.translationX);
        const absY = Math.abs(e.translationY);
        if (absY > absX * 1.8 && absX < distanceThreshold * 0.3) {
            gestureValid.current = false;
            return;
        }

        // Direct finger tracking
        const clampedX = Math.max(-menuWidth, Math.min(0,
            gestureStartMenuX.current + e.translationX
        ));
        menuX.setValue(clampedX);
    })
    .onEnd((e) => {
        const vx = e.velocityX;
        const dx = currentMenuX.current - gestureStartMenuX.current;

        if (vx > velocityThreshold || dx > distanceThreshold) snapOpen();
        else if (vx < -velocityThreshold || dx < -distanceThreshold) snapClose();
        else currentMenuX.current >= -menuWidth / 2 ? snapOpen() : snapClose();
    })
    // Critical — prevents conflict with Android back gesture and ScrollView
    .activeOffsetX([-15, 15])
    .failOffsetY([-22, 22]);

Recommended settings for gesture navigation: swipeWidth: 100–120px, velocityThreshold: 600+
Recommended settings for button navigation: swipeWidth: 60–80px, velocityThreshold: 400


4. No auto-focus / keyboard popup in view mode

Small but annoying: opening a note in view mode currently triggers keyboard popup on some devices. Three-line fix in Note.tsx:

// 1. scheduleFocusUpdate — skip entirely in view mode
public scheduleFocusUpdate() {
    if (this.state.mode === 'view') return;
    // ...
}

// 2. Multiline title recalc — guard against layout-triggered focus
if (prevState.multiline !== this.state.multiline
    && this.titleTextFieldRef.current
    && this.state.mode === 'edit') {       // ← added
    focus('Note::focusUpdate::title', this.titleTextFieldRef.current);
}

// 3. Title TextInput — physically non-editable in view mode
editable={this.state.mode === 'edit' && !this.state.readOnly}

Without fix #3, Android can still focus a TextInput programmatically even when you don't intend it. editable={false} blocks that at the OS level.

Current behavior: sidebar overlays the note list
Proposed behavior: sidebar pushes the note list to the right

As far as I can tell from your description, the sidebar already does push the note list to the right in the Joplin mobile app. What is does not do is drag the notebook list as it is moved, but it reveals/uncovers it instead. So it doesn’t quite give the effect of moving as one unit.

Small but annoying: opening a note in view mode currently triggers keyboard popup on some devices

That’s a regression which has been fixed but is not released yet

1 Like

Also if you’re going to submit changes for unreported / not triaged issues, don’t submit a big PR with all these changes. Submit individual PRs which only fix / change one thing for each

The comment I would have here is that it breaks from the UI/UX established in the other two applications which all follow a 3 pane model rather than a hierarchical tree.

1 Like