Open History to see the last commit
1import { SheetHeader } from "../../components/ui/SheetHeader";
2import {
3 DatePicker,
4 Host,
5 HStack,
6 Picker,
7 Popover,
8 RNHostView,
9 Spacer,
10 Text,
11 VStack,
12} from "@expo/ui/swift-ui";
13import {
14 accessibilityHidden,
15 clipped,
16 font,
17 datePickerStyle,
18 foregroundStyle,
19 frame,
20 labelsHidden,
21 padding,
22 pickerStyle,
23 tag,
24} from "@expo/ui/swift-ui/modifiers";
25import {
26 localSnoozeDate,
27 localSnoozeTime,
28 resolveCustomSnooze,
29 type CustomSnoozeInput,
30} from "@t3tools/client-runtime/state/thread-settled";
31import { useState, type ReactNode } from "react";
32import { StandaloneStack } from "../layout/nested-stacks";
33import { ScrollView, useWindowDimensions, View } from "react-native";
34
35import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
36
37const durationAmounts = Array.from({ length: 99 }, (_, index) => index + 1);
38const modes = [
39 { value: "date", label: "Date and time" },
40 { value: "duration", label: "Duration" },
41] as const;
42const units = [
43 { value: "minutes", label: "Minutes" },
44 { value: "hours", label: "Hours" },
45 { value: "days", label: "Days" },
46] as const;
47
48export function CustomSnoozeSheet(props: {
49 readonly onClose: () => void;
50 readonly onSnooze: (snoozedUntil: string) => void;
51}) {
52 const { width, height } = useWindowDimensions();
53 const [mode, setMode] = useState<CustomSnoozeInput["mode"]>("date");
54 const [date, setDate] = useState(() => new Date(Date.now() + 3_600_000));
55 const [amount, setAmount] = useState(2);
56 const [unit, setUnit] = useState<"minutes" | "hours" | "days">("hours");
57 const [error, setError] = useState<string | null>(null);
58 const { themeVariables: colors, themeAppearance } = useAppearancePreferences();
59 const popoverWidth = Math.min(360, width - 32);
60 const popoverHeight = Math.min(error ? 364 : 324, height - 96);
61 const updateDate = (value: Date) => {
62 setDate(value);
63 setError(null);
64 };
65
66 const submit = () => {
67 const input: CustomSnoozeInput =
68 mode === "date"
69 ? { mode, date: localSnoozeDate(date), time: localSnoozeTime(date) }
70 : { mode, amount: String(amount), unit };
71 const snoozedUntil = resolveCustomSnooze(input, new Date());
72 if (!snoozedUntil) {
73 setError(
74 mode === "date" ? "Choose a date and time in the future." : "Enter a positive duration.",
75 );
76 return;
77 }
78 props.onSnooze(snoozedUntil);
79 props.onClose();
80 };
81
82 return (
83 <Host
84 style={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }}
85 colorScheme={themeAppearance}
86 seedColor={colors["--color-primary"]}
87 >
88 <Popover
89 isPresented
90 onIsPresentedChange={(presented) => {
91 if (!presented) props.onClose();
92 }}
93 >
94 <Popover.Trigger>
95 <Spacer
96 modifiers={[frame({ maxWidth: Infinity, maxHeight: Infinity }), accessibilityHidden()]}
97 />
98 </Popover.Trigger>
99 <Popover.Content>
100 <SnoozePopoverNavigation
101 width={popoverWidth}
102 height={popoverHeight}
103 onClose={props.onClose}
104 onSubmit={submit}
105 >
106 <Host
107 matchContents={{ vertical: true }}
108 colorScheme={themeAppearance}
109 style={{ width: popoverWidth }}
110 >
111 <VStack
112 spacing={16}
113 modifiers={[padding({ all: 16 }), foregroundStyle(colors["--color-foreground"])]}
114 >
115 <Picker
116 label="Snooze mode"
117 selection={mode}
118 onSelectionChange={(value: CustomSnoozeInput["mode"]) => {
119 setMode(value);
120 setError(null);
121 }}
122 modifiers={[pickerStyle("segmented")]}
123 >
124 {modes.map((option) => (
125 <Text key={option.value} modifiers={[tag(option.value)]}>
126 {option.label}
127 </Text>
128 ))}
129 </Picker>
130 {mode === "date" ? (
131 <DatePicker
132 title="Snooze until"
133 selection={date}
134 displayedComponents={["date", "hourAndMinute"]}
135 onDateChange={updateDate}
136 modifiers={[
137 datePickerStyle("wheel"),
138 labelsHidden(),
139 frame({ maxWidth: Infinity, minHeight: 180, maxHeight: 180 }),
140 ]}
141 />
142 ) : (
143 <HStack spacing={0}>
144 <Picker
145 label="Duration amount"
146 selection={amount}
147 onSelectionChange={(value: number) => {
148 setAmount(value);
149 setError(null);
150 }}
151 modifiers={[
152 pickerStyle("wheel"),
153 labelsHidden(),
154 frame({ minWidth: 0, maxWidth: Infinity, minHeight: 180, maxHeight: 180 }),
155 clipped(),
156 ]}
157 >
158 {durationAmounts.map((value) => (
159 <Text
160 key={value}
161 modifiers={[tag(value), foregroundStyle(colors["--color-foreground"])]}
162 >
163 {String(value)}
164 </Text>
165 ))}
166 </Picker>
167 <Picker
168 label="Duration unit"
169 selection={unit}
170 onSelectionChange={(value: typeof unit) => {
171 setUnit(value);
172 setError(null);
173 }}
174 modifiers={[
175 pickerStyle("wheel"),
176 labelsHidden(),
177 frame({ minWidth: 0, maxWidth: Infinity, minHeight: 180, maxHeight: 180 }),
178 clipped(),
179 ]}
180 >
181 {units.map((option) => (
182 <Text
183 key={option.value}
184 modifiers={[
185 tag(option.value),
186 foregroundStyle(colors["--color-foreground"]),
187 ]}
188 >
189 {option.label}
190 </Text>
191 ))}
192 </Picker>
193 </HStack>
194 )}
195 {error ? (
196 <Text
197 modifiers={[
198 font({ textStyle: "footnote" }),
199 foregroundStyle(colors["--color-danger-foreground"]),
200 ]}
201 >
202 {error}
203 </Text>
204 ) : null}
205 </VStack>
206 </Host>
207 </SnoozePopoverNavigation>
208 </Popover.Content>
209 </Popover>
210 </Host>
211 );
212}
213
214function SnoozePopoverNavigation(props: {
215 readonly width: number;
216 readonly height: number;
217 readonly onClose: () => void;
218 readonly onSubmit: () => void;
219 readonly children: ReactNode;
220}) {
221 return (
222 <VStack modifiers={[frame({ width: props.width, height: props.height })]}>
223 <RNHostView>
224 <View style={{ width: props.width, height: props.height }}>
225 <StandaloneStack>
226 <SheetHeader
227 title="Custom snooze"
228 onCancel={props.onClose}
229 accessibilityLabel="Cancel custom snooze"
230 confirm={{ label: "Snooze", onPress: props.onSubmit }}
231 />
232 <ScrollView
233 contentInsetAdjustmentBehavior="automatic"
234 showsVerticalScrollIndicator={false}
235 >
236 {props.children}
237 </ScrollView>
238 </StandaloneStack>
239 </View>
240 </RNHostView>
241 </VStack>
242 );
243}