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
2 changes: 2 additions & 0 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function HomeRouteScreen() {
pinThread,
unpinThread,
setThreadAutoSettle,
setThreadMuted,
moveThread,
renameThread,
regenerateThreadTitle,
Expand Down Expand Up @@ -224,6 +225,7 @@ export function HomeRouteScreen() {
onPinThread={pinThread}
onUnpinThread={unpinThread}
onSetThreadAutoSettle={setThreadAutoSettle}
onSetThreadMuted={setThreadMuted}
onMoveThread={moveThread}
onRenameThread={renameThread}
onRegenerateThreadTitle={regenerateThreadTitle}
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ interface HomeScreenProps {
thread: EnvironmentThreadShell,
enabled: boolean,
) => Promise<boolean>;
readonly onSetThreadMuted: (thread: EnvironmentThreadShell, muted: boolean) => Promise<boolean>;
readonly onMoveThread: (
thread: EnvironmentThreadShell,
direction: ThreadMoveDestination,
Expand Down Expand Up @@ -437,6 +438,12 @@ export function HomeScreen(props: HomeScreenProps) {
},
[props.onSetThreadAutoSettle],
);
const handleSetThreadMuted = useCallback(
(thread: EnvironmentThreadShell, muted: boolean) => {
void props.onSetThreadMuted(thread, muted);
},
[props.onSetThreadMuted],
);
const handleRegenerateThreadTitle = useCallback(
(thread: EnvironmentThreadShell) => {
void props.onRegenerateThreadTitle(thread);
Expand Down Expand Up @@ -498,6 +505,7 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeEnvironmentIds,
pinningEnvironmentIds,
autoSettleOptOutEnvironmentIds,
muteEnvironmentIds,
pinReorderEnvironmentIds,
activeReorderEnvironmentIds,
titleRegenerationEnvironmentIds,
Expand Down Expand Up @@ -757,6 +765,7 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
pinningSupported={pinningEnvironmentIds.has(thread.environmentId)}
autoSettleOptOutSupported={autoSettleOptOutEnvironmentIds.has(thread.environmentId)}
muteSupported={muteEnvironmentIds.has(thread.environmentId)}
reorderSupported={
item.item.pinned
? pinReorderEnvironmentIds.has(thread.environmentId)
Expand All @@ -770,6 +779,7 @@ export function HomeScreen(props: HomeScreenProps) {
onPinThread={handlePinThread}
onUnpinThread={handleUnpinThread}
onSetThreadAutoSettle={handleSetThreadAutoSettle}
onSetThreadMuted={handleSetThreadMuted}
onMoveThread={handleMoveThread}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand All @@ -792,7 +802,9 @@ export function HomeScreen(props: HomeScreenProps) {
handleSwipeableWillOpen,
handleUnsettleThread,
handleSetThreadAutoSettle,
handleSetThreadMuted,
autoSettleOptOutEnvironmentIds,
muteEnvironmentIds,
pinningEnvironmentIds,
autoSettleOptOutEnvironmentIds,
machineByEnvironmentId,
Expand Down
24 changes: 24 additions & 0 deletions apps/mobile/src/features/home/useThreadListActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export function useThreadListActions(): {
thread: EnvironmentThreadShell,
enabled: boolean,
) => Promise<boolean>;
readonly setThreadMuted: (thread: EnvironmentThreadShell, muted: boolean) => Promise<boolean>;
readonly moveThread: (
thread: EnvironmentThreadShell,
direction: ThreadMoveDestination,
Expand All @@ -273,6 +274,7 @@ export function useThreadListActions(): {
const setAutoSettleMutation = useAtomCommand(threadEnvironment.setAutoSettle, {
reportFailure: false,
});
const setMutedMutation = useAtomCommand(threadEnvironment.setMuted, { reportFailure: false });
const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, {
reportFailure: false,
});
Expand Down Expand Up @@ -491,6 +493,27 @@ export function useThreadListActions(): {
},
[setAutoSettleMutation],
);
const setThreadMuted = useCallback(
async (thread: EnvironmentThreadShell, muted: boolean) => {
selectionHaptic();
const result = await setMutedMutation({
environmentId: thread.environmentId,
input: { threadId: thread.id, muted },
});
if (result._tag === "Failure") {
const error = Cause.squash(result.cause);
Alert.alert(
"Could not update notifications",
error instanceof Error && error.message.trim().length > 0
? error.message
: "The notification setting could not be changed.",
);
return false;
}
return true;
},
[setMutedMutation],
);
const regenerateThreadTitle = useCallback(
async (thread: EnvironmentThreadShell) => {
if (!checkThreadOperationPermission(thread, "Could not regenerate title")) return false;
Expand Down Expand Up @@ -763,6 +786,7 @@ export function useThreadListActions(): {
pinThread,
unpinThread,
setThreadAutoSettle,
setThreadMuted,
moveThread,
renameThread,
regenerateThreadTitle,
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ function ThreadNavigationSidebarPane(
pinThread,
unpinThread,
setThreadAutoSettle,
setThreadMuted,
moveThread,
renameThread,
regenerateThreadTitle,
Expand Down Expand Up @@ -321,6 +322,7 @@ function ThreadNavigationSidebarPane(
snoozeEnvironmentIds,
pinningEnvironmentIds,
autoSettleOptOutEnvironmentIds,
muteEnvironmentIds,
pinReorderEnvironmentIds,
activeReorderEnvironmentIds,
titleRegenerationEnvironmentIds,
Expand Down Expand Up @@ -732,6 +734,7 @@ function ThreadNavigationSidebarPane(
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
pinningSupported={pinningEnvironmentIds.has(thread.environmentId)}
autoSettleOptOutSupported={autoSettleOptOutEnvironmentIds.has(thread.environmentId)}
muteSupported={muteEnvironmentIds.has(thread.environmentId)}
reorderSupported={
item.item.pinned
? pinReorderEnvironmentIds.has(thread.environmentId)
Expand All @@ -745,6 +748,7 @@ function ThreadNavigationSidebarPane(
onPinThread={pinThread}
onUnpinThread={unpinThread}
onSetThreadAutoSettle={setThreadAutoSettle}
onSetThreadMuted={setThreadMuted}
onMoveThread={moveThread}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand Down Expand Up @@ -809,6 +813,8 @@ function ThreadNavigationSidebarPane(
autoSettleOptOutEnvironmentIds,
autoSettleOptOutEnvironmentIds,
setThreadAutoSettle,
muteEnvironmentIds,
setThreadMuted,
projectByKey,
projectTitleByProjectKey,
regenerateThreadTitle,
Expand Down
54 changes: 39 additions & 15 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
readonly onPinThread: (thread: EnvironmentThreadShell) => void;
readonly onUnpinThread: (thread: EnvironmentThreadShell) => void;
readonly onSetThreadAutoSettle: (thread: EnvironmentThreadShell, enabled: boolean) => void;
readonly onSetThreadMuted: (thread: EnvironmentThreadShell, muted: boolean) => void;
/** False on environments whose server predates thread.settle/unsettle:
swipe + menu fall back to Archive instead of failing on use. */
readonly settlementSupported: boolean;
Expand All @@ -520,6 +521,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
readonly pinningSupported: boolean;
/** False on servers that predate thread.auto-settle.set. */
readonly autoSettleOptOutSupported: boolean;
/** False on servers that predate thread.mute.set. */
readonly muteSupported: boolean;
/** False on servers that predate thread title regeneration. */
readonly titleRegenerationSupported: boolean;
/** Server supports reordering this card's section. */
Expand Down Expand Up @@ -557,6 +560,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
onPinThread,
onUnpinThread,
onSetThreadAutoSettle,
onSetThreadMuted,
onMoveThread,
} = props;
const snoozedRow = props.snoozed === true;
Expand Down Expand Up @@ -633,6 +637,10 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
(enabled: boolean) => onSetThreadAutoSettle(thread, enabled),
[onSetThreadAutoSettle, thread],
);
const handleSetMuted = useCallback(
(muted: boolean) => onSetThreadMuted(thread, muted),
[onSetThreadMuted, thread],
);
const handleMoveUp = useCallback(() => onMoveThread?.(thread, "up"), [onMoveThread, thread]);
const handleMoveDown = useCallback(() => onMoveThread?.(thread, "down"), [onMoveThread, thread]);
const handleArchive = useCallback(() => onArchiveThread(thread), [onArchiveThread, thread]);
Expand Down Expand Up @@ -711,11 +719,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
variant,
],
);
// A submenu with the current option checked, matching web. This is a
// per-thread setting, not a lifecycle verb.
const autoSettleMenuItems = useMemo<MenuAction[]>(
() =>
props.autoSettleOptOutSupported
// Per-thread settings, not lifecycle verbs. Auto-settle is a submenu with
// the current option checked, matching web.
const threadSettingMenuItems = useMemo<MenuAction[]>(
() => [
...(props.autoSettleOptOutSupported
? [
{
id: "auto-settle",
Expand All @@ -735,8 +743,21 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
],
} satisfies MenuAction,
]
: [],
[props.autoSettleOptOutSupported, thread.autoSettleDisabledAt],
: []),
...(props.muteSupported
? [
thread.mutedAt != null
? { id: "unmute", title: "Unmute notifications", image: "bell" }
: { id: "mute", title: "Mute notifications", image: "bell.slash" },
]
: []),
],
[
props.autoSettleOptOutSupported,
props.muteSupported,
thread.autoSettleDisabledAt,
thread.mutedAt,
],
);
const titleMenuItems = useMemo<MenuAction[]>(
() => [
Expand All @@ -759,20 +780,20 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
},
...arrangementMenuItems,
...titleMenuItems,
...autoSettleMenuItems,
...threadSettingMenuItems,
{ id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } },
],
[arrangementMenuItems, autoSettleMenuItems, snoozePresetActions, titleMenuItems],
[arrangementMenuItems, threadSettingMenuItems, snoozePresetActions, titleMenuItems],
);
const cardMenuActions = useMemo<MenuAction[]>(
() => [
CARD_MENU_ACTIONS[0]!,
...arrangementMenuItems,
...titleMenuItems,
...autoSettleMenuItems,
...threadSettingMenuItems,
...CARD_MENU_ACTIONS.slice(1),
],
[arrangementMenuItems, autoSettleMenuItems, titleMenuItems],
[arrangementMenuItems, threadSettingMenuItems, titleMenuItems],
);
// Settled and snoozed rows keep the setting too, matching web where every
// row shares one menu builder.
Expand All @@ -783,19 +804,19 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
(action) => action.id !== "move-up" && action.id !== "move-down",
),
...titleMenuItems,
...autoSettleMenuItems,
...threadSettingMenuItems,
SLIM_MENU_ACTIONS[1]!,
],
[arrangementMenuItems, autoSettleMenuItems, titleMenuItems],
[arrangementMenuItems, threadSettingMenuItems, titleMenuItems],
);
const snoozedMenuActions = useMemo<MenuAction[]>(
() => [
SNOOZED_MENU_ACTIONS[0]!,
...titleMenuItems,
...autoSettleMenuItems,
...threadSettingMenuItems,
SNOOZED_MENU_ACTIONS[1]!,
],
[autoSettleMenuItems, titleMenuItems],
[threadSettingMenuItems, titleMenuItems],
);
const legacyMenuActions = useMemo<MenuAction[]>(
() => [
Expand All @@ -816,6 +837,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
if (nativeEvent.event === "unpin") handleUnpin();
if (nativeEvent.event === "auto-settle:enabled") handleSetAutoSettle(true);
if (nativeEvent.event === "auto-settle:disabled") handleSetAutoSettle(false);
if (nativeEvent.event === "mute") handleSetMuted(true);
if (nativeEvent.event === "unmute") handleSetMuted(false);
if (nativeEvent.event === "arrange") appAtomRegistry.set(threadArrangementOpenAtom, true);
if (nativeEvent.event === "move-up") handleMoveUp();
if (nativeEvent.event === "move-down") handleMoveDown();
Expand Down Expand Up @@ -854,6 +877,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
handleSettle,
handleSnooze,
handleSetAutoSettle,
handleSetMuted,
handleUnpin,
handleUnsettle,
handleUnsnooze,
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/state/thread-list-environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const capabilityKeys = [
"threadSnooze",
"threadPinning",
"threadAutoSettleOptOut",
"threadMute",
"threadPinReorder",
"threadActiveReorder",
"threadTitleRegeneration",
Expand Down Expand Up @@ -66,6 +67,7 @@ function collectEnvironments(environments: ReadonlyMap<EnvironmentId, ListEnviro
const snoozeEnvironmentIds = new Set<EnvironmentId>();
const pinningEnvironmentIds = new Set<EnvironmentId>();
const autoSettleOptOutEnvironmentIds = new Set<EnvironmentId>();
const muteEnvironmentIds = new Set<EnvironmentId>();
const pinReorderEnvironmentIds = new Set<EnvironmentId>();
const activeReorderEnvironmentIds = new Set<EnvironmentId>();
const titleRegenerationEnvironmentIds = new Set<EnvironmentId>();
Expand All @@ -75,6 +77,7 @@ function collectEnvironments(environments: ReadonlyMap<EnvironmentId, ListEnviro
if (capabilities.threadSettlement === true) settlementEnvironmentIds.add(id);
if (capabilities.threadSnooze === true) snoozeEnvironmentIds.add(id);
if (capabilities.threadAutoSettleOptOut === true) autoSettleOptOutEnvironmentIds.add(id);
if (capabilities.threadMute === true) muteEnvironmentIds.add(id);
if (capabilities.threadPinning === true) pinningEnvironmentIds.add(id);
if (capabilities.threadPinReorder === true) pinReorderEnvironmentIds.add(id);
if (capabilities.threadActiveReorder === true) activeReorderEnvironmentIds.add(id);
Expand All @@ -87,6 +90,7 @@ function collectEnvironments(environments: ReadonlyMap<EnvironmentId, ListEnviro
snoozeEnvironmentIds,
pinningEnvironmentIds,
autoSettleOptOutEnvironmentIds,
muteEnvironmentIds,
pinReorderEnvironmentIds,
activeReorderEnvironmentIds,
titleRegenerationEnvironmentIds,
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/state/use-thread-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function threadDetailToShell(
unsettledAt: thread.unsettledAt,
activeOrderKey: thread.activeOrderKey,
autoSettleDisabledAt: thread.autoSettleDisabledAt,
mutedAt: thread.mutedAt,
pinnedAt: thread.pinnedAt,
pinOrderKey: thread.pinOrderKey,
snoozedUntil: thread.snoozedUntil ?? null,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const make = Effect.gen(function* () {
threadPinReorder: true,
threadActiveReorder: true,
threadAutoSettleOptOut: true,
threadMute: true,
threadTitleRegeneration: true,
threadVisitedTracking: true,
threadPullRequests: true,
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, {
case "mark_unread":
command = { ...common, type: "thread.mark-unread" };
break;
case "mute":
case "unmute":
command = { ...common, type: "thread.mute.set", muted: input.action === "mute" };
break;
default:
command = { ...common, type: `thread.${input.action}` };
}
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/mcp/toolkits/thread/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import * as McpInvocationContext from "../../McpInvocationContext.ts";

const ThreadOrganizeTool = Tool.make("t3_thread_organize", {
description:
"Pin, snooze, settle, archive, or mark a thread unread. Omit threadId for this thread. snooze requires snoozedUntil. Existing thread lifecycle rules apply; this does not schedule a future action.",
"Pin, snooze, settle, archive, mute, or mark a thread unread. Omit threadId for this thread. snooze requires snoozedUntil. Existing thread lifecycle rules apply; this does not schedule a future action.",
parameters: Schema.Struct({
threadId: Schema.optional(ThreadId),
action: Schema.Literals([
Expand All @@ -43,6 +43,8 @@ const ThreadOrganizeTool = Tool.make("t3_thread_organize", {
"archive",
"unarchive",
"mark_unread",
"mute",
"unmute",
]),
snoozedUntil: Schema.optional(IsoDateTime),
}),
Expand Down
Loading