Skip to content
Merged
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
10 changes: 10 additions & 0 deletions packages/backend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
updateNote,
} from "./note";
import { getCurrentProjectId } from "./project";
import {
createReminder,
deleteReminder,
dismissReminder,
getReminders,
} from "./reminder";

export {
getTree,
Expand All @@ -26,4 +32,8 @@ export {
getLegacyNotes,
migrateNote,
getFileContent,
getReminders,
createReminder,
deleteReminder,
dismissReminder,
};
139 changes: 139 additions & 0 deletions packages/backend/src/api/reminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { randomUUID } from "crypto";

import type { SDK } from "caido:plugin";
import type { Reminder, Result } from "shared";
import { error, ok } from "shared";

import {
createReminderSchema,
deleteReminderSchema,
dismissReminderSchema,
} from "../schemas/reminder";
import { ensureProjectDirectory } from "../utils/fileSystem";
import { readRemindersFile, writeRemindersFile } from "../utils/reminderFile";

/**
* Get all reminders for the current project
*/
export async function getReminders(sdk: SDK): Promise<Result<Reminder[]>> {
try {
const projectIDResult = await ensureProjectDirectory(sdk);
if (projectIDResult.kind === "Error") {
return error(projectIDResult.error);
}

const reminders = readRemindersFile(projectIDResult.value);
return ok(reminders);
} catch (err) {
sdk.console.error(`Error getting reminders: ${err}`);
return error(err instanceof Error ? err.message : String(err));
}
}

/**
* Create a new reminder
*/
export async function createReminder(
sdk: SDK,
notePath: string,
context: string,
reminderAt: string,
): Promise<Result<Reminder>> {
try {
createReminderSchema.parse({ notePath, context, reminderAt });

const parsedDate = new Date(reminderAt);
if (isNaN(parsedDate.getTime())) {
return error("Invalid reminder date");
}
const normalizedReminderAt = parsedDate.toISOString();

const projectIDResult = await ensureProjectDirectory(sdk);
if (projectIDResult.kind === "Error") {
return error(projectIDResult.error);
}

const projectID = projectIDResult.value;
const reminder: Reminder = {
id: randomUUID(),
notePath,
context,
reminderAt: normalizedReminderAt,
createdAt: new Date().toISOString(),
triggered: false,
dismissed: false,
};

const reminders = readRemindersFile(projectID);
reminders.push(reminder);
writeRemindersFile(projectID, reminders);

return ok(reminder);
} catch (err) {
sdk.console.error(`Error creating reminder: ${err}`);
return error(err instanceof Error ? err.message : String(err));
}
}

/**
* Delete a reminder by ID
*/
export async function deleteReminder(
sdk: SDK,
reminderId: string,
): Promise<Result<boolean>> {
try {
deleteReminderSchema.parse({ reminderId });

const projectIDResult = await ensureProjectDirectory(sdk);
if (projectIDResult.kind === "Error") {
return error(projectIDResult.error);
}

const projectID = projectIDResult.value;
const reminders = readRemindersFile(projectID);
const filtered = reminders.filter((r) => r.id !== reminderId);

if (filtered.length === reminders.length) {
return error(`Reminder not found: ${reminderId}`);
}

writeRemindersFile(projectID, filtered);
return ok(true);
} catch (err) {
sdk.console.error(`Error deleting reminder: ${err}`);
return error(err instanceof Error ? err.message : String(err));
}
}

/**
* Dismiss a reminder (mark as acknowledged by user)
*/
export async function dismissReminder(
sdk: SDK,
reminderId: string,
): Promise<Result<boolean>> {
try {
dismissReminderSchema.parse({ reminderId });

const projectIDResult = await ensureProjectDirectory(sdk);
if (projectIDResult.kind === "Error") {
return error(projectIDResult.error);
}

const projectID = projectIDResult.value;
const reminders = readRemindersFile(projectID);
const target = reminders.find((r) => r.id === reminderId);

if (!target) {
return error(`Reminder not found: ${reminderId}`);
}

target.dismissed = true;
writeRemindersFile(projectID, reminders);
return ok(true);
} catch (err) {
sdk.console.error(`Error dismissing reminder: ${err}`);
return error(err instanceof Error ? err.message : String(err));
}
}
18 changes: 18 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@ import type { DefineAPI, SDK } from "caido:plugin";
import {
createFolder,
createNote,
createReminder,
deleteFolder,
deleteNote,
deleteReminder,
dismissReminder,
getCurrentProjectId,
getFileContent,
getLegacyNotes,
getNote,
getReminders,
getTree,
migrateNote,
moveItem,
searchNotes,
updateNote,
} from "./api";
import { type BackendEvents } from "./types/events";
import { startReminderTimer } from "./utils/reminderTimer";

export type { BackendEvents } from "./types/events";

let stopReminderTimer: (() => void) | undefined;

export type API = DefineAPI<{
getTree: typeof getTree;
getNote: typeof getNote;
Expand All @@ -33,6 +40,10 @@ export type API = DefineAPI<{
getLegacyNotes: typeof getLegacyNotes;
migrateNote: typeof migrateNote;
getFileContent: typeof getFileContent;
getReminders: typeof getReminders;
createReminder: typeof createReminder;
deleteReminder: typeof deleteReminder;
dismissReminder: typeof dismissReminder;
}>;

export function init(sdk: SDK<API, BackendEvents>) {
Expand All @@ -49,10 +60,17 @@ export function init(sdk: SDK<API, BackendEvents>) {
sdk.api.register("getLegacyNotes", getLegacyNotes);
sdk.api.register("migrateNote", migrateNote);
sdk.api.register("getFileContent", getFileContent);
sdk.api.register("getReminders", getReminders);
sdk.api.register("createReminder", createReminder);
sdk.api.register("deleteReminder", deleteReminder);
sdk.api.register("dismissReminder", dismissReminder);

sdk.events.onProjectChange((sdk, project) => {
sdk.api.send("notes++:projectChange", project?.getId());
});

stopReminderTimer?.();
stopReminderTimer = startReminderTimer(sdk);

sdk.console.log("Notes++ backend initialized successfully");
}
15 changes: 15 additions & 0 deletions packages/backend/src/schemas/reminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from "zod";

export const createReminderSchema = z.object({
notePath: z.string().min(1),
context: z.string(),
reminderAt: z.string().datetime({ offset: true }),
});

export const deleteReminderSchema = z.object({
reminderId: z.string().min(1),
});

export const dismissReminderSchema = z.object({
reminderId: z.string().min(1),
});
2 changes: 2 additions & 0 deletions packages/backend/src/types/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { DefineEvents } from "caido:plugin";
import type { Reminder } from "shared";

export type BackendEvents = DefineEvents<{
"notes++:projectChange": (projectId: string) => void;
"notes++:reminderDue": (reminder: Reminder) => void;
}>;
62 changes: 62 additions & 0 deletions packages/backend/src/utils/reminderFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as fs from "fs";
import path from "path";

import type { Reminder } from "shared";
import { z } from "zod";

import {
createDirectory,
directoryExists,
fileExists,
toSystemPath,
} from "./fileSystem";
import { getNoteRootPath } from "./paths";

const REMINDERS_FILENAME = "reminders.json";

const ReminderSchema = z.object({
id: z.string(),
notePath: z.string(),
context: z.string(),
reminderAt: z.string(),
createdAt: z.string(),
triggered: z.boolean(),
dismissed: z.boolean(),
});

const RemindersArraySchema = z.array(ReminderSchema);

export function getRemindersFilePath(projectID: string): string {
return path.join(getNoteRootPath(projectID), REMINDERS_FILENAME);
}

export function readRemindersFile(projectID: string): Reminder[] {
const filePath = getRemindersFilePath(projectID);

if (!fileExists(filePath)) {
return [];
}

try {
const raw = fs.readFileSync(toSystemPath(filePath), "utf8");
const parsed = JSON.parse(raw);
const result = RemindersArraySchema.safeParse(parsed);
return result.success ? (result.data as Reminder[]) : [];
} catch {
return [];
}
}

export function writeRemindersFile(
projectID: string,
reminders: Reminder[],
): void {
const filePath = getRemindersFilePath(projectID);
const dirPath = path.dirname(filePath);

if (!directoryExists(dirPath)) {
createDirectory(dirPath);
}

fs.writeFileSync(toSystemPath(filePath), JSON.stringify(reminders, null, 2));
}
46 changes: 46 additions & 0 deletions packages/backend/src/utils/reminderTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { SDK } from "caido:plugin";

import type { API } from "../index";
import type { BackendEvents } from "../types/events";

import { readRemindersFile, writeRemindersFile } from "./reminderFile";

const CHECK_INTERVAL_MS = 30_000;

/** Periodically checks for due reminders and dispatches notification events. */
export function startReminderTimer(sdk: SDK<API, BackendEvents>): () => void {
async function checkReminders() {
try {
const project = await sdk.projects.getCurrent();
const projectID = project?.getId();

if (!projectID) return;

const reminders = readRemindersFile(projectID);
const now = new Date();
let changed = false;

for (const reminder of reminders) {
if (reminder.triggered || reminder.dismissed) continue;

const dueDate = new Date(reminder.reminderAt);
if (dueDate <= now) {
reminder.triggered = true;
changed = true;
sdk.api.send("notes++:reminderDue", reminder);
}
}

if (changed) {
writeRemindersFile(projectID, reminders);
}
} catch (err) {
sdk.console.error(`Error checking reminders: ${err}`);
}
}

checkReminders();
const intervalId = setInterval(checkReminders, CHECK_INTERVAL_MS);

return () => clearInterval(intervalId);
}
29 changes: 29 additions & 0 deletions packages/frontend/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type NoteContentItem } from "shared";
import { createApp, h } from "vue";

import NoteFloatModal from "@/components/shared/NoteFloatModal.vue";
import NoteSearchModal from "@/components/shared/NoteSearchModal.vue";
import { SDKPlugin } from "@/plugins/sdk";
import { useNotesStore } from "@/stores/notes";
import type { FrontendSDK } from "@/types";
Expand Down Expand Up @@ -44,6 +45,34 @@ export const showNoteModal = (sdk: FrontendSDK) => {
modalApp.mount(modalContainer);
};

/**
* Shows the search modal for finding and viewing existing notes
*/
export const showSearchModal = (sdk: FrontendSDK) => {
const modalContainer = document.createElement("div");
modalContainer.id = "note-search-modal-container";
document.body.appendChild(modalContainer);

const position = {
x: Math.max(0, window.innerWidth / 2 - 250),
y: Math.max(0, window.innerHeight / 2 - 200),
};

const modalApp = createApp({
render: () =>
h(NoteSearchModal, {
initialPosition: position,
onClose: () => {
modalApp.unmount();
modalContainer.remove();
},
}),
});

modalApp.use(SDKPlugin, sdk);
modalApp.mount(modalContainer);
};

/**
* Sends selected text to the currently open note
*/
Expand Down
Loading
Loading