kennel 0.2.3 → 0.2.5
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 +23 -13
- package/dist/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/usage.js +1 -1
- package/dist/cli/src/core/command.js +1 -0
- package/dist/cli/src/core/init.js +5 -5
- package/dist/cli/src/plugins/external.d.ts +28 -2
- package/dist/cli/src/plugins/external.js +75 -21
- package/dist/cli/src/site/server-plugins.d.ts +2 -7
- package/dist/cli/src/site/viewer.js +1 -0
- package/dist/cli/usage.js +1 -1
- package/dist/core/command.js +1 -0
- package/dist/core/init.js +5 -5
- package/dist/core/runtime.js +357 -3207
- package/dist/package/preview.js +1232 -5113
- package/dist/plugins/external.d.ts +28 -2
- package/dist/plugins/external.js +75 -21
- package/dist/site/server-plugins.d.ts +2 -7
- package/dist/site/server-plugins.js +552 -10447
- package/dist/site/viewer.js +1 -0
- package/kernel/apps/web/package.json +1 -5
- package/kernel/apps/web/scripts/dev.mjs +53 -0
- package/kernel/apps/web/src/app.css +0 -3
- package/kernel/apps/web/src/main.tsx +0 -1
- package/kernel/apps/web/src/plugins.ts +0 -30
- package/kernel/apps/web/tsconfig.json +1 -2
- package/kernel/apps/web/vite.config.ts +31 -13
- package/kernel/packages/api/package.json +0 -4
- package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
- package/kernel/packages/api/src/server/index.ts +0 -1
- package/kernel/packages/api/src/server/plugins.ts +49 -16
- package/kernel/packages/api/test/core.test.ts +13 -25
- package/kernel/packages/markdown/README.md +1 -1
- package/package.json +1 -1
- package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
- package/web/assets/app-CX20nNIa.js +2 -0
- package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
- package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
- package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
- package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
- package/web/assets/channel-X7WHyxxT.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
- package/web/assets/client-7OWw5kW2.js +6 -0
- package/web/assets/client-DYj_td4Q.js +17 -0
- package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
- package/web/assets/command-zkzH4hVz.js +38 -0
- package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
- package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
- package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
- package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
- package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
- package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
- package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
- package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
- package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
- package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
- package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
- package/web/assets/index-CegvVQ1C.js +1 -0
- package/web/assets/index-Dwhf8F_U.css +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
- package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
- package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
- package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
- package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
- package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
- package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
- package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
- package/web/assets/select-CtXC_PLC.js +30 -0
- package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
- package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
- package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
- package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
- package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
- package/web/index.html +2 -2
- package/kernel/packages/api/test/partial-loading.test.ts +0 -43
- package/kernel/plugins/calendar/SKILL.md +0 -173
- package/kernel/plugins/calendar/eslint.config.js +0 -10
- package/kernel/plugins/calendar/package.json +0 -73
- package/kernel/plugins/calendar/schema.json +0 -137
- package/kernel/plugins/calendar/src/calendar.tsx +0 -328
- package/kernel/plugins/calendar/src/client.tsx +0 -139
- package/kernel/plugins/calendar/src/context.ts +0 -48
- package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
- package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
- package/kernel/plugins/calendar/src/index.ts +0 -4
- package/kernel/plugins/calendar/src/manifest.ts +0 -7
- package/kernel/plugins/calendar/src/parse.ts +0 -610
- package/kernel/plugins/calendar/src/server.ts +0 -160
- package/kernel/plugins/calendar/src/utils.ts +0 -80
- package/kernel/plugins/calendar/src/views.tsx +0 -418
- package/kernel/plugins/calendar/test/context.test.mjs +0 -60
- package/kernel/plugins/calendar/test/package.test.mjs +0 -43
- package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
- package/kernel/plugins/calendar/test/server.test.mjs +0 -126
- package/kernel/plugins/calendar/tsconfig.json +0 -17
- package/kernel/plugins/mail/SKILL.md +0 -97
- package/kernel/plugins/mail/eslint.config.js +0 -10
- package/kernel/plugins/mail/package.json +0 -79
- package/kernel/plugins/mail/src/api.ts +0 -359
- package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
- package/kernel/plugins/mail/src/client.tsx +0 -14
- package/kernel/plugins/mail/src/flock.ts +0 -137
- package/kernel/plugins/mail/src/folders.ts +0 -37
- package/kernel/plugins/mail/src/history.ts +0 -485
- package/kernel/plugins/mail/src/index.ts +0 -3
- package/kernel/plugins/mail/src/mail-card.tsx +0 -123
- package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
- package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
- package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
- package/kernel/plugins/mail/src/mail-page.tsx +0 -109
- package/kernel/plugins/mail/src/mail-request.ts +0 -78
- package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
- package/kernel/plugins/mail/src/mailos.ts +0 -723
- package/kernel/plugins/mail/src/manifest.ts +0 -7
- package/kernel/plugins/mail/src/node.ts +0 -171
- package/kernel/plugins/mail/src/parse.ts +0 -139
- package/kernel/plugins/mail/src/server.ts +0 -125
- package/kernel/plugins/mail/src/types.ts +0 -10
- package/kernel/plugins/mail/src/viewer.tsx +0 -147
- package/kernel/plugins/mail/test/flock.test.mjs +0 -156
- package/kernel/plugins/mail/test/folders.test.mjs +0 -138
- package/kernel/plugins/mail/test/history.test.mjs +0 -460
- package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
- package/kernel/plugins/mail/test/node.test.mjs +0 -207
- package/kernel/plugins/mail/test/package.test.mjs +0 -54
- package/kernel/plugins/mail/test/parse.test.mjs +0 -32
- package/kernel/plugins/mail/test/server.test.mjs +0 -116
- package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
- package/kernel/plugins/mail/tsconfig.json +0 -17
- package/kernel/plugins/projects/SKILL.md +0 -73
- package/kernel/plugins/projects/eslint.config.js +0 -10
- package/kernel/plugins/projects/package.json +0 -70
- package/kernel/plugins/projects/src/client.tsx +0 -17
- package/kernel/plugins/projects/src/data/model.ts +0 -129
- package/kernel/plugins/projects/src/index.ts +0 -17
- package/kernel/plugins/projects/src/manifest.ts +0 -7
- package/kernel/plugins/projects/src/project-context.ts +0 -41
- package/kernel/plugins/projects/src/server.ts +0 -276
- package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
- package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
- package/kernel/plugins/projects/test/package.test.mjs +0 -51
- package/kernel/plugins/projects/tsconfig.json +0 -17
- package/kernel/plugins/tasks/SKILL.md +0 -341
- package/kernel/plugins/tasks/eslint.config.js +0 -10
- package/kernel/plugins/tasks/package.json +0 -74
- package/kernel/plugins/tasks/schema.json +0 -939
- package/kernel/plugins/tasks/src/api.ts +0 -212
- package/kernel/plugins/tasks/src/client.tsx +0 -23
- package/kernel/plugins/tasks/src/data/parse.ts +0 -544
- package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
- package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
- package/kernel/plugins/tasks/src/index.ts +0 -34
- package/kernel/plugins/tasks/src/manifest.ts +0 -7
- package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
- package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
- package/kernel/plugins/tasks/src/server.ts +0 -447
- package/kernel/plugins/tasks/src/styles.css +0 -9
- package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
- package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
- package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
- package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
- package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
- package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
- package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
- package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
- package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
- package/kernel/plugins/tasks/test/api.test.mjs +0 -99
- package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
- package/kernel/plugins/tasks/test/package.test.mjs +0 -54
- package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
- package/kernel/plugins/tasks/test/server.test.mjs +0 -386
- package/kernel/plugins/tasks/tsconfig.json +0 -17
- package/web/assets/app-DoGr2Zi7.js +0 -2
- package/web/assets/channel-uMEjV8qc.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
- package/web/assets/client-C6Nxdfif.js +0 -2
- package/web/assets/client-CXYfBko5.js +0 -17
- package/web/assets/client-CoKtOq-X.js +0 -7
- package/web/assets/client-DyRQz5RU.js +0 -53
- package/web/assets/client-OY6HUzuD.js +0 -98
- package/web/assets/client-YOYWZIFi.js +0 -34
- package/web/assets/command-BugZBGXE.js +0 -38
- package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
- package/web/assets/index-BaRnbYRw.js +0 -1
- package/web/assets/index-BexmLxIP.css +0 -1
- package/web/assets/index-DDirZMgx.js +0 -1
- package/web/assets/note-card-DBfX6gW1.js +0 -6
- package/web/assets/select-D8cqNgcp.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
- package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react"
|
|
2
|
-
import {
|
|
3
|
-
Archive,
|
|
4
|
-
ArrowLeft,
|
|
5
|
-
ExternalLink,
|
|
6
|
-
RefreshCw,
|
|
7
|
-
Trash2,
|
|
8
|
-
X,
|
|
9
|
-
} from "lucide-react"
|
|
10
|
-
|
|
11
|
-
import type { FolderPluginViewProps } from "@raggle-ai/folder-plugin/client"
|
|
12
|
-
import { BarkdownMarkdown } from "@raggle-ai/markdown"
|
|
13
|
-
import { CopyContextButton } from "@raggle-ai/ui/components/copy-context"
|
|
14
|
-
import { FilePreview } from "@raggle-ai/ui/components/file-preview"
|
|
15
|
-
import { toast } from "@raggle-ai/ui/components/sonner"
|
|
16
|
-
|
|
17
|
-
import type { MailPreview } from "./types.js"
|
|
18
|
-
import type { MailDetail } from "./viewer.js"
|
|
19
|
-
import { MailConversationPanel } from "./mail-conversation.js"
|
|
20
|
-
import { useMailAction, useMailDetail, useMailSender } from "./mail-hooks.js"
|
|
21
|
-
import { mailErrorText } from "./mail-request.js"
|
|
22
|
-
import { MailSendBox } from "./mail-send-box.js"
|
|
23
|
-
import { mailItemContext } from "./viewer.js"
|
|
24
|
-
|
|
25
|
-
function displayTimestamp(value: string | null) {
|
|
26
|
-
if (!value) return "—"
|
|
27
|
-
const parsed = Date.parse(value)
|
|
28
|
-
if (Number.isNaN(parsed)) return value
|
|
29
|
-
return new Date(parsed).toLocaleString(undefined, {
|
|
30
|
-
dateStyle: "medium",
|
|
31
|
-
timeStyle: "short",
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function MailMetaRow({ label, value }: { label: string; value: ReactNode }) {
|
|
36
|
-
return (
|
|
37
|
-
<div className="grid grid-cols-[88px_1fr] items-start gap-x-3 gap-y-1">
|
|
38
|
-
<span
|
|
39
|
-
className="pt-0.5 text-[0.75rem] font-medium tracking-wide
|
|
40
|
-
text-[#6c7a6d] uppercase"
|
|
41
|
-
>
|
|
42
|
-
{label}
|
|
43
|
-
</span>
|
|
44
|
-
<span className="min-w-0 text-sm break-words">{value}</span>
|
|
45
|
-
</div>
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function RecipientItems({ items, max }: { items: string[]; max?: number }) {
|
|
50
|
-
if (!items.length) {
|
|
51
|
-
return <span className="text-[#6c7a6d]">—</span>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const visible = max ? items.slice(0, max) : items
|
|
55
|
-
const extra = max && items.length > max ? items.length - max : 0
|
|
56
|
-
const pillClassName =
|
|
57
|
-
"rounded-md bg-[#f3f5f2] px-2 py-1 text-[0.85rem] text-[#2f382f]"
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<span className="flex min-w-0 flex-wrap gap-1">
|
|
61
|
-
{visible.map((recipient) => (
|
|
62
|
-
<span key={recipient} className={pillClassName} title={recipient}>
|
|
63
|
-
<span className="break-all">{recipient}</span>
|
|
64
|
-
</span>
|
|
65
|
-
))}
|
|
66
|
-
{extra ? (
|
|
67
|
-
<span className={pillClassName} title={`${extra} more recipients`}>
|
|
68
|
-
+{extra}
|
|
69
|
-
</span>
|
|
70
|
-
) : null}
|
|
71
|
-
</span>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Attachment cards with an optional per-card remove control. The remove
|
|
77
|
-
* button only unlinks the file from the draft's frontmatter `file:` list;
|
|
78
|
-
* the file stays on disk.
|
|
79
|
-
*/
|
|
80
|
-
function AttachmentList({
|
|
81
|
-
paths,
|
|
82
|
-
pending,
|
|
83
|
-
onRemove,
|
|
84
|
-
}: {
|
|
85
|
-
paths: string[]
|
|
86
|
-
pending?: boolean
|
|
87
|
-
onRemove?: (attachment: string) => void
|
|
88
|
-
}) {
|
|
89
|
-
return (
|
|
90
|
-
<span className="flex min-w-0 flex-wrap gap-2">
|
|
91
|
-
{paths.map((path) => {
|
|
92
|
-
const name = path.slice(path.lastIndexOf("/") + 1)
|
|
93
|
-
return (
|
|
94
|
-
<span key={path} className="relative">
|
|
95
|
-
<FilePreview path={path} />
|
|
96
|
-
{onRemove ? (
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
aria-label={`Remove ${name} from this draft`}
|
|
100
|
-
title={`Remove ${name} from this draft}`}
|
|
101
|
-
className="absolute top-1.5 right-1.5 z-10 flex size-6
|
|
102
|
-
cursor-pointer items-center justify-center rounded-full border
|
|
103
|
-
border-[#e5c4c4] bg-white text-[#9c3a3a] shadow-xs
|
|
104
|
-
hover:bg-[#fbf3f3] disabled:pointer-events-none
|
|
105
|
-
disabled:opacity-50"
|
|
106
|
-
onClick={() => onRemove(path)}
|
|
107
|
-
disabled={pending}
|
|
108
|
-
>
|
|
109
|
-
<X className="size-3.5" />
|
|
110
|
-
</button>
|
|
111
|
-
) : null}
|
|
112
|
-
</span>
|
|
113
|
-
)
|
|
114
|
-
})}
|
|
115
|
-
</span>
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function MailDetailView({
|
|
120
|
-
mail,
|
|
121
|
-
error,
|
|
122
|
-
preview,
|
|
123
|
-
refreshing,
|
|
124
|
-
onRefresh,
|
|
125
|
-
onArchive,
|
|
126
|
-
onDelete,
|
|
127
|
-
actionPending,
|
|
128
|
-
onOpenPath,
|
|
129
|
-
onRemoveAttachment,
|
|
130
|
-
}: {
|
|
131
|
-
mail: MailDetail
|
|
132
|
-
error?: string
|
|
133
|
-
preview?: MailPreview
|
|
134
|
-
refreshing: boolean
|
|
135
|
-
onRefresh: () => void
|
|
136
|
-
/** Archive this draft; omit to hide the Archive button. */
|
|
137
|
-
onArchive?: () => void
|
|
138
|
-
/** Delete this draft; omit to hide the Delete button. */
|
|
139
|
-
onDelete?: () => void
|
|
140
|
-
/** True while an archive or delete request is running. */
|
|
141
|
-
actionPending?: boolean
|
|
142
|
-
/** Open a physical file in the host's default app; omit to hide the button. */
|
|
143
|
-
onOpenPath?: (path: string) => Promise<unknown>
|
|
144
|
-
/** Unlink one attachment from this draft; omit to hide the remove button. */
|
|
145
|
-
onRemoveAttachment?: (attachment: string) => void
|
|
146
|
-
}) {
|
|
147
|
-
return (
|
|
148
|
-
<div className="grid min-w-0 grid-cols-1 gap-4">
|
|
149
|
-
<header className="flex flex-wrap items-start justify-between gap-3">
|
|
150
|
-
<div className="min-w-0">
|
|
151
|
-
<h1 className="m-0 text-2xl font-semibold break-words text-[#233025]">
|
|
152
|
-
{mail.subject ?? "(no subject)"}
|
|
153
|
-
</h1>
|
|
154
|
-
<p className="m-0 mt-1 text-sm text-[#6c7a6d]">
|
|
155
|
-
Updated:{" "}
|
|
156
|
-
{(mail.updatedAt ?? mail.createdAt) ? (
|
|
157
|
-
<time
|
|
158
|
-
dateTime={mail.updatedAt ?? mail.createdAt ?? undefined}
|
|
159
|
-
title={mail.updatedAt ?? mail.createdAt ?? undefined}
|
|
160
|
-
>
|
|
161
|
-
{displayTimestamp(mail.updatedAt ?? mail.createdAt)}
|
|
162
|
-
</time>
|
|
163
|
-
) : (
|
|
164
|
-
"—"
|
|
165
|
-
)}
|
|
166
|
-
</p>
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
170
|
-
<CopyContextButton
|
|
171
|
-
className="border bg-white text-[#4a554b] shadow-xs
|
|
172
|
-
hover:bg-[#f3f5f2]"
|
|
173
|
-
context={() => mailItemContext(mail, preview?.body ?? mail.body)}
|
|
174
|
-
label={`Copy full context for ${mail.subject ?? "this draft"}`}
|
|
175
|
-
size="sm"
|
|
176
|
-
variant="ghost"
|
|
177
|
-
/>
|
|
178
|
-
<CopyContextButton
|
|
179
|
-
className="border bg-white text-[#4a554b] shadow-xs
|
|
180
|
-
hover:bg-[#f3f5f2]"
|
|
181
|
-
context={mail.filePath}
|
|
182
|
-
label="Copy draft path"
|
|
183
|
-
size="sm"
|
|
184
|
-
variant="ghost"
|
|
185
|
-
/>
|
|
186
|
-
<button
|
|
187
|
-
type="button"
|
|
188
|
-
className="inline-flex items-center gap-2 rounded-md border px-3
|
|
189
|
-
py-1.5 text-sm"
|
|
190
|
-
onClick={onRefresh}
|
|
191
|
-
disabled={refreshing}
|
|
192
|
-
>
|
|
193
|
-
<RefreshCw
|
|
194
|
-
className={refreshing ? "size-4 animate-spin" : "size-4"}
|
|
195
|
-
/>
|
|
196
|
-
Refresh
|
|
197
|
-
</button>
|
|
198
|
-
{onOpenPath ? (
|
|
199
|
-
<button
|
|
200
|
-
type="button"
|
|
201
|
-
className="inline-flex cursor-pointer items-center gap-2
|
|
202
|
-
rounded-md border px-3 py-1.5 text-sm"
|
|
203
|
-
onClick={() => void onOpenPath(mail.filePath)}
|
|
204
|
-
>
|
|
205
|
-
<ExternalLink className="size-4" />
|
|
206
|
-
Open
|
|
207
|
-
</button>
|
|
208
|
-
) : null}
|
|
209
|
-
{onArchive && mail.status !== "sent" ? (
|
|
210
|
-
<button
|
|
211
|
-
type="button"
|
|
212
|
-
className="inline-flex items-center gap-2 rounded-md border px-3
|
|
213
|
-
py-1.5 text-sm disabled:pointer-events-none disabled:opacity-50"
|
|
214
|
-
onClick={onArchive}
|
|
215
|
-
disabled={actionPending}
|
|
216
|
-
>
|
|
217
|
-
<Archive className="size-4" />
|
|
218
|
-
Archive
|
|
219
|
-
</button>
|
|
220
|
-
) : null}
|
|
221
|
-
{onDelete ? (
|
|
222
|
-
<button
|
|
223
|
-
type="button"
|
|
224
|
-
className="inline-flex items-center gap-2 rounded-md border
|
|
225
|
-
border-[#e5c4c4] px-3 py-1.5 text-sm text-[#9c3a3a]
|
|
226
|
-
hover:bg-[#fbf3f3] disabled:pointer-events-none
|
|
227
|
-
disabled:opacity-50"
|
|
228
|
-
onClick={onDelete}
|
|
229
|
-
disabled={actionPending}
|
|
230
|
-
>
|
|
231
|
-
<Trash2 className="size-4" />
|
|
232
|
-
Delete
|
|
233
|
-
</button>
|
|
234
|
-
) : null}
|
|
235
|
-
</div>
|
|
236
|
-
</header>
|
|
237
|
-
|
|
238
|
-
<div
|
|
239
|
-
className="grid grid-cols-1 gap-3 rounded-xl border border-[#e7ebe7]
|
|
240
|
-
bg-white px-4 py-4 shadow-[0_1px_2px_rgb(31_41_55/0.05)]"
|
|
241
|
-
>
|
|
242
|
-
{mail.from ? <MailMetaRow label="From" value={mail.from} /> : null}
|
|
243
|
-
{mail.subject ? (
|
|
244
|
-
<MailMetaRow label="Subject" value={mail.subject} />
|
|
245
|
-
) : null}
|
|
246
|
-
<MailMetaRow label="To" value={<RecipientItems items={mail.to} />} />
|
|
247
|
-
<MailMetaRow label="CC" value={<RecipientItems items={mail.cc} />} />
|
|
248
|
-
<MailMetaRow label="BCC" value={<RecipientItems items={mail.bcc} />} />
|
|
249
|
-
{mail.noSignature ? (
|
|
250
|
-
<MailMetaRow label="Signature" value="none" />
|
|
251
|
-
) : null}
|
|
252
|
-
{mail.attachments.length ? (
|
|
253
|
-
<MailMetaRow
|
|
254
|
-
label="Attachments"
|
|
255
|
-
value={
|
|
256
|
-
<AttachmentList
|
|
257
|
-
paths={mail.attachments}
|
|
258
|
-
pending={actionPending}
|
|
259
|
-
onRemove={onRemoveAttachment}
|
|
260
|
-
/>
|
|
261
|
-
}
|
|
262
|
-
/>
|
|
263
|
-
) : null}
|
|
264
|
-
</div>
|
|
265
|
-
|
|
266
|
-
{error ? <p className="m-0 text-sm text-[#9c3a3a]">{error}</p> : null}
|
|
267
|
-
|
|
268
|
-
{preview ? (
|
|
269
|
-
<article
|
|
270
|
-
className="overflow-hidden rounded-xl border border-[#e7ebe7] bg-white
|
|
271
|
-
shadow-[0_1px_2px_rgb(31_41_55/0.05)]"
|
|
272
|
-
>
|
|
273
|
-
<section className="px-4 py-4">
|
|
274
|
-
{!preview.body.trim() && !preview.signature ? (
|
|
275
|
-
<p className="m-0 text-sm text-[#6c7a6d]">(empty body)</p>
|
|
276
|
-
) : (
|
|
277
|
-
<div className="prose prose-neutral max-w-none">
|
|
278
|
-
<BarkdownMarkdown
|
|
279
|
-
value={
|
|
280
|
-
[preview.body, preview.signature]
|
|
281
|
-
.filter(Boolean)
|
|
282
|
-
.join("\n\n") as string
|
|
283
|
-
}
|
|
284
|
-
/>
|
|
285
|
-
</div>
|
|
286
|
-
)}
|
|
287
|
-
</section>
|
|
288
|
-
</article>
|
|
289
|
-
) : null}
|
|
290
|
-
</div>
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function MailDetailPage({
|
|
295
|
-
folder,
|
|
296
|
-
subpath: mailId,
|
|
297
|
-
listHref,
|
|
298
|
-
hostActions,
|
|
299
|
-
}: Pick<
|
|
300
|
-
FolderPluginViewProps,
|
|
301
|
-
"folder" | "subpath" | "listHref" | "hostActions"
|
|
302
|
-
>) {
|
|
303
|
-
const query = useMailDetail(folder, mailId)
|
|
304
|
-
const action = useMailAction(folder)
|
|
305
|
-
|
|
306
|
-
async function handleArchive() {
|
|
307
|
-
if (!mail) return
|
|
308
|
-
try {
|
|
309
|
-
await action.mutateAsync({
|
|
310
|
-
action: "archive",
|
|
311
|
-
id: mailId,
|
|
312
|
-
path: folder ?? "",
|
|
313
|
-
revision: mail.revision,
|
|
314
|
-
})
|
|
315
|
-
void query.refetch()
|
|
316
|
-
} catch (error) {
|
|
317
|
-
toast.error(mailErrorText(error))
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async function handleRemoveAttachment(attachment: string) {
|
|
322
|
-
if (!mail) return
|
|
323
|
-
if (
|
|
324
|
-
!window.confirm(
|
|
325
|
-
`Remove "${attachment}" from this draft? The file is kept on disk.`,
|
|
326
|
-
)
|
|
327
|
-
)
|
|
328
|
-
return
|
|
329
|
-
try {
|
|
330
|
-
await action.mutateAsync({
|
|
331
|
-
action: "remove-attachment",
|
|
332
|
-
id: mailId,
|
|
333
|
-
path: folder ?? "",
|
|
334
|
-
revision: mail.revision,
|
|
335
|
-
attachment,
|
|
336
|
-
})
|
|
337
|
-
void query.refetch()
|
|
338
|
-
} catch (error) {
|
|
339
|
-
toast.error(mailErrorText(error))
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async function handleDelete() {
|
|
344
|
-
if (!mail) return
|
|
345
|
-
if (
|
|
346
|
-
!window.confirm(
|
|
347
|
-
"Delete this draft? It will be moved to draft history first.",
|
|
348
|
-
)
|
|
349
|
-
)
|
|
350
|
-
return
|
|
351
|
-
try {
|
|
352
|
-
await action.mutateAsync({
|
|
353
|
-
action: "delete",
|
|
354
|
-
id: mailId,
|
|
355
|
-
path: folder ?? "",
|
|
356
|
-
revision: mail.revision,
|
|
357
|
-
})
|
|
358
|
-
window.location.assign(listHref)
|
|
359
|
-
} catch (error) {
|
|
360
|
-
toast.error(mailErrorText(error))
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const senderNeeded = Boolean(query.data?.mail && !query.data.mail.from)
|
|
365
|
-
const fromQuery = useMailSender(folder, mailId, senderNeeded)
|
|
366
|
-
|
|
367
|
-
const resolvedFrom =
|
|
368
|
-
query.data?.mail?.from ??
|
|
369
|
-
(fromQuery.data?.name && fromQuery.data?.from
|
|
370
|
-
? `${fromQuery.data.name} <${fromQuery.data.from}>`
|
|
371
|
-
: (fromQuery.data?.from ?? null))
|
|
372
|
-
const mail = query.data?.mail
|
|
373
|
-
? { ...query.data.mail, from: resolvedFrom }
|
|
374
|
-
: undefined
|
|
375
|
-
|
|
376
|
-
const conversationEnabled = Boolean(
|
|
377
|
-
query.data?.mail &&
|
|
378
|
-
(query.data.mail.inReplyTo ?? query.data.mail.references.length > 0),
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
if (!folder || !mailId) {
|
|
382
|
-
return (
|
|
383
|
-
<div
|
|
384
|
-
className="mx-auto grid w-full max-w-3xl min-w-0 grid-cols-1 gap-4 px-4
|
|
385
|
-
py-5"
|
|
386
|
-
>
|
|
387
|
-
<p className="m-0 text-sm text-[#6c7a6d]">
|
|
388
|
-
Select a mail from the list.
|
|
389
|
-
</p>
|
|
390
|
-
<a
|
|
391
|
-
className="w-fit cursor-pointer rounded-md border border-[#dde1dc]
|
|
392
|
-
bg-white px-3 py-1.5 text-[0.85rem] text-[#4a554b]
|
|
393
|
-
hover:bg-[#f3f5f2]"
|
|
394
|
-
href={listHref}
|
|
395
|
-
>
|
|
396
|
-
Back
|
|
397
|
-
</a>
|
|
398
|
-
</div>
|
|
399
|
-
)
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return (
|
|
403
|
-
<div
|
|
404
|
-
className="mx-auto grid w-full max-w-3xl min-w-0 grid-cols-1 gap-4 px-4
|
|
405
|
-
py-5"
|
|
406
|
-
>
|
|
407
|
-
<a
|
|
408
|
-
className="inline-flex w-fit items-center gap-2 rounded-md border
|
|
409
|
-
border-[#dde1dc] bg-white px-3 py-1.5 text-sm text-[#4a554b]
|
|
410
|
-
no-underline hover:bg-[#f3f5f2]"
|
|
411
|
-
href={listHref}
|
|
412
|
-
>
|
|
413
|
-
<ArrowLeft className="size-4" />
|
|
414
|
-
Back to Mail
|
|
415
|
-
</a>
|
|
416
|
-
|
|
417
|
-
{query.isPending ? (
|
|
418
|
-
<p className="m-0 text-sm text-[#6c7a6d]">Loading…</p>
|
|
419
|
-
) : null}
|
|
420
|
-
|
|
421
|
-
{query.error && !query.data?.mail ? (
|
|
422
|
-
<p className="m-0 text-sm text-[#9c3a3a]">
|
|
423
|
-
Could not load mail preview.
|
|
424
|
-
</p>
|
|
425
|
-
) : null}
|
|
426
|
-
|
|
427
|
-
{mail ? (
|
|
428
|
-
<MailDetailView
|
|
429
|
-
mail={mail}
|
|
430
|
-
preview={query.error ? undefined : query.data?.preview}
|
|
431
|
-
error={
|
|
432
|
-
query.error
|
|
433
|
-
? "Could not load mail preview."
|
|
434
|
-
: query.data?.preview
|
|
435
|
-
? undefined
|
|
436
|
-
: "Could not load mail preview."
|
|
437
|
-
}
|
|
438
|
-
refreshing={query.isFetching}
|
|
439
|
-
onRefresh={() => void query.refetch()}
|
|
440
|
-
onArchive={
|
|
441
|
-
mail.status === "sent" ? undefined : () => void handleArchive()
|
|
442
|
-
}
|
|
443
|
-
onDelete={() => void handleDelete()}
|
|
444
|
-
actionPending={action.isPending}
|
|
445
|
-
onOpenPath={hostActions?.openPath}
|
|
446
|
-
onRemoveAttachment={(attachment) =>
|
|
447
|
-
void handleRemoveAttachment(attachment)
|
|
448
|
-
}
|
|
449
|
-
/>
|
|
450
|
-
) : null}
|
|
451
|
-
|
|
452
|
-
<MailConversationPanel
|
|
453
|
-
enabled={conversationEnabled}
|
|
454
|
-
folder={folder}
|
|
455
|
-
mailId={mailId}
|
|
456
|
-
/>
|
|
457
|
-
|
|
458
|
-
{mail ? (
|
|
459
|
-
<MailSendBox folder={folder} mailId={mailId} status={mail.status} />
|
|
460
|
-
) : null}
|
|
461
|
-
|
|
462
|
-
{!query.isPending && !query.error && !query.data?.mail ? (
|
|
463
|
-
<p className="m-0 text-sm text-[#6c7a6d]">Mail not found.</p>
|
|
464
|
-
) : null}
|
|
465
|
-
</div>
|
|
466
|
-
)
|
|
467
|
-
}
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
|
2
|
-
|
|
3
|
-
import { folderPathId } from "@raggle-ai/folder-plugin/client"
|
|
4
|
-
|
|
5
|
-
import { mailRequest } from "./mail-request.js"
|
|
6
|
-
|
|
7
|
-
export type MailListResponse = {
|
|
8
|
-
folder: string
|
|
9
|
-
items: MailListItemPayload[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type MailListItemPayload = {
|
|
13
|
-
attachments: string[]
|
|
14
|
-
bcc: string[]
|
|
15
|
-
cc: string[]
|
|
16
|
-
createdAt: string | null
|
|
17
|
-
excerpt: string
|
|
18
|
-
filePath: string
|
|
19
|
-
folder: string | null
|
|
20
|
-
from: string | null
|
|
21
|
-
id: string
|
|
22
|
-
inReplyTo: string | null
|
|
23
|
-
references: string[]
|
|
24
|
-
revision: string
|
|
25
|
-
status: string | null
|
|
26
|
-
subject: string | null
|
|
27
|
-
tags: string[]
|
|
28
|
-
to: string[]
|
|
29
|
-
updatedAt: string | null
|
|
30
|
-
noSignature: boolean
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type MailDetailPayload = Omit<MailListItemPayload, "excerpt"> & {
|
|
34
|
-
body: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type MailSenderPayload = {
|
|
38
|
-
from: string | null
|
|
39
|
-
name: string | null
|
|
40
|
-
ok: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type ConversationMessagePayload = {
|
|
44
|
-
body: string
|
|
45
|
-
date: string | null
|
|
46
|
-
from: string | null
|
|
47
|
-
id: string
|
|
48
|
-
subject: string | null
|
|
49
|
-
to: string | null
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type ConversationOutcome =
|
|
53
|
-
"empty" | "ok" | "partial" | "not-found" | "timeout"
|
|
54
|
-
|
|
55
|
-
export type ConversationPayload = {
|
|
56
|
-
items: ConversationMessagePayload[]
|
|
57
|
-
missing: string[]
|
|
58
|
-
outcome: ConversationOutcome
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export type MailSendPreviewPayload = {
|
|
62
|
-
dryRun: true
|
|
63
|
-
output: string
|
|
64
|
-
revision: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type MailSendResultPayload = {
|
|
68
|
-
dryRun: false
|
|
69
|
-
output: string
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function mailPath(folder: string | null | undefined) {
|
|
73
|
-
return { path: folder ?? "" }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function folderCacheKey(folder: string | null | undefined) {
|
|
77
|
-
return ["folder", folder ? folderPathId(folder) : "none", "mail"] as const
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function useMailList(folder: string | null | undefined) {
|
|
81
|
-
return useQuery({
|
|
82
|
-
enabled: Boolean(folder),
|
|
83
|
-
gcTime: 5 * 60_000,
|
|
84
|
-
queryFn: ({ signal }) =>
|
|
85
|
-
mailRequest<MailListResponse>("/api/mail", {
|
|
86
|
-
search: mailPath(folder),
|
|
87
|
-
signal,
|
|
88
|
-
}),
|
|
89
|
-
queryKey: [...folderCacheKey(folder)],
|
|
90
|
-
staleTime: 15_000,
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export type MailPreviewPayload = {
|
|
95
|
-
body: string
|
|
96
|
-
signature: string | null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export type MailDetailResponsePayload = {
|
|
100
|
-
mail: MailDetailPayload
|
|
101
|
-
preview?: MailPreviewPayload
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function useMailDetail(
|
|
105
|
-
folder: string | null | undefined,
|
|
106
|
-
mailId: string | null | undefined,
|
|
107
|
-
) {
|
|
108
|
-
return useQuery<MailDetailResponsePayload>({
|
|
109
|
-
enabled: Boolean(folder && mailId),
|
|
110
|
-
gcTime: 5 * 60_000,
|
|
111
|
-
queryFn: ({ signal }) =>
|
|
112
|
-
mailRequest<MailDetailResponsePayload>("/api/mail/item", {
|
|
113
|
-
search: { ...mailPath(folder), id: mailId ?? "" },
|
|
114
|
-
signal,
|
|
115
|
-
}),
|
|
116
|
-
queryKey: [...folderCacheKey(folder), mailId ?? ""],
|
|
117
|
-
retry: false,
|
|
118
|
-
staleTime: 15_000,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Lazy sender resolution: runs only when the draft has no recorded From. */
|
|
123
|
-
export function useMailSender(
|
|
124
|
-
folder: string | null | undefined,
|
|
125
|
-
mailId: string | null | undefined,
|
|
126
|
-
enabled: boolean,
|
|
127
|
-
) {
|
|
128
|
-
return useQuery({
|
|
129
|
-
enabled: Boolean(enabled && mailId),
|
|
130
|
-
gcTime: 5 * 60_000,
|
|
131
|
-
queryFn: ({ signal }) =>
|
|
132
|
-
mailRequest<MailSenderPayload>("/api/mail/from", {
|
|
133
|
-
search: { ...mailPath(folder), id: mailId ?? "" },
|
|
134
|
-
signal,
|
|
135
|
-
}),
|
|
136
|
-
queryKey: [...folderCacheKey(folder), mailId ?? "", "from"],
|
|
137
|
-
retry: false,
|
|
138
|
-
staleTime: 15_000,
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function useMailConversation(
|
|
143
|
-
folder: string | null | undefined,
|
|
144
|
-
mailId: string | null | undefined,
|
|
145
|
-
enabled: boolean,
|
|
146
|
-
) {
|
|
147
|
-
return useQuery<ConversationPayload>({
|
|
148
|
-
enabled: Boolean(enabled && mailId),
|
|
149
|
-
gcTime: 5 * 60_000,
|
|
150
|
-
queryFn: ({ signal }) =>
|
|
151
|
-
mailRequest<ConversationPayload>("/api/mail/conversation", {
|
|
152
|
-
search: { ...mailPath(folder), id: mailId ?? "" },
|
|
153
|
-
signal,
|
|
154
|
-
}),
|
|
155
|
-
queryKey: [...folderCacheKey(folder), mailId ?? "", "conversation"],
|
|
156
|
-
retry: false,
|
|
157
|
-
staleTime: 5 * 60_000,
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export type MailActionInput = {
|
|
162
|
-
action: "archive" | "delete" | "remove-attachment"
|
|
163
|
-
id: string
|
|
164
|
-
path: string
|
|
165
|
-
revision: string
|
|
166
|
-
/** Absolute attachment path; used only with the remove-attachment action. */
|
|
167
|
-
attachment?: string
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function useMailAction(folder: string | null | undefined) {
|
|
171
|
-
const queryClient = useQueryClient()
|
|
172
|
-
return useMutation({
|
|
173
|
-
mutationFn: (input: MailActionInput) =>
|
|
174
|
-
mailRequest<{ action: string; id: string; ok: true }>(
|
|
175
|
-
"/api/mail/action",
|
|
176
|
-
{
|
|
177
|
-
body: input,
|
|
178
|
-
method: "POST",
|
|
179
|
-
},
|
|
180
|
-
),
|
|
181
|
-
onSuccess: () => {
|
|
182
|
-
void queryClient.invalidateQueries({
|
|
183
|
-
queryKey: [...folderCacheKey(folder)],
|
|
184
|
-
})
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function useMailSend(folder: string | null | undefined) {
|
|
190
|
-
const queryClient = useQueryClient()
|
|
191
|
-
return useMutation({
|
|
192
|
-
mutationFn: (input: { dryRun: boolean; id: string; revision?: string }) =>
|
|
193
|
-
mailRequest<MailSendPreviewPayload | MailSendResultPayload>(
|
|
194
|
-
"/api/mail/send",
|
|
195
|
-
{
|
|
196
|
-
body: { path: folder ?? "", ...input },
|
|
197
|
-
method: "POST",
|
|
198
|
-
},
|
|
199
|
-
),
|
|
200
|
-
onSuccess: () => {
|
|
201
|
-
void queryClient.invalidateQueries({
|
|
202
|
-
queryKey: [...folderCacheKey(folder)],
|
|
203
|
-
})
|
|
204
|
-
},
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export type ConversationPanelState =
|
|
209
|
-
| { variant: "hidden" }
|
|
210
|
-
| { variant: "loading" }
|
|
211
|
-
| { variant: "error"; message: string }
|
|
212
|
-
| { variant: "empty" }
|
|
213
|
-
| { variant: "ok" }
|
|
214
|
-
| { variant: "partial"; missing: number }
|
|
215
|
-
| { variant: "not-found" }
|
|
216
|
-
| { variant: "timeout" }
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Pure mapping from the conversation query to a UI state. A disabled query
|
|
220
|
-
* (no references, or the detail still loading) is "hidden" — never an
|
|
221
|
-
* endless loading spinner.
|
|
222
|
-
*/
|
|
223
|
-
export function conversationPanelState(input: {
|
|
224
|
-
data?: ConversationPayload
|
|
225
|
-
enabled: boolean
|
|
226
|
-
error: unknown
|
|
227
|
-
isPending: boolean
|
|
228
|
-
}): ConversationPanelState {
|
|
229
|
-
if (!input.enabled) return { variant: "hidden" }
|
|
230
|
-
if (input.isPending && !input.data) return { variant: "loading" }
|
|
231
|
-
if (input.error && !input.data) {
|
|
232
|
-
const message =
|
|
233
|
-
input.error instanceof Error
|
|
234
|
-
? input.error.message
|
|
235
|
-
: "Could not load the conversation."
|
|
236
|
-
return { variant: "error", message }
|
|
237
|
-
}
|
|
238
|
-
switch (input.data?.outcome) {
|
|
239
|
-
case "empty":
|
|
240
|
-
return { variant: "empty" }
|
|
241
|
-
case "ok":
|
|
242
|
-
return input.data.items.length ? { variant: "ok" } : { variant: "empty" }
|
|
243
|
-
case "partial":
|
|
244
|
-
return { variant: "partial", missing: input.data.missing.length }
|
|
245
|
-
case "not-found":
|
|
246
|
-
return { variant: "not-found" }
|
|
247
|
-
case "timeout":
|
|
248
|
-
return { variant: "timeout" }
|
|
249
|
-
default:
|
|
250
|
-
return { variant: "hidden" }
|
|
251
|
-
}
|
|
252
|
-
}
|