@nkootstra/block-kit 0.0.1 → 0.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/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/index-BV_5_J_Q.d.ts +909 -0
- package/dist/index-gBduJdQh.d.ts +127 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +10870 -0
- package/dist/index.js.map +1 -0
- package/dist/mrkdwn.d.ts +3 -0
- package/dist/mrkdwn.js +3 -0
- package/dist/parser-GbXBz4_f.js +441 -0
- package/dist/parser-GbXBz4_f.js.map +1 -0
- package/dist/server.d.ts +60 -0
- package/dist/server.js +83 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +4780 -0
- package/dist/transport.d.ts +50 -0
- package/dist/transport.js +84 -0
- package/dist/transport.js.map +1 -0
- package/dist/web-api.d.ts +45 -0
- package/dist/web-api.js +121 -0
- package/dist/web-api.js.map +1 -0
- package/package.json +65 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
import { N as BlockKitProviderProps, ht as ViewResponseAction } from "./index-BV_5_J_Q.js";
|
|
3
|
+
//#region src/interactivity/transport.d.ts
|
|
4
|
+
export interface InteractionResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
status: number;
|
|
7
|
+
/** Parsed `response_action` body, when the app replied with one (view_submission acks). */
|
|
8
|
+
responseAction?: ViewResponseAction;
|
|
9
|
+
/** The raw parsed (or, if not JSON, raw text) response body. */
|
|
10
|
+
raw?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** Encodes a payload the way Slack does: `application/x-www-form-urlencoded` `payload=<json>`. */
|
|
13
|
+
export declare function encodeInteraction(payload: unknown): string;
|
|
14
|
+
/** Reads an app's reply to an interaction: JSON when it parses, text otherwise. */
|
|
15
|
+
export declare function readInteractionResponse(res: Response): Promise<InteractionResult>;
|
|
16
|
+
/** Thrown by `onSubmit`/`onOptions` when the app couldn't be reached or didn't answer 2xx. */
|
|
17
|
+
export declare class InteractionError extends Error {
|
|
18
|
+
readonly result?: InteractionResult | undefined;
|
|
19
|
+
constructor(message: string, result?: InteractionResult | undefined);
|
|
20
|
+
}
|
|
21
|
+
export interface HttpTransportOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Where payloads are POSTed, in Slack's wire format but unsigned: a relay made with
|
|
24
|
+
* `createInteractionRelay` (which signs and forwards them), or an app that doesn't verify
|
|
25
|
+
* signatures (e.g. Bolt with `signatureVerification: false`).
|
|
26
|
+
*/
|
|
27
|
+
url: string;
|
|
28
|
+
headers?: HeadersInit;
|
|
29
|
+
/** Injectable for tests; defaults to the global `fetch`. */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
/** Called with every payload sent and the app's reply. */
|
|
32
|
+
onResponse?: (payload: unknown, result: InteractionResult) => void;
|
|
33
|
+
/** Called when a payload couldn't be delivered or the app answered with a non-2xx status. */
|
|
34
|
+
onError?: (payload: unknown, error: InteractionError) => void;
|
|
35
|
+
}
|
|
36
|
+
/** The `BlockKitProvider` callbacks that deliver interactions, ready to spread onto it. */
|
|
37
|
+
export type InteractionTransport = Required<Pick<BlockKitProviderProps, "onPayload" | "onSubmit" | "onClose" | "onOptions">>;
|
|
38
|
+
/**
|
|
39
|
+
* Sends every interaction a `BlockKitProvider` produces to an app over HTTP, in place of the
|
|
40
|
+
* local handlers: `block_actions` and `view_closed` are fire-and-forget, a `view_submission`'s
|
|
41
|
+
* `response_action` ack is applied to the modal, and a `block_suggestion`'s reply fills the
|
|
42
|
+
* external select.
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <BlockKitProvider {...httpTransport({ url: "/slack/relay" })}>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function httpTransport(options: HttpTransportOptions): InteractionTransport;
|
|
49
|
+
//#endregion
|
|
50
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/interactivity/transport.ts
|
|
3
|
+
/** Encodes a payload the way Slack does: `application/x-www-form-urlencoded` `payload=<json>`. */
|
|
4
|
+
function encodeInteraction(payload) {
|
|
5
|
+
return `payload=${encodeURIComponent(JSON.stringify(payload))}`;
|
|
6
|
+
}
|
|
7
|
+
/** Reads an app's reply to an interaction: JSON when it parses, text otherwise. */
|
|
8
|
+
async function readInteractionResponse(res) {
|
|
9
|
+
const text = await res.text();
|
|
10
|
+
let raw;
|
|
11
|
+
if (text) try {
|
|
12
|
+
raw = JSON.parse(text);
|
|
13
|
+
} catch {
|
|
14
|
+
raw = text;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
ok: res.ok,
|
|
18
|
+
status: res.status,
|
|
19
|
+
responseAction: isViewResponseAction(raw) ? raw : void 0,
|
|
20
|
+
raw
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isViewResponseAction(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && "response_action" in value && typeof value.response_action === "string";
|
|
25
|
+
}
|
|
26
|
+
/** Thrown by `onSubmit`/`onOptions` when the app couldn't be reached or didn't answer 2xx. */
|
|
27
|
+
var InteractionError = class extends Error {
|
|
28
|
+
result;
|
|
29
|
+
constructor(message, result) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.result = result;
|
|
32
|
+
this.name = "InteractionError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Sends every interaction a `BlockKitProvider` produces to an app over HTTP, in place of the
|
|
37
|
+
* local handlers: `block_actions` and `view_closed` are fire-and-forget, a `view_submission`'s
|
|
38
|
+
* `response_action` ack is applied to the modal, and a `block_suggestion`'s reply fills the
|
|
39
|
+
* external select.
|
|
40
|
+
*
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <BlockKitProvider {...httpTransport({ url: "/slack/relay" })}>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function httpTransport(options) {
|
|
46
|
+
const send = async (payload) => {
|
|
47
|
+
const doFetch = options.fetch ?? fetch;
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = await readInteractionResponse(await doFetch(options.url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
54
|
+
...Object.fromEntries(new Headers(options.headers))
|
|
55
|
+
},
|
|
56
|
+
body: encodeInteraction(payload)
|
|
57
|
+
}));
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
const error = new InteractionError(`Couldn't reach ${options.url}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
60
|
+
options.onError?.(payload, error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
options.onResponse?.(payload, result);
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
const error = new InteractionError(`${options.url} answered ${result.status}`, result);
|
|
66
|
+
options.onError?.(payload, error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
const fireAndForget = (payload) => {
|
|
72
|
+
send(payload).catch(() => {});
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
onPayload: fireAndForget,
|
|
76
|
+
onClose: fireAndForget,
|
|
77
|
+
onSubmit: async (payload) => (await send(payload)).responseAction,
|
|
78
|
+
onOptions: async (payload) => (await send(payload)).raw
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { InteractionError, encodeInteraction, httpTransport, readInteractionResponse };
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","names":[],"sources":["../src/interactivity/transport.ts"],"sourcesContent":["import type { BlockKitProviderProps, OptionsResponse, ViewResponseAction } from \"../index\";\n\n// Browser-safe: no Node imports here, so `@nkootstra/block-kit/transport` can be\n// bundled into a page. Signing lives in `sign.ts`/`relay.ts`, which only run on a server.\n\nexport interface InteractionResult {\n ok: boolean;\n status: number;\n /** Parsed `response_action` body, when the app replied with one (view_submission acks). */\n responseAction?: ViewResponseAction;\n /** The raw parsed (or, if not JSON, raw text) response body. */\n raw?: unknown;\n}\n\n/** Encodes a payload the way Slack does: `application/x-www-form-urlencoded` `payload=<json>`. */\nexport function encodeInteraction(payload: unknown): string {\n return `payload=${encodeURIComponent(JSON.stringify(payload))}`;\n}\n\n/** Reads an app's reply to an interaction: JSON when it parses, text otherwise. */\nexport async function readInteractionResponse(res: Response): Promise<InteractionResult> {\n const text = await res.text();\n let raw: unknown;\n if (text) {\n try {\n raw = JSON.parse(text);\n } catch {\n raw = text;\n }\n }\n return {\n ok: res.ok,\n status: res.status,\n responseAction: isViewResponseAction(raw) ? raw : undefined,\n raw,\n };\n}\n\nfunction isViewResponseAction(value: unknown): value is ViewResponseAction {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"response_action\" in value &&\n typeof (value as { response_action: unknown }).response_action === \"string\"\n );\n}\n\n/** Thrown by `onSubmit`/`onOptions` when the app couldn't be reached or didn't answer 2xx. */\nexport class InteractionError extends Error {\n constructor(\n message: string,\n readonly result?: InteractionResult,\n ) {\n super(message);\n this.name = \"InteractionError\";\n }\n}\n\nexport interface HttpTransportOptions {\n /**\n * Where payloads are POSTed, in Slack's wire format but unsigned: a relay made with\n * `createInteractionRelay` (which signs and forwards them), or an app that doesn't verify\n * signatures (e.g. Bolt with `signatureVerification: false`).\n */\n url: string;\n headers?: HeadersInit;\n /** Injectable for tests; defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** Called with every payload sent and the app's reply. */\n onResponse?: (payload: unknown, result: InteractionResult) => void;\n /** Called when a payload couldn't be delivered or the app answered with a non-2xx status. */\n onError?: (payload: unknown, error: InteractionError) => void;\n}\n\n/** The `BlockKitProvider` callbacks that deliver interactions, ready to spread onto it. */\nexport type InteractionTransport = Required<\n Pick<BlockKitProviderProps, \"onPayload\" | \"onSubmit\" | \"onClose\" | \"onOptions\">\n>;\n\n/**\n * Sends every interaction a `BlockKitProvider` produces to an app over HTTP, in place of the\n * local handlers: `block_actions` and `view_closed` are fire-and-forget, a `view_submission`'s\n * `response_action` ack is applied to the modal, and a `block_suggestion`'s reply fills the\n * external select.\n *\n * ```tsx\n * <BlockKitProvider {...httpTransport({ url: \"/slack/relay\" })}>\n * ```\n */\nexport function httpTransport(options: HttpTransportOptions): InteractionTransport {\n const send = async (payload: unknown): Promise<InteractionResult> => {\n const doFetch = options.fetch ?? fetch;\n let result: InteractionResult;\n try {\n result = await readInteractionResponse(\n await doFetch(options.url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n ...Object.fromEntries(new Headers(options.headers)),\n },\n body: encodeInteraction(payload),\n }),\n );\n } catch (cause) {\n const error = new InteractionError(\n `Couldn't reach ${options.url}: ${cause instanceof Error ? cause.message : String(cause)}`,\n );\n options.onError?.(payload, error);\n throw error;\n }\n options.onResponse?.(payload, result);\n if (!result.ok) {\n const error = new InteractionError(`${options.url} answered ${result.status}`, result);\n options.onError?.(payload, error);\n throw error;\n }\n return result;\n };\n // Nothing awaits these; failures are reported through `onError` instead.\n const fireAndForget = (payload: unknown) => {\n send(payload).catch(() => {});\n };\n\n return {\n onPayload: fireAndForget,\n onClose: fireAndForget,\n onSubmit: async (payload) => (await send(payload)).responseAction,\n onOptions: async (payload) => (await send(payload)).raw as OptionsResponse,\n };\n}\n"],"mappings":";;;AAeA,SAAgB,kBAAkB,SAA0B;CAC1D,OAAO,WAAW,mBAAmB,KAAK,UAAU,OAAO,CAAC;AAC9D;;AAGA,eAAsB,wBAAwB,KAA2C;CACvF,MAAM,OAAO,MAAM,IAAI,KAAK;CAC5B,IAAI;CACJ,IAAI,MACF,IAAI;EACF,MAAM,KAAK,MAAM,IAAI;CACvB,QAAQ;EACN,MAAM;CACR;CAEF,OAAO;EACL,IAAI,IAAI;EACR,QAAQ,IAAI;EACZ,gBAAgB,qBAAqB,GAAG,IAAI,MAAM,KAAA;EAClD;CACF;AACF;AAEA,SAAS,qBAAqB,OAA6C;CACzE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,qBAAqB,SACrB,OAAQ,MAAuC,oBAAoB;AAEvE;;AAGA,IAAa,mBAAb,cAAsC,MAAM;CAG/B;CAFX,YACE,SACA,QACA;EACA,MAAM,OAAO;EAFJ,KAAA,SAAA;EAGT,KAAK,OAAO;CACd;AACF;;;;;;;;;;;AAiCA,SAAgB,cAAc,SAAqD;CACjF,MAAM,OAAO,OAAO,YAAiD;EACnE,MAAM,UAAU,QAAQ,SAAS;EACjC,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,wBACb,MAAM,QAAQ,QAAQ,KAAK;IACzB,QAAQ;IACR,SAAS;KACP,gBAAgB;KAChB,GAAG,OAAO,YAAY,IAAI,QAAQ,QAAQ,OAAO,CAAC;IACpD;IACA,MAAM,kBAAkB,OAAO;GACjC,CAAC,CACH;EACF,SAAS,OAAO;GACd,MAAM,QAAQ,IAAI,iBAChB,kBAAkB,QAAQ,IAAI,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACzF;GACA,QAAQ,UAAU,SAAS,KAAK;GAChC,MAAM;EACR;EACA,QAAQ,aAAa,SAAS,MAAM;EACpC,IAAI,CAAC,OAAO,IAAI;GACd,MAAM,QAAQ,IAAI,iBAAiB,GAAG,QAAQ,IAAI,YAAY,OAAO,UAAU,MAAM;GACrF,QAAQ,UAAU,SAAS,KAAK;GAChC,MAAM;EACR;EACA,OAAO;CACT;CAEA,MAAM,iBAAiB,YAAqB;EAC1C,KAAK,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;CAC9B;CAEA,OAAO;EACL,WAAW;EACX,SAAS;EACT,UAAU,OAAO,aAAa,MAAM,KAAK,OAAO,EAAA,CAAG;EACnD,WAAW,OAAO,aAAa,MAAM,KAAK,OAAO,EAAA,CAAG;CACtD;AACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
import { z as Resolvers } from "./index-BV_5_J_Q.js";
|
|
3
|
+
import { WebClient } from "@slack/web-api";
|
|
4
|
+
//#region src/web-api/resolvers.d.ts
|
|
5
|
+
interface WebApiResolversOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Milliseconds to coalesce `user`/`channel` lookups requested in quick succession (e.g. every
|
|
8
|
+
* mention in a long message) before firing the batch of requests concurrently. Default 20.
|
|
9
|
+
*/
|
|
10
|
+
batchWindowMs?: number;
|
|
11
|
+
}
|
|
12
|
+
interface WebApiResolvers extends Resolvers {
|
|
13
|
+
/**
|
|
14
|
+
* Registers a listener called whenever a previously-unresolved id finishes looking up (whether
|
|
15
|
+
* it resolved to a name or turned out to be unknown). Returns an unsubscribe function. Since the
|
|
16
|
+
* resolver functions themselves are synchronous, a consumer re-renders by subscribing and
|
|
17
|
+
* forcing an update — see `useWebApiResolvers`.
|
|
18
|
+
*/
|
|
19
|
+
subscribe(listener: () => void): () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds `Resolvers` (see `context.tsx`) backed by the real Slack Web API
|
|
23
|
+
* or an emulator's implementation of it: `users.info`, `conversations.info`, and `usergroups.list`
|
|
24
|
+
* for usergroups (there is no per-id usergroup lookup in the Web API, so the whole list is fetched
|
|
25
|
+
* once and cached).
|
|
26
|
+
*
|
|
27
|
+
* The resolver functions are synchronous — `(id) => string | undefined` — so a lookup that hasn't
|
|
28
|
+
* completed yet returns `undefined` immediately and schedules a background fetch; call `subscribe`
|
|
29
|
+
* (or the `useWebApiResolvers` hook) to be notified when it completes so the tree can re-render
|
|
30
|
+
* with the resolved name.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createWebApiResolvers(client: WebClient, opts?: WebApiResolversOptions): WebApiResolvers;
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/web-api/useWebApiResolvers.d.ts
|
|
35
|
+
/**
|
|
36
|
+
* `createWebApiResolvers`, wired up to re-render the calling component whenever a lookup
|
|
37
|
+
* completes. Pass the returned resolvers straight to `<BlockKitProvider resolvers={...}>`.
|
|
38
|
+
*
|
|
39
|
+
* `opts` is only read when `client` (identity) changes, matching `createWebApiResolvers` being
|
|
40
|
+
* called once per client rather than on every render.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useWebApiResolvers(client: WebClient, opts?: WebApiResolversOptions): WebApiResolvers;
|
|
43
|
+
//#endregion
|
|
44
|
+
export type { WebApiResolvers, WebApiResolversOptions };
|
|
45
|
+
//# sourceMappingURL=web-api.d.ts.map
|
package/dist/web-api.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
//#region src/web-api/resolvers.ts
|
|
4
|
+
/**
|
|
5
|
+
* Builds `Resolvers` (see `context.tsx`) backed by the real Slack Web API
|
|
6
|
+
* or an emulator's implementation of it: `users.info`, `conversations.info`, and `usergroups.list`
|
|
7
|
+
* for usergroups (there is no per-id usergroup lookup in the Web API, so the whole list is fetched
|
|
8
|
+
* once and cached).
|
|
9
|
+
*
|
|
10
|
+
* The resolver functions are synchronous — `(id) => string | undefined` — so a lookup that hasn't
|
|
11
|
+
* completed yet returns `undefined` immediately and schedules a background fetch; call `subscribe`
|
|
12
|
+
* (or the `useWebApiResolvers` hook) to be notified when it completes so the tree can re-render
|
|
13
|
+
* with the resolved name.
|
|
14
|
+
*/
|
|
15
|
+
function createWebApiResolvers(client, opts = {}) {
|
|
16
|
+
const batchWindowMs = opts.batchWindowMs ?? 20;
|
|
17
|
+
const cache = /* @__PURE__ */ new Map();
|
|
18
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
19
|
+
const queues = /* @__PURE__ */ new Map([["user", /* @__PURE__ */ new Set()], ["channel", /* @__PURE__ */ new Set()]]);
|
|
20
|
+
const timers = /* @__PURE__ */ new Map();
|
|
21
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
let usergroupsLoaded = false;
|
|
23
|
+
let usergroupsLoading;
|
|
24
|
+
function notify() {
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
}
|
|
27
|
+
function cacheKey(kind, id) {
|
|
28
|
+
return `${kind}:${id}`;
|
|
29
|
+
}
|
|
30
|
+
function lookup(kind, id) {
|
|
31
|
+
const key = cacheKey(kind, id);
|
|
32
|
+
if (cache.has(key)) return cache.get(key) ?? void 0;
|
|
33
|
+
schedule(kind, id);
|
|
34
|
+
}
|
|
35
|
+
function schedule(kind, id) {
|
|
36
|
+
const key = cacheKey(kind, id);
|
|
37
|
+
if (inflight.has(key)) return;
|
|
38
|
+
inflight.add(key);
|
|
39
|
+
queues.get(kind)?.add(id);
|
|
40
|
+
if (!timers.has(kind)) timers.set(kind, setTimeout(() => {
|
|
41
|
+
timers.delete(kind);
|
|
42
|
+
flush(kind);
|
|
43
|
+
}, batchWindowMs));
|
|
44
|
+
}
|
|
45
|
+
async function flush(kind) {
|
|
46
|
+
const queue = queues.get(kind);
|
|
47
|
+
const ids = queue ? [...queue] : [];
|
|
48
|
+
queue?.clear();
|
|
49
|
+
await Promise.all(ids.map((id) => fetchOne(kind, id)));
|
|
50
|
+
}
|
|
51
|
+
async function fetchOne(kind, id) {
|
|
52
|
+
const key = cacheKey(kind, id);
|
|
53
|
+
try {
|
|
54
|
+
const name = kind === "user" ? await fetchUserName(id) : await fetchChannelName(id);
|
|
55
|
+
cache.set(key, name ?? null);
|
|
56
|
+
} catch {
|
|
57
|
+
cache.set(key, null);
|
|
58
|
+
} finally {
|
|
59
|
+
inflight.delete(key);
|
|
60
|
+
notify();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function fetchUserName(id) {
|
|
64
|
+
const user = (await client.users.info({ user: id })).user;
|
|
65
|
+
return user?.profile?.display_name || user?.profile?.real_name || user?.real_name || user?.name || void 0;
|
|
66
|
+
}
|
|
67
|
+
async function fetchChannelName(id) {
|
|
68
|
+
const name = (await client.conversations.info({ channel: id })).channel?.name;
|
|
69
|
+
return name ? `#${name}` : void 0;
|
|
70
|
+
}
|
|
71
|
+
function loadUsergroups() {
|
|
72
|
+
if (usergroupsLoaded || usergroupsLoading) return;
|
|
73
|
+
usergroupsLoading = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const res = await client.usergroups.list({});
|
|
76
|
+
for (const group of res.usergroups ?? []) {
|
|
77
|
+
if (!group.id) continue;
|
|
78
|
+
const name = group.handle ? `@${group.handle}` : group.name ?? void 0;
|
|
79
|
+
cache.set(cacheKey("usergroup", group.id), name ?? null);
|
|
80
|
+
}
|
|
81
|
+
} catch {} finally {
|
|
82
|
+
usergroupsLoaded = true;
|
|
83
|
+
usergroupsLoading = void 0;
|
|
84
|
+
notify();
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
}
|
|
88
|
+
function lookupUsergroup(id) {
|
|
89
|
+
const key = cacheKey("usergroup", id);
|
|
90
|
+
if (cache.has(key)) return cache.get(key) ?? void 0;
|
|
91
|
+
loadUsergroups();
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
user: (id) => lookup("user", id),
|
|
95
|
+
channel: (id) => lookup("channel", id),
|
|
96
|
+
usergroup: (id) => lookupUsergroup(id),
|
|
97
|
+
subscribe(listener) {
|
|
98
|
+
listeners.add(listener);
|
|
99
|
+
return () => listeners.delete(listener);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/web-api/useWebApiResolvers.ts
|
|
105
|
+
/**
|
|
106
|
+
* `createWebApiResolvers`, wired up to re-render the calling component whenever a lookup
|
|
107
|
+
* completes. Pass the returned resolvers straight to `<BlockKitProvider resolvers={...}>`.
|
|
108
|
+
*
|
|
109
|
+
* `opts` is only read when `client` (identity) changes, matching `createWebApiResolvers` being
|
|
110
|
+
* called once per client rather than on every render.
|
|
111
|
+
*/
|
|
112
|
+
function useWebApiResolvers(client, opts) {
|
|
113
|
+
const resolvers = useMemo(() => createWebApiResolvers(client, opts), [client]);
|
|
114
|
+
const [, forceUpdate] = useState(0);
|
|
115
|
+
useEffect(() => resolvers.subscribe(() => forceUpdate((v) => v + 1)), [resolvers]);
|
|
116
|
+
return resolvers;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { createWebApiResolvers, useWebApiResolvers };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=web-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-api.js","names":[],"sources":["../src/web-api/resolvers.ts","../src/web-api/useWebApiResolvers.ts"],"sourcesContent":["import type { WebClient } from \"@slack/web-api\";\nimport type { Resolvers } from \"../index\";\n\nexport interface WebApiResolversOptions {\n /**\n * Milliseconds to coalesce `user`/`channel` lookups requested in quick succession (e.g. every\n * mention in a long message) before firing the batch of requests concurrently. Default 20.\n */\n batchWindowMs?: number;\n}\n\nexport interface WebApiResolvers extends Resolvers {\n /**\n * Registers a listener called whenever a previously-unresolved id finishes looking up (whether\n * it resolved to a name or turned out to be unknown). Returns an unsubscribe function. Since the\n * resolver functions themselves are synchronous, a consumer re-renders by subscribing and\n * forcing an update — see `useWebApiResolvers`.\n */\n subscribe(listener: () => void): () => void;\n}\n\ntype Kind = \"user\" | \"channel\" | \"usergroup\";\n\n/**\n * Builds `Resolvers` (see `context.tsx`) backed by the real Slack Web API\n * or an emulator's implementation of it: `users.info`, `conversations.info`, and `usergroups.list`\n * for usergroups (there is no per-id usergroup lookup in the Web API, so the whole list is fetched\n * once and cached).\n *\n * The resolver functions are synchronous — `(id) => string | undefined` — so a lookup that hasn't\n * completed yet returns `undefined` immediately and schedules a background fetch; call `subscribe`\n * (or the `useWebApiResolvers` hook) to be notified when it completes so the tree can re-render\n * with the resolved name.\n */\nexport function createWebApiResolvers(\n client: WebClient,\n opts: WebApiResolversOptions = {},\n): WebApiResolvers {\n const batchWindowMs = opts.batchWindowMs ?? 20;\n\n // `undefined` = not yet looked up, `null` = looked up and unknown/failed, `string` = resolved.\n const cache = new Map<string, string | null>();\n const inflight = new Set<string>();\n const queues = new Map<Kind, Set<string>>([\n [\"user\", new Set()],\n [\"channel\", new Set()],\n ]);\n const timers = new Map<Kind, ReturnType<typeof setTimeout>>();\n const listeners = new Set<() => void>();\n\n let usergroupsLoaded = false;\n let usergroupsLoading: Promise<void> | undefined;\n\n function notify() {\n for (const listener of listeners) listener();\n }\n\n function cacheKey(kind: Kind, id: string) {\n return `${kind}:${id}`;\n }\n\n function lookup(kind: \"user\" | \"channel\", id: string): string | undefined {\n const key = cacheKey(kind, id);\n if (cache.has(key)) return cache.get(key) ?? undefined;\n schedule(kind, id);\n return undefined;\n }\n\n function schedule(kind: \"user\" | \"channel\", id: string) {\n const key = cacheKey(kind, id);\n if (inflight.has(key)) return;\n inflight.add(key);\n const queue = queues.get(kind);\n queue?.add(id);\n if (!timers.has(kind)) {\n timers.set(\n kind,\n setTimeout(() => {\n timers.delete(kind);\n void flush(kind);\n }, batchWindowMs),\n );\n }\n }\n\n async function flush(kind: \"user\" | \"channel\") {\n const queue = queues.get(kind);\n const ids = queue ? [...queue] : [];\n queue?.clear();\n // Fired concurrently (the Web API has no bulk users.info/conversations.info), rather than\n // waiting on each lookup one at a time.\n await Promise.all(ids.map((id) => fetchOne(kind, id)));\n }\n\n async function fetchOne(kind: \"user\" | \"channel\", id: string) {\n const key = cacheKey(kind, id);\n try {\n const name = kind === \"user\" ? await fetchUserName(id) : await fetchChannelName(id);\n cache.set(key, name ?? null);\n } catch {\n // Unknown id, revoked token, rate limit, or an emulator that doesn't implement the method:\n // treat it the same as \"unknown\" rather than throwing out of a render-time resolver.\n cache.set(key, null);\n } finally {\n inflight.delete(key);\n notify();\n }\n }\n\n async function fetchUserName(id: string): Promise<string | undefined> {\n const res = await client.users.info({ user: id });\n const user = res.user;\n return (\n user?.profile?.display_name ||\n user?.profile?.real_name ||\n user?.real_name ||\n user?.name ||\n undefined\n );\n }\n\n async function fetchChannelName(id: string): Promise<string | undefined> {\n const res = await client.conversations.info({ channel: id });\n const name = res.channel?.name;\n return name ? `#${name}` : undefined;\n }\n\n function loadUsergroups(): void {\n if (usergroupsLoaded || usergroupsLoading) return;\n usergroupsLoading = (async () => {\n try {\n const res = await client.usergroups.list({});\n for (const group of res.usergroups ?? []) {\n if (!group.id) continue;\n const name = group.handle ? `@${group.handle}` : (group.name ?? undefined);\n cache.set(cacheKey(\"usergroup\", group.id), name ?? null);\n }\n } catch {\n // Several emulators (and workspaces without Enterprise Grid) don't implement\n // usergroups.list; leave usergroup ids unresolved rather than failing the whole surface.\n } finally {\n usergroupsLoaded = true;\n usergroupsLoading = undefined;\n notify();\n }\n })();\n }\n\n function lookupUsergroup(id: string): string | undefined {\n const key = cacheKey(\"usergroup\", id);\n if (cache.has(key)) return cache.get(key) ?? undefined;\n loadUsergroups();\n return undefined;\n }\n\n return {\n user: (id) => lookup(\"user\", id),\n channel: (id) => lookup(\"channel\", id),\n usergroup: (id) => lookupUsergroup(id),\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n","import type { WebClient } from \"@slack/web-api\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n createWebApiResolvers,\n type WebApiResolvers,\n type WebApiResolversOptions,\n} from \"./resolvers\";\n\n/**\n * `createWebApiResolvers`, wired up to re-render the calling component whenever a lookup\n * completes. Pass the returned resolvers straight to `<BlockKitProvider resolvers={...}>`.\n *\n * `opts` is only read when `client` (identity) changes, matching `createWebApiResolvers` being\n * called once per client rather than on every render.\n */\nexport function useWebApiResolvers(\n client: WebClient,\n opts?: WebApiResolversOptions,\n): WebApiResolvers {\n // `opts` is a snapshot taken at creation time, like `client`: intentionally excluded below.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n const resolvers = useMemo(() => createWebApiResolvers(client, opts), [client]);\n const [, forceUpdate] = useState(0);\n\n useEffect(() => resolvers.subscribe(() => forceUpdate((v) => v + 1)), [resolvers]);\n\n return resolvers;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkootstra/block-kit",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Render Slack Block Kit JSON in React, pixel-for-pixel the way Slack does.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"block-kit",
|
|
7
|
+
"blocks",
|
|
8
|
+
"mrkdwn",
|
|
9
|
+
"react",
|
|
10
|
+
"renderer",
|
|
11
|
+
"slack"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://block-kit.kootstra.io",
|
|
14
|
+
"bugs": "https://github.com/nkootstra/block-kit/issues",
|
|
5
15
|
"license": "MIT",
|
|
16
|
+
"author": "Niels Kootstra",
|
|
6
17
|
"repository": {
|
|
7
18
|
"type": "git",
|
|
8
19
|
"url": "git+https://github.com/nkootstra/block-kit.git",
|
|
9
20
|
"directory": "packages/block-kit"
|
|
10
21
|
},
|
|
11
|
-
"files": [
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": [
|
|
27
|
+
"*.css"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.js",
|
|
31
|
+
"./mrkdwn": "./dist/mrkdwn.js",
|
|
32
|
+
"./transport": "./dist/transport.js",
|
|
33
|
+
"./server": "./dist/server.js",
|
|
34
|
+
"./web-api": "./dist/web-api.js",
|
|
35
|
+
"./styles.css": "./dist/styles.css",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"dev": "tsdown --watch",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"type-check": "tsc --noEmit",
|
|
46
|
+
"prepublishOnly": "tsdown"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@slack/types": "3.1.0",
|
|
50
|
+
"mdast-util-from-markdown": "^2.0.3",
|
|
51
|
+
"mdast-util-gfm": "^3.1.0",
|
|
52
|
+
"micromark-extension-gfm": "^3.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@slack/bolt": "^5.1.0",
|
|
56
|
+
"@slack/web-api": "^8.1.1",
|
|
57
|
+
"@testing-library/react": "16.3.3",
|
|
58
|
+
"@types/mdast": "^4.0.4",
|
|
59
|
+
"@types/react": "19.3.0",
|
|
60
|
+
"@types/react-dom": "19.3.0",
|
|
61
|
+
"jsdom": "30.1.1",
|
|
62
|
+
"tsdown": "0.23.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@slack/web-api": ">=8.0.0",
|
|
66
|
+
"react": "^19.0.0",
|
|
67
|
+
"react-dom": "^19.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@slack/web-api": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
12
74
|
}
|