mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-15 21:11:08 +02:00
feat(email): drafts, search, bulk read/unread, configurable backfill
Gmail client enhancements in apps/x, bundling four independent features:
- Drafts: save/autosave, update, delete, and list Gmail drafts. The
composer autosaves to a real Gmail draft (debounced ~1.5s) while
typing, reuses the thread's existing draft so edits update in place,
flushes a final save on close, and deletes the draft on discard. Adds
isDraft/draftId to the shared thread types. New core helpers:
saveThreadDraft, deleteThreadDraft, listDraftThreads,
buildDraftSnapshot, buildRawMimeMessage.
- Search: searchThreads(query, {limit}) backed by an on-disk snapshot
cache (read/writeSearchSnapshot). Extracts parseThreadSnapshot as the
shared parse core reused by both the cache-building sync and search.
- Read state: markThreadRead now takes a `read` flag so it toggles
read/unread; new markSectionRead marks a whole section
(important/other) read/unread and returns the affected count.
- Backfill: the onboarding/recovery sync is now bounded by a
configurable thread COUNT instead of a fixed 7-day window. New
gmail_sync_config.ts (getMaxEmails/setMaxEmails) backed by
~/.rowboat/config/gmail_sync.json, default 500, clamped to 1-5000;
seeded on first run.
New IPC channels: gmail:saveDraft, gmail:deleteDraft, gmail:getDrafts,
gmail:search, gmail:markSectionRead (plus a `read` field on
gmail:markThreadRead).
This commit is contained in:
parent
4a26bc9d80
commit
ab9bce6203
7 changed files with 1149 additions and 165 deletions
|
|
@ -124,6 +124,8 @@ export const GmailThreadMessageSchema = z.object({
|
|||
bodyHeight: z.number().int().positive().optional(),
|
||||
attachments: z.array(GmailAttachmentSchema).optional(),
|
||||
messageIdHeader: z.string().optional(),
|
||||
// Set on the unsent draft message within a thread (used by the Drafts view).
|
||||
isDraft: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type GmailThreadMessage = z.infer<typeof GmailThreadMessageSchema>;
|
||||
|
|
@ -134,6 +136,9 @@ export const GmailThreadSchema = EmailBlockSchema.extend({
|
|||
unread: z.boolean().optional(),
|
||||
importance: z.enum(['important', 'other']).optional(),
|
||||
gmail_draft: z.string().optional(),
|
||||
// Gmail-side draft id, present on entries returned by the Drafts list so the
|
||||
// composer can update/delete that exact draft.
|
||||
draftId: z.string().optional(),
|
||||
messages: z.array(GmailThreadMessageSchema),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,56 @@ const ipcSchemas = {
|
|||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'gmail:saveDraft': {
|
||||
req: z.object({
|
||||
// Existing Gmail draft to update; omitted on first save (creates a new one).
|
||||
draftId: z.string().min(1).optional(),
|
||||
threadId: z.string().min(1).optional(),
|
||||
// Recipients may be blank for a draft (unlike a send).
|
||||
to: z.string().optional(),
|
||||
cc: z.string().optional(),
|
||||
bcc: z.string().optional(),
|
||||
subject: z.string(),
|
||||
bodyHtml: z.string(),
|
||||
bodyText: z.string(),
|
||||
inReplyTo: z.string().optional(),
|
||||
references: z.string().optional(),
|
||||
attachments: z
|
||||
.array(
|
||||
z.object({
|
||||
filename: z.string(),
|
||||
mimeType: z.string(),
|
||||
contentBase64: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
draftId: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'gmail:deleteDraft': {
|
||||
req: z.object({ draftId: z.string().min(1) }),
|
||||
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
||||
},
|
||||
'gmail:getDrafts': {
|
||||
req: z.object({}),
|
||||
res: z.object({
|
||||
threads: z.array(GmailThreadSchema),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'gmail:search': {
|
||||
req: z.object({
|
||||
query: z.string(),
|
||||
limit: z.number().int().positive().optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
threads: z.array(GmailThreadSchema),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'gmail:getConnectionStatus': {
|
||||
req: z.object({}),
|
||||
res: z.object({
|
||||
|
|
@ -235,9 +285,13 @@ const ipcSchemas = {
|
|||
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
||||
},
|
||||
'gmail:markThreadRead': {
|
||||
req: z.object({ threadId: z.string().min(1) }),
|
||||
req: z.object({ threadId: z.string().min(1), read: z.boolean().optional() }),
|
||||
res: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
||||
},
|
||||
'gmail:markSectionRead': {
|
||||
req: z.object({ section: z.enum(['important', 'other']), read: z.boolean() }),
|
||||
res: z.object({ ok: z.boolean(), count: z.number().int().nonnegative(), error: z.string().optional() }),
|
||||
},
|
||||
'gmail:saveMessageHeight': {
|
||||
req: z.object({
|
||||
threadId: z.string().min(1),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue