Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/mobile/src/components/AppSymbol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
IconChevronUp,
IconCircleCheck,
IconCircleXFilled,
IconClock,
IconCopy,
IconDeviceDesktop,
IconDots,
Expand Down Expand Up @@ -96,6 +97,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
camera: IconCamera,
checkmark: IconCheck,
"checkmark.circle": IconCircleCheck,
clock: IconClock,
"chevron.down": IconChevronDown,
"chevron.left": IconChevronLeft,
"chevron.left.forwardslash.chevron.right": IconCode,
Expand Down
12 changes: 10 additions & 2 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ export function HomeRouteScreen() {
void checkForAppUpdateOnLaunch();
}, []);

const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } =
useThreadListActions();
const {
archiveThread,
confirmDeleteThread,
settleThread,
snoozeThread,
unsnoozeThread,
unsettleThread,
} = useThreadListActions();
const pendingTasks = usePendingNewTasks();
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
const environments = useMemo(() => {
Expand Down Expand Up @@ -143,6 +149,8 @@ export function HomeRouteScreen() {
onArchiveThread={archiveThread}
onDeleteThread={confirmDeleteThread}
onSettleThread={settleThread}
onSnoozeThread={snoozeThread}
onUnsnoozeThread={unsnoozeThread}
onUnsettleThread={unsettleThread}
onEnvironmentChange={setSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
Expand Down
92 changes: 65 additions & 27 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ import {
ThreadListRow,
ThreadListShowMoreRow,
} from "../threads/thread-list-items";
import { ThreadListV2PendingRow, ThreadListV2Row } from "../threads/thread-list-v2-items";
import {
ThreadListV2PendingRow,
ThreadListV2Row,
ThreadListV2SnoozedShelfHeader,
} from "../threads/thread-list-v2-items";
import {
buildThreadListV2Items,
buildThreadListV2ListItems,
Expand Down Expand Up @@ -100,6 +104,11 @@ interface HomeScreenProps {
readonly onDeleteThread: (thread: EnvironmentThreadShell) => void;
/** Resolves true iff the settle was dispatched and succeeded. */
readonly onSettleThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onSnoozeThread: (
thread: EnvironmentThreadShell,
snoozedUntil: string,
) => Promise<boolean>;
readonly onUnsnoozeThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void;
readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void;
readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
Expand Down Expand Up @@ -494,6 +503,18 @@ export function HomeScreen(props: HomeScreenProps) {
},
[props.onSettleThread],
);
const handleSnoozeThread = useCallback(
(thread: EnvironmentThreadShell, snoozedUntil: string) => {
void props.onSnoozeThread(thread, snoozedUntil);
},
[props.onSnoozeThread],
);
const handleUnsnoozeThread = useCallback(
(thread: EnvironmentThreadShell) => {
void props.onUnsnoozeThread(thread);
},
[props.onUnsnoozeThread],
);
const handleDeleteThread = props.onDeleteThread;
const handleUnsettleThread = props.onUnsettleThread;
// The settled tail renders in pages; expansion resets when the filter
Expand All @@ -511,6 +532,8 @@ export function HomeScreen(props: HomeScreenProps) {
() => setSettledVisibleCount((count) => count + THREAD_LIST_V2_SETTLED_PAGE_COUNT),
[],
);
const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false);
const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []);
// now is quantized to the minute and ticks so the inactivity auto-settle
// boundary is actually crossed while the app stays open (mirrors web);
// without a clock dependency the partition memoizes a frozen "now".
Expand Down Expand Up @@ -551,7 +574,13 @@ export function HomeScreen(props: HomeScreenProps) {
}, [serverConfigs]);
const threadListV2Layout = useMemo(() => {
if (!threadListV2Enabled)
return { items: [], hiddenSettledCount: 0, snoozedCount: 0, nextSnoozeWakeAt: null };
return {
items: [],
hiddenSettledCount: 0,
snoozedCount: 0,
snoozedShelfHeaderIndex: null,
nextSnoozeWakeAt: null,
};
// Settled threads are live shells; archived threads keep their original
// "hidden from lists" meaning.
return buildThreadListV2Items({
Expand All @@ -566,11 +595,14 @@ export function HomeScreen(props: HomeScreenProps) {
settledLimit: settledVisibleCount,
now: `${nowMinute}:00.000Z`,
snoozeNow: new Date().toISOString(),
snoozedShelfExpanded,
selectedThreadKey: null,
});
}, [
changeRequestStateByKey,
nowMinute,
snoozeWakeTick,
snoozedShelfExpanded,
settledVisibleCount,
settlementEnvironmentIds,
snoozeEnvironmentIds,
Expand Down Expand Up @@ -620,8 +652,12 @@ export function HomeScreen(props: HomeScreenProps) {
buildThreadListV2ListItems({
items: threadListV2Layout.items,
pendingTasks: v2PendingTasks,
snoozedCount: threadListV2Layout.snoozedCount,
snoozedShelfExpanded,
snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
snoozeLabelNow: `${nowMinute}:00.000Z`,
}),
[threadListV2Layout.items, v2PendingTasks],
[snoozedShelfExpanded, threadListV2Layout, v2PendingTasks],
);

const renderV2Item = useCallback(
Expand All @@ -648,12 +684,24 @@ export function HomeScreen(props: HomeScreenProps) {
/>
);
}
if (item.type === "v2-snoozed-shelf") {
return (
<ThreadListV2SnoozedShelfHeader
count={item.count}
expanded={item.expanded}
onToggle={toggleSnoozedShelf}
/>
);
}
const thread = item.item.thread;
return (
<ThreadListV2Row
thread={thread}
variant={item.item.variant}
showSettledDivider={item.item.showSettledDivider}
snoozed={item.item.snoozed}
snoozePresetMinute={nowMinute}
snoozeWakeLabelText={item.snoozeWakeLabelText}
project={
projectByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
}
Expand Down Expand Up @@ -686,6 +734,9 @@ export function HomeScreen(props: HomeScreenProps) {
onArchiveThread={props.onArchiveThread}
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
onSettleThread={handleSettleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
onSnoozeThread={handleSnoozeThread}
onUnsnoozeThread={handleUnsnoozeThread}
onUnsettleThread={handleUnsettleThread}
onChangeRequestState={handleChangeRequestState}
projectCwd={
Expand All @@ -700,6 +751,8 @@ export function HomeScreen(props: HomeScreenProps) {
handleChangeRequestState,
handleDeleteThread,
handleSettleThread,
handleSnoozeThread,
handleUnsnoozeThread,
handleSwipeableClose,
handleSwipeableWillOpen,
handleUnsettleThread,
Expand All @@ -712,9 +765,12 @@ export function HomeScreen(props: HomeScreenProps) {
props.savedConnectionsById,
serverConfigs,
settlementEnvironmentIds,
snoozeEnvironmentIds,
threadSearchMatchByKey,
toggleSnoozedShelf,
v2ProjectTitleByProjectKey,
props.searchQuery,
nowMinute,
],
);
const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []);
Expand All @@ -730,6 +786,7 @@ export function HomeScreen(props: HomeScreenProps) {
serverConfigs,
savedConnectionsById: props.savedConnectionsById,
searchQuery: props.searchQuery,
snoozePresetMinute: nowMinute,
threadSearchMatchByKey,
}),
[
Expand All @@ -738,6 +795,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.searchQuery,
props.savedConnectionsById,
serverConfigs,
nowMinute,
threadSearchMatchByKey,
v2ProjectTitleByProjectKey,
],
Expand Down Expand Up @@ -953,31 +1011,11 @@ export function HomeScreen(props: HomeScreenProps) {
) : null;
// Self-contained: v1's listEmpty keys off projectGroups, which ignores the
// v2 project scope, so it can be null (results elsewhere) while this list
// is empty. Search outranks the scope — "No results" names the actionable
// fact when a query is active. Snoozed threads outrank the rest: "No
// threads yet" over an inbox that is merely all-snoozed reads as data
// loss.
const v2SnoozedCount = threadListV2Layout.snoozedCount;
// is empty. Snoozed threads need no special empty state: their shelf header
// is a list row even while collapsed.
const v2ListEmpty =
hasSearchQuery && threadSearch.isPending && v2SnoozedCount === 0 ? null : hasSearchQuery ? (
v2SnoozedCount > 0 ? (
// The snoozed threads already passed this search filter: "No
// results" would claim nothing matched when matches are merely
// parked.
<EmptyState
title={
v2SnoozedCount === 1 ? "1 matching thread snoozed" : `All matching threads snoozed`
}
detail={`Threads matching "${props.searchQuery}" are snoozed and return when their wake time passes.`}
/>
) : (
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
)
) : v2SnoozedCount > 0 ? (
<EmptyState
title={v2SnoozedCount === 1 ? "1 thread snoozed" : `${v2SnoozedCount} threads snoozed`}
detail="Snoozed threads return when their wake time passes."
/>
hasSearchQuery && threadSearch.isPending ? null : hasSearchQuery ? (
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
) : v2ScopedProjectGroup !== null ? (
<EmptyState
title={`No threads in ${v2ScopedProjectGroup.title}`}
Expand Down
Loading
Loading