@aglyn/plugins-outreach 1.0.0-beta.274 → 1.0.0-beta.277
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 +7 -2
- package/package.json +14 -14
- package/src/lib/components/enrollment-actions.d.ts +11 -4
- package/src/lib/components/enrollment-actions.js +16 -0
- package/src/lib/components/enrollment-actions.js.map +1 -1
- package/src/lib/components/enrollment-detail.js +40 -13
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/mailbox-card.js +13 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/record-reply-button.d.ts +52 -0
- package/src/lib/components/record-reply-button.js +92 -0
- package/src/lib/components/record-reply-button.js.map +1 -0
- package/src/lib/components/reply-dialog.d.ts +67 -0
- package/src/lib/components/reply-dialog.js +353 -0
- package/src/lib/components/reply-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +5 -1
- package/src/lib/components/use-outreach-api.js +10 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +8 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/index.d.ts +1 -0
- package/src/lib/engine/index.js +1 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/manual-reply.d.ts +122 -0
- package/src/lib/engine/manual-reply.js +311 -0
- package/src/lib/engine/manual-reply.js.map +1 -0
- package/src/lib/model/enrollment-history.d.ts +2 -0
- package/src/lib/model/enrollment-history.js +10 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -1
- package/src/lib/model/enrollment-timeline.js +90 -9
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +68 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +91 -3
- package/src/lib/model/outreach.types.js +6 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +7 -1
- package/src/lib/model/stored-records.js +37 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +7 -0
- package/src/lib/routes/register-routes.js +26 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/reply-routes.d.ts +92 -0
- package/src/lib/routes/reply-routes.js +644 -0
- package/src/lib/routes/reply-routes.js.map +1 -0
- package/src/lib/runtime/click-events.d.ts +13 -0
- package/src/lib/runtime/click-events.js +19 -6
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-link.d.ts +8 -0
- package/src/lib/runtime/click-link.js +7 -0
- package/src/lib/runtime/click-link.js.map +1 -1
- package/src/lib/runtime/sync-job.js +14 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/transport/send-message.d.ts +11 -0
- package/src/lib/transport/send-message.js +22 -0
- 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":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
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 — and, on a sequence that tracks clicks, the links they
|
|
31
|
+
* wrote as the same short links a step's carry (AGL-4022). Whatever still refuses — the do-not-contact list, a paused
|
|
32
|
+
* or disconnected mailbox — is said before anything is typed, and Send waits.
|
|
33
|
+
*
|
|
34
|
+
* The answer's key is minted once per open: sending again after a lost
|
|
35
|
+
* answer from the server is the same answer, which the route recognises
|
|
36
|
+
* rather than sending twice.
|
|
37
|
+
*/
|
|
38
|
+
export interface OutreachReplyDialogProps {
|
|
39
|
+
/** The enrollment to answer, or `null` while closed. */
|
|
40
|
+
enrollment: OutreachEnrollment | null;
|
|
41
|
+
api: Pick<OutreachApi, 'readEnrollmentThread' | 'replyToEnrollment'>;
|
|
42
|
+
/** The mailbox's zone, for the times shown. */
|
|
43
|
+
timeZone?: string | null;
|
|
44
|
+
onClose(): void;
|
|
45
|
+
/** After the answer went. */
|
|
46
|
+
onSent?(answer: OutreachEnrollmentReplyResponse): void;
|
|
47
|
+
}
|
|
48
|
+
/** One message of the conversation as the dialog shows it. */
|
|
49
|
+
interface ThreadLine {
|
|
50
|
+
key: string;
|
|
51
|
+
atMs: number;
|
|
52
|
+
who: string;
|
|
53
|
+
subject: string;
|
|
54
|
+
body: string;
|
|
55
|
+
/** Theirs, or ours. */
|
|
56
|
+
inbound: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The conversation, oldest first: what the person wrote, every answer a
|
|
60
|
+
* member sent, and the sequence's own emails by their subject.
|
|
61
|
+
*/
|
|
62
|
+
export declare function outreachReplyThreadLines(enrollment: Pick<OutreachEnrollment, 'inboundMessages' | 'stepRecords'>): ThreadLine[];
|
|
63
|
+
export declare function OutreachReplyDialog(props: OutreachReplyDialogProps): import("react").JSX.Element;
|
|
64
|
+
export declare namespace OutreachReplyDialog {
|
|
65
|
+
var displayName: string;
|
|
66
|
+
}
|
|
67
|
+
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.${draft.tracksLinks ? ' Links you write go out as tracked short links, as in the sequence’s emails.' : ''} 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 — and, on a sequence that tracks clicks, the links they\n * wrote as the same short links a step's carry (AGL-4022). 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.${\n draft.tracksLinks ? ' Links you write go out as tracked short links, as in the sequence’s emails.' : ''\n } 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","tracksLinks","onClick","displayName"],"mappings":"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"}
|