@company-semantics/contracts 51.3.0 → 52.1.0
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/package.json +1 -1
- package/src/identity/__tests__/people-org-chart.test.ts +75 -0
- package/src/identity/people-org-chart.ts +25 -6
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +23 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +309 -259
- package/src/notifications/__tests__/monospace-budget.test.ts +75 -0
- package/src/notifications/renderers/README.md +8 -4
- package/src/notifications/renderers/ascii/README.md +75 -0
- package/src/notifications/renderers/ascii/__tests__/README.md +39 -0
- package/src/notifications/renderers/ascii/__tests__/layout.test.ts +228 -0
- package/src/notifications/renderers/ascii/chat.ts +179 -0
- package/src/notifications/renderers/ascii/cta.ts +57 -0
- package/src/notifications/renderers/ascii/geometry.ts +112 -0
- package/src/notifications/renderers/ascii/index.ts +40 -0
- package/src/notifications/renderers/ascii/keyvalue.ts +34 -0
- package/src/notifications/renderers/ascii/rule.ts +53 -0
- package/src/notifications/renderers/ascii/runs.ts +84 -0
- package/src/notifications/renderers/ascii/signature.ts +41 -0
- package/src/notifications/renderers/ascii/wrap.ts +96 -0
- package/src/notifications/renderers/brand.ts +12 -0
- package/src/notifications/renderers/email/chat.ts +62 -146
- package/src/notifications/renderers/email/constants.ts +17 -2
- package/src/notifications/renderers/email/cta.ts +8 -13
- package/src/notifications/renderers/email/render.ts +29 -5
- package/src/notifications/renderers/layout.ts +31 -0
- package/src/notifications/renderers/slack/README.md +135 -79
- package/src/notifications/renderers/slack/__tests__/README.md +3 -2
- package/src/notifications/renderers/slack/__tests__/index.test.ts +233 -93
- package/src/notifications/renderers/slack/blocks.ts +149 -0
- package/src/notifications/renderers/slack/chat.ts +167 -0
- package/src/notifications/renderers/slack/cta.ts +69 -0
- package/src/notifications/renderers/slack/index.ts +136 -229
- package/src/notifications/renderers/slack/message.ts +23 -0
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Where `../sms` proves the content model survives the poorest channel, this one
|
|
5
5
|
* proves the other edge: a channel whose output is neither a string nor email's
|
|
6
|
-
* `{ subject, text, html }`, but a tree of typed blocks.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* `{ subject, text, html }`, but a tree of typed blocks. The blocks are Slack's
|
|
7
|
+
* OWN vocabulary — `@slack/types`' `KnownBlock` is the union Slack's API
|
|
8
|
+
* validates against, so a drift from the wire format is a compile error rather
|
|
9
|
+
* than a silent one.
|
|
10
10
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* **The voice.** This channel's narrative content is monospace, drawn by
|
|
12
|
+
* `../ascii` — the same functions the email's `text/plain` surface uses, so a
|
|
13
|
+
* conversation cannot come out a column apart on the two channels. Slack's
|
|
14
|
+
* `rich_text_preformatted` is the surface that makes it possible: it is set in a
|
|
15
|
+
* monospace face by Slack itself, so the voice here is REAL rather than a
|
|
16
|
+
* stylesheet the gallery applies and a workspace ignores.
|
|
17
|
+
*
|
|
18
|
+
* **Where the voice stops.** Controls, structure and the title are exempt and
|
|
19
|
+
* use Slack's own primitives: a linked CTA is an `actions` button (`./cta.ts`),
|
|
20
|
+
* a table is a `table` block (`./table.ts`), the headline is a `header`, and an
|
|
21
|
+
* image is an `image`. Sharing a VOCABULARY across channels is the goal; sharing
|
|
22
|
+
* a control REPRESENTATION is not, and a native button beats an ASCII box drawn
|
|
23
|
+
* inside a code block on every measure that matters to a reader.
|
|
18
24
|
*
|
|
19
25
|
* It is STILL non-functional in the one sense that matters most:
|
|
20
26
|
*
|
|
@@ -22,143 +28,71 @@
|
|
|
22
28
|
* renderer renders; delivery and the decision to deliver are the backend's
|
|
23
29
|
* (`SlackChannelDelivery` throws `ChannelNotImplementedError` by design).
|
|
24
30
|
*
|
|
25
|
-
* What it is no longer: low fidelity. The copy is still invented rather than
|
|
26
|
-
* relocated from a shipped Slack app — there is no golden here the way `../email`
|
|
27
|
-
* has one — but the SHAPE is legitimate Block Kit and the `__tests__` pin it.
|
|
28
|
-
*
|
|
29
31
|
* INVARIANTS:
|
|
30
32
|
* - Pure — a function of `(content, context)`. No clock, no environment, no I/O.
|
|
31
33
|
* The brand and the copyright year come from `context.brand`.
|
|
32
|
-
* - `supports` is total
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `plain_text` and `rich_text` are literal surfaces and must NOT be escaped, or
|
|
37
|
-
* the reader sees `&`. See `escapeMrkdwn`.
|
|
34
|
+
* - `supports` is total-true: Slack can now depict every element. That is a
|
|
35
|
+
* claim about TODAY, and `assertNever` below is what keeps it true tomorrow —
|
|
36
|
+
* see the note on the switch.
|
|
37
|
+
* - Escaping follows the SURFACE, not the string. See `./blocks.ts`.
|
|
38
38
|
* - Slack's documented limits are enforced here, not left to the API to reject:
|
|
39
|
-
* a header truncates at 150 chars,
|
|
39
|
+
* a header truncates at 150 chars, an `actions` block splits at 25 elements.
|
|
40
40
|
* - Nothing here reads `metadata.kind`. A renderer that special-cases a kind has
|
|
41
41
|
* lost the model — the fix for a missing fact is upstream in `compose`.
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
-
import type {
|
|
45
|
-
ActionsBlock,
|
|
46
|
-
ContextBlock,
|
|
47
|
-
DividerBlock,
|
|
48
|
-
HeaderBlock,
|
|
49
|
-
ImageBlock,
|
|
50
|
-
KnownBlock,
|
|
51
|
-
MrkdwnElement,
|
|
52
|
-
PlainTextElement,
|
|
53
|
-
RichTextBlock,
|
|
54
|
-
RichTextSection,
|
|
55
|
-
SectionBlock,
|
|
56
|
-
} from "@slack/types";
|
|
44
|
+
import type { KnownBlock } from "@slack/types";
|
|
57
45
|
|
|
58
46
|
import type { NotificationContent, NotificationElement } from "../../content";
|
|
59
47
|
import type { RenderContext } from "../../context";
|
|
60
48
|
import type { Renderer } from "../../renderer";
|
|
49
|
+
import {
|
|
50
|
+
type AsciiGeometry,
|
|
51
|
+
geometryFor,
|
|
52
|
+
keyValueLines,
|
|
53
|
+
proseLines,
|
|
54
|
+
fullRuleAscii,
|
|
55
|
+
signatureLines,
|
|
56
|
+
} from "../ascii";
|
|
57
|
+
import { COMPANY_URL } from "../brand";
|
|
58
|
+
import { MONOSPACE_COLUMNS } from "../layout";
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
import { coalescePreformatted, preformatted } from "./blocks";
|
|
61
|
+
import { renderChatUnitBlocks } from "./chat";
|
|
62
|
+
import { renderCtaBlocks } from "./cta";
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
const HEADER_MAX_CHARS = 150;
|
|
64
|
+
export type { SlackMessage } from "./message";
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
const SECTION_MAX_FIELDS = 10;
|
|
66
|
+
import type { SlackMessage } from "./message";
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// =============================================================================
|
|
68
|
+
/** The same budget email's plain-text surface draws inside (`../layout.ts`). */
|
|
69
|
+
const GEOMETRY: AsciiGeometry = geometryFor(MONOSPACE_COLUMNS);
|
|
75
70
|
|
|
76
71
|
/**
|
|
77
|
-
*
|
|
72
|
+
* A refusal to let a future element disappear quietly.
|
|
78
73
|
*
|
|
79
|
-
* `
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*/
|
|
86
|
-
export interface SlackMessage {
|
|
87
|
-
text: string;
|
|
88
|
-
blocks: KnownBlock[];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// =============================================================================
|
|
92
|
-
// Surfaces
|
|
93
|
-
// =============================================================================
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Slack's three reserved characters, for `mrkdwn` surfaces ONLY.
|
|
74
|
+
* `supports` returns true for everything, which is a fact about the twelve
|
|
75
|
+
* elements that exist today. Left alone, that answer would also cover a
|
|
76
|
+
* thirteenth: the switch below would fall through, return no blocks, and Slack
|
|
77
|
+
* would post a message with a silent hole in it — which is precisely the
|
|
78
|
+
* `chatUnit` bug, reincarnated under a new name and harder to see, because there
|
|
79
|
+
* would no longer be a `supports` returning false to point at it.
|
|
97
80
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* ⚠️ Applying this to a `plain_text` or `rich_text` surface is a DEFECT, not
|
|
101
|
-
* belt-and-braces: those surfaces do not parse `&`/`<`/`>`, so an escaped string
|
|
102
|
-
* renders the entity itself and the reader sees `Acme & Co`. The old
|
|
103
|
-
* hand-written subset could not express the distinction because it had no literal
|
|
104
|
-
* surfaces beyond a button label; `header` and `rich_text` make it load-bearing.
|
|
105
|
-
* `__tests__/index.test.ts` pins both halves.
|
|
81
|
+
* So the switch has no `default`. Adding a `NotificationElement` variant makes
|
|
82
|
+
* this line fail to compile until someone has chosen how Slack says it.
|
|
106
83
|
*/
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
.replace(/>/g, ">");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** An mrkdwn text object, from an already-escaped string. */
|
|
115
|
-
function mrkdwn(text: string): MrkdwnElement {
|
|
116
|
-
return { type: "mrkdwn", text };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** A literal text object. Never escaped — see `escapeMrkdwn`. */
|
|
120
|
-
function plainText(text: string): PlainTextElement {
|
|
121
|
-
return { type: "plain_text", text };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** A section carrying pre-escaped mrkdwn. */
|
|
125
|
-
function section(text: string): SectionBlock {
|
|
126
|
-
return { type: "section", text: mrkdwn(text) };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** A context block carrying pre-escaped mrkdwn. */
|
|
130
|
-
function context(text: string): ContextBlock {
|
|
131
|
-
return { type: "context", elements: [mrkdwn(text)] };
|
|
84
|
+
function assertNever(element: never): never {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Slack renderer has no representation for element: ${JSON.stringify(element)}`,
|
|
87
|
+
);
|
|
132
88
|
}
|
|
133
89
|
|
|
134
|
-
/** A rich-text section wrapping literal text. */
|
|
135
|
-
function richTextSection(text: string): RichTextSection {
|
|
136
|
-
return { type: "rich_text_section", elements: [{ type: "text", text }] };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Split into chunks of at most `size`. Slack caps several collections. */
|
|
140
|
-
function chunk<T>(items: T[], size: number): T[][] {
|
|
141
|
-
const chunks: T[][] = [];
|
|
142
|
-
for (let index = 0; index < items.length; index += size) {
|
|
143
|
-
chunks.push(items.slice(index, index + size));
|
|
144
|
-
}
|
|
145
|
-
return chunks;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// =============================================================================
|
|
149
|
-
// Rendering
|
|
150
|
-
// =============================================================================
|
|
151
|
-
|
|
152
90
|
/**
|
|
153
91
|
* One element as zero or more blocks.
|
|
154
92
|
*
|
|
155
|
-
* Zero
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* types rather than the eleven `supports` admits: the declined arm is unreachable
|
|
159
|
-
* (`render` filters on `supports` first), but writing it out is what makes the
|
|
160
|
-
* compiler prove the two lists agree. A `default` arm would silently swallow a
|
|
161
|
-
* thirteenth member of the union.
|
|
93
|
+
* Zero is still reachable — an empty table or an empty drawing yields nothing —
|
|
94
|
+
* but it is now always a statement about EMPTY CONTENT, never about a channel
|
|
95
|
+
* that gave up on the element.
|
|
162
96
|
*/
|
|
163
97
|
function renderElement(
|
|
164
98
|
element: NotificationElement,
|
|
@@ -166,58 +100,25 @@ function renderElement(
|
|
|
166
100
|
): KnownBlock[] {
|
|
167
101
|
switch (element.type) {
|
|
168
102
|
case "greeting":
|
|
169
|
-
return
|
|
103
|
+
return preformatted(
|
|
104
|
+
proseLines(`Hi ${element.recipientName ?? "there"},`, GEOMETRY),
|
|
105
|
+
);
|
|
170
106
|
case "body":
|
|
171
|
-
return
|
|
107
|
+
return preformatted(proseLines(element.text, GEOMETRY));
|
|
172
108
|
case "keyValueTable":
|
|
173
|
-
// `
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
// Slack caps a section at ten fields; a longer table becomes more sections
|
|
181
|
-
// rather than an API rejection.
|
|
182
|
-
return chunk(element.rows, SECTION_MAX_FIELDS).map((rows) => ({
|
|
183
|
-
type: "section",
|
|
184
|
-
fields: rows.map((row) =>
|
|
185
|
-
mrkdwn(`*${escapeMrkdwn(row.label)}*\n${escapeMrkdwn(row.value)}`),
|
|
186
|
-
),
|
|
187
|
-
}));
|
|
109
|
+
// `Label: value` lines in the monospace flow, exactly as the email says
|
|
110
|
+
// them. A `table` block was tried here and removed: it draws its own
|
|
111
|
+
// bordered grid, so every key/value pair cut the message into another
|
|
112
|
+
// container and a two-fact notification arrived as five stacked boxes.
|
|
113
|
+
// The email states these as plain lines and reads as one thing; matching
|
|
114
|
+
// that is worth more than column alignment two rows never needed.
|
|
115
|
+
return preformatted(keyValueLines(element.rows));
|
|
188
116
|
case "callToAction":
|
|
189
|
-
|
|
190
|
-
// a preformatted block — Slack's code-block surface, which renders it in a
|
|
191
|
-
// monospaced box the reader can select cleanly — rather than a button with
|
|
192
|
-
// a fabricated destination. `rich_text` is a LITERAL surface: no escaping.
|
|
193
|
-
return element.href
|
|
194
|
-
? [
|
|
195
|
-
{
|
|
196
|
-
type: "actions",
|
|
197
|
-
elements: [
|
|
198
|
-
{
|
|
199
|
-
type: "button",
|
|
200
|
-
text: plainText(element.label),
|
|
201
|
-
url: element.href,
|
|
202
|
-
},
|
|
203
|
-
],
|
|
204
|
-
},
|
|
205
|
-
]
|
|
206
|
-
: [
|
|
207
|
-
{
|
|
208
|
-
type: "rich_text",
|
|
209
|
-
elements: [
|
|
210
|
-
{
|
|
211
|
-
type: "rich_text_preformatted",
|
|
212
|
-
elements: [{ type: "text", text: element.label }],
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
},
|
|
216
|
-
];
|
|
117
|
+
return renderCtaBlocks(element, GEOMETRY);
|
|
217
118
|
case "list":
|
|
218
|
-
// A real `rich_text_list`, not bullet characters glued onto a string.
|
|
219
|
-
// draws the markers, the indentation and the numbering; `ordered`
|
|
220
|
-
// its own `style`. Literal surface — no escaping.
|
|
119
|
+
// A real `rich_text_list`, not bullet characters glued onto a string.
|
|
120
|
+
// Slack draws the markers, the indentation and the numbering; `ordered`
|
|
121
|
+
// maps onto its own `style`. Literal surface — no escaping.
|
|
221
122
|
return [
|
|
222
123
|
{
|
|
223
124
|
type: "rich_text",
|
|
@@ -225,7 +126,10 @@ function renderElement(
|
|
|
225
126
|
{
|
|
226
127
|
type: "rich_text_list",
|
|
227
128
|
style: element.ordered ? "ordered" : "bullet",
|
|
228
|
-
elements: element.items.map(
|
|
129
|
+
elements: element.items.map((item) => ({
|
|
130
|
+
type: "rich_text_section",
|
|
131
|
+
elements: [{ type: "text", text: item }],
|
|
132
|
+
})),
|
|
229
133
|
},
|
|
230
134
|
],
|
|
231
135
|
},
|
|
@@ -233,68 +137,68 @@ function renderElement(
|
|
|
233
137
|
case "divider":
|
|
234
138
|
return [{ type: "divider" }];
|
|
235
139
|
case "warning":
|
|
236
|
-
// Fieldless by design: the banner IS the content
|
|
237
|
-
//
|
|
238
|
-
|
|
140
|
+
// Fieldless by design: the banner IS the content. Email sets it in the
|
|
141
|
+
// same squared glyphs, and they are the only part of this notification
|
|
142
|
+
// whose shape survives a channel with no colour of its own.
|
|
143
|
+
return preformatted(proseLines("🆆🅰🆁🅽🅸🅽🅶", GEOMETRY));
|
|
239
144
|
case "metadata":
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
...element.rows.map(
|
|
247
|
-
(row) => `${escapeMrkdwn(row.label)}: ${escapeMrkdwn(row.value)}`,
|
|
248
|
-
),
|
|
249
|
-
]
|
|
250
|
-
.filter(Boolean)
|
|
251
|
-
.join(" · "),
|
|
252
|
-
),
|
|
253
|
-
];
|
|
145
|
+
// Same lines as `keyValueTable`, under the element's own heading. The
|
|
146
|
+
// de-emphasis a `context` block used to give this is dropped on purpose:
|
|
147
|
+
// it pulled the diagnostics out of the monospace flow into Slack's sans,
|
|
148
|
+
// so the one element meant to recede was the one that looked different
|
|
149
|
+
// from everything around it.
|
|
150
|
+
return preformatted(keyValueLines(element.rows, element.label));
|
|
254
151
|
case "notice":
|
|
255
|
-
return
|
|
152
|
+
return preformatted(
|
|
153
|
+
element.lines.flatMap((line) => proseLines(line, GEOMETRY)),
|
|
154
|
+
);
|
|
155
|
+
case "chatUnit":
|
|
156
|
+
return renderChatUnitBlocks(element.items, GEOMETRY);
|
|
256
157
|
case "signature":
|
|
158
|
+
// The same sign-off email draws, from the same function — the marker, the
|
|
159
|
+
// copyright line, the URL. The URL arrives as a LINK run, which the
|
|
160
|
+
// preformatted surface admits; email's plain-text body can only flatten it.
|
|
161
|
+
//
|
|
257
162
|
// Both the signer fallback and the year come from the context. This is the
|
|
258
163
|
// element that used to hide a `new Date()`; here there is nowhere to hide.
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
164
|
+
return preformatted(
|
|
165
|
+
signatureLines(
|
|
166
|
+
element.signer ?? renderContext.brand.name,
|
|
167
|
+
renderContext.brand.copyrightYear,
|
|
168
|
+
COMPANY_URL,
|
|
264
169
|
),
|
|
265
|
-
|
|
170
|
+
);
|
|
266
171
|
case "heroImage":
|
|
267
172
|
return [{ type: "image", image_url: element.src, alt_text: element.alt }];
|
|
268
|
-
|
|
269
|
-
return
|
|
173
|
+
default:
|
|
174
|
+
return assertNever(element);
|
|
270
175
|
}
|
|
271
176
|
}
|
|
272
177
|
|
|
273
178
|
/**
|
|
274
|
-
* The
|
|
275
|
-
* subject.
|
|
179
|
+
* The title, as the first lines of the message body.
|
|
276
180
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
181
|
+
* NOT a `header` block. A header is `plain_text` and Slack owns its typography
|
|
182
|
+
* entirely — `HeaderBlock` is `{ type, text }` and there is no font, size,
|
|
183
|
+
* weight or colour anywhere in the schema — so a header would be the one line of
|
|
184
|
+
* the notification that could never be set in the brand's face. Putting the
|
|
185
|
+
* title in the monospace body makes it monospace FOR REAL, in a workspace as
|
|
186
|
+
* well as in the gallery, rather than by a stylesheet the gallery applies and
|
|
187
|
+
* Slack ignores.
|
|
282
188
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
189
|
+
* What that costs is Slack's large headline treatment, which is what makes a
|
|
190
|
+
* message scannable in a busy channel. The trade is deliberate: the title is
|
|
191
|
+
* still the FALLBACK (`text`), so a push notification and a screen reader
|
|
192
|
+
* announce it exactly as before — the header block was never what carried it
|
|
193
|
+
* there.
|
|
194
|
+
*
|
|
195
|
+
* A rule underneath does the work the type size used to.
|
|
286
196
|
*/
|
|
287
|
-
function
|
|
197
|
+
function renderTitle(content: NotificationContent): KnownBlock[] {
|
|
288
198
|
const title = content.metadata.title.trim();
|
|
289
199
|
if (!title) return [];
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** Cut to `max` characters, marking the cut so a clipped headline reads as one. */
|
|
296
|
-
function truncate(value: string, max: number): string {
|
|
297
|
-
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
200
|
+
const lines = proseLines(title, GEOMETRY);
|
|
201
|
+
return preformatted([...lines, fullRuleAscii(GEOMETRY)]);
|
|
298
202
|
}
|
|
299
203
|
|
|
300
204
|
/**
|
|
@@ -305,25 +209,27 @@ function truncate(value: string, max: number): string {
|
|
|
305
209
|
export const slackRenderer: Renderer<SlackMessage> = {
|
|
306
210
|
id: "slack",
|
|
307
211
|
/**
|
|
308
|
-
*
|
|
212
|
+
* Slack depicts every element.
|
|
309
213
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
214
|
+
* `chatUnit` was the one decline, on the grounds that Block Kit has no
|
|
215
|
+
* vocabulary for turn-taking. True of blocks, and beside the point about
|
|
216
|
+
* messages: the kinds that nest a payload inside a `chatUnit` were arriving
|
|
217
|
+
* with the payload missing. A monospace drawing depicts the conversation and
|
|
218
|
+
* keeps the attribution the decline was protecting, so there is nothing left
|
|
219
|
+
* to decline.
|
|
220
|
+
*
|
|
221
|
+
* `assertNever` in `renderElement`, not this answer, is what protects the
|
|
222
|
+
* NEXT element.
|
|
316
223
|
*/
|
|
317
|
-
supports: (
|
|
224
|
+
supports: () => true,
|
|
318
225
|
render: (content: NotificationContent, renderContext: RenderContext) => ({
|
|
319
226
|
text: content.metadata.title,
|
|
320
|
-
blocks: [
|
|
321
|
-
...
|
|
227
|
+
blocks: coalescePreformatted([
|
|
228
|
+
...renderTitle(content),
|
|
322
229
|
...content.sections
|
|
323
230
|
.flatMap((section_) => section_.elements)
|
|
324
|
-
.filter((element) => slackRenderer.supports(element.type))
|
|
325
231
|
.flatMap((element) => renderElement(element, renderContext)),
|
|
326
|
-
],
|
|
232
|
+
]),
|
|
327
233
|
}),
|
|
328
234
|
};
|
|
329
235
|
|
|
@@ -343,4 +249,5 @@ export type {
|
|
|
343
249
|
PlainTextElement,
|
|
344
250
|
RichTextBlock,
|
|
345
251
|
SectionBlock,
|
|
346
|
-
|
|
252
|
+
TableBlock,
|
|
253
|
+
} from "@slack/types";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This channel's natural output type.
|
|
3
|
+
*
|
|
4
|
+
* In its own module so `./index.ts`, `./chat.ts` and the backend's delivery can
|
|
5
|
+
* all name it without importing the renderer itself.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { KnownBlock } from "@slack/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One Slack message.
|
|
12
|
+
*
|
|
13
|
+
* `blocks` is `KnownBlock[]` — Slack's own union, not ours. `text` is the
|
|
14
|
+
* notification-and-fallback string Slack shows where blocks cannot render (a push
|
|
15
|
+
* notification, a screen reader). It is `metadata.title`, which is the same field
|
|
16
|
+
* email spends as its subject and this channel ALSO spends as its `header` block:
|
|
17
|
+
* one content-level fact, three channel-level uses, which is why it lives on the
|
|
18
|
+
* content rather than in a channel's registry.
|
|
19
|
+
*/
|
|
20
|
+
export interface SlackMessage {
|
|
21
|
+
text: string;
|
|
22
|
+
blocks: KnownBlock[];
|
|
23
|
+
}
|