@aglyn/plugins-outreach 1.0.0-beta.274 → 1.0.0-beta.275

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.
Files changed (52) hide show
  1. package/README.md +7 -2
  2. package/package.json +14 -14
  3. package/src/lib/components/enrollment-actions.d.ts +11 -4
  4. package/src/lib/components/enrollment-actions.js +16 -0
  5. package/src/lib/components/enrollment-actions.js.map +1 -1
  6. package/src/lib/components/enrollment-detail.js +40 -13
  7. package/src/lib/components/enrollment-detail.js.map +1 -1
  8. package/src/lib/components/mailbox-card.js +13 -2
  9. package/src/lib/components/mailbox-card.js.map +1 -1
  10. package/src/lib/components/record-reply-button.d.ts +52 -0
  11. package/src/lib/components/record-reply-button.js +92 -0
  12. package/src/lib/components/record-reply-button.js.map +1 -0
  13. package/src/lib/components/reply-dialog.d.ts +66 -0
  14. package/src/lib/components/reply-dialog.js +353 -0
  15. package/src/lib/components/reply-dialog.js.map +1 -0
  16. package/src/lib/components/use-outreach-api.d.ts +5 -1
  17. package/src/lib/components/use-outreach-api.js +10 -0
  18. package/src/lib/components/use-outreach-api.js.map +1 -1
  19. package/src/lib/constants/api-routes.d.ts +8 -0
  20. package/src/lib/constants/api-routes.js +6 -0
  21. package/src/lib/constants/api-routes.js.map +1 -1
  22. package/src/lib/engine/index.d.ts +1 -0
  23. package/src/lib/engine/index.js +1 -0
  24. package/src/lib/engine/index.js.map +1 -1
  25. package/src/lib/engine/manual-reply.d.ts +108 -0
  26. package/src/lib/engine/manual-reply.js +301 -0
  27. package/src/lib/engine/manual-reply.js.map +1 -0
  28. package/src/lib/model/enrollment-timeline.d.ts +3 -1
  29. package/src/lib/model/enrollment-timeline.js +83 -7
  30. package/src/lib/model/enrollment-timeline.js.map +1 -1
  31. package/src/lib/model/outreach-api.d.ts +66 -2
  32. package/src/lib/model/outreach-api.js.map +1 -1
  33. package/src/lib/model/outreach.types.d.ts +85 -3
  34. package/src/lib/model/outreach.types.js +6 -0
  35. package/src/lib/model/outreach.types.js.map +1 -1
  36. package/src/lib/model/stored-records.d.ts +7 -1
  37. package/src/lib/model/stored-records.js +37 -1
  38. package/src/lib/model/stored-records.js.map +1 -1
  39. package/src/lib/plugin.d.ts +4 -0
  40. package/src/lib/plugin.js +17 -0
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/routes/register-routes.d.ts +6 -0
  43. package/src/lib/routes/register-routes.js +18 -0
  44. package/src/lib/routes/register-routes.js.map +1 -1
  45. package/src/lib/routes/reply-routes.d.ts +76 -0
  46. package/src/lib/routes/reply-routes.js +563 -0
  47. package/src/lib/routes/reply-routes.js.map +1 -0
  48. package/src/lib/runtime/sync-job.js +14 -0
  49. package/src/lib/runtime/sync-job.js.map +1 -1
  50. package/src/lib/transport/send-message.d.ts +11 -0
  51. package/src/lib/transport/send-message.js +22 -0
  52. package/src/lib/transport/send-message.js.map +1 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type OutreachEnrollment } from '../model/outreach.types';
18
+ /**
19
+ * "REPLY IN THREAD FROM <MAILBOX>" ON A CRM RECORD (AGL-4020), in the
20
+ * record header's `crmRecordEmailActions` zone, before the CRM's own Send
21
+ * email.
22
+ *
23
+ * Send email starts a new message from the site's sending address; a person
24
+ * who answered a sequence is answered in that thread, from the mailbox that
25
+ * wrote to them. So once one of their enrollments holds something they wrote
26
+ * — or was stopped by their reply — the header offers that first, and opens
27
+ * the same composer the enrollment's own page does. Nothing is drawn for a
28
+ * person no sequence has heard from, one who opted out or bounced, or a
29
+ * reader the Sequences rules do not let read enrollments.
30
+ *
31
+ * The props are the CRM zone's, restated here: a plugin never imports
32
+ * another plugin's package.
33
+ */
34
+ export interface OutreachRecordReplyButtonProps {
35
+ hostId: string | null;
36
+ orgId: string | undefined;
37
+ kind: 'contact' | 'lead';
38
+ recordId: string;
39
+ name: string;
40
+ email: string | null;
41
+ }
42
+ /**
43
+ * The enrollment an answer from the record goes into: of the person's
44
+ * enrollments that heard from them, the one that heard from them last. Not
45
+ * one that opted out or bounced.
46
+ */
47
+ export declare function outreachEnrollmentToAnswer(enrollments: readonly OutreachEnrollment[]): OutreachEnrollment | null;
48
+ export declare function OutreachRecordReplyButton(props: OutreachRecordReplyButtonProps): import("react").JSX.Element;
49
+ export declare namespace OutreachRecordReplyButton {
50
+ var displayName: string;
51
+ }
52
+ export default OutreachRecordReplyButton;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
19
+ import { useFirestore } from "@aglyn/tenant-feature-instance";
20
+ import { Button } from "@mui/material";
21
+ import { collection, doc, limit, onSnapshot, query, where } from "firebase/firestore";
22
+ import { useEffect, useState } from "react";
23
+ import { outreachEnrollmentHasInbound, outreachMailboxAddress } from "../engine/manual-reply.js";
24
+ import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
25
+ import { readStoredOutreachEnrollment, readStoredOutreachMailbox } from "../model/stored-records.js";
26
+ import { OutreachReplyDialog } from "./reply-dialog.js";
27
+ import { useOutreachApi } from "./use-outreach-api.js";
28
+ /** The most enrollments of one address read; a person is in few sequences. */ const ENROLLMENTS_READ_MAX = 20;
29
+ /** When the person last wrote, or was last heard from: what picks the thread to answer. */ const lastHeardMs = (enrollment)=>{
30
+ var _ref, _enrollment_stoppedAtMs;
31
+ var _enrollment_inboundMessages_at, _enrollment_inboundMessages;
32
+ return Math.max((_ref = (_enrollment_inboundMessages = enrollment.inboundMessages) == null ? void 0 : (_enrollment_inboundMessages_at = _enrollment_inboundMessages.at(-1)) == null ? void 0 : _enrollment_inboundMessages_at.atMs) != null ? _ref : 0, enrollment.stopReason === 'reply' ? (_enrollment_stoppedAtMs = enrollment.stoppedAtMs) != null ? _enrollment_stoppedAtMs : 0 : 0);
33
+ };
34
+ /**
35
+ * The enrollment an answer from the record goes into: of the person's
36
+ * enrollments that heard from them, the one that heard from them last. Not
37
+ * one that opted out or bounced.
38
+ */ export function outreachEnrollmentToAnswer(enrollments) {
39
+ var _enrollments_filter_sort_;
40
+ return (_enrollments_filter_sort_ = enrollments.filter((enrollment)=>outreachEnrollmentHasInbound(enrollment) && enrollment.status !== 'opted_out' && enrollment.status !== 'bounced').sort((a, b)=>lastHeardMs(b) - lastHeardMs(a))[0]) != null ? _enrollments_filter_sort_ : null;
41
+ }
42
+ export function OutreachRecordReplyButton(props) {
43
+ var _ref;
44
+ const { orgId } = props;
45
+ const firestore = useFirestore();
46
+ const api = useOutreachApi(orgId != null ? orgId : null);
47
+ const email = normalizeContactEmail(props.email);
48
+ const [enrollment, setEnrollment] = useState(null);
49
+ const [mailbox, setMailbox] = useState(null);
50
+ const [open, setOpen] = useState(false);
51
+ useEffect(()=>{
52
+ setEnrollment(null);
53
+ if (!orgId || !email) return undefined;
54
+ return onSnapshot(query(collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments), where('email', '==', email), limit(ENROLLMENTS_READ_MAX)), (snapshot)=>setEnrollment(outreachEnrollmentToAnswer(snapshot.docs.map((entry)=>readStoredOutreachEnrollment(entry.id, entry.data())).filter((entry)=>entry !== null))), // A reader the rules refuse simply has nothing to answer from here.
55
+ ()=>setEnrollment(null));
56
+ }, [
57
+ firestore,
58
+ orgId,
59
+ email
60
+ ]);
61
+ const mailboxId = (_ref = enrollment == null ? void 0 : enrollment.mailboxId) != null ? _ref : null;
62
+ useEffect(()=>{
63
+ setMailbox(null);
64
+ if (!orgId || !mailboxId) return undefined;
65
+ return onSnapshot(doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.mailboxes, mailboxId), (snapshot)=>setMailbox(readStoredOutreachMailbox(snapshot.id, snapshot.exists() ? snapshot.data() : undefined)), ()=>setMailbox(null));
66
+ }, [
67
+ firestore,
68
+ orgId,
69
+ mailboxId
70
+ ]);
71
+ if (!enrollment || !mailbox) return null;
72
+ return /*#__PURE__*/ _jsxs(_Fragment, {
73
+ children: [
74
+ /*#__PURE__*/ _jsx(Button, {
75
+ size: "small",
76
+ variant: "outlined",
77
+ onClick: ()=>setOpen(true),
78
+ children: `Reply in thread from ${outreachMailboxAddress(mailbox)}`
79
+ }),
80
+ /*#__PURE__*/ _jsx(OutreachReplyDialog, {
81
+ enrollment: open ? enrollment : null,
82
+ api: api,
83
+ timeZone: mailbox.timezone,
84
+ onClose: ()=>setOpen(false)
85
+ })
86
+ ]
87
+ });
88
+ }
89
+ OutreachRecordReplyButton.displayName = 'OutreachRecordReplyButton';
90
+ export default OutreachRecordReplyButton;
91
+
92
+ //# sourceMappingURL=record-reply-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/record-reply-button.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button } from '@mui/material'\nimport { collection, doc, limit, onSnapshot, query, where } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { outreachEnrollmentHasInbound, outreachMailboxAddress } from '../engine/manual-reply'\nimport { OUTREACH_COLLECTIONS, type OutreachEnrollment, type OutreachMailbox } from '../model/outreach.types'\nimport { readStoredOutreachEnrollment, readStoredOutreachMailbox } from '../model/stored-records'\nimport { OutreachReplyDialog } from './reply-dialog'\nimport { useOutreachApi } from './use-outreach-api'\n\n/**\n * \"REPLY IN THREAD FROM <MAILBOX>\" ON A CRM RECORD (AGL-4020), in the\n * record header's `crmRecordEmailActions` zone, before the CRM's own Send\n * email.\n *\n * Send email starts a new message from the site's sending address; a person\n * who answered a sequence is answered in that thread, from the mailbox that\n * wrote to them. So once one of their enrollments holds something they wrote\n * — or was stopped by their reply — the header offers that first, and opens\n * the same composer the enrollment's own page does. Nothing is drawn for a\n * person no sequence has heard from, one who opted out or bounced, or a\n * reader the Sequences rules do not let read enrollments.\n *\n * The props are the CRM zone's, restated here: a plugin never imports\n * another plugin's package.\n */\n\nexport interface OutreachRecordReplyButtonProps {\n hostId: string | null\n orgId: string | undefined\n kind: 'contact' | 'lead'\n recordId: string\n name: string\n email: string | null\n}\n\n/** The most enrollments of one address read; a person is in few sequences. */\nconst ENROLLMENTS_READ_MAX = 20\n\n/** When the person last wrote, or was last heard from: what picks the thread to answer. */\nconst lastHeardMs = (enrollment: OutreachEnrollment) =>\n Math.max(\n enrollment.inboundMessages?.at(-1)?.atMs ?? 0,\n enrollment.stopReason === 'reply' ? (enrollment.stoppedAtMs ?? 0) : 0,\n )\n\n/**\n * The enrollment an answer from the record goes into: of the person's\n * enrollments that heard from them, the one that heard from them last. Not\n * one that opted out or bounced.\n */\nexport function outreachEnrollmentToAnswer(enrollments: readonly OutreachEnrollment[]): OutreachEnrollment | null {\n return (\n enrollments\n .filter(\n (enrollment) =>\n outreachEnrollmentHasInbound(enrollment) &&\n enrollment.status !== 'opted_out' &&\n enrollment.status !== 'bounced',\n )\n .sort((a, b) => lastHeardMs(b) - lastHeardMs(a))[0] ?? null\n )\n}\n\nexport function OutreachRecordReplyButton(props: OutreachRecordReplyButtonProps) {\n const { orgId } = props\n const firestore = useFirestore()\n const api = useOutreachApi(orgId ?? null)\n const email = normalizeContactEmail(props.email)\n const [enrollment, setEnrollment] = useState<OutreachEnrollment | null>(null)\n const [mailbox, setMailbox] = useState<OutreachMailbox | null>(null)\n const [open, setOpen] = useState(false)\n\n useEffect(() => {\n setEnrollment(null)\n if (!orgId || !email) return undefined\n return onSnapshot(\n query(\n collection(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.enrollments),\n where('email', '==', email),\n limit(ENROLLMENTS_READ_MAX),\n ),\n (snapshot) =>\n setEnrollment(\n outreachEnrollmentToAnswer(\n snapshot.docs\n .map((entry) => readStoredOutreachEnrollment(entry.id, entry.data()))\n .filter((entry): entry is OutreachEnrollment => entry !== null),\n ),\n ),\n // A reader the rules refuse simply has nothing to answer from here.\n () => setEnrollment(null),\n )\n }, [firestore, orgId, email])\n\n const mailboxId = enrollment?.mailboxId ?? null\n useEffect(() => {\n setMailbox(null)\n if (!orgId || !mailboxId) return undefined\n return onSnapshot(\n doc(firestore, 'orgs', orgId, OUTREACH_COLLECTIONS.mailboxes, mailboxId),\n (snapshot) => setMailbox(readStoredOutreachMailbox(snapshot.id, snapshot.exists() ? snapshot.data() : undefined)),\n () => setMailbox(null),\n )\n }, [firestore, orgId, mailboxId])\n\n if (!enrollment || !mailbox) return null\n return (\n <>\n <Button size=\"small\" variant=\"outlined\" onClick={() => setOpen(true)}>\n {`Reply in thread from ${outreachMailboxAddress(mailbox)}`}\n </Button>\n <OutreachReplyDialog\n enrollment={open ? enrollment : null}\n api={api}\n timeZone={mailbox.timezone}\n onClose={() => setOpen(false)}\n />\n </>\n )\n}\nOutreachRecordReplyButton.displayName = 'OutreachRecordReplyButton'\n\nexport default OutreachRecordReplyButton\n"],"names":["normalizeContactEmail","useFirestore","Button","collection","doc","limit","onSnapshot","query","where","useEffect","useState","outreachEnrollmentHasInbound","outreachMailboxAddress","OUTREACH_COLLECTIONS","readStoredOutreachEnrollment","readStoredOutreachMailbox","OutreachReplyDialog","useOutreachApi","ENROLLMENTS_READ_MAX","lastHeardMs","enrollment","Math","max","inboundMessages","at","atMs","stopReason","stoppedAtMs","outreachEnrollmentToAnswer","enrollments","filter","status","sort","a","b","OutreachRecordReplyButton","props","orgId","firestore","api","email","setEnrollment","mailbox","setMailbox","open","setOpen","undefined","snapshot","docs","map","entry","id","data","mailboxId","mailboxes","exists","size","variant","onClick","timeZone","timezone","onClose","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAEA,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,MAAM,QAAQ,gBAAe;AACtC,SAASC,UAAU,EAAEC,GAAG,EAAEC,KAAK,EAAEC,UAAU,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AACrF,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,4BAA4B,EAAEC,sBAAsB,QAAQ,4BAAwB;AAC7F,SAASC,oBAAoB,QAAuD,6BAAyB;AAC7G,SAASC,4BAA4B,EAAEC,yBAAyB,QAAQ,6BAAyB;AACjG,SAASC,mBAAmB,QAAQ,oBAAgB;AACpD,SAASC,cAAc,QAAQ,wBAAoB;AA4BnD,4EAA4E,GAC5E,MAAMC,uBAAuB;AAE7B,yFAAyF,GACzF,MAAMC,cAAc,CAACC;cAGoBA;QADrCA,gCAAAA;WADFC,KAAKC,GAAG,UACNF,8BAAAA,WAAWG,eAAe,sBAA1BH,iCAAAA,4BAA4BI,EAAE,CAAC,CAAC,uBAAhCJ,+BAAoCK,IAAI,mBAAI,GAC5CL,WAAWM,UAAU,KAAK,WAAWN,0BAAAA,WAAWO,WAAW,YAAtBP,0BAA0B,IAAK;;AAGxE;;;;CAIC,GACD,OAAO,SAASQ,2BAA2BC,WAA0C;QAEjFA;IADF,QACEA,4BAAAA,YACGC,MAAM,CACL,CAACV,aACCT,6BAA6BS,eAC7BA,WAAWW,MAAM,KAAK,eACtBX,WAAWW,MAAM,KAAK,WAEzBC,IAAI,CAAC,CAACC,GAAGC,IAAMf,YAAYe,KAAKf,YAAYc,GAAG,CAAC,EAAE,YAPrDJ,4BAOyD;AAE7D;AAEA,OAAO,SAASM,0BAA0BC,KAAqC;;IAC7E,MAAM,EAAEC,KAAK,EAAE,GAAGD;IAClB,MAAME,YAAYrC;IAClB,MAAMsC,MAAMtB,eAAeoB,gBAAAA,QAAS;IACpC,MAAMG,QAAQxC,sBAAsBoC,MAAMI,KAAK;IAC/C,MAAM,CAACpB,YAAYqB,cAAc,GAAG/B,SAAoC;IACxE,MAAM,CAACgC,SAASC,WAAW,GAAGjC,SAAiC;IAC/D,MAAM,CAACkC,MAAMC,QAAQ,GAAGnC,SAAS;IAEjCD,UAAU;QACRgC,cAAc;QACd,IAAI,CAACJ,SAAS,CAACG,OAAO,OAAOM;QAC7B,OAAOxC,WACLC,MACEJ,WAAWmC,WAAW,QAAQD,OAAOxB,qBAAqBgB,WAAW,GACrErB,MAAM,SAAS,MAAMgC,QACrBnC,MAAMa,wBAER,CAAC6B,WACCN,cACEb,2BACEmB,SAASC,IAAI,CACVC,GAAG,CAAC,CAACC,QAAUpC,6BAA6BoC,MAAMC,EAAE,EAAED,MAAME,IAAI,KAChEtB,MAAM,CAAC,CAACoB,QAAuCA,UAAU,SAGlE,oEAAoE;QACpE,IAAMT,cAAc;IAExB,GAAG;QAACH;QAAWD;QAAOG;KAAM;IAE5B,MAAMa,oBAAYjC,8BAAAA,WAAYiC,SAAS,mBAAI;IAC3C5C,UAAU;QACRkC,WAAW;QACX,IAAI,CAACN,SAAS,CAACgB,WAAW,OAAOP;QACjC,OAAOxC,WACLF,IAAIkC,WAAW,QAAQD,OAAOxB,qBAAqByC,SAAS,EAAED,YAC9D,CAACN,WAAaJ,WAAW5B,0BAA0BgC,SAASI,EAAE,EAAEJ,SAASQ,MAAM,KAAKR,SAASK,IAAI,KAAKN,aACtG,IAAMH,WAAW;IAErB,GAAG;QAACL;QAAWD;QAAOgB;KAAU;IAEhC,IAAI,CAACjC,cAAc,CAACsB,SAAS,OAAO;IACpC,qBACE;;0BACE,KAACxC;gBAAOsD,MAAK;gBAAQC,SAAQ;gBAAWC,SAAS,IAAMb,QAAQ;0BAC5D,CAAC,qBAAqB,EAAEjC,uBAAuB8B,UAAU;;0BAE5D,KAAC1B;gBACCI,YAAYwB,OAAOxB,aAAa;gBAChCmB,KAAKA;gBACLoB,UAAUjB,QAAQkB,QAAQ;gBAC1BC,SAAS,IAAMhB,QAAQ;;;;AAI/B;AACAV,0BAA0B2B,WAAW,GAAG;AAExC,eAAe3B,0BAAyB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { OutreachEnrollmentReplyResponse } from '../model/outreach-api';
18
+ import type { OutreachEnrollment } from '../model/outreach.types';
19
+ import type { OutreachApi } from './use-outreach-api';
20
+ /**
21
+ * ANSWER A PERSON WHO WROTE BACK (AGL-4020): a replied enrollment's
22
+ * "Reply in thread", on the Enrollments tab, the person's page, and the CRM
23
+ * record of the person.
24
+ *
25
+ * The dialog reads the thread the moment it opens — what the person wrote,
26
+ * kept on the enrollment or read from the mailbox when it was synced before
27
+ * replies were kept — and says where the answer goes: from the enrollment's
28
+ * own mailbox, to the person, `Re:` their subject. The member writes plain
29
+ * text; Send puts it in the same thread in both mailboxes, with their message
30
+ * quoted beneath. Whatever still refuses — the do-not-contact list, a paused
31
+ * or disconnected mailbox — is said before anything is typed, and Send waits.
32
+ *
33
+ * The answer's key is minted once per open: sending again after a lost
34
+ * answer from the server is the same answer, which the route recognises
35
+ * rather than sending twice.
36
+ */
37
+ export interface OutreachReplyDialogProps {
38
+ /** The enrollment to answer, or `null` while closed. */
39
+ enrollment: OutreachEnrollment | null;
40
+ api: Pick<OutreachApi, 'readEnrollmentThread' | 'replyToEnrollment'>;
41
+ /** The mailbox's zone, for the times shown. */
42
+ timeZone?: string | null;
43
+ onClose(): void;
44
+ /** After the answer went. */
45
+ onSent?(answer: OutreachEnrollmentReplyResponse): void;
46
+ }
47
+ /** One message of the conversation as the dialog shows it. */
48
+ interface ThreadLine {
49
+ key: string;
50
+ atMs: number;
51
+ who: string;
52
+ subject: string;
53
+ body: string;
54
+ /** Theirs, or ours. */
55
+ inbound: boolean;
56
+ }
57
+ /**
58
+ * The conversation, oldest first: what the person wrote, every answer a
59
+ * member sent, and the sequence's own emails by their subject.
60
+ */
61
+ export declare function outreachReplyThreadLines(enrollment: Pick<OutreachEnrollment, 'inboundMessages' | 'stepRecords'>): ThreadLine[];
62
+ export declare function OutreachReplyDialog(props: OutreachReplyDialogProps): import("react").JSX.Element;
63
+ export declare namespace OutreachReplyDialog {
64
+ var displayName: string;
65
+ }
66
+ export default OutreachReplyDialog;
@@ -0,0 +1,353 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
19
+ import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
20
+ import { useEffect, useMemo, useState } from "react";
21
+ import { OUTREACH_MANUAL_REPLY_BODY_MAX } from "../engine/manual-reply.js";
22
+ import { formatOutreachTime, OutreachLoading } from "./outreach-ui.js";
23
+ /**
24
+ * The conversation, oldest first: what the person wrote, every answer a
25
+ * member sent, and the sequence's own emails by their subject.
26
+ */ export function outreachReplyThreadLines(enrollment) {
27
+ var _enrollment_inboundMessages, _enrollment_stepRecords;
28
+ const lines = [];
29
+ for (const message of (_enrollment_inboundMessages = enrollment.inboundMessages) != null ? _enrollment_inboundMessages : []){
30
+ lines.push({
31
+ key: `in-${message.id}`,
32
+ atMs: message.atMs,
33
+ who: message.fromName ? `${message.fromName} <${message.from}>` : message.from,
34
+ subject: message.subject,
35
+ body: message.body,
36
+ inbound: true
37
+ });
38
+ }
39
+ ;
40
+ ((_enrollment_stepRecords = enrollment.stepRecords) != null ? _enrollment_stepRecords : []).forEach((record, index)=>{
41
+ if ((record == null ? void 0 : record.kind) === 'manual_reply') {
42
+ var _record_subject, _record_body;
43
+ lines.push({
44
+ key: `answer-${index}`,
45
+ atMs: record.atMs,
46
+ who: record.from ? `You, from ${record.from}` : 'You',
47
+ subject: (_record_subject = record.subject) != null ? _record_subject : '',
48
+ body: (_record_body = record.body) != null ? _record_body : '',
49
+ inbound: false
50
+ });
51
+ } else if ((record == null ? void 0 : record.kind) === 'email') {
52
+ var _record_subject1;
53
+ lines.push({
54
+ key: `step-${index}`,
55
+ atMs: record.atMs,
56
+ who: 'Sequence email',
57
+ subject: (_record_subject1 = record.subject) != null ? _record_subject1 : '',
58
+ body: '',
59
+ inbound: false
60
+ });
61
+ }
62
+ });
63
+ return lines.sort((a, b)=>a.atMs - b.atMs);
64
+ }
65
+ /** A fresh key for one answer. */ function newReplyKey() {
66
+ const crypto = globalThis.crypto;
67
+ if (crypto == null ? void 0 : crypto.randomUUID) return crypto.randomUUID();
68
+ return `r${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
69
+ }
70
+ export function OutreachReplyDialog(props) {
71
+ var _ref, _ref1;
72
+ const { enrollment, api, timeZone } = props;
73
+ const theme = useTheme();
74
+ const narrow = useMediaQuery(theme.breakpoints.down('sm'));
75
+ const { enqueueSnackbar } = useSnackbar();
76
+ const [stage, setStage] = useState({
77
+ kind: 'reading'
78
+ });
79
+ const [text, setText] = useState('');
80
+ const [sending, setSending] = useState(false);
81
+ const [problem, setProblem] = useState(null);
82
+ const enrollmentId = (_ref = enrollment == null ? void 0 : enrollment.id) != null ? _ref : null;
83
+ // One key per opening: a resend after a lost answer is the same answer.
84
+ const replyKey = useMemo(()=>enrollmentId ? newReplyKey() : '', [
85
+ enrollmentId
86
+ ]);
87
+ useEffect(()=>{
88
+ if (!enrollmentId) return undefined;
89
+ let cancelled = false;
90
+ setStage({
91
+ kind: 'reading'
92
+ });
93
+ setText('');
94
+ setProblem(null);
95
+ api.readEnrollmentThread(enrollmentId).then((thread)=>{
96
+ if (!cancelled) setStage({
97
+ kind: 'ready',
98
+ thread
99
+ });
100
+ }, (error)=>{
101
+ if (!cancelled) setStage({
102
+ kind: 'failed',
103
+ message: error.message
104
+ });
105
+ });
106
+ return ()=>{
107
+ cancelled = true;
108
+ };
109
+ }, [
110
+ api,
111
+ enrollmentId
112
+ ]);
113
+ const thread = stage.kind === 'ready' ? stage.thread : null;
114
+ const draft = (_ref1 = thread == null ? void 0 : thread.draft) != null ? _ref1 : null;
115
+ const lines = thread ? outreachReplyThreadLines(thread.enrollment) : [];
116
+ const name = (enrollment == null ? void 0 : enrollment.contactName) || (enrollment == null ? void 0 : enrollment.email) || '';
117
+ const title = name ? `Reply to ${name}` : 'Reply in thread';
118
+ const canSend = Boolean(draft) && !(thread == null ? void 0 : thread.refusal) && Boolean(text.trim()) && !sending;
119
+ const send = async ()=>{
120
+ if (!enrollmentId || !draft || !canSend) return;
121
+ setSending(true);
122
+ setProblem(null);
123
+ try {
124
+ const answer = await api.replyToEnrollment({
125
+ enrollmentId,
126
+ body: text,
127
+ answerId: draft.answerId,
128
+ replyKey
129
+ });
130
+ enqueueSnackbar(answer.alreadySent ? `That answer had already gone to ${answer.sentTo}.` : `Answer sent to ${answer.sentTo}.`, {
131
+ variant: 'success'
132
+ });
133
+ props.onSent == null ? void 0 : props.onSent.call(props, answer);
134
+ props.onClose();
135
+ } catch (error) {
136
+ setProblem(error.message);
137
+ } finally{
138
+ setSending(false);
139
+ }
140
+ };
141
+ return /*#__PURE__*/ _jsxs(Dialog, {
142
+ open: Boolean(enrollment),
143
+ onClose: sending ? undefined : props.onClose,
144
+ fullWidth: true,
145
+ maxWidth: "md",
146
+ fullScreen: narrow,
147
+ "aria-label": title,
148
+ children: [
149
+ /*#__PURE__*/ _jsx(DialogTitle, {
150
+ children: title
151
+ }),
152
+ /*#__PURE__*/ _jsx(DialogContent, {
153
+ dividers: true,
154
+ children: /*#__PURE__*/ _jsxs(Stack, {
155
+ spacing: 2,
156
+ children: [
157
+ stage.kind === 'reading' ? /*#__PURE__*/ _jsx(OutreachLoading, {
158
+ label: "Reading their reply…"
159
+ }) : null,
160
+ stage.kind === 'failed' ? /*#__PURE__*/ _jsx(Alert, {
161
+ severity: "warning",
162
+ children: stage.message
163
+ }) : null,
164
+ thread && !draft ? /*#__PURE__*/ _jsx(Alert, {
165
+ severity: "info",
166
+ children: "Nothing they wrote could be read from the mailbox, so there is nothing to answer here yet. Their reply is in the mailbox that sent the sequence."
167
+ }) : null,
168
+ draft ? /*#__PURE__*/ _jsxs(Box, {
169
+ component: "dl",
170
+ sx: {
171
+ m: 0,
172
+ display: 'grid',
173
+ gridTemplateColumns: 'auto 1fr',
174
+ columnGap: 2,
175
+ rowGap: 0.5
176
+ },
177
+ children: [
178
+ /*#__PURE__*/ _jsx(Typography, {
179
+ component: "dt",
180
+ variant: "body2",
181
+ color: "text.secondary",
182
+ children: "From"
183
+ }),
184
+ /*#__PURE__*/ _jsx(Typography, {
185
+ component: "dd",
186
+ variant: "body2",
187
+ sx: {
188
+ m: 0,
189
+ wordBreak: 'break-word'
190
+ },
191
+ children: draft.from.name ? `${draft.from.name} <${draft.from.address}>` : draft.from.address
192
+ }),
193
+ /*#__PURE__*/ _jsx(Typography, {
194
+ component: "dt",
195
+ variant: "body2",
196
+ color: "text.secondary",
197
+ children: "To"
198
+ }),
199
+ /*#__PURE__*/ _jsx(Typography, {
200
+ component: "dd",
201
+ variant: "body2",
202
+ sx: {
203
+ m: 0,
204
+ wordBreak: 'break-word'
205
+ },
206
+ children: draft.to
207
+ }),
208
+ /*#__PURE__*/ _jsx(Typography, {
209
+ component: "dt",
210
+ variant: "body2",
211
+ color: "text.secondary",
212
+ children: "Subject"
213
+ }),
214
+ /*#__PURE__*/ _jsx(Typography, {
215
+ component: "dd",
216
+ variant: "body2",
217
+ sx: {
218
+ m: 0,
219
+ wordBreak: 'break-word'
220
+ },
221
+ children: draft.subject
222
+ })
223
+ ]
224
+ }) : null,
225
+ lines.length ? /*#__PURE__*/ _jsx(Stack, {
226
+ spacing: 1.5,
227
+ role: "list",
228
+ "aria-label": "The conversation",
229
+ sx: {
230
+ maxHeight: 320,
231
+ overflowY: 'auto',
232
+ border: 1,
233
+ borderColor: 'divider',
234
+ borderRadius: 1,
235
+ p: 1.5
236
+ },
237
+ children: lines.map((line)=>/*#__PURE__*/ _jsxs(Box, {
238
+ role: "listitem",
239
+ "data-inbound": line.inbound,
240
+ sx: {
241
+ pl: 1.5,
242
+ borderLeft: 2,
243
+ borderColor: line.inbound ? 'info.main' : 'divider'
244
+ },
245
+ children: [
246
+ /*#__PURE__*/ _jsxs(Stack, {
247
+ direction: {
248
+ xs: 'column',
249
+ sm: 'row'
250
+ },
251
+ spacing: {
252
+ xs: 0,
253
+ sm: 1
254
+ },
255
+ sx: {
256
+ justifyContent: 'space-between',
257
+ alignItems: {
258
+ sm: 'baseline'
259
+ }
260
+ },
261
+ children: [
262
+ /*#__PURE__*/ _jsx(Typography, {
263
+ variant: "body2",
264
+ sx: {
265
+ fontWeight: 500,
266
+ wordBreak: 'break-word'
267
+ },
268
+ children: line.who
269
+ }),
270
+ /*#__PURE__*/ _jsx(Typography, {
271
+ variant: "caption",
272
+ color: "text.secondary",
273
+ sx: {
274
+ whiteSpace: 'nowrap'
275
+ },
276
+ children: formatOutreachTime(line.atMs, timeZone)
277
+ })
278
+ ]
279
+ }),
280
+ line.subject ? /*#__PURE__*/ _jsx(Typography, {
281
+ variant: "caption",
282
+ color: "text.secondary",
283
+ component: "div",
284
+ children: `Subject: ${line.subject}`
285
+ }) : null,
286
+ line.body ? /*#__PURE__*/ _jsx(Typography, {
287
+ variant: "body2",
288
+ sx: {
289
+ whiteSpace: 'pre-wrap',
290
+ wordBreak: 'break-word',
291
+ mt: 0.5
292
+ },
293
+ children: line.body
294
+ }) : null
295
+ ]
296
+ }, line.key))
297
+ }) : null,
298
+ (thread == null ? void 0 : thread.refusal) ? /*#__PURE__*/ _jsx(Alert, {
299
+ severity: "warning",
300
+ children: thread.refusal.message
301
+ }) : null,
302
+ draft && !(thread == null ? void 0 : thread.refusal) ? /*#__PURE__*/ _jsxs(_Fragment, {
303
+ children: [
304
+ /*#__PURE__*/ _jsx(TextField, {
305
+ label: "Your answer",
306
+ value: text,
307
+ onChange: (event)=>setText(event.target.value),
308
+ multiline: true,
309
+ minRows: 6,
310
+ fullWidth: true,
311
+ autoFocus: true,
312
+ disabled: sending,
313
+ slotProps: {
314
+ htmlInput: {
315
+ maxLength: OUTREACH_MANUAL_REPLY_BODY_MAX
316
+ }
317
+ }
318
+ }),
319
+ /*#__PURE__*/ _jsx(DialogContentText, {
320
+ variant: "body2",
321
+ children: `Sent as plain text from ${draft.from.address}, in the same thread, with their message quoted beneath. The sequence stays where it is.`
322
+ })
323
+ ]
324
+ }) : null,
325
+ problem ? /*#__PURE__*/ _jsx(Alert, {
326
+ severity: "error",
327
+ children: problem
328
+ }) : null
329
+ ]
330
+ })
331
+ }),
332
+ /*#__PURE__*/ _jsxs(DialogActions, {
333
+ children: [
334
+ /*#__PURE__*/ _jsx(Button, {
335
+ onClick: props.onClose,
336
+ disabled: sending,
337
+ children: "Cancel"
338
+ }),
339
+ /*#__PURE__*/ _jsx(Button, {
340
+ variant: "contained",
341
+ onClick: ()=>void send(),
342
+ disabled: !canSend,
343
+ children: sending ? 'Sending…' : 'Send answer'
344
+ })
345
+ ]
346
+ })
347
+ ]
348
+ });
349
+ }
350
+ OutreachReplyDialog.displayName = 'OutreachReplyDialog';
351
+ export default OutreachReplyDialog;
352
+
353
+ //# sourceMappingURL=reply-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/reply-dialog.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport { OUTREACH_MANUAL_REPLY_BODY_MAX } from '../engine/manual-reply'\nimport type { OutreachEnrollmentReplyResponse, OutreachEnrollmentThreadResponse } from '../model/outreach-api'\nimport type { OutreachEnrollment } from '../model/outreach.types'\nimport { formatOutreachTime, OutreachLoading } from './outreach-ui'\nimport type { OutreachApi } from './use-outreach-api'\n\n/**\n * ANSWER A PERSON WHO WROTE BACK (AGL-4020): a replied enrollment's\n * \"Reply in thread\", on the Enrollments tab, the person's page, and the CRM\n * record of the person.\n *\n * The dialog reads the thread the moment it opens — what the person wrote,\n * kept on the enrollment or read from the mailbox when it was synced before\n * replies were kept — and says where the answer goes: from the enrollment's\n * own mailbox, to the person, `Re:` their subject. The member writes plain\n * text; Send puts it in the same thread in both mailboxes, with their message\n * quoted beneath. Whatever still refuses — the do-not-contact list, a paused\n * or disconnected mailbox — is said before anything is typed, and Send waits.\n *\n * The answer's key is minted once per open: sending again after a lost\n * answer from the server is the same answer, which the route recognises\n * rather than sending twice.\n */\n\nexport interface OutreachReplyDialogProps {\n /** The enrollment to answer, or `null` while closed. */\n enrollment: OutreachEnrollment | null\n api: Pick<OutreachApi, 'readEnrollmentThread' | 'replyToEnrollment'>\n /** The mailbox's zone, for the times shown. */\n timeZone?: string | null\n onClose(): void\n /** After the answer went. */\n onSent?(answer: OutreachEnrollmentReplyResponse): void\n}\n\ntype Stage =\n | { kind: 'reading' }\n | { kind: 'failed'; message: string }\n | { kind: 'ready'; thread: OutreachEnrollmentThreadResponse }\n\n/** One message of the conversation as the dialog shows it. */\ninterface ThreadLine {\n key: string\n atMs: number\n who: string\n subject: string\n body: string\n /** Theirs, or ours. */\n inbound: boolean\n}\n\n/**\n * The conversation, oldest first: what the person wrote, every answer a\n * member sent, and the sequence's own emails by their subject.\n */\nexport function outreachReplyThreadLines(\n enrollment: Pick<OutreachEnrollment, 'inboundMessages' | 'stepRecords'>,\n): ThreadLine[] {\n const lines: ThreadLine[] = []\n for (const message of enrollment.inboundMessages ?? []) {\n lines.push({\n key: `in-${message.id}`,\n atMs: message.atMs,\n who: message.fromName ? `${message.fromName} <${message.from}>` : message.from,\n subject: message.subject,\n body: message.body,\n inbound: true,\n })\n }\n ;(enrollment.stepRecords ?? []).forEach((record, index) => {\n if (record?.kind === 'manual_reply') {\n lines.push({\n key: `answer-${index}`,\n atMs: record.atMs,\n who: record.from ? `You, from ${record.from}` : 'You',\n subject: record.subject ?? '',\n body: record.body ?? '',\n inbound: false,\n })\n } else if (record?.kind === 'email') {\n lines.push({\n key: `step-${index}`,\n atMs: record.atMs,\n who: 'Sequence email',\n subject: record.subject ?? '',\n body: '',\n inbound: false,\n })\n }\n })\n return lines.sort((a, b) => a.atMs - b.atMs)\n}\n\n/** A fresh key for one answer. */\nfunction newReplyKey(): string {\n const crypto = globalThis.crypto as Crypto | undefined\n if (crypto?.randomUUID) return crypto.randomUUID()\n return `r${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`\n}\n\nexport function OutreachReplyDialog(props: OutreachReplyDialogProps) {\n const { enrollment, api, timeZone } = props\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('sm'))\n const { enqueueSnackbar } = useSnackbar()\n const [stage, setStage] = useState<Stage>({ kind: 'reading' })\n const [text, setText] = useState('')\n const [sending, setSending] = useState(false)\n const [problem, setProblem] = useState<string | null>(null)\n const enrollmentId = enrollment?.id ?? null\n // One key per opening: a resend after a lost answer is the same answer.\n const replyKey = useMemo(() => (enrollmentId ? newReplyKey() : ''), [enrollmentId])\n\n useEffect(() => {\n if (!enrollmentId) return undefined\n let cancelled = false\n setStage({ kind: 'reading' })\n setText('')\n setProblem(null)\n api.readEnrollmentThread(enrollmentId).then(\n (thread) => {\n if (!cancelled) setStage({ kind: 'ready', thread })\n },\n (error: unknown) => {\n if (!cancelled) setStage({ kind: 'failed', message: (error as Error).message })\n },\n )\n return () => {\n cancelled = true\n }\n }, [api, enrollmentId])\n\n const thread = stage.kind === 'ready' ? stage.thread : null\n const draft = thread?.draft ?? null\n const lines = thread ? outreachReplyThreadLines(thread.enrollment) : []\n const name = enrollment?.contactName || enrollment?.email || ''\n const title = name ? `Reply to ${name}` : 'Reply in thread'\n const canSend = Boolean(draft) && !thread?.refusal && Boolean(text.trim()) && !sending\n\n const send = async () => {\n if (!enrollmentId || !draft || !canSend) return\n setSending(true)\n setProblem(null)\n try {\n const answer = await api.replyToEnrollment({ enrollmentId, body: text, answerId: draft.answerId, replyKey })\n enqueueSnackbar(\n answer.alreadySent ? `That answer had already gone to ${answer.sentTo}.` : `Answer sent to ${answer.sentTo}.`,\n { variant: 'success' },\n )\n props.onSent?.(answer)\n props.onClose()\n } catch (error) {\n setProblem((error as Error).message)\n } finally {\n setSending(false)\n }\n }\n\n return (\n <Dialog\n open={Boolean(enrollment)}\n onClose={sending ? undefined : props.onClose}\n fullWidth\n maxWidth=\"md\"\n fullScreen={narrow}\n aria-label={title}\n >\n <DialogTitle>{title}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n {stage.kind === 'reading' ? <OutreachLoading label=\"Reading their reply…\" /> : null}\n {stage.kind === 'failed' ? <Alert severity=\"warning\">{stage.message}</Alert> : null}\n {thread && !draft ? (\n <Alert severity=\"info\">\n Nothing they wrote could be read from the mailbox, so there is nothing to answer here yet. Their\n reply is in the mailbox that sent the sequence.\n </Alert>\n ) : null}\n {draft ? (\n <Box\n component=\"dl\"\n sx={{ m: 0, display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 2, rowGap: 0.5 }}\n >\n <Typography component=\"dt\" variant=\"body2\" color=\"text.secondary\">\n From\n </Typography>\n <Typography component=\"dd\" variant=\"body2\" sx={{ m: 0, wordBreak: 'break-word' }}>\n {draft.from.name ? `${draft.from.name} <${draft.from.address}>` : draft.from.address}\n </Typography>\n <Typography component=\"dt\" variant=\"body2\" color=\"text.secondary\">\n To\n </Typography>\n <Typography component=\"dd\" variant=\"body2\" sx={{ m: 0, wordBreak: 'break-word' }}>\n {draft.to}\n </Typography>\n <Typography component=\"dt\" variant=\"body2\" color=\"text.secondary\">\n Subject\n </Typography>\n <Typography component=\"dd\" variant=\"body2\" sx={{ m: 0, wordBreak: 'break-word' }}>\n {draft.subject}\n </Typography>\n </Box>\n ) : null}\n {lines.length ? (\n <Stack\n spacing={1.5}\n role=\"list\"\n aria-label=\"The conversation\"\n sx={{ maxHeight: 320, overflowY: 'auto', border: 1, borderColor: 'divider', borderRadius: 1, p: 1.5 }}\n >\n {lines.map((line) => (\n <Box\n key={line.key}\n role=\"listitem\"\n data-inbound={line.inbound}\n sx={{ pl: 1.5, borderLeft: 2, borderColor: line.inbound ? 'info.main' : 'divider' }}\n >\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={{ xs: 0, sm: 1 }}\n sx={{ justifyContent: 'space-between', alignItems: { sm: 'baseline' } }}\n >\n <Typography variant=\"body2\" sx={{ fontWeight: 500, wordBreak: 'break-word' }}>\n {line.who}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ whiteSpace: 'nowrap' }}>\n {formatOutreachTime(line.atMs, timeZone)}\n </Typography>\n </Stack>\n {line.subject ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n {`Subject: ${line.subject}`}\n </Typography>\n ) : null}\n {line.body ? (\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', mt: 0.5 }}>\n {line.body}\n </Typography>\n ) : null}\n </Box>\n ))}\n </Stack>\n ) : null}\n {thread?.refusal ? <Alert severity=\"warning\">{thread.refusal.message}</Alert> : null}\n {draft && !thread?.refusal ? (\n <>\n <TextField\n label=\"Your answer\"\n value={text}\n onChange={(event) => setText(event.target.value)}\n multiline\n minRows={6}\n fullWidth\n autoFocus\n disabled={sending}\n slotProps={{ htmlInput: { maxLength: OUTREACH_MANUAL_REPLY_BODY_MAX } }}\n />\n <DialogContentText variant=\"body2\">\n {`Sent as plain text from ${draft.from.address}, in the same thread, with their message quoted beneath. The sequence stays where it is.`}\n </DialogContentText>\n </>\n ) : null}\n {problem ? <Alert severity=\"error\">{problem}</Alert> : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={props.onClose} disabled={sending}>\n Cancel\n </Button>\n <Button variant=\"contained\" onClick={() => void send()} disabled={!canSend}>\n {sending ? 'Sending…' : 'Send answer'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nOutreachReplyDialog.displayName = 'OutreachReplyDialog'\n\nexport default OutreachReplyDialog\n"],"names":["useSnackbar","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useMediaQuery","useTheme","useEffect","useMemo","useState","OUTREACH_MANUAL_REPLY_BODY_MAX","formatOutreachTime","OutreachLoading","outreachReplyThreadLines","enrollment","lines","message","inboundMessages","push","key","id","atMs","who","fromName","from","subject","body","inbound","stepRecords","forEach","record","index","kind","sort","a","b","newReplyKey","crypto","globalThis","randomUUID","Date","now","toString","Math","random","slice","OutreachReplyDialog","props","api","timeZone","theme","narrow","breakpoints","down","enqueueSnackbar","stage","setStage","text","setText","sending","setSending","problem","setProblem","enrollmentId","replyKey","undefined","cancelled","readEnrollmentThread","then","thread","error","draft","name","contactName","email","title","canSend","Boolean","refusal","trim","send","answer","replyToEnrollment","answerId","alreadySent","sentTo","variant","onSent","onClose","open","fullWidth","maxWidth","fullScreen","aria-label","dividers","spacing","label","severity","component","sx","m","display","gridTemplateColumns","columnGap","rowGap","color","wordBreak","address","to","length","role","maxHeight","overflowY","border","borderColor","borderRadius","p","map","line","data-inbound","pl","borderLeft","direction","xs","sm","justifyContent","alignItems","fontWeight","whiteSpace","mt","value","onChange","event","target","multiline","minRows","autoFocus","disabled","slotProps","htmlInput","maxLength","onClick","displayName"],"mappings":"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"}