Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/mobile/src/components/AppSymbol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
IconMinus,
IconNetwork,
IconPalette,
IconPin,
IconPinnedOff,
IconPlayerPlay,
IconPlayerStopFilled,
IconPlus,
Expand Down Expand Up @@ -121,6 +123,8 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
magnifyingglass: IconSearch,
paintbrush: IconPalette,
"person.crop.circle": IconUserCircle,
pin: IconPin,
"pin.slash": IconPinnedOff,
play: IconPlayerPlay,
plus: IconPlus,
"qrcode.viewfinder": IconQrcode,
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export function HomeRouteScreen() {
settleThread,
snoozeThread,
unsnoozeThread,
pinThread,
unpinThread,
unsettleThread,
} = useThreadListActions();
const pendingTasks = usePendingNewTasks();
Expand Down Expand Up @@ -155,6 +157,8 @@ export function HomeRouteScreen() {
onSnoozeThread={snoozeThread}
onUnsnoozeThread={unsnoozeThread}
onUnsettleThread={unsettleThread}
onPinThread={pinThread}
onUnpinThread={unpinThread}
onEnvironmentChange={setSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
onOpenEnvironments={() =>
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/src/features/home/HomeScreen.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ interface HomeScreenProps {
) => Promise<boolean>;
readonly onUnsnoozeThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void;
readonly onPinThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onUnpinThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void;
readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
readonly onNewThreadInProject: (project: EnvironmentProject) => void;
Expand Down Expand Up @@ -516,6 +518,18 @@ export function HomeScreen(props: HomeScreenProps) {
},
[props.onUnsnoozeThread],
);
const handlePinThread = useCallback(
(thread: EnvironmentThreadShell) => {
void props.onPinThread(thread);
},
[props.onPinThread],
);
const handleUnpinThread = useCallback(
(thread: EnvironmentThreadShell) => {
void props.onUnpinThread(thread);
},
[props.onUnpinThread],
);
const handleDeleteThread = props.onDeleteThread;
const handleUnsettleThread = props.onUnsettleThread;
// The settled tail renders in pages; expansion resets when the filter
Expand Down Expand Up @@ -575,6 +589,15 @@ export function HomeScreen(props: HomeScreenProps) {
}
return supported;
}, [serverConfigs]);
const pinningEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
if (config.environment.capabilities.threadPinning === true) {
supported.add(environmentId);
}
}
return supported;
}, [serverConfigs]);
const threadListV2Layout = useMemo(() => {
if (!threadListV2Enabled)
return {
Expand Down Expand Up @@ -718,6 +741,7 @@ export function HomeScreen(props: HomeScreenProps) {
thread={thread}
variant={item.item.variant}
snoozed={item.item.snoozed}
pinned={item.item.pinned}
snoozePresetMinute={nowMinute}
snoozeWakeLabelText={item.snoozeWakeLabelText}
project={
Expand Down Expand Up @@ -753,9 +777,12 @@ export function HomeScreen(props: HomeScreenProps) {
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
onSettleThread={handleSettleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
pinningSupported={pinningEnvironmentIds.has(thread.environmentId)}
onSnoozeThread={handleSnoozeThread}
onUnsnoozeThread={handleUnsnoozeThread}
onUnsettleThread={handleUnsettleThread}
onPinThread={handlePinThread}
onUnpinThread={handleUnpinThread}
Comment thread
cursor[bot] marked this conversation as resolved.
onChangeRequestState={handleChangeRequestState}
projectCwd={
projectCwdByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
Expand All @@ -768,12 +795,15 @@ export function HomeScreen(props: HomeScreenProps) {
[
handleChangeRequestState,
handleDeleteThread,
handlePinThread,
handleSettleThread,
handleSnoozeThread,
handleUnpinThread,
handleUnsnoozeThread,
handleSwipeableClose,
handleSwipeableWillOpen,
handleUnsettleThread,
pinningEnvironmentIds,
projectByKey,
projectCwdByKey,
props.onArchiveThread,
Expand Down
69 changes: 69 additions & 0 deletions apps/mobile/src/features/home/useThreadListActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ function environmentSupportsSnooze(environmentId: EnvironmentThreadShell["enviro
);
}

function environmentSupportsPinning(environmentId: EnvironmentThreadShell["environmentId"]) {
return (
appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities
.threadPinning === true
);
}

type ThreadListAction = "archive" | "unarchive" | "delete" | "settle" | "unsettle";

const ACTION_VERBS: Record<ThreadListAction, string> = {
Expand Down Expand Up @@ -202,10 +209,14 @@ export function useThreadListActions(): {
readonly snoozeThread: (thread: EnvironmentThreadShell, snoozedUntil: string) => Promise<boolean>;
readonly unsnoozeThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly unsettleThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly pinThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly unpinThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
} {
const executeAction = useThreadActionExecutor();
const snoozeMutation = useAtomCommand(threadEnvironment.snooze, { reportFailure: false });
const unsnoozeMutation = useAtomCommand(threadEnvironment.unsnooze, { reportFailure: false });
const pinMutation = useAtomCommand(threadEnvironment.pin, { reportFailure: false });
const unpinMutation = useAtomCommand(threadEnvironment.unpin, { reportFailure: false });
const snoozeInFlightThreadKeys = useRef(new Set<string>());

const archiveThread = useCallback(
Expand Down Expand Up @@ -310,6 +321,62 @@ export function useThreadListActions(): {
async (thread: EnvironmentThreadShell) => (await executeAction("unsettle", thread)) === true,
[executeAction],
);
const pinThread = useCallback(
async (thread: EnvironmentThreadShell) => {
if (!environmentSupportsPinning(thread.environmentId)) {
Alert.alert(
"Could not pin thread",
"This environment's server does not support pinning yet. Update the server to use Pin.",
);
return false;
}
selectionHaptic();
const result = await pinMutation({
environmentId: thread.environmentId,
input: { threadId: thread.id },
});
if (result._tag === "Failure") {
const error = Cause.squash(result.cause);
Alert.alert(
"Could not pin thread",
error instanceof Error && error.message.trim().length > 0
? error.message
: "The thread could not be pinned.",
);
return false;
}
return true;
},
[pinMutation],
);
const unpinThread = useCallback(
async (thread: EnvironmentThreadShell) => {
if (!environmentSupportsPinning(thread.environmentId)) {
Alert.alert(
"Could not unpin thread",
"This environment's server does not support pinning yet. Update the server to use Pin.",
);
return false;
}
selectionHaptic();
const result = await unpinMutation({
environmentId: thread.environmentId,
input: { threadId: thread.id },
});
if (result._tag === "Failure") {
const error = Cause.squash(result.cause);
Alert.alert(
"Could not unpin thread",
error instanceof Error && error.message.trim().length > 0
? error.message
: "The thread could not be unpinned.",
);
return false;
}
return true;
},
[unpinMutation],
);

const confirmDeleteThread = useConfirmDeleteThread(executeAction);

Expand All @@ -320,6 +387,8 @@ export function useThreadListActions(): {
snoozeThread,
unsnoozeThread,
unsettleThread,
pinThread,
unpinThread,
};
}

Expand Down
19 changes: 19 additions & 0 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ function ThreadNavigationSidebarPane(
snoozeThread,
unsnoozeThread,
unsettleThread,
pinThread,
unpinThread,
} = useThreadListActions();
const threadListV2Enabled = useThreadListV2Enabled();
const pendingTasks = usePendingNewTasks();
Expand Down Expand Up @@ -481,6 +483,15 @@ function ThreadNavigationSidebarPane(
}
return supported;
}, [serverConfigs]);
const pinningEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
if (config.environment.capabilities.threadPinning === true) {
supported.add(environmentId);
}
}
return supported;
}, [serverConfigs]);
const threadListV2Layout = useMemo(() => {
if (!threadListV2Enabled)
return {
Expand Down Expand Up @@ -792,6 +803,7 @@ function ThreadNavigationSidebarPane(
previous.item.thread === item.item.thread &&
previous.item.variant === item.item.variant &&
previous.item.snoozed === item.item.snoozed &&
previous.item.pinned === item.item.pinned &&
previous.snoozeWakeLabelText === item.snoozeWakeLabelText
);
}
Expand Down Expand Up @@ -879,6 +891,7 @@ function ThreadNavigationSidebarPane(
thread={thread}
variant={item.item.variant}
snoozed={item.item.snoozed}
pinned={item.item.pinned}
snoozePresetMinute={nowMinute}
snoozeWakeLabelText={item.snoozeWakeLabelText}
project={projectByKey.get(scopeKey) ?? null}
Expand Down Expand Up @@ -915,9 +928,12 @@ function ThreadNavigationSidebarPane(
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
onSettleThread={settleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
pinningSupported={pinningEnvironmentIds.has(thread.environmentId)}
onSnoozeThread={snoozeThread}
onUnsnoozeThread={unsnoozeThread}
onUnsettleThread={unsettleThread}
onPinThread={pinThread}
onUnpinThread={unpinThread}
onChangeRequestState={handleChangeRequestState}
projectCwd={projectCwdByKey.get(scopeKey) ?? null}
onSwipeableClose={handleSwipeableClose}
Expand Down Expand Up @@ -1045,6 +1061,8 @@ function ThreadNavigationSidebarPane(
handleSwipeableClose,
handleSwipeableWillOpen,
openPendingTask,
pinThread,
pinningEnvironmentIds,
projectByKey,
projectCwdByKey,
projectTitleByProjectKey,
Expand All @@ -1064,6 +1082,7 @@ function ThreadNavigationSidebarPane(
nowMinute,
toggleSettledShelf,
toggleSnoozedShelf,
unpinThread,
unsettleThread,
unsnoozeThread,
updateGroupDisplay,
Expand Down
Loading
Loading