The Language of Sorting
I added priority, difficulty, and satisfaction sorting to the Task Record list. But it wasn't just about sort order — it meant giving each axis its own visual language, and collapsible groups followed naturally.
The Language of Sorting
When Created-Date Isn’t Enough
For a long time Fecit’s Tasks tab only sorted by creation date, newest first. It’s simple and easy to understand — a good default.
But once tasks pile up, that default stops being enough. Two of the most common needs I kept running into:
- “I want to see what’s most important first.”
- “I want to get the quick ones out of the way first.”
The first is sorting by priority, the second by difficulty. Both fields already existed on the task model. They just weren’t wired into the list sort.
There was one more axis worth thinking about: satisfaction. It’s not a planning field — it belongs to the retrospect stage. But that’s exactly why it’s useful. “Which tasks from last month did I find most satisfying?” is a question you can only ask when you can sort by it.
So three axes joined the list — priority · difficulty · satisfaction. Together with creation date, that’s four ways to slice the same list. For now each axis is descending only; ascending can come later if a real use case shows up.
A Different Visual Language for Each Axis
Adding the sort axes surfaced an immediate problem. When sorted by creation date, the list had date separators weaving through it — “from here on it’s yesterday,” “from here on it’s two days ago.” That flow gave the list a rhythm.
But the moment you sort by priority, those date separators lose their meaning. Tasks from the same day scatter across priority levels.
So each sort axis got its own dedicated Separator.
- Priority Separator — an icon for each priority level (
priority-high.svg,priority-lowest.svg, etc.) and a label like “High” or “Lowest” - Difficulty Separator — stars, one through five. Trivial is a single star; Extreme is five
- Satisfaction Separator — the existing
SatisfactionIconreused as-is. The face expression and background color already shift across the 1–5 levels
Same “group header” role, but different visual language for each axis. The number 5 can read as “five stars,” as “HIGHEST,” or as a smiling face — and the right expression depends on what the axis actually means. A unified design component would have been less expressive than letting each axis speak its own language.
Groups Invite Collapsing
Once separators split the list into sections, the next desire came quickly: “let me collapse this group so I can reach what’s below.”
With date sorting, the old groups just drift down and stay there. You rarely revisit them. But priority, difficulty, and satisfaction are different. Sorted descending, the top holds the highest values, and the lower groups have their own purpose. Low-priority tasks are what you pick up when you have room to spare. Easy tasks are what you want to clear when a spare pocket of time shows up. Low-satisfaction tasks are what you want to revisit to figure out what to do differently.
Getting to those lower groups quickly means collapsing the ones you’ve already scanned on top.
So every separator got a collapse toggle. Tapping it hides the tasks in that group. A chevron on the right shows the state — pointing down (∨) when expanded, rotating to the right (>) when collapsed.
The implementation is simple. A Set<string> holds the keys of collapsed groups, and when building the list items we skip any task whose group key is in that set. The keys are computed per axis:
date-2026-04-22T...
priority-4
difficulty-2
satisfaction-5
Finding the Right Animation
The real puzzle was how collapsing should feel. When a bunch of items disappear or appear at once, the default layout animation calculates each cell independently — some cells move a little faster, others slightly behind, and you can see the disagreement.
A few approaches:
- Add fade in/out to each task cell — That made individual items visibly appear or disappear, which read as “items were created/destroyed” rather than “a group collapsed.” Dropped.
- Turn off layout animation at the moment of collapse — Everything snaps to place, perfectly synchronized, but it felt rigid.
- Shorten the duration — A shorter animation is smoother on its own, but that duration also governs other situations (creation, edits), so it was a blunt tool.
The one that stuck: temporarily disable layout animation the instant a collapse happens. A collapsingRef flag gets set, the state updates, and two frames later the flag goes back down. Any layout change during that window happens without animation. Creation, edits, focus transitions — all the other situations — still use the smooth 200ms transition.
It’s not a perfect result. What I’d actually want is “the whole list feels like it’s sliding upward as one block,” but that’s structurally hard to achieve with a FlatList plus per-cell Reanimated layout animation. Getting there would mean replacing FlatList with a ScrollView — giving up virtualization and redesigning pagination, sticky headers, and the focused slot. Not worth it right now.
What Got Built
- Four sort axes — creation date, priority, difficulty, satisfaction
- Group headers with a different visual language per axis
- Collapse/expand so users can shape the list’s length themselves
Sorting looks like a surface-level feature — just reordering a list. But done well, it opens up different ways of looking at task management. Some people scan their day by priority, others by difficulty, others by how they felt about things. Sorting is the shape that holds those perspectives.
Server Too, Web Too
This wasn’t a mobile-only change. The server API’s TaskSortType enum got the three new values, and the gather endpoint grew its sort branches. The desktop web app’s filter modal picked up the same options. Because all three surfaces share the same enum values, sorting on mobile produces the same result as sorting on desktop.
The local SQLite cache also got a migration — adding priority, difficulty, and satisfaction columns with indexes. Each time the schema version goes up, an ALTER TABLE ADD COLUMN runs and the missing values are recovered from the stored JSON. I tripped over the migration condition at one point, ending up in a broken middle state where the schema version was bumped but the columns weren’t, which reminded me of a principle I should’ve kept straight: each version’s migration block represents the schema at the moment it was committed, and it must never be edited afterward.
A single feature — sorting — ended up touching a surprising number of layers. That’s partly how far Fecit’s task model travels, and partly just how many surfaces a feature has to pass through before it reaches a user.