mysticals 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +856 -173
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createElement } from "react";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.5.0";
|
|
9
9
|
|
|
10
10
|
// src/client.ts
|
|
11
11
|
import { spawn } from "child_process";
|
|
@@ -46,6 +46,7 @@ var methodSet = {
|
|
|
46
46
|
"accounts.addCaldav": true,
|
|
47
47
|
"accounts.update": true,
|
|
48
48
|
"accounts.remove": true,
|
|
49
|
+
"accounts.reauth": true,
|
|
49
50
|
"calendars.list": true,
|
|
50
51
|
"calendars.setVisible": true,
|
|
51
52
|
"events.list": true,
|
|
@@ -53,6 +54,7 @@ var methodSet = {
|
|
|
53
54
|
"events.update": true,
|
|
54
55
|
"events.delete": true,
|
|
55
56
|
"events.respond": true,
|
|
57
|
+
"events.recurrence": true,
|
|
56
58
|
"sync.now": true
|
|
57
59
|
};
|
|
58
60
|
var METHODS = Object.keys(methodSet);
|
|
@@ -238,6 +240,21 @@ function when(e, now) {
|
|
|
238
240
|
return `${isSameDay(s, now) ? "Today" : format(s, "EEE, d MMM")} \xB7 ${format(s, "HH:mm")}`;
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
// ../../packages/core/src/providers/http.ts
|
|
244
|
+
var http = { timeoutMs: 3e4 };
|
|
245
|
+
var timedFetch = async (input, init) => {
|
|
246
|
+
const timeout = AbortSignal.timeout(http.timeoutMs);
|
|
247
|
+
try {
|
|
248
|
+
return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
|
|
249
|
+
} catch (e) {
|
|
250
|
+
if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
|
|
251
|
+
throw e;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var AuthError = class extends Error {
|
|
255
|
+
name = "AuthError";
|
|
256
|
+
};
|
|
257
|
+
|
|
241
258
|
// ../../packages/core/src/sync/engine.ts
|
|
242
259
|
var MIN = 6e4;
|
|
243
260
|
var SyncEngine = class {
|
|
@@ -260,6 +277,8 @@ var SyncEngine = class {
|
|
|
260
277
|
queued = /* @__PURE__ */ new Map();
|
|
261
278
|
/** Bumped by `edited`: a pass that started before a local edit must not overwrite it. */
|
|
262
279
|
edits = /* @__PURE__ */ new Map();
|
|
280
|
+
/** Accounts whose last pass listed no calendars while the cache had some (see syncAccount). */
|
|
281
|
+
emptied = /* @__PURE__ */ new Set();
|
|
263
282
|
running = false;
|
|
264
283
|
unsubscribe;
|
|
265
284
|
start() {
|
|
@@ -279,6 +298,7 @@ var SyncEngine = class {
|
|
|
279
298
|
/**
|
|
280
299
|
* Sync one account, or all accounts independently when id omitted.
|
|
281
300
|
* A single-account call rejects with that account's error; the all-accounts call never rejects.
|
|
301
|
+
* The all-accounts call skips accounts whose credentials were rejected (`authError`): retrying can't help.
|
|
282
302
|
*/
|
|
283
303
|
/**
|
|
284
304
|
* `quiet`: the change came from this app (own edit/RSVP), so don't notify about it.
|
|
@@ -287,7 +307,7 @@ var SyncEngine = class {
|
|
|
287
307
|
*/
|
|
288
308
|
async syncNow(accountId, opts = {}) {
|
|
289
309
|
if (accountId === void 0) {
|
|
290
|
-
await Promise.allSettled(this.store.list().map((a) => this.syncNow(a.id)));
|
|
310
|
+
await Promise.allSettled(this.store.list().filter((a) => !a.authError).map((a) => this.syncNow(a.id)));
|
|
291
311
|
return;
|
|
292
312
|
}
|
|
293
313
|
const existing = this.inflight.get(accountId);
|
|
@@ -325,7 +345,8 @@ var SyncEngine = class {
|
|
|
325
345
|
schedule(id2) {
|
|
326
346
|
clearTimeout(this.timers.get(id2));
|
|
327
347
|
this.timers.delete(id2);
|
|
328
|
-
|
|
348
|
+
const acc = this.store.list().find((a) => a.id === id2);
|
|
349
|
+
if (!this.running || !acc || acc.authError) {
|
|
329
350
|
this.failures.delete(id2);
|
|
330
351
|
return;
|
|
331
352
|
}
|
|
@@ -339,7 +360,8 @@ var SyncEngine = class {
|
|
|
339
360
|
}
|
|
340
361
|
// ponytail: quiet silences the whole sync, so an external change landing in the same pass is not announced.
|
|
341
362
|
async syncAccount(id2, quiet) {
|
|
342
|
-
const
|
|
363
|
+
const acc = this.store.list().find((a) => a.id === id2);
|
|
364
|
+
const hadError = !!acc?.error;
|
|
343
365
|
const edits = this.edits.get(id2) ?? 0;
|
|
344
366
|
try {
|
|
345
367
|
const provider = this.store.getProvider(id2);
|
|
@@ -362,10 +384,15 @@ var SyncEngine = class {
|
|
|
362
384
|
} catch {
|
|
363
385
|
prev = void 0;
|
|
364
386
|
}
|
|
387
|
+
if (!calendars.length && prev?.calendars.length && !this.emptied.has(id2)) {
|
|
388
|
+
this.emptied.add(id2);
|
|
389
|
+
throw new Error("The server returned no calendars; showing the last synced data");
|
|
390
|
+
}
|
|
391
|
+
this.emptied.delete(id2);
|
|
365
392
|
const changed = !prev || JSON.stringify([prev.calendars, prev.events]) !== JSON.stringify([next.calendars, next.events]);
|
|
366
393
|
await this.store.writeCache(id2, next);
|
|
367
394
|
this.failures.delete(id2);
|
|
368
|
-
if (hadError) await this.store.update(id2, { error: void 0 });
|
|
395
|
+
if (hadError) await this.store.update(id2, { error: void 0, authError: false });
|
|
369
396
|
if (changed || hadError) this.onChanged(id2);
|
|
370
397
|
if (changed && !quiet && prev?.syncedAt && this.opts.onEvents) {
|
|
371
398
|
const notes = diffEvents(prev.events, next.events, now);
|
|
@@ -374,9 +401,10 @@ var SyncEngine = class {
|
|
|
374
401
|
} catch (e) {
|
|
375
402
|
this.failures.set(id2, (this.failures.get(id2) ?? 0) + 1);
|
|
376
403
|
const message = e instanceof Error ? e.message : String(e);
|
|
377
|
-
|
|
404
|
+
const auth = e instanceof AuthError;
|
|
405
|
+
await this.store.update(id2, { error: message, ...auth ? { authError: true } : {} }).catch(() => {
|
|
378
406
|
});
|
|
379
|
-
if (!hadError) this.onChanged(id2);
|
|
407
|
+
if (!hadError || auth && !acc?.authError) this.onChanged(id2);
|
|
380
408
|
throw e;
|
|
381
409
|
}
|
|
382
410
|
}
|
|
@@ -474,6 +502,15 @@ var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
|
|
474
502
|
var text = (max2) => z.string().max(max2);
|
|
475
503
|
var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
|
|
476
504
|
var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
|
|
505
|
+
var Recurrence = z.object({
|
|
506
|
+
freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
|
|
507
|
+
interval: z.number().int().min(1).max(999).optional(),
|
|
508
|
+
byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
|
|
509
|
+
until: z.iso.date().optional(),
|
|
510
|
+
count: z.number().int().min(1).max(999).optional(),
|
|
511
|
+
// An RRULE value kept from the provider: one line of plain RRULE parts only.
|
|
512
|
+
rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
|
|
513
|
+
}).strict();
|
|
477
514
|
var NewEvent = z.object({
|
|
478
515
|
accountId: id,
|
|
479
516
|
calendarId: id,
|
|
@@ -483,7 +520,8 @@ var NewEvent = z.object({
|
|
|
483
520
|
allDay: z.boolean(),
|
|
484
521
|
location: text(1e3).optional(),
|
|
485
522
|
description: text(2e4).optional(),
|
|
486
|
-
attendees: z.array(z.email()).max(500).optional()
|
|
523
|
+
attendees: z.array(z.email()).max(500).optional(),
|
|
524
|
+
recurrence: Recurrence.optional()
|
|
487
525
|
}).refine(ordered, "end before start");
|
|
488
526
|
var Attendee = z.object({
|
|
489
527
|
email: text(320).min(1),
|
|
@@ -500,7 +538,9 @@ var EventEdit = EventRef.extend({
|
|
|
500
538
|
allDay: z.boolean(),
|
|
501
539
|
location: text(1e3).optional(),
|
|
502
540
|
description: text(2e4).optional(),
|
|
503
|
-
attendees: z.array(Attendee).max(500)
|
|
541
|
+
attendees: z.array(Attendee).max(500),
|
|
542
|
+
// Absent keeps the series' rule; null stops repeating.
|
|
543
|
+
recurrence: Recurrence.nullish()
|
|
504
544
|
}).refine(ordered, "end before start");
|
|
505
545
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
|
|
506
546
|
var CaldavInput = z.object({
|
|
@@ -514,9 +554,10 @@ var CaldavInput = z.object({
|
|
|
514
554
|
password: text(2e3).min(1),
|
|
515
555
|
color: color.optional()
|
|
516
556
|
});
|
|
557
|
+
var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
|
|
517
558
|
var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
|
|
518
559
|
var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
|
|
519
|
-
var Scope = z.enum(["one", "following", "all"]).
|
|
560
|
+
var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
|
|
520
561
|
function createApi(store, sync, deps) {
|
|
521
562
|
const account = (accountId) => {
|
|
522
563
|
const a = store.get(id.parse(accountId));
|
|
@@ -579,6 +620,26 @@ function createApi(store, sync, deps) {
|
|
|
579
620
|
deps.onChanged?.(a.id);
|
|
580
621
|
return a;
|
|
581
622
|
},
|
|
623
|
+
reauth: async (accountId, raw) => {
|
|
624
|
+
const a = account(accountId);
|
|
625
|
+
let credentials;
|
|
626
|
+
if (a.kind === "google") {
|
|
627
|
+
const signed = await deps.googleSignIn();
|
|
628
|
+
if (signed.email.toLowerCase() !== a.email.toLowerCase())
|
|
629
|
+
throw new Error(`Signed in as ${signed.email}; sign in as ${a.email} to reconnect this account`);
|
|
630
|
+
credentials = signed.credentials;
|
|
631
|
+
} else {
|
|
632
|
+
const { password } = ReauthInput.parse(raw);
|
|
633
|
+
const old = store.readCreds(a.id);
|
|
634
|
+
if (old.kind !== "caldav") throw new Error("Credentials kind does not match account kind");
|
|
635
|
+
await deps.verifyCaldav({ label: a.label, serverUrl: old.serverUrl, username: old.username, password });
|
|
636
|
+
credentials = { ...old, password };
|
|
637
|
+
}
|
|
638
|
+
const updated = await store.update(a.id, { credentials, error: void 0, authError: false });
|
|
639
|
+
deps.onChanged?.(a.id);
|
|
640
|
+
syncOne(a.id);
|
|
641
|
+
return updated;
|
|
642
|
+
},
|
|
582
643
|
remove: async (accountId) => {
|
|
583
644
|
const a = account(accountId);
|
|
584
645
|
await store.remove(a.id);
|
|
@@ -612,12 +673,19 @@ function createApi(store, sync, deps) {
|
|
|
612
673
|
syncOne(a.id);
|
|
613
674
|
return ev;
|
|
614
675
|
},
|
|
615
|
-
update: async (raw) => {
|
|
676
|
+
update: async (raw, scope) => {
|
|
616
677
|
const edit = EventEdit.parse(raw);
|
|
617
678
|
const cached2 = writable(cachedEvent(edit));
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
-
|
|
679
|
+
const how = cached2.recurringEventId ? Scope.parse(scope) : "one";
|
|
680
|
+
const { title: title2, start, end, allDay, location, description, attendees, recurrence } = edit;
|
|
681
|
+
if (recurrence !== void 0 && cached2.recurringEventId && how === "one")
|
|
682
|
+
throw new Error("The repeat rule belongs to the series: choose \u201CThis and following\u201D or \u201CAll events\u201D");
|
|
683
|
+
const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees, ...recurrence !== void 0 ? { recurrence } : {} }, how);
|
|
684
|
+
if (how === "one") upsert(ev, cached2.accountId, cached2.calendarId);
|
|
685
|
+
else {
|
|
686
|
+
const own = { ...ev, accountId: cached2.accountId, calendarId: cached2.calendarId };
|
|
687
|
+
applyLocal(cached2.accountId, (events) => [...events.filter((e) => e.id !== own.id && !deletedBy(cached2, how, e)), own]);
|
|
688
|
+
}
|
|
621
689
|
syncOne(cached2.accountId);
|
|
622
690
|
return ev;
|
|
623
691
|
},
|
|
@@ -628,6 +696,10 @@ function createApi(store, sync, deps) {
|
|
|
628
696
|
applyLocal(cached2.accountId, (events) => events.filter((e) => !deletedBy(cached2, how, e)));
|
|
629
697
|
syncOne(cached2.accountId);
|
|
630
698
|
},
|
|
699
|
+
recurrence: async (raw) => {
|
|
700
|
+
const cached2 = cachedEvent(EventRef.parse(raw));
|
|
701
|
+
return store.getProvider(cached2.accountId).getRecurrence(cached2);
|
|
702
|
+
},
|
|
631
703
|
respond: async (raw, status) => {
|
|
632
704
|
const cached2 = cachedEvent(EventRef.parse(raw));
|
|
633
705
|
const ev = await store.getProvider(cached2.accountId).respond(cached2, RsvpStatus.parse(status));
|
|
@@ -716,6 +788,7 @@ var AccountStore = class {
|
|
|
716
788
|
await mkdir(dir, { recursive: true });
|
|
717
789
|
await writeAtomic(join4(dir, "creds.bin"), data);
|
|
718
790
|
}
|
|
791
|
+
/** Decrypted credentials; main/daemon only (accounts.reauth keeps the server and username), never to the renderer. */
|
|
719
792
|
readCreds(id2) {
|
|
720
793
|
return JSON.parse(this.crypto.decrypt(readFileSync2(join4(this.accountDir(id2), "creds.bin"))));
|
|
721
794
|
}
|
|
@@ -751,6 +824,10 @@ var AccountStore = class {
|
|
|
751
824
|
if (patch.error) acc.error = patch.error;
|
|
752
825
|
else delete acc.error;
|
|
753
826
|
}
|
|
827
|
+
if ("authError" in patch) {
|
|
828
|
+
if (patch.authError) acc.authError = true;
|
|
829
|
+
else delete acc.authError;
|
|
830
|
+
}
|
|
754
831
|
await this.saveRegistry();
|
|
755
832
|
return { ...acc };
|
|
756
833
|
});
|
|
@@ -829,11 +906,171 @@ var AccountStore = class {
|
|
|
829
906
|
};
|
|
830
907
|
|
|
831
908
|
// ../../packages/core/src/mock/mockApi.ts
|
|
832
|
-
import { addDays, format as
|
|
909
|
+
import { addDays as addDays2, format as format4 } from "date-fns";
|
|
833
910
|
|
|
834
911
|
// ../../packages/core/src/mock/MockProvider.ts
|
|
835
912
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
836
|
-
import { parseISO as
|
|
913
|
+
import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format3, parseISO as parseISO4, startOfWeek } from "date-fns";
|
|
914
|
+
|
|
915
|
+
// ../../packages/core/src/logic/recurrence.ts
|
|
916
|
+
import { format as format2, isValid, parseISO as parseISO3 } from "date-fns";
|
|
917
|
+
var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
|
918
|
+
var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
|
919
|
+
var SHORT = { MO: "Mon", TU: "Tue", WE: "Wed", TH: "Thu", FR: "Fri", SA: "Sat", SU: "Sun" };
|
|
920
|
+
var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
|
|
921
|
+
var FREQS = ["daily", "weekly", "monthly", "yearly"];
|
|
922
|
+
var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
|
|
923
|
+
var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
|
|
924
|
+
var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
|
|
925
|
+
var shortDay = (d) => SHORT[d];
|
|
926
|
+
function normalize(r, start) {
|
|
927
|
+
if (r.rule) return { freq: r.freq, rule: r.rule };
|
|
928
|
+
const out = { freq: r.freq };
|
|
929
|
+
if (r.interval && r.interval > 1) out.interval = r.interval;
|
|
930
|
+
if (r.freq === "weekly" && r.byDay?.length) {
|
|
931
|
+
const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
|
|
932
|
+
if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
|
|
933
|
+
}
|
|
934
|
+
if (r.until) out.until = r.until;
|
|
935
|
+
else if (r.count) out.count = r.count;
|
|
936
|
+
return out;
|
|
937
|
+
}
|
|
938
|
+
function sameRecurrence(a, b, start) {
|
|
939
|
+
if (!a || !b) return a === b;
|
|
940
|
+
return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
|
|
941
|
+
}
|
|
942
|
+
function toRRule(r, allDay) {
|
|
943
|
+
if (r.rule) return r.rule;
|
|
944
|
+
const parts = [`FREQ=${r.freq.toUpperCase()}`];
|
|
945
|
+
if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
|
|
946
|
+
if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
|
|
947
|
+
if (r.until) {
|
|
948
|
+
const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
|
|
949
|
+
parts.push(`UNTIL=${until}`);
|
|
950
|
+
} else if (r.count) parts.push(`COUNT=${r.count}`);
|
|
951
|
+
return parts.join(";");
|
|
952
|
+
}
|
|
953
|
+
var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
|
|
954
|
+
function fromRRule(text2, start) {
|
|
955
|
+
const rule = text2.replace(/^RRULE:/i, "").trim();
|
|
956
|
+
const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
|
|
957
|
+
const i = p.indexOf("=");
|
|
958
|
+
return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
|
|
959
|
+
}));
|
|
960
|
+
const freq = (kv.get("FREQ") ?? "").toLowerCase();
|
|
961
|
+
const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
|
|
962
|
+
if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
|
|
963
|
+
const out = { freq };
|
|
964
|
+
const interval = Number(kv.get("INTERVAL") ?? 1);
|
|
965
|
+
if (!Number.isInteger(interval) || interval < 1) return custom();
|
|
966
|
+
if (interval > 1) out.interval = interval;
|
|
967
|
+
if (kv.has("BYDAY")) {
|
|
968
|
+
const days = kv.get("BYDAY").split(",");
|
|
969
|
+
if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
|
|
970
|
+
out.byDay = WEEKDAYS.filter((d) => days.includes(d));
|
|
971
|
+
}
|
|
972
|
+
const until = kv.get("UNTIL");
|
|
973
|
+
if (until) {
|
|
974
|
+
const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
|
|
975
|
+
if (!m) return custom();
|
|
976
|
+
if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
|
|
977
|
+
else {
|
|
978
|
+
const at3 = m[7] ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])) : new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
|
|
979
|
+
const s = start && start.length > 10 ? asDate(start) : void 0;
|
|
980
|
+
const minutes = (d) => d.getHours() * 60 + d.getMinutes();
|
|
981
|
+
if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
|
|
982
|
+
out.until = format2(at3, "yyyy-MM-dd");
|
|
983
|
+
}
|
|
984
|
+
} else if (kv.has("COUNT")) {
|
|
985
|
+
const count = Number(kv.get("COUNT"));
|
|
986
|
+
if (!Number.isInteger(count) || count < 1) return custom();
|
|
987
|
+
out.count = count;
|
|
988
|
+
}
|
|
989
|
+
return out;
|
|
990
|
+
}
|
|
991
|
+
function presets(start) {
|
|
992
|
+
const d = asDate(start);
|
|
993
|
+
return [
|
|
994
|
+
{ id: "none", label: "Does not repeat", recurrence: null },
|
|
995
|
+
{ id: "daily", label: "Every day", recurrence: { freq: "daily" } },
|
|
996
|
+
{ id: "weekly", label: `Every week on ${format2(d, "EEEE")}`, recurrence: { freq: "weekly" } },
|
|
997
|
+
{ id: "monthly", label: `Every month on day ${d.getDate()}`, recurrence: { freq: "monthly" } },
|
|
998
|
+
{ id: "yearly", label: `Every year on ${format2(d, "d MMMM")}`, recurrence: { freq: "yearly" } },
|
|
999
|
+
{ id: "weekdays", label: "Every weekday (Mon\u2013Fri)", recurrence: { freq: "weekly", byDay: WORKWEEK } }
|
|
1000
|
+
];
|
|
1001
|
+
}
|
|
1002
|
+
function presetOf(r, start) {
|
|
1003
|
+
if (!r) return "none";
|
|
1004
|
+
if (r.rule) return "custom";
|
|
1005
|
+
const { until: _u, count: _c, ...base } = normalize(r, start);
|
|
1006
|
+
const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
|
|
1007
|
+
return hit?.id ?? "custom";
|
|
1008
|
+
}
|
|
1009
|
+
var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
|
|
1010
|
+
function describe(r, start) {
|
|
1011
|
+
if (!r) return "Does not repeat";
|
|
1012
|
+
if (r.rule) return `Custom rule (${r.rule})`;
|
|
1013
|
+
const n = normalize(r, start);
|
|
1014
|
+
const d = asDate(start);
|
|
1015
|
+
const every = n.interval ? `Every ${n.interval} ${UNIT[n.freq]}s` : `Every ${UNIT[n.freq]}`;
|
|
1016
|
+
const on = n.freq === "weekly" ? !n.interval && n.byDay?.join() === WORKWEEK.join() ? "" : ` on ${(n.byDay ?? [weekdayOf(start)]).map(shortDay).join(", ")}` : n.freq === "monthly" ? ` on day ${d.getDate()}` : n.freq === "yearly" ? ` on ${format2(d, "d MMMM")}` : "";
|
|
1017
|
+
const head = n.freq === "weekly" && !n.interval && n.byDay?.join() === WORKWEEK.join() ? "Every weekday (Mon\u2013Fri)" : `${every}${on}`;
|
|
1018
|
+
const end = n.until ? ` \xB7 until ${format2(parseISO3(n.until), "d MMM yyyy")}` : n.count ? ` \xB7 ${n.count} time${n.count > 1 ? "s" : ""}` : "";
|
|
1019
|
+
return head + end;
|
|
1020
|
+
}
|
|
1021
|
+
var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
|
|
1022
|
+
function parseEvery(text2) {
|
|
1023
|
+
const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
|
|
1024
|
+
const n = Number(m?.[1] ?? 1);
|
|
1025
|
+
if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
|
|
1026
|
+
const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
|
|
1027
|
+
return { freq, interval: n > 1 ? n : void 0 };
|
|
1028
|
+
}
|
|
1029
|
+
var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
|
|
1030
|
+
function parseDays(text2) {
|
|
1031
|
+
const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
|
|
1032
|
+
const bad = words.filter((w) => !WEEKDAYS.includes(w));
|
|
1033
|
+
if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
|
|
1034
|
+
return WEEKDAYS.filter((d) => words.includes(d));
|
|
1035
|
+
}
|
|
1036
|
+
var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
|
|
1037
|
+
function parseEnds(text2) {
|
|
1038
|
+
const t = text2.trim().toLowerCase();
|
|
1039
|
+
if (!t || t === "never") return {};
|
|
1040
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(t) && isValid(parseISO3(t)) && format2(parseISO3(t), "yyyy-MM-dd") === t) return { until: t };
|
|
1041
|
+
const m = /^(\d+)(\s*times?)?$/.exec(t);
|
|
1042
|
+
if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
|
|
1043
|
+
throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ../../packages/core/src/mock/MockProvider.ts
|
|
1047
|
+
var MAX_INSTANCES = 60;
|
|
1048
|
+
function expand(first, r, series) {
|
|
1049
|
+
const start = parseISO4(first.start);
|
|
1050
|
+
const length = parseISO4(first.end).getTime() - start.getTime();
|
|
1051
|
+
const every = r.interval ?? 1;
|
|
1052
|
+
const days = r.byDay?.length ? r.byDay : [weekdayOf(first.start)];
|
|
1053
|
+
const until = r.until ? parseISO4(r.until) : void 0;
|
|
1054
|
+
const hits = (d) => {
|
|
1055
|
+
const n = differenceInCalendarDays(d, start);
|
|
1056
|
+
if (r.freq === "daily") return n % every === 0;
|
|
1057
|
+
if (r.freq === "weekly")
|
|
1058
|
+
return days.includes(weekdayOf(format3(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
|
|
1059
|
+
const months = differenceInCalendarMonths(d, start);
|
|
1060
|
+
if (d.getDate() !== start.getDate()) return false;
|
|
1061
|
+
return r.freq === "monthly" ? months % every === 0 : months % (12 * every) === 0;
|
|
1062
|
+
};
|
|
1063
|
+
const out = [];
|
|
1064
|
+
for (let n = 0; n < 3 * 366 && out.length < (r.count ?? MAX_INSTANCES) && out.length < MAX_INSTANCES; n++) {
|
|
1065
|
+
const d = addDays(start, n);
|
|
1066
|
+
if (until && differenceInCalendarDays(d, until) > 0) break;
|
|
1067
|
+
if (!hits(d)) continue;
|
|
1068
|
+
const s = first.allDay ? format3(d, "yyyy-MM-dd") : d.toISOString();
|
|
1069
|
+
const e = first.allDay ? format3(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
|
|
1070
|
+
out.push({ ...first, id: `${series}@${s}`, start: s, end: e, recurringEventId: series, recurrence: void 0 });
|
|
1071
|
+
}
|
|
1072
|
+
return out;
|
|
1073
|
+
}
|
|
837
1074
|
var MockProvider = class {
|
|
838
1075
|
constructor(accountId, email, calendars) {
|
|
839
1076
|
this.accountId = accountId;
|
|
@@ -844,11 +1081,13 @@ var MockProvider = class {
|
|
|
844
1081
|
email;
|
|
845
1082
|
calendars;
|
|
846
1083
|
events = [];
|
|
1084
|
+
/** Series id -> its rule. */
|
|
1085
|
+
rules = /* @__PURE__ */ new Map();
|
|
847
1086
|
async listCalendars() {
|
|
848
1087
|
return structuredClone(this.calendars);
|
|
849
1088
|
}
|
|
850
1089
|
async listEvents(calendarId, range) {
|
|
851
|
-
const t = (iso2) =>
|
|
1090
|
+
const t = (iso2) => parseISO4(iso2).getTime();
|
|
852
1091
|
return structuredClone(
|
|
853
1092
|
this.events.filter((e) => e.calendarId === calendarId && t(e.end) > t(range.start) && t(e.start) < t(range.end))
|
|
854
1093
|
);
|
|
@@ -867,12 +1106,45 @@ var MockProvider = class {
|
|
|
867
1106
|
organizer: { email: this.email },
|
|
868
1107
|
attendees: (input.attendees ?? []).map((email) => ({ email, status: "needsAction" }))
|
|
869
1108
|
};
|
|
1109
|
+
if (input.recurrence) return this.startSeries(ev, input.recurrence, ev.id);
|
|
870
1110
|
this.events.push(ev);
|
|
871
1111
|
return structuredClone(ev);
|
|
872
1112
|
}
|
|
873
|
-
|
|
1113
|
+
/** Add the series `first` starts under `r` (or `first` alone for null); returns its first event. */
|
|
1114
|
+
startSeries(first, r, series) {
|
|
1115
|
+
const evs = r ? expand(first, r, series) : [{ ...first, recurringEventId: void 0, recurrence: void 0 }];
|
|
1116
|
+
if (r) this.rules.set(series, r);
|
|
1117
|
+
this.events.push(...evs);
|
|
1118
|
+
return structuredClone(evs[0]);
|
|
1119
|
+
}
|
|
1120
|
+
async getRecurrence(event) {
|
|
1121
|
+
return event.recurringEventId && this.rules.get(event.recurringEventId) || null;
|
|
1122
|
+
}
|
|
1123
|
+
async updateEvent(event, scope = "one") {
|
|
874
1124
|
const i = this.events.findIndex((e) => e.id === event.id);
|
|
875
1125
|
if (i < 0) throw new Error("not found");
|
|
1126
|
+
const old = this.events[i];
|
|
1127
|
+
const series = old.recurringEventId;
|
|
1128
|
+
if (event.recurrence !== void 0 && (!series || scope !== "one")) {
|
|
1129
|
+
const own = (e) => e.id === old.id || !!series && e.recurringEventId === series && (scope === "all" || e.start >= old.start);
|
|
1130
|
+
const firstOld = series && scope === "all" ? this.events.filter(own).sort((a, b) => a.start.localeCompare(b.start))[0] : old;
|
|
1131
|
+
this.events = this.events.filter((e) => !own(e));
|
|
1132
|
+
const shift = Date.parse(event.start) - Date.parse(old.start);
|
|
1133
|
+
const anchor = event.recurrence && firstOld !== old && !event.allDay ? { start: new Date(Date.parse(firstOld.start) + shift).toISOString(), end: new Date(Date.parse(firstOld.start) + shift + Date.parse(event.end) - Date.parse(event.start)).toISOString() } : {};
|
|
1134
|
+
const id2 = scope === "all" && series ? series : randomUUID3();
|
|
1135
|
+
return this.startSeries({ ...event, ...anchor, accountId: this.accountId }, event.recurrence, id2);
|
|
1136
|
+
}
|
|
1137
|
+
if (series && scope !== "one") {
|
|
1138
|
+
const shift = Date.parse(event.start) - Date.parse(old.start);
|
|
1139
|
+
const length = Date.parse(event.end) - Date.parse(event.start);
|
|
1140
|
+
const at3 = (ms) => new Date(ms).toISOString();
|
|
1141
|
+
const { title: title2, location, description, attendees } = event;
|
|
1142
|
+
this.events = this.events.map((e) => {
|
|
1143
|
+
if (e.id === event.id || e.recurringEventId !== series || scope === "following" && e.start < old.start) return e;
|
|
1144
|
+
const moved = old.allDay || event.allDay ? {} : { start: at3(Date.parse(e.start) + shift), end: at3(Date.parse(e.start) + shift + length) };
|
|
1145
|
+
return { ...e, title: title2, location, description, attendees, ...moved };
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
876
1148
|
this.events[i] = { ...event, accountId: this.accountId };
|
|
877
1149
|
return structuredClone(this.events[i]);
|
|
878
1150
|
}
|
|
@@ -899,7 +1171,15 @@ var iso = (dayOffset, h, m = 0) => {
|
|
|
899
1171
|
};
|
|
900
1172
|
function createMockApi(onChanged) {
|
|
901
1173
|
const accounts = [
|
|
902
|
-
{
|
|
1174
|
+
{
|
|
1175
|
+
id: "work",
|
|
1176
|
+
kind: "caldav",
|
|
1177
|
+
label: "Work",
|
|
1178
|
+
email: "me@work.example",
|
|
1179
|
+
color: "#8be9fd",
|
|
1180
|
+
// e2e: a revoked app password; accounts.reauth with any password fixes it.
|
|
1181
|
+
...process.env.MYSTICALS_MOCK_AUTH_ERROR ? { error: "CalDAV login failed: the password was changed or revoked", authError: true } : {}
|
|
1182
|
+
},
|
|
903
1183
|
{ id: "personal", kind: "google", label: "Personal", email: "me@gmail.example", color: "#50fa7b" }
|
|
904
1184
|
];
|
|
905
1185
|
const providers = /* @__PURE__ */ new Map([
|
|
@@ -938,13 +1218,22 @@ function createMockApi(onChanged) {
|
|
|
938
1218
|
myStatus: "needsAction",
|
|
939
1219
|
attendees: [{ email: "pm@work.example", status: "accepted", organizer: true }, { email: "me@work.example", status: "needsAction", self: true }]
|
|
940
1220
|
});
|
|
1221
|
+
seed(work, "work-main", {
|
|
1222
|
+
title: "Quarterly roadmap sync with design",
|
|
1223
|
+
start: iso(0, 11, 30),
|
|
1224
|
+
end: iso(0, 12),
|
|
1225
|
+
location: "Zoom https://zoom.us/j/1234567890?pwd=abc"
|
|
1226
|
+
});
|
|
941
1227
|
seed(personal, "p-main", { title: "Gym", start: iso(0, 19), end: iso(0, 20) });
|
|
942
1228
|
for (let d = -2; d <= 4; d++) {
|
|
943
1229
|
seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
|
|
944
1230
|
}
|
|
1231
|
+
personal.rules.set("run-series", { freq: "daily", count: 7 });
|
|
945
1232
|
seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
|
|
946
|
-
const day = (offset) =>
|
|
1233
|
+
const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
|
|
947
1234
|
seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
|
|
1235
|
+
for (const title2 of ["Conference", "Team offsite", "Release freeze", "Birthday"])
|
|
1236
|
+
seed(personal, "p-main", { title: title2, start: day(3), end: day(4), allDay: true });
|
|
948
1237
|
const prov = (id2) => {
|
|
949
1238
|
const p = providers.get(id2);
|
|
950
1239
|
if (!p) throw new Error(`unknown account ${id2}`);
|
|
@@ -971,6 +1260,14 @@ function createMockApi(onChanged) {
|
|
|
971
1260
|
onChanged(id2);
|
|
972
1261
|
return structuredClone(a);
|
|
973
1262
|
},
|
|
1263
|
+
reauth: async (id2) => {
|
|
1264
|
+
const a = accounts.find((x) => x.id === id2);
|
|
1265
|
+
if (!a) throw new Error("unknown account");
|
|
1266
|
+
delete a.error;
|
|
1267
|
+
delete a.authError;
|
|
1268
|
+
onChanged(id2);
|
|
1269
|
+
return structuredClone(a);
|
|
1270
|
+
},
|
|
974
1271
|
remove: async (id2) => {
|
|
975
1272
|
const i = accounts.findIndex((a) => a.id === id2);
|
|
976
1273
|
if (i >= 0) accounts.splice(i, 1);
|
|
@@ -999,8 +1296,8 @@ function createMockApi(onChanged) {
|
|
|
999
1296
|
onChanged(input.accountId);
|
|
1000
1297
|
return ev;
|
|
1001
1298
|
},
|
|
1002
|
-
update: async (ev) => {
|
|
1003
|
-
const r = await own(ev.accountId, ev.calendarId).updateEvent(ev);
|
|
1299
|
+
update: async (ev, scope) => {
|
|
1300
|
+
const r = await own(ev.accountId, ev.calendarId).updateEvent(ev, scope);
|
|
1004
1301
|
onChanged(ev.accountId);
|
|
1005
1302
|
return r;
|
|
1006
1303
|
},
|
|
@@ -1008,6 +1305,7 @@ function createMockApi(onChanged) {
|
|
|
1008
1305
|
await own(ev.accountId, ev.calendarId).deleteEvent(ev, scope);
|
|
1009
1306
|
onChanged(ev.accountId);
|
|
1010
1307
|
},
|
|
1308
|
+
recurrence: async (ev) => own(ev.accountId, ev.calendarId).getRecurrence(ev),
|
|
1011
1309
|
respond: async (ev, status) => {
|
|
1012
1310
|
const r = await own(ev.accountId, ev.calendarId).respond(ev, status);
|
|
1013
1311
|
onChanged(ev.accountId);
|
|
@@ -1034,7 +1332,7 @@ import {
|
|
|
1034
1332
|
|
|
1035
1333
|
// ../../packages/core/src/providers/caldav/ics.ts
|
|
1036
1334
|
import ICAL from "ical.js";
|
|
1037
|
-
var
|
|
1335
|
+
var MAX_INSTANCES2 = 1e5;
|
|
1038
1336
|
var PARTSTAT_IN = {
|
|
1039
1337
|
ACCEPTED: "accepted",
|
|
1040
1338
|
DECLINED: "declined",
|
|
@@ -1132,7 +1430,7 @@ function parseEvents(ics, href, etag, ctx, range) {
|
|
|
1132
1430
|
-Infinity,
|
|
1133
1431
|
...vevents.filter((v) => v !== master).map((v) => v.getFirstPropertyValue("recurrence-id").toJSDate().getTime())
|
|
1134
1432
|
);
|
|
1135
|
-
for (let i = 0, next = it.next(); next && i <
|
|
1433
|
+
for (let i = 0, next = it.next(); next && i < MAX_INSTANCES2; i++, next = it.next()) {
|
|
1136
1434
|
const ms = next.toJSDate().getTime();
|
|
1137
1435
|
if (ms >= rangeEnd && ms > lastOverride) break;
|
|
1138
1436
|
const d = event.getOccurrenceDetails(next);
|
|
@@ -1155,9 +1453,40 @@ function buildIcs(uid, input, email) {
|
|
|
1155
1453
|
setText(v, "location", input.location);
|
|
1156
1454
|
setText(v, "description", input.description);
|
|
1157
1455
|
setAttendees(v, input.attendees ?? [], email);
|
|
1456
|
+
if (input.recurrence) setRule(v, input.recurrence, input.allDay);
|
|
1158
1457
|
root.addSubcomponent(v);
|
|
1159
1458
|
return root.toString();
|
|
1160
1459
|
}
|
|
1460
|
+
function setRule(v, r, allDay) {
|
|
1461
|
+
v.removeAllProperties("rrule");
|
|
1462
|
+
v.addPropertyWithValue("rrule", ICAL.Recur.fromString(toRRule(r, allDay)));
|
|
1463
|
+
}
|
|
1464
|
+
function recurrenceOf(raw) {
|
|
1465
|
+
const master = findMaster(parse(raw.ics));
|
|
1466
|
+
const rule = master?.getFirstPropertyValue("rrule");
|
|
1467
|
+
return master && rule ? fromRRule(rule.toString(), toIso(dtstart(master))) : null;
|
|
1468
|
+
}
|
|
1469
|
+
function applyRule(root, master, event) {
|
|
1470
|
+
if (event.recurrence === void 0) return;
|
|
1471
|
+
const overrides = root.getAllSubcomponents("vevent").filter((v) => v !== master);
|
|
1472
|
+
if (event.recurrence === null) {
|
|
1473
|
+
for (const n of ["rrule", "rdate", "exdate"]) master.removeAllProperties(n);
|
|
1474
|
+
for (const o of overrides) root.removeSubcomponent(o);
|
|
1475
|
+
master.addProperty(replaceTime(master, "dtstart", fromIso(event.start, event.allDay)));
|
|
1476
|
+
master.addProperty(replaceTime(master, "dtend", fromIso(event.end, event.allDay)));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
setRule(master, event.recurrence, event.allDay);
|
|
1480
|
+
const ms = (t) => t.toJSDate().getTime();
|
|
1481
|
+
const rid = (o) => o.getFirstPropertyValue("recurrence-id");
|
|
1482
|
+
const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t) => t instanceof ICAL.Time);
|
|
1483
|
+
const last = Math.max(-Infinity, ...overrides.map((o) => ms(rid(o))), ...exdates.map(ms));
|
|
1484
|
+
const slots2 = /* @__PURE__ */ new Set();
|
|
1485
|
+
const it = master.getFirstPropertyValue("rrule").iterator(dtstart(master));
|
|
1486
|
+
for (let t = it.next(), n = 0; t && ms(t) <= last && n < MAX_INSTANCES2; t = it.next(), n++) slots2.add(ms(t));
|
|
1487
|
+
for (const o of overrides) if (!slots2.has(ms(rid(o)))) root.removeSubcomponent(o);
|
|
1488
|
+
mapDates(master, "exdate", (t) => slots2.has(ms(t)) ? t : null);
|
|
1489
|
+
}
|
|
1161
1490
|
function setAttendees(v, emails, email) {
|
|
1162
1491
|
const wanted = [...new Set(emails.map(cleanEmail).filter(Boolean))];
|
|
1163
1492
|
const existing = new Map(v.getAllProperties("attendee").map((p) => [cleanEmail(p.getFirstValue()), p]));
|
|
@@ -1217,9 +1546,14 @@ function applyUpdate(event, email) {
|
|
|
1217
1546
|
const raw = event.raw;
|
|
1218
1547
|
const root = parse(raw.ics);
|
|
1219
1548
|
const v = targetVevent(root, raw);
|
|
1220
|
-
|
|
1549
|
+
writeEdit(v, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
|
|
1550
|
+
if (event.recurrence && !raw.recurrenceId && !v.hasProperty("rrule")) setRule(v, event.recurrence, event.allDay);
|
|
1551
|
+
return root.toString();
|
|
1552
|
+
}
|
|
1553
|
+
function writeEdit(v, event, email, start, end) {
|
|
1554
|
+
v.addProperty(replaceTime(v, "dtstart", start));
|
|
1221
1555
|
v.removeAllProperties("duration");
|
|
1222
|
-
|
|
1556
|
+
v.addProperty(replaceTime(v, "dtend", end));
|
|
1223
1557
|
setText(v, "summary", event.title);
|
|
1224
1558
|
setText(v, "location", event.location);
|
|
1225
1559
|
setText(v, "description", event.description);
|
|
@@ -1228,10 +1562,156 @@ function applyUpdate(event, email) {
|
|
|
1228
1562
|
event.attendees.map((a) => a.email),
|
|
1229
1563
|
email
|
|
1230
1564
|
);
|
|
1565
|
+
bump(v);
|
|
1566
|
+
}
|
|
1567
|
+
function bump(v) {
|
|
1231
1568
|
v.updatePropertyWithValue("sequence", Number(v.getFirstPropertyValue("sequence") ?? 0) + 1);
|
|
1232
1569
|
touch(v);
|
|
1570
|
+
}
|
|
1571
|
+
function timeProp(name, t) {
|
|
1572
|
+
const p = new ICAL.Property(name);
|
|
1573
|
+
p.setValue(t);
|
|
1574
|
+
const tzid = t.zone?.tzid;
|
|
1575
|
+
if (!t.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
|
|
1576
|
+
return p;
|
|
1577
|
+
}
|
|
1578
|
+
function replaceTime(v, name, t) {
|
|
1579
|
+
v.removeAllProperties(name);
|
|
1580
|
+
return timeProp(name, t);
|
|
1581
|
+
}
|
|
1582
|
+
var dtstart = (v) => v.getFirstPropertyValue("dtstart");
|
|
1583
|
+
var dateOf = (t) => ICAL.Time.fromData({ year: t.year, month: t.month, day: t.day, isDate: true });
|
|
1584
|
+
var daysBetween = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
|
|
1585
|
+
function addDays3(t, days) {
|
|
1586
|
+
const x = t.clone();
|
|
1587
|
+
x.adjust(days, 0, 0, 0);
|
|
1588
|
+
return x;
|
|
1589
|
+
}
|
|
1590
|
+
function spanFrom(start, event) {
|
|
1591
|
+
if (event.allDay) return [start, addDays3(start, daysBetween(fromIso(event.start, true), fromIso(event.end, true)))];
|
|
1592
|
+
const end = start.clone();
|
|
1593
|
+
end.adjust(0, 0, 0, (Date.parse(event.end) - Date.parse(event.start)) / 1e3);
|
|
1594
|
+
return [start, end];
|
|
1595
|
+
}
|
|
1596
|
+
function slotShift(master, before, event) {
|
|
1597
|
+
const anchor = dtstart(master);
|
|
1598
|
+
const zone = anchor.isDate ? void 0 : anchor.zone;
|
|
1599
|
+
const local = (t) => zone && !t.isDate ? t.convertToZone(zone) : t.clone();
|
|
1600
|
+
const after = fromIso(event.start, event.allDay);
|
|
1601
|
+
if (event.allDay) {
|
|
1602
|
+
const days2 = daysBetween(local(before), after);
|
|
1603
|
+
return (t) => addDays3(dateOf(local(t)), days2);
|
|
1604
|
+
}
|
|
1605
|
+
if (!anchor.isDate) {
|
|
1606
|
+
const seconds = (zone ? after.convertToZone(zone) : after).subtractDate(local(before)).toSeconds();
|
|
1607
|
+
return (t) => {
|
|
1608
|
+
const x = local(t);
|
|
1609
|
+
x.adjust(0, 0, 0, seconds);
|
|
1610
|
+
return x;
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
const days = daysBetween(before, after);
|
|
1614
|
+
return (t) => {
|
|
1615
|
+
const d = addDays3(dateOf(t), days);
|
|
1616
|
+
return ICAL.Time.fromData({ year: d.year, month: d.month, day: d.day, hour: after.hour, minute: after.minute, second: after.second }, ICAL.Timezone.utcTimezone);
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function mapDates(v, name, map) {
|
|
1620
|
+
const props = v.getAllProperties(name);
|
|
1621
|
+
if (!props.length) return;
|
|
1622
|
+
const out = [];
|
|
1623
|
+
for (const p of props) {
|
|
1624
|
+
for (const value of p.getValues()) {
|
|
1625
|
+
if (!(value instanceof ICAL.Time)) {
|
|
1626
|
+
const keep = new ICAL.Property(name);
|
|
1627
|
+
keep.setValue(value);
|
|
1628
|
+
out.push(keep);
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
const t = map(value);
|
|
1632
|
+
if (t) out.push(timeProp(name, t));
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
v.removeAllProperties(name);
|
|
1636
|
+
for (const p of out) v.addProperty(p);
|
|
1637
|
+
}
|
|
1638
|
+
var attendeeKey = (v) => v.getAllProperties("attendee").map((p) => cleanEmail(p.getFirstValue())).sort().join(",");
|
|
1639
|
+
function rewriteSeries(root, master, edited, event, email, shift, anchor) {
|
|
1640
|
+
const shared = ["summary", "location", "description"].map((n) => [n, master.getFirstPropertyValue(n)]);
|
|
1641
|
+
const people = attendeeKey(master);
|
|
1642
|
+
const retyped = dtstart(master).isDate !== event.allDay;
|
|
1643
|
+
const [start, end] = spanFrom(shift(anchor), event);
|
|
1644
|
+
writeEdit(master, event, email, start, end);
|
|
1645
|
+
mapDates(master, "exdate", shift);
|
|
1646
|
+
mapDates(master, "rdate", shift);
|
|
1647
|
+
for (const o of root.getAllSubcomponents("vevent")) {
|
|
1648
|
+
const rid = o.getFirstPropertyValue("recurrence-id");
|
|
1649
|
+
if (!rid) continue;
|
|
1650
|
+
o.addProperty(replaceTime(o, "recurrence-id", shift(rid)));
|
|
1651
|
+
if (o === edited) {
|
|
1652
|
+
writeEdit(o, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
const [s, e] = retyped ? spanFrom(shift(dtstart(o)), event) : [shift(dtstart(o)), shift(new ICAL.Event(o).endDate)];
|
|
1656
|
+
o.addProperty(replaceTime(o, "dtstart", s));
|
|
1657
|
+
o.removeAllProperties("duration");
|
|
1658
|
+
o.addProperty(replaceTime(o, "dtend", e));
|
|
1659
|
+
for (const [name, old] of shared) {
|
|
1660
|
+
if (String(o.getFirstPropertyValue(name) ?? "") === String(old ?? "")) setText(o, name, event[name === "summary" ? "title" : name]);
|
|
1661
|
+
}
|
|
1662
|
+
if (attendeeKey(o) === people) setAttendees(o, event.attendees.map((a) => a.email), email);
|
|
1663
|
+
bump(o);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
var instanceStart = (master, override, rid) => override ? dtstart(override) : ridTime(master, rid).time;
|
|
1667
|
+
function applyUpdateSeries(event, email) {
|
|
1668
|
+
const raw = event.raw;
|
|
1669
|
+
const root = parse(raw.ics);
|
|
1670
|
+
const master = findMaster(root);
|
|
1671
|
+
if (!raw.recurrenceId || !master) return applyUpdate(event, email);
|
|
1672
|
+
const edited = findOverride(root, raw.recurrenceId);
|
|
1673
|
+
const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
|
|
1674
|
+
rewriteSeries(root, master, edited, event, email, shift, dtstart(master));
|
|
1675
|
+
applyRule(root, master, event);
|
|
1233
1676
|
return root.toString();
|
|
1234
1677
|
}
|
|
1678
|
+
function applySplitFollowing(event, email, uid) {
|
|
1679
|
+
const raw = event.raw;
|
|
1680
|
+
if (!raw.recurrenceId) throw new Error("Not a recurring instance");
|
|
1681
|
+
const rest = applyDeleteFollowing(raw);
|
|
1682
|
+
if (!rest) return null;
|
|
1683
|
+
const root = parse(raw.ics);
|
|
1684
|
+
const master = masterOf(root);
|
|
1685
|
+
const slot = ridTime(master, raw.recurrenceId).time;
|
|
1686
|
+
const cut = slot.toJSDate().getTime();
|
|
1687
|
+
const edited = findOverride(root, raw.recurrenceId);
|
|
1688
|
+
const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
|
|
1689
|
+
for (const v of root.getAllSubcomponents("vevent")) {
|
|
1690
|
+
const t = v.getFirstPropertyValue("recurrence-id");
|
|
1691
|
+
if (t && t.toJSDate().getTime() < cut) root.removeSubcomponent(v);
|
|
1692
|
+
}
|
|
1693
|
+
const later = (t) => t.toJSDate().getTime() >= cut ? t : null;
|
|
1694
|
+
mapDates(master, "exdate", later);
|
|
1695
|
+
mapDates(master, "rdate", later);
|
|
1696
|
+
const first = dtstart(master);
|
|
1697
|
+
for (const p of event.recurrence === void 0 ? master.getAllProperties("rrule") : []) {
|
|
1698
|
+
const r = p.getFirstValue().clone();
|
|
1699
|
+
if (!r.count) continue;
|
|
1700
|
+
const it = r.iterator(first);
|
|
1701
|
+
let passed = 0;
|
|
1702
|
+
for (let t = it.next(); t && t.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t = it.next()) passed++;
|
|
1703
|
+
r.count = Math.max(1, r.count - passed);
|
|
1704
|
+
p.setValue(r);
|
|
1705
|
+
}
|
|
1706
|
+
for (const v of root.getAllSubcomponents("vevent")) {
|
|
1707
|
+
v.updatePropertyWithValue("uid", uid);
|
|
1708
|
+
v.updatePropertyWithValue("sequence", -1);
|
|
1709
|
+
v.updatePropertyWithValue("created", ICAL.Time.now());
|
|
1710
|
+
}
|
|
1711
|
+
rewriteSeries(root, master, edited, event, email, shift, slot);
|
|
1712
|
+
applyRule(root, master, event);
|
|
1713
|
+
return { rest, next: root.toString() };
|
|
1714
|
+
}
|
|
1235
1715
|
function applyDeleteInstance(raw) {
|
|
1236
1716
|
if (!raw.recurrenceId) throw new Error("Not a recurring instance");
|
|
1237
1717
|
const root = parse(raw.ics);
|
|
@@ -1289,18 +1769,6 @@ function applyRespond(raw, email, status) {
|
|
|
1289
1769
|
return root.toString();
|
|
1290
1770
|
}
|
|
1291
1771
|
|
|
1292
|
-
// ../../packages/core/src/providers/http.ts
|
|
1293
|
-
var http = { timeoutMs: 3e4 };
|
|
1294
|
-
var timedFetch = async (input, init) => {
|
|
1295
|
-
const timeout = AbortSignal.timeout(http.timeoutMs);
|
|
1296
|
-
try {
|
|
1297
|
-
return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
|
|
1298
|
-
} catch (e) {
|
|
1299
|
-
if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
|
|
1300
|
-
throw e;
|
|
1301
|
-
}
|
|
1302
|
-
};
|
|
1303
|
-
|
|
1304
1772
|
// ../../packages/core/src/providers/caldav/index.ts
|
|
1305
1773
|
var FALLBACK_COLOR = "#8e8e93";
|
|
1306
1774
|
var DAY = 864e5;
|
|
@@ -1325,7 +1793,7 @@ async function connect2(serverUrl, username, password) {
|
|
|
1325
1793
|
});
|
|
1326
1794
|
return { account, headers };
|
|
1327
1795
|
} catch (e) {
|
|
1328
|
-
if (unauthorized) throw new
|
|
1796
|
+
if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
|
|
1329
1797
|
throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
|
|
1330
1798
|
}
|
|
1331
1799
|
}
|
|
@@ -1351,10 +1819,15 @@ async function listVeventCalendars(conn) {
|
|
|
1351
1819
|
headers: conn.headers,
|
|
1352
1820
|
props: CALENDAR_PROPS,
|
|
1353
1821
|
projectedProps: { currentUserPrivilegeSet: true },
|
|
1354
|
-
fetch:
|
|
1822
|
+
fetch: davFetch
|
|
1355
1823
|
});
|
|
1356
1824
|
return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
|
|
1357
1825
|
}
|
|
1826
|
+
var davFetch = async (input, init) => {
|
|
1827
|
+
const res = await timedFetch(input, init);
|
|
1828
|
+
if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
|
|
1829
|
+
return res;
|
|
1830
|
+
};
|
|
1358
1831
|
function check(res, what) {
|
|
1359
1832
|
if (res.ok) return;
|
|
1360
1833
|
if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
|
|
@@ -1374,18 +1847,19 @@ var createCaldavProvider = (ctx) => {
|
|
|
1374
1847
|
const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
|
|
1375
1848
|
async function reload(calendarId, href, id2, around) {
|
|
1376
1849
|
const { headers } = await getConn();
|
|
1377
|
-
const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch:
|
|
1850
|
+
const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
|
|
1378
1851
|
if (!obj?.data) throw new Error("Saved event could not be read back from the server");
|
|
1379
1852
|
const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
|
|
1380
1853
|
const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
|
|
1381
|
-
const
|
|
1854
|
+
const at3 = Date.parse(around.start);
|
|
1855
|
+
const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
|
|
1382
1856
|
if (!ev) throw new Error("Saved event not found on the server");
|
|
1383
1857
|
return ev;
|
|
1384
1858
|
}
|
|
1385
1859
|
const put = async (event, ics, what) => {
|
|
1386
1860
|
const { headers } = await getConn();
|
|
1387
1861
|
const raw = event.raw;
|
|
1388
|
-
check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch:
|
|
1862
|
+
check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
|
|
1389
1863
|
};
|
|
1390
1864
|
const provider = {
|
|
1391
1865
|
async listCalendars() {
|
|
@@ -1394,7 +1868,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1394
1868
|
},
|
|
1395
1869
|
async listEvents(calendarId, range) {
|
|
1396
1870
|
const { headers } = await getConn();
|
|
1397
|
-
const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch:
|
|
1871
|
+
const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
|
|
1398
1872
|
return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
|
|
1399
1873
|
},
|
|
1400
1874
|
async createEvent(calendarId, input) {
|
|
@@ -1402,20 +1876,38 @@ var createCaldavProvider = (ctx) => {
|
|
|
1402
1876
|
const uid = randomUUID4();
|
|
1403
1877
|
const filename = `${uid}.ics`;
|
|
1404
1878
|
const ics = buildIcs(uid, input, ctx.email);
|
|
1405
|
-
check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch:
|
|
1879
|
+
check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
|
|
1406
1880
|
const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
|
|
1407
1881
|
return reload(calendarId, href, void 0, input);
|
|
1408
1882
|
},
|
|
1409
|
-
async updateEvent(event) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1883
|
+
async updateEvent(event, scope = "one") {
|
|
1884
|
+
const raw = event.raw;
|
|
1885
|
+
if (!raw.recurrenceId || scope === "one") {
|
|
1886
|
+
await put(event, applyUpdate(event, ctx.email), "Update event");
|
|
1887
|
+
return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
|
|
1888
|
+
}
|
|
1889
|
+
const uid = randomUUID4();
|
|
1890
|
+
const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
|
|
1891
|
+
if (!split) {
|
|
1892
|
+
await put(event, applyUpdateSeries(event, ctx.email), "Update event");
|
|
1893
|
+
return reload(event.calendarId, raw.href, void 0, event);
|
|
1894
|
+
}
|
|
1895
|
+
await put(event, split.rest, "Update event");
|
|
1896
|
+
const { headers } = await getConn();
|
|
1897
|
+
const filename = `${uid}.ics`;
|
|
1898
|
+
check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
|
|
1899
|
+
const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
|
|
1900
|
+
return reload(event.calendarId, href, void 0, event);
|
|
1412
1901
|
},
|
|
1413
1902
|
async deleteEvent(event, scope = "one") {
|
|
1414
1903
|
const raw = event.raw;
|
|
1415
1904
|
const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
|
|
1416
1905
|
if (rest) return put(event, rest, "Delete event");
|
|
1417
1906
|
const { headers } = await getConn();
|
|
1418
|
-
check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch:
|
|
1907
|
+
check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
|
|
1908
|
+
},
|
|
1909
|
+
async getRecurrence(event) {
|
|
1910
|
+
return recurrenceOf(event.raw);
|
|
1419
1911
|
},
|
|
1420
1912
|
async respond(event, status) {
|
|
1421
1913
|
await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
|
|
@@ -1427,7 +1919,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1427
1919
|
async function verifyCaldav(input) {
|
|
1428
1920
|
const c = await connect2(input.serverUrl.trim(), input.username, input.password);
|
|
1429
1921
|
await listVeventCalendars(c);
|
|
1430
|
-
const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch:
|
|
1922
|
+
const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
|
|
1431
1923
|
const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
|
|
1432
1924
|
const login = cleanEmail(input.username);
|
|
1433
1925
|
const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
|
|
@@ -1480,7 +1972,10 @@ function emailFromIdToken(idToken) {
|
|
|
1480
1972
|
}
|
|
1481
1973
|
function parseTokenResponse(json, now = Date.now()) {
|
|
1482
1974
|
const j = json ?? {};
|
|
1483
|
-
if (typeof j.error === "string")
|
|
1975
|
+
if (typeof j.error === "string") {
|
|
1976
|
+
const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
|
|
1977
|
+
throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
|
|
1978
|
+
}
|
|
1484
1979
|
if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
|
|
1485
1980
|
const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
|
|
1486
1981
|
return {
|
|
@@ -1607,7 +2102,29 @@ function truncateRecurrence(recurrence, cutoff) {
|
|
|
1607
2102
|
return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
|
|
1608
2103
|
});
|
|
1609
2104
|
}
|
|
2105
|
+
var DAY2 = 864e5;
|
|
2106
|
+
var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY2).toISOString().slice(0, 10);
|
|
2107
|
+
var daysBetween2 = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY2);
|
|
2108
|
+
var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
|
|
2109
|
+
function shiftAnchor(anchor, before, start, allDay, timeZone) {
|
|
2110
|
+
if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween2(before.date ?? before.dateTime, start)) };
|
|
2111
|
+
if (anchor.dateTime && before.dateTime) {
|
|
2112
|
+
return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
|
|
2113
|
+
}
|
|
2114
|
+
const at3 = new Date(start);
|
|
2115
|
+
const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween2(before.date, localDate(at3)))}T00:00:00`);
|
|
2116
|
+
day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
|
|
2117
|
+
return { dateTime: day.toISOString(), timeZone };
|
|
2118
|
+
}
|
|
2119
|
+
function endFrom(from, event, timeZone) {
|
|
2120
|
+
if (event.allDay) return { date: plusDays(from.date, daysBetween2(event.start, event.end)) };
|
|
2121
|
+
return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
|
|
2122
|
+
}
|
|
1610
2123
|
var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
|
|
2124
|
+
var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
|
|
2125
|
+
var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2126
|
+
var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
|
|
2127
|
+
var nulled = (t) => ({ date: null, dateTime: null, timeZone: null, ...t });
|
|
1611
2128
|
function createGoogleProviderImpl(ctx) {
|
|
1612
2129
|
if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
|
|
1613
2130
|
let creds = ctx.credentials;
|
|
@@ -1630,6 +2147,7 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1630
2147
|
});
|
|
1631
2148
|
let res = await send(await token());
|
|
1632
2149
|
if (res.status === 401) res = await send(await refresh());
|
|
2150
|
+
if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
|
|
1633
2151
|
if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
|
|
1634
2152
|
if (!res.ok) {
|
|
1635
2153
|
const err = await res.json().catch(() => ({}));
|
|
@@ -1645,6 +2163,35 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1645
2163
|
return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
|
|
1646
2164
|
};
|
|
1647
2165
|
const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
|
|
2166
|
+
const fields = (event, prevAttendees, organizer) => {
|
|
2167
|
+
const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
|
|
2168
|
+
if (organizer) {
|
|
2169
|
+
const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
|
|
2170
|
+
body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
|
|
2171
|
+
}
|
|
2172
|
+
return body;
|
|
2173
|
+
};
|
|
2174
|
+
async function remainingRecurrence(calendarId, series, recurrence, slot) {
|
|
2175
|
+
const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
|
|
2176
|
+
if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
|
|
2177
|
+
const cut = Date.parse(slot.dateTime ?? slot.date);
|
|
2178
|
+
let passed = 0;
|
|
2179
|
+
let pageToken;
|
|
2180
|
+
do {
|
|
2181
|
+
const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
|
|
2182
|
+
showDeleted: "true",
|
|
2183
|
+
timeMax: new Date(cut).toISOString(),
|
|
2184
|
+
maxResults: "2500",
|
|
2185
|
+
...pageToken ? { pageToken } : {}
|
|
2186
|
+
});
|
|
2187
|
+
for (const g of page.items ?? []) {
|
|
2188
|
+
const t = g.originalStartTime ?? g.start;
|
|
2189
|
+
if (Date.parse(t.dateTime ?? t.date) < cut) passed++;
|
|
2190
|
+
}
|
|
2191
|
+
pageToken = page.nextPageToken;
|
|
2192
|
+
} while (pageToken);
|
|
2193
|
+
return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
|
|
2194
|
+
}
|
|
1648
2195
|
return {
|
|
1649
2196
|
async listCalendars() {
|
|
1650
2197
|
const out = [];
|
|
@@ -1685,36 +2232,67 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1685
2232
|
},
|
|
1686
2233
|
async createEvent(calendarId, input) {
|
|
1687
2234
|
const attendees = (input.attendees ?? []).map((email) => ({ email }));
|
|
2235
|
+
const rule = input.recurrence;
|
|
1688
2236
|
const g = await api(
|
|
1689
2237
|
"POST",
|
|
1690
2238
|
eventPath(calendarId),
|
|
1691
2239
|
{ sendUpdates: attendees.length ? "all" : "none" },
|
|
1692
2240
|
{
|
|
1693
2241
|
summary: input.title,
|
|
1694
|
-
start: toGTime(input.start, input.allDay),
|
|
1695
|
-
end: toGTime(input.end, input.allDay),
|
|
2242
|
+
start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
|
|
2243
|
+
end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
|
|
1696
2244
|
location: input.location,
|
|
1697
2245
|
description: input.description,
|
|
1698
|
-
attendees
|
|
2246
|
+
attendees,
|
|
2247
|
+
...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
|
|
1699
2248
|
}
|
|
1700
2249
|
);
|
|
1701
2250
|
return map(g, calendarId);
|
|
1702
2251
|
},
|
|
1703
|
-
async updateEvent(event) {
|
|
2252
|
+
async updateEvent(event, scope = "one") {
|
|
1704
2253
|
const organizer = isOrganizer(event);
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2254
|
+
const q = { sendUpdates: organizer ? "all" : "none" };
|
|
2255
|
+
const series = event.recurringEventId;
|
|
2256
|
+
const before = rawOf(event)?.start;
|
|
2257
|
+
if (!series || scope === "one" || !before) {
|
|
2258
|
+
const rule2 = !series ? event.recurrence : void 0;
|
|
2259
|
+
const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
|
|
2260
|
+
const body = {
|
|
2261
|
+
...fields(event, rawOf(event)?.attendees, organizer),
|
|
2262
|
+
start: time(event.start),
|
|
2263
|
+
end: time(event.end),
|
|
2264
|
+
...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
|
|
2265
|
+
};
|
|
2266
|
+
return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
|
|
1715
2267
|
}
|
|
1716
|
-
const
|
|
1717
|
-
|
|
2268
|
+
const master = await api("GET", eventPath(event.calendarId, series));
|
|
2269
|
+
const timeZone = master.start.timeZone ?? localZone();
|
|
2270
|
+
const rule = event.recurrence;
|
|
2271
|
+
const slot = rawOf(event)?.originalStartTime ?? before;
|
|
2272
|
+
const first = master.start.dateTime ?? master.start.date;
|
|
2273
|
+
const at3 = slot.dateTime ?? slot.date;
|
|
2274
|
+
if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
|
|
2275
|
+
const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
|
|
2276
|
+
const body = {
|
|
2277
|
+
...fields(event, master.attendees, organizer),
|
|
2278
|
+
start: nulled(start2),
|
|
2279
|
+
end: nulled(endFrom(start2, event, timeZone)),
|
|
2280
|
+
// A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
|
|
2281
|
+
...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
|
|
2282
|
+
};
|
|
2283
|
+
await api("PATCH", eventPath(event.calendarId, series), q, body);
|
|
2284
|
+
return event;
|
|
2285
|
+
}
|
|
2286
|
+
const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
|
|
2287
|
+
await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
|
|
2288
|
+
const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
|
|
2289
|
+
const created = await api("POST", eventPath(event.calendarId), q, {
|
|
2290
|
+
...fields(event, master.attendees, organizer),
|
|
2291
|
+
start,
|
|
2292
|
+
end: endFrom(start, event, timeZone),
|
|
2293
|
+
...recurrence ? { recurrence } : {}
|
|
2294
|
+
});
|
|
2295
|
+
return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
|
|
1718
2296
|
},
|
|
1719
2297
|
async deleteEvent(event, scope = "one") {
|
|
1720
2298
|
const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
|
|
@@ -1732,6 +2310,12 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1732
2310
|
}
|
|
1733
2311
|
await api("DELETE", eventPath(event.calendarId, series), q);
|
|
1734
2312
|
},
|
|
2313
|
+
async getRecurrence(event) {
|
|
2314
|
+
if (!event.recurringEventId) return null;
|
|
2315
|
+
const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
|
|
2316
|
+
const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
|
|
2317
|
+
return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
|
|
2318
|
+
},
|
|
1735
2319
|
async respond(event, status) {
|
|
1736
2320
|
const attendees = rawOf(event)?.attendees ?? event.attendees.map((a) => ({ email: a.email, displayName: a.name, responseStatus: a.status, self: a.self, organizer: a.organizer }));
|
|
1737
2321
|
if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
|
|
@@ -2070,32 +2654,32 @@ async function runDaemon() {
|
|
|
2070
2654
|
// src/ui/hooks.ts
|
|
2071
2655
|
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
2072
2656
|
import { useStdout } from "ink";
|
|
2073
|
-
import { addDays as
|
|
2657
|
+
import { addDays as addDays5, addWeeks as addWeeks2, startOfDay as startOfDay2 } from "date-fns";
|
|
2074
2658
|
|
|
2075
2659
|
// ../../packages/core/src/logic/layout.ts
|
|
2076
2660
|
import {
|
|
2077
|
-
addDays as
|
|
2661
|
+
addDays as addDays4,
|
|
2078
2662
|
addMonths as addMonths2,
|
|
2079
2663
|
addWeeks,
|
|
2080
|
-
format as
|
|
2664
|
+
format as format5,
|
|
2081
2665
|
isSameDay as isSameDay2,
|
|
2082
|
-
parseISO as
|
|
2666
|
+
parseISO as parseISO5,
|
|
2083
2667
|
startOfDay,
|
|
2084
2668
|
startOfMonth,
|
|
2085
|
-
startOfWeek
|
|
2669
|
+
startOfWeek as startOfWeek2
|
|
2086
2670
|
} from "date-fns";
|
|
2087
2671
|
var WEEK_STARTS_ON = 1;
|
|
2088
2672
|
function eventBounds(e) {
|
|
2089
|
-
const start =
|
|
2090
|
-
let end =
|
|
2091
|
-
if (end <= start) end = e.allDay ?
|
|
2673
|
+
const start = parseISO5(e.start);
|
|
2674
|
+
let end = parseISO5(e.end);
|
|
2675
|
+
if (end <= start) end = e.allDay ? addDays4(start, 1) : new Date(start.getTime() + 6e4);
|
|
2092
2676
|
return { start, end };
|
|
2093
2677
|
}
|
|
2094
2678
|
var isPast = (e, now) => eventBounds(e).end.getTime() <= +now;
|
|
2095
2679
|
function overlapsDay(e, day) {
|
|
2096
2680
|
const d0 = startOfDay(day);
|
|
2097
2681
|
const { start, end } = eventBounds(e);
|
|
2098
|
-
return start <
|
|
2682
|
+
return start < addDays4(d0, 1) && end > d0;
|
|
2099
2683
|
}
|
|
2100
2684
|
function eventsOnDay(events, day) {
|
|
2101
2685
|
return events.filter((e) => overlapsDay(e, day)).sort((a, b) => {
|
|
@@ -2142,31 +2726,31 @@ function layoutDay(events, day, minDur = 0) {
|
|
|
2142
2726
|
return packColumns(items, minDur);
|
|
2143
2727
|
}
|
|
2144
2728
|
function monthGrid(date) {
|
|
2145
|
-
const first =
|
|
2146
|
-
return Array.from({ length: 42 }, (_, i) =>
|
|
2729
|
+
const first = startOfWeek2(startOfMonth(date), { weekStartsOn: WEEK_STARTS_ON });
|
|
2730
|
+
return Array.from({ length: 42 }, (_, i) => addDays4(first, i));
|
|
2147
2731
|
}
|
|
2148
2732
|
function viewDays(view, date) {
|
|
2149
2733
|
if (view === "month") return monthGrid(date);
|
|
2150
2734
|
if (view === "day") return [startOfDay(date)];
|
|
2151
|
-
if (view === "3day") return [0, 1, 2].map((i) =>
|
|
2152
|
-
const first =
|
|
2153
|
-
return Array.from({ length: 7 }, (_, i) =>
|
|
2735
|
+
if (view === "3day") return [0, 1, 2].map((i) => addDays4(startOfDay(date), i));
|
|
2736
|
+
const first = startOfWeek2(date, { weekStartsOn: WEEK_STARTS_ON });
|
|
2737
|
+
return Array.from({ length: 7 }, (_, i) => addDays4(first, i));
|
|
2154
2738
|
}
|
|
2155
2739
|
function viewRange(view, date) {
|
|
2156
2740
|
const days = viewDays(view, date);
|
|
2157
|
-
return { start: days[0].toISOString(), end:
|
|
2741
|
+
return { start: days[0].toISOString(), end: addDays4(days[days.length - 1], 1).toISOString() };
|
|
2158
2742
|
}
|
|
2159
2743
|
function shiftDate(view, date, dir) {
|
|
2160
|
-
if (view === "day") return
|
|
2161
|
-
if (view === "3day") return
|
|
2744
|
+
if (view === "day") return addDays4(date, dir);
|
|
2745
|
+
if (view === "3day") return addDays4(date, 3 * dir);
|
|
2162
2746
|
if (view === "week") return addWeeks(date, dir);
|
|
2163
2747
|
return addMonths2(date, dir);
|
|
2164
2748
|
}
|
|
2165
2749
|
function rangeLabel(a, b) {
|
|
2166
2750
|
const head = a.getFullYear() !== b.getFullYear() ? "d MMM yyyy" : a.getMonth() !== b.getMonth() ? "d MMM" : "d";
|
|
2167
|
-
return `${
|
|
2751
|
+
return `${format5(a, head)} \u2013 ${format5(b, "d MMM yyyy")}`;
|
|
2168
2752
|
}
|
|
2169
|
-
var ymd = (d) =>
|
|
2753
|
+
var ymd = (d) => format5(d, "yyyy-MM-dd");
|
|
2170
2754
|
|
|
2171
2755
|
// ../../packages/core/src/logic/visible.ts
|
|
2172
2756
|
var key2 = (accountId, calendarId) => `${accountId}/${calendarId}`;
|
|
@@ -2186,7 +2770,7 @@ var eventKey = (e) => `${e.accountId}/${e.calendarId}/${e.id}`;
|
|
|
2186
2770
|
function useLive(load, deps) {
|
|
2187
2771
|
const api = useApi();
|
|
2188
2772
|
const [state, setState] = useState({});
|
|
2189
|
-
const [
|
|
2773
|
+
const [bump2, setBump] = useState(0);
|
|
2190
2774
|
useEffect(() => {
|
|
2191
2775
|
let seq = 0;
|
|
2192
2776
|
let alive2 = true;
|
|
@@ -2203,7 +2787,7 @@ function useLive(load, deps) {
|
|
|
2203
2787
|
alive2 = false;
|
|
2204
2788
|
off();
|
|
2205
2789
|
};
|
|
2206
|
-
}, [api,
|
|
2790
|
+
}, [api, bump2, ...deps]);
|
|
2207
2791
|
return { ...state, reload: useCallback(() => setBump((b) => b + 1), []) };
|
|
2208
2792
|
}
|
|
2209
2793
|
function useDirectory() {
|
|
@@ -2224,13 +2808,13 @@ var AGENDA_DAYS = 14;
|
|
|
2224
2808
|
function navRange({ view, date }) {
|
|
2225
2809
|
if (view === "agenda" || view === "2day") {
|
|
2226
2810
|
const start = startOfDay2(date);
|
|
2227
|
-
return { start: start.toISOString(), end:
|
|
2811
|
+
return { start: start.toISOString(), end: addDays5(start, view === "agenda" ? AGENDA_DAYS : 2).toISOString() };
|
|
2228
2812
|
}
|
|
2229
2813
|
return viewRange(view, date);
|
|
2230
2814
|
}
|
|
2231
2815
|
var shiftNav = ({ view, date }, dir) => ({
|
|
2232
2816
|
view,
|
|
2233
|
-
date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ?
|
|
2817
|
+
date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ? addDays5(date, 2 * dir) : shiftDate(view, date, dir)
|
|
2234
2818
|
});
|
|
2235
2819
|
function useNav(initial = { view: "agenda", date: /* @__PURE__ */ new Date() }) {
|
|
2236
2820
|
const [nav, setNav] = useState(initial);
|
|
@@ -2265,12 +2849,12 @@ function useTermSize() {
|
|
|
2265
2849
|
// src/ui/App.tsx
|
|
2266
2850
|
import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
|
|
2267
2851
|
import { Box as Box11, Text as Text11, useApp, useStdin as useStdin2 } from "ink";
|
|
2268
|
-
import { addDays as
|
|
2852
|
+
import { addDays as addDays11, differenceInMinutes as differenceInMinutes3, format as format13, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
|
|
2269
2853
|
|
|
2270
2854
|
// ../../packages/core/src/logic/editor.ts
|
|
2271
|
-
import { addDays as
|
|
2855
|
+
import { addDays as addDays6, addHours, format as format6, parseISO as parseISO6, startOfHour } from "date-fns";
|
|
2272
2856
|
var LOCAL = "yyyy-MM-dd'T'HH:mm";
|
|
2273
|
-
var toLocalInput = (iso2) =>
|
|
2857
|
+
var toLocalInput = (iso2) => format6(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
|
|
2274
2858
|
var fromLocalInput = (local) => new Date(local).toISOString();
|
|
2275
2859
|
var writableCalendars = (calendars, accountId) => calendars.filter((c) => c.accountId === accountId && !c.readOnly);
|
|
2276
2860
|
var writableAccounts = (accounts, calendars) => accounts.filter((a) => writableCalendars(calendars, a.id).length > 0);
|
|
@@ -2280,8 +2864,8 @@ var splitEmails = (text2) => text2.split(/[\s,;]+/).map((s) => s.trim()).filter(
|
|
|
2280
2864
|
var isEmail = (s) => EMAIL.test(s);
|
|
2281
2865
|
function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new Date()) {
|
|
2282
2866
|
const accountId = soleId(writableAccounts(accounts, calendars));
|
|
2283
|
-
const start = prefill.start ? toLocalInput(prefill.start) :
|
|
2284
|
-
let end = prefill.end ? toLocalInput(prefill.end) :
|
|
2867
|
+
const start = prefill.start ? toLocalInput(prefill.start) : format6(startOfHour(addHours(now, 1)), LOCAL);
|
|
2868
|
+
let end = prefill.end ? toLocalInput(prefill.end) : format6(addHours(new Date(start), 1), LOCAL);
|
|
2285
2869
|
if (prefill.allDay && prefill.end) end = inclusiveEnd(start, end);
|
|
2286
2870
|
return {
|
|
2287
2871
|
accountId,
|
|
@@ -2292,11 +2876,13 @@ function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new
|
|
|
2292
2876
|
allDay: !!prefill.allDay,
|
|
2293
2877
|
location: "",
|
|
2294
2878
|
description: "",
|
|
2295
|
-
attendees: []
|
|
2879
|
+
attendees: [],
|
|
2880
|
+
repeat: null,
|
|
2881
|
+
repeatWas: null
|
|
2296
2882
|
};
|
|
2297
2883
|
}
|
|
2298
2884
|
var inclusiveEnd = (startLocal, endLocal) => {
|
|
2299
|
-
const d =
|
|
2885
|
+
const d = format6(addDays6(new Date(endLocal), -1), LOCAL);
|
|
2300
2886
|
return d.slice(0, 10) < startLocal.slice(0, 10) ? startLocal : d;
|
|
2301
2887
|
};
|
|
2302
2888
|
function formFromEvent(e) {
|
|
@@ -2311,23 +2897,28 @@ function formFromEvent(e) {
|
|
|
2311
2897
|
allDay: e.allDay,
|
|
2312
2898
|
location: e.location ?? "",
|
|
2313
2899
|
description: e.description ?? "",
|
|
2314
|
-
attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email)
|
|
2900
|
+
attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email),
|
|
2901
|
+
repeat: null,
|
|
2902
|
+
// A series' rule isn't cached: the editor loads it (events.recurrence) and sets both fields.
|
|
2903
|
+
repeatWas: e.recurringEventId ? void 0 : null
|
|
2315
2904
|
};
|
|
2316
2905
|
}
|
|
2906
|
+
var withLoadedRepeat = (f, rule) => ({ ...f, repeat: rule, repeatWas: rule });
|
|
2907
|
+
var repeatChanged = (f) => f.repeatWas !== void 0 && !sameRecurrence(f.repeat, f.repeatWas, f.start);
|
|
2317
2908
|
function setAllDay(f, allDay) {
|
|
2318
2909
|
if (allDay || f.end > f.start) return { ...f, allDay };
|
|
2319
|
-
return { ...f, allDay, end:
|
|
2910
|
+
return { ...f, allDay, end: format6(addHours(new Date(f.start), 1), LOCAL) };
|
|
2320
2911
|
}
|
|
2321
2912
|
function moveStart(f, start) {
|
|
2322
2913
|
const end = new Date(new Date(f.end).getTime() + new Date(start).getTime() - new Date(f.start).getTime());
|
|
2323
|
-
return { ...f, start, end:
|
|
2914
|
+
return { ...f, start, end: format6(end, LOCAL) };
|
|
2324
2915
|
}
|
|
2325
2916
|
function times(f) {
|
|
2326
2917
|
if (f.allDay) {
|
|
2327
2918
|
const start2 = f.start.slice(0, 10);
|
|
2328
2919
|
const endIncl = f.end.slice(0, 10);
|
|
2329
2920
|
if (endIncl < start2) throw new Error("End date is before start date");
|
|
2330
|
-
return { allDay: true, start: start2, end:
|
|
2921
|
+
return { allDay: true, start: start2, end: format6(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
|
|
2331
2922
|
}
|
|
2332
2923
|
const start = fromLocalInput(f.start);
|
|
2333
2924
|
const end = fromLocalInput(f.end);
|
|
@@ -2350,7 +2941,8 @@ function formToInput(f) {
|
|
|
2350
2941
|
...times(f),
|
|
2351
2942
|
location: f.location.trim() || void 0,
|
|
2352
2943
|
description: f.description.trim() || void 0,
|
|
2353
|
-
...attendees.length ? { attendees } : {}
|
|
2944
|
+
...attendees.length ? { attendees } : {},
|
|
2945
|
+
...f.repeat ? { recurrence: f.repeat } : {}
|
|
2354
2946
|
};
|
|
2355
2947
|
}
|
|
2356
2948
|
function applyForm(e, f) {
|
|
@@ -2364,7 +2956,9 @@ function applyForm(e, f) {
|
|
|
2364
2956
|
...times(f),
|
|
2365
2957
|
location: f.location.trim() || void 0,
|
|
2366
2958
|
description: f.description.trim() || void 0,
|
|
2367
|
-
attendees: [...keep, ...invited]
|
|
2959
|
+
attendees: [...keep, ...invited],
|
|
2960
|
+
// Only a changed rule is sent; absent keeps the series' rule.
|
|
2961
|
+
...repeatChanged(f) ? { recurrence: f.repeat } : {}
|
|
2368
2962
|
};
|
|
2369
2963
|
}
|
|
2370
2964
|
var errorText = (e) => String(e instanceof Error ? e.message : e).replace(/^Error invoking remote method '[^']+': (\w*Error: )?/, "");
|
|
@@ -2483,10 +3077,10 @@ function Button({ k, label, onPress, color: color2 = C.accent }) {
|
|
|
2483
3077
|
// src/ui/views/Agenda.tsx
|
|
2484
3078
|
import { useMemo, useReducer, useRef as useRef2 } from "react";
|
|
2485
3079
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
2486
|
-
import { addDays as
|
|
3080
|
+
import { addDays as addDays8, differenceInCalendarDays as differenceInCalendarDays2, differenceInMinutes as differenceInMinutes2, format as format9, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
|
|
2487
3081
|
|
|
2488
3082
|
// ../../packages/core/src/logic/details.ts
|
|
2489
|
-
import { addDays as
|
|
3083
|
+
import { addDays as addDays7, format as format7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
|
|
2490
3084
|
var same = (a, b) => !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
2491
3085
|
function ownerLine(calendar, label, email) {
|
|
2492
3086
|
return { calendar: same(calendar, label) || same(calendar, email) ? void 0 : calendar, label, email: same(email, label) ? void 0 : email };
|
|
@@ -2496,17 +3090,17 @@ function canEdit(e, account, calendar) {
|
|
|
2496
3090
|
if (e.attendees.length === 0) return true;
|
|
2497
3091
|
return same(e.organizer?.email, account.email) || e.attendees.some((a) => a.self && a.organizer);
|
|
2498
3092
|
}
|
|
2499
|
-
var at2 = (iso2, allDay) => (allDay ?
|
|
3093
|
+
var at2 = (iso2, allDay) => (allDay ? parseISO7(iso2) : new Date(iso2)).getTime();
|
|
2500
3094
|
var pendingInvites = (events, now = /* @__PURE__ */ new Date()) => events.filter((e) => e.myStatus === "needsAction" && at2(e.end, e.allDay) > now.getTime()).sort((a, b) => at2(a.start, a.allDay) - at2(b.start, b.allDay));
|
|
2501
3095
|
function formatWhen(e) {
|
|
2502
3096
|
if (e.allDay) {
|
|
2503
|
-
const s2 =
|
|
2504
|
-
const last =
|
|
2505
|
-
return last > s2 ? `${
|
|
3097
|
+
const s2 = parseISO7(e.start);
|
|
3098
|
+
const last = addDays7(parseISO7(e.end), -1);
|
|
3099
|
+
return last > s2 ? `${format7(s2, "EEE, d MMM")} \u2013 ${format7(last, "EEE, d MMM")} \xB7 all day` : `${format7(s2, "EEE, d MMM")} \xB7 all day`;
|
|
2506
3100
|
}
|
|
2507
3101
|
const s = new Date(e.start);
|
|
2508
3102
|
const t = new Date(e.end);
|
|
2509
|
-
return isSameDay3(s, t) ? `${
|
|
3103
|
+
return isSameDay3(s, t) ? `${format7(s, "EEE, d MMM")} \xB7 ${format7(s, "HH:mm")} \u2013 ${format7(t, "HH:mm")}` : `${format7(s, "EEE, d MMM HH:mm")} \u2013 ${format7(t, "EEE, d MMM HH:mm")}`;
|
|
2510
3104
|
}
|
|
2511
3105
|
var STATUS_ICON = {
|
|
2512
3106
|
accepted: "\u2713",
|
|
@@ -2521,13 +3115,12 @@ function meetingUrl(location) {
|
|
|
2521
3115
|
return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
|
|
2522
3116
|
}
|
|
2523
3117
|
function place(location = "") {
|
|
2524
|
-
const
|
|
2525
|
-
|
|
2526
|
-
return rest || (url ? "video call" : "");
|
|
3118
|
+
const rest = location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
|
|
3119
|
+
return rest || (meetingUrl(location) ? "video call" : "");
|
|
2527
3120
|
}
|
|
2528
3121
|
|
|
2529
3122
|
// ../../packages/core/src/logic/status.ts
|
|
2530
|
-
import { differenceInMinutes, format as
|
|
3123
|
+
import { differenceInMinutes, format as format8 } from "date-fns";
|
|
2531
3124
|
function pickNowNext(events, now) {
|
|
2532
3125
|
const t = now.getTime();
|
|
2533
3126
|
const timed = events.filter((e) => !e.allDay && e.myStatus !== "declined").sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
|
@@ -2539,7 +3132,7 @@ function pickNowNext(events, now) {
|
|
|
2539
3132
|
function startsLabel(start, now) {
|
|
2540
3133
|
const s = new Date(start);
|
|
2541
3134
|
const m = differenceInMinutes(s, now, { roundingMethod: "ceil" });
|
|
2542
|
-
return m <= 60 ? `in ${m}m` : `at ${
|
|
3135
|
+
return m <= 60 ? `in ${m}m` : `at ${format8(s, "HH:mm")}`;
|
|
2543
3136
|
}
|
|
2544
3137
|
|
|
2545
3138
|
// src/ui/screens/EventDetails.tsx
|
|
@@ -2753,7 +3346,7 @@ var span = (m) => {
|
|
|
2753
3346
|
};
|
|
2754
3347
|
function busyMinutes(list, day) {
|
|
2755
3348
|
const from = +startOfDay3(day);
|
|
2756
|
-
const to = +
|
|
3349
|
+
const to = +addDays8(day, 1);
|
|
2757
3350
|
const iv = list.filter((e) => !e.allDay && e.myStatus !== "declined").map((e) => [Math.max(+eventBounds(e).start, from), Math.min(+eventBounds(e).end, to)]).sort((a, b) => a[0] - b[0]);
|
|
2758
3351
|
let total = 0;
|
|
2759
3352
|
let end = -Infinity;
|
|
@@ -2768,7 +3361,7 @@ var overlaps = (e, list) => !e.allDay && e.myStatus !== "declined" && list.some(
|
|
|
2768
3361
|
(o) => o !== e && !o.allDay && o.myStatus !== "declined" && eventBounds(o).start < eventBounds(e).end && eventBounds(e).start < eventBounds(o).end
|
|
2769
3362
|
);
|
|
2770
3363
|
function relativeDay(day, now) {
|
|
2771
|
-
const d =
|
|
3364
|
+
const d = differenceInCalendarDays2(day, now);
|
|
2772
3365
|
return d === 0 ? "today" : d === 1 ? "tomorrow" : d === -1 ? "yesterday" : d > 0 ? `in ${d} days` : `${-d} days ago`;
|
|
2773
3366
|
}
|
|
2774
3367
|
function fitParts(parts, w) {
|
|
@@ -2797,7 +3390,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2797
3390
|
const rows = useMemo(() => {
|
|
2798
3391
|
const out = [];
|
|
2799
3392
|
for (let i = 0; i < AGENDA_DAYS; i++) {
|
|
2800
|
-
const day =
|
|
3393
|
+
const day = addDays8(startOfDay3(date), i);
|
|
2801
3394
|
const list = eventsOnDay(events, day);
|
|
2802
3395
|
if (!list.length) {
|
|
2803
3396
|
const last = out.at(-1);
|
|
@@ -2838,7 +3431,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2838
3431
|
const key3 = "free" in r ? `free${ymd(r.free[0])}` : "e" in r ? `${ymd(r.day)}/${eventKey(r.e)}` : "nowLine" in r ? `${ymd(r.day)}/now` : "divider" in r ? `${ymd(r.day)}/d${i}` : ymd(r.day);
|
|
2839
3432
|
if ("free" in r) {
|
|
2840
3433
|
const [a, b] = r.free;
|
|
2841
|
-
const label = isSameDay4(a, b) ?
|
|
3434
|
+
const label = isSameDay4(a, b) ? format9(a, "EEE d MMM") : `${format9(a, "EEE d MMM")} \u2013 ${format9(b, "EEE d MMM")}`;
|
|
2842
3435
|
lines.push(
|
|
2843
3436
|
/* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.muted, wrap: "truncate", children: ` ${label} free` }) }, key3)
|
|
2844
3437
|
);
|
|
@@ -2852,7 +3445,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2852
3445
|
}
|
|
2853
3446
|
if ("nowLine" in r) {
|
|
2854
3447
|
lines.push(
|
|
2855
|
-
/* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${
|
|
3448
|
+
/* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${format9(now, "HH:mm")} ${"\u2500".repeat(width)}` }) }, key3)
|
|
2856
3449
|
);
|
|
2857
3450
|
return;
|
|
2858
3451
|
}
|
|
@@ -2860,7 +3453,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2860
3453
|
const today = isSameDay4(r.day, now);
|
|
2861
3454
|
const list = eventsOnDay(events, r.day);
|
|
2862
3455
|
const busy = busyMinutes(list, r.day);
|
|
2863
|
-
const left = ` ${
|
|
3456
|
+
const left = ` ${format9(r.day, "EEE d MMM")} `;
|
|
2864
3457
|
const rel = relativeDay(r.day, now);
|
|
2865
3458
|
const right = ` ${list.length} ${list.length === 1 ? "event" : "events"}${busy ? ` \xB7 ${span(busy)} busy` : ""} `;
|
|
2866
3459
|
const rule = "\u2500".repeat(Math.max(width - left.length - rel.length - right.length - 1, 1));
|
|
@@ -2878,7 +3471,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2878
3471
|
const { start, end } = eventBounds(e);
|
|
2879
3472
|
const running = !e.allDay && start <= now && now < end;
|
|
2880
3473
|
const past = isPast(e, now) || e.myStatus === "declined";
|
|
2881
|
-
const clockOf = (d, edge) => isSameDay4(d, r.day) ?
|
|
3474
|
+
const clockOf = (d, edge) => isSameDay4(d, r.day) ? format9(d, "HH:mm") : +d === +addDays8(r.day, 1) ? "24:00" : edge;
|
|
2882
3475
|
const badges = [];
|
|
2883
3476
|
if (running) badges.push({ text: ` \u25CF now \xB7 ends in ${span(Math.max(differenceInMinutes2(end, now, { roundingMethod: "ceil" }), 1))}`, color: C.green, bold: true });
|
|
2884
3477
|
else if (e === upNext) badges.push({ text: ` ${startsLabel(e.start, now)}`, color: C.yellow });
|
|
@@ -2916,16 +3509,16 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2916
3509
|
|
|
2917
3510
|
// src/ui/views/Day.tsx
|
|
2918
3511
|
import { useMemo as useMemo3 } from "react";
|
|
2919
|
-
import { addDays as
|
|
3512
|
+
import { addDays as addDays9, startOfDay as startOfDay5 } from "date-fns";
|
|
2920
3513
|
|
|
2921
3514
|
// src/ui/views/TimeGrid.tsx
|
|
2922
3515
|
import { useMemo as useMemo2 } from "react";
|
|
2923
3516
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
2924
|
-
import { format as
|
|
3517
|
+
import { format as format11, isSameDay as isSameDay6 } from "date-fns";
|
|
2925
3518
|
|
|
2926
3519
|
// src/ui/views/Month.tsx
|
|
2927
3520
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2928
|
-
import { format as
|
|
3521
|
+
import { format as format10, isSameDay as isSameDay5, isSameMonth, startOfDay as startOfDay4 } from "date-fns";
|
|
2929
3522
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2930
3523
|
function fit(items, rows, selected) {
|
|
2931
3524
|
if (items.length <= rows) return { shown: items, more: 0 };
|
|
@@ -2936,7 +3529,7 @@ function fit(items, rows, selected) {
|
|
|
2936
3529
|
}
|
|
2937
3530
|
function EventLine({ e, day, now, selected, time, onSelect, onOpen }) {
|
|
2938
3531
|
const { start, end } = eventBounds(e);
|
|
2939
|
-
const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${
|
|
3532
|
+
const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${format10(start, "HH:mm")} `;
|
|
2940
3533
|
const live = !e.allDay && start <= now && now < end;
|
|
2941
3534
|
return /* @__PURE__ */ jsx4(Clickable, { height: 1, onClick: clickEvent(e, selected, { onSelect, onOpen }), children: /* @__PURE__ */ jsxs4(
|
|
2942
3535
|
Text4,
|
|
@@ -2960,7 +3553,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
|
|
|
2960
3553
|
const cellHeight = Math.max(Math.floor((height - 1) / 6), 1);
|
|
2961
3554
|
const weeks = [0, 1, 2, 3, 4, 5].map((w) => days.slice(w * 7, w * 7 + 7));
|
|
2962
3555
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
2963
|
-
/* @__PURE__ */ jsx4(Box4, { children: weeks[0].map((d) => /* @__PURE__ */ jsx4(Box4, { width: colWidth, children: /* @__PURE__ */ jsx4(Text4, { color: C.accent, bold: true, children:
|
|
3556
|
+
/* @__PURE__ */ jsx4(Box4, { children: weeks[0].map((d) => /* @__PURE__ */ jsx4(Box4, { width: colWidth, children: /* @__PURE__ */ jsx4(Text4, { color: C.accent, bold: true, children: format10(d, "EEE") }) }, d.getDay())) }),
|
|
2964
3557
|
weeks.map((week) => /* @__PURE__ */ jsx4(Box4, { height: cellHeight, children: week.map((day) => {
|
|
2965
3558
|
const items = eventsOnDay(events, day);
|
|
2966
3559
|
const { shown, more } = fit(items, cellHeight - 1, items.findIndex((e) => eventKey(e) === selectedKey));
|
|
@@ -2974,7 +3567,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
|
|
|
2974
3567
|
inverse: here,
|
|
2975
3568
|
color: today ? C.now : here ? C.accent : isSameMonth(day, date) ? void 0 : C.muted,
|
|
2976
3569
|
children: [
|
|
2977
|
-
here ? ` ${
|
|
3570
|
+
here ? ` ${format10(day, "d")} ` : format10(day, "d"),
|
|
2978
3571
|
cellHeight === 1 && items.length > 0 && /* @__PURE__ */ jsxs4(Text4, { color: C.muted, children: [
|
|
2979
3572
|
" \xB7",
|
|
2980
3573
|
items.length
|
|
@@ -3064,10 +3657,10 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3064
3657
|
const selected = k === selectedKey;
|
|
3065
3658
|
const first = p.start / 60;
|
|
3066
3659
|
const { start, end } = eventBounds(e);
|
|
3067
|
-
const from = isSameDay6(start, day) ?
|
|
3660
|
+
const from = isSameDay6(start, day) ? format11(start, "HH:mm") : "\u2026";
|
|
3068
3661
|
const title2 = `${rsvpMark(e)}${e.title || "(no title)"}`;
|
|
3069
3662
|
const where = place(e.location);
|
|
3070
|
-
const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${
|
|
3663
|
+
const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${format11(start, "mm")} ${title2}`, `${from}\u2013${isSameDay6(end, day) ? format11(end, "HH:mm") : "\u2026"}`, where] : [`${from} ${title2}`, [where, duration(e)].filter(Boolean).join(" \xB7 ")];
|
|
3071
3664
|
const text2 = lines[h - first] ?? "";
|
|
3072
3665
|
const inner = p.col < p.cols - 1 && w > 1 ? w - 1 : w;
|
|
3073
3666
|
const past = isPast(e, now) || e.myStatus === "declined";
|
|
@@ -3095,7 +3688,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3095
3688
|
const isToday = isSameDay6(c.day, now);
|
|
3096
3689
|
const here = !!cursor && isSameDay6(c.day, cursor) && days.length > 1;
|
|
3097
3690
|
return /* @__PURE__ */ jsx5(Clickable, { height: 1, width: colW, onClick: onPickDay && (() => onPickDay(c.day)), children: /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
|
|
3098
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${
|
|
3691
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${format11(c.day, "EEE d")} ` }),
|
|
3099
3692
|
c.count > 0 && /* @__PURE__ */ jsxs5(Text5, { color: C.muted, children: [
|
|
3100
3693
|
"\xB7",
|
|
3101
3694
|
c.count
|
|
@@ -3125,7 +3718,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3125
3718
|
/* @__PURE__ */ jsx5(Clickable, { flexDirection: "column", width, onWheel: (dir) => scrollBy(dir), children: Array.from({ length: Math.min(gridH, 24) }, (_, i) => {
|
|
3126
3719
|
const h = top + i;
|
|
3127
3720
|
const current = today && h === now.getHours();
|
|
3128
|
-
const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${
|
|
3721
|
+
const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${format11(now, "HH:mm")}\u2500` : `${String(h).padStart(2, "0")}:00` }) });
|
|
3129
3722
|
return row(
|
|
3130
3723
|
`h${h}`,
|
|
3131
3724
|
gutter,
|
|
@@ -3140,7 +3733,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3140
3733
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
3141
3734
|
function useDays(date, n) {
|
|
3142
3735
|
const t = startOfDay5(date).getTime();
|
|
3143
|
-
return useMemo3(() => Array.from({ length: n }, (_, i) =>
|
|
3736
|
+
return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t, i)), [t, n]);
|
|
3144
3737
|
}
|
|
3145
3738
|
var Day = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 1) });
|
|
3146
3739
|
var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 2) });
|
|
@@ -3157,7 +3750,7 @@ function Week(p) {
|
|
|
3157
3750
|
// src/ui/screens/EventEditor.tsx
|
|
3158
3751
|
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
3159
3752
|
import { Box as Box6, Text as Text6 } from "ink";
|
|
3160
|
-
import { format as
|
|
3753
|
+
import { format as format12, isValid as isValid2, parse as parse2 } from "date-fns";
|
|
3161
3754
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3162
3755
|
var LABELS = {
|
|
3163
3756
|
title: "Title",
|
|
@@ -3170,20 +3763,29 @@ var LABELS = {
|
|
|
3170
3763
|
endTime: "End time",
|
|
3171
3764
|
location: "Location",
|
|
3172
3765
|
description: "Notes",
|
|
3173
|
-
attendees: "Invitees"
|
|
3766
|
+
attendees: "Invitees",
|
|
3767
|
+
repeat: "Repeat",
|
|
3768
|
+
repeatEvery: "Every",
|
|
3769
|
+
repeatDays: "On days",
|
|
3770
|
+
repeatEnds: "Stops"
|
|
3174
3771
|
};
|
|
3772
|
+
var SCOPE_KEYS2 = { "1": "one", "2": "following", "3": "all" };
|
|
3175
3773
|
var HINTS = {
|
|
3176
3774
|
startDate: "YYYY-MM-DD",
|
|
3177
3775
|
startTime: "HH:mm",
|
|
3178
3776
|
endDate: "YYYY-MM-DD",
|
|
3179
3777
|
endTime: "HH:mm",
|
|
3180
3778
|
attendees: "a@x.com, b@y.com",
|
|
3181
|
-
title: "New Event"
|
|
3779
|
+
title: "New Event",
|
|
3780
|
+
repeatEvery: "2 weeks",
|
|
3781
|
+
repeatDays: "mo th",
|
|
3782
|
+
repeatEnds: "never \xB7 2026-12-31 \xB7 10 times"
|
|
3182
3783
|
};
|
|
3784
|
+
var PICKERS = /* @__PURE__ */ new Set(["account", "calendar", "allDay", "repeat"]);
|
|
3183
3785
|
function toLocal(date, time) {
|
|
3184
3786
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time)) return;
|
|
3185
3787
|
const d = parse2(`${date}T${time}`, "yyyy-MM-dd'T'HH:mm", /* @__PURE__ */ new Date());
|
|
3186
|
-
return
|
|
3788
|
+
return isValid2(d) && format12(d, "yyyy-MM-dd'T'HH:mm") === `${date}T${time}` ? `${date}T${time}` : void 0;
|
|
3187
3789
|
}
|
|
3188
3790
|
var timeTexts = (f) => ({
|
|
3189
3791
|
startDate: f.start.slice(0, 10),
|
|
@@ -3191,25 +3793,48 @@ var timeTexts = (f) => ({
|
|
|
3191
3793
|
endDate: f.end.slice(0, 10),
|
|
3192
3794
|
endTime: f.end.slice(11)
|
|
3193
3795
|
});
|
|
3796
|
+
var repeatTexts = (r, start) => ({
|
|
3797
|
+
repeatEvery: r ? everyText(r) : "",
|
|
3798
|
+
repeatDays: daysText(r?.byDay?.length ? r.byDay : [weekdayOf(start)]),
|
|
3799
|
+
repeatEnds: r ? endsText(r) : "never"
|
|
3800
|
+
});
|
|
3194
3801
|
var textsFrom = (f) => ({
|
|
3195
3802
|
title: f.title,
|
|
3196
3803
|
location: f.location,
|
|
3197
3804
|
description: f.description,
|
|
3198
3805
|
attendees: f.attendees.join(", "),
|
|
3199
|
-
...timeTexts(f)
|
|
3806
|
+
...timeTexts(f),
|
|
3807
|
+
...repeatTexts(f.repeat, f.start)
|
|
3200
3808
|
});
|
|
3201
|
-
var
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
]
|
|
3809
|
+
var isCustom = (s) => !!s.form.repeat && !s.form.repeat.rule && (s.custom || presetOf(s.form.repeat, s.form.start) === "custom");
|
|
3810
|
+
var fieldsFor = (s, editing) => {
|
|
3811
|
+
const { form } = s;
|
|
3812
|
+
const custom = isCustom(s);
|
|
3813
|
+
return [
|
|
3814
|
+
"title",
|
|
3815
|
+
...editing ? [] : ["account", "calendar"],
|
|
3816
|
+
"allDay",
|
|
3817
|
+
"startDate",
|
|
3818
|
+
...form.allDay ? [] : ["startTime"],
|
|
3819
|
+
"endDate",
|
|
3820
|
+
...form.allDay ? [] : ["endTime"],
|
|
3821
|
+
"repeat",
|
|
3822
|
+
...custom ? ["repeatEvery"] : [],
|
|
3823
|
+
...custom && form.repeat?.freq === "weekly" ? ["repeatDays"] : [],
|
|
3824
|
+
...form.repeat && !form.repeat.rule ? ["repeatEnds"] : [],
|
|
3825
|
+
"location",
|
|
3826
|
+
"description",
|
|
3827
|
+
"attendees"
|
|
3828
|
+
];
|
|
3829
|
+
};
|
|
3830
|
+
function readRepeat(s) {
|
|
3831
|
+
const r = s.form.repeat;
|
|
3832
|
+
if (!r || r.rule) return r;
|
|
3833
|
+
const { until: _u, count: _c, ...base } = r;
|
|
3834
|
+
const rule = isCustom(s) ? { ...parseEvery(s.texts.repeatEvery) } : base;
|
|
3835
|
+
if (isCustom(s) && rule.freq === "weekly") rule.byDay = parseDays(s.texts.repeatDays);
|
|
3836
|
+
return { ...rule, ...parseEnds(s.texts.repeatEnds) };
|
|
3837
|
+
}
|
|
3213
3838
|
function EventEditor({ event, initialStart, onClose }) {
|
|
3214
3839
|
const api = useApi();
|
|
3215
3840
|
const { accounts, calendars, loaded } = useDirectory();
|
|
@@ -3223,18 +3848,41 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3223
3848
|
const [error, setError] = useState3("");
|
|
3224
3849
|
const [saving, setSaving] = useState3(false);
|
|
3225
3850
|
const busy = useRef3(false);
|
|
3851
|
+
const [askScope, setAskScopeState] = useState3(false);
|
|
3852
|
+
const asking = useRef3(false);
|
|
3853
|
+
const setAskScope = (on) => {
|
|
3854
|
+
asking.current = on;
|
|
3855
|
+
setAskScopeState(on);
|
|
3856
|
+
};
|
|
3857
|
+
const [repeatFailed, setRepeatFailed] = useState3(false);
|
|
3226
3858
|
const alive2 = useRef3(true);
|
|
3227
3859
|
useEffect3(() => () => void (alive2.current = false), []);
|
|
3228
3860
|
useEffect3(() => {
|
|
3229
3861
|
if (latest.current || !loaded) return;
|
|
3230
3862
|
const form2 = event ? formFromEvent(event) : emptyForm(accounts, calendars, {}, initialStart);
|
|
3231
|
-
latest.current = { form: form2, texts: textsFrom(form2), focus: 0 };
|
|
3863
|
+
latest.current = { form: form2, texts: textsFrom(form2), focus: 0, custom: false };
|
|
3232
3864
|
setState(latest.current);
|
|
3865
|
+
if (event?.recurringEventId) {
|
|
3866
|
+
api.events.recurrence(event).then(
|
|
3867
|
+
(rule) => {
|
|
3868
|
+
if (!alive2.current || !latest.current) return;
|
|
3869
|
+
const f = withLoadedRepeat(latest.current.form, rule);
|
|
3870
|
+
update({ form: f, texts: { ...latest.current.texts, ...repeatTexts(rule, f.start) } });
|
|
3871
|
+
},
|
|
3872
|
+
() => alive2.current && setRepeatFailed(true)
|
|
3873
|
+
);
|
|
3874
|
+
}
|
|
3233
3875
|
}, [loaded]);
|
|
3234
3876
|
const choices = writableAccounts(accounts, calendars);
|
|
3235
|
-
const save = async () => {
|
|
3877
|
+
const save = async (scope) => {
|
|
3236
3878
|
if (!latest.current || busy.current) return;
|
|
3237
3879
|
const { form: form2, texts: texts2 } = latest.current;
|
|
3880
|
+
let repeat;
|
|
3881
|
+
try {
|
|
3882
|
+
repeat = readRepeat(latest.current);
|
|
3883
|
+
} catch (e) {
|
|
3884
|
+
return setError(errorText(e));
|
|
3885
|
+
}
|
|
3238
3886
|
setError("");
|
|
3239
3887
|
const start = toLocal(texts2.startDate, form2.allDay ? form2.start.slice(11) : texts2.startTime);
|
|
3240
3888
|
const end = toLocal(texts2.endDate, form2.allDay ? form2.end.slice(11) : texts2.endTime);
|
|
@@ -3247,14 +3895,18 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3247
3895
|
description: texts2.description,
|
|
3248
3896
|
attendees: splitEmails(texts2.attendees),
|
|
3249
3897
|
start,
|
|
3250
|
-
end
|
|
3898
|
+
end,
|
|
3899
|
+
repeat
|
|
3251
3900
|
};
|
|
3252
3901
|
let request;
|
|
3253
3902
|
try {
|
|
3254
|
-
|
|
3903
|
+
const edited = event && applyForm(event, f);
|
|
3904
|
+
if (edited && event.recurringEventId && !scope) return setAskScope(repeatChanged(f) ? "rule" : "any");
|
|
3905
|
+
request = edited ? api.events.update(edited, scope) : api.events.create(formToInput(f));
|
|
3255
3906
|
} catch (e) {
|
|
3256
3907
|
return setError(errorText(e));
|
|
3257
3908
|
}
|
|
3909
|
+
setAskScope(false);
|
|
3258
3910
|
busy.current = true;
|
|
3259
3911
|
setSaving(true);
|
|
3260
3912
|
try {
|
|
@@ -3273,7 +3925,8 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3273
3925
|
const i = items.findIndex((x) => x.id === id2);
|
|
3274
3926
|
return items[i < 0 ? dir > 0 ? 0 : items.length - 1 : (i + dir + items.length) % items.length].id;
|
|
3275
3927
|
};
|
|
3276
|
-
const pick = (
|
|
3928
|
+
const pick = (st, field, dir) => {
|
|
3929
|
+
const { form: form2, texts: texts2 } = st;
|
|
3277
3930
|
if (field === "account") {
|
|
3278
3931
|
const accountId = cycle2(choices, form2.accountId, dir);
|
|
3279
3932
|
update({ form: { ...form2, accountId, calendarId: soleId(writableCalendars(calendars, accountId)) } });
|
|
@@ -3282,6 +3935,19 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3282
3935
|
} else if (field === "allDay") {
|
|
3283
3936
|
const f = setAllDay(form2, !form2.allDay);
|
|
3284
3937
|
update({ form: f, texts: { ...texts2, ...timeTexts(f) } });
|
|
3938
|
+
} else if (field === "repeat") {
|
|
3939
|
+
if (form2.repeatWas === void 0) return;
|
|
3940
|
+
const ids = [...presets(form2.start).map((p) => p.id), "custom"];
|
|
3941
|
+
const at3 = ids.indexOf(isCustom(st) ? "custom" : presetOf(form2.repeat, form2.start));
|
|
3942
|
+
const next = ids[(at3 + dir + ids.length) % ids.length];
|
|
3943
|
+
let prev = form2.repeat;
|
|
3944
|
+
try {
|
|
3945
|
+
prev = readRepeat(st);
|
|
3946
|
+
} catch {
|
|
3947
|
+
}
|
|
3948
|
+
const preset = presets(form2.start).find((p) => p.id === next)?.recurrence;
|
|
3949
|
+
const repeat = next === "custom" ? prev && !prev.rule ? prev : withEnd({ freq: "weekly" }, prev) : preset ? withEnd(preset, prev) : null;
|
|
3950
|
+
update({ form: { ...form2, repeat }, custom: next === "custom", texts: { ...texts2, ...repeatTexts(repeat, form2.start) } });
|
|
3285
3951
|
}
|
|
3286
3952
|
};
|
|
3287
3953
|
const edit = ({ form: form2, texts: texts2 }, field, value2) => {
|
|
@@ -3301,17 +3967,22 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3301
3967
|
const clickField = (f) => {
|
|
3302
3968
|
const s = latest.current;
|
|
3303
3969
|
if (!s || busy.current) return;
|
|
3304
|
-
const i = fieldsFor(s
|
|
3970
|
+
const i = fieldsFor(s, !!event).indexOf(f);
|
|
3305
3971
|
if (i < 0) return;
|
|
3306
|
-
if (i === s.focus && (f
|
|
3972
|
+
if (i === s.focus && PICKERS.has(f)) return pick(s, f, 1);
|
|
3307
3973
|
update({ focus: i });
|
|
3308
3974
|
};
|
|
3309
3975
|
useKeys((input, key3) => {
|
|
3976
|
+
if (key3.escape && asking.current && !busy.current) return setAskScope(false);
|
|
3310
3977
|
if (key3.escape) return onClose();
|
|
3311
3978
|
const s = latest.current;
|
|
3312
3979
|
if (!s || busy.current) return;
|
|
3980
|
+
if (asking.current) {
|
|
3981
|
+
if (!Object.hasOwn(SCOPE_KEYS2, input) || asking.current === "rule" && SCOPE_KEYS2[input] === "one") return;
|
|
3982
|
+
return void save(SCOPE_KEYS2[input]);
|
|
3983
|
+
}
|
|
3313
3984
|
if (key3.ctrl && input === "s") return void save();
|
|
3314
|
-
const fields2 = fieldsFor(s
|
|
3985
|
+
const fields2 = fieldsFor(s, !!event);
|
|
3315
3986
|
const i = Math.min(s.focus, fields2.length - 1);
|
|
3316
3987
|
const field = fields2[i];
|
|
3317
3988
|
const move = (dir) => update({ focus: (i + dir + fields2.length) % fields2.length });
|
|
@@ -3319,19 +3990,20 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3319
3990
|
if (key3.downArrow) return move(1);
|
|
3320
3991
|
if (key3.upArrow) return move(-1);
|
|
3321
3992
|
if (key3.return) return i === fields2.length - 1 ? void save() : move(1);
|
|
3322
|
-
if (field
|
|
3993
|
+
if (PICKERS.has(field)) {
|
|
3323
3994
|
if (key3.leftArrow) pick(s, field, -1);
|
|
3324
3995
|
else if (key3.rightArrow || input === " ") pick(s, field, 1);
|
|
3325
3996
|
return;
|
|
3326
3997
|
}
|
|
3327
|
-
|
|
3998
|
+
const text2 = field;
|
|
3999
|
+
if (key3.backspace || key3.delete) return edit(s, text2, s.texts[text2].slice(0, -1));
|
|
3328
4000
|
if (key3.ctrl || key3.meta || !input) return;
|
|
3329
4001
|
const printable = input.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
3330
|
-
if (printable) edit(s,
|
|
4002
|
+
if (printable) edit(s, text2, s.texts[text2] + printable);
|
|
3331
4003
|
});
|
|
3332
4004
|
if (!state) return /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "Loading\u2026" });
|
|
3333
4005
|
const { form, texts } = state;
|
|
3334
|
-
const fields = fieldsFor(
|
|
4006
|
+
const fields = fieldsFor(state, !!event);
|
|
3335
4007
|
const current = fields[Math.min(state.focus, fields.length - 1)];
|
|
3336
4008
|
const account = accounts.find((a) => a.id === form.accountId);
|
|
3337
4009
|
const calendar = calendars.find((c) => c.accountId === form.accountId && c.id === form.calendarId);
|
|
@@ -3346,6 +4018,11 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3346
4018
|
return calendar ? { text: calendar.name } : { text: "\u2039 choose calendar \u203A", dim: true };
|
|
3347
4019
|
case "allDay":
|
|
3348
4020
|
return { text: form.allDay ? "[x]" : "[ ]" };
|
|
4021
|
+
case "repeat":
|
|
4022
|
+
if (form.repeatWas === void 0) return { text: repeatFailed ? "couldn\u2019t load; stays as it is" : "loading\u2026", dim: true };
|
|
4023
|
+
if (form.repeat?.rule) return { text: describe(form.repeat, form.start) };
|
|
4024
|
+
if (isCustom(state)) return { text: "Custom" };
|
|
4025
|
+
return { text: presets(form.start).find((p) => p.id === presetOf(form.repeat, form.start)).label };
|
|
3349
4026
|
default:
|
|
3350
4027
|
return texts[f] ? { text: texts[f] } : { text: HINTS[f] ?? "", dim: true };
|
|
3351
4028
|
}
|
|
@@ -3356,7 +4033,7 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3356
4033
|
rows.map((f) => {
|
|
3357
4034
|
const on = f === current;
|
|
3358
4035
|
const v = value(f);
|
|
3359
|
-
const picker = !event && (f === "account" || f === "calendar");
|
|
4036
|
+
const picker = !event && (f === "account" || f === "calendar") || f === "repeat";
|
|
3360
4037
|
return /* @__PURE__ */ jsxs6(Clickable, { onClick: () => clickField(f), children: [
|
|
3361
4038
|
/* @__PURE__ */ jsxs6(Text6, { color: on ? C.cyan : void 0, children: [
|
|
3362
4039
|
on ? "\u203A " : " ",
|
|
@@ -3368,7 +4045,13 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3368
4045
|
}),
|
|
3369
4046
|
/* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: account ? `Organizer: ${account.email}` : "Choose which account this event belongs to." }) }),
|
|
3370
4047
|
error && /* @__PURE__ */ jsx8(Text6, { color: C.red, children: error }),
|
|
3371
|
-
saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
|
|
4048
|
+
saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : askScope ? /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
|
|
4049
|
+
/* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: askScope === "rule" ? "Change the repeat rule for: " : "Save recurring event: " }),
|
|
4050
|
+
askScope !== "rule" && /* @__PURE__ */ jsx8(Button, { k: "1", label: "this event", color: C.green, onPress: () => void save("one") }),
|
|
4051
|
+
/* @__PURE__ */ jsx8(Button, { k: "2", label: "this and following", color: C.green, onPress: () => void save("following") }),
|
|
4052
|
+
/* @__PURE__ */ jsx8(Button, { k: "3", label: "all events", color: C.green, onPress: () => void save("all") }),
|
|
4053
|
+
/* @__PURE__ */ jsx8(Button, { k: "esc", label: "back", onPress: () => setAskScope(false) })
|
|
4054
|
+
] }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
|
|
3372
4055
|
/* @__PURE__ */ jsx8(Box6, { marginRight: 2, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "tab/\u2191\u2193 move \xB7 \u2190/\u2192/space pick" }) }),
|
|
3373
4056
|
/* @__PURE__ */ jsx8(Button, { k: "ctrl+s", label: "save", color: C.green, onPress: () => void save() }),
|
|
3374
4057
|
/* @__PURE__ */ jsx8(Button, { k: "esc", label: "cancel", onPress: onClose })
|
|
@@ -3391,12 +4074,12 @@ import { Box as Box8, Text as Text8 } from "ink";
|
|
|
3391
4074
|
// src/ui/screens/Invites.tsx
|
|
3392
4075
|
import { useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
|
|
3393
4076
|
import { Box as Box7, Text as Text7 } from "ink";
|
|
3394
|
-
import { addDays as
|
|
4077
|
+
import { addDays as addDays10, startOfDay as startOfDay6 } from "date-fns";
|
|
3395
4078
|
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3396
4079
|
var INVITE_DAYS = 60;
|
|
3397
4080
|
function useUpcoming(now) {
|
|
3398
4081
|
const day = startOfDay6(now).getTime();
|
|
3399
|
-
const range = useMemo5(() => ({ start: new Date(day).toISOString(), end:
|
|
4082
|
+
const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays10(day, INVITE_DAYS).toISOString() }), [day]);
|
|
3400
4083
|
return useEvents(range);
|
|
3401
4084
|
}
|
|
3402
4085
|
var REPLY = { y: "accepted", n: "declined", m: "tentative" };
|
|
@@ -3574,12 +4257,12 @@ var NAMES = {
|
|
|
3574
4257
|
};
|
|
3575
4258
|
function useKeyState(init) {
|
|
3576
4259
|
const ref = useRef5(init);
|
|
3577
|
-
const [,
|
|
4260
|
+
const [, bump2] = useState6(0);
|
|
3578
4261
|
return [
|
|
3579
4262
|
() => ref.current,
|
|
3580
4263
|
(next) => {
|
|
3581
4264
|
ref.current = next;
|
|
3582
|
-
|
|
4265
|
+
bump2((n) => n + 1);
|
|
3583
4266
|
}
|
|
3584
4267
|
];
|
|
3585
4268
|
}
|
|
@@ -3996,9 +4679,9 @@ function FirstSyncCheck({ width, height, children }) {
|
|
|
3996
4679
|
] }) });
|
|
3997
4680
|
}
|
|
3998
4681
|
function title({ view, date }) {
|
|
3999
|
-
if (view === "month") return
|
|
4000
|
-
if (view === "day") return
|
|
4001
|
-
const days = view === "agenda" ? [date,
|
|
4682
|
+
if (view === "month") return format13(date, "MMMM yyyy");
|
|
4683
|
+
if (view === "day") return format13(date, "EEE d MMM yyyy");
|
|
4684
|
+
const days = view === "agenda" ? [date, addDays11(date, AGENDA_DAYS - 1)] : view === "2day" ? [date, addDays11(date, 1)] : viewDays(view, date);
|
|
4002
4685
|
return rangeLabel(days[0], days[days.length - 1]);
|
|
4003
4686
|
}
|
|
4004
4687
|
var chronological = (a, b) => eventBounds(a).start.getTime() - eventBounds(b).start.getTime() || Number(b.allDay) - Number(a.allDay);
|
|
@@ -4076,8 +4759,8 @@ function Shell({ initialNav, updateCheck, notice }) {
|
|
|
4076
4759
|
const { cursor: from, view, date, key: key3 } = live.current;
|
|
4077
4760
|
const sel = ordered2.find((e) => eventKey(e) === key3);
|
|
4078
4761
|
const minute = sel ? sel.allDay ? void 0 : Math.max(differenceInMinutes3(eventBounds(sel).start, from), 0) : live.current.minute;
|
|
4079
|
-
const day =
|
|
4080
|
-
const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ?
|
|
4762
|
+
const day = addDays11(from, n);
|
|
4763
|
+
const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ? addDays11(day, -1) : day;
|
|
4081
4764
|
Object.assign(live.current, { cursor: day, date: nextDate, minute });
|
|
4082
4765
|
setCursor(day);
|
|
4083
4766
|
if (+nextDate !== +date) setDate(nextDate);
|