mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
- Remove the "Mark as read" toggle for Everything else end-to-end (UI, IPC, config, sync auto-read, markSectionRead) — it could bulk-mark backlog and archived mail read on the server during cache-recovery syncs - Sending an edited draft now reuses the draft's stored In-Reply-To/References instead of self-referencing its own Message-ID (external clients thread correctly); standalone drafts send as fresh messages - Keep draft autosaves out of the sync pipeline: skip DRAFT history messages (no more md/knowledge-event leaks, phantom "New email" notifications, or per-autosave LLM reclassification) and mirror the draft body onto the cached snapshot surgically instead of waking the sync loop - Only recreate a Gmail draft when update fails with 404/410 — transient errors no longer pile up duplicates - Emptying a draft and closing now deletes it from Gmail (matches Gmail) - Forward keydown out of message iframes so j/k/e/r etc. keep working after clicking into an email body; restore expanded quotes across theme reloads - Composer footer wraps on narrow panes: formatting toolbar + Discard move as one unit instead of overflowing into each other Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const IFRAME_LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
|
|
export function isAllowedIframeUrl(url: string): boolean {
|
|
try {
|
|
const parsed = new URL(url);
|
|
if (parsed.protocol === 'https:') return true;
|
|
if (parsed.protocol !== 'http:') return false;
|
|
return IFRAME_LOCAL_HOSTS.has(parsed.hostname.toLowerCase());
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export const ImageBlockSchema = z.object({
|
|
src: z.string(),
|
|
alt: z.string().optional(),
|
|
caption: z.string().optional(),
|
|
});
|
|
|
|
export type ImageBlock = z.infer<typeof ImageBlockSchema>;
|
|
|
|
export const EmbedBlockSchema = z.object({
|
|
provider: z.enum(['youtube', 'figma', 'tweet', 'generic']),
|
|
url: z.string().url(),
|
|
caption: z.string().optional(),
|
|
});
|
|
|
|
export type EmbedBlock = z.infer<typeof EmbedBlockSchema>;
|
|
|
|
export const IframeBlockSchema = z.object({
|
|
url: z.string().url().refine(isAllowedIframeUrl, {
|
|
message: 'Iframe URLs must use https:// or local http://localhost / 127.0.0.1.',
|
|
}),
|
|
title: z.string().optional(),
|
|
caption: z.string().optional(),
|
|
height: z.number().int().min(240).max(1600).optional(),
|
|
allow: z.string().optional(),
|
|
});
|
|
|
|
export type IframeBlock = z.infer<typeof IframeBlockSchema>;
|
|
|
|
export const ChartBlockSchema = z.object({
|
|
chart: z.enum(['line', 'bar', 'pie']),
|
|
title: z.string().optional(),
|
|
data: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
source: z.string().optional(),
|
|
x: z.string(),
|
|
y: z.string(),
|
|
});
|
|
|
|
export type ChartBlock = z.infer<typeof ChartBlockSchema>;
|
|
|
|
export const TableBlockSchema = z.object({
|
|
columns: z.array(z.string()),
|
|
data: z.array(z.record(z.string(), z.unknown())),
|
|
title: z.string().optional(),
|
|
});
|
|
|
|
export type TableBlock = z.infer<typeof TableBlockSchema>;
|
|
|
|
export const CalendarEventSchema = z.object({
|
|
summary: z.string().optional(),
|
|
start: z.object({
|
|
dateTime: z.string().optional(),
|
|
date: z.string().optional(),
|
|
}).optional(),
|
|
end: z.object({
|
|
dateTime: z.string().optional(),
|
|
date: z.string().optional(),
|
|
}).optional(),
|
|
location: z.string().optional(),
|
|
htmlLink: z.string().optional(),
|
|
conferenceLink: z.string().optional(),
|
|
source: z.string().optional(),
|
|
});
|
|
|
|
export type CalendarEvent = z.infer<typeof CalendarEventSchema>;
|
|
|
|
export const CalendarBlockSchema = z.object({
|
|
title: z.string().optional(),
|
|
events: z.array(CalendarEventSchema),
|
|
showJoinButton: z.boolean().optional(),
|
|
});
|
|
|
|
export type CalendarBlock = z.infer<typeof CalendarBlockSchema>;
|
|
|
|
export const EmailBlockSchema = z.object({
|
|
threadId: z.string().optional(),
|
|
threadUrl: z.string().url().optional(),
|
|
summary: z.string().optional(),
|
|
subject: z.string().optional(),
|
|
from: z.string().optional(),
|
|
to: z.string().optional(),
|
|
date: z.string().optional(),
|
|
latest_email: z.string().optional(),
|
|
past_summary: z.string().optional(),
|
|
draft_response: z.string().optional(),
|
|
response_mode: z.enum(['inline', 'assistant', 'both']).optional(),
|
|
});
|
|
|
|
export type EmailBlock = z.infer<typeof EmailBlockSchema>;
|
|
|
|
export const GmailAttachmentSchema = z.object({
|
|
filename: z.string(),
|
|
mimeType: z.string().optional(),
|
|
sizeBytes: z.number().int().nonnegative().optional(),
|
|
savedPath: z.string(),
|
|
// Gmail identifiers used to fetch the attachment on demand when it hasn't
|
|
// been downloaded to disk yet (e.g. attachments on search results).
|
|
messageId: z.string().optional(),
|
|
attachmentId: z.string().optional(),
|
|
});
|
|
|
|
export type GmailAttachment = z.infer<typeof GmailAttachmentSchema>;
|
|
|
|
export const GmailThreadMessageSchema = z.object({
|
|
id: z.string().optional(),
|
|
from: z.string().optional(),
|
|
to: z.string().optional(),
|
|
cc: z.string().optional(),
|
|
date: z.string().optional(),
|
|
subject: z.string().optional(),
|
|
body: z.string().optional(),
|
|
bodyHtml: z.string().optional(),
|
|
unread: z.boolean().optional(),
|
|
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(),
|
|
// The draft's own stored In-Reply-To / References headers. Only set on
|
|
// draft messages: a Drafts-view pseudo-thread contains just the draft, so
|
|
// the composer can't rebuild the reply chain from thread messages and must
|
|
// reuse what the draft already carries.
|
|
inReplyToHeader: z.string().optional(),
|
|
referencesHeader: z.string().optional(),
|
|
});
|
|
|
|
export type GmailThreadMessage = z.infer<typeof GmailThreadMessageSchema>;
|
|
|
|
export const GmailThreadSchema = EmailBlockSchema.extend({
|
|
threadId: z.string(),
|
|
threadUrl: z.string().url(),
|
|
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),
|
|
});
|
|
|
|
export type GmailThread = z.infer<typeof GmailThreadSchema>;
|
|
|
|
export const EmailsBlockSchema = z.object({
|
|
title: z.string().optional(),
|
|
emails: z.array(EmailBlockSchema),
|
|
});
|
|
|
|
export type EmailsBlock = z.infer<typeof EmailsBlockSchema>;
|
|
|
|
export const TranscriptBlockSchema = z.object({
|
|
transcript: z.string(),
|
|
});
|
|
|
|
export type TranscriptBlock = z.infer<typeof TranscriptBlockSchema>;
|
|
|
|
export const SuggestedTopicBlockSchema = z.object({
|
|
title: z.string(),
|
|
description: z.string(),
|
|
category: z.string().optional(),
|
|
});
|
|
|
|
export type SuggestedTopicBlock = z.infer<typeof SuggestedTopicBlockSchema>;
|