diff --git a/Cargo.lock b/Cargo.lock index 59f0f216fa..00a03fbd5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17782,6 +17782,7 @@ dependencies = [ "tauri-plugin", "tauri-plugin-apple-contact", "tauri-plugin-auth", + "tauri-plugin-permissions", "tauri-specta", "thiserror 2.0.18", ] diff --git a/apps/desktop/src/calendar/components/apple/calendar-selection.tsx b/apps/desktop/src/calendar/components/apple/calendar-selection.tsx index 7659610565..42463b526a 100644 --- a/apps/desktop/src/calendar/components/apple/calendar-selection.tsx +++ b/apps/desktop/src/calendar/components/apple/calendar-selection.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo } from "react"; import { Button } from "@hypr/ui/components/ui/button"; import { cn } from "@hypr/utils"; -import { useSync } from "./context"; +import { useSync } from "../context"; import { SyncIndicator } from "./status"; import { diff --git a/apps/desktop/src/calendar/components/apple/status.tsx b/apps/desktop/src/calendar/components/apple/status.tsx index fd9d96276a..8bcabe5944 100644 --- a/apps/desktop/src/calendar/components/apple/status.tsx +++ b/apps/desktop/src/calendar/components/apple/status.tsx @@ -5,7 +5,7 @@ import { } from "@hypr/ui/components/ui/tooltip"; import { cn } from "@hypr/utils"; -import { useSync } from "./context"; +import { useSync } from "../context"; export function SyncIndicator() { const { status } = useSync(); diff --git a/apps/desktop/src/calendar/components/apple/context.tsx b/apps/desktop/src/calendar/components/context.tsx similarity index 97% rename from apps/desktop/src/calendar/components/apple/context.tsx rename to apps/desktop/src/calendar/components/context.tsx index 5da5849fcc..74aedf2884 100644 --- a/apps/desktop/src/calendar/components/apple/context.tsx +++ b/apps/desktop/src/calendar/components/context.tsx @@ -11,7 +11,7 @@ import { useTaskRunRunning, } from "tinytick/ui-react"; -import { CALENDAR_SYNC_TASK_ID } from "~/services/apple-calendar"; +import { CALENDAR_SYNC_TASK_ID } from "~/services/calendar"; export const TOGGLE_SYNC_DEBOUNCE_MS = 5000; diff --git a/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx b/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx index 8b083f8c66..058bd8cadf 100644 --- a/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx +++ b/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx @@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo } from "react"; import { googleListCalendars } from "@hypr/api-client"; import { createClient } from "@hypr/api-client/client"; +import { useSync } from "../context"; + import { useAuth } from "~/auth"; import { type CalendarGroup, @@ -38,6 +40,8 @@ export function useOAuthCalendarSelection(config: CalendarProvider) { const store = main.UI.useStore(main.STORE_ID); const calendars = main.UI.useTable("calendars", main.STORE_ID); const { user_id } = main.UI.useValues(main.STORE_ID); + const { status, scheduleSync, scheduleDebouncedSync, cancelDebouncedSync } = + useSync(); const { data: incomingCalendars, @@ -110,18 +114,21 @@ export function useOAuthCalendarSelection(config: CalendarProvider) { const handleToggle = useCallback( (calendar: CalendarItem, enabled: boolean) => { store?.setPartialRow("calendars", calendar.id, { enabled }); + scheduleDebouncedSync(); }, - [store], + [store, scheduleDebouncedSync], ); const handleRefresh = useCallback(async () => { + cancelDebouncedSync(); await refetch(); - }, [refetch]); + scheduleSync(); + }, [refetch, scheduleSync, cancelDebouncedSync]); return { groups, handleToggle, handleRefresh, - isLoading: isFetching, + isLoading: isFetching || status === "syncing", }; } diff --git a/apps/desktop/src/calendar/components/sidebar.tsx b/apps/desktop/src/calendar/components/sidebar.tsx index 01ce237ebd..4cfcd0077b 100644 --- a/apps/desktop/src/calendar/components/sidebar.tsx +++ b/apps/desktop/src/calendar/components/sidebar.tsx @@ -8,8 +8,8 @@ import { } from "@hypr/ui/components/ui/accordion"; import { AppleCalendarSelection } from "./apple/calendar-selection"; -import { SyncProvider } from "./apple/context"; import { AccessPermissionRow, TroubleShootingLink } from "./apple/permission"; +import { SyncProvider } from "./context"; import { OAuthProviderContent } from "./oauth/provider-content"; import { PROVIDERS } from "./shared"; @@ -24,54 +24,56 @@ export function CalendarSidebarContent() { ); return ( - - {visibleProviders.map((provider) => - provider.disabled ? ( - - {provider.icon} - {provider.displayName} - {provider.badge && ( - - {provider.badge} + + + {visibleProviders.map((provider) => + provider.disabled ? ( + + {provider.icon} + + {provider.displayName} - )} - - ) : ( - - - - {provider.icon} - - {provider.displayName} + {provider.badge && ( + + {provider.badge} - {provider.badge && ( - - {provider.badge} + )} + + ) : ( + + + + {provider.icon} + + {provider.displayName} - )} - - - - {provider.id === "apple" && ( - - {calendar.status !== "authorized" ? ( - - ) : ( - + {provider.badge && ( + + {provider.badge} + + )} + + + + {provider.id === "apple" && ( + + {calendar.status !== "authorized" ? ( + + ) : ( } /> - - )} - - )} - {provider.nangoIntegrationId && ( - - )} - - - ), - )} - + )} + + )} + {provider.nangoIntegrationId && ( + + )} + + + ), + )} + + ); } diff --git a/apps/desktop/src/onboarding/calendar.tsx b/apps/desktop/src/onboarding/calendar.tsx index 9aff357c22..794adf1746 100644 --- a/apps/desktop/src/onboarding/calendar.tsx +++ b/apps/desktop/src/onboarding/calendar.tsx @@ -6,9 +6,9 @@ import { Button } from "@hypr/ui/components/ui/button"; import { OnboardingButton } from "./shared"; import { useAppleCalendarSelection } from "~/calendar/components/apple/calendar-selection"; -import { SyncProvider } from "~/calendar/components/apple/context"; import { ApplePermissions } from "~/calendar/components/apple/permission"; import { CalendarSelection } from "~/calendar/components/calendar-selection"; +import { SyncProvider } from "~/calendar/components/context"; import { usePermission } from "~/shared/hooks/usePermissions"; function AppleCalendarList() { diff --git a/apps/desktop/src/services/apple-calendar/ctx.ts b/apps/desktop/src/services/apple-calendar/ctx.ts deleted file mode 100644 index 80a3a7d9f3..0000000000 --- a/apps/desktop/src/services/apple-calendar/ctx.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { Queries } from "tinybase/with-schemas"; - -import { commands as calendarCommands } from "@hypr/plugin-calendar"; - -import { findCalendarByTrackingId } from "~/calendar/utils"; -import { QUERIES, type Schemas, type Store } from "~/store/tinybase/store/main"; - -// --- - -export interface Ctx { - store: Store; - userId: string; - from: Date; - to: Date; - calendarIds: Set; - calendarTrackingIdToId: Map; -} - -// --- - -export function createCtx(store: Store, queries: Queries): Ctx | null { - const resultTable = queries.getResultTable(QUERIES.enabledAppleCalendars); - - const calendarIds = new Set(Object.keys(resultTable)); - const calendarTrackingIdToId = new Map(); - - for (const calendarId of calendarIds) { - const calendar = store.getRow("calendars", calendarId); - const trackingId = calendar?.tracking_id_calendar as string | undefined; - if (trackingId) { - calendarTrackingIdToId.set(trackingId, calendarId); - } - } - - if (calendarTrackingIdToId.size === 0) { - return null; - } - - const userId = store.getValue("user_id"); - if (!userId) { - return null; - } - - const { from, to } = getRange(); - - return { - store, - userId: String(userId), - from, - to, - calendarIds, - calendarTrackingIdToId, - }; -} - -// --- - -export async function syncCalendars(store: Store): Promise { - const userId = store.getValue("user_id"); - if (!userId) return; - - const result = await calendarCommands.listCalendars("apple"); - if (result.status === "error") return; - - const incomingCalendars = result.data; - const incomingIds = new Set(incomingCalendars.map((cal) => cal.id)); - - store.transaction(() => { - for (const rowId of store.getRowIds("calendars")) { - const row = store.getRow("calendars", rowId); - if ( - row.provider === "apple" && - !incomingIds.has(row.tracking_id_calendar as string) - ) { - store.delRow("calendars", rowId); - } - } - - for (const cal of incomingCalendars) { - const existingRowId = findCalendarByTrackingId(store, cal.id); - const rowId = existingRowId ?? crypto.randomUUID(); - const existing = existingRowId - ? store.getRow("calendars", existingRowId) - : null; - - store.setRow("calendars", rowId, { - user_id: String(userId), - created_at: existing?.created_at || new Date().toISOString(), - tracking_id_calendar: cal.id, - name: cal.title, - enabled: existing?.enabled ?? false, - provider: "apple", - source: cal.source ?? "Apple Calendar", - color: cal.color ?? "#888", - }); - } - }); -} - -// --- - -const getRange = () => { - const now = new Date(); - const from = new Date(now); - from.setDate(from.getDate() - 7); - const to = new Date(now); - to.setDate(to.getDate() + 30); - return { from, to }; -}; diff --git a/apps/desktop/src/services/calendar/ctx.ts b/apps/desktop/src/services/calendar/ctx.ts new file mode 100644 index 0000000000..a1ca57706f --- /dev/null +++ b/apps/desktop/src/services/calendar/ctx.ts @@ -0,0 +1,145 @@ +import type { Queries } from "tinybase/with-schemas"; + +import { commands as calendarCommands } from "@hypr/plugin-calendar"; +import type { CalendarProviderType } from "@hypr/plugin-calendar"; + +import { findCalendarByTrackingId } from "~/calendar/utils"; +import { QUERIES, type Schemas, type Store } from "~/store/tinybase/store/main"; + +// --- + +export interface Ctx { + store: Store; + provider: CalendarProviderType; + userId: string; + from: Date; + to: Date; + calendarIds: Set; + calendarTrackingIdToId: Map; +} + +// --- + +export function createCtx( + store: Store, + queries: Queries, + provider: CalendarProviderType, +): Ctx | null { + const resultTable = queries.getResultTable(QUERIES.enabledCalendars); + + const calendarIds = new Set(); + const calendarTrackingIdToId = new Map(); + + for (const calendarId of Object.keys(resultTable)) { + const calendar = store.getRow("calendars", calendarId); + if (calendar?.provider !== provider) { + continue; + } + + calendarIds.add(calendarId); + + const trackingId = calendar?.tracking_id_calendar as string | undefined; + if (trackingId) { + calendarTrackingIdToId.set(trackingId, calendarId); + } + } + + // We can't do this because we need a ctx to delete + // left-over events from old calendars in sync + // if (calendarTrackingIdToId.size === 0) { + // return null; + // } + + const userId = store.getValue("user_id"); + if (!userId) { + return null; + } + + const { from, to } = getRange(); + + return { + store, + provider, + userId: String(userId), + from, + to, + calendarIds, + calendarTrackingIdToId, + }; +} + +// --- + +export async function getActiveProviders(): Promise { + const available = await calendarCommands.availableProviders(); + const active: CalendarProviderType[] = []; + + for (const provider of available) { + const result = await calendarCommands.isProviderEnabled(provider); + if (result.status === "ok" && result.data) { + active.push(provider); + } + } + + return active; +} + +// --- + +export async function syncCalendars( + store: Store, + providers: CalendarProviderType[], +): Promise { + const userId = store.getValue("user_id"); + if (!userId) return; + + for (const provider of providers) { + const result = await calendarCommands.listCalendars(provider); + if (result.status === "error") continue; + + const incomingCalendars = result.data; + const incomingIds = new Set(incomingCalendars.map((cal) => cal.id)); + + store.transaction(() => { + for (const rowId of store.getRowIds("calendars")) { + const row = store.getRow("calendars", rowId); + if ( + row.provider === provider && + !incomingIds.has(row.tracking_id_calendar as string) + ) { + store.delRow("calendars", rowId); + } + } + + for (const cal of incomingCalendars) { + const existingRowId = findCalendarByTrackingId(store, cal.id); + const rowId = existingRowId ?? crypto.randomUUID(); + const existing = existingRowId + ? store.getRow("calendars", existingRowId) + : null; + + store.setRow("calendars", rowId, { + user_id: String(userId), + created_at: existing?.created_at || new Date().toISOString(), + tracking_id_calendar: cal.id, + name: cal.title, + enabled: existing?.enabled ?? false, + provider, + source: cal.source ?? provider, + color: cal.color ?? "#888", + }); + } + }); + } +} + +// --- + +const getRange = () => { + const now = new Date(); + const from = new Date(now); + from.setDate(from.getDate() - 7); + const to = new Date(now); + to.setDate(to.getDate() + 30); + return { from, to }; +}; diff --git a/apps/desktop/src/services/apple-calendar/fetch/existing.ts b/apps/desktop/src/services/calendar/fetch/existing.ts similarity index 90% rename from apps/desktop/src/services/apple-calendar/fetch/existing.ts rename to apps/desktop/src/services/calendar/fetch/existing.ts index 3368ef818b..348d727b50 100644 --- a/apps/desktop/src/services/apple-calendar/fetch/existing.ts +++ b/apps/desktop/src/services/calendar/fetch/existing.ts @@ -1,7 +1,6 @@ +import type { Ctx } from "../ctx"; import type { ExistingEvent } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; - function isEventInRange( startedAt: string, endedAt: string | undefined, @@ -26,6 +25,10 @@ export function fetchExistingEvents(ctx: Ctx): ExistingEvent[] { return; } + if (event.provider && event.provider !== ctx.provider) { + return; + } + const startedAt = event.started_at; if (!startedAt) return; @@ -46,6 +49,7 @@ export function fetchExistingEvents(ctx: Ctx): ExistingEvent[] { note: event.note, recurrence_series_id: event.recurrence_series_id, has_recurrence_rules: event.has_recurrence_rules, + provider: event.provider, }); } }); diff --git a/apps/desktop/src/services/apple-calendar/fetch/incoming.ts b/apps/desktop/src/services/calendar/fetch/incoming.ts similarity index 96% rename from apps/desktop/src/services/apple-calendar/fetch/incoming.ts rename to apps/desktop/src/services/calendar/fetch/incoming.ts index 58fd2fe2ab..090ffc34c4 100644 --- a/apps/desktop/src/services/apple-calendar/fetch/incoming.ts +++ b/apps/desktop/src/services/calendar/fetch/incoming.ts @@ -2,14 +2,13 @@ import { commands as calendarCommands } from "@hypr/plugin-calendar"; import type { CalendarEvent } from "@hypr/plugin-calendar"; import { commands as miscCommands } from "@hypr/plugin-misc"; +import type { Ctx } from "../ctx"; import type { EventParticipant, IncomingEvent, IncomingParticipants, } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; - export class CalendarFetchError extends Error { constructor( public readonly calendarTrackingId: string, @@ -30,7 +29,7 @@ export async function fetchIncomingEvents(ctx: Ctx): Promise<{ const results = await Promise.all( trackingIds.map(async (trackingId) => { - const result = await calendarCommands.listEvents("apple", { + const result = await calendarCommands.listEvents(ctx.provider, { calendar_tracking_id: trackingId, from: ctx.from.toISOString(), to: ctx.to.toISOString(), diff --git a/apps/desktop/src/services/apple-calendar/fetch/index.ts b/apps/desktop/src/services/calendar/fetch/index.ts similarity index 100% rename from apps/desktop/src/services/apple-calendar/fetch/index.ts rename to apps/desktop/src/services/calendar/fetch/index.ts diff --git a/apps/desktop/src/services/apple-calendar/fetch/types.ts b/apps/desktop/src/services/calendar/fetch/types.ts similarity index 100% rename from apps/desktop/src/services/apple-calendar/fetch/types.ts rename to apps/desktop/src/services/calendar/fetch/types.ts diff --git a/apps/desktop/src/services/apple-calendar/index.ts b/apps/desktop/src/services/calendar/index.ts similarity index 66% rename from apps/desktop/src/services/apple-calendar/index.ts rename to apps/desktop/src/services/calendar/index.ts index 5dcb8848a9..9e6e693a7a 100644 --- a/apps/desktop/src/services/apple-calendar/index.ts +++ b/apps/desktop/src/services/calendar/index.ts @@ -1,6 +1,8 @@ import type { Queries } from "tinybase/with-schemas"; -import { createCtx, syncCalendars } from "./ctx"; +import type { CalendarProviderType } from "@hypr/plugin-calendar"; + +import { createCtx, getActiveProviders, syncCalendars } from "./ctx"; import { CalendarFetchError, fetchExistingEvents, @@ -29,11 +31,25 @@ export async function syncCalendarEvents( } async function run(store: Store, queries: Queries) { - await syncCalendars(store); + const providers = await getActiveProviders(); + await syncCalendars(store, providers); + for (const provider of providers) { + try { + await runForProvider(store, queries, provider); + } catch (error) { + console.error(`[calendar-sync] Error syncing ${provider}: ${error}`); + } + } +} - const ctx = createCtx(store, queries); +async function runForProvider( + store: Store, + queries: Queries, + provider: CalendarProviderType, +) { + const ctx = createCtx(store, queries, provider); if (!ctx) { - return null; + return; } let incoming; @@ -46,9 +62,9 @@ async function run(store: Store, queries: Queries) { } catch (error) { if (error instanceof CalendarFetchError) { console.error( - `[calendar-sync] Aborting sync due to fetch error: ${error.message}`, + `[calendar-sync] Aborting ${provider} sync due to fetch error: ${error.message}`, ); - return null; + return; } throw error; } diff --git a/apps/desktop/src/services/apple-calendar/process/events/execute.test.ts b/apps/desktop/src/services/calendar/process/events/execute.test.ts similarity index 97% rename from apps/desktop/src/services/apple-calendar/process/events/execute.test.ts rename to apps/desktop/src/services/calendar/process/events/execute.test.ts index 4c134f3c24..967a0ee797 100644 --- a/apps/desktop/src/services/apple-calendar/process/events/execute.test.ts +++ b/apps/desktop/src/services/calendar/process/events/execute.test.ts @@ -2,11 +2,10 @@ import { describe, expect, test } from "vitest"; import type { SessionEvent } from "@hypr/store"; +import type { Ctx } from "../../ctx"; +import type { IncomingEvent } from "../../fetch/types"; import { syncSessionEmbeddedEvents } from "./execute"; -import type { Ctx } from "~/services/apple-calendar/ctx"; -import type { IncomingEvent } from "~/services/apple-calendar/fetch/types"; - type MockStoreData = { sessions: Record>; events: Record>; @@ -57,6 +56,7 @@ function createMockCtx( ): Ctx { return { store: createMockStore(storeData), + provider: "apple" as const, userId: "user-1", from: new Date("2024-01-01"), to: new Date("2024-02-01"), diff --git a/apps/desktop/src/services/apple-calendar/process/events/execute.ts b/apps/desktop/src/services/calendar/process/events/execute.ts similarity index 95% rename from apps/desktop/src/services/apple-calendar/process/events/execute.ts rename to apps/desktop/src/services/calendar/process/events/execute.ts index 19adf66123..ac199fb9ef 100644 --- a/apps/desktop/src/services/apple-calendar/process/events/execute.ts +++ b/apps/desktop/src/services/calendar/process/events/execute.ts @@ -1,9 +1,9 @@ import type { EventStorage, SessionEvent } from "@hypr/store"; +import type { Ctx } from "../../ctx"; +import type { IncomingEvent } from "../../fetch/types"; import type { EventsSyncOutput } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; -import type { IncomingEvent } from "~/services/apple-calendar/fetch/types"; import { getSessionEventById } from "~/session/utils"; import { id } from "~/shared/utils"; @@ -33,6 +33,7 @@ export function executeForEventsSync(ctx: Ctx, out: EventsSyncOutput): void { recurrence_series_id: event.recurrence_series_id, has_recurrence_rules: event.has_recurrence_rules, is_all_day: event.is_all_day, + provider: ctx.provider, participants_json: event.participants.length > 0 ? JSON.stringify(event.participants) @@ -64,6 +65,7 @@ export function executeForEventsSync(ctx: Ctx, out: EventsSyncOutput): void { recurrence_series_id: eventToAdd.recurrence_series_id, has_recurrence_rules: eventToAdd.has_recurrence_rules, is_all_day: eventToAdd.is_all_day, + provider: ctx.provider, participants_json: eventToAdd.participants.length > 0 ? JSON.stringify(eventToAdd.participants) diff --git a/apps/desktop/src/services/apple-calendar/process/events/index.ts b/apps/desktop/src/services/calendar/process/events/index.ts similarity index 100% rename from apps/desktop/src/services/apple-calendar/process/events/index.ts rename to apps/desktop/src/services/calendar/process/events/index.ts diff --git a/apps/desktop/src/services/apple-calendar/process/events/sync.test.ts b/apps/desktop/src/services/calendar/process/events/sync.test.ts similarity index 98% rename from apps/desktop/src/services/apple-calendar/process/events/sync.test.ts rename to apps/desktop/src/services/calendar/process/events/sync.test.ts index 2d9af7f9d0..feb9d5f648 100644 --- a/apps/desktop/src/services/apple-calendar/process/events/sync.test.ts +++ b/apps/desktop/src/services/calendar/process/events/sync.test.ts @@ -1,14 +1,10 @@ import { describe, expect, test } from "vitest"; +import type { Ctx } from "../../ctx"; +import type { ExistingEvent, IncomingEvent } from "../../fetch/types"; import { syncEvents } from "./sync"; import type { EventsSyncInput } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; -import type { - ExistingEvent, - IncomingEvent, -} from "~/services/apple-calendar/fetch/types"; - function createMockStore(config: { eventToSession?: Map; nonEmptySessions?: Set; @@ -56,6 +52,7 @@ function createMockCtx( }); return { + provider: "apple" as const, userId: "user-1", from: new Date("2024-01-01"), to: new Date("2024-02-01"), @@ -95,6 +92,7 @@ function createExistingEvent( title: "Existing Event", started_at: "2024-01-15T10:00:00Z", ended_at: "2024-01-15T11:00:00Z", + provider: "apple", ...overrides, }; } diff --git a/apps/desktop/src/services/apple-calendar/process/events/sync.ts b/apps/desktop/src/services/calendar/process/events/sync.ts similarity index 96% rename from apps/desktop/src/services/apple-calendar/process/events/sync.ts rename to apps/desktop/src/services/calendar/process/events/sync.ts index 23b716218c..a779d135d9 100644 --- a/apps/desktop/src/services/apple-calendar/process/events/sync.ts +++ b/apps/desktop/src/services/calendar/process/events/sync.ts @@ -1,7 +1,6 @@ +import type { Ctx } from "../../ctx"; import type { EventsSyncInput, EventsSyncOutput } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; - export function syncEvents( ctx: Ctx, { incoming, existing, incomingParticipants }: EventsSyncInput, diff --git a/apps/desktop/src/services/apple-calendar/process/events/types.ts b/apps/desktop/src/services/calendar/process/events/types.ts similarity index 92% rename from apps/desktop/src/services/apple-calendar/process/events/types.ts rename to apps/desktop/src/services/calendar/process/events/types.ts index 4fe5e44649..e944e6046b 100644 --- a/apps/desktop/src/services/apple-calendar/process/events/types.ts +++ b/apps/desktop/src/services/calendar/process/events/types.ts @@ -4,7 +4,7 @@ import type { ExistingEvent, IncomingEvent, IncomingParticipants, -} from "~/services/apple-calendar/fetch/types"; +} from "../../fetch/types"; export type EventId = string; diff --git a/apps/desktop/src/services/apple-calendar/process/index.ts b/apps/desktop/src/services/calendar/process/index.ts similarity index 100% rename from apps/desktop/src/services/apple-calendar/process/index.ts rename to apps/desktop/src/services/calendar/process/index.ts diff --git a/apps/desktop/src/services/apple-calendar/process/participants/execute.ts b/apps/desktop/src/services/calendar/process/participants/execute.ts similarity index 95% rename from apps/desktop/src/services/apple-calendar/process/participants/execute.ts rename to apps/desktop/src/services/calendar/process/participants/execute.ts index e2e4b6b5f5..7625f4bcdc 100644 --- a/apps/desktop/src/services/apple-calendar/process/participants/execute.ts +++ b/apps/desktop/src/services/calendar/process/participants/execute.ts @@ -3,9 +3,9 @@ import type { MappingSessionParticipantStorage, } from "@hypr/store"; +import type { Ctx } from "../../ctx"; import type { ParticipantsSyncOutput } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; import { id } from "~/shared/utils"; export function executeForParticipantsSync( diff --git a/apps/desktop/src/services/apple-calendar/process/participants/index.ts b/apps/desktop/src/services/calendar/process/participants/index.ts similarity index 100% rename from apps/desktop/src/services/apple-calendar/process/participants/index.ts rename to apps/desktop/src/services/calendar/process/participants/index.ts diff --git a/apps/desktop/src/services/apple-calendar/process/participants/sync.test.ts b/apps/desktop/src/services/calendar/process/participants/sync.test.ts similarity index 98% rename from apps/desktop/src/services/apple-calendar/process/participants/sync.test.ts rename to apps/desktop/src/services/calendar/process/participants/sync.test.ts index 9f654ee0d9..0f1d49f667 100644 --- a/apps/desktop/src/services/apple-calendar/process/participants/sync.test.ts +++ b/apps/desktop/src/services/calendar/process/participants/sync.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from "vitest"; +import type { Ctx } from "../../ctx"; import { syncSessionParticipants } from "./sync"; -import type { Ctx } from "~/services/apple-calendar/ctx"; - type MockStoreData = { humans: Record; sessions: Record; @@ -33,6 +32,7 @@ function createMockStore(data: MockStoreData) { function createMockCtx(store: Ctx["store"]): Ctx { return { store, + provider: "apple" as const, userId: "user-1", from: new Date("2024-01-01"), to: new Date("2024-02-01"), diff --git a/apps/desktop/src/services/apple-calendar/process/participants/sync.ts b/apps/desktop/src/services/calendar/process/participants/sync.ts similarity index 96% rename from apps/desktop/src/services/apple-calendar/process/participants/sync.ts rename to apps/desktop/src/services/calendar/process/participants/sync.ts index 1570570dcc..268516894f 100644 --- a/apps/desktop/src/services/apple-calendar/process/participants/sync.ts +++ b/apps/desktop/src/services/calendar/process/participants/sync.ts @@ -1,3 +1,5 @@ +import type { Ctx } from "../../ctx"; +import type { EventParticipant } from "../../fetch/types"; import type { HumanToCreate, ParticipantMappingToAdd, @@ -5,8 +7,6 @@ import type { ParticipantsSyncOutput, } from "./types"; -import type { Ctx } from "~/services/apple-calendar/ctx"; -import type { EventParticipant } from "~/services/apple-calendar/fetch/types"; import { findSessionByTrackingId } from "~/session/utils"; import { id } from "~/shared/utils"; import type { Store } from "~/store/tinybase/store/main"; diff --git a/apps/desktop/src/services/apple-calendar/process/participants/types.ts b/apps/desktop/src/services/calendar/process/participants/types.ts similarity index 84% rename from apps/desktop/src/services/apple-calendar/process/participants/types.ts rename to apps/desktop/src/services/calendar/process/participants/types.ts index 9bf7fd88e0..4df4fc8eb1 100644 --- a/apps/desktop/src/services/apple-calendar/process/participants/types.ts +++ b/apps/desktop/src/services/calendar/process/participants/types.ts @@ -1,4 +1,4 @@ -import type { IncomingParticipants } from "~/services/apple-calendar/fetch/types"; +import type { IncomingParticipants } from "../../fetch/types"; export type ParticipantMappingId = string; diff --git a/apps/desktop/src/services/task-manager.tsx b/apps/desktop/src/services/task-manager.tsx index 96f3029861..f983d00837 100644 --- a/apps/desktop/src/services/task-manager.tsx +++ b/apps/desktop/src/services/task-manager.tsx @@ -8,7 +8,7 @@ import { import { events as appleCalendarEvents } from "@hypr/plugin-calendar"; -import { CALENDAR_SYNC_TASK_ID, syncCalendarEvents } from "./apple-calendar"; +import { CALENDAR_SYNC_TASK_ID, syncCalendarEvents } from "./calendar"; import { checkEventNotifications, EVENT_NOTIFICATION_INTERVAL, diff --git a/apps/desktop/src/store/tinybase/store/main.ts b/apps/desktop/src/store/tinybase/store/main.ts index ec39d01c93..106b0b0226 100644 --- a/apps/desktop/src/store/tinybase/store/main.ts +++ b/apps/desktop/src/store/tinybase/store/main.ts @@ -181,14 +181,11 @@ export const StoreComponent = () => { }, ) .setQueryDefinition( - QUERIES.enabledAppleCalendars, + QUERIES.enabledCalendars, "calendars", ({ select, where }) => { select("provider"); - where( - (getCell) => - getCell("enabled") === true && getCell("provider") === "apple", - ); + where("enabled", true); }, ) .setQueryDefinition( @@ -368,7 +365,7 @@ export const QUERIES = { visibleVocabs: "visibleVocabs", sessionParticipantsWithDetails: "sessionParticipantsWithDetails", sessionRecordingTimes: "sessionRecordingTimes", - enabledAppleCalendars: "enabledAppleCalendars", + enabledCalendars: "enabledCalendars", userTemplates: "userTemplates", } as const; @@ -459,7 +456,7 @@ interface _QueryResultRows { min_started_at: number; max_ended_at: number; }; - enabledAppleCalendars: { + enabledCalendars: { provider: string; }; userTemplates: { diff --git a/packages/store/src/tinybase.ts b/packages/store/src/tinybase.ts index db3d5e56b0..0800cc2f33 100644 --- a/packages/store/src/tinybase.ts +++ b/packages/store/src/tinybase.ts @@ -85,6 +85,7 @@ export const tableSchemaForTinybase = { recurrence_series_id: { type: "string" }, has_recurrence_rules: { type: "boolean" }, is_all_day: { type: "boolean" }, + provider: { type: "string" }, participants_json: { type: "string" }, } as const satisfies InferTinyBaseSchema, mapping_session_participant: { diff --git a/packages/store/src/zod.ts b/packages/store/src/zod.ts index 1e495a8d3c..e4fa41e965 100644 --- a/packages/store/src/zod.ts +++ b/packages/store/src/zod.ts @@ -49,6 +49,9 @@ export const eventParticipantSchema = z.object({ is_current_user: z.boolean().optional(), }); +export const calendarProviderSchema = z.enum(["apple", "google", "outlook"]); +export type CalendarProvider = z.infer; + export const eventSchema = z.object({ user_id: z.string(), created_at: z.string(), @@ -70,15 +73,13 @@ export const eventSchema = z.object({ z.boolean().optional(), ), is_all_day: z.preprocess((val) => val ?? undefined, z.boolean().optional()), + provider: calendarProviderSchema, participants_json: z.preprocess( (val) => val ?? undefined, z.string().optional(), ), }); -export const calendarProviderSchema = z.enum(["apple", "google", "outlook"]); -export type CalendarProvider = z.infer; - export const calendarSchema = z.object({ user_id: z.string(), created_at: z.string(), diff --git a/plugins/calendar/Cargo.toml b/plugins/calendar/Cargo.toml index 7b502b18ca..22c62d64ce 100644 --- a/plugins/calendar/Cargo.toml +++ b/plugins/calendar/Cargo.toml @@ -16,6 +16,7 @@ hypr-outlook-calendar = { workspace = true, features = ["specta"] } tauri = { workspace = true, features = ["test"] } tauri-plugin-auth = { workspace = true } +tauri-plugin-permissions = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } chrono = { workspace = true, features = ["serde"] } diff --git a/plugins/calendar/build.rs b/plugins/calendar/build.rs index bc94a39a3c..04ee7a113b 100644 --- a/plugins/calendar/build.rs +++ b/plugins/calendar/build.rs @@ -1,5 +1,6 @@ const COMMANDS: &[&str] = &[ "available_providers", + "is_provider_enabled", "list_calendars", "list_events", "open_calendar", diff --git a/plugins/calendar/js/bindings.gen.ts b/plugins/calendar/js/bindings.gen.ts index 3ee9bae84d..b60095eded 100644 --- a/plugins/calendar/js/bindings.gen.ts +++ b/plugins/calendar/js/bindings.gen.ts @@ -9,6 +9,14 @@ export const commands = { async availableProviders() : Promise { return await TAURI_INVOKE("plugin:calendar|available_providers"); }, +async isProviderEnabled(provider: CalendarProviderType) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:calendar|is_provider_enabled", { provider }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async listCalendars(provider: CalendarProviderType) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("plugin:calendar|list_calendars", { provider }) }; diff --git a/plugins/calendar/permissions/autogenerated/commands/is_provider_enabled.toml b/plugins/calendar/permissions/autogenerated/commands/is_provider_enabled.toml new file mode 100644 index 0000000000..339cb03356 --- /dev/null +++ b/plugins/calendar/permissions/autogenerated/commands/is_provider_enabled.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-provider-enabled" +description = "Enables the is_provider_enabled command without any pre-configured scope." +commands.allow = ["is_provider_enabled"] + +[[permission]] +identifier = "deny-is-provider-enabled" +description = "Denies the is_provider_enabled command without any pre-configured scope." +commands.deny = ["is_provider_enabled"] diff --git a/plugins/calendar/permissions/autogenerated/reference.md b/plugins/calendar/permissions/autogenerated/reference.md index 86a88f8810..da1bdcee48 100644 --- a/plugins/calendar/permissions/autogenerated/reference.md +++ b/plugins/calendar/permissions/autogenerated/reference.md @@ -5,6 +5,7 @@ Default permissions for the plugin #### This default permission set includes the following: - `allow-available-providers` +- `allow-is-provider-enabled` - `allow-list-calendars` - `allow-list-events` - `allow-open-calendar` @@ -74,6 +75,32 @@ Denies the create_event command without any pre-configured scope. +`calendar:allow-is-provider-enabled` + + + + +Enables the is_provider_enabled command without any pre-configured scope. + + + + + + + +`calendar:deny-is-provider-enabled` + + + + +Denies the is_provider_enabled command without any pre-configured scope. + + + + + + + `calendar:allow-list-calendars` diff --git a/plugins/calendar/permissions/default.toml b/plugins/calendar/permissions/default.toml index 92948b1110..af2a28e521 100644 --- a/plugins/calendar/permissions/default.toml +++ b/plugins/calendar/permissions/default.toml @@ -2,6 +2,7 @@ description = "Default permissions for the plugin" permissions = [ "allow-available-providers", + "allow-is-provider-enabled", "allow-list-calendars", "allow-list-events", "allow-open-calendar", diff --git a/plugins/calendar/permissions/schemas/schema.json b/plugins/calendar/permissions/schemas/schema.json index f12156b2fa..0b290de22f 100644 --- a/plugins/calendar/permissions/schemas/schema.json +++ b/plugins/calendar/permissions/schemas/schema.json @@ -318,6 +318,18 @@ "const": "deny-create-event", "markdownDescription": "Denies the create_event command without any pre-configured scope." }, + { + "description": "Enables the is_provider_enabled command without any pre-configured scope.", + "type": "string", + "const": "allow-is-provider-enabled", + "markdownDescription": "Enables the is_provider_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_provider_enabled command without any pre-configured scope.", + "type": "string", + "const": "deny-is-provider-enabled", + "markdownDescription": "Denies the is_provider_enabled command without any pre-configured scope." + }, { "description": "Enables the list_calendars command without any pre-configured scope.", "type": "string", @@ -355,10 +367,10 @@ "markdownDescription": "Denies the open_calendar command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-available-providers`\n- `allow-list-calendars`\n- `allow-list-events`\n- `allow-open-calendar`\n- `allow-create-event`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-available-providers`\n- `allow-is-provider-enabled`\n- `allow-list-calendars`\n- `allow-list-events`\n- `allow-open-calendar`\n- `allow-create-event`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-available-providers`\n- `allow-list-calendars`\n- `allow-list-events`\n- `allow-open-calendar`\n- `allow-create-event`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-available-providers`\n- `allow-is-provider-enabled`\n- `allow-list-calendars`\n- `allow-list-events`\n- `allow-open-calendar`\n- `allow-create-event`" } ] } diff --git a/plugins/calendar/src/commands.rs b/plugins/calendar/src/commands.rs index 38b5afcc0f..dc8fcd5b9d 100644 --- a/plugins/calendar/src/commands.rs +++ b/plugins/calendar/src/commands.rs @@ -11,6 +11,15 @@ pub fn available_providers() -> Vec { crate::ext::available_providers() } +#[tauri::command] +#[specta::specta] +pub async fn is_provider_enabled( + app: tauri::AppHandle, + provider: CalendarProviderType, +) -> Result { + app.calendar().is_provider_enabled(provider).await +} + #[tauri::command] #[specta::specta] pub async fn list_calendars( diff --git a/plugins/calendar/src/ext.rs b/plugins/calendar/src/ext.rs index e3a62ba40f..92ded54120 100644 --- a/plugins/calendar/src/ext.rs +++ b/plugins/calendar/src/ext.rs @@ -6,6 +6,7 @@ use hypr_calendar_interface::{ use hypr_google_calendar::{CalendarListEntry as GoogleCalendar, Event as GoogleEvent}; use hypr_outlook_calendar::{Calendar as OutlookCalendar, Event as OutlookEvent}; use tauri_plugin_auth::AuthPluginExt; +use tauri_plugin_permissions::PermissionsPluginExt; use crate::error::Error; use crate::fetch; @@ -126,6 +127,40 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> CalendarExt<'a, R, M> { } } + pub async fn is_provider_enabled(&self, provider: CalendarProviderType) -> Result { + match provider { + CalendarProviderType::Apple => { + let status = self + .manager + .permissions() + .check(tauri_plugin_permissions::Permission::Calendar) + .await + .map_err(|e| Error::Api(e.to_string()))?; + + Ok(matches!( + status, + tauri_plugin_permissions::PermissionStatus::Authorized + )) + } + CalendarProviderType::Google => { + let token = match self.get_access_token() { + Ok(token) => token, + Err(_) => return Ok(false), + }; + let config = self.manager.state::(); + fetch::has_nango_connection(&config.api_base_url, &token, "google-calendar").await + } + CalendarProviderType::Outlook => { + let token = match self.get_access_token() { + Ok(token) => token, + Err(_) => return Ok(false), + }; + let config = self.manager.state::(); + fetch::has_nango_connection(&config.api_base_url, &token, "outlook-calendar").await + } + } + } + fn get_access_token(&self) -> Result { let token = self .manager diff --git a/plugins/calendar/src/fetch.rs b/plugins/calendar/src/fetch.rs index b040f765e9..8a598b873a 100644 --- a/plugins/calendar/src/fetch.rs +++ b/plugins/calendar/src/fetch.rs @@ -4,6 +4,24 @@ use hypr_outlook_calendar::{Calendar as OutlookCalendar, Event as OutlookEvent}; use crate::error::Error; +pub async fn has_nango_connection( + api_base_url: &str, + access_token: &str, + integration_id: &str, +) -> Result { + let client = make_client(api_base_url, access_token)?; + + let response = client + .list_connections() + .await + .map_err(|e| Error::Api(e.to_string()))?; + + let connections = response.into_inner().connections; + Ok(connections + .iter() + .any(|c| c.integration_id == integration_id)) +} + fn make_client(api_base_url: &str, access_token: &str) -> Result { let auth_value = format!("Bearer {access_token}").parse()?; let mut headers = reqwest::header::HeaderMap::new(); diff --git a/plugins/calendar/src/lib.rs b/plugins/calendar/src/lib.rs index 76bab1ff5a..1fe7175b21 100644 --- a/plugins/calendar/src/lib.rs +++ b/plugins/calendar/src/lib.rs @@ -20,6 +20,7 @@ fn make_specta_builder() -> tauri_specta::Builder { .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ commands::available_providers, + commands::is_provider_enabled::, commands::list_calendars::, commands::list_events::, commands::open_calendar::,