Skip to content

Commit feab8cd

Browse files
fix(mobile): address snooze review feedback
1 parent 571605a commit feab8cd

5 files changed

Lines changed: 91 additions & 72 deletions

File tree

‎apps/mobile/src/features/home/HomeScreen.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ export function HomeScreen(props: HomeScreenProps) {
700700
variant={item.item.variant}
701701
showSettledDivider={item.item.showSettledDivider}
702702
snoozed={item.item.snoozed}
703+
snoozePresetMinute={nowMinute}
703704
snoozeWakeLabelText={item.snoozeWakeLabelText}
704705
project={
705706
projectByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
@@ -785,6 +786,7 @@ export function HomeScreen(props: HomeScreenProps) {
785786
serverConfigs,
786787
savedConnectionsById: props.savedConnectionsById,
787788
searchQuery: props.searchQuery,
789+
snoozePresetMinute: nowMinute,
788790
threadSearchMatchByKey,
789791
}),
790792
[
@@ -793,6 +795,7 @@ export function HomeScreen(props: HomeScreenProps) {
793795
props.searchQuery,
794796
props.savedConnectionsById,
795797
serverConfigs,
798+
nowMinute,
796799
threadSearchMatchByKey,
797800
v2ProjectTitleByProjectKey,
798801
],

‎apps/mobile/src/features/home/thread-swipe-actions.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,9 @@ export function ThreadSwipeable(props: {
229229
readonly enabled?: boolean;
230230
readonly enableTrackpadSwipe?: boolean;
231231
/**
232-
* What a full swipe commits: "delete" (default, v1 behavior — the Delete
233-
* button stretches) or "primary" — the advertised primary action fires and
234-
* its button stretches instead. A full swipe must always match the action
235-
* the stretching button advertises.
232+
* What a full swipe commits. Omitted keeps the v1 Delete behavior only when
233+
* the built-in Delete secondary action is in use; custom or absent
234+
* secondary actions default to the advertised primary action.
236235
*/
237236
readonly fullSwipeAction?: "delete" | "primary";
238237
readonly fullSwipeWidth: number;
@@ -262,7 +261,8 @@ export function ThreadSwipeable(props: {
262261
const hasSecondaryAction = props.secondaryAction !== null;
263262
const actionsWidth = swipeActionsWidth(hasSecondaryAction);
264263
const fullSwipeThreshold = Math.max(actionsWidth + 44, props.fullSwipeWidth * 0.58);
265-
const fullSwipeAction = hasSecondaryAction ? (props.fullSwipeAction ?? "delete") : "primary";
264+
const fullSwipeAction =
265+
props.fullSwipeAction ?? (props.secondaryAction === undefined ? "delete" : "primary");
266266
const close = useCallback(() => swipeableRef.current?.close(), []);
267267
const gateEnabled = use(SwipeableScrollGateContext);
268268
const resetKey = props.resetKey;

‎apps/mobile/src/features/home/useThreadListActions.ts‎

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ export function useThreadListActions(): {
206206
const executeAction = useThreadActionExecutor();
207207
const snoozeMutation = useAtomCommand(threadEnvironment.snooze, { reportFailure: false });
208208
const unsnoozeMutation = useAtomCommand(threadEnvironment.unsnooze, { reportFailure: false });
209+
const snoozeInFlightThreadKeys = useRef(new Set<string>());
209210

210211
const archiveThread = useCallback(
211212
(thread: EnvironmentThreadShell) => {
@@ -219,71 +220,89 @@ export function useThreadListActions(): {
219220
);
220221
const snoozeThread = useCallback(
221222
async (thread: EnvironmentThreadShell, snoozedUntil: string) => {
222-
if (!environmentSupportsSnooze(thread.environmentId)) {
223-
Alert.alert(
224-
"Could not snooze thread",
225-
"This environment's server does not support snoozing yet. Update the server to use Snooze.",
226-
);
227-
return false;
228-
}
229-
if (!canSnooze(thread, { now: new Date().toISOString() })) {
230-
Alert.alert(
231-
"Could not snooze thread",
232-
thread.hasPendingApprovals || thread.hasPendingUserInput
233-
? "This thread is waiting on you. Respond to the pending request before snoozing it."
234-
: "This thread is still starting a turn. Try again once it's running.",
235-
);
223+
const key = scopedThreadKey(thread.environmentId, thread.id);
224+
if (snoozeInFlightThreadKeys.current.has(key)) {
236225
return false;
237226
}
227+
snoozeInFlightThreadKeys.current.add(key);
228+
try {
229+
if (!environmentSupportsSnooze(thread.environmentId)) {
230+
Alert.alert(
231+
"Could not snooze thread",
232+
"This environment's server does not support snoozing yet. Update the server to use Snooze.",
233+
);
234+
return false;
235+
}
236+
if (!canSnooze(thread, { now: new Date().toISOString() })) {
237+
Alert.alert(
238+
"Could not snooze thread",
239+
thread.hasPendingApprovals || thread.hasPendingUserInput
240+
? "This thread is waiting on you. Respond to the pending request before snoozing it."
241+
: "This thread is still starting a turn. Try again once it's running.",
242+
);
243+
return false;
244+
}
238245

239-
selectionHaptic();
240-
const result = await snoozeMutation({
241-
environmentId: thread.environmentId,
242-
input: {
243-
threadId: thread.id,
244-
snoozedUntil,
245-
},
246-
});
247-
if (result._tag === "Failure") {
248-
const error = Cause.squash(result.cause);
249-
Alert.alert(
250-
"Could not snooze thread",
251-
error instanceof Error && error.message.trim().length > 0
252-
? error.message
253-
: "The thread could not be snoozed.",
254-
);
255-
return false;
246+
selectionHaptic();
247+
const result = await snoozeMutation({
248+
environmentId: thread.environmentId,
249+
input: {
250+
threadId: thread.id,
251+
snoozedUntil,
252+
},
253+
});
254+
if (result._tag === "Failure") {
255+
const error = Cause.squash(result.cause);
256+
Alert.alert(
257+
"Could not snooze thread",
258+
error instanceof Error && error.message.trim().length > 0
259+
? error.message
260+
: "The thread could not be snoozed.",
261+
);
262+
return false;
263+
}
264+
return true;
265+
} finally {
266+
snoozeInFlightThreadKeys.current.delete(key);
256267
}
257-
return true;
258268
},
259269
[snoozeMutation],
260270
);
261271
const unsnoozeThread = useCallback(
262272
async (thread: EnvironmentThreadShell) => {
263-
if (!environmentSupportsSnooze(thread.environmentId)) {
264-
Alert.alert(
265-
"Could not wake thread",
266-
"This environment's server does not support snoozing yet. Update the server to wake this thread.",
267-
);
273+
const key = scopedThreadKey(thread.environmentId, thread.id);
274+
if (snoozeInFlightThreadKeys.current.has(key)) {
268275
return false;
269276
}
277+
snoozeInFlightThreadKeys.current.add(key);
278+
try {
279+
if (!environmentSupportsSnooze(thread.environmentId)) {
280+
Alert.alert(
281+
"Could not wake thread",
282+
"This environment's server does not support snoozing yet. Update the server to wake this thread.",
283+
);
284+
return false;
285+
}
270286

271-
selectionHaptic();
272-
const result = await unsnoozeMutation({
273-
environmentId: thread.environmentId,
274-
input: { threadId: thread.id, reason: "user" },
275-
});
276-
if (result._tag === "Failure") {
277-
const error = Cause.squash(result.cause);
278-
Alert.alert(
279-
"Could not wake thread",
280-
error instanceof Error && error.message.trim().length > 0
281-
? error.message
282-
: "The thread could not be woken.",
283-
);
284-
return false;
287+
selectionHaptic();
288+
const result = await unsnoozeMutation({
289+
environmentId: thread.environmentId,
290+
input: { threadId: thread.id, reason: "user" },
291+
});
292+
if (result._tag === "Failure") {
293+
const error = Cause.squash(result.cause);
294+
Alert.alert(
295+
"Could not wake thread",
296+
error instanceof Error && error.message.trim().length > 0
297+
? error.message
298+
: "The thread could not be woken.",
299+
);
300+
return false;
301+
}
302+
return true;
303+
} finally {
304+
snoozeInFlightThreadKeys.current.delete(key);
285305
}
286-
return true;
287306
},
288307
[unsnoozeMutation],
289308
);

‎apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ function ThreadNavigationSidebarPane(
759759
projectTitleByProjectKey,
760760
savedConnectionsById,
761761
serverConfigs,
762+
snoozePresetMinute: nowMinute,
762763
threadSearchMatchByKey,
763764
}),
764765
[
@@ -768,6 +769,7 @@ function ThreadNavigationSidebarPane(
768769
projectTitleByProjectKey,
769770
savedConnectionsById,
770771
serverConfigs,
772+
nowMinute,
771773
threadSearchMatchByKey,
772774
],
773775
);
@@ -863,6 +865,7 @@ function ThreadNavigationSidebarPane(
863865
variant={item.item.variant}
864866
showSettledDivider={item.item.showSettledDivider}
865867
snoozed={item.item.snoozed}
868+
snoozePresetMinute={nowMinute}
866869
snoozeWakeLabelText={item.snoozeWakeLabelText}
867870
project={projectByKey.get(scopeKey) ?? null}
868871
projectTitle={projectTitleByProjectKey.get(scopeKey)}
@@ -1035,6 +1038,7 @@ function ThreadNavigationSidebarPane(
10351038
sidebarScrollGesture,
10361039
snoozeEnvironmentIds,
10371040
snoozeThread,
1041+
nowMinute,
10381042
toggleSnoozedShelf,
10391043
unsettleThread,
10401044
unsnoozeThread,

‎apps/mobile/src/features/threads/thread-list-v2-items.tsx‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
265265
/** Preformatted against the parent minute tick so this memoized row's
266266
countdown keeps moving. */
267267
readonly snoozeWakeLabelText?: string;
268+
/** Parent minute tick passed as a prop so this memoized row refreshes its
269+
native snooze menu while mounted. */
270+
readonly snoozePresetMinute: string;
268271
readonly project: EnvironmentProject | null;
269272
readonly projectTitle?: string;
270273
readonly providerDriver: string | null;
@@ -375,10 +378,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
375378
snoozable: canSnooze(thread, { now: new Date().toISOString() }),
376379
snoozed: snoozedRow,
377380
});
378-
const snoozePresetMinute = new Date().toISOString().slice(0, 16);
379381
const snoozePresets = useMemo(
380382
() => (swipeActions.secondary === "snooze" ? resolveSnoozePresets(new Date()) : ([] as const)),
381-
[snoozePresetMinute, swipeActions.secondary],
383+
[props.snoozePresetMinute, swipeActions.secondary],
382384
);
383385
const snoozePresetActions = useMemo<MenuAction[]>(
384386
() =>
@@ -409,21 +411,12 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
409411
if (nativeEvent.event === "unsnooze") handleUnsnooze();
410412
if (nativeEvent.event === "archive") handleArchive();
411413
if (nativeEvent.event === "delete") handleDelete();
412-
const preset =
413-
resolveSnoozePresets(new Date()).find(
414-
(candidate) => nativeEvent.event === `snooze:${candidate.id}`,
415-
) ?? snoozePresets.find((candidate) => nativeEvent.event === `snooze:${candidate.id}`);
414+
const preset = resolveSnoozePresets(new Date()).find(
415+
(candidate) => nativeEvent.event === `snooze:${candidate.id}`,
416+
);
416417
if (preset) handleSnooze(preset.snoozedUntil);
417418
},
418-
[
419-
handleArchive,
420-
handleDelete,
421-
handleSettle,
422-
handleSnooze,
423-
handleUnsettle,
424-
handleUnsnooze,
425-
snoozePresets,
426-
],
419+
[handleArchive, handleDelete, handleSettle, handleSnooze, handleUnsettle, handleUnsnooze],
427420
);
428421
const primaryAction = useMemo(() => {
429422
// Pre-settlement server: archive is the swipe action, as in v1. (Slim

0 commit comments

Comments
 (0)