From e6db31eb8bbd960bc6d2323da5a0f98fdd2872c9 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Sat, 11 Jul 2026 11:22:57 +0500 Subject: [PATCH] mobile: add reminder screen --- .../list-items/headers/section-header.tsx | 2 +- .../components/list-items/reminder/index.tsx | 232 ++-- .../list-items/selection-wrapper/index.tsx | 18 +- .../app/components/properties/items.tsx | 1 + apps/mobile/app/hooks/use-actions.tsx | 8 +- .../mobile/app/screens/add-reminder/index.tsx | 1079 +++++++++-------- apps/mobile/fonts/MaterialCommunityIcons.ttf | Bin 24164 -> 23740 bytes packages/intl/locale/en.po | 68 ++ packages/intl/locale/pseudo-LOCALE.po | 68 ++ packages/intl/src/strings.ts | 28 +- 10 files changed, 884 insertions(+), 620 deletions(-) diff --git a/apps/mobile/app/components/list-items/headers/section-header.tsx b/apps/mobile/app/components/list-items/headers/section-header.tsx index c2191e603..c544a15b4 100644 --- a/apps/mobile/app/components/list-items/headers/section-header.tsx +++ b/apps/mobile/app/components/list-items/headers/section-header.tsx @@ -101,7 +101,7 @@ export const SectionHeader = React.memo< alignSelf: "center", textAlignVertical: "center" }} - color={colors.secondary.paragraph} + color={colors.primary.accent} > {!item.title || item.title === "" ? screen === "Search" diff --git a/apps/mobile/app/components/list-items/reminder/index.tsx b/apps/mobile/app/components/list-items/reminder/index.tsx index 5773b86c9..3bf1ef69a 100644 --- a/apps/mobile/app/components/list-items/reminder/index.tsx +++ b/apps/mobile/app/components/list-items/reminder/index.tsx @@ -16,28 +16,28 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ +import { getFormattedReminderTime } from "@notesnook/common"; import { Reminder } from "@notesnook/core"; import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; -import React from "react"; +import React, { useState } from "react"; import { View } from "react-native"; -import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { notesnook } from "../../../../e2e/test.ids"; +import { Radius, Spacing } from "../../../common/design/spacing"; import useIsSelected from "../../../hooks/use-selected"; import AddReminder from "../../../screens/add-reminder"; import { eSendEvent } from "../../../services/event-manager"; -import { useSelectionStore } from "../../../stores/use-selection-store"; +import { + selectItem, + useSelectionStore +} from "../../../stores/use-selection-store"; import { eCloseSheet } from "../../../utils/events"; -import { AppFontSize, defaultBorderRadius } from "../../../utils/size"; -import { DefaultAppStyles } from "../../../utils/styles"; import { Properties } from "../../properties"; import AppIcon from "../../ui/AppIcon"; import { IconButton } from "../../ui/icon-button"; -import { ReminderTime } from "../../ui/reminder-time"; import Heading from "../../ui/typography/heading"; import Paragraph from "../../ui/typography/paragraph"; import SelectionWrapper from "../selection-wrapper"; -import { selectItem } from "../../../stores/use-selection-store"; const ReminderItem = React.memo( ({ @@ -50,6 +50,7 @@ const ReminderItem = React.memo( isSheet: boolean; }) => { const { colors } = useThemeColors(); + const [checked, setChecked] = useState(false); const openReminder = () => { if (selectItem(item)) return; AddReminder.present(item, undefined); @@ -59,148 +60,129 @@ const ReminderItem = React.memo( }; const selectionMode = useSelectionStore((state) => state.selectionMode); const [selected] = useIsSelected(item); + + // Subtitle segments (reminder time · recurring mode · disabled), rendered as + // plain text separated by a dot to match the redesign. + const subtitleSegments = [ + getFormattedReminderTime(item, false), + item.mode === "repeat" && item.recurringMode + ? strings.reminderRecurringMode[item.recurringMode]() + : undefined, + item.disabled ? strings.disabled() : undefined + ].filter(Boolean) as string[]; + return ( - + { + if (selectItem(item)) return; + + setChecked(!checked); + }} + item={item} + isSheet={isSheet} + hideSeparator + style={{ + borderWidth: 1, + borderColor: colors.primary.border, + borderRadius: Radius.S, + marginBottom: Spacing.LEVEL_2, + backgroundColor: selected ? colors.primary.shade : undefined + }} + wrapperStyle={{ + paddingHorizontal: Spacing.LEVEL_3 + }} + > + {/* Leading "done" checkbox — visual only for now, not yet wired. */} + + - - {item.title} - + + {item.title} + + + Properties.present(item, isSheet)} + style={{ + justifyContent: "center", + height: undefined, + width: undefined, + borderRadius: 100, + alignItems: "center" + }} + /> + {item.description ? ( - - {item.description} - + {item.description} ) : null} - {item.disabled ? ( - - + {subtitleSegments.map((segment, i) => ( + + {i > 0 ? ( + + ) : null} - {strings.disabled()} + {segment} - - ) : null} - {item.mode === "repeat" && item.recurringMode ? ( - - - - {strings.reminderRecurringMode[item.recurringMode]()} - - - ) : null} - - + + ))} - - {selectionMode === "note" || selectionMode === "trash" ? ( - <> - - - - - ) : ( - Properties.present(item, isSheet)} - style={{ - justifyContent: "center", - height: 35, - width: 35, - borderRadius: 100, - alignItems: "center" - }} - /> - )} ); }, diff --git a/apps/mobile/app/components/list-items/selection-wrapper/index.tsx b/apps/mobile/app/components/list-items/selection-wrapper/index.tsx index 9f9f3800d..d3dc04aa4 100644 --- a/apps/mobile/app/components/list-items/selection-wrapper/index.tsx +++ b/apps/mobile/app/components/list-items/selection-wrapper/index.tsx @@ -24,7 +24,7 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store"; import { useSelectionStore } from "../../../stores/use-selection-store"; import { Pressable } from "../../ui/pressable"; -import { View } from "react-native"; +import { View, ViewStyle } from "react-native"; import { Spacing } from "../../../common/design/spacing"; type SelectionWrapperProps = PropsWithChildren<{ @@ -35,6 +35,9 @@ type SelectionWrapperProps = PropsWithChildren<{ color?: string; index?: number; hasGroupHeader?: boolean; + style?: ViewStyle; + wrapperStyle?: ViewStyle; + hideSeparator?: boolean; }>; const SelectionWrapper = ({ @@ -45,7 +48,10 @@ const SelectionWrapper = ({ children, color, hasGroupHeader, - index = 0 + index = 0, + style, + wrapperStyle, + hideSeparator }: SelectionWrapperProps) => { const itemId = useRef(item.id); const { colors, isDark } = useThemeColors(); @@ -72,7 +78,7 @@ const SelectionWrapper = ({ return ( <> - {hasGroupHeader ? null : ( + {hasGroupHeader || hideSeparator ? null : ( { close(); await db.reminders.add({ @@ -524,7 +522,7 @@ export const useActions = ({ { id: "edit-reminder", title: strings.editReminder(), - icon: "pencil", + icon: "pencil-simple", onPress: async () => { AddReminder.present(item); close(); diff --git a/apps/mobile/app/screens/add-reminder/index.tsx b/apps/mobile/app/screens/add-reminder/index.tsx index 4ab3c81f5..3ef71452e 100644 --- a/apps/mobile/app/screens/add-reminder/index.tsx +++ b/apps/mobile/app/screens/add-reminder/index.tsx @@ -17,9 +17,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ import { Note, Reminder } from "@notesnook/core"; +import { + getFormattedDate, + useIsFeatureAvailable, + usePromise +} from "@notesnook/common"; import { strings } from "@notesnook/intl"; import { useThemeColors } from "@notesnook/theme"; -import dayjs from "dayjs"; import React, { useRef, useState } from "react"; import { KeyboardAvoidingView, @@ -28,60 +32,39 @@ import { TextInput, View } from "react-native"; -import DatePicker from "react-native-date-picker"; import DateTimePickerModal from "react-native-modal-datetime-picker"; import { SafeAreaView } from "react-native-safe-area-context"; +import { Spacing, Radius } from "../../common/design/spacing"; import { db } from "../../common/database"; import { Dialog } from "../../components/dialog"; import { Header } from "../../components/header"; +import AppIcon from "../../components/ui/AppIcon"; import { Button } from "../../components/ui/button"; -import { ReminderTime } from "../../components/ui/reminder-time"; -import Paragraph from "../../components/ui/typography/paragraph"; -import { DDS } from "../../services/device-detection"; -import { eSendEvent, ToastManager } from "../../services/event-manager"; -import Navigation, { NavigationProps } from "../../services/navigation"; -import Notifications from "../../services/notifications"; -import SettingsService from "../../services/settings"; -import { useRelationStore } from "../../stores/use-relation-store"; -import { useSettingStore } from "../../stores/use-setting-store"; -import { AppFontSize, defaultBorderRadius } from "../../utils/size"; -import { DefaultAppStyles } from "../../utils/styles"; -import { - getFormattedDate, - useIsFeatureAvailable, - usePromise -} from "@notesnook/common"; -import PaywallSheet from "../../components/sheets/paywall"; -import { useNavigationFocus } from "../../hooks/use-navigation-focus"; -import { Pressable } from "../../components/ui/pressable"; -import { TimeSince } from "../../components/ui/time-since"; -import Heading from "../../components/ui/typography/heading"; -import { eOnLoadNote } from "../../utils/events"; -import { fluidTabsRef } from "../../utils/global-refs"; import FormInput, { createFormRef, validators } from "../../components/ui/input/form-input"; -import AppIcon from "../../components/ui/AppIcon"; +import { Pressable } from "../../components/ui/pressable"; +import LineSeparator from "../../components/ui/seperator/line-separator"; +import { TimeSince } from "../../components/ui/time-since"; +import Heading from "../../components/ui/typography/heading"; +import Paragraph from "../../components/ui/typography/paragraph"; +import { useNavigationFocus } from "../../hooks/use-navigation-focus"; +import { eSendEvent, ToastManager } from "../../services/event-manager"; +import Navigation, { NavigationProps } from "../../services/navigation"; +import Notifications from "../../services/notifications"; +import SettingsService from "../../services/settings"; +import PaywallSheet from "../../components/sheets/paywall"; +import { useRelationStore } from "../../stores/use-relation-store"; +import { useSettingStore } from "../../stores/use-setting-store"; +import { eOnLoadNote } from "../../utils/events"; +import { fluidTabsRef } from "../../utils/global-refs"; +import { AppFontSize } from "../../utils/size"; -const ReminderModes = - Platform.OS === "ios" - ? { - Once: "once", - Repeat: "repeat" - } - : { - Once: "once", - Repeat: "repeat", - Permanent: "permanent" - }; - -const RecurringModes = { - Daily: "day", - Week: "week", - Month: "month", - Year: "year" -}; +// Frequency chips shown in the "Reminder frequency" section. "once" maps to +// reminderMode = "once", the rest map to reminderMode = "repeat" + recurringMode. +const FrequencyModes = ["once", "day", "week", "month", "year"] as const; +type FrequencyMode = (typeof FrequencyModes)[number]; const WeekDays = [0, 1, 2, 3, 4, 5, 6]; const WeekDaysMon = [1, 2, 3, 4, 5, 6, 0]; @@ -91,7 +74,7 @@ const ReminderNotificationModes = { Silent: "silent", Vibrate: "vibrate", Urgent: "urgent" -}; +} as const; export default function AddReminder(props: NavigationProps<"AddReminder">) { const { reminder, reference } = props.route.params; @@ -109,8 +92,9 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { const { colors, isDark } = useThemeColors(); const weekFormat = useSettingStore((state) => state.weekFormat); const [reminderMode, setReminderMode] = useState( - reminder?.mode || "once" + reminder?.mode === "permanent" ? "once" : reminder?.mode || "once" ); + const [allDay, setAllDay] = useState(reminder?.mode === "permanent"); const [recurringMode, setRecurringMode] = useState( reminder?.recurringMode || "week" ); @@ -124,7 +108,11 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { Reminder["priority"] >(reminder?.priority || SettingsService.get().reminderNotificationMode); const [isDatePickerVisible, setDatePickerVisibility] = useState(false); - const [repeatFrequency, setRepeatFrequency] = useState(1); + const [pickerMode, setPickerMode] = useState<"date" | "time">("date"); + const [dateSelected, setDateSelected] = useState(!!reminder); + const [timeSelected, setTimeSelected] = useState(!!reminder); + const [frequencyExpanded, setFrequencyExpanded] = useState(true); + const [moreOptionsExpanded, setMoreOptionsExpanded] = useState(false); const referencedItem = reference ? (reference as Note) : null; const recurringReminderFeature = useIsFeatureAvailable("recurringReminders"); const formRef = useRef( @@ -141,7 +129,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { ); const titleRef = useRef(null); const descriptionRef = useRef(null); - const timer = useRef(undefined); const referencedNotes = usePromise( () => reminder?.id @@ -154,54 +141,90 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { const [dateError, setDateError] = useState(); const [selectDayError, setSelectDayError] = useState(); - const showDatePicker = () => { + // The frequency chip currently reflected by reminderMode + recurringMode. + const currentFrequency: FrequencyMode = + reminderMode === "once" ? "once" : recurringMode || "week"; + // Once and yearly reminders pick a specific date; daily/weekly/monthly only a time. + const showDatePicker = reminderMode === "once" || recurringMode === "year"; + const showDaySelector = + reminderMode === "repeat" && + (recurringMode === "week" || recurringMode === "month"); + + const openPicker = (mode: "date" | "time") => { + setPickerMode(mode); setDatePickerVisibility(true); }; - const hideDatePicker = () => { + const hidePicker = () => { setDatePickerVisibility(false); }; - const handleConfirm = (date: Date) => { - timer.current = setTimeout(() => { - setDateError(undefined); - hideDatePicker(); - setDate(date); - }, 10); + const handleConfirm = (selected: Date) => { + hidePicker(); + setDateError(undefined); + const next = new Date(date); + if (pickerMode === "time") { + next.setHours(selected.getHours(), selected.getMinutes(), 0, 0); + setTimeSelected(true); + } else { + next.setFullYear( + selected.getFullYear(), + selected.getMonth(), + selected.getDate() + ); + setDateSelected(true); + } + setDate(next); }; - function nth(n: number) { - return ( - ["st", "nd", "rd"][(((((n < 0 ? -n : n) + 90) % 100) - 10) % 10) - 1] || - "th" - ); - } - function getSelectedDaysText(selectedDays: number[]) { - const text = selectedDays - .sort((a, b) => a - b) - .map((day, index) => { - const isLast = index === selectedDays.length - 1; - const isSecondLast = index === selectedDays.length - 2; - const joinWith = isSecondLast ? " & " : isLast ? "" : ", "; - return recurringMode === RecurringModes.Week - ? strings.weekDayNames[day as keyof typeof strings.weekDayNames]() + - joinWith - : `${day}${nth(day)} ${joinWith}`; - }) - .join(""); - return text; - } + const selectFrequency = (mode: FrequencyMode) => { + if ( + mode !== "once" && + recurringReminderFeature && + !recurringReminderFeature?.isAllowed + ) { + PaywallSheet.present(recurringReminderFeature); + return; + } + setSelectDayError(undefined); + if (mode === "once") { + setReminderMode("once"); + return; + } + setReminderMode("repeat"); + setRecurringMode(mode as Reminder["recurringMode"]); + if (mode === "week") { + setSelectedDays((days) => (days.length ? days : [date.getDay()])); + } else if (mode === "month") { + setSelectedDays((days) => (days.length ? days : [date.getDate()])); + } else { + setSelectedDays([]); + } + }; + + const toggleDay = (day: number) => { + setSelectDayError(undefined); + setSelectedDays((days) => { + if (days.indexOf(day) > -1) { + return days.filter((d) => d !== day); + } + return [...days, day]; + }); + }; async function saveReminder() { try { if (!formRef.current.validate()) return; - if (date.getTime() < Date.now() && reminderMode === "once") { + + const mode: Reminder["mode"] = allDay ? "permanent" : reminderMode; + + if (date.getTime() < Date.now() && mode === "once") { setDateError(strings.dateError()); return; } if ( - reminderMode === ReminderModes.Repeat && + mode === "repeat" && recurringMode !== "day" && recurringMode !== "year" && selectedDays.length === 0 @@ -210,7 +233,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { return; } - if (!date && reminderMode !== ReminderModes.Permanent) return; + if (!date && mode !== "permanent") return; if (!(await Notifications.checkAndRequestPermissions(true))) throw new Error(strings.noNotificationPermission()); @@ -225,8 +248,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) { description: details.current, recurringMode: recurringMode, selectedDays: selectedDays, - mode: reminderMode, - localOnly: reminderMode === "permanent", + mode: mode, + localOnly: mode === "permanent", snoozeUntil: date?.getTime() > Date.now() ? undefined : reminder?.snoozeUntil, disabled: false @@ -267,449 +290,467 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
+ + - (title.current = text)} - wrapperStyle={{ - marginTop: DefaultAppStyles.GAP_VERTICAL - }} - onSubmitEditing={() => { - descriptionRef.current?.focus(); - }} - /> - - (details.current = text)} - containerStyle={{ - maxHeight: 80 - }} - multiline - textAlignVertical="top" - inputStyle={{ - minHeight: 80, - paddingVertical: DefaultAppStyles.GAP_VERTICAL - }} - height={80} - /> - - - {Object.keys(ReminderModes).map((mode) => ( -