@tightknitai/block-kitchen 0.9.2 → 0.9.4

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/dist/index.cjs CHANGED
@@ -1917,6 +1917,10 @@ function sanitizeImageSrc(value) {
1917
1917
  // src/lib/sanitize-blocks.ts
1918
1918
  var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
1919
1919
  var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
1920
+ var RETRIEVAL_ONLY_KEYS = /* @__PURE__ */ new Map([
1921
+ // Covers both the image block and the image element — both `type: 'image'`.
1922
+ ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])]
1923
+ ]);
1920
1924
  function sanitizeValue(value) {
1921
1925
  if (value === null || typeof value !== "object") {
1922
1926
  return value;
@@ -1934,8 +1938,14 @@ function sanitizeValue(value) {
1934
1938
  return changed ? out : value;
1935
1939
  }
1936
1940
  const src = value;
1941
+ const dropKeys = typeof src.type === "string" ? RETRIEVAL_ONLY_KEYS.get(src.type) : void 0;
1937
1942
  let copy = null;
1938
1943
  for (const key of Object.keys(src)) {
1944
+ if (dropKeys?.has(key)) {
1945
+ copy ??= { ...src };
1946
+ delete copy[key];
1947
+ continue;
1948
+ }
1939
1949
  const original = src[key];
1940
1950
  let next = original;
1941
1951
  if (typeof original === "string") {
@@ -1959,6 +1969,18 @@ function sanitizeValue(value) {
1959
1969
  function sanitizeBlock(block) {
1960
1970
  return sanitizeValue(block);
1961
1971
  }
1972
+ function sanitizeBlocks(blocks) {
1973
+ let changed = false;
1974
+ const out = new Array(blocks.length);
1975
+ for (let i = 0; i < blocks.length; i++) {
1976
+ const next = sanitizeBlock(blocks[i]);
1977
+ if (next !== blocks[i]) {
1978
+ changed = true;
1979
+ }
1980
+ out[i] = next;
1981
+ }
1982
+ return changed ? out : blocks;
1983
+ }
1962
1984
 
1963
1985
  // src/lib/to-slack-blocks.ts
1964
1986
  function toSlackBlocks(blocks) {
@@ -2216,7 +2238,7 @@ function useBlockKitchenState({
2216
2238
  });
2217
2239
  }, []);
2218
2240
  const replaceAll = React.useCallback((newBlocks) => {
2219
- setBlocks(newBlocks.map(wrap));
2241
+ setBlocks(sanitizeBlocks(newBlocks).map(wrap));
2220
2242
  }, []);
2221
2243
  return {
2222
2244
  blocks,
@@ -2625,8 +2647,10 @@ function JsonDrawer({
2625
2647
  const [value, setValue] = React.useState("");
2626
2648
  const [parseError, setParseError] = React.useState(null);
2627
2649
  const [validationErrors, setValidationErrors] = React.useState([]);
2650
+ const [copied, setCopied] = React.useState(false);
2628
2651
  const textareaRef = React.useRef(null);
2629
2652
  const gutterRef = React.useRef(null);
2653
+ const copyResetRef = React.useRef(null);
2630
2654
  const blocksRef = React.useRef(blocks);
2631
2655
  React.useEffect(() => {
2632
2656
  blocksRef.current = blocks;
@@ -2636,8 +2660,27 @@ function JsonDrawer({
2636
2660
  setValue(JSON.stringify(blocksRef.current, null, 2));
2637
2661
  setParseError(null);
2638
2662
  setValidationErrors([]);
2663
+ setCopied(false);
2639
2664
  }
2640
2665
  }, [open]);
2666
+ React.useEffect(
2667
+ () => () => {
2668
+ if (copyResetRef.current) {
2669
+ clearTimeout(copyResetRef.current);
2670
+ }
2671
+ },
2672
+ []
2673
+ );
2674
+ const handleCopy = () => {
2675
+ navigator.clipboard?.writeText(value).then(() => {
2676
+ setCopied(true);
2677
+ if (copyResetRef.current) {
2678
+ clearTimeout(copyResetRef.current);
2679
+ }
2680
+ copyResetRef.current = setTimeout(() => setCopied(false), 2e3);
2681
+ }).catch(() => {
2682
+ });
2683
+ };
2641
2684
  const handleChange = (next) => {
2642
2685
  setValue(next);
2643
2686
  if (next.length > MAX_JSON_BYTES) {
@@ -2684,7 +2727,17 @@ function JsonDrawer({
2684
2727
  /* @__PURE__ */ jsxRuntime.jsx(SheetTitle, { children: "Block Kit JSON" }),
2685
2728
  /* @__PURE__ */ jsxRuntime.jsx(SheetDescription, { children: "Edits update the preview as you type. Parse errors show below." })
2686
2729
  ] }),
2687
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 overflow-hidden rounded-md border border-input bg-muted/30 shadow-sm focus-within:ring-1 focus-within:ring-ring", children: [
2730
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-1 overflow-hidden rounded-md border border-input bg-muted/30 shadow-sm focus-within:ring-1 focus-within:ring-ring", children: [
2731
+ /* @__PURE__ */ jsxRuntime.jsx(
2732
+ "button",
2733
+ {
2734
+ type: "button",
2735
+ onClick: handleCopy,
2736
+ "aria-label": copied ? "Copied to clipboard" : "Copy JSON",
2737
+ className: "absolute right-2 top-2 z-10 inline-flex items-center justify-center rounded-md border border-input bg-background/80 p-1.5 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
2738
+ children: copied ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5 text-emerald-600" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3.5 w-3.5" })
2739
+ }
2740
+ ),
2688
2741
  /* @__PURE__ */ jsxRuntime.jsx(
2689
2742
  "div",
2690
2743
  {
@@ -2833,6 +2886,79 @@ var Label = React__namespace.forwardRef(({ className, ...props }, ref) => /* @__
2833
2886
  }
2834
2887
  ));
2835
2888
  Label.displayName = LabelPrimitive__namespace.Root.displayName;
2889
+ var POLL_INTERVAL_MS = 2500;
2890
+ var MAX_POLLS = 24;
2891
+ function useSlackSignIn(loadStatus, { open, enabled }) {
2892
+ const [userStatus, setUserStatus] = React.useState(null);
2893
+ const [polling, setPolling] = React.useState(false);
2894
+ const loadStatusRef = React.useRef(loadStatus);
2895
+ React.useEffect(() => {
2896
+ loadStatusRef.current = loadStatus;
2897
+ });
2898
+ const refresh = React.useCallback(() => {
2899
+ loadStatusRef.current().then(setUserStatus).catch(() => setUserStatus({ canSendAsUser: false }));
2900
+ }, []);
2901
+ const pollRef = React.useRef(null);
2902
+ const stopPolling = React.useCallback(() => {
2903
+ if (pollRef.current) {
2904
+ clearInterval(pollRef.current);
2905
+ pollRef.current = null;
2906
+ }
2907
+ setPolling(false);
2908
+ }, []);
2909
+ const startSignIn = React.useCallback(() => {
2910
+ const url = userStatus?.oauthUrl;
2911
+ if (!url || !isSafeHref(url)) {
2912
+ return;
2913
+ }
2914
+ window.open(url, "_blank", "noopener,noreferrer");
2915
+ stopPolling();
2916
+ setPolling(true);
2917
+ let tries = 0;
2918
+ pollRef.current = setInterval(() => {
2919
+ tries += 1;
2920
+ loadStatusRef.current().then((next) => {
2921
+ setUserStatus(next);
2922
+ if (next.canSendAsUser) {
2923
+ stopPolling();
2924
+ }
2925
+ }).catch(() => {
2926
+ }).finally(() => {
2927
+ if (tries >= MAX_POLLS) {
2928
+ stopPolling();
2929
+ }
2930
+ });
2931
+ }, POLL_INTERVAL_MS);
2932
+ }, [userStatus, stopPolling]);
2933
+ React.useEffect(() => {
2934
+ if (!open || !enabled) {
2935
+ setUserStatus(null);
2936
+ stopPolling();
2937
+ return;
2938
+ }
2939
+ setUserStatus(null);
2940
+ refresh();
2941
+ return stopPolling;
2942
+ }, [open, enabled, refresh, stopPolling]);
2943
+ React.useEffect(() => {
2944
+ if (!open || !enabled) {
2945
+ return;
2946
+ }
2947
+ const handler = () => refresh();
2948
+ window.addEventListener("focus", handler);
2949
+ return () => window.removeEventListener("focus", handler);
2950
+ }, [open, enabled, refresh]);
2951
+ return { userStatus, polling, startSignIn };
2952
+ }
2953
+ function SlackSignInButton({ onClick, polling }) {
2954
+ return /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", variant: "outline", size: "sm", className: "w-fit", onClick, disabled: polling, children: polling ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2955
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "animate-spin" }),
2956
+ " Waiting for Slack\u2026"
2957
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2958
+ "Sign in with Slack ",
2959
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, {})
2960
+ ] }) });
2961
+ }
2836
2962
  function recentToResult(msg) {
2837
2963
  return {
2838
2964
  ok: true,
@@ -2846,6 +2972,17 @@ function recentToResult(msg) {
2846
2972
  iconUrl: msg.iconUrl
2847
2973
  };
2848
2974
  }
2975
+ function previewText(m) {
2976
+ if (m.label) {
2977
+ return m.label;
2978
+ }
2979
+ for (const b of m.blocks) {
2980
+ if ((b.type === "header" || b.type === "section") && "text" in b && b.text && "text" in b.text) {
2981
+ return b.text.text;
2982
+ }
2983
+ }
2984
+ return `${m.blocks.length} block${m.blocks.length === 1 ? "" : "s"}`;
2985
+ }
2849
2986
  function LoadMessageDialog({
2850
2987
  open,
2851
2988
  onOpenChange,
@@ -2862,7 +2999,10 @@ function LoadMessageDialog({
2862
2999
  const [channelId, setChannelId] = React.useState("");
2863
3000
  const [recent, setRecent] = React.useState(null);
2864
3001
  const [recentError, setRecentError] = React.useState(null);
3002
+ const [selectedRecent, setSelectedRecent] = React.useState(null);
3003
+ const [signInPolling, setSignInPolling] = React.useState(false);
2865
3004
  const inputRef = React.useRef(null);
3005
+ const signInPollRef = React.useRef(null);
2866
3006
  const onLoadMessageRef = React.useRef(onLoadMessage);
2867
3007
  const loadRecentMessagesRef = React.useRef(loadRecentMessages);
2868
3008
  const loadChannelsRef = React.useRef(loadChannels);
@@ -2881,6 +3021,7 @@ function LoadMessageDialog({
2881
3021
  setChannelId("");
2882
3022
  setRecent(null);
2883
3023
  setRecentError(null);
3024
+ setSelectedRecent(null);
2884
3025
  if (!loadRecentMessagesRef.current) {
2885
3026
  setChannels([]);
2886
3027
  setChannelsError(null);
@@ -2908,6 +3049,7 @@ function LoadMessageDialog({
2908
3049
  }
2909
3050
  setRecent(null);
2910
3051
  setRecentError(null);
3052
+ setSelectedRecent(null);
2911
3053
  let cancelled = false;
2912
3054
  loadRecentMessagesRef.current(channelId).then((list) => {
2913
3055
  if (!cancelled) {
@@ -2922,7 +3064,25 @@ function LoadMessageDialog({
2922
3064
  cancelled = true;
2923
3065
  };
2924
3066
  }, [open, channelId]);
3067
+ const stopSignInPoll = React.useCallback(() => {
3068
+ if (signInPollRef.current) {
3069
+ clearInterval(signInPollRef.current);
3070
+ signInPollRef.current = null;
3071
+ }
3072
+ setSignInPolling(false);
3073
+ }, []);
3074
+ React.useEffect(() => {
3075
+ if (!open) {
3076
+ stopSignInPoll();
3077
+ }
3078
+ return stopSignInPoll;
3079
+ }, [open, stopSignInPoll]);
2925
3080
  const handleLoad = async () => {
3081
+ stopSignInPoll();
3082
+ if (selectedRecent) {
3083
+ onLoaded(recentToResult(selectedRecent));
3084
+ return;
3085
+ }
2926
3086
  const trimmed = link.trim();
2927
3087
  if (!trimmed) {
2928
3088
  setStatus({ kind: "error", error: "Paste a Slack message link first." });
@@ -2935,12 +3095,42 @@ function LoadMessageDialog({
2935
3095
  onLoaded(result);
2936
3096
  return;
2937
3097
  }
2938
- setStatus({ kind: "not-editable", reason: result.reason, blocks: result.blocks });
3098
+ setStatus({ kind: "not-editable", reason: result.reason, blocks: result.blocks, oauthUrl: result.oauthUrl });
2939
3099
  } catch (e) {
2940
3100
  setStatus({ kind: "error", error: e instanceof Error ? e.message : "Failed to load message." });
2941
3101
  }
2942
3102
  };
3103
+ const startFindSignIn = (oauthUrl) => {
3104
+ if (!isSafeHref(oauthUrl)) {
3105
+ return;
3106
+ }
3107
+ const trimmed = link.trim();
3108
+ if (!trimmed) {
3109
+ return;
3110
+ }
3111
+ window.open(oauthUrl, "_blank", "noopener,noreferrer");
3112
+ stopSignInPoll();
3113
+ setSignInPolling(true);
3114
+ let tries = 0;
3115
+ const POLL_INTERVAL_MS2 = 2500;
3116
+ const MAX_POLLS2 = 24;
3117
+ signInPollRef.current = setInterval(() => {
3118
+ tries += 1;
3119
+ onLoadMessageRef.current({ link: trimmed }).then((result) => {
3120
+ if (result.ok) {
3121
+ stopSignInPoll();
3122
+ onLoaded(result);
3123
+ }
3124
+ }).catch(() => {
3125
+ }).finally(() => {
3126
+ if (tries >= MAX_POLLS2) {
3127
+ stopSignInPoll();
3128
+ }
3129
+ });
3130
+ }, POLL_INTERVAL_MS2);
3131
+ };
2943
3132
  const notEditable = status.kind === "not-editable" ? status : null;
3133
+ const signInUrl = notEditable?.oauthUrl && isSafeHref(notEditable.oauthUrl) ? notEditable.oauthUrl : null;
2944
3134
  return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
2945
3135
  DialogContent,
2946
3136
  {
@@ -2951,7 +3141,7 @@ function LoadMessageDialog({
2951
3141
  },
2952
3142
  children: [
2953
3143
  /* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { children: [
2954
- /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: "Edit an existing message" }),
3144
+ /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: "Find an existing message" }),
2955
3145
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5 text-left", children: [
2956
3146
  /* @__PURE__ */ jsxRuntime.jsx(DialogDescription, { children: hasRecent ? "Paste a Slack message link, or pick a recent message your app posted." : `Paste a Slack message link (Slack's "Copy link") to load its blocks for editing.` }),
2957
3147
  /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
@@ -2985,6 +3175,8 @@ function LoadMessageDialog({
2985
3175
  value: link,
2986
3176
  onChange: (e) => {
2987
3177
  setLink(e.target.value);
3178
+ setSelectedRecent(null);
3179
+ stopSignInPoll();
2988
3180
  if (status.kind !== "idle" && status.kind !== "loading") {
2989
3181
  setStatus({ kind: "idle" });
2990
3182
  }
@@ -3007,7 +3199,8 @@ function LoadMessageDialog({
3007
3199
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
3008
3200
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: notEditable.reason })
3009
3201
  ] }),
3010
- /* @__PURE__ */ jsxRuntime.jsx(
3202
+ signInUrl && /* @__PURE__ */ jsxRuntime.jsx(SlackSignInButton, { onClick: () => startFindSignIn(signInUrl), polling: signInPolling }),
3203
+ notEditable.blocks && notEditable.blocks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3011
3204
  Button,
3012
3205
  {
3013
3206
  type: "button",
@@ -3053,26 +3246,52 @@ function LoadMessageDialog({
3053
3246
  channelId && recent && recent.length === 0 && !recentError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "No recent messages from this app in this channel." }),
3054
3247
  channelId && recent && recent.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex max-h-48 min-w-0 flex-col gap-1 overflow-y-auto", children: recent.map((m) => {
3055
3248
  const asUser = (m.editableVia ?? "bot") === "user";
3249
+ const authorName = m.username || (asUser ? "You" : "App bot");
3250
+ const safeIcon = isSafeImageSrc(m.iconUrl) ? m.iconUrl : void 0;
3251
+ const isSelected = selectedRecent?.channelId === m.channelId && selectedRecent?.ts === m.ts;
3056
3252
  return /* @__PURE__ */ jsxRuntime.jsxs(
3057
3253
  "button",
3058
3254
  {
3059
3255
  type: "button",
3060
- onClick: () => onLoaded(recentToResult(m)),
3061
- className: "flex min-w-0 flex-col gap-0.5 rounded-md border border-input bg-background px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3256
+ "aria-pressed": isSelected,
3257
+ onClick: () => {
3258
+ setSelectedRecent(m);
3259
+ setLink("");
3260
+ if (status.kind !== "idle" && status.kind !== "loading") {
3261
+ setStatus({ kind: "idle" });
3262
+ }
3263
+ },
3264
+ className: cn(
3265
+ // Inset rings so the scroll container's overflow clip
3266
+ // can't shave the corners off the selected outline.
3267
+ "flex min-w-0 items-start gap-2.5 rounded-md border border-input bg-background px-3 py-2 text-left hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
3268
+ isSelected && "border-primary! bg-accent ring-1 ring-inset ring-primary"
3269
+ ),
3062
3270
  children: [
3063
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-baseline justify-between gap-2", children: [
3064
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 rounded border px-1 py-px text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: asUser ? "You" : "Bot" }),
3065
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "shrink-0 text-xs text-muted-foreground", children: [
3066
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: new Date(Number(m.ts) * 1e3).toLocaleString() }),
3271
+ safeIcon ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: safeIcon, alt: "", className: "h-9 w-9 shrink-0 rounded-md object-cover" }) : /* @__PURE__ */ jsxRuntime.jsx(
3272
+ "span",
3273
+ {
3274
+ "aria-hidden": true,
3275
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-sm font-semibold text-muted-foreground",
3276
+ children: authorName.charAt(0).toUpperCase()
3277
+ }
3278
+ ),
3279
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
3280
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
3281
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: authorName }),
3282
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 rounded border px-1 py-px text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: asUser ? "You" : "Bot" })
3283
+ ] }),
3284
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
3285
+ new Date(Number(m.ts) * 1e3).toLocaleString(),
3067
3286
  " ",
3068
3287
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono", children: [
3069
3288
  "(",
3070
3289
  m.ts,
3071
3290
  ")"
3072
3291
  ] })
3073
- ] })
3074
- ] }),
3075
- m.label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-sm text-muted-foreground", children: m.label })
3292
+ ] }),
3293
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "line-clamp-2 text-sm text-foreground", children: previewText(m) })
3294
+ ] })
3076
3295
  ]
3077
3296
  },
3078
3297
  `${m.channelId}:${m.ts}`
@@ -3082,7 +3301,15 @@ function LoadMessageDialog({
3082
3301
  ] }),
3083
3302
  /* @__PURE__ */ jsxRuntime.jsxs(DialogFooter, { children: [
3084
3303
  /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }),
3085
- /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", onClick: handleLoad, disabled: status.kind === "loading" || !link.trim(), children: status.kind === "loading" ? "Loading\u2026" : "Load message" })
3304
+ /* @__PURE__ */ jsxRuntime.jsx(
3305
+ Button,
3306
+ {
3307
+ type: "button",
3308
+ onClick: handleLoad,
3309
+ disabled: status.kind === "loading" || !selectedRecent && !link.trim(),
3310
+ children: status.kind === "loading" ? "Loading\u2026" : "Find message"
3311
+ }
3312
+ )
3086
3313
  ] })
3087
3314
  ]
3088
3315
  }
@@ -3301,18 +3528,13 @@ function SendDialog({
3301
3528
  const [channels, setChannels] = React.useState(null);
3302
3529
  const [channelsError, setChannelsError] = React.useState(null);
3303
3530
  const [channelId, setChannelId] = React.useState("");
3304
- const [userStatus, setUserStatus] = React.useState(null);
3305
3531
  const [sendAs, setSendAs] = React.useState("bot");
3306
3532
  const [status, setStatus] = React.useState({ kind: "idle" });
3533
+ const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: true });
3307
3534
  const loadChannelsRef = React.useRef(loadChannels);
3308
- const loadSendAsUserStatusRef = React.useRef(loadSendAsUserStatus);
3309
3535
  React.useEffect(() => {
3310
3536
  loadChannelsRef.current = loadChannels;
3311
- loadSendAsUserStatusRef.current = loadSendAsUserStatus;
3312
3537
  });
3313
- const refreshSendAsUser = React.useCallback(() => {
3314
- loadSendAsUserStatusRef.current().then(setUserStatus).catch(() => setUserStatus({ canSendAsUser: false }));
3315
- }, []);
3316
3538
  React.useEffect(() => {
3317
3539
  if (!open) {
3318
3540
  return;
@@ -3320,7 +3542,6 @@ function SendDialog({
3320
3542
  setStatus({ kind: "idle" });
3321
3543
  setChannels(null);
3322
3544
  setChannelsError(null);
3323
- setUserStatus(null);
3324
3545
  setChannelId("");
3325
3546
  setSendAs("bot");
3326
3547
  let cancelled = false;
@@ -3336,19 +3557,10 @@ function SendDialog({
3336
3557
  }
3337
3558
  setChannelsError(e instanceof Error ? e.message : "Failed to load channels");
3338
3559
  });
3339
- refreshSendAsUser();
3340
3560
  return () => {
3341
3561
  cancelled = true;
3342
3562
  };
3343
- }, [open, refreshSendAsUser]);
3344
- React.useEffect(() => {
3345
- if (!open) {
3346
- return;
3347
- }
3348
- const handler = () => refreshSendAsUser();
3349
- window.addEventListener("focus", handler);
3350
- return () => window.removeEventListener("focus", handler);
3351
- }, [open, refreshSendAsUser]);
3563
+ }, [open]);
3352
3564
  const handleSubmit = async () => {
3353
3565
  if (!channelId) {
3354
3566
  setStatus({ kind: "error", error: "Please pick a channel." });
@@ -3402,22 +3614,9 @@ function SendDialog({
3402
3614
  ]
3403
3615
  }
3404
3616
  ),
3405
- userStatus && !userStatus.canSendAsUser && userStatus.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
3406
- /* @__PURE__ */ jsxRuntime.jsxs(
3407
- "a",
3408
- {
3409
- href: userStatus.oauthUrl,
3410
- target: "_blank",
3411
- rel: "noopener noreferrer",
3412
- className: "inline-flex items-center gap-1 text-primary hover:underline",
3413
- children: [
3414
- "Sign in with Slack ",
3415
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3" })
3416
- ]
3417
- }
3418
- ),
3419
- " ",
3420
- "to post as yourself."
3617
+ userStatus && !userStatus.canSendAsUser && userStatus.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3618
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "Connect your Slack account to post as yourself." }),
3619
+ /* @__PURE__ */ jsxRuntime.jsx(SlackSignInButton, { onClick: startSignIn, polling })
3421
3620
  ] })
3422
3621
  ] }),
3423
3622
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5", children: [
@@ -9191,7 +9390,7 @@ function Toolbar2({
9191
9390
  onOpenLoad,
9192
9391
  onOpenUpdate,
9193
9392
  onExitEdit,
9194
- loadButtonLabel = "Load message",
9393
+ loadButtonLabel = "Find message",
9195
9394
  updateButtonLabel = "Review & update"
9196
9395
  }) {
9197
9396
  const activeTheme = THEME_OPTIONS.find((t) => t.value === previewTheme) ?? THEME_OPTIONS[0];
@@ -9475,33 +9674,13 @@ function UpdateDialog({
9475
9674
  onShowIssues
9476
9675
  }) {
9477
9676
  const asUser = target.editableVia === "user";
9478
- const [userStatus, setUserStatus] = React.useState(null);
9479
9677
  const [status, setStatus] = React.useState({ kind: "idle" });
9480
- const loadSendAsUserStatusRef = React.useRef(loadSendAsUserStatus);
9678
+ const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: asUser });
9481
9679
  React.useEffect(() => {
9482
- loadSendAsUserStatusRef.current = loadSendAsUserStatus;
9483
- });
9484
- const refreshSendAsUser = React.useCallback(() => {
9485
- loadSendAsUserStatusRef.current().then(setUserStatus).catch(() => setUserStatus({ canSendAsUser: false }));
9486
- }, []);
9487
- React.useEffect(() => {
9488
- if (!open) {
9489
- return;
9490
- }
9491
- setStatus({ kind: "idle" });
9492
- if (asUser) {
9493
- setUserStatus(null);
9494
- refreshSendAsUser();
9495
- }
9496
- }, [open, asUser, refreshSendAsUser]);
9497
- React.useEffect(() => {
9498
- if (!open || !asUser) {
9499
- return;
9680
+ if (open) {
9681
+ setStatus({ kind: "idle" });
9500
9682
  }
9501
- const handler = () => refreshSendAsUser();
9502
- window.addEventListener("focus", handler);
9503
- return () => window.removeEventListener("focus", handler);
9504
- }, [open, asUser, refreshSendAsUser]);
9683
+ }, [open]);
9505
9684
  const needsSignIn = asUser && userStatus !== null && !userStatus.canSendAsUser;
9506
9685
  const handleSubmit = async () => {
9507
9686
  setStatus({ kind: "updating" });
@@ -9536,22 +9715,9 @@ function UpdateDialog({
9536
9715
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5", children: [
9537
9716
  /* @__PURE__ */ jsxRuntime.jsx(Label, { children: "Posting as" }),
9538
9717
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex h-9 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground", children: asUser ? "Your account" : "App bot" }),
9539
- needsSignIn && userStatus?.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
9540
- /* @__PURE__ */ jsxRuntime.jsxs(
9541
- "a",
9542
- {
9543
- href: userStatus.oauthUrl,
9544
- target: "_blank",
9545
- rel: "noopener noreferrer",
9546
- className: "inline-flex items-center gap-1 text-primary hover:underline",
9547
- children: [
9548
- "Sign in with Slack ",
9549
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3" })
9550
- ]
9551
- }
9552
- ),
9553
- " ",
9554
- "to update your own message."
9718
+ needsSignIn && userStatus?.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9719
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "Connect your Slack account to edit your own messages." }),
9720
+ /* @__PURE__ */ jsxRuntime.jsx(SlackSignInButton, { onClick: startSignIn, polling })
9555
9721
  ] }),
9556
9722
  needsSignIn && !userStatus?.oauthUrl && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "Sign in with Slack to update your own message." })
9557
9723
  ] }),