From 3b5e737e1fc3f1bc5f82a085970372a0d70e82b2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 5 Oct 2026 09:30:32 -0700 Subject: [PATCH] fix(mobile): skip move indexes for empty and single-thread sections --- .../src/features/threads/threadOrder.ts | 5 ++++ .../threads/threadOrderAvailability.test.ts | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/apps/mobile/src/features/threads/threadOrder.ts b/apps/mobile/src/features/threads/threadOrder.ts index 570251f4a985..a8ba14e31fd4 100644 --- a/apps/mobile/src/features/threads/threadOrder.ts +++ b/apps/mobile/src/features/threads/threadOrder.ts @@ -134,6 +134,11 @@ export function computeThreadMoveAvailability(input: { // A reorder in flight locks the whole list until its receipt lands. if (input.pendingOrder != null) return result; const rows = input.ordered; + // With no neighbor there is no up/down move, so skip the allThreads indexes. + if (rows.length < 2) { + for (const row of rows) result.set(rowId(row), { canMoveUp: false, canMoveDown: false }); + return result; + } const orderedIds = rows.map(rowId); const indexById = new Map(orderedIds.map((id, index) => [id, index] as const)); const keysById = new Map( diff --git a/apps/mobile/src/features/threads/threadOrderAvailability.test.ts b/apps/mobile/src/features/threads/threadOrderAvailability.test.ts index 765eff4cf61e..fdd90c14dcbe 100644 --- a/apps/mobile/src/features/threads/threadOrderAvailability.test.ts +++ b/apps/mobile/src/features/threads/threadOrderAvailability.test.ts @@ -208,6 +208,34 @@ describe("computeThreadMoveAvailability matches the reference planner", () => { expect(batch.get("env-w:t0")).toEqual({ canMoveUp: false, canMoveDown: false }); }); + it("answers empty and single-row sections without consulting hidden rows", () => { + const hidden = [makeRow("h0", "env-w", "b", true), makeRow("h1", "env-x", null, true)]; + expect( + computeThreadMoveAvailability({ + ordered: [], + allThreads: hidden, + section: "pinned", + reorderableEnvironmentIds: WRITABLE, + }).size, + ).toBe(0); + for (const environment of ["env-w", "env-x"]) { + const ordered = [makeRow("t0", environment, "a", true)]; + const allThreads = [...ordered, ...hidden]; + const batch = computeThreadMoveAvailability({ + ordered, + allThreads, + section: "pinned", + reorderableEnvironmentIds: WRITABLE, + }); + expect(Object.fromEntries(batch)).toEqual({ + [`${environment}:t0`]: { canMoveUp: false, canMoveDown: false }, + }); + expect(Object.fromEntries(batch)).toEqual( + Object.fromEntries(referenceAvailability(ordered, allThreads, WRITABLE)), + ); + } + }); + it("denies rows whose section has non-writable neighbours when the fast path fails", () => { // Keyless neighbors force the section-rewrite fallback; a non-writable // neighbor makes the rewrite illegal for every row in the section.