@bobfrankston/mailx 1.0.377 → 1.0.379

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.
@@ -10,68 +10,33 @@
10
10
  * Sidebar and the full-screen calendar modal (calendar.ts) read the SAME
11
11
  * underlying data — two views onto one source.
12
12
  *
13
- * Local-only events (LOCAL_STORE_KEY) are merged with Google events.
13
+ * All storage goes through the service-side two-way cache (calendar_events
14
+ * and tasks tables); this file does not use localStorage for data.
14
15
  */
15
- import { getPrimaryAccount } from "../lib/api-client.js";
16
- const LOCAL_STORE_KEY = "mailx-cal-events-v1";
17
- const TASK_STORE_KEY = "mailx-tasks-v1";
16
+ import { getCalendarEvents, createCalendarEvent, getTasks, createTask, updateTask, } from "../lib/api-client.js";
18
17
  const SIDEBAR_PREF = "mailx-calendar-sidebar-on";
19
18
  let viewYear = new Date().getFullYear();
20
19
  let viewMonth = new Date().getMonth();
21
20
  let viewDay = new Date().getDate();
22
21
  let lastEvents = [];
23
- function loadLocalEvents() {
24
- try {
25
- const raw = localStorage.getItem(LOCAL_STORE_KEY);
26
- if (!raw)
27
- return [];
28
- const arr = JSON.parse(raw);
29
- return Array.isArray(arr) ? arr : [];
30
- }
31
- catch {
32
- return [];
33
- }
34
- }
35
- function loadTasks() {
36
- try {
37
- const raw = localStorage.getItem(TASK_STORE_KEY);
38
- if (!raw)
39
- return [];
40
- const arr = JSON.parse(raw);
41
- return Array.isArray(arr) ? arr : [];
42
- }
43
- catch {
44
- return [];
45
- }
46
- }
47
- function saveTasks(tasks) {
48
- try {
49
- localStorage.setItem(TASK_STORE_KEY, JSON.stringify(tasks));
50
- }
51
- catch { /* */ }
52
- }
53
- /** Fetch upcoming events from Google Calendar via the primary account's
54
- * OAuth token. Returns merged Google + local events for the next 30 days
55
- * starting at `from`. Quietly returns local-only on any error so the
56
- * sidebar still works without network / without Google Calendar scope. */
22
+ /** Fetch events from the local two-way cache; service returns local rows
23
+ * immediately and kicks a background refresh from Google. Next render
24
+ * (view-nav or user action) picks up the refreshed rows. No localStorage
25
+ * — everything lives in the service-side DB so phone / desktop share
26
+ * the same events. */
57
27
  async function fetchUpcoming(from) {
58
- const local = loadLocalEvents();
59
- let google = [];
60
- try {
61
- const primary = await getPrimaryAccount();
62
- if (primary?.email) {
63
- // The OAuth token is held by the service-side OAuthTokenManager;
64
- // calendar fetch goes through a server-side proxy method (not
65
- // implemented yet — this is the seam). For now: local-only.
66
- // Wire-up: TODO — service.fetchGoogleCalendarEvents(accountId, from, days).
67
- // Sidebar already renders correctly with the merged result.
68
- }
69
- }
70
- catch { /* google unavailable, fall back to local */ }
71
28
  const horizon = from.getTime() + 30 * 86400_000;
72
- return [...local, ...google]
73
- .filter(e => e.start >= from.getTime() && e.start < horizon)
74
- .sort((a, b) => a.start - b.start);
29
+ const rows = await getCalendarEvents(from.getTime(), horizon);
30
+ return rows.map((r) => ({
31
+ id: r.uuid,
32
+ title: r.title,
33
+ start: r.startMs,
34
+ end: r.endMs,
35
+ allDay: !!r.allDay,
36
+ location: r.location,
37
+ notes: r.notes,
38
+ source: r.providerId ? "google" : "local",
39
+ }));
75
40
  }
76
41
  function formatDayHeader(d, today, tomorrow) {
77
42
  const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
@@ -124,9 +89,9 @@ function renderEvents(events) {
124
89
  }
125
90
  body.innerHTML = html;
126
91
  }
127
- function renderTasks() {
92
+ async function renderTasks() {
128
93
  const showDone = document.getElementById("cal-side-show-done")?.checked || false;
129
- const tasks = loadTasks().filter(t => showDone || !t.completed);
94
+ const tasks = await getTasks(showDone);
130
95
  const host = document.getElementById("cal-side-tasks");
131
96
  if (!host)
132
97
  return;
@@ -136,23 +101,19 @@ function renderTasks() {
136
101
  }
137
102
  let html = "<div class='cal-side-task-head'>Title</div>";
138
103
  for (const t of tasks) {
139
- html += `<div class="cal-side-task" data-id="${t.id}">
140
- <input type="checkbox" ${t.completed ? "checked" : ""} class="cal-side-task-check">
141
- <span class="cal-side-task-title${t.completed ? " done" : ""}">${escapeHtml(t.title)}</span>
104
+ const done = !!t.completedMs;
105
+ html += `<div class="cal-side-task" data-uuid="${t.uuid}">
106
+ <input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
107
+ <span class="cal-side-task-title${done ? " done" : ""}">${escapeHtml(t.title)}</span>
142
108
  </div>`;
143
109
  }
144
110
  host.innerHTML = html;
145
111
  host.querySelectorAll(".cal-side-task").forEach(row => {
146
- const id = row.dataset.id;
147
- row.querySelector(".cal-side-task-check")?.addEventListener("change", (e) => {
112
+ const uuid = row.dataset.uuid;
113
+ row.querySelector(".cal-side-task-check")?.addEventListener("change", async (e) => {
148
114
  const checked = e.target.checked;
149
- const all = loadTasks();
150
- const t = all.find(x => x.id === id);
151
- if (t) {
152
- t.completed = checked ? Date.now() : undefined;
153
- saveTasks(all);
154
- renderTasks();
155
- }
115
+ await updateTask(uuid, { completedMs: checked ? Date.now() : null });
116
+ renderTasks();
156
117
  });
157
118
  });
158
119
  }
@@ -225,19 +186,19 @@ export function initCalendarSidebar() {
225
186
  alert("Couldn't parse that date.");
226
187
  return;
227
188
  }
228
- const ev = {
229
- id: `cal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
230
- title, start, end: start + (allDay ? 86400_000 : 3600_000),
231
- allDay, source: "local",
232
- };
233
- const all = loadLocalEvents();
234
- all.push(ev);
235
- try {
236
- localStorage.setItem(LOCAL_STORE_KEY, JSON.stringify(all));
237
- }
238
- catch { /* */ }
189
+ // Two-way cache: commit locally, service queues the push to Google.
190
+ await createCalendarEvent({
191
+ title, startMs: start, endMs: start + (allDay ? 86400_000 : 3600_000), allDay,
192
+ });
239
193
  await refresh();
240
194
  });
195
+ wireOnce("cal-side-new-task", async () => {
196
+ const title = prompt("Task title:");
197
+ if (!title)
198
+ return;
199
+ await createTask({ title });
200
+ renderTasks();
201
+ });
241
202
  const showDoneCb = document.getElementById("cal-side-show-done");
242
203
  if (showDoneCb && !showDoneCb.__wired) {
243
204
  showDoneCb.__wired = true;
@@ -370,7 +370,7 @@ function restoreSelection(body, savedUid) {
370
370
  /** Show a floating list of all messages in a thread when the pill is clicked.
371
371
  * Each entry in the popup selects that message in the viewer when clicked.
372
372
  * This is simpler than inline expansion and avoids duplicating the row builder. */
373
- async function showThreadPopup(pillEl, headMsg) {
373
+ export async function showThreadPopup(pillEl, headMsg) {
374
374
  // Remove any existing popup
375
375
  document.querySelectorAll(".ml-thread-popup").forEach(el => el.remove());
376
376
  let thread = [];
@@ -464,6 +464,11 @@ function appendMessages(body, accountId, items) {
464
464
  // action (move/flag/delete) hasn't been ACK'd by the server yet.
465
465
  if (msg.pending)
466
466
  row.classList.add("pending-reconcile");
467
+ // Reply-row marker: messages with In-Reply-To are replies. Shows a
468
+ // subtle left-border accent so the eye can pick out threaded replies
469
+ // without enabling full thread grouping.
470
+ if (msg.inReplyTo)
471
+ row.classList.add("is-reply");
467
472
  row.dataset.uid = String(msg.uid);
468
473
  row.dataset.accountId = msgAccountId;
469
474
  row.dataset.folderId = String(msg.folderId);
@@ -600,6 +605,37 @@ function appendMessages(body, accountId, items) {
600
605
  }
601
606
  });
602
607
  row.addEventListener("dragend", () => row.classList.remove("dragging"));
608
+ // ── Q66: long-press on touch → context menu ──
609
+ // Mirrors right-click on the phone where right-click isn't a thing.
610
+ // Cancelled by any touchmove or touchend before the threshold.
611
+ let longPressTimer = null;
612
+ const LONG_PRESS_MS = 550;
613
+ row.addEventListener("touchstart", (e) => {
614
+ const t = e.touches[0];
615
+ if (!t)
616
+ return;
617
+ const cx = t.clientX, cy = t.clientY;
618
+ if (longPressTimer)
619
+ clearTimeout(longPressTimer);
620
+ longPressTimer = setTimeout(() => {
621
+ longPressTimer = null;
622
+ // Synthesize a contextmenu event so the existing handler below
623
+ // owns all the menu logic — no per-event duplication.
624
+ const ev = new MouseEvent("contextmenu", {
625
+ clientX: cx, clientY: cy, bubbles: true, cancelable: true,
626
+ });
627
+ row.dispatchEvent(ev);
628
+ }, LONG_PRESS_MS);
629
+ }, { passive: true });
630
+ const cancelLongPress = () => {
631
+ if (longPressTimer) {
632
+ clearTimeout(longPressTimer);
633
+ longPressTimer = null;
634
+ }
635
+ };
636
+ row.addEventListener("touchmove", cancelLongPress, { passive: true });
637
+ row.addEventListener("touchend", cancelLongPress, { passive: true });
638
+ row.addEventListener("touchcancel", cancelLongPress, { passive: true });
603
639
  // ── Right-click context menu ──
604
640
  row.addEventListener("contextmenu", (e) => {
605
641
  e.preventDefault();
@@ -395,6 +395,23 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
395
395
  unsubBtn.hidden = true;
396
396
  }
397
397
  }
398
+ // View Thread button — opens the thread popup from the message list
399
+ // so the user can see all messages in the conversation. Works from
400
+ // the viewer even when thread-grouping is off.
401
+ const threadBtn = document.getElementById("mv-view-thread");
402
+ if (threadBtn) {
403
+ const tid = msg.threadId || "";
404
+ if (tid) {
405
+ threadBtn.hidden = false;
406
+ threadBtn.onclick = async () => {
407
+ const { showThreadPopup } = await import("./message-list.js");
408
+ await showThreadPopup(threadBtn, { accountId, threadId: tid });
409
+ };
410
+ }
411
+ else {
412
+ threadBtn.hidden = true;
413
+ }
414
+ }
398
415
  // View Source button — shows .eml file path
399
416
  const srcBtn = document.getElementById("mv-view-source");
400
417
  if (srcBtn) {
package/client/index.html CHANGED
@@ -134,6 +134,7 @@
134
134
  <button class="tb-btn" id="btn-spam" title="Mark as spam — move to configured spam folder" hidden>⚠</button>
135
135
  <button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
136
136
  <button class="tb-btn" id="btn-mark-unread" title="Mark unread (R)">◉</button>
137
+ <button class="tb-btn" id="mv-view-thread" title="View thread (conversation)" hidden>💬</button>
137
138
  <span style="flex:1"></span>
138
139
  <button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
139
140
  <a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
@@ -169,7 +170,10 @@
169
170
  <div class="cal-side-empty">Loading…</div>
170
171
  </div>
171
172
  <footer class="cal-side-foot">
172
- <label><input type="checkbox" id="cal-side-show-done"> Show completed Tasks</label>
173
+ <div class="cal-side-task-header-row">
174
+ <label><input type="checkbox" id="cal-side-show-done"> Show completed Tasks</label>
175
+ <button class="cal-side-new" id="cal-side-new-task" title="New task">+ Task</button>
176
+ </div>
173
177
  <div class="cal-side-tasks" id="cal-side-tasks"></div>
174
178
  </footer>
175
179
  </aside>
@@ -134,10 +134,41 @@ export function getSyncPending() {
134
134
  export function getDiagnostics() {
135
135
  return ipc().getDiagnostics?.() ?? Promise.resolve([]);
136
136
  }
137
- /** Account marked `primary: true` in accounts.jsonc used by Calendar /
138
- * Tasks / Contacts to pick which Google account's data to show. */
139
- export function getPrimaryAccount() {
140
- return ipc().getPrimaryAccount?.() ?? Promise.resolve(null);
137
+ /** Account that supplies `feature` data (calendar / tasks / contacts).
138
+ * Resolution: per-feature primary flag catch-all `primary` first account.
139
+ * Pass e.g. "calendar" to honor `primaryCalendar:true` overrides; omit for
140
+ * back-compat single-flag behavior. */
141
+ export function getPrimaryAccount(feature) {
142
+ return ipc().getPrimaryAccount?.(feature) ?? Promise.resolve(null);
143
+ }
144
+ // Calendar / Tasks: two-way cache. Reads return local-cached rows; writes
145
+ // commit locally and queue a push to Google. Service layer handles drain.
146
+ export function getCalendarEvents(fromMs, toMs) {
147
+ return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
148
+ }
149
+ export function createCalendarEvent(ev) {
150
+ return ipc().createCalendarEvent?.(ev);
151
+ }
152
+ export function updateCalendarEvent(uuid, patch) {
153
+ return ipc().updateCalendarEvent?.(uuid, patch);
154
+ }
155
+ export function deleteCalendarEvent(uuid) {
156
+ return ipc().deleteCalendarEvent?.(uuid);
157
+ }
158
+ export function getTasks(includeCompleted = false) {
159
+ return ipc().getTasks?.(includeCompleted) ?? Promise.resolve([]);
160
+ }
161
+ export function createTask(t) {
162
+ return ipc().createTask?.(t);
163
+ }
164
+ export function updateTask(uuid, patch) {
165
+ return ipc().updateTask?.(uuid, patch);
166
+ }
167
+ export function deleteTask(uuid) {
168
+ return ipc().deleteTask?.(uuid);
169
+ }
170
+ export function drainStoreSync() {
171
+ return ipc().drainStoreSync?.();
141
172
  }
142
173
  export function getOutboxStatus() {
143
174
  return ipc().getOutboxStatus();
@@ -109,6 +109,19 @@
109
109
  getThreadMessages: function(accountId, threadId) {
110
110
  return callNode("getThreadMessages", { accountId: accountId, threadId: threadId });
111
111
  },
112
+ // Calendar / Tasks two-way cache. Reads return local DB immediately;
113
+ // writes commit locally and queue a push-to-Google action.
114
+ getCalendarEvents: function(fromMs, toMs) {
115
+ return callNode("getCalendarEvents", { fromMs: fromMs, toMs: toMs });
116
+ },
117
+ createCalendarEvent: function(ev) { return callNode("createCalendarEvent", ev); },
118
+ updateCalendarEvent: function(uuid, patch) { return callNode("updateCalendarEvent", { uuid: uuid, patch: patch }); },
119
+ deleteCalendarEvent: function(uuid) { return callNode("deleteCalendarEvent", { uuid: uuid }); },
120
+ getTasks: function(includeCompleted) { return callNode("getTasks", { includeCompleted: !!includeCompleted }); },
121
+ createTask: function(t) { return callNode("createTask", t); },
122
+ updateTask: function(uuid, patch) { return callNode("updateTask", { uuid: uuid, patch: patch }); },
123
+ deleteTask: function(uuid) { return callNode("deleteTask", { uuid: uuid }); },
124
+ drainStoreSync: function() { return callNode("drainStoreSync"); },
112
125
  readJsoncFile: function(name) {
113
126
  return callNode("readJsoncFile", { name: name });
114
127
  },
@@ -160,7 +173,7 @@
160
173
  getSyncPending: function() { return callNode("getSyncPending"); },
161
174
  getOutboxStatus: function() { return callNode("getOutboxStatus"); },
162
175
  getDiagnostics: function() { return callNode("getDiagnostics"); },
163
- getPrimaryAccount: function() { return callNode("getPrimaryAccount"); },
176
+ getPrimaryAccount: function(feature) { return callNode("getPrimaryAccount", { feature: feature }); },
164
177
  listQueuedOutgoing: function() { return callNode("listQueuedOutgoing"); },
165
178
  cancelQueuedOutgoing: function(p) { return callNode("cancelQueuedOutgoing", { path: p }); },
166
179
  reauthenticate: function(accountId) { return callNode("reauthenticate", { accountId: accountId }); },
@@ -788,6 +788,24 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
788
788
  opacity: 0.5;
789
789
  }
790
790
 
791
+ /* Reply marker — messages whose In-Reply-To points at another message get a
792
+ subtle left-edge accent so threaded replies visibly distinguish from top-
793
+ level posts without needing thread-grouping mode on. Works regardless of
794
+ whether thread-grouping is enabled. */
795
+ .ml-row.is-reply::before {
796
+ content: "";
797
+ position: absolute;
798
+ left: 0;
799
+ top: 2px;
800
+ bottom: 2px;
801
+ width: 2px;
802
+ background: oklch(0.70 0.12 250); /* muted blue */
803
+ border-radius: 0 1px 1px 0;
804
+ pointer-events: none;
805
+ opacity: 0.5;
806
+ }
807
+ .ml-row { position: relative; }
808
+
791
809
  /* S1 slice C — local action (move/flag/delete) queued but server hasn't
792
810
  ACK'd. Reuses the same date-column dot as the download indicator so
793
811
  "still on client, not yet on server" sits in the same visual slot as
@@ -44,15 +44,17 @@ body.calendar-sidebar-on {
44
44
  .main-area { grid-area: main; }
45
45
  .status-bar { grid-area: status; }
46
46
 
47
- /* Vertical icon rail (Dovecot/Thunderbird-style). Always visible on
48
- wide+medium tiers; collapses on narrow (icons move into the hamburger
49
- menu TBD; for now hidden on narrow). */
47
+ /* Vertical icon rail Thunderbird Supernova style: dark background with
48
+ light icons so the rail reads as "chrome" and contrasts visibly against
49
+ the content area (whether the app's in light or dark theme). Always
50
+ visible on wide+medium tiers; hidden on narrow (icons fold into
51
+ hamburger — TBD). */
50
52
  .icon-rail {
51
53
  display: flex;
52
54
  flex-direction: column;
53
55
  justify-content: space-between;
54
- background: var(--color-bg-alt, #f4f4f5);
55
- border-right: 1px solid var(--color-border);
56
+ background: oklch(0.25 0.01 250);
57
+ border-right: 1px solid oklch(0.18 0.01 250);
56
58
  padding: 6px 0;
57
59
  overflow: hidden;
58
60
  }
@@ -71,16 +73,21 @@ body.calendar-sidebar-on {
71
73
  background: transparent;
72
74
  cursor: pointer;
73
75
  font-size: 16px;
74
- color: var(--color-text);
76
+ color: oklch(0.88 0.01 250);
75
77
  border-left: 3px solid transparent;
76
- transition: background 0.12s, border-color 0.12s;
78
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
77
79
  }
78
80
  .rail-btn:hover:not([disabled]) {
79
- background: var(--color-hover, rgba(0,0,0,0.06));
81
+ background: oklch(0.32 0.02 250);
82
+ color: oklch(0.96 0.01 250);
80
83
  }
81
84
  .rail-btn[data-active="true"] {
82
- background: var(--color-hover, rgba(0,0,0,0.06));
83
- border-left-color: var(--color-accent, #1a6dd4);
85
+ background: oklch(0.35 0.03 250);
86
+ color: oklch(0.98 0.01 250);
87
+ border-left-color: var(--color-accent, #4a9cff);
88
+ }
89
+ .rail-btn[disabled] {
90
+ color: oklch(0.55 0.01 250);
84
91
  }
85
92
  .rail-btn[disabled] {
86
93
  opacity: 0.35;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.377",
3
+ "version": "1.0.379",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -564,7 +564,11 @@ export class ImapManager extends EventEmitter {
564
564
  const hasToken = fs.existsSync(path.join(tokenDir, "oauth-token.json"));
565
565
  const TOKEN_FETCH_TIMEOUT_MS = hasToken ? 30_000 : 120_000;
566
566
  const authPromise = authenticateOAuth(credPath, {
567
- scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/drive",
567
+ // Scope set covers two-way sync of all mailx-managed local
568
+ // stores: mail (mail.google.com), contacts (full, not
569
+ // readonly — we write edits back), calendar (full), tasks
570
+ // (full), drive (for shared accounts.jsonc).
571
+ scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/drive",
568
572
  tokenDirectory: tokenDir,
569
573
  credentialsKey: "installed",
570
574
  loginHint: account.imap.user,
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Google Calendar / Tasks / People (Contacts) two-way sync helpers.
3
+ *
4
+ * Used by MailxService to push local edits to Google and pull server
5
+ * changes into the local cache. All functions take a `getToken` function
6
+ * and a fetch implementation so they stay platform-agnostic (Node uses
7
+ * global `fetch` on Node 18+; browsers use window.fetch).
8
+ *
9
+ * Error handling: throws on network / HTTP errors. Caller catches and
10
+ * either retries via the store_sync drainer or surfaces to the UI.
11
+ */
12
+ type TokenProvider = () => Promise<string>;
13
+ export interface GCalEvent {
14
+ id: string;
15
+ summary: string;
16
+ start: {
17
+ dateTime?: string;
18
+ date?: string;
19
+ };
20
+ end: {
21
+ dateTime?: string;
22
+ date?: string;
23
+ };
24
+ location?: string;
25
+ description?: string;
26
+ etag?: string;
27
+ }
28
+ export declare function listCalendarEvents(tokenProvider: TokenProvider, fromMs: number, toMs: number, calendarId?: string): Promise<GCalEvent[]>;
29
+ export declare function createCalendarEvent(tokenProvider: TokenProvider, event: any, calendarId?: string): Promise<GCalEvent>;
30
+ export declare function updateCalendarEvent(tokenProvider: TokenProvider, eventId: string, event: any, calendarId?: string): Promise<GCalEvent>;
31
+ export declare function deleteCalendarEvent(tokenProvider: TokenProvider, eventId: string, calendarId?: string): Promise<void>;
32
+ export interface GTask {
33
+ id: string;
34
+ title: string;
35
+ notes?: string;
36
+ due?: string;
37
+ completed?: string;
38
+ status?: "needsAction" | "completed";
39
+ etag?: string;
40
+ }
41
+ export declare function listTasks(tokenProvider: TokenProvider, listId?: string, showCompleted?: boolean): Promise<GTask[]>;
42
+ export declare function createTask(tokenProvider: TokenProvider, task: any, listId?: string): Promise<GTask>;
43
+ export declare function updateTask(tokenProvider: TokenProvider, taskId: string, task: any, listId?: string): Promise<GTask>;
44
+ export declare function deleteTask(tokenProvider: TokenProvider, taskId: string, listId?: string): Promise<void>;
45
+ export declare function createContact(tokenProvider: TokenProvider, person: any): Promise<any>;
46
+ export declare function updateContact(tokenProvider: TokenProvider, resourceName: string, updatePersonFields: string, person: any): Promise<any>;
47
+ export declare function deleteContact(tokenProvider: TokenProvider, resourceName: string): Promise<void>;
48
+ export declare function calendarEventToLocal(ev: GCalEvent, accountId: string): {
49
+ providerId: string;
50
+ accountId: string;
51
+ title: string;
52
+ startMs: number;
53
+ endMs: number;
54
+ allDay: boolean;
55
+ location: string;
56
+ notes: string;
57
+ etag: string;
58
+ };
59
+ export declare function localToCalendarEvent(local: {
60
+ title: string;
61
+ startMs: number;
62
+ endMs: number;
63
+ allDay?: boolean;
64
+ location?: string;
65
+ notes?: string;
66
+ }): any;
67
+ export declare function taskToLocal(t: GTask, accountId: string): {
68
+ providerId: string;
69
+ accountId: string;
70
+ title: string;
71
+ notes: string;
72
+ dueMs: number | undefined;
73
+ completedMs: number | undefined;
74
+ etag: string;
75
+ };
76
+ export declare function localToTask(local: {
77
+ title: string;
78
+ notes?: string;
79
+ dueMs?: number;
80
+ completedMs?: number;
81
+ }): any;
82
+ export {};
83
+ //# sourceMappingURL=google-sync.d.ts.map