From ea831d1857d1231f5ddda051891215fa6c10f6f6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 16 Oct 2026 14:41:33 -0400 Subject: [PATCH] Rail and drawer: only the 5 tags most recently used or viewed (#5931) A long tag list pushed the drawer's Archive, Trash and Sync off screen on Android, where Tags still sat above them (#5915 only reordered the web rail). Tags now come last on Android too, and both clients list the five tags most recently used or viewed, by name, with "All tags (N)" opening the rest in place. Used: the newest untrashed note carrying the tag. GET /api/labels and the core's list_labels return it as last_used, and each client also folds in the notes it holds, so a tag applied a moment ago counts before the next listing. Viewed: opening the tag's lens, remembered on the device (per user on the web). Co-Authored-By: Claude Opus 5.5 --- .../java/com/fabledsword/inkwell/TagViews.kt | 31 ++++++++ .../com/fabledsword/inkwell/ui/BoardScreen.kt | 76 ++++++++++++++----- .../com/fabledsword/inkwell/ui/RecentTags.kt | 52 +++++++++++++ android/app/src/main/res/values/strings.xml | 2 + .../fabledsword/inkwell/ui/RecentTagsTest.kt | 41 ++++++++++ android/ffi/src/models.rs | 5 ++ core/src/local/models.rs | 3 + core/src/local/store.rs | 35 +++++++++ frontend/src/components/AppShell.vue | 40 +++++++++- frontend/src/notes/recentTags.test.ts | 40 ++++++++++ frontend/src/notes/recentTags.ts | 75 ++++++++++++++++++ frontend/src/stores/labels.ts | 3 + src/inkwell/labels.py | 26 ++++++- tests/test_integration.py | 20 +++++ 14 files changed, 428 insertions(+), 21 deletions(-) create mode 100644 android/app/src/main/java/com/fabledsword/inkwell/TagViews.kt create mode 100644 android/app/src/main/java/com/fabledsword/inkwell/ui/RecentTags.kt create mode 100644 android/app/src/test/java/com/fabledsword/inkwell/ui/RecentTagsTest.kt create mode 100644 frontend/src/notes/recentTags.test.ts create mode 100644 frontend/src/notes/recentTags.ts diff --git a/android/app/src/main/java/com/fabledsword/inkwell/TagViews.kt b/android/app/src/main/java/com/fabledsword/inkwell/TagViews.kt new file mode 100644 index 0000000..051d2ea --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/inkwell/TagViews.kt @@ -0,0 +1,31 @@ +package com.fabledsword.inkwell + +import android.content.Context + +/** + * When each tag's lens was last opened on this phone, for the drawer's recent tags + * (#5931). Opening a tag counts as using it, alongside the notes that carry it. + * + * Kept on the device rather than synced: it is a convenience about where this + * person's thumb goes on this screen, and losing it costs one tap. + */ +class TagViews( + context: Context, +) { + // applicationContext, as in SyncSettings: this must not hold an Activity. + private val prefs = + context.applicationContext.getSharedPreferences(FILE, Context.MODE_PRIVATE) + + fun all(): Map = prefs.all.mapNotNull { (id, at) -> (at as? Long)?.let { id to it } }.toMap() + + /** Records a view now and returns every view, this one included. */ + fun record(tagId: String): Map { + val now = System.currentTimeMillis() + prefs.edit().putLong(tagId, now).apply() + return all() + (tagId to now) + } + + private companion object { + const val FILE = "inkwell-tag-views" + } +} diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardScreen.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardScreen.kt index 238e92a..3ac40fe 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardScreen.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardScreen.kt @@ -60,6 +60,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -68,11 +69,13 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.fabledsword.inkwell.R +import com.fabledsword.inkwell.TagViews import com.fabledsword.inkwell.core.Label import com.fabledsword.inkwell.core.Note import kotlinx.coroutines.launch @@ -96,6 +99,15 @@ fun BoardScreen( ) { val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() + + // Opening a tag's lens counts as using it, however it was opened: from the + // drawer, or a tag on a card (#5931). + val context = LocalContext.current + val tagViews = remember(context) { TagViews(context) } + var views by remember { mutableStateOf(tagViews.all()) } + LaunchedEffect(state.destination) { + (state.destination as? Destination.WithLabel)?.let { views = tagViews.record(it.id) } + } val snackbars = remember { SnackbarHostState() } // Held HERE rather than on the card. A card lives in a lazy grid and is disposed @@ -143,6 +155,7 @@ fun BoardScreen( NavigationDrawer( current = state.destination, labels = state.labels, + recency = tagRecency(state.labels, state.notes, views), syncSummary = sync.summary, onOpen = { onOpen(it) @@ -401,11 +414,16 @@ private fun SearchBar( private fun NavigationDrawer( current: Destination, labels: List