mysticals 0.3.0 → 0.4.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 +753 -143
- 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.4.0";
|
|
9
9
|
|
|
10
10
|
// src/client.ts
|
|
11
11
|
import { spawn } from "child_process";
|
|
@@ -53,6 +53,7 @@ var methodSet = {
|
|
|
53
53
|
"events.update": true,
|
|
54
54
|
"events.delete": true,
|
|
55
55
|
"events.respond": true,
|
|
56
|
+
"events.recurrence": true,
|
|
56
57
|
"sync.now": true
|
|
57
58
|
};
|
|
58
59
|
var METHODS = Object.keys(methodSet);
|
|
@@ -474,6 +475,15 @@ var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
|
|
474
475
|
var text = (max2) => z.string().max(max2);
|
|
475
476
|
var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
|
|
476
477
|
var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
|
|
478
|
+
var Recurrence = z.object({
|
|
479
|
+
freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
|
|
480
|
+
interval: z.number().int().min(1).max(999).optional(),
|
|
481
|
+
byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
|
|
482
|
+
until: z.iso.date().optional(),
|
|
483
|
+
count: z.number().int().min(1).max(999).optional(),
|
|
484
|
+
// An RRULE value kept from the provider: one line of plain RRULE parts only.
|
|
485
|
+
rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
|
|
486
|
+
}).strict();
|
|
477
487
|
var NewEvent = z.object({
|
|
478
488
|
accountId: id,
|
|
479
489
|
calendarId: id,
|
|
@@ -483,7 +493,8 @@ var NewEvent = z.object({
|
|
|
483
493
|
allDay: z.boolean(),
|
|
484
494
|
location: text(1e3).optional(),
|
|
485
495
|
description: text(2e4).optional(),
|
|
486
|
-
attendees: z.array(z.email()).max(500).optional()
|
|
496
|
+
attendees: z.array(z.email()).max(500).optional(),
|
|
497
|
+
recurrence: Recurrence.optional()
|
|
487
498
|
}).refine(ordered, "end before start");
|
|
488
499
|
var Attendee = z.object({
|
|
489
500
|
email: text(320).min(1),
|
|
@@ -500,7 +511,9 @@ var EventEdit = EventRef.extend({
|
|
|
500
511
|
allDay: z.boolean(),
|
|
501
512
|
location: text(1e3).optional(),
|
|
502
513
|
description: text(2e4).optional(),
|
|
503
|
-
attendees: z.array(Attendee).max(500)
|
|
514
|
+
attendees: z.array(Attendee).max(500),
|
|
515
|
+
// Absent keeps the series' rule; null stops repeating.
|
|
516
|
+
recurrence: Recurrence.nullish()
|
|
504
517
|
}).refine(ordered, "end before start");
|
|
505
518
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
|
|
506
519
|
var CaldavInput = z.object({
|
|
@@ -516,7 +529,7 @@ var CaldavInput = z.object({
|
|
|
516
529
|
});
|
|
517
530
|
var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
|
|
518
531
|
var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
|
|
519
|
-
var Scope = z.enum(["one", "following", "all"]).
|
|
532
|
+
var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
|
|
520
533
|
function createApi(store, sync, deps) {
|
|
521
534
|
const account = (accountId) => {
|
|
522
535
|
const a = store.get(id.parse(accountId));
|
|
@@ -612,12 +625,19 @@ function createApi(store, sync, deps) {
|
|
|
612
625
|
syncOne(a.id);
|
|
613
626
|
return ev;
|
|
614
627
|
},
|
|
615
|
-
update: async (raw) => {
|
|
628
|
+
update: async (raw, scope) => {
|
|
616
629
|
const edit = EventEdit.parse(raw);
|
|
617
630
|
const cached2 = writable(cachedEvent(edit));
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
-
|
|
631
|
+
const how = cached2.recurringEventId ? Scope.parse(scope) : "one";
|
|
632
|
+
const { title: title2, start, end, allDay, location, description, attendees, recurrence } = edit;
|
|
633
|
+
if (recurrence !== void 0 && cached2.recurringEventId && how === "one")
|
|
634
|
+
throw new Error("The repeat rule belongs to the series: choose \u201CThis and following\u201D or \u201CAll events\u201D");
|
|
635
|
+
const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees, ...recurrence !== void 0 ? { recurrence } : {} }, how);
|
|
636
|
+
if (how === "one") upsert(ev, cached2.accountId, cached2.calendarId);
|
|
637
|
+
else {
|
|
638
|
+
const own = { ...ev, accountId: cached2.accountId, calendarId: cached2.calendarId };
|
|
639
|
+
applyLocal(cached2.accountId, (events) => [...events.filter((e) => e.id !== own.id && !deletedBy(cached2, how, e)), own]);
|
|
640
|
+
}
|
|
621
641
|
syncOne(cached2.accountId);
|
|
622
642
|
return ev;
|
|
623
643
|
},
|
|
@@ -628,6 +648,10 @@ function createApi(store, sync, deps) {
|
|
|
628
648
|
applyLocal(cached2.accountId, (events) => events.filter((e) => !deletedBy(cached2, how, e)));
|
|
629
649
|
syncOne(cached2.accountId);
|
|
630
650
|
},
|
|
651
|
+
recurrence: async (raw) => {
|
|
652
|
+
const cached2 = cachedEvent(EventRef.parse(raw));
|
|
653
|
+
return store.getProvider(cached2.accountId).getRecurrence(cached2);
|
|
654
|
+
},
|
|
631
655
|
respond: async (raw, status) => {
|
|
632
656
|
const cached2 = cachedEvent(EventRef.parse(raw));
|
|
633
657
|
const ev = await store.getProvider(cached2.accountId).respond(cached2, RsvpStatus.parse(status));
|
|
@@ -829,11 +853,171 @@ var AccountStore = class {
|
|
|
829
853
|
};
|
|
830
854
|
|
|
831
855
|
// ../../packages/core/src/mock/mockApi.ts
|
|
832
|
-
import { addDays, format as
|
|
856
|
+
import { addDays as addDays2, format as format4 } from "date-fns";
|
|
833
857
|
|
|
834
858
|
// ../../packages/core/src/mock/MockProvider.ts
|
|
835
859
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
836
|
-
import { parseISO as
|
|
860
|
+
import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format3, parseISO as parseISO4, startOfWeek } from "date-fns";
|
|
861
|
+
|
|
862
|
+
// ../../packages/core/src/logic/recurrence.ts
|
|
863
|
+
import { format as format2, isValid, parseISO as parseISO3 } from "date-fns";
|
|
864
|
+
var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
|
865
|
+
var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
|
866
|
+
var SHORT = { MO: "Mon", TU: "Tue", WE: "Wed", TH: "Thu", FR: "Fri", SA: "Sat", SU: "Sun" };
|
|
867
|
+
var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
|
|
868
|
+
var FREQS = ["daily", "weekly", "monthly", "yearly"];
|
|
869
|
+
var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
|
|
870
|
+
var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
|
|
871
|
+
var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
|
|
872
|
+
var shortDay = (d) => SHORT[d];
|
|
873
|
+
function normalize(r, start) {
|
|
874
|
+
if (r.rule) return { freq: r.freq, rule: r.rule };
|
|
875
|
+
const out = { freq: r.freq };
|
|
876
|
+
if (r.interval && r.interval > 1) out.interval = r.interval;
|
|
877
|
+
if (r.freq === "weekly" && r.byDay?.length) {
|
|
878
|
+
const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
|
|
879
|
+
if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
|
|
880
|
+
}
|
|
881
|
+
if (r.until) out.until = r.until;
|
|
882
|
+
else if (r.count) out.count = r.count;
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
function sameRecurrence(a, b, start) {
|
|
886
|
+
if (!a || !b) return a === b;
|
|
887
|
+
return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
|
|
888
|
+
}
|
|
889
|
+
function toRRule(r, allDay) {
|
|
890
|
+
if (r.rule) return r.rule;
|
|
891
|
+
const parts = [`FREQ=${r.freq.toUpperCase()}`];
|
|
892
|
+
if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
|
|
893
|
+
if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
|
|
894
|
+
if (r.until) {
|
|
895
|
+
const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
|
|
896
|
+
parts.push(`UNTIL=${until}`);
|
|
897
|
+
} else if (r.count) parts.push(`COUNT=${r.count}`);
|
|
898
|
+
return parts.join(";");
|
|
899
|
+
}
|
|
900
|
+
var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
|
|
901
|
+
function fromRRule(text2, start) {
|
|
902
|
+
const rule = text2.replace(/^RRULE:/i, "").trim();
|
|
903
|
+
const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
|
|
904
|
+
const i = p.indexOf("=");
|
|
905
|
+
return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
|
|
906
|
+
}));
|
|
907
|
+
const freq = (kv.get("FREQ") ?? "").toLowerCase();
|
|
908
|
+
const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
|
|
909
|
+
if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
|
|
910
|
+
const out = { freq };
|
|
911
|
+
const interval = Number(kv.get("INTERVAL") ?? 1);
|
|
912
|
+
if (!Number.isInteger(interval) || interval < 1) return custom();
|
|
913
|
+
if (interval > 1) out.interval = interval;
|
|
914
|
+
if (kv.has("BYDAY")) {
|
|
915
|
+
const days = kv.get("BYDAY").split(",");
|
|
916
|
+
if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
|
|
917
|
+
out.byDay = WEEKDAYS.filter((d) => days.includes(d));
|
|
918
|
+
}
|
|
919
|
+
const until = kv.get("UNTIL");
|
|
920
|
+
if (until) {
|
|
921
|
+
const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
|
|
922
|
+
if (!m) return custom();
|
|
923
|
+
if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
|
|
924
|
+
else {
|
|
925
|
+
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]);
|
|
926
|
+
const s = start && start.length > 10 ? asDate(start) : void 0;
|
|
927
|
+
const minutes = (d) => d.getHours() * 60 + d.getMinutes();
|
|
928
|
+
if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
|
|
929
|
+
out.until = format2(at3, "yyyy-MM-dd");
|
|
930
|
+
}
|
|
931
|
+
} else if (kv.has("COUNT")) {
|
|
932
|
+
const count = Number(kv.get("COUNT"));
|
|
933
|
+
if (!Number.isInteger(count) || count < 1) return custom();
|
|
934
|
+
out.count = count;
|
|
935
|
+
}
|
|
936
|
+
return out;
|
|
937
|
+
}
|
|
938
|
+
function presets(start) {
|
|
939
|
+
const d = asDate(start);
|
|
940
|
+
return [
|
|
941
|
+
{ id: "none", label: "Does not repeat", recurrence: null },
|
|
942
|
+
{ id: "daily", label: "Every day", recurrence: { freq: "daily" } },
|
|
943
|
+
{ id: "weekly", label: `Every week on ${format2(d, "EEEE")}`, recurrence: { freq: "weekly" } },
|
|
944
|
+
{ id: "monthly", label: `Every month on day ${d.getDate()}`, recurrence: { freq: "monthly" } },
|
|
945
|
+
{ id: "yearly", label: `Every year on ${format2(d, "d MMMM")}`, recurrence: { freq: "yearly" } },
|
|
946
|
+
{ id: "weekdays", label: "Every weekday (Mon\u2013Fri)", recurrence: { freq: "weekly", byDay: WORKWEEK } }
|
|
947
|
+
];
|
|
948
|
+
}
|
|
949
|
+
function presetOf(r, start) {
|
|
950
|
+
if (!r) return "none";
|
|
951
|
+
if (r.rule) return "custom";
|
|
952
|
+
const { until: _u, count: _c, ...base } = normalize(r, start);
|
|
953
|
+
const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
|
|
954
|
+
return hit?.id ?? "custom";
|
|
955
|
+
}
|
|
956
|
+
var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
|
|
957
|
+
function describe(r, start) {
|
|
958
|
+
if (!r) return "Does not repeat";
|
|
959
|
+
if (r.rule) return `Custom rule (${r.rule})`;
|
|
960
|
+
const n = normalize(r, start);
|
|
961
|
+
const d = asDate(start);
|
|
962
|
+
const every = n.interval ? `Every ${n.interval} ${UNIT[n.freq]}s` : `Every ${UNIT[n.freq]}`;
|
|
963
|
+
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")}` : "";
|
|
964
|
+
const head = n.freq === "weekly" && !n.interval && n.byDay?.join() === WORKWEEK.join() ? "Every weekday (Mon\u2013Fri)" : `${every}${on}`;
|
|
965
|
+
const end = n.until ? ` \xB7 until ${format2(parseISO3(n.until), "d MMM yyyy")}` : n.count ? ` \xB7 ${n.count} time${n.count > 1 ? "s" : ""}` : "";
|
|
966
|
+
return head + end;
|
|
967
|
+
}
|
|
968
|
+
var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
|
|
969
|
+
function parseEvery(text2) {
|
|
970
|
+
const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
|
|
971
|
+
const n = Number(m?.[1] ?? 1);
|
|
972
|
+
if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
|
|
973
|
+
const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
|
|
974
|
+
return { freq, interval: n > 1 ? n : void 0 };
|
|
975
|
+
}
|
|
976
|
+
var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
|
|
977
|
+
function parseDays(text2) {
|
|
978
|
+
const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
|
|
979
|
+
const bad = words.filter((w) => !WEEKDAYS.includes(w));
|
|
980
|
+
if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
|
|
981
|
+
return WEEKDAYS.filter((d) => words.includes(d));
|
|
982
|
+
}
|
|
983
|
+
var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
|
|
984
|
+
function parseEnds(text2) {
|
|
985
|
+
const t = text2.trim().toLowerCase();
|
|
986
|
+
if (!t || t === "never") return {};
|
|
987
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(t) && isValid(parseISO3(t)) && format2(parseISO3(t), "yyyy-MM-dd") === t) return { until: t };
|
|
988
|
+
const m = /^(\d+)(\s*times?)?$/.exec(t);
|
|
989
|
+
if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
|
|
990
|
+
throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ../../packages/core/src/mock/MockProvider.ts
|
|
994
|
+
var MAX_INSTANCES = 60;
|
|
995
|
+
function expand(first, r, series) {
|
|
996
|
+
const start = parseISO4(first.start);
|
|
997
|
+
const length = parseISO4(first.end).getTime() - start.getTime();
|
|
998
|
+
const every = r.interval ?? 1;
|
|
999
|
+
const days = r.byDay?.length ? r.byDay : [weekdayOf(first.start)];
|
|
1000
|
+
const until = r.until ? parseISO4(r.until) : void 0;
|
|
1001
|
+
const hits = (d) => {
|
|
1002
|
+
const n = differenceInCalendarDays(d, start);
|
|
1003
|
+
if (r.freq === "daily") return n % every === 0;
|
|
1004
|
+
if (r.freq === "weekly")
|
|
1005
|
+
return days.includes(weekdayOf(format3(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
|
|
1006
|
+
const months = differenceInCalendarMonths(d, start);
|
|
1007
|
+
if (d.getDate() !== start.getDate()) return false;
|
|
1008
|
+
return r.freq === "monthly" ? months % every === 0 : months % (12 * every) === 0;
|
|
1009
|
+
};
|
|
1010
|
+
const out = [];
|
|
1011
|
+
for (let n = 0; n < 3 * 366 && out.length < (r.count ?? MAX_INSTANCES) && out.length < MAX_INSTANCES; n++) {
|
|
1012
|
+
const d = addDays(start, n);
|
|
1013
|
+
if (until && differenceInCalendarDays(d, until) > 0) break;
|
|
1014
|
+
if (!hits(d)) continue;
|
|
1015
|
+
const s = first.allDay ? format3(d, "yyyy-MM-dd") : d.toISOString();
|
|
1016
|
+
const e = first.allDay ? format3(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
|
|
1017
|
+
out.push({ ...first, id: `${series}@${s}`, start: s, end: e, recurringEventId: series, recurrence: void 0 });
|
|
1018
|
+
}
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
837
1021
|
var MockProvider = class {
|
|
838
1022
|
constructor(accountId, email, calendars) {
|
|
839
1023
|
this.accountId = accountId;
|
|
@@ -844,11 +1028,13 @@ var MockProvider = class {
|
|
|
844
1028
|
email;
|
|
845
1029
|
calendars;
|
|
846
1030
|
events = [];
|
|
1031
|
+
/** Series id -> its rule. */
|
|
1032
|
+
rules = /* @__PURE__ */ new Map();
|
|
847
1033
|
async listCalendars() {
|
|
848
1034
|
return structuredClone(this.calendars);
|
|
849
1035
|
}
|
|
850
1036
|
async listEvents(calendarId, range) {
|
|
851
|
-
const t = (iso2) =>
|
|
1037
|
+
const t = (iso2) => parseISO4(iso2).getTime();
|
|
852
1038
|
return structuredClone(
|
|
853
1039
|
this.events.filter((e) => e.calendarId === calendarId && t(e.end) > t(range.start) && t(e.start) < t(range.end))
|
|
854
1040
|
);
|
|
@@ -867,12 +1053,45 @@ var MockProvider = class {
|
|
|
867
1053
|
organizer: { email: this.email },
|
|
868
1054
|
attendees: (input.attendees ?? []).map((email) => ({ email, status: "needsAction" }))
|
|
869
1055
|
};
|
|
1056
|
+
if (input.recurrence) return this.startSeries(ev, input.recurrence, ev.id);
|
|
870
1057
|
this.events.push(ev);
|
|
871
1058
|
return structuredClone(ev);
|
|
872
1059
|
}
|
|
873
|
-
|
|
1060
|
+
/** Add the series `first` starts under `r` (or `first` alone for null); returns its first event. */
|
|
1061
|
+
startSeries(first, r, series) {
|
|
1062
|
+
const evs = r ? expand(first, r, series) : [{ ...first, recurringEventId: void 0, recurrence: void 0 }];
|
|
1063
|
+
if (r) this.rules.set(series, r);
|
|
1064
|
+
this.events.push(...evs);
|
|
1065
|
+
return structuredClone(evs[0]);
|
|
1066
|
+
}
|
|
1067
|
+
async getRecurrence(event) {
|
|
1068
|
+
return event.recurringEventId && this.rules.get(event.recurringEventId) || null;
|
|
1069
|
+
}
|
|
1070
|
+
async updateEvent(event, scope = "one") {
|
|
874
1071
|
const i = this.events.findIndex((e) => e.id === event.id);
|
|
875
1072
|
if (i < 0) throw new Error("not found");
|
|
1073
|
+
const old = this.events[i];
|
|
1074
|
+
const series = old.recurringEventId;
|
|
1075
|
+
if (event.recurrence !== void 0 && (!series || scope !== "one")) {
|
|
1076
|
+
const own = (e) => e.id === old.id || !!series && e.recurringEventId === series && (scope === "all" || e.start >= old.start);
|
|
1077
|
+
const firstOld = series && scope === "all" ? this.events.filter(own).sort((a, b) => a.start.localeCompare(b.start))[0] : old;
|
|
1078
|
+
this.events = this.events.filter((e) => !own(e));
|
|
1079
|
+
const shift = Date.parse(event.start) - Date.parse(old.start);
|
|
1080
|
+
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() } : {};
|
|
1081
|
+
const id2 = scope === "all" && series ? series : randomUUID3();
|
|
1082
|
+
return this.startSeries({ ...event, ...anchor, accountId: this.accountId }, event.recurrence, id2);
|
|
1083
|
+
}
|
|
1084
|
+
if (series && scope !== "one") {
|
|
1085
|
+
const shift = Date.parse(event.start) - Date.parse(old.start);
|
|
1086
|
+
const length = Date.parse(event.end) - Date.parse(event.start);
|
|
1087
|
+
const at3 = (ms) => new Date(ms).toISOString();
|
|
1088
|
+
const { title: title2, location, description, attendees } = event;
|
|
1089
|
+
this.events = this.events.map((e) => {
|
|
1090
|
+
if (e.id === event.id || e.recurringEventId !== series || scope === "following" && e.start < old.start) return e;
|
|
1091
|
+
const moved = old.allDay || event.allDay ? {} : { start: at3(Date.parse(e.start) + shift), end: at3(Date.parse(e.start) + shift + length) };
|
|
1092
|
+
return { ...e, title: title2, location, description, attendees, ...moved };
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
876
1095
|
this.events[i] = { ...event, accountId: this.accountId };
|
|
877
1096
|
return structuredClone(this.events[i]);
|
|
878
1097
|
}
|
|
@@ -942,8 +1161,9 @@ function createMockApi(onChanged) {
|
|
|
942
1161
|
for (let d = -2; d <= 4; d++) {
|
|
943
1162
|
seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
|
|
944
1163
|
}
|
|
1164
|
+
personal.rules.set("run-series", { freq: "daily", count: 7 });
|
|
945
1165
|
seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
|
|
946
|
-
const day = (offset) =>
|
|
1166
|
+
const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
|
|
947
1167
|
seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
|
|
948
1168
|
const prov = (id2) => {
|
|
949
1169
|
const p = providers.get(id2);
|
|
@@ -999,8 +1219,8 @@ function createMockApi(onChanged) {
|
|
|
999
1219
|
onChanged(input.accountId);
|
|
1000
1220
|
return ev;
|
|
1001
1221
|
},
|
|
1002
|
-
update: async (ev) => {
|
|
1003
|
-
const r = await own(ev.accountId, ev.calendarId).updateEvent(ev);
|
|
1222
|
+
update: async (ev, scope) => {
|
|
1223
|
+
const r = await own(ev.accountId, ev.calendarId).updateEvent(ev, scope);
|
|
1004
1224
|
onChanged(ev.accountId);
|
|
1005
1225
|
return r;
|
|
1006
1226
|
},
|
|
@@ -1008,6 +1228,7 @@ function createMockApi(onChanged) {
|
|
|
1008
1228
|
await own(ev.accountId, ev.calendarId).deleteEvent(ev, scope);
|
|
1009
1229
|
onChanged(ev.accountId);
|
|
1010
1230
|
},
|
|
1231
|
+
recurrence: async (ev) => own(ev.accountId, ev.calendarId).getRecurrence(ev),
|
|
1011
1232
|
respond: async (ev, status) => {
|
|
1012
1233
|
const r = await own(ev.accountId, ev.calendarId).respond(ev, status);
|
|
1013
1234
|
onChanged(ev.accountId);
|
|
@@ -1034,7 +1255,7 @@ import {
|
|
|
1034
1255
|
|
|
1035
1256
|
// ../../packages/core/src/providers/caldav/ics.ts
|
|
1036
1257
|
import ICAL from "ical.js";
|
|
1037
|
-
var
|
|
1258
|
+
var MAX_INSTANCES2 = 1e5;
|
|
1038
1259
|
var PARTSTAT_IN = {
|
|
1039
1260
|
ACCEPTED: "accepted",
|
|
1040
1261
|
DECLINED: "declined",
|
|
@@ -1132,7 +1353,7 @@ function parseEvents(ics, href, etag, ctx, range) {
|
|
|
1132
1353
|
-Infinity,
|
|
1133
1354
|
...vevents.filter((v) => v !== master).map((v) => v.getFirstPropertyValue("recurrence-id").toJSDate().getTime())
|
|
1134
1355
|
);
|
|
1135
|
-
for (let i = 0, next = it.next(); next && i <
|
|
1356
|
+
for (let i = 0, next = it.next(); next && i < MAX_INSTANCES2; i++, next = it.next()) {
|
|
1136
1357
|
const ms = next.toJSDate().getTime();
|
|
1137
1358
|
if (ms >= rangeEnd && ms > lastOverride) break;
|
|
1138
1359
|
const d = event.getOccurrenceDetails(next);
|
|
@@ -1155,9 +1376,40 @@ function buildIcs(uid, input, email) {
|
|
|
1155
1376
|
setText(v, "location", input.location);
|
|
1156
1377
|
setText(v, "description", input.description);
|
|
1157
1378
|
setAttendees(v, input.attendees ?? [], email);
|
|
1379
|
+
if (input.recurrence) setRule(v, input.recurrence, input.allDay);
|
|
1158
1380
|
root.addSubcomponent(v);
|
|
1159
1381
|
return root.toString();
|
|
1160
1382
|
}
|
|
1383
|
+
function setRule(v, r, allDay) {
|
|
1384
|
+
v.removeAllProperties("rrule");
|
|
1385
|
+
v.addPropertyWithValue("rrule", ICAL.Recur.fromString(toRRule(r, allDay)));
|
|
1386
|
+
}
|
|
1387
|
+
function recurrenceOf(raw) {
|
|
1388
|
+
const master = findMaster(parse(raw.ics));
|
|
1389
|
+
const rule = master?.getFirstPropertyValue("rrule");
|
|
1390
|
+
return master && rule ? fromRRule(rule.toString(), toIso(dtstart(master))) : null;
|
|
1391
|
+
}
|
|
1392
|
+
function applyRule(root, master, event) {
|
|
1393
|
+
if (event.recurrence === void 0) return;
|
|
1394
|
+
const overrides = root.getAllSubcomponents("vevent").filter((v) => v !== master);
|
|
1395
|
+
if (event.recurrence === null) {
|
|
1396
|
+
for (const n of ["rrule", "rdate", "exdate"]) master.removeAllProperties(n);
|
|
1397
|
+
for (const o of overrides) root.removeSubcomponent(o);
|
|
1398
|
+
master.addProperty(replaceTime(master, "dtstart", fromIso(event.start, event.allDay)));
|
|
1399
|
+
master.addProperty(replaceTime(master, "dtend", fromIso(event.end, event.allDay)));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
setRule(master, event.recurrence, event.allDay);
|
|
1403
|
+
const ms = (t) => t.toJSDate().getTime();
|
|
1404
|
+
const rid = (o) => o.getFirstPropertyValue("recurrence-id");
|
|
1405
|
+
const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t) => t instanceof ICAL.Time);
|
|
1406
|
+
const last = Math.max(-Infinity, ...overrides.map((o) => ms(rid(o))), ...exdates.map(ms));
|
|
1407
|
+
const slots2 = /* @__PURE__ */ new Set();
|
|
1408
|
+
const it = master.getFirstPropertyValue("rrule").iterator(dtstart(master));
|
|
1409
|
+
for (let t = it.next(), n = 0; t && ms(t) <= last && n < MAX_INSTANCES2; t = it.next(), n++) slots2.add(ms(t));
|
|
1410
|
+
for (const o of overrides) if (!slots2.has(ms(rid(o)))) root.removeSubcomponent(o);
|
|
1411
|
+
mapDates(master, "exdate", (t) => slots2.has(ms(t)) ? t : null);
|
|
1412
|
+
}
|
|
1161
1413
|
function setAttendees(v, emails, email) {
|
|
1162
1414
|
const wanted = [...new Set(emails.map(cleanEmail).filter(Boolean))];
|
|
1163
1415
|
const existing = new Map(v.getAllProperties("attendee").map((p) => [cleanEmail(p.getFirstValue()), p]));
|
|
@@ -1217,9 +1469,14 @@ function applyUpdate(event, email) {
|
|
|
1217
1469
|
const raw = event.raw;
|
|
1218
1470
|
const root = parse(raw.ics);
|
|
1219
1471
|
const v = targetVevent(root, raw);
|
|
1220
|
-
|
|
1472
|
+
writeEdit(v, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
|
|
1473
|
+
if (event.recurrence && !raw.recurrenceId && !v.hasProperty("rrule")) setRule(v, event.recurrence, event.allDay);
|
|
1474
|
+
return root.toString();
|
|
1475
|
+
}
|
|
1476
|
+
function writeEdit(v, event, email, start, end) {
|
|
1477
|
+
v.addProperty(replaceTime(v, "dtstart", start));
|
|
1221
1478
|
v.removeAllProperties("duration");
|
|
1222
|
-
|
|
1479
|
+
v.addProperty(replaceTime(v, "dtend", end));
|
|
1223
1480
|
setText(v, "summary", event.title);
|
|
1224
1481
|
setText(v, "location", event.location);
|
|
1225
1482
|
setText(v, "description", event.description);
|
|
@@ -1228,10 +1485,156 @@ function applyUpdate(event, email) {
|
|
|
1228
1485
|
event.attendees.map((a) => a.email),
|
|
1229
1486
|
email
|
|
1230
1487
|
);
|
|
1488
|
+
bump(v);
|
|
1489
|
+
}
|
|
1490
|
+
function bump(v) {
|
|
1231
1491
|
v.updatePropertyWithValue("sequence", Number(v.getFirstPropertyValue("sequence") ?? 0) + 1);
|
|
1232
1492
|
touch(v);
|
|
1493
|
+
}
|
|
1494
|
+
function timeProp(name, t) {
|
|
1495
|
+
const p = new ICAL.Property(name);
|
|
1496
|
+
p.setValue(t);
|
|
1497
|
+
const tzid = t.zone?.tzid;
|
|
1498
|
+
if (!t.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
|
|
1499
|
+
return p;
|
|
1500
|
+
}
|
|
1501
|
+
function replaceTime(v, name, t) {
|
|
1502
|
+
v.removeAllProperties(name);
|
|
1503
|
+
return timeProp(name, t);
|
|
1504
|
+
}
|
|
1505
|
+
var dtstart = (v) => v.getFirstPropertyValue("dtstart");
|
|
1506
|
+
var dateOf = (t) => ICAL.Time.fromData({ year: t.year, month: t.month, day: t.day, isDate: true });
|
|
1507
|
+
var daysBetween = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
|
|
1508
|
+
function addDays3(t, days) {
|
|
1509
|
+
const x = t.clone();
|
|
1510
|
+
x.adjust(days, 0, 0, 0);
|
|
1511
|
+
return x;
|
|
1512
|
+
}
|
|
1513
|
+
function spanFrom(start, event) {
|
|
1514
|
+
if (event.allDay) return [start, addDays3(start, daysBetween(fromIso(event.start, true), fromIso(event.end, true)))];
|
|
1515
|
+
const end = start.clone();
|
|
1516
|
+
end.adjust(0, 0, 0, (Date.parse(event.end) - Date.parse(event.start)) / 1e3);
|
|
1517
|
+
return [start, end];
|
|
1518
|
+
}
|
|
1519
|
+
function slotShift(master, before, event) {
|
|
1520
|
+
const anchor = dtstart(master);
|
|
1521
|
+
const zone = anchor.isDate ? void 0 : anchor.zone;
|
|
1522
|
+
const local = (t) => zone && !t.isDate ? t.convertToZone(zone) : t.clone();
|
|
1523
|
+
const after = fromIso(event.start, event.allDay);
|
|
1524
|
+
if (event.allDay) {
|
|
1525
|
+
const days2 = daysBetween(local(before), after);
|
|
1526
|
+
return (t) => addDays3(dateOf(local(t)), days2);
|
|
1527
|
+
}
|
|
1528
|
+
if (!anchor.isDate) {
|
|
1529
|
+
const seconds = (zone ? after.convertToZone(zone) : after).subtractDate(local(before)).toSeconds();
|
|
1530
|
+
return (t) => {
|
|
1531
|
+
const x = local(t);
|
|
1532
|
+
x.adjust(0, 0, 0, seconds);
|
|
1533
|
+
return x;
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const days = daysBetween(before, after);
|
|
1537
|
+
return (t) => {
|
|
1538
|
+
const d = addDays3(dateOf(t), days);
|
|
1539
|
+
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);
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function mapDates(v, name, map) {
|
|
1543
|
+
const props = v.getAllProperties(name);
|
|
1544
|
+
if (!props.length) return;
|
|
1545
|
+
const out = [];
|
|
1546
|
+
for (const p of props) {
|
|
1547
|
+
for (const value of p.getValues()) {
|
|
1548
|
+
if (!(value instanceof ICAL.Time)) {
|
|
1549
|
+
const keep = new ICAL.Property(name);
|
|
1550
|
+
keep.setValue(value);
|
|
1551
|
+
out.push(keep);
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
const t = map(value);
|
|
1555
|
+
if (t) out.push(timeProp(name, t));
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
v.removeAllProperties(name);
|
|
1559
|
+
for (const p of out) v.addProperty(p);
|
|
1560
|
+
}
|
|
1561
|
+
var attendeeKey = (v) => v.getAllProperties("attendee").map((p) => cleanEmail(p.getFirstValue())).sort().join(",");
|
|
1562
|
+
function rewriteSeries(root, master, edited, event, email, shift, anchor) {
|
|
1563
|
+
const shared = ["summary", "location", "description"].map((n) => [n, master.getFirstPropertyValue(n)]);
|
|
1564
|
+
const people = attendeeKey(master);
|
|
1565
|
+
const retyped = dtstart(master).isDate !== event.allDay;
|
|
1566
|
+
const [start, end] = spanFrom(shift(anchor), event);
|
|
1567
|
+
writeEdit(master, event, email, start, end);
|
|
1568
|
+
mapDates(master, "exdate", shift);
|
|
1569
|
+
mapDates(master, "rdate", shift);
|
|
1570
|
+
for (const o of root.getAllSubcomponents("vevent")) {
|
|
1571
|
+
const rid = o.getFirstPropertyValue("recurrence-id");
|
|
1572
|
+
if (!rid) continue;
|
|
1573
|
+
o.addProperty(replaceTime(o, "recurrence-id", shift(rid)));
|
|
1574
|
+
if (o === edited) {
|
|
1575
|
+
writeEdit(o, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const [s, e] = retyped ? spanFrom(shift(dtstart(o)), event) : [shift(dtstart(o)), shift(new ICAL.Event(o).endDate)];
|
|
1579
|
+
o.addProperty(replaceTime(o, "dtstart", s));
|
|
1580
|
+
o.removeAllProperties("duration");
|
|
1581
|
+
o.addProperty(replaceTime(o, "dtend", e));
|
|
1582
|
+
for (const [name, old] of shared) {
|
|
1583
|
+
if (String(o.getFirstPropertyValue(name) ?? "") === String(old ?? "")) setText(o, name, event[name === "summary" ? "title" : name]);
|
|
1584
|
+
}
|
|
1585
|
+
if (attendeeKey(o) === people) setAttendees(o, event.attendees.map((a) => a.email), email);
|
|
1586
|
+
bump(o);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
var instanceStart = (master, override, rid) => override ? dtstart(override) : ridTime(master, rid).time;
|
|
1590
|
+
function applyUpdateSeries(event, email) {
|
|
1591
|
+
const raw = event.raw;
|
|
1592
|
+
const root = parse(raw.ics);
|
|
1593
|
+
const master = findMaster(root);
|
|
1594
|
+
if (!raw.recurrenceId || !master) return applyUpdate(event, email);
|
|
1595
|
+
const edited = findOverride(root, raw.recurrenceId);
|
|
1596
|
+
const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
|
|
1597
|
+
rewriteSeries(root, master, edited, event, email, shift, dtstart(master));
|
|
1598
|
+
applyRule(root, master, event);
|
|
1233
1599
|
return root.toString();
|
|
1234
1600
|
}
|
|
1601
|
+
function applySplitFollowing(event, email, uid) {
|
|
1602
|
+
const raw = event.raw;
|
|
1603
|
+
if (!raw.recurrenceId) throw new Error("Not a recurring instance");
|
|
1604
|
+
const rest = applyDeleteFollowing(raw);
|
|
1605
|
+
if (!rest) return null;
|
|
1606
|
+
const root = parse(raw.ics);
|
|
1607
|
+
const master = masterOf(root);
|
|
1608
|
+
const slot = ridTime(master, raw.recurrenceId).time;
|
|
1609
|
+
const cut = slot.toJSDate().getTime();
|
|
1610
|
+
const edited = findOverride(root, raw.recurrenceId);
|
|
1611
|
+
const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
|
|
1612
|
+
for (const v of root.getAllSubcomponents("vevent")) {
|
|
1613
|
+
const t = v.getFirstPropertyValue("recurrence-id");
|
|
1614
|
+
if (t && t.toJSDate().getTime() < cut) root.removeSubcomponent(v);
|
|
1615
|
+
}
|
|
1616
|
+
const later = (t) => t.toJSDate().getTime() >= cut ? t : null;
|
|
1617
|
+
mapDates(master, "exdate", later);
|
|
1618
|
+
mapDates(master, "rdate", later);
|
|
1619
|
+
const first = dtstart(master);
|
|
1620
|
+
for (const p of event.recurrence === void 0 ? master.getAllProperties("rrule") : []) {
|
|
1621
|
+
const r = p.getFirstValue().clone();
|
|
1622
|
+
if (!r.count) continue;
|
|
1623
|
+
const it = r.iterator(first);
|
|
1624
|
+
let passed = 0;
|
|
1625
|
+
for (let t = it.next(); t && t.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t = it.next()) passed++;
|
|
1626
|
+
r.count = Math.max(1, r.count - passed);
|
|
1627
|
+
p.setValue(r);
|
|
1628
|
+
}
|
|
1629
|
+
for (const v of root.getAllSubcomponents("vevent")) {
|
|
1630
|
+
v.updatePropertyWithValue("uid", uid);
|
|
1631
|
+
v.updatePropertyWithValue("sequence", -1);
|
|
1632
|
+
v.updatePropertyWithValue("created", ICAL.Time.now());
|
|
1633
|
+
}
|
|
1634
|
+
rewriteSeries(root, master, edited, event, email, shift, slot);
|
|
1635
|
+
applyRule(root, master, event);
|
|
1636
|
+
return { rest, next: root.toString() };
|
|
1637
|
+
}
|
|
1235
1638
|
function applyDeleteInstance(raw) {
|
|
1236
1639
|
if (!raw.recurrenceId) throw new Error("Not a recurring instance");
|
|
1237
1640
|
const root = parse(raw.ics);
|
|
@@ -1378,7 +1781,8 @@ var createCaldavProvider = (ctx) => {
|
|
|
1378
1781
|
if (!obj?.data) throw new Error("Saved event could not be read back from the server");
|
|
1379
1782
|
const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
|
|
1380
1783
|
const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
|
|
1381
|
-
const
|
|
1784
|
+
const at3 = Date.parse(around.start);
|
|
1785
|
+
const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
|
|
1382
1786
|
if (!ev) throw new Error("Saved event not found on the server");
|
|
1383
1787
|
return ev;
|
|
1384
1788
|
}
|
|
@@ -1406,9 +1810,24 @@ var createCaldavProvider = (ctx) => {
|
|
|
1406
1810
|
const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
|
|
1407
1811
|
return reload(calendarId, href, void 0, input);
|
|
1408
1812
|
},
|
|
1409
|
-
async updateEvent(event) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1813
|
+
async updateEvent(event, scope = "one") {
|
|
1814
|
+
const raw = event.raw;
|
|
1815
|
+
if (!raw.recurrenceId || scope === "one") {
|
|
1816
|
+
await put(event, applyUpdate(event, ctx.email), "Update event");
|
|
1817
|
+
return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
|
|
1818
|
+
}
|
|
1819
|
+
const uid = randomUUID4();
|
|
1820
|
+
const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
|
|
1821
|
+
if (!split) {
|
|
1822
|
+
await put(event, applyUpdateSeries(event, ctx.email), "Update event");
|
|
1823
|
+
return reload(event.calendarId, raw.href, void 0, event);
|
|
1824
|
+
}
|
|
1825
|
+
await put(event, split.rest, "Update event");
|
|
1826
|
+
const { headers } = await getConn();
|
|
1827
|
+
const filename = `${uid}.ics`;
|
|
1828
|
+
check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: timedFetch }), "Update event");
|
|
1829
|
+
const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
|
|
1830
|
+
return reload(event.calendarId, href, void 0, event);
|
|
1412
1831
|
},
|
|
1413
1832
|
async deleteEvent(event, scope = "one") {
|
|
1414
1833
|
const raw = event.raw;
|
|
@@ -1417,6 +1836,9 @@ var createCaldavProvider = (ctx) => {
|
|
|
1417
1836
|
const { headers } = await getConn();
|
|
1418
1837
|
check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: timedFetch }), "Delete event");
|
|
1419
1838
|
},
|
|
1839
|
+
async getRecurrence(event) {
|
|
1840
|
+
return recurrenceOf(event.raw);
|
|
1841
|
+
},
|
|
1420
1842
|
async respond(event, status) {
|
|
1421
1843
|
await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
|
|
1422
1844
|
return reload(event.calendarId, event.raw.href, event.id, event);
|
|
@@ -1440,7 +1862,7 @@ import { createHash as createHash2, randomBytes } from "crypto";
|
|
|
1440
1862
|
import { createServer } from "http";
|
|
1441
1863
|
var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
1442
1864
|
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
1443
|
-
var SCOPES = "openid email https://www.googleapis.com/auth/calendar";
|
|
1865
|
+
var SCOPES = "openid email https://www.googleapis.com/auth/calendar.calendarlist.readonly https://www.googleapis.com/auth/calendar.events";
|
|
1444
1866
|
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1445
1867
|
var clientConfig = {};
|
|
1446
1868
|
function setClientConfig(cfg) {
|
|
@@ -1607,7 +2029,29 @@ function truncateRecurrence(recurrence, cutoff) {
|
|
|
1607
2029
|
return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
|
|
1608
2030
|
});
|
|
1609
2031
|
}
|
|
2032
|
+
var DAY2 = 864e5;
|
|
2033
|
+
var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY2).toISOString().slice(0, 10);
|
|
2034
|
+
var daysBetween2 = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY2);
|
|
2035
|
+
var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
|
|
2036
|
+
function shiftAnchor(anchor, before, start, allDay, timeZone) {
|
|
2037
|
+
if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween2(before.date ?? before.dateTime, start)) };
|
|
2038
|
+
if (anchor.dateTime && before.dateTime) {
|
|
2039
|
+
return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
|
|
2040
|
+
}
|
|
2041
|
+
const at3 = new Date(start);
|
|
2042
|
+
const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween2(before.date, localDate(at3)))}T00:00:00`);
|
|
2043
|
+
day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
|
|
2044
|
+
return { dateTime: day.toISOString(), timeZone };
|
|
2045
|
+
}
|
|
2046
|
+
function endFrom(from, event, timeZone) {
|
|
2047
|
+
if (event.allDay) return { date: plusDays(from.date, daysBetween2(event.start, event.end)) };
|
|
2048
|
+
return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
|
|
2049
|
+
}
|
|
1610
2050
|
var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
|
|
2051
|
+
var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
|
|
2052
|
+
var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2053
|
+
var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
|
|
2054
|
+
var nulled = (t) => ({ date: null, dateTime: null, timeZone: null, ...t });
|
|
1611
2055
|
function createGoogleProviderImpl(ctx) {
|
|
1612
2056
|
if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
|
|
1613
2057
|
let creds = ctx.credentials;
|
|
@@ -1645,6 +2089,35 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1645
2089
|
return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
|
|
1646
2090
|
};
|
|
1647
2091
|
const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
|
|
2092
|
+
const fields = (event, prevAttendees, organizer) => {
|
|
2093
|
+
const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
|
|
2094
|
+
if (organizer) {
|
|
2095
|
+
const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
|
|
2096
|
+
body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
|
|
2097
|
+
}
|
|
2098
|
+
return body;
|
|
2099
|
+
};
|
|
2100
|
+
async function remainingRecurrence(calendarId, series, recurrence, slot) {
|
|
2101
|
+
const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
|
|
2102
|
+
if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
|
|
2103
|
+
const cut = Date.parse(slot.dateTime ?? slot.date);
|
|
2104
|
+
let passed = 0;
|
|
2105
|
+
let pageToken;
|
|
2106
|
+
do {
|
|
2107
|
+
const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
|
|
2108
|
+
showDeleted: "true",
|
|
2109
|
+
timeMax: new Date(cut).toISOString(),
|
|
2110
|
+
maxResults: "2500",
|
|
2111
|
+
...pageToken ? { pageToken } : {}
|
|
2112
|
+
});
|
|
2113
|
+
for (const g of page.items ?? []) {
|
|
2114
|
+
const t = g.originalStartTime ?? g.start;
|
|
2115
|
+
if (Date.parse(t.dateTime ?? t.date) < cut) passed++;
|
|
2116
|
+
}
|
|
2117
|
+
pageToken = page.nextPageToken;
|
|
2118
|
+
} while (pageToken);
|
|
2119
|
+
return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
|
|
2120
|
+
}
|
|
1648
2121
|
return {
|
|
1649
2122
|
async listCalendars() {
|
|
1650
2123
|
const out = [];
|
|
@@ -1685,36 +2158,67 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1685
2158
|
},
|
|
1686
2159
|
async createEvent(calendarId, input) {
|
|
1687
2160
|
const attendees = (input.attendees ?? []).map((email) => ({ email }));
|
|
2161
|
+
const rule = input.recurrence;
|
|
1688
2162
|
const g = await api(
|
|
1689
2163
|
"POST",
|
|
1690
2164
|
eventPath(calendarId),
|
|
1691
2165
|
{ sendUpdates: attendees.length ? "all" : "none" },
|
|
1692
2166
|
{
|
|
1693
2167
|
summary: input.title,
|
|
1694
|
-
start: toGTime(input.start, input.allDay),
|
|
1695
|
-
end: toGTime(input.end, input.allDay),
|
|
2168
|
+
start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
|
|
2169
|
+
end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
|
|
1696
2170
|
location: input.location,
|
|
1697
2171
|
description: input.description,
|
|
1698
|
-
attendees
|
|
2172
|
+
attendees,
|
|
2173
|
+
...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
|
|
1699
2174
|
}
|
|
1700
2175
|
);
|
|
1701
2176
|
return map(g, calendarId);
|
|
1702
2177
|
},
|
|
1703
|
-
async updateEvent(event) {
|
|
2178
|
+
async updateEvent(event, scope = "one") {
|
|
1704
2179
|
const organizer = isOrganizer(event);
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2180
|
+
const q = { sendUpdates: organizer ? "all" : "none" };
|
|
2181
|
+
const series = event.recurringEventId;
|
|
2182
|
+
const before = rawOf(event)?.start;
|
|
2183
|
+
if (!series || scope === "one" || !before) {
|
|
2184
|
+
const rule2 = !series ? event.recurrence : void 0;
|
|
2185
|
+
const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
|
|
2186
|
+
const body = {
|
|
2187
|
+
...fields(event, rawOf(event)?.attendees, organizer),
|
|
2188
|
+
start: time(event.start),
|
|
2189
|
+
end: time(event.end),
|
|
2190
|
+
...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
|
|
2191
|
+
};
|
|
2192
|
+
return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
|
|
1715
2193
|
}
|
|
1716
|
-
const
|
|
1717
|
-
|
|
2194
|
+
const master = await api("GET", eventPath(event.calendarId, series));
|
|
2195
|
+
const timeZone = master.start.timeZone ?? localZone();
|
|
2196
|
+
const rule = event.recurrence;
|
|
2197
|
+
const slot = rawOf(event)?.originalStartTime ?? before;
|
|
2198
|
+
const first = master.start.dateTime ?? master.start.date;
|
|
2199
|
+
const at3 = slot.dateTime ?? slot.date;
|
|
2200
|
+
if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
|
|
2201
|
+
const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
|
|
2202
|
+
const body = {
|
|
2203
|
+
...fields(event, master.attendees, organizer),
|
|
2204
|
+
start: nulled(start2),
|
|
2205
|
+
end: nulled(endFrom(start2, event, timeZone)),
|
|
2206
|
+
// A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
|
|
2207
|
+
...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
|
|
2208
|
+
};
|
|
2209
|
+
await api("PATCH", eventPath(event.calendarId, series), q, body);
|
|
2210
|
+
return event;
|
|
2211
|
+
}
|
|
2212
|
+
const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
|
|
2213
|
+
await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
|
|
2214
|
+
const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
|
|
2215
|
+
const created = await api("POST", eventPath(event.calendarId), q, {
|
|
2216
|
+
...fields(event, master.attendees, organizer),
|
|
2217
|
+
start,
|
|
2218
|
+
end: endFrom(start, event, timeZone),
|
|
2219
|
+
...recurrence ? { recurrence } : {}
|
|
2220
|
+
});
|
|
2221
|
+
return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
|
|
1718
2222
|
},
|
|
1719
2223
|
async deleteEvent(event, scope = "one") {
|
|
1720
2224
|
const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
|
|
@@ -1732,6 +2236,12 @@ function createGoogleProviderImpl(ctx) {
|
|
|
1732
2236
|
}
|
|
1733
2237
|
await api("DELETE", eventPath(event.calendarId, series), q);
|
|
1734
2238
|
},
|
|
2239
|
+
async getRecurrence(event) {
|
|
2240
|
+
if (!event.recurringEventId) return null;
|
|
2241
|
+
const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
|
|
2242
|
+
const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
|
|
2243
|
+
return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
|
|
2244
|
+
},
|
|
1735
2245
|
async respond(event, status) {
|
|
1736
2246
|
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
2247
|
if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
|
|
@@ -2070,32 +2580,32 @@ async function runDaemon() {
|
|
|
2070
2580
|
// src/ui/hooks.ts
|
|
2071
2581
|
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
2072
2582
|
import { useStdout } from "ink";
|
|
2073
|
-
import { addDays as
|
|
2583
|
+
import { addDays as addDays5, addWeeks as addWeeks2, startOfDay as startOfDay2 } from "date-fns";
|
|
2074
2584
|
|
|
2075
2585
|
// ../../packages/core/src/logic/layout.ts
|
|
2076
2586
|
import {
|
|
2077
|
-
addDays as
|
|
2587
|
+
addDays as addDays4,
|
|
2078
2588
|
addMonths as addMonths2,
|
|
2079
2589
|
addWeeks,
|
|
2080
|
-
format as
|
|
2590
|
+
format as format5,
|
|
2081
2591
|
isSameDay as isSameDay2,
|
|
2082
|
-
parseISO as
|
|
2592
|
+
parseISO as parseISO5,
|
|
2083
2593
|
startOfDay,
|
|
2084
2594
|
startOfMonth,
|
|
2085
|
-
startOfWeek
|
|
2595
|
+
startOfWeek as startOfWeek2
|
|
2086
2596
|
} from "date-fns";
|
|
2087
2597
|
var WEEK_STARTS_ON = 1;
|
|
2088
2598
|
function eventBounds(e) {
|
|
2089
|
-
const start =
|
|
2090
|
-
let end =
|
|
2091
|
-
if (end <= start) end = e.allDay ?
|
|
2599
|
+
const start = parseISO5(e.start);
|
|
2600
|
+
let end = parseISO5(e.end);
|
|
2601
|
+
if (end <= start) end = e.allDay ? addDays4(start, 1) : new Date(start.getTime() + 6e4);
|
|
2092
2602
|
return { start, end };
|
|
2093
2603
|
}
|
|
2094
2604
|
var isPast = (e, now) => eventBounds(e).end.getTime() <= +now;
|
|
2095
2605
|
function overlapsDay(e, day) {
|
|
2096
2606
|
const d0 = startOfDay(day);
|
|
2097
2607
|
const { start, end } = eventBounds(e);
|
|
2098
|
-
return start <
|
|
2608
|
+
return start < addDays4(d0, 1) && end > d0;
|
|
2099
2609
|
}
|
|
2100
2610
|
function eventsOnDay(events, day) {
|
|
2101
2611
|
return events.filter((e) => overlapsDay(e, day)).sort((a, b) => {
|
|
@@ -2142,31 +2652,31 @@ function layoutDay(events, day, minDur = 0) {
|
|
|
2142
2652
|
return packColumns(items, minDur);
|
|
2143
2653
|
}
|
|
2144
2654
|
function monthGrid(date) {
|
|
2145
|
-
const first =
|
|
2146
|
-
return Array.from({ length: 42 }, (_, i) =>
|
|
2655
|
+
const first = startOfWeek2(startOfMonth(date), { weekStartsOn: WEEK_STARTS_ON });
|
|
2656
|
+
return Array.from({ length: 42 }, (_, i) => addDays4(first, i));
|
|
2147
2657
|
}
|
|
2148
2658
|
function viewDays(view, date) {
|
|
2149
2659
|
if (view === "month") return monthGrid(date);
|
|
2150
2660
|
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) =>
|
|
2661
|
+
if (view === "3day") return [0, 1, 2].map((i) => addDays4(startOfDay(date), i));
|
|
2662
|
+
const first = startOfWeek2(date, { weekStartsOn: WEEK_STARTS_ON });
|
|
2663
|
+
return Array.from({ length: 7 }, (_, i) => addDays4(first, i));
|
|
2154
2664
|
}
|
|
2155
2665
|
function viewRange(view, date) {
|
|
2156
2666
|
const days = viewDays(view, date);
|
|
2157
|
-
return { start: days[0].toISOString(), end:
|
|
2667
|
+
return { start: days[0].toISOString(), end: addDays4(days[days.length - 1], 1).toISOString() };
|
|
2158
2668
|
}
|
|
2159
2669
|
function shiftDate(view, date, dir) {
|
|
2160
|
-
if (view === "day") return
|
|
2161
|
-
if (view === "3day") return
|
|
2670
|
+
if (view === "day") return addDays4(date, dir);
|
|
2671
|
+
if (view === "3day") return addDays4(date, 3 * dir);
|
|
2162
2672
|
if (view === "week") return addWeeks(date, dir);
|
|
2163
2673
|
return addMonths2(date, dir);
|
|
2164
2674
|
}
|
|
2165
2675
|
function rangeLabel(a, b) {
|
|
2166
2676
|
const head = a.getFullYear() !== b.getFullYear() ? "d MMM yyyy" : a.getMonth() !== b.getMonth() ? "d MMM" : "d";
|
|
2167
|
-
return `${
|
|
2677
|
+
return `${format5(a, head)} \u2013 ${format5(b, "d MMM yyyy")}`;
|
|
2168
2678
|
}
|
|
2169
|
-
var ymd = (d) =>
|
|
2679
|
+
var ymd = (d) => format5(d, "yyyy-MM-dd");
|
|
2170
2680
|
|
|
2171
2681
|
// ../../packages/core/src/logic/visible.ts
|
|
2172
2682
|
var key2 = (accountId, calendarId) => `${accountId}/${calendarId}`;
|
|
@@ -2186,7 +2696,7 @@ var eventKey = (e) => `${e.accountId}/${e.calendarId}/${e.id}`;
|
|
|
2186
2696
|
function useLive(load, deps) {
|
|
2187
2697
|
const api = useApi();
|
|
2188
2698
|
const [state, setState] = useState({});
|
|
2189
|
-
const [
|
|
2699
|
+
const [bump2, setBump] = useState(0);
|
|
2190
2700
|
useEffect(() => {
|
|
2191
2701
|
let seq = 0;
|
|
2192
2702
|
let alive2 = true;
|
|
@@ -2203,7 +2713,7 @@ function useLive(load, deps) {
|
|
|
2203
2713
|
alive2 = false;
|
|
2204
2714
|
off();
|
|
2205
2715
|
};
|
|
2206
|
-
}, [api,
|
|
2716
|
+
}, [api, bump2, ...deps]);
|
|
2207
2717
|
return { ...state, reload: useCallback(() => setBump((b) => b + 1), []) };
|
|
2208
2718
|
}
|
|
2209
2719
|
function useDirectory() {
|
|
@@ -2224,13 +2734,13 @@ var AGENDA_DAYS = 14;
|
|
|
2224
2734
|
function navRange({ view, date }) {
|
|
2225
2735
|
if (view === "agenda" || view === "2day") {
|
|
2226
2736
|
const start = startOfDay2(date);
|
|
2227
|
-
return { start: start.toISOString(), end:
|
|
2737
|
+
return { start: start.toISOString(), end: addDays5(start, view === "agenda" ? AGENDA_DAYS : 2).toISOString() };
|
|
2228
2738
|
}
|
|
2229
2739
|
return viewRange(view, date);
|
|
2230
2740
|
}
|
|
2231
2741
|
var shiftNav = ({ view, date }, dir) => ({
|
|
2232
2742
|
view,
|
|
2233
|
-
date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ?
|
|
2743
|
+
date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ? addDays5(date, 2 * dir) : shiftDate(view, date, dir)
|
|
2234
2744
|
});
|
|
2235
2745
|
function useNav(initial = { view: "agenda", date: /* @__PURE__ */ new Date() }) {
|
|
2236
2746
|
const [nav, setNav] = useState(initial);
|
|
@@ -2265,12 +2775,12 @@ function useTermSize() {
|
|
|
2265
2775
|
// src/ui/App.tsx
|
|
2266
2776
|
import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
|
|
2267
2777
|
import { Box as Box11, Text as Text11, useApp, useStdin as useStdin2 } from "ink";
|
|
2268
|
-
import { addDays as
|
|
2778
|
+
import { addDays as addDays11, differenceInMinutes as differenceInMinutes3, format as format13, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
|
|
2269
2779
|
|
|
2270
2780
|
// ../../packages/core/src/logic/editor.ts
|
|
2271
|
-
import { addDays as
|
|
2781
|
+
import { addDays as addDays6, addHours, format as format6, parseISO as parseISO6, startOfHour } from "date-fns";
|
|
2272
2782
|
var LOCAL = "yyyy-MM-dd'T'HH:mm";
|
|
2273
|
-
var toLocalInput = (iso2) =>
|
|
2783
|
+
var toLocalInput = (iso2) => format6(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
|
|
2274
2784
|
var fromLocalInput = (local) => new Date(local).toISOString();
|
|
2275
2785
|
var writableCalendars = (calendars, accountId) => calendars.filter((c) => c.accountId === accountId && !c.readOnly);
|
|
2276
2786
|
var writableAccounts = (accounts, calendars) => accounts.filter((a) => writableCalendars(calendars, a.id).length > 0);
|
|
@@ -2280,8 +2790,8 @@ var splitEmails = (text2) => text2.split(/[\s,;]+/).map((s) => s.trim()).filter(
|
|
|
2280
2790
|
var isEmail = (s) => EMAIL.test(s);
|
|
2281
2791
|
function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new Date()) {
|
|
2282
2792
|
const accountId = soleId(writableAccounts(accounts, calendars));
|
|
2283
|
-
const start = prefill.start ? toLocalInput(prefill.start) :
|
|
2284
|
-
let end = prefill.end ? toLocalInput(prefill.end) :
|
|
2793
|
+
const start = prefill.start ? toLocalInput(prefill.start) : format6(startOfHour(addHours(now, 1)), LOCAL);
|
|
2794
|
+
let end = prefill.end ? toLocalInput(prefill.end) : format6(addHours(new Date(start), 1), LOCAL);
|
|
2285
2795
|
if (prefill.allDay && prefill.end) end = inclusiveEnd(start, end);
|
|
2286
2796
|
return {
|
|
2287
2797
|
accountId,
|
|
@@ -2292,11 +2802,13 @@ function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new
|
|
|
2292
2802
|
allDay: !!prefill.allDay,
|
|
2293
2803
|
location: "",
|
|
2294
2804
|
description: "",
|
|
2295
|
-
attendees: []
|
|
2805
|
+
attendees: [],
|
|
2806
|
+
repeat: null,
|
|
2807
|
+
repeatWas: null
|
|
2296
2808
|
};
|
|
2297
2809
|
}
|
|
2298
2810
|
var inclusiveEnd = (startLocal, endLocal) => {
|
|
2299
|
-
const d =
|
|
2811
|
+
const d = format6(addDays6(new Date(endLocal), -1), LOCAL);
|
|
2300
2812
|
return d.slice(0, 10) < startLocal.slice(0, 10) ? startLocal : d;
|
|
2301
2813
|
};
|
|
2302
2814
|
function formFromEvent(e) {
|
|
@@ -2311,23 +2823,28 @@ function formFromEvent(e) {
|
|
|
2311
2823
|
allDay: e.allDay,
|
|
2312
2824
|
location: e.location ?? "",
|
|
2313
2825
|
description: e.description ?? "",
|
|
2314
|
-
attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email)
|
|
2826
|
+
attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email),
|
|
2827
|
+
repeat: null,
|
|
2828
|
+
// A series' rule isn't cached: the editor loads it (events.recurrence) and sets both fields.
|
|
2829
|
+
repeatWas: e.recurringEventId ? void 0 : null
|
|
2315
2830
|
};
|
|
2316
2831
|
}
|
|
2832
|
+
var withLoadedRepeat = (f, rule) => ({ ...f, repeat: rule, repeatWas: rule });
|
|
2833
|
+
var repeatChanged = (f) => f.repeatWas !== void 0 && !sameRecurrence(f.repeat, f.repeatWas, f.start);
|
|
2317
2834
|
function setAllDay(f, allDay) {
|
|
2318
2835
|
if (allDay || f.end > f.start) return { ...f, allDay };
|
|
2319
|
-
return { ...f, allDay, end:
|
|
2836
|
+
return { ...f, allDay, end: format6(addHours(new Date(f.start), 1), LOCAL) };
|
|
2320
2837
|
}
|
|
2321
2838
|
function moveStart(f, start) {
|
|
2322
2839
|
const end = new Date(new Date(f.end).getTime() + new Date(start).getTime() - new Date(f.start).getTime());
|
|
2323
|
-
return { ...f, start, end:
|
|
2840
|
+
return { ...f, start, end: format6(end, LOCAL) };
|
|
2324
2841
|
}
|
|
2325
2842
|
function times(f) {
|
|
2326
2843
|
if (f.allDay) {
|
|
2327
2844
|
const start2 = f.start.slice(0, 10);
|
|
2328
2845
|
const endIncl = f.end.slice(0, 10);
|
|
2329
2846
|
if (endIncl < start2) throw new Error("End date is before start date");
|
|
2330
|
-
return { allDay: true, start: start2, end:
|
|
2847
|
+
return { allDay: true, start: start2, end: format6(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
|
|
2331
2848
|
}
|
|
2332
2849
|
const start = fromLocalInput(f.start);
|
|
2333
2850
|
const end = fromLocalInput(f.end);
|
|
@@ -2350,7 +2867,8 @@ function formToInput(f) {
|
|
|
2350
2867
|
...times(f),
|
|
2351
2868
|
location: f.location.trim() || void 0,
|
|
2352
2869
|
description: f.description.trim() || void 0,
|
|
2353
|
-
...attendees.length ? { attendees } : {}
|
|
2870
|
+
...attendees.length ? { attendees } : {},
|
|
2871
|
+
...f.repeat ? { recurrence: f.repeat } : {}
|
|
2354
2872
|
};
|
|
2355
2873
|
}
|
|
2356
2874
|
function applyForm(e, f) {
|
|
@@ -2364,7 +2882,9 @@ function applyForm(e, f) {
|
|
|
2364
2882
|
...times(f),
|
|
2365
2883
|
location: f.location.trim() || void 0,
|
|
2366
2884
|
description: f.description.trim() || void 0,
|
|
2367
|
-
attendees: [...keep, ...invited]
|
|
2885
|
+
attendees: [...keep, ...invited],
|
|
2886
|
+
// Only a changed rule is sent; absent keeps the series' rule.
|
|
2887
|
+
...repeatChanged(f) ? { recurrence: f.repeat } : {}
|
|
2368
2888
|
};
|
|
2369
2889
|
}
|
|
2370
2890
|
var errorText = (e) => String(e instanceof Error ? e.message : e).replace(/^Error invoking remote method '[^']+': (\w*Error: )?/, "");
|
|
@@ -2483,10 +3003,10 @@ function Button({ k, label, onPress, color: color2 = C.accent }) {
|
|
|
2483
3003
|
// src/ui/views/Agenda.tsx
|
|
2484
3004
|
import { useMemo, useReducer, useRef as useRef2 } from "react";
|
|
2485
3005
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
2486
|
-
import { addDays as
|
|
3006
|
+
import { addDays as addDays8, differenceInCalendarDays as differenceInCalendarDays2, differenceInMinutes as differenceInMinutes2, format as format9, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
|
|
2487
3007
|
|
|
2488
3008
|
// ../../packages/core/src/logic/details.ts
|
|
2489
|
-
import { addDays as
|
|
3009
|
+
import { addDays as addDays7, format as format7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
|
|
2490
3010
|
var same = (a, b) => !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
2491
3011
|
function ownerLine(calendar, label, email) {
|
|
2492
3012
|
return { calendar: same(calendar, label) || same(calendar, email) ? void 0 : calendar, label, email: same(email, label) ? void 0 : email };
|
|
@@ -2496,17 +3016,17 @@ function canEdit(e, account, calendar) {
|
|
|
2496
3016
|
if (e.attendees.length === 0) return true;
|
|
2497
3017
|
return same(e.organizer?.email, account.email) || e.attendees.some((a) => a.self && a.organizer);
|
|
2498
3018
|
}
|
|
2499
|
-
var at2 = (iso2, allDay) => (allDay ?
|
|
3019
|
+
var at2 = (iso2, allDay) => (allDay ? parseISO7(iso2) : new Date(iso2)).getTime();
|
|
2500
3020
|
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
3021
|
function formatWhen(e) {
|
|
2502
3022
|
if (e.allDay) {
|
|
2503
|
-
const s2 =
|
|
2504
|
-
const last =
|
|
2505
|
-
return last > s2 ? `${
|
|
3023
|
+
const s2 = parseISO7(e.start);
|
|
3024
|
+
const last = addDays7(parseISO7(e.end), -1);
|
|
3025
|
+
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
3026
|
}
|
|
2507
3027
|
const s = new Date(e.start);
|
|
2508
3028
|
const t = new Date(e.end);
|
|
2509
|
-
return isSameDay3(s, t) ? `${
|
|
3029
|
+
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
3030
|
}
|
|
2511
3031
|
var STATUS_ICON = {
|
|
2512
3032
|
accepted: "\u2713",
|
|
@@ -2527,7 +3047,7 @@ function place(location = "") {
|
|
|
2527
3047
|
}
|
|
2528
3048
|
|
|
2529
3049
|
// ../../packages/core/src/logic/status.ts
|
|
2530
|
-
import { differenceInMinutes, format as
|
|
3050
|
+
import { differenceInMinutes, format as format8 } from "date-fns";
|
|
2531
3051
|
function pickNowNext(events, now) {
|
|
2532
3052
|
const t = now.getTime();
|
|
2533
3053
|
const timed = events.filter((e) => !e.allDay && e.myStatus !== "declined").sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
|
@@ -2539,7 +3059,7 @@ function pickNowNext(events, now) {
|
|
|
2539
3059
|
function startsLabel(start, now) {
|
|
2540
3060
|
const s = new Date(start);
|
|
2541
3061
|
const m = differenceInMinutes(s, now, { roundingMethod: "ceil" });
|
|
2542
|
-
return m <= 60 ? `in ${m}m` : `at ${
|
|
3062
|
+
return m <= 60 ? `in ${m}m` : `at ${format8(s, "HH:mm")}`;
|
|
2543
3063
|
}
|
|
2544
3064
|
|
|
2545
3065
|
// src/ui/screens/EventDetails.tsx
|
|
@@ -2753,7 +3273,7 @@ var span = (m) => {
|
|
|
2753
3273
|
};
|
|
2754
3274
|
function busyMinutes(list, day) {
|
|
2755
3275
|
const from = +startOfDay3(day);
|
|
2756
|
-
const to = +
|
|
3276
|
+
const to = +addDays8(day, 1);
|
|
2757
3277
|
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
3278
|
let total = 0;
|
|
2759
3279
|
let end = -Infinity;
|
|
@@ -2768,7 +3288,7 @@ var overlaps = (e, list) => !e.allDay && e.myStatus !== "declined" && list.some(
|
|
|
2768
3288
|
(o) => o !== e && !o.allDay && o.myStatus !== "declined" && eventBounds(o).start < eventBounds(e).end && eventBounds(e).start < eventBounds(o).end
|
|
2769
3289
|
);
|
|
2770
3290
|
function relativeDay(day, now) {
|
|
2771
|
-
const d =
|
|
3291
|
+
const d = differenceInCalendarDays2(day, now);
|
|
2772
3292
|
return d === 0 ? "today" : d === 1 ? "tomorrow" : d === -1 ? "yesterday" : d > 0 ? `in ${d} days` : `${-d} days ago`;
|
|
2773
3293
|
}
|
|
2774
3294
|
function fitParts(parts, w) {
|
|
@@ -2797,7 +3317,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2797
3317
|
const rows = useMemo(() => {
|
|
2798
3318
|
const out = [];
|
|
2799
3319
|
for (let i = 0; i < AGENDA_DAYS; i++) {
|
|
2800
|
-
const day =
|
|
3320
|
+
const day = addDays8(startOfDay3(date), i);
|
|
2801
3321
|
const list = eventsOnDay(events, day);
|
|
2802
3322
|
if (!list.length) {
|
|
2803
3323
|
const last = out.at(-1);
|
|
@@ -2838,7 +3358,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2838
3358
|
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
3359
|
if ("free" in r) {
|
|
2840
3360
|
const [a, b] = r.free;
|
|
2841
|
-
const label = isSameDay4(a, b) ?
|
|
3361
|
+
const label = isSameDay4(a, b) ? format9(a, "EEE d MMM") : `${format9(a, "EEE d MMM")} \u2013 ${format9(b, "EEE d MMM")}`;
|
|
2842
3362
|
lines.push(
|
|
2843
3363
|
/* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.muted, wrap: "truncate", children: ` ${label} free` }) }, key3)
|
|
2844
3364
|
);
|
|
@@ -2852,7 +3372,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2852
3372
|
}
|
|
2853
3373
|
if ("nowLine" in r) {
|
|
2854
3374
|
lines.push(
|
|
2855
|
-
/* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${
|
|
3375
|
+
/* @__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
3376
|
);
|
|
2857
3377
|
return;
|
|
2858
3378
|
}
|
|
@@ -2860,7 +3380,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2860
3380
|
const today = isSameDay4(r.day, now);
|
|
2861
3381
|
const list = eventsOnDay(events, r.day);
|
|
2862
3382
|
const busy = busyMinutes(list, r.day);
|
|
2863
|
-
const left = ` ${
|
|
3383
|
+
const left = ` ${format9(r.day, "EEE d MMM")} `;
|
|
2864
3384
|
const rel = relativeDay(r.day, now);
|
|
2865
3385
|
const right = ` ${list.length} ${list.length === 1 ? "event" : "events"}${busy ? ` \xB7 ${span(busy)} busy` : ""} `;
|
|
2866
3386
|
const rule = "\u2500".repeat(Math.max(width - left.length - rel.length - right.length - 1, 1));
|
|
@@ -2878,7 +3398,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2878
3398
|
const { start, end } = eventBounds(e);
|
|
2879
3399
|
const running = !e.allDay && start <= now && now < end;
|
|
2880
3400
|
const past = isPast(e, now) || e.myStatus === "declined";
|
|
2881
|
-
const clockOf = (d, edge) => isSameDay4(d, r.day) ?
|
|
3401
|
+
const clockOf = (d, edge) => isSameDay4(d, r.day) ? format9(d, "HH:mm") : +d === +addDays8(r.day, 1) ? "24:00" : edge;
|
|
2882
3402
|
const badges = [];
|
|
2883
3403
|
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
3404
|
else if (e === upNext) badges.push({ text: ` ${startsLabel(e.start, now)}`, color: C.yellow });
|
|
@@ -2916,16 +3436,16 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
|
|
|
2916
3436
|
|
|
2917
3437
|
// src/ui/views/Day.tsx
|
|
2918
3438
|
import { useMemo as useMemo3 } from "react";
|
|
2919
|
-
import { addDays as
|
|
3439
|
+
import { addDays as addDays9, startOfDay as startOfDay5 } from "date-fns";
|
|
2920
3440
|
|
|
2921
3441
|
// src/ui/views/TimeGrid.tsx
|
|
2922
3442
|
import { useMemo as useMemo2 } from "react";
|
|
2923
3443
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
2924
|
-
import { format as
|
|
3444
|
+
import { format as format11, isSameDay as isSameDay6 } from "date-fns";
|
|
2925
3445
|
|
|
2926
3446
|
// src/ui/views/Month.tsx
|
|
2927
3447
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2928
|
-
import { format as
|
|
3448
|
+
import { format as format10, isSameDay as isSameDay5, isSameMonth, startOfDay as startOfDay4 } from "date-fns";
|
|
2929
3449
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2930
3450
|
function fit(items, rows, selected) {
|
|
2931
3451
|
if (items.length <= rows) return { shown: items, more: 0 };
|
|
@@ -2936,7 +3456,7 @@ function fit(items, rows, selected) {
|
|
|
2936
3456
|
}
|
|
2937
3457
|
function EventLine({ e, day, now, selected, time, onSelect, onOpen }) {
|
|
2938
3458
|
const { start, end } = eventBounds(e);
|
|
2939
|
-
const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${
|
|
3459
|
+
const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${format10(start, "HH:mm")} `;
|
|
2940
3460
|
const live = !e.allDay && start <= now && now < end;
|
|
2941
3461
|
return /* @__PURE__ */ jsx4(Clickable, { height: 1, onClick: clickEvent(e, selected, { onSelect, onOpen }), children: /* @__PURE__ */ jsxs4(
|
|
2942
3462
|
Text4,
|
|
@@ -2960,7 +3480,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
|
|
|
2960
3480
|
const cellHeight = Math.max(Math.floor((height - 1) / 6), 1);
|
|
2961
3481
|
const weeks = [0, 1, 2, 3, 4, 5].map((w) => days.slice(w * 7, w * 7 + 7));
|
|
2962
3482
|
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:
|
|
3483
|
+
/* @__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
3484
|
weeks.map((week) => /* @__PURE__ */ jsx4(Box4, { height: cellHeight, children: week.map((day) => {
|
|
2965
3485
|
const items = eventsOnDay(events, day);
|
|
2966
3486
|
const { shown, more } = fit(items, cellHeight - 1, items.findIndex((e) => eventKey(e) === selectedKey));
|
|
@@ -2974,7 +3494,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
|
|
|
2974
3494
|
inverse: here,
|
|
2975
3495
|
color: today ? C.now : here ? C.accent : isSameMonth(day, date) ? void 0 : C.muted,
|
|
2976
3496
|
children: [
|
|
2977
|
-
here ? ` ${
|
|
3497
|
+
here ? ` ${format10(day, "d")} ` : format10(day, "d"),
|
|
2978
3498
|
cellHeight === 1 && items.length > 0 && /* @__PURE__ */ jsxs4(Text4, { color: C.muted, children: [
|
|
2979
3499
|
" \xB7",
|
|
2980
3500
|
items.length
|
|
@@ -3064,10 +3584,10 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3064
3584
|
const selected = k === selectedKey;
|
|
3065
3585
|
const first = p.start / 60;
|
|
3066
3586
|
const { start, end } = eventBounds(e);
|
|
3067
|
-
const from = isSameDay6(start, day) ?
|
|
3587
|
+
const from = isSameDay6(start, day) ? format11(start, "HH:mm") : "\u2026";
|
|
3068
3588
|
const title2 = `${rsvpMark(e)}${e.title || "(no title)"}`;
|
|
3069
3589
|
const where = place(e.location);
|
|
3070
|
-
const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${
|
|
3590
|
+
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
3591
|
const text2 = lines[h - first] ?? "";
|
|
3072
3592
|
const inner = p.col < p.cols - 1 && w > 1 ? w - 1 : w;
|
|
3073
3593
|
const past = isPast(e, now) || e.myStatus === "declined";
|
|
@@ -3095,7 +3615,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3095
3615
|
const isToday = isSameDay6(c.day, now);
|
|
3096
3616
|
const here = !!cursor && isSameDay6(c.day, cursor) && days.length > 1;
|
|
3097
3617
|
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: ` ${
|
|
3618
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${format11(c.day, "EEE d")} ` }),
|
|
3099
3619
|
c.count > 0 && /* @__PURE__ */ jsxs5(Text5, { color: C.muted, children: [
|
|
3100
3620
|
"\xB7",
|
|
3101
3621
|
c.count
|
|
@@ -3125,7 +3645,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3125
3645
|
/* @__PURE__ */ jsx5(Clickable, { flexDirection: "column", width, onWheel: (dir) => scrollBy(dir), children: Array.from({ length: Math.min(gridH, 24) }, (_, i) => {
|
|
3126
3646
|
const h = top + i;
|
|
3127
3647
|
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 ? `${
|
|
3648
|
+
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
3649
|
return row(
|
|
3130
3650
|
`h${h}`,
|
|
3131
3651
|
gutter,
|
|
@@ -3140,7 +3660,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
|
|
|
3140
3660
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
3141
3661
|
function useDays(date, n) {
|
|
3142
3662
|
const t = startOfDay5(date).getTime();
|
|
3143
|
-
return useMemo3(() => Array.from({ length: n }, (_, i) =>
|
|
3663
|
+
return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t, i)), [t, n]);
|
|
3144
3664
|
}
|
|
3145
3665
|
var Day = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 1) });
|
|
3146
3666
|
var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 2) });
|
|
@@ -3157,7 +3677,7 @@ function Week(p) {
|
|
|
3157
3677
|
// src/ui/screens/EventEditor.tsx
|
|
3158
3678
|
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
3159
3679
|
import { Box as Box6, Text as Text6 } from "ink";
|
|
3160
|
-
import { format as
|
|
3680
|
+
import { format as format12, isValid as isValid2, parse as parse2 } from "date-fns";
|
|
3161
3681
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3162
3682
|
var LABELS = {
|
|
3163
3683
|
title: "Title",
|
|
@@ -3170,20 +3690,29 @@ var LABELS = {
|
|
|
3170
3690
|
endTime: "End time",
|
|
3171
3691
|
location: "Location",
|
|
3172
3692
|
description: "Notes",
|
|
3173
|
-
attendees: "Invitees"
|
|
3693
|
+
attendees: "Invitees",
|
|
3694
|
+
repeat: "Repeat",
|
|
3695
|
+
repeatEvery: "Every",
|
|
3696
|
+
repeatDays: "On days",
|
|
3697
|
+
repeatEnds: "Stops"
|
|
3174
3698
|
};
|
|
3699
|
+
var SCOPE_KEYS2 = { "1": "one", "2": "following", "3": "all" };
|
|
3175
3700
|
var HINTS = {
|
|
3176
3701
|
startDate: "YYYY-MM-DD",
|
|
3177
3702
|
startTime: "HH:mm",
|
|
3178
3703
|
endDate: "YYYY-MM-DD",
|
|
3179
3704
|
endTime: "HH:mm",
|
|
3180
3705
|
attendees: "a@x.com, b@y.com",
|
|
3181
|
-
title: "New Event"
|
|
3706
|
+
title: "New Event",
|
|
3707
|
+
repeatEvery: "2 weeks",
|
|
3708
|
+
repeatDays: "mo th",
|
|
3709
|
+
repeatEnds: "never \xB7 2026-12-31 \xB7 10 times"
|
|
3182
3710
|
};
|
|
3711
|
+
var PICKERS = /* @__PURE__ */ new Set(["account", "calendar", "allDay", "repeat"]);
|
|
3183
3712
|
function toLocal(date, time) {
|
|
3184
3713
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time)) return;
|
|
3185
3714
|
const d = parse2(`${date}T${time}`, "yyyy-MM-dd'T'HH:mm", /* @__PURE__ */ new Date());
|
|
3186
|
-
return
|
|
3715
|
+
return isValid2(d) && format12(d, "yyyy-MM-dd'T'HH:mm") === `${date}T${time}` ? `${date}T${time}` : void 0;
|
|
3187
3716
|
}
|
|
3188
3717
|
var timeTexts = (f) => ({
|
|
3189
3718
|
startDate: f.start.slice(0, 10),
|
|
@@ -3191,25 +3720,48 @@ var timeTexts = (f) => ({
|
|
|
3191
3720
|
endDate: f.end.slice(0, 10),
|
|
3192
3721
|
endTime: f.end.slice(11)
|
|
3193
3722
|
});
|
|
3723
|
+
var repeatTexts = (r, start) => ({
|
|
3724
|
+
repeatEvery: r ? everyText(r) : "",
|
|
3725
|
+
repeatDays: daysText(r?.byDay?.length ? r.byDay : [weekdayOf(start)]),
|
|
3726
|
+
repeatEnds: r ? endsText(r) : "never"
|
|
3727
|
+
});
|
|
3194
3728
|
var textsFrom = (f) => ({
|
|
3195
3729
|
title: f.title,
|
|
3196
3730
|
location: f.location,
|
|
3197
3731
|
description: f.description,
|
|
3198
3732
|
attendees: f.attendees.join(", "),
|
|
3199
|
-
...timeTexts(f)
|
|
3733
|
+
...timeTexts(f),
|
|
3734
|
+
...repeatTexts(f.repeat, f.start)
|
|
3200
3735
|
});
|
|
3201
|
-
var
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
]
|
|
3736
|
+
var isCustom = (s) => !!s.form.repeat && !s.form.repeat.rule && (s.custom || presetOf(s.form.repeat, s.form.start) === "custom");
|
|
3737
|
+
var fieldsFor = (s, editing) => {
|
|
3738
|
+
const { form } = s;
|
|
3739
|
+
const custom = isCustom(s);
|
|
3740
|
+
return [
|
|
3741
|
+
"title",
|
|
3742
|
+
...editing ? [] : ["account", "calendar"],
|
|
3743
|
+
"allDay",
|
|
3744
|
+
"startDate",
|
|
3745
|
+
...form.allDay ? [] : ["startTime"],
|
|
3746
|
+
"endDate",
|
|
3747
|
+
...form.allDay ? [] : ["endTime"],
|
|
3748
|
+
"repeat",
|
|
3749
|
+
...custom ? ["repeatEvery"] : [],
|
|
3750
|
+
...custom && form.repeat?.freq === "weekly" ? ["repeatDays"] : [],
|
|
3751
|
+
...form.repeat && !form.repeat.rule ? ["repeatEnds"] : [],
|
|
3752
|
+
"location",
|
|
3753
|
+
"description",
|
|
3754
|
+
"attendees"
|
|
3755
|
+
];
|
|
3756
|
+
};
|
|
3757
|
+
function readRepeat(s) {
|
|
3758
|
+
const r = s.form.repeat;
|
|
3759
|
+
if (!r || r.rule) return r;
|
|
3760
|
+
const { until: _u, count: _c, ...base } = r;
|
|
3761
|
+
const rule = isCustom(s) ? { ...parseEvery(s.texts.repeatEvery) } : base;
|
|
3762
|
+
if (isCustom(s) && rule.freq === "weekly") rule.byDay = parseDays(s.texts.repeatDays);
|
|
3763
|
+
return { ...rule, ...parseEnds(s.texts.repeatEnds) };
|
|
3764
|
+
}
|
|
3213
3765
|
function EventEditor({ event, initialStart, onClose }) {
|
|
3214
3766
|
const api = useApi();
|
|
3215
3767
|
const { accounts, calendars, loaded } = useDirectory();
|
|
@@ -3223,18 +3775,41 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3223
3775
|
const [error, setError] = useState3("");
|
|
3224
3776
|
const [saving, setSaving] = useState3(false);
|
|
3225
3777
|
const busy = useRef3(false);
|
|
3778
|
+
const [askScope, setAskScopeState] = useState3(false);
|
|
3779
|
+
const asking = useRef3(false);
|
|
3780
|
+
const setAskScope = (on) => {
|
|
3781
|
+
asking.current = on;
|
|
3782
|
+
setAskScopeState(on);
|
|
3783
|
+
};
|
|
3784
|
+
const [repeatFailed, setRepeatFailed] = useState3(false);
|
|
3226
3785
|
const alive2 = useRef3(true);
|
|
3227
3786
|
useEffect3(() => () => void (alive2.current = false), []);
|
|
3228
3787
|
useEffect3(() => {
|
|
3229
3788
|
if (latest.current || !loaded) return;
|
|
3230
3789
|
const form2 = event ? formFromEvent(event) : emptyForm(accounts, calendars, {}, initialStart);
|
|
3231
|
-
latest.current = { form: form2, texts: textsFrom(form2), focus: 0 };
|
|
3790
|
+
latest.current = { form: form2, texts: textsFrom(form2), focus: 0, custom: false };
|
|
3232
3791
|
setState(latest.current);
|
|
3792
|
+
if (event?.recurringEventId) {
|
|
3793
|
+
api.events.recurrence(event).then(
|
|
3794
|
+
(rule) => {
|
|
3795
|
+
if (!alive2.current || !latest.current) return;
|
|
3796
|
+
const f = withLoadedRepeat(latest.current.form, rule);
|
|
3797
|
+
update({ form: f, texts: { ...latest.current.texts, ...repeatTexts(rule, f.start) } });
|
|
3798
|
+
},
|
|
3799
|
+
() => alive2.current && setRepeatFailed(true)
|
|
3800
|
+
);
|
|
3801
|
+
}
|
|
3233
3802
|
}, [loaded]);
|
|
3234
3803
|
const choices = writableAccounts(accounts, calendars);
|
|
3235
|
-
const save = async () => {
|
|
3804
|
+
const save = async (scope) => {
|
|
3236
3805
|
if (!latest.current || busy.current) return;
|
|
3237
3806
|
const { form: form2, texts: texts2 } = latest.current;
|
|
3807
|
+
let repeat;
|
|
3808
|
+
try {
|
|
3809
|
+
repeat = readRepeat(latest.current);
|
|
3810
|
+
} catch (e) {
|
|
3811
|
+
return setError(errorText(e));
|
|
3812
|
+
}
|
|
3238
3813
|
setError("");
|
|
3239
3814
|
const start = toLocal(texts2.startDate, form2.allDay ? form2.start.slice(11) : texts2.startTime);
|
|
3240
3815
|
const end = toLocal(texts2.endDate, form2.allDay ? form2.end.slice(11) : texts2.endTime);
|
|
@@ -3247,14 +3822,18 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3247
3822
|
description: texts2.description,
|
|
3248
3823
|
attendees: splitEmails(texts2.attendees),
|
|
3249
3824
|
start,
|
|
3250
|
-
end
|
|
3825
|
+
end,
|
|
3826
|
+
repeat
|
|
3251
3827
|
};
|
|
3252
3828
|
let request;
|
|
3253
3829
|
try {
|
|
3254
|
-
|
|
3830
|
+
const edited = event && applyForm(event, f);
|
|
3831
|
+
if (edited && event.recurringEventId && !scope) return setAskScope(repeatChanged(f) ? "rule" : "any");
|
|
3832
|
+
request = edited ? api.events.update(edited, scope) : api.events.create(formToInput(f));
|
|
3255
3833
|
} catch (e) {
|
|
3256
3834
|
return setError(errorText(e));
|
|
3257
3835
|
}
|
|
3836
|
+
setAskScope(false);
|
|
3258
3837
|
busy.current = true;
|
|
3259
3838
|
setSaving(true);
|
|
3260
3839
|
try {
|
|
@@ -3273,7 +3852,8 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3273
3852
|
const i = items.findIndex((x) => x.id === id2);
|
|
3274
3853
|
return items[i < 0 ? dir > 0 ? 0 : items.length - 1 : (i + dir + items.length) % items.length].id;
|
|
3275
3854
|
};
|
|
3276
|
-
const pick = (
|
|
3855
|
+
const pick = (st, field, dir) => {
|
|
3856
|
+
const { form: form2, texts: texts2 } = st;
|
|
3277
3857
|
if (field === "account") {
|
|
3278
3858
|
const accountId = cycle2(choices, form2.accountId, dir);
|
|
3279
3859
|
update({ form: { ...form2, accountId, calendarId: soleId(writableCalendars(calendars, accountId)) } });
|
|
@@ -3282,6 +3862,19 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3282
3862
|
} else if (field === "allDay") {
|
|
3283
3863
|
const f = setAllDay(form2, !form2.allDay);
|
|
3284
3864
|
update({ form: f, texts: { ...texts2, ...timeTexts(f) } });
|
|
3865
|
+
} else if (field === "repeat") {
|
|
3866
|
+
if (form2.repeatWas === void 0) return;
|
|
3867
|
+
const ids = [...presets(form2.start).map((p) => p.id), "custom"];
|
|
3868
|
+
const at3 = ids.indexOf(isCustom(st) ? "custom" : presetOf(form2.repeat, form2.start));
|
|
3869
|
+
const next = ids[(at3 + dir + ids.length) % ids.length];
|
|
3870
|
+
let prev = form2.repeat;
|
|
3871
|
+
try {
|
|
3872
|
+
prev = readRepeat(st);
|
|
3873
|
+
} catch {
|
|
3874
|
+
}
|
|
3875
|
+
const preset = presets(form2.start).find((p) => p.id === next)?.recurrence;
|
|
3876
|
+
const repeat = next === "custom" ? prev && !prev.rule ? prev : withEnd({ freq: "weekly" }, prev) : preset ? withEnd(preset, prev) : null;
|
|
3877
|
+
update({ form: { ...form2, repeat }, custom: next === "custom", texts: { ...texts2, ...repeatTexts(repeat, form2.start) } });
|
|
3285
3878
|
}
|
|
3286
3879
|
};
|
|
3287
3880
|
const edit = ({ form: form2, texts: texts2 }, field, value2) => {
|
|
@@ -3301,17 +3894,22 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3301
3894
|
const clickField = (f) => {
|
|
3302
3895
|
const s = latest.current;
|
|
3303
3896
|
if (!s || busy.current) return;
|
|
3304
|
-
const i = fieldsFor(s
|
|
3897
|
+
const i = fieldsFor(s, !!event).indexOf(f);
|
|
3305
3898
|
if (i < 0) return;
|
|
3306
|
-
if (i === s.focus && (f
|
|
3899
|
+
if (i === s.focus && PICKERS.has(f)) return pick(s, f, 1);
|
|
3307
3900
|
update({ focus: i });
|
|
3308
3901
|
};
|
|
3309
3902
|
useKeys((input, key3) => {
|
|
3903
|
+
if (key3.escape && asking.current && !busy.current) return setAskScope(false);
|
|
3310
3904
|
if (key3.escape) return onClose();
|
|
3311
3905
|
const s = latest.current;
|
|
3312
3906
|
if (!s || busy.current) return;
|
|
3907
|
+
if (asking.current) {
|
|
3908
|
+
if (!Object.hasOwn(SCOPE_KEYS2, input) || asking.current === "rule" && SCOPE_KEYS2[input] === "one") return;
|
|
3909
|
+
return void save(SCOPE_KEYS2[input]);
|
|
3910
|
+
}
|
|
3313
3911
|
if (key3.ctrl && input === "s") return void save();
|
|
3314
|
-
const fields2 = fieldsFor(s
|
|
3912
|
+
const fields2 = fieldsFor(s, !!event);
|
|
3315
3913
|
const i = Math.min(s.focus, fields2.length - 1);
|
|
3316
3914
|
const field = fields2[i];
|
|
3317
3915
|
const move = (dir) => update({ focus: (i + dir + fields2.length) % fields2.length });
|
|
@@ -3319,19 +3917,20 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3319
3917
|
if (key3.downArrow) return move(1);
|
|
3320
3918
|
if (key3.upArrow) return move(-1);
|
|
3321
3919
|
if (key3.return) return i === fields2.length - 1 ? void save() : move(1);
|
|
3322
|
-
if (field
|
|
3920
|
+
if (PICKERS.has(field)) {
|
|
3323
3921
|
if (key3.leftArrow) pick(s, field, -1);
|
|
3324
3922
|
else if (key3.rightArrow || input === " ") pick(s, field, 1);
|
|
3325
3923
|
return;
|
|
3326
3924
|
}
|
|
3327
|
-
|
|
3925
|
+
const text2 = field;
|
|
3926
|
+
if (key3.backspace || key3.delete) return edit(s, text2, s.texts[text2].slice(0, -1));
|
|
3328
3927
|
if (key3.ctrl || key3.meta || !input) return;
|
|
3329
3928
|
const printable = input.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
3330
|
-
if (printable) edit(s,
|
|
3929
|
+
if (printable) edit(s, text2, s.texts[text2] + printable);
|
|
3331
3930
|
});
|
|
3332
3931
|
if (!state) return /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "Loading\u2026" });
|
|
3333
3932
|
const { form, texts } = state;
|
|
3334
|
-
const fields = fieldsFor(
|
|
3933
|
+
const fields = fieldsFor(state, !!event);
|
|
3335
3934
|
const current = fields[Math.min(state.focus, fields.length - 1)];
|
|
3336
3935
|
const account = accounts.find((a) => a.id === form.accountId);
|
|
3337
3936
|
const calendar = calendars.find((c) => c.accountId === form.accountId && c.id === form.calendarId);
|
|
@@ -3346,6 +3945,11 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3346
3945
|
return calendar ? { text: calendar.name } : { text: "\u2039 choose calendar \u203A", dim: true };
|
|
3347
3946
|
case "allDay":
|
|
3348
3947
|
return { text: form.allDay ? "[x]" : "[ ]" };
|
|
3948
|
+
case "repeat":
|
|
3949
|
+
if (form.repeatWas === void 0) return { text: repeatFailed ? "couldn\u2019t load; stays as it is" : "loading\u2026", dim: true };
|
|
3950
|
+
if (form.repeat?.rule) return { text: describe(form.repeat, form.start) };
|
|
3951
|
+
if (isCustom(state)) return { text: "Custom" };
|
|
3952
|
+
return { text: presets(form.start).find((p) => p.id === presetOf(form.repeat, form.start)).label };
|
|
3349
3953
|
default:
|
|
3350
3954
|
return texts[f] ? { text: texts[f] } : { text: HINTS[f] ?? "", dim: true };
|
|
3351
3955
|
}
|
|
@@ -3356,7 +3960,7 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3356
3960
|
rows.map((f) => {
|
|
3357
3961
|
const on = f === current;
|
|
3358
3962
|
const v = value(f);
|
|
3359
|
-
const picker = !event && (f === "account" || f === "calendar");
|
|
3963
|
+
const picker = !event && (f === "account" || f === "calendar") || f === "repeat";
|
|
3360
3964
|
return /* @__PURE__ */ jsxs6(Clickable, { onClick: () => clickField(f), children: [
|
|
3361
3965
|
/* @__PURE__ */ jsxs6(Text6, { color: on ? C.cyan : void 0, children: [
|
|
3362
3966
|
on ? "\u203A " : " ",
|
|
@@ -3368,7 +3972,13 @@ function EventEditor({ event, initialStart, onClose }) {
|
|
|
3368
3972
|
}),
|
|
3369
3973
|
/* @__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
3974
|
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: [
|
|
3975
|
+
saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : askScope ? /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
|
|
3976
|
+
/* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: askScope === "rule" ? "Change the repeat rule for: " : "Save recurring event: " }),
|
|
3977
|
+
askScope !== "rule" && /* @__PURE__ */ jsx8(Button, { k: "1", label: "this event", color: C.green, onPress: () => void save("one") }),
|
|
3978
|
+
/* @__PURE__ */ jsx8(Button, { k: "2", label: "this and following", color: C.green, onPress: () => void save("following") }),
|
|
3979
|
+
/* @__PURE__ */ jsx8(Button, { k: "3", label: "all events", color: C.green, onPress: () => void save("all") }),
|
|
3980
|
+
/* @__PURE__ */ jsx8(Button, { k: "esc", label: "back", onPress: () => setAskScope(false) })
|
|
3981
|
+
] }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
|
|
3372
3982
|
/* @__PURE__ */ jsx8(Box6, { marginRight: 2, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "tab/\u2191\u2193 move \xB7 \u2190/\u2192/space pick" }) }),
|
|
3373
3983
|
/* @__PURE__ */ jsx8(Button, { k: "ctrl+s", label: "save", color: C.green, onPress: () => void save() }),
|
|
3374
3984
|
/* @__PURE__ */ jsx8(Button, { k: "esc", label: "cancel", onPress: onClose })
|
|
@@ -3391,12 +4001,12 @@ import { Box as Box8, Text as Text8 } from "ink";
|
|
|
3391
4001
|
// src/ui/screens/Invites.tsx
|
|
3392
4002
|
import { useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
|
|
3393
4003
|
import { Box as Box7, Text as Text7 } from "ink";
|
|
3394
|
-
import { addDays as
|
|
4004
|
+
import { addDays as addDays10, startOfDay as startOfDay6 } from "date-fns";
|
|
3395
4005
|
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3396
4006
|
var INVITE_DAYS = 60;
|
|
3397
4007
|
function useUpcoming(now) {
|
|
3398
4008
|
const day = startOfDay6(now).getTime();
|
|
3399
|
-
const range = useMemo5(() => ({ start: new Date(day).toISOString(), end:
|
|
4009
|
+
const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays10(day, INVITE_DAYS).toISOString() }), [day]);
|
|
3400
4010
|
return useEvents(range);
|
|
3401
4011
|
}
|
|
3402
4012
|
var REPLY = { y: "accepted", n: "declined", m: "tentative" };
|
|
@@ -3574,12 +4184,12 @@ var NAMES = {
|
|
|
3574
4184
|
};
|
|
3575
4185
|
function useKeyState(init) {
|
|
3576
4186
|
const ref = useRef5(init);
|
|
3577
|
-
const [,
|
|
4187
|
+
const [, bump2] = useState6(0);
|
|
3578
4188
|
return [
|
|
3579
4189
|
() => ref.current,
|
|
3580
4190
|
(next) => {
|
|
3581
4191
|
ref.current = next;
|
|
3582
|
-
|
|
4192
|
+
bump2((n) => n + 1);
|
|
3583
4193
|
}
|
|
3584
4194
|
];
|
|
3585
4195
|
}
|
|
@@ -3996,9 +4606,9 @@ function FirstSyncCheck({ width, height, children }) {
|
|
|
3996
4606
|
] }) });
|
|
3997
4607
|
}
|
|
3998
4608
|
function title({ view, date }) {
|
|
3999
|
-
if (view === "month") return
|
|
4000
|
-
if (view === "day") return
|
|
4001
|
-
const days = view === "agenda" ? [date,
|
|
4609
|
+
if (view === "month") return format13(date, "MMMM yyyy");
|
|
4610
|
+
if (view === "day") return format13(date, "EEE d MMM yyyy");
|
|
4611
|
+
const days = view === "agenda" ? [date, addDays11(date, AGENDA_DAYS - 1)] : view === "2day" ? [date, addDays11(date, 1)] : viewDays(view, date);
|
|
4002
4612
|
return rangeLabel(days[0], days[days.length - 1]);
|
|
4003
4613
|
}
|
|
4004
4614
|
var chronological = (a, b) => eventBounds(a).start.getTime() - eventBounds(b).start.getTime() || Number(b.allDay) - Number(a.allDay);
|
|
@@ -4076,8 +4686,8 @@ function Shell({ initialNav, updateCheck, notice }) {
|
|
|
4076
4686
|
const { cursor: from, view, date, key: key3 } = live.current;
|
|
4077
4687
|
const sel = ordered2.find((e) => eventKey(e) === key3);
|
|
4078
4688
|
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 ?
|
|
4689
|
+
const day = addDays11(from, n);
|
|
4690
|
+
const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ? addDays11(day, -1) : day;
|
|
4081
4691
|
Object.assign(live.current, { cursor: day, date: nextDate, minute });
|
|
4082
4692
|
setCursor(day);
|
|
4083
4693
|
if (+nextDate !== +date) setDate(nextDate);
|