Tighten mine-only inbox swipe archive

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-26 17:14:48 -05:00
parent 4426d96610
commit 826da2973d
5 changed files with 29 additions and 16 deletions

View file

@ -83,7 +83,7 @@ describe("SwipeToArchive", () => {
expect(onClick).not.toHaveBeenCalled(); expect(onClick).not.toHaveBeenCalled();
act(() => { act(() => {
vi.advanceTimersByTime(210); vi.advanceTimersByTime(140);
}); });
expect(onArchive).toHaveBeenCalledTimes(1); expect(onArchive).toHaveBeenCalledTimes(1);

View file

@ -9,9 +9,9 @@ interface SwipeToArchiveProps {
className?: string; className?: string;
} }
const COMMIT_THRESHOLD = 0.4; const COMMIT_THRESHOLD = 0.32;
const MAX_SWIPE = 0.92; const MAX_SWIPE = 0.88;
const COMMIT_DELAY_MS = 210; const COMMIT_DELAY_MS = 140;
export function SwipeToArchive({ export function SwipeToArchive({
children, children,

View file

@ -8,6 +8,7 @@ import {
getInboxWorkItems, getInboxWorkItems,
getRecentTouchedIssues, getRecentTouchedIssues,
getUnreadTouchedIssues, getUnreadTouchedIssues,
isMineInboxTab,
loadLastInboxTab, loadLastInboxTab,
RECENT_ISSUES_LIMIT, RECENT_ISSUES_LIMIT,
saveLastInboxTab, saveLastInboxTab,
@ -400,4 +401,11 @@ describe("inbox helpers", () => {
localStorage.setItem("paperclip:inbox:last-tab", "new"); localStorage.setItem("paperclip:inbox:last-tab", "new");
expect(loadLastInboxTab()).toBe("mine"); expect(loadLastInboxTab()).toBe("mine");
}); });
it("enables swipe archive only on the mine tab", () => {
expect(isMineInboxTab("mine")).toBe(true);
expect(isMineInboxTab("recent")).toBe(false);
expect(isMineInboxTab("unread")).toBe(false);
expect(isMineInboxTab("all")).toBe(false);
});
}); });

View file

@ -98,6 +98,10 @@ export function saveLastInboxTab(tab: InboxTab) {
} }
} }
export function isMineInboxTab(tab: InboxTab): boolean {
return tab === "mine";
}
export function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] { export function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] {
const sorted = [...runs].sort( const sorted = [...runs].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),

View file

@ -48,6 +48,7 @@ import {
getInboxWorkItems, getInboxWorkItems,
getLatestFailedRunsByAgent, getLatestFailedRunsByAgent,
getRecentTouchedIssues, getRecentTouchedIssues,
isMineInboxTab,
InboxApprovalFilter, InboxApprovalFilter,
saveLastInboxTab, saveLastInboxTab,
shouldShowInboxSection, shouldShowInboxSection,
@ -520,6 +521,7 @@ export function Inbox() {
pathSegment === "mine" || pathSegment === "recent" || pathSegment === "all" || pathSegment === "unread" pathSegment === "mine" || pathSegment === "recent" || pathSegment === "all" || pathSegment === "unread"
? pathSegment ? pathSegment
: "mine"; : "mine";
const canArchiveFromTab = isMineInboxTab(tab);
const issueLinkState = useMemo( const issueLinkState = useMemo(
() => () =>
createIssueDetailLocationState( createIssueDetailLocationState(
@ -911,7 +913,7 @@ export function Inbox() {
}, [dismiss]); }, [dismiss]);
const nonIssueUnreadState = (key: string): NonIssueUnreadState => { const nonIssueUnreadState = (key: string): NonIssueUnreadState => {
if (tab !== "mine") return null; if (!canArchiveFromTab) return null;
const isRead = readItems.has(key); const isRead = readItems.has(key);
const isFading = fadingNonIssueItems.has(key); const isFading = fadingNonIssueItems.has(key);
if (isFading) return "fading"; if (isFading) return "fading";
@ -951,7 +953,7 @@ export function Inbox() {
} }
// Keyboard shortcuts are only active on the "mine" tab // Keyboard shortcuts are only active on the "mine" tab
if (tab !== "mine") return; if (!canArchiveFromTab) return;
const itemCount = workItemsToRender.length; const itemCount = workItemsToRender.length;
if (itemCount === 0) return; if (itemCount === 0) return;
@ -1033,7 +1035,7 @@ export function Inbox() {
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, [ }, [
workItemsToRender, selectedIndex, tab, navigate, issueLinkState, workItemsToRender, selectedIndex, canArchiveFromTab, navigate, issueLinkState,
getWorkItemKey, archivingIssueIds, archivingNonIssueIds, getWorkItemKey, archivingIssueIds, archivingNonIssueIds,
fadingOutIssues, readItems, fadingOutIssues, readItems,
archiveIssueMutation, markReadMutation, markUnreadMutation, archiveIssueMutation, markReadMutation, markUnreadMutation,
@ -1219,7 +1221,6 @@ export function Inbox() {
</div>, </div>,
); );
} }
const isMineTab = tab === "mine";
const isSelected = selectedIndex === index; const isSelected = selectedIndex === index;
if (item.kind === "approval") { if (item.kind === "approval") {
@ -1235,7 +1236,7 @@ export function Inbox() {
isPending={approveMutation.isPending || rejectMutation.isPending} isPending={approveMutation.isPending || rejectMutation.isPending}
unreadState={nonIssueUnreadState(approvalKey)} unreadState={nonIssueUnreadState(approvalKey)}
onMarkRead={() => handleMarkNonIssueRead(approvalKey)} onMarkRead={() => handleMarkNonIssueRead(approvalKey)}
onArchive={isMineTab ? () => handleArchiveNonIssue(approvalKey) : undefined} onArchive={canArchiveFromTab ? () => handleArchiveNonIssue(approvalKey) : undefined}
archiveDisabled={isArchiving} archiveDisabled={isArchiving}
className={ className={
isArchiving isArchiving
@ -1244,7 +1245,7 @@ export function Inbox() {
} }
/> />
); );
elements.push(wrapItem(approvalKey, isSelected, isMineTab ? ( elements.push(wrapItem(approvalKey, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={approvalKey} key={approvalKey}
disabled={isArchiving} disabled={isArchiving}
@ -1271,7 +1272,7 @@ export function Inbox() {
isRetrying={retryingRunIds.has(item.run.id)} isRetrying={retryingRunIds.has(item.run.id)}
unreadState={nonIssueUnreadState(runKey)} unreadState={nonIssueUnreadState(runKey)}
onMarkRead={() => handleMarkNonIssueRead(runKey)} onMarkRead={() => handleMarkNonIssueRead(runKey)}
onArchive={isMineTab ? () => handleArchiveNonIssue(runKey) : undefined} onArchive={canArchiveFromTab ? () => handleArchiveNonIssue(runKey) : undefined}
archiveDisabled={isArchiving} archiveDisabled={isArchiving}
className={ className={
isArchiving isArchiving
@ -1280,7 +1281,7 @@ export function Inbox() {
} }
/> />
); );
elements.push(wrapItem(runKey, isSelected, isMineTab ? ( elements.push(wrapItem(runKey, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={runKey} key={runKey}
disabled={isArchiving} disabled={isArchiving}
@ -1304,7 +1305,7 @@ export function Inbox() {
isPending={approveJoinMutation.isPending || rejectJoinMutation.isPending} isPending={approveJoinMutation.isPending || rejectJoinMutation.isPending}
unreadState={nonIssueUnreadState(joinKey)} unreadState={nonIssueUnreadState(joinKey)}
onMarkRead={() => handleMarkNonIssueRead(joinKey)} onMarkRead={() => handleMarkNonIssueRead(joinKey)}
onArchive={isMineTab ? () => handleArchiveNonIssue(joinKey) : undefined} onArchive={canArchiveFromTab ? () => handleArchiveNonIssue(joinKey) : undefined}
archiveDisabled={isArchiving} archiveDisabled={isArchiving}
className={ className={
isArchiving isArchiving
@ -1313,7 +1314,7 @@ export function Inbox() {
} }
/> />
); );
elements.push(wrapItem(joinKey, isSelected, isMineTab ? ( elements.push(wrapItem(joinKey, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={joinKey} key={joinKey}
disabled={isArchiving} disabled={isArchiving}
@ -1370,7 +1371,7 @@ export function Inbox() {
} }
onMarkRead={() => markReadMutation.mutate(issue.id)} onMarkRead={() => markReadMutation.mutate(issue.id)}
onArchive={ onArchive={
isMineTab canArchiveFromTab
? () => archiveIssueMutation.mutate(issue.id) ? () => archiveIssueMutation.mutate(issue.id)
: undefined : undefined
} }
@ -1383,7 +1384,7 @@ export function Inbox() {
/> />
); );
elements.push(wrapItem(`issue:${issue.id}`, isSelected, isMineTab ? ( elements.push(wrapItem(`issue:${issue.id}`, isSelected, canArchiveFromTab ? (
<SwipeToArchive <SwipeToArchive
key={`issue:${issue.id}`} key={`issue:${issue.id}`}
disabled={isArchiving || archiveIssueMutation.isPending} disabled={isArchiving || archiveIssueMutation.isPending}