Skip to content

Commit

Permalink
feat: add recurrent reminders (raycast#12208)
Browse files Browse the repository at this point in the history
  • Loading branch information
comoser authored May 7, 2024
1 parent e5c9585 commit e309f54
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 36 deletions.
9 changes: 9 additions & 0 deletions extensions/simple-reminder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Simple Reminder Changelog

## [Recurrent reminders] - 2024-05-06

- Add action to set reminders as recurrent (daily, weekly, bi-weekly, monthly)
- Visual improvements and reordering of the reminder actions to be more user-friendly
- Copy reminder and delete reminder actions now have intuitive shortcuts

Notes:
- This feature addition makes some visual changes (2 sections to separate recurrent from normal reminders), but shouldn't cause any breaking change

## [Fix] - 2024-03-18

- Fix issue with topics having quoted words not triggering notifications
Expand Down
Binary file added extensions/simple-reminder/assets/repeat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions extensions/simple-reminder/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion extensions/simple-reminder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"@raycast/api": "^1.69.3",
"@raycast/utils": "^1.13.3",
"chrono-node": "^2.6.3",
"natural": "^6.2.0"
"natural": "^6.2.0",
"date-fns": "^3.6.0"
},
"devDependencies": {
"@raycast/eslint-config": "1.0.5",
Expand Down
64 changes: 64 additions & 0 deletions extensions/simple-reminder/src/components/listActionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Action, ActionPanel, Color, Icon } from "@raycast/api";
import { Frequency } from "../types/frequency";
import { Reminder } from "../types/reminder";

type ActionPanelProps = {
searchText: string;
reminder: Reminder;
onSetReminderAction: () => void;
onSetRecurrenceForReminderAction: (reminderId: string, frequency: Frequency) => void;
onCopyReminderTopicAction: (reminderTopic: string) => void;
onDeleteReminderAction: (reminderId: string) => void;
};

export function ListActionPanel({
searchText,
reminder,
onSetReminderAction,
onSetRecurrenceForReminderAction,
onCopyReminderTopicAction,
onDeleteReminderAction,
}: ActionPanelProps) {
return (
<ActionPanel>
{searchText.length > 0 && <Action title="Set Reminder" icon={Icon.AlarmRinging} onAction={onSetReminderAction} />}
{!reminder.frequency && (
<ActionPanel.Submenu title="Set Recurrence" icon={Icon.Repeat}>
<Action
icon={{ source: Icon.Repeat, tintColor: Color.Red }}
title="Daily"
onAction={() => onSetRecurrenceForReminderAction(reminder.id, Frequency.DAILY)}
/>
<Action
icon={{ source: Icon.Repeat, tintColor: Color.Orange }}
title="Weekly"
onAction={() => onSetRecurrenceForReminderAction(reminder.id, Frequency.WEEKLY)}
/>
<Action
icon={{ source: Icon.Repeat, tintColor: Color.Yellow }}
title="Bi-Weekly"
onAction={() => onSetRecurrenceForReminderAction(reminder.id, Frequency.BI_WEEKLY)}
/>
<Action
icon={{ source: Icon.Repeat, tintColor: Color.Green }}
title="Monthly"
onAction={() => onSetRecurrenceForReminderAction(reminder.id, Frequency.MONTHLY)}
/>
</ActionPanel.Submenu>
)}
<Action
title="Copy to Clipboard"
icon={Icon.CopyClipboard}
shortcut={{ modifiers: ["cmd", "shift"], key: "c" }}
onAction={() => onCopyReminderTopicAction(reminder.topic)}
/>
<Action
title="Delete Reminder"
style={Action.Style.Destructive}
icon={Icon.Trash}
shortcut={{ modifiers: ["cmd", "shift"], key: "backspace" }}
onAction={() => onDeleteReminderAction(reminder.id)}
/>
</ActionPanel>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { LocalStorage } from "@raycast/api";
import { Frequency } from "../types/frequency";
import { Reminder } from "../types/reminder";

type SetRecurrenceForReminderProps = {
reminderId: string;
frequency: Frequency;
existingReminders: Reminder[];
setReminders: (reminders: Reminder[]) => void;
};

export async function setRecurrenceForReminder(props: SetRecurrenceForReminderProps) {
const reminder: Reminder = props.existingReminders.find((reminder) => reminder.id === props.reminderId)!;
reminder.frequency = props.frequency;
props.setReminders([...props.existingReminders]);
await LocalStorage.setItem(reminder.id, JSON.stringify(reminder));
}
83 changes: 57 additions & 26 deletions extensions/simple-reminder/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { extractTopicAndDateFromInputText } from "./utils/extractTopicAndDateFro
import { deleteExistingReminder } from "./handlers/deleteExistingReminder";
import { copyExistingReminder } from "./handlers/copyExistingReminder";
import { showError } from "./utils/showError";
import { Frequency } from "./types/frequency";
import { setRecurrenceForReminder } from "./handlers/setRecurrenceForReminder";
import { hasFrequencyPredicate, hasNoFrequencyPredicate } from "./utils/arrayPredicates";
import { ListActionPanel } from "./components/listActionPanel";

export default function Command() {
const [searchText, setSearchText] = useState("");
Expand Down Expand Up @@ -55,6 +59,19 @@ export default function Command() {
}
};

const onSetRecurrenceForReminderAction = async (reminderId: string, frequency: Frequency) => {
try {
await setRecurrenceForReminder({
reminderId,
frequency,
existingReminders: reminders,
setReminders,
});
} catch (e) {
await showError("Couldn't set recurrence for reminder");
}
};

return (
<List
searchText={searchText}
Expand All @@ -75,34 +92,48 @@ export default function Command() {
icon="no_bell.png"
/>
) : (
<List.Section title="Existing reminders" subtitle="you can delete existing reminders">
{reminders.map((reminder) => (
<List.Item
key={reminder.id}
title={reminder.topic}
subtitle={`set to ${reminder.date.toLocaleString()}`}
icon="bell.png"
actions={
<ActionPanel>
{searchText.length > 0 && (
<Action title="Set Reminder" icon={Icon.AlarmRinging} onAction={onSetReminderAction} />
)}
<Action
title="Copy to Clipboard"
icon={Icon.CopyClipboard}
onAction={() => onCopyReminderTopicAction(reminder.topic)}
<>
<List.Section title="Recurrent reminders">
{reminders.filter(hasFrequencyPredicate).map((reminder) => (
<List.Item
key={reminder.id}
title={reminder.topic}
subtitle={`set to ${reminder.date.toLocaleString()} ${reminder.frequency ? `(happening ${reminder.frequency})` : ""}`}
icon="repeat.png"
actions={
<ListActionPanel
searchText={searchText}
reminder={reminder}
onSetReminderAction={onSetReminderAction}
onSetRecurrenceForReminderAction={onSetRecurrenceForReminderAction}
onCopyReminderTopicAction={onCopyReminderTopicAction}
onDeleteReminderAction={onDeleteReminderAction}
/>
<Action
title="Delete Reminder"
style={Action.Style.Destructive}
icon={Icon.Trash}
onAction={() => onDeleteReminderAction(reminder.id)}
}
/>
))}
</List.Section>
<List.Section title="Other reminders">
{reminders.filter(hasNoFrequencyPredicate).map((reminder) => (
<List.Item
key={reminder.id}
title={reminder.topic}
subtitle={`set to ${reminder.date.toLocaleString()} ${reminder.frequency ? `(happening ${reminder.frequency})` : ""}`}
icon="bell.png"
actions={
<ListActionPanel
searchText={searchText}
reminder={reminder}
onSetReminderAction={onSetReminderAction}
onSetRecurrenceForReminderAction={onSetRecurrenceForReminderAction}
onCopyReminderTopicAction={onCopyReminderTopicAction}
onDeleteReminderAction={onDeleteReminderAction}
/>
</ActionPanel>
}
/>
))}
</List.Section>
}
/>
))}
</List.Section>
</>
)}
</List>
);
Expand Down
46 changes: 37 additions & 9 deletions extensions/simple-reminder/src/reminderNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import { getPreferenceValues, LocalStorage, updateCommandMetadata } from "@raycast/api";
import { runAppleScript } from "@raycast/utils";
import { addDays } from "date-fns/addDays";
import { addWeeks } from "date-fns/addWeeks";
import { addMonths } from "date-fns/addMonths";
import { Reminder } from "./types/reminder";
import { sendPushNotificationWithNtfy } from "./utils/sendPushNotificationWithNtfy";
import { sanitizeTopicForNotification } from "./utils/sanitizeTopicForNotification";
import { runAppleScript } from "@raycast/utils";

interface Preferences {
mobileNotificationNtfy: boolean;
mobileNotificationNtfyTopic: string;
}
import { SimpleReminderPreferences } from "./types/preferences";
import { Frequency } from "./types/frequency";
import { addMinutes } from "date-fns/addMinutes";

export default async function Command() {
const { mobileNotificationNtfy, mobileNotificationNtfyTopic } = getPreferenceValues<Preferences>();
const { mobileNotificationNtfy, mobileNotificationNtfyTopic } = getPreferenceValues<SimpleReminderPreferences>();
const storedRemindersObject = await LocalStorage.allItems<Record<string, string>>();
if (!Object.keys(storedRemindersObject).length) return;

for (const key in storedRemindersObject) {
const reminder: Reminder = JSON.parse(storedRemindersObject[key]);
if (new Date().getTime() >= new Date(reminder.date).getTime()) {
if (isReminderInThePast(reminder)) {
const cleanTopic = sanitizeTopicForNotification(reminder.topic);

await runAppleScript(`display notification "${cleanTopic}" with title "Simple Reminder" sound name "default"`);
await sendPushNotificationToMacOS(cleanTopic);
if (mobileNotificationNtfy) {
await sendPushNotificationWithNtfy(mobileNotificationNtfyTopic, cleanTopic);
}

if (reminder.frequency) {
const newDate = updateReminderDateForRecurrence(reminder);
reminder.date = newDate!;
return await LocalStorage.setItem(reminder.id, JSON.stringify(reminder));
}
await LocalStorage.removeItem(reminder.id);
}
}
Expand All @@ -31,3 +38,24 @@ export default async function Command() {
subtitle: `Last checked for reminders: ${new Date().toLocaleString()}`,
});
}

function isReminderInThePast(reminder: Reminder) {
return new Date().getTime() >= new Date(reminder.date).getTime();
}

function sendPushNotificationToMacOS(cleanTopic: string) {
return runAppleScript(`display notification "${cleanTopic}" with title "Simple Reminder" sound name "default"`);
}

function updateReminderDateForRecurrence(reminder: Reminder) {
switch (reminder.frequency) {
case Frequency.DAILY:
return addDays(reminder.date, 1);
case Frequency.WEEKLY:
return addWeeks(reminder.date, 1);
case Frequency.BI_WEEKLY:
return addWeeks(reminder.date, 2);
case Frequency.MONTHLY:
return addMonths(reminder.date, 1);
}
}
6 changes: 6 additions & 0 deletions extensions/simple-reminder/src/types/frequency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum Frequency {
DAILY = "daily",
WEEKLY = "weekly",
BI_WEEKLY = "bi-weekly",
MONTHLY = "monthly",
}
4 changes: 4 additions & 0 deletions extensions/simple-reminder/src/types/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SimpleReminderPreferences {
mobileNotificationNtfy: boolean;
mobileNotificationNtfyTopic: string;
}
3 changes: 3 additions & 0 deletions extensions/simple-reminder/src/types/reminder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Frequency } from "./frequency";

export type Reminder = {
id: string;
topic: string;
date: Date;
frequency?: Frequency;
};
9 changes: 9 additions & 0 deletions extensions/simple-reminder/src/utils/arrayPredicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Reminder } from "../types/reminder";

export function hasFrequencyPredicate(reminder: Reminder) {
return !!reminder.frequency;
}

export function hasNoFrequencyPredicate(reminder: Reminder) {
return !reminder.frequency;
}

0 comments on commit e309f54

Please sign in to comment.