Align the mobile thread list with the web sidebar and add section reorder - #2058
Conversation
…rder The home thread list had four left text edges (headers, parent threads pushed by a leading chevron, children, leaf threads), a 20px section label that outweighed 17px titles, a count chip and "+" in different columns from the status glyph, and a project line under every row. Rows now follow the web sidebar rules: one text edge per depth (16 + 24 × depth), the disclosure chevron after the label, a hairline group line under nested rows, one 36px trailing slot, 12px uppercase section labels, and one line per row on the home list (search and archive keep their subtitle). Sections can be reordered: the order is stored per organize mode under the web localStorage keys, normalized with the shared client-core helper, and applied when the rows are built. A drag-to-reorder sheet opens from the display options, the project and section menus, and a long-press on a built-in or machine header. Co-Authored-By: Claude <noreply@anthropic.com>
| const isReady = model.isReady; | ||
| useEffect(() => { | ||
| if (!isReady || haveSameOrder(storedOrder, order)) return; | ||
| setSectionOrder(organize, order); |
There was a problem hiding this comment.
🚨 slopcop/review — [P1] Keep the order separate for each server profile.
This effect writes the normalized order to one app-wide key. Profile B removes Profile A identifiers during normalization.
This destroys the custom order for Profile A. Store entity-specific orders by profile ID, and migrate each old global key once.
| layout="scroll" | ||
| // The reorder list owns vertical drags, so it gets a plain view body | ||
| // and the sheet stops following the finger. | ||
| layout={state?.kind === "section-reorder" ? "view" : "scroll"} |
There was a problem hiding this comment.
🚨 slopcop/review — [P1] Keep every reorder row and the Done action reachable.
This sheet uses a plain view and disables body panning. Each section adds 48 pixels.
With enough projects, the sheet hides lower rows and the Done action. Use a bounded scroll list and add edge scrolling during a drag.
| return ( | ||
| <GestureDetector gesture={rowPan}> | ||
| <Animated.View | ||
| accessibilityRole="button" |
There was a problem hiding this comment.
🚨 slopcop/review — [P1] Add an accessible reorder action.
This row has a button role but no button action. VoiceOver uses drag gestures itself, so users cannot reorder these rows.
Use the adjustable role with increment and decrement actions. Move the row one slot for each action, and announce its position.
| const organize = model.organize; | ||
| const isReady = model.isReady; | ||
| useEffect(() => { | ||
| if (!isReady || haveSameOrder(storedOrder, order)) return; |
There was a problem hiding this comment.
🚨 slopcop/review — [P2] Wait for the host query before machine-order persistence.
The bootstrap query can finish before the host query. Machine mode then builds and saves a temporary unknown-host order.
Wait for host-query readiness before this effect saves a machine order. Add a test for bootstrap data that arrives first.
| "worklet"; | ||
| const current = slots.value[id] ?? 0; | ||
| if (nextIndex === current) return; | ||
| const next = { ...slots.value }; |
There was a problem hiding this comment.
🚨 slopcop/review — [P2] Do not copy every slot at each drag boundary.
Each crossed boundary copies and scans all slots on the UI thread. A long list causes quadratic work during one drag.
Keep an order array or direct slot references. Update only the slots in the crossed range.
| emptyProjectLabel: string, | ||
| rows: SidebarListRow[], | ||
| ): void { | ||
| const group = model.groups.find((candidate) => candidate.id === sectionId); |
There was a problem hiding this comment.
🚨 slopcop/review — [P2] Avoid a full group scan for every ordered section.
This lookup scans all groups once for each section. Large project sets give each list projection quadratic work.
Build one ID-to-group map before the loop, then use constant-time lookups.
| { | ||
| height: SECTION_REORDER_ROW_HEIGHT, | ||
| borderRadius: radii.md, | ||
| shadowColor: "#000", |
There was a problem hiding this comment.
🚨 slopcop/review — [P3] Use the theme shadow color.
The fixed black shadow ignores custom palettes. The mobile theme already supplies tokens.shadowColor.
Use that token here, and set the opacity for the token.
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
This change makes the mobile thread list match the web sidebar. It also adds saved drag order for project, machine, and manual sections.
I found seven issues:
- P1: A profile switch can delete the prior profile's saved section order.
- P1: A long section list can hide lower rows and the Done action.
- P1: VoiceOver users cannot use the new reorder control.
- P2: Machine order can save before the host query finishes.
- P2: The drag operation copies and scans all slots at each boundary.
- P2: The list projection scans all groups once for each section.
- P3: The drag shadow ignores the theme shadow token.
I found no security or authorization issue.
I compared this code with the web sidebar. Client-core already owns section identifiers and normalization. I found no other useful shared refactor.
The mobile Turbo test passed 122 files and 829 tests. The Turbo type check, lint check, and diff check also passed.
GitHub CI passed, but the iOS simulator job did not run. I started Metro and used doobie against its web route.
The web bundle failed because this native app does not include react-native-web. Maestro is not installed, so I could not run the native flow.
I left inline comments with fixes for each issue. I used a final GPT-5.6 review to check the findings.
What was wrong
The mobile home thread list did not line up. It had four left text edges (section labels, parent threads pushed by a leading chevron, child threads, leaf threads), 20px section labels that outweighed the 17px titles, a count chip and "+" action in different columns from the status glyph, no tree line under nested rows, and a project line under every thread. Sections also had no order control: the web sidebar lets the user drag sections, the mobile app did not.
What changed
apps/mobile/src/screens/sidebar/SidebarRows.tsx: rows follow the web sidebar rules. One text edge per depth (16 + 24 × depth, webgetSidebarThreadRowPaddingLeft), the disclosure chevron after the label (header 12px, thread 14px), a hairline group line under nested rows (webSIDEBAR_PROJECT_GROUP_LINE_CLASS), one 36px trailing slot for the status glyph and the header "+", 12px uppercasesectionLabelheaders.SidebarThreadList.tsx: the home list passes no subtitle (one line per row). Search and Archive keep theirs. The deadgroupProjectIdrow field is removed.data/sidebar/sidebar-preferences.ts:sectionOrderper organize mode under the web keys (bb.sidebar.sectionOrder,bb.sidebar.manualSectionOrder,bb.sidebar.machineSectionOrder) plussetSectionOrder.data/sidebar/sidebar-section-order.ts:resolveSidebarSectionOrder(client-corenormalizeSidebarSectionOrder),useSidebarSectionOrder(writes the normalized order back like the web),listSidebarSectionOrderEntries,mergeHiddenSectionOrder.sidebar-list-rows.ts:buildSidebarListRowstakessectionOrderand emits Pinned and groups in that order.SectionReorderList.tsx: drag-to-reorder list (reanimated + gesture-handler), commits on drop, haptic ticks.SidebarActionsProvider.tsx: "Reorder sections…" in the display options, "Reorder sections" in project and section menus, a long-press on a built-in or machine header opens the sheet.ui/Sheet.tsxexposesenableContentPanningGestureso the list owns vertical drags.apps/mobile/README.md: one line for the feature.No wire change. Client-local preference only, so no CLI surface (same as the web).
How you verified
pnpm exec turbo run typecheck lint test --filter=@bb/mobile: pass (829 tests). New tests: section-order persistence and keys (sidebar-preferences.test.ts), section ordering inbuildSidebarListRows(sidebar-list-rows.test.ts),mergeHiddenSectionOrder(sidebar-section-order.test.ts).Fixes #