@mrrocket/sdk 0.14.0 → 0.15.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.
@@ -50,7 +50,7 @@ module.exports = __toCommonJS(native_exports);
50
50
  var import_react3 = require("react");
51
51
 
52
52
  // src/core.ts
53
- var SDK_VERSION = "0.14.0";
53
+ var SDK_VERSION = "0.15.0";
54
54
  var propOf = (person, name) => {
55
55
  const want = name.trim().toLowerCase();
56
56
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -173,7 +173,14 @@ var textChanges = (config, distinctId, preview) => {
173
173
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
174
174
 
175
175
  // src/api-client.ts
176
- var replyLine = (t) => t === "minutes" ? "We usually reply in a few minutes" : t === "day" ? "We usually reply within a day" : "We usually reply in a few hours";
176
+ var CHAT_FILE_MAX_BYTES = 10 * 1024 * 1024;
177
+ var CHAT_FILES_PER_MESSAGE = 4;
178
+ var CORNERS = {
179
+ round: { panel: 22, bubble: 18, tail: 6, card: 16, field: 20 },
180
+ soft: { panel: 14, bubble: 12, tail: 4, card: 10, field: 10 },
181
+ square: { panel: 6, bubble: 6, tail: 2, card: 4, field: 4 }
182
+ };
183
+ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-by";
177
184
  var inkOn = (hex) => {
178
185
  const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
179
186
  if (!m) return "#ffffff";
@@ -197,7 +204,8 @@ var call = async (url, init, signal) => {
197
204
  res = await fetch(url, {
198
205
  ...init,
199
206
  signal,
200
- headers: init?.body ? { "content-type": "application/json" } : void 0
207
+ // A form (a file upload) sets its own multipart type with the boundary.
208
+ headers: typeof init?.body === "string" ? { "content-type": "application/json" } : void 0
201
209
  });
202
210
  } catch (e) {
203
211
  if (e.name === "AbortError") throw e;
@@ -230,7 +238,19 @@ var createApi = (c) => {
230
238
  seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
231
239
  respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
232
240
  chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
241
+ /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
233
242
  sendChat: (m) => post("/messages", m),
243
+ /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
244
+ uploadChatFile: (file, signal) => {
245
+ const form = new FormData();
246
+ form.append("key", c.apiKey);
247
+ form.append("distinctId", c.distinctId);
248
+ if ("uri" in file) form.append("file", file);
249
+ else form.append("file", file, file.name || "file");
250
+ return call(`${base}/messages/attachments`, { method: "POST", body: form }, signal);
251
+ },
252
+ /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
253
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`,
234
254
  articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
235
255
  article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
236
256
  messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
@@ -343,6 +363,9 @@ var pageMatches = (pattern, path) => {
343
363
  return at === (p.replace(/\/+$/, "") || "/");
344
364
  };
345
365
  var stepsIn = (nodes) => nodes.flatMap((n) => n.kind === "condition" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === "wait" ? [] : [n]);
366
+ var monthsSince = (since, now = Date.now()) => since == null || since === "" ? NaN : Math.floor((now - new Date(since).getTime()) / 2630016e3);
367
+ var low = (x) => x?.trim().toLowerCase();
368
+ var ruleMatches = ({ when: w = {} }, c) => (w.answer == null || low(w.answer) === low(c.answer)) && (w.plan == null || [w.plan].flat().some((x) => low(x) === low(c.plan))) && !(c.months >= w.tenureUnder) && !(c.months < w.tenureOver) && (w.tenureUnder == null || c.months >= 0) && (w.tenureOver == null || c.months >= 0);
346
369
  var createFlows = (env) => {
347
370
  let config = null;
348
371
  let cancel = null;
@@ -510,18 +533,23 @@ var createFlows = (env) => {
510
533
  };
511
534
  const view = () => {
512
535
  if (cancel) {
513
- const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : void 0;
536
+ const c = cancel;
537
+ const followUp = c.flow.followUps?.[c.reason];
538
+ const f = followUp ? 1 : 0;
539
+ const steps = c.plain ? 1 : 3 + f + (c.flow.second?.[c.reason] ? 1 : 0);
514
540
  return {
515
541
  type: "cancel",
516
- screen: cancel.screen,
517
- flow: cancel.flow,
518
- reason: cancel.reason,
519
- offer: cancel.screen === "offer" ? offer : void 0,
520
- price: cancel.options.price,
521
- busy: cancel.busy,
522
- error: cancel.error,
523
- step: cancel.plain ? 1 : cancel.screen === "survey" ? 1 : cancel.screen === "offer" ? 2 : 3,
524
- steps: cancel.plain ? 1 : 3
542
+ screen: c.screen,
543
+ flow: c.flow,
544
+ reason: c.reason,
545
+ ...c.screen === "followup" && followUp ? { followUp, answer: c.answer } : {},
546
+ offer: c.screen === "offer" ? c.offer : void 0,
547
+ ...c.screen === "offer" ? { round: c.round } : {},
548
+ price: c.options.price,
549
+ busy: c.busy,
550
+ error: c.error,
551
+ step: c.screen === "survey" ? 1 : c.screen === "followup" ? 2 : c.screen === "offer" ? 1 + f + c.round : steps,
552
+ steps
525
553
  };
526
554
  }
527
555
  const ob = onboardingView();
@@ -600,12 +628,22 @@ var createFlows = (env) => {
600
628
  });
601
629
  return loading;
602
630
  };
631
+ const offerAt = (c) => c.round > 1 ? { round: 2 } : c.rule >= 0 ? { rule: c.rule } : {};
632
+ const toOffer = (c) => {
633
+ const who = { answer: c.answer, plan: c.options.plan, months: monthsSince(c.options.customerSince, env.now?.()) };
634
+ const rules = c.flow.rules?.[c.reason] ?? [];
635
+ c.rule = rules.findIndex((r) => ruleMatches(r, who));
636
+ c.offer = c.rule < 0 ? c.flow.offers[c.reason] : rules[c.rule].offer ?? void 0;
637
+ c.round = 1;
638
+ c.screen = c.offer ? "offer" : "confirm";
639
+ };
603
640
  const result = (c, outcome, comment) => {
604
- const offer = c.reason ? c.flow.offers[c.reason] : void 0;
641
+ const offer = c.screen === "offer" ? c.offer : void 0;
605
642
  return {
606
643
  outcome,
607
644
  run: c.run,
608
645
  ...c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {},
646
+ ...c.answer ? { answer: c.answer } : {},
609
647
  ...outcome === "saved" && offer ? { offer } : {},
610
648
  ...comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}
611
649
  };
@@ -618,7 +656,8 @@ var createFlows = (env) => {
618
656
  outcome: r.outcome,
619
657
  customer: c.options.customer ?? "consumer",
620
658
  ...r.reason ? { reason: r.reason } : {},
621
- ...r.offer ? { offer: r.offer.kind } : {},
659
+ ...r.answer ? { answer: r.answer } : {},
660
+ ...r.offer ? { offer: r.offer.kind, ...offerAt(c) } : {},
622
661
  ...r.comment ? { comment: r.comment } : {}
623
662
  });
624
663
  if (cancel === c) cancel = null;
@@ -648,7 +687,7 @@ var createFlows = (env) => {
648
687
  let resolve = () => {
649
688
  };
650
689
  const promise = new Promise((r) => resolve = r);
651
- const c = { flow, options, plain, screen: plain ? "confirm" : "survey", run: runId(), busy: false, resolve, promise };
690
+ const c = { flow, options, plain, screen: plain ? "confirm" : "survey", round: 1, run: runId(), busy: false, resolve, promise };
652
691
  cancel = c;
653
692
  env.track(CANCEL_EVENT, { action: "shown", run: c.run, version: flow.version, customer: options.customer ?? "consumer", plain });
654
693
  emit();
@@ -700,29 +739,43 @@ var createFlows = (env) => {
700
739
  },
701
740
  cancel: {
702
741
  pick: (reason) => {
703
- if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;
742
+ if (!cancel || cancel.busy || cancel.screen !== "survey" || !cancel.flow.reasons.some((r) => r.id === reason)) return;
743
+ if (cancel.reason !== reason) cancel.answer = void 0;
704
744
  cancel.reason = reason;
705
745
  cancel.error = void 0;
706
746
  emit();
707
747
  },
748
+ answer: (text2) => {
749
+ const c = cancel;
750
+ if (!c || c.busy || c.screen !== "followup") return;
751
+ const t = text2.slice(0, 500);
752
+ c.answer = t.trim() ? t : void 0;
753
+ emit();
754
+ },
708
755
  next: () => {
709
756
  const c = cancel;
710
- if (!c || c.screen !== "survey" || !c.reason) return;
711
- env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
712
- c.screen = c.flow.offers[c.reason] ? "offer" : "confirm";
757
+ if (!c || !c.reason) return;
758
+ if (c.screen === "survey") {
759
+ env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
760
+ if (c.flow.followUps?.[c.reason]) c.screen = "followup";
761
+ else toOffer(c);
762
+ } else if (c.screen === "followup") {
763
+ c.answer = c.answer?.trim();
764
+ toOffer(c);
765
+ } else return;
713
766
  emit();
714
767
  },
715
768
  accept: (comment) => {
716
769
  const c = cancel;
717
770
  if (!c || c.screen !== "offer" || !c.reason) return;
718
- const offer = c.flow.offers[c.reason];
719
771
  if (!c.busy)
720
772
  env.track(CANCEL_EVENT, {
721
773
  action: "offer",
722
774
  run: c.run,
723
775
  version: c.flow.version,
724
776
  reason: c.reason,
725
- offer: offer?.kind,
777
+ offer: c.offer?.kind,
778
+ ...offerAt(c),
726
779
  outcome: "accepted"
727
780
  });
728
781
  settle(c, "saved", comment);
@@ -735,10 +788,16 @@ var createFlows = (env) => {
735
788
  run: c.run,
736
789
  version: c.flow.version,
737
790
  reason: c.reason,
738
- offer: c.reason ? c.flow.offers[c.reason]?.kind : void 0,
791
+ offer: c.offer?.kind,
792
+ ...offerAt(c),
739
793
  outcome: "declined"
740
794
  });
741
- c.screen = "confirm";
795
+ const second = c.round === 1 && c.reason ? c.flow.second?.[c.reason] : void 0;
796
+ if (second) {
797
+ c.offer = second;
798
+ c.rule = void 0;
799
+ c.round = 2;
800
+ } else c.screen = "confirm";
742
801
  c.error = void 0;
743
802
  emit();
744
803
  },
@@ -1899,9 +1958,9 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
1899
1958
  card(children, { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, paddingBottom: 34 })
1900
1959
  )
1901
1960
  );
1902
- const input = (placeholder) => (0, import_react.createElement)(RN.TextInput, {
1903
- value: comment,
1904
- onChangeText: setComment,
1961
+ const input = (placeholder, value = comment, onChange = setComment) => (0, import_react.createElement)(RN.TextInput, {
1962
+ value,
1963
+ onChangeText: onChange,
1905
1964
  placeholder,
1906
1965
  placeholderTextColor: p.muted,
1907
1966
  multiline: true,
@@ -1986,38 +2045,49 @@ var MRRocketFlows = ({ flows, paywall, appearance = {} }) => {
1986
2045
  );
1987
2046
  const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
1988
2047
  let body = [];
2048
+ const choices = (items) => (0, import_react.createElement)(
2049
+ RN.ScrollView,
2050
+ { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
2051
+ ...items.map(
2052
+ (c) => (0, import_react.createElement)(
2053
+ RN.Pressable,
2054
+ {
2055
+ key: c.key,
2056
+ onPress: c.press,
2057
+ accessibilityRole: "radio",
2058
+ accessibilityState: { checked: c.on },
2059
+ style: {
2060
+ minHeight: 44,
2061
+ paddingHorizontal: 12,
2062
+ borderRadius: 10,
2063
+ borderWidth: 1,
2064
+ borderColor: c.on ? p.accent : p.line,
2065
+ backgroundColor: c.on ? p.soft : p.bg,
2066
+ justifyContent: "center"
2067
+ }
2068
+ },
2069
+ text2(c.label, { fontWeight: "500" })
2070
+ )
2071
+ )
2072
+ );
1989
2073
  if (v.screen === "survey")
1990
2074
  body = [
1991
2075
  title(v.flow.question || "Why are you leaving?"),
1992
- (0, import_react.createElement)(
1993
- RN.ScrollView,
1994
- { style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
1995
- ...v.flow.reasons.map(
1996
- (r) => (0, import_react.createElement)(
1997
- RN.Pressable,
1998
- {
1999
- key: r.id,
2000
- onPress: () => flows.cancel.pick(r.id),
2001
- accessibilityRole: "radio",
2002
- accessibilityState: { checked: v.reason === r.id },
2003
- style: {
2004
- minHeight: 44,
2005
- paddingHorizontal: 12,
2006
- borderRadius: 10,
2007
- borderWidth: 1,
2008
- borderColor: v.reason === r.id ? p.accent : p.line,
2009
- backgroundColor: v.reason === r.id ? p.soft : p.bg,
2010
- justifyContent: "center"
2011
- }
2012
- },
2013
- text2(r.label, { fontWeight: "500" })
2014
- )
2015
- )
2016
- ),
2076
+ choices(v.flow.reasons.map((r) => ({ key: r.id, label: r.label, on: v.reason === r.id, press: () => flows.cancel.pick(r.id) }))),
2017
2077
  btn("Continue", "primary", flows.cancel.next, !v.reason),
2018
2078
  btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2019
2079
  ];
2020
- else if (v.screen === "offer" && v.offer) {
2080
+ else if (v.screen === "followup" && v.followUp) {
2081
+ const f = v.followUp;
2082
+ const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2083
+ body = [
2084
+ label ? muted(`You said: ${label}`) : null,
2085
+ title(f.question),
2086
+ f.answers.length ? choices(f.answers.map((a, i) => ({ key: String(i), label: a, on: v.answer === a, press: () => flows.cancel.answer(a) }))) : input("Your answer (optional)", v.answer ?? "", flows.cancel.answer),
2087
+ btn("Continue", "primary", flows.cancel.next),
2088
+ btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2089
+ ];
2090
+ } else if (v.screen === "offer" && v.offer) {
2021
2091
  const o = v.offer;
2022
2092
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2023
2093
  const detail = o.kind === "discount" && o.percent ? v.price ? `${money(discounted(v.price.amount, o.percent), v.price.currency)} instead of ${money(v.price.amount, v.price.currency)}${o.months ? ` for ${months(o.months)}` : ""}` : `${o.percent}% off${o.months ? ` for ${months(o.months)}` : ""}` : o.kind === "pause" && o.months ? `Pause for up to ${months(o.months)}. Your data stays.` : o.kind === "downgrade" && o.planName ? o.planName : null;
@@ -2693,6 +2763,13 @@ var buzz = () => {
2693
2763
  } catch {
2694
2764
  }
2695
2765
  };
2766
+ var imagePicker = null;
2767
+ try {
2768
+ imagePicker = require("expo-image-picker");
2769
+ } catch {
2770
+ imagePicker = null;
2771
+ }
2772
+ var PHOTO_TYPES = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif" };
2696
2773
  var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2697
2774
  var svg = null;
2698
2775
  try {
@@ -2708,9 +2785,10 @@ var GLYPH = {
2708
2785
  up: "M8 13V3M3.5 7.5 8 3l4.5 4.5",
2709
2786
  next: "M6 3.5 10.5 8 6 12.5",
2710
2787
  search: "M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3",
2711
- cross: "m4 4 8 8M12 4l-8 8"
2788
+ cross: "m4 4 8 8M12 4l-8 8",
2789
+ clip: "m14.3 7.4-6.1 6.1a4 4 0 0 1-5.7-5.7l5.7-5.7a2.7 2.7 0 0 1 3.8 3.8L6.3 11.6a1.3 1.3 0 0 1-1.9-1.9l5.7-5.6"
2712
2790
  };
2713
- var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7" };
2791
+ var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7", clip: "\u{1F4CE}" };
2714
2792
  var glyph = (name, color, size = 20) => {
2715
2793
  const Svg = svg?.Svg ?? svg?.default;
2716
2794
  if (!svg || !Svg) return (0, import_react2.createElement)(RN3.Text, { style: { color, fontSize: Math.round(size * 0.85), lineHeight: size + 2, textAlign: "center", minWidth: size } }, FALLBACK[name]);
@@ -2722,7 +2800,11 @@ var teamMark = (title, bg, fg, size = 36) => (0, import_react2.createElement)(
2722
2800
  { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: bg, alignItems: "center", justifyContent: "center" } },
2723
2801
  (0, import_react2.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
2724
2802
  );
2725
- var lift = { shadowColor: "#101218", shadowOpacity: 0.07, shadowRadius: 14, shadowOffset: { width: 0, height: 4 }, elevation: 2 };
2803
+ var HU = {
2804
+ light: { bg: "#ffffff", surface: "#f4f4f5", def: "#ececee", segment: "#ffffff", fg: "#18181b", muted: "#71717a" },
2805
+ dark: { bg: "#111113", surface: "#1c1c1f", def: "#26262a", segment: "#3a3a40", fg: "#fafafa", muted: "#a1a1aa" }
2806
+ };
2807
+ var hairline = { shadowColor: "#000", shadowOpacity: 0.07, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 1 };
2726
2808
  var useChat = (client, o, open, view) => {
2727
2809
  const [thread, setThread] = (0, import_react2.useState)(null);
2728
2810
  const [loaded, setLoaded] = (0, import_react2.useState)(false);
@@ -2889,12 +2971,14 @@ var MessengerSheet = ({
2889
2971
  o,
2890
2972
  chat,
2891
2973
  view,
2892
- setView
2974
+ setView,
2975
+ brand
2893
2976
  }) => {
2894
2977
  const k = kit(p);
2895
2978
  const [body, setBody] = (0, import_react2.useState)("");
2896
2979
  const [email, setEmail] = (0, import_react2.useState)("");
2897
2980
  const [pending, setPending] = (0, import_react2.useState)(null);
2981
+ const [photos, setPhotos] = (0, import_react2.useState)([]);
2898
2982
  const [query, setQuery] = (0, import_react2.useState)("");
2899
2983
  const helpOn = o.help !== false;
2900
2984
  const arts = useArticles(client, helpOn, query);
@@ -2908,52 +2992,135 @@ var MessengerSheet = ({
2908
2992
  }, [count, view]);
2909
2993
  const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
2910
2994
  const emailOk = !email.trim() || EMAIL.test(email.trim());
2995
+ const pickPhotos = async () => {
2996
+ const api = client.api();
2997
+ if (!api || !imagePicker?.launchImageLibraryAsync || pending) return;
2998
+ const room = CHAT_FILES_PER_MESSAGE - photos.length;
2999
+ if (room <= 0) return chat.setError(`Up to ${CHAT_FILES_PER_MESSAGE} files per message.`);
3000
+ let res;
3001
+ try {
3002
+ res = await imagePicker.launchImageLibraryAsync({ allowsMultipleSelection: room > 1, selectionLimit: room, quality: 0.8, exif: false });
3003
+ } catch (e) {
3004
+ return chat.setError(e.message || "The photos could not be opened.");
3005
+ }
3006
+ if (res.canceled || res.cancelled || !res.assets?.length) return;
3007
+ chat.setError("");
3008
+ const added = [];
3009
+ for (const a of res.assets.slice(0, room)) {
3010
+ const name = a.fileName || a.uri.split("/").pop() || "photo.jpg";
3011
+ const type = a.mimeType && Object.values(PHOTO_TYPES).includes(a.mimeType) ? a.mimeType : PHOTO_TYPES[name.split(".").pop()?.toLowerCase() ?? ""];
3012
+ if (!type) {
3013
+ chat.setError("Only pictures (PNG, JPEG, WebP, GIF) can be sent.");
3014
+ continue;
3015
+ }
3016
+ if (a.fileSize && a.fileSize > CHAT_FILE_MAX_BYTES) {
3017
+ chat.setError(`${name} is larger than 10 MB.`);
3018
+ continue;
3019
+ }
3020
+ const ph = { key: `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`, uri: a.uri, name, type, size: a.fileSize ?? 0, up: Promise.resolve() };
3021
+ ph.up = api.uploadChatFile({ uri: a.uri, name, type }).then(
3022
+ (r2) => {
3023
+ ph.id = r2.id;
3024
+ setPhotos((list) => list.map((x) => x.key === ph.key ? { ...ph } : x));
3025
+ },
3026
+ (e) => {
3027
+ setPhotos((list) => list.filter((x) => x.key !== ph.key));
3028
+ chat.setError(`${name}: ${e.message}`);
3029
+ }
3030
+ );
3031
+ added.push(ph);
3032
+ }
3033
+ setPhotos((list) => [...list, ...added]);
3034
+ };
3035
+ const canSend = (!!body.trim() || photos.length > 0) && !pending && emailOk;
2911
3036
  const send = async () => {
2912
3037
  const text2 = body.trim();
2913
3038
  const api = client.api();
2914
- if (!text2 || pending || !emailOk || !api) return;
2915
- setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
3039
+ const files = photos;
3040
+ if (!text2 && !files.length || pending || !emailOk || !api) return;
3041
+ setPending({
3042
+ id: "pending",
3043
+ from: "visitor",
3044
+ body: text2,
3045
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3046
+ ...files.length ? { attachments: files.map((f) => ({ id: f.key, name: f.name, type: f.type, size: f.size })) } : {}
3047
+ });
2916
3048
  setBody("");
3049
+ setPhotos([]);
2917
3050
  chat.setError("");
2918
3051
  try {
2919
- chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
3052
+ await Promise.all(files.map((f) => f.up));
3053
+ const ids = files.filter((f) => f.id).map((f) => f.id);
3054
+ if (!text2 && !ids.length) throw new Error("The photo could not be sent.");
3055
+ const win = RN3.Dimensions.get("screen");
3056
+ chat.take(
3057
+ await api.sendChat({
3058
+ body: text2,
3059
+ email: needEmail && email.trim() ? email.trim() : void 0,
3060
+ ...ids.length ? { attachments: ids } : {},
3061
+ // A little context for a bug report: the system and the screen size.
3062
+ context: { os: `${RN3.Platform.OS === "ios" ? "iOS" : RN3.Platform.OS === "android" ? "Android" : RN3.Platform.OS} ${RN3.Platform.Version ?? ""}`.trim(), browser: "App", viewport: `${Math.round(win.width)}\xD7${Math.round(win.height)}` }
3063
+ })
3064
+ );
2920
3065
  } catch (e) {
2921
3066
  setBody(text2);
3067
+ setPhotos(files.filter((f) => f.id));
2922
3068
  chat.setError(e.message);
2923
3069
  } finally {
2924
3070
  setPending(null);
2925
3071
  }
2926
3072
  };
3073
+ const localUris = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3074
+ for (const f of photos) localUris.current.set(f.key, f.uri);
3075
+ const fileUrl = (id) => localUris.current.get(id) ?? client.api()?.attachmentUrl(id) ?? "";
2927
3076
  const ink = inkOn(p.accent);
2928
3077
  const dark = p.bg === DARK.bg;
2929
- const canvas = dark ? p.bg : p.soft;
2930
- const raised = dark ? p.soft : p.bg;
2931
- const title = o.title ?? "Chat with us";
2932
- const reply = replyLine(o.replyTime);
3078
+ const r = CORNERS[o.corners ?? "round"];
3079
+ const plain = o.header !== "color";
3080
+ const hu = dark ? HU.dark : HU.light;
3081
+ const surf = {};
3082
+ const segLift = dark ? {} : hairline;
3083
+ const canvas = hu.bg;
3084
+ const raised = hu.surface;
3085
+ const title = o.title?.trim() ?? "";
2933
3086
  const bubble = (m, isPending = false) => {
2934
3087
  const mine = m.from === "visitor";
2935
3088
  return (0, import_react2.createElement)(
2936
3089
  RN3.View,
2937
3090
  { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
2938
- (0, import_react2.createElement)(
3091
+ ...(m.attachments ?? []).map(
3092
+ (f) => (0, import_react2.createElement)(
3093
+ RN3.Pressable,
3094
+ {
3095
+ key: f.id,
3096
+ onPress: () => void RN3.Linking.openURL(fileUrl(f.id)).catch(() => void 0),
3097
+ accessibilityRole: "imagebutton",
3098
+ accessibilityLabel: f.name,
3099
+ style: { alignSelf: mine ? "flex-end" : "flex-start", opacity: isPending ? 0.7 : 1 }
3100
+ },
3101
+ f.type.startsWith("image/") ? (0, import_react2.createElement)(RN3.Image, { source: { uri: fileUrl(f.id) }, resizeMode: "cover", style: { width: 200, height: 150, borderRadius: r.bubble, backgroundColor: p.soft } }) : (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } }, k.text(`\u{1F4C4} ${f.name}`, { fontSize: 13 }))
3102
+ )
3103
+ ),
3104
+ !m.body && m.attachments?.length ? null : (0, import_react2.createElement)(
2939
3105
  RN3.View,
2940
3106
  {
2941
3107
  style: {
2942
- backgroundColor: mine ? p.accent : dark ? p.soft : "#eeeff1",
2943
- borderRadius: 18,
2944
- borderBottomRightRadius: mine ? 6 : 18,
2945
- borderBottomLeftRadius: mine ? 18 : 6,
3108
+ backgroundColor: mine ? p.accent : hu.surface,
3109
+ ...mine ? {} : surf,
3110
+ borderRadius: r.bubble,
3111
+ borderBottomRightRadius: mine ? r.tail : r.bubble,
3112
+ borderBottomLeftRadius: mine ? r.bubble : r.tail,
2946
3113
  paddingHorizontal: 14,
2947
3114
  paddingVertical: 9,
2948
3115
  opacity: isPending ? 0.7 : 1
2949
3116
  }
2950
3117
  },
2951
- k.text(m.body, { color: mine ? ink : p.fg })
3118
+ k.text(m.body, { color: mine ? ink : hu.fg })
2952
3119
  ),
2953
3120
  ...(m.articles ?? []).map(
2954
3121
  (a) => (0, import_react2.createElement)(
2955
3122
  RN3.Pressable,
2956
- { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 7 } },
3123
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { backgroundColor: p.soft, borderRadius: r.card, paddingHorizontal: 10, paddingVertical: 7 } },
2957
3124
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
2958
3125
  )
2959
3126
  ),
@@ -2966,60 +3133,80 @@ var MessengerSheet = ({
2966
3133
  k.text(a.title, { flex: 1 }),
2967
3134
  glyph("next", p.accent, 14)
2968
3135
  );
2969
- const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: 16, padding: 16, ...dark ? { borderWidth: 1, borderColor: p.line } : lift, ...style } }, ...children);
3136
+ const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: r.card, padding: 16, ...surf, ...style } }, ...children);
2970
3137
  const searchRow = (onPress) => (0, import_react2.createElement)(
2971
3138
  RN3.Pressable,
2972
- { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: 12, paddingHorizontal: 14, backgroundColor: canvas } },
3139
+ { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: r.field, paddingHorizontal: 14, backgroundColor: hu.def } },
2973
3140
  k.muted("Search for help", { flex: 1, fontSize: 15 }),
2974
3141
  glyph("search", p.accent, 16)
2975
3142
  );
2976
- const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: 12, backgroundColor: raised, ...dark ? {} : lift } });
3143
+ const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
3144
+ const first = client.getUser()?.name?.trim().split(/\s+/)[0];
3145
+ const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
3146
+ const hero = (fg, sub) => (0, import_react2.createElement)(
3147
+ RN3.Text,
3148
+ { key: "hero", accessibilityRole: "header", style: { color: fg, fontSize: 25, fontWeight: "600", letterSpacing: -0.6, lineHeight: 30 } },
3149
+ first ? `Hi, ${first} \u{1F44B}
3150
+ ` : "Hi there \u{1F44B}\n",
3151
+ (0, import_react2.createElement)(RN3.Text, { style: { color: sub, fontWeight: "500" } }, rest)
3152
+ );
2977
3153
  const last = thread?.messages.at(-1);
2978
3154
  const showHelp = helpOn && arts.any !== false;
2979
3155
  let content;
2980
- if (view === "home")
2981
- content = (0, import_react2.createElement)(
2982
- RN3.ScrollView,
2983
- { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
2984
- (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, (0, import_react2.createElement)(RN3.Text, { style: { color: ink, fontSize: 28, fontWeight: "700", letterSpacing: -0.4, lineHeight: 34 } }, greeting)),
3156
+ if (view === "home") {
3157
+ const cards = [
3158
+ // "Ask a question": the words go into the chat's field and are sent as the chat opens.
2985
3159
  (0, import_react2.createElement)(
2986
3160
  RN3.View,
2987
- { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } },
2988
- last ? (0, import_react2.createElement)(
2989
- RN3.Pressable,
2990
- { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
2991
- card([
2992
- k.muted("Recent message", { fontSize: 12, fontWeight: "600", marginBottom: 10 }),
2993
- (0, import_react2.createElement)(
2994
- RN3.View,
2995
- { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 12 } },
2996
- teamMark(title, p.accent, ink),
2997
- (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: p.fg, fontSize: 15 } }, last.body), k.muted(`${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`, { fontSize: 12 })),
2998
- chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { minWidth: 20, height: 20, borderRadius: 10, paddingHorizontal: 6, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(chat.unseen.length), { color: "#fff", fontSize: 12, fontWeight: "700" })) : glyph("next", p.muted, 14)
2999
- )
3000
- ])
3001
- ) : null,
3161
+ { key: "ask", style: { flexDirection: "row", alignItems: "center", gap: 8, height: 48, borderRadius: 24, paddingLeft: 18, paddingRight: 6, backgroundColor: hu.surface, ...surf } },
3162
+ k.field({ value: body, onChange: setBody, placeholder: "Ask a question", max: 4e3, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
3002
3163
  (0, import_react2.createElement)(
3003
3164
  RN3.Pressable,
3004
- { key: "send", onPress: () => setView("messages"), accessibilityRole: "button" },
3005
- card(
3006
- [
3007
- (0, import_react2.createElement)(RN3.View, { key: "t", style: { flex: 1, gap: 2 } }, k.text("Send us a message", { fontWeight: "700", fontSize: 16 }), k.muted(reply)),
3008
- (0, import_react2.createElement)(RN3.View, { key: "g", style: { width: 36, height: 36, borderRadius: 18, backgroundColor: alpha(p.accent, 0.12), alignItems: "center", justifyContent: "center" } }, glyph("send", p.accent, 18))
3009
- ],
3010
- { flexDirection: "row", alignItems: "center", gap: 12 }
3165
+ {
3166
+ onPress: () => {
3167
+ setView("messages");
3168
+ if (body.trim()) void send();
3169
+ },
3170
+ accessibilityRole: "button",
3171
+ accessibilityLabel: "Send",
3172
+ style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: body.trim() ? p.accent : hu.def }
3173
+ },
3174
+ glyph("up", body.trim() ? ink : hu.muted, 18)
3175
+ )
3176
+ ),
3177
+ last ? (0, import_react2.createElement)(
3178
+ RN3.Pressable,
3179
+ { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
3180
+ card([
3181
+ (0, import_react2.createElement)(
3182
+ RN3.View,
3183
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
3184
+ title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
3185
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 1 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, last.body || (last.attachments?.length ? `Sent ${last.attachments.length === 1 ? last.attachments[0].name : `${last.attachments.length} files`}` : "")), k.muted(`${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`, { fontSize: 11.5 })),
3186
+ chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : glyph("next", p.muted, 14)
3011
3187
  )
3012
- ),
3013
- showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3014
- )
3188
+ ], { paddingVertical: 10, paddingHorizontal: 12 })
3189
+ ) : null,
3190
+ showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3191
+ ];
3192
+ content = plain ? (0, import_react2.createElement)(
3193
+ RN3.ScrollView,
3194
+ { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 24, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3195
+ (0, import_react2.createElement)(RN3.View, { key: "top", style: { paddingHorizontal: 6, paddingBottom: 10 } }, hero(p.fg, p.muted)),
3196
+ ...cards
3197
+ ) : (0, import_react2.createElement)(
3198
+ RN3.ScrollView,
3199
+ { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
3200
+ (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, hero(ink, alpha(ink, 0.8))),
3201
+ (0, import_react2.createElement)(RN3.View, { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } }, ...cards)
3015
3202
  );
3016
- else if (view === "messages")
3203
+ } else if (view === "messages")
3017
3204
  content = (0, import_react2.createElement)(
3018
3205
  RN3.View,
3019
- { key: "messages", style: { flex: 1, backgroundColor: p.bg } },
3206
+ { key: "messages", style: { flex: 1, backgroundColor: hu.bg } },
3020
3207
  (0, import_react2.createElement)(
3021
3208
  RN3.ScrollView,
3022
- { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 12 } },
3209
+ { ref: scroll, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
3023
3210
  bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
3024
3211
  !chat.loaded ? k.muted("Loading\u2026") : null,
3025
3212
  ...(thread?.messages ?? []).map((m) => bubble(m)),
@@ -3028,23 +3215,57 @@ var MessengerSheet = ({
3028
3215
  ),
3029
3216
  (0, import_react2.createElement)(
3030
3217
  RN3.View,
3031
- { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 10, gap: 8 } },
3218
+ { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 6, gap: 8 } },
3032
3219
  chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
3033
- needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: { borderWidth: emailOk ? 0 : 1, borderColor: p.danger, borderRadius: 12, backgroundColor: canvas } }) : null,
3220
+ needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: { borderWidth: emailOk ? 0 : 1, borderColor: p.danger, borderRadius: r.field, paddingHorizontal: 16, backgroundColor: hu.surface, ...surf } }) : null,
3221
+ // Photos picked for the next message, each with a way to take it out again.
3222
+ photos.length ? (0, import_react2.createElement)(
3223
+ RN3.View,
3224
+ { style: { flexDirection: "row", flexWrap: "wrap", gap: 10, paddingTop: 6, paddingHorizontal: 2 } },
3225
+ ...photos.map(
3226
+ (f) => (0, import_react2.createElement)(
3227
+ RN3.View,
3228
+ { key: f.key, style: { width: 56, height: 56 } },
3229
+ (0, import_react2.createElement)(RN3.Image, { source: { uri: f.uri }, resizeMode: "cover", accessibilityLabel: f.id ? f.name : `${f.name}, uploading`, style: { width: 56, height: 56, borderRadius: Math.max(4, r.card - 4), backgroundColor: p.soft, opacity: f.id ? 1 : 0.5 } }),
3230
+ (0, import_react2.createElement)(
3231
+ RN3.Pressable,
3232
+ {
3233
+ onPress: () => setPhotos((list) => list.filter((x) => x.key !== f.key)),
3234
+ accessibilityRole: "button",
3235
+ accessibilityLabel: `Remove ${f.name}`,
3236
+ hitSlop: 8,
3237
+ style: { position: "absolute", top: -7, right: -7, width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: p.bg, backgroundColor: p.fg, alignItems: "center", justifyContent: "center" }
3238
+ },
3239
+ glyph("cross", p.bg, 10)
3240
+ )
3241
+ )
3242
+ )
3243
+ ) : null,
3244
+ // The message field on top, the paperclip (when the app has expo-image-picker) and the send button under it.
3034
3245
  (0, import_react2.createElement)(
3035
3246
  RN3.View,
3036
- { style: { flexDirection: "row", alignItems: "flex-end", gap: 6, borderWidth: 1, borderColor: p.line, borderRadius: 22, paddingVertical: 5, paddingRight: 5, paddingLeft: 16, backgroundColor: p.bg } },
3037
- k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 34, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 7, backgroundColor: "transparent" } }),
3247
+ { style: { gap: 2, borderRadius: 20, paddingTop: 10, paddingBottom: 6, paddingRight: 6, paddingLeft: 16, backgroundColor: hu.surface, ...surf } },
3248
+ k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { minHeight: 28, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 4, backgroundColor: "transparent" } }),
3038
3249
  (0, import_react2.createElement)(
3039
- RN3.Pressable,
3040
- {
3041
- onPress: () => void send(),
3042
- disabled: !body.trim() || !!pending || !emailOk,
3043
- accessibilityRole: "button",
3044
- accessibilityLabel: "Send",
3045
- style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: !body.trim() || !!pending || !emailOk ? p.line : p.accent }
3046
- },
3047
- glyph("up", !body.trim() || !!pending || !emailOk ? p.muted : ink, 18)
3250
+ RN3.View,
3251
+ { style: { flexDirection: "row", alignItems: "center", marginLeft: -8 } },
3252
+ imagePicker?.launchImageLibraryAsync ? (0, import_react2.createElement)(
3253
+ RN3.Pressable,
3254
+ { onPress: () => void pickPhotos(), disabled: !!pending, accessibilityRole: "button", accessibilityLabel: "Attach a photo or screenshot", hitSlop: 6, style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" } },
3255
+ glyph("clip", hu.muted, 19)
3256
+ ) : null,
3257
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
3258
+ (0, import_react2.createElement)(
3259
+ RN3.Pressable,
3260
+ {
3261
+ onPress: () => void send(),
3262
+ disabled: !canSend,
3263
+ accessibilityRole: "button",
3264
+ accessibilityLabel: "Send",
3265
+ style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: canSend ? p.accent : hu.def }
3266
+ },
3267
+ glyph("up", canSend ? ink : hu.muted, 18)
3268
+ )
3048
3269
  )
3049
3270
  )
3050
3271
  )
@@ -3054,7 +3275,7 @@ var MessengerSheet = ({
3054
3275
  for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
3055
3276
  content = (0, import_react2.createElement)(
3056
3277
  RN3.ScrollView,
3057
- { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3278
+ { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3058
3279
  search,
3059
3280
  arts.list === null ? k.muted("Loading\u2026") : null,
3060
3281
  arts.list?.length === 0 ? (0, import_react2.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), k.btn("Send us a message", "primary", () => setView("messages"))) : null,
@@ -3063,46 +3284,73 @@ var MessengerSheet = ({
3063
3284
  } else
3064
3285
  content = (0, import_react2.createElement)(
3065
3286
  RN3.ScrollView,
3066
- { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
3287
+ { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 32 } },
3067
3288
  (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
3068
3289
  );
3069
3290
  const current = typeof view === "string" ? view : "help";
3070
- const tab = (v, name, icon, badge = 0) => (0, import_react2.createElement)(
3291
+ const tab = (v, name, icon, badge = 0) => {
3292
+ const on = current === v;
3293
+ return (0, import_react2.createElement)(
3294
+ RN3.Pressable,
3295
+ {
3296
+ key: v,
3297
+ onPress: () => setView(v),
3298
+ accessibilityRole: "tab",
3299
+ accessibilityState: { selected: on },
3300
+ style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 6, height: 36, borderRadius: 18, ...on ? { backgroundColor: hu.segment, ...segLift } : {} }
3301
+ },
3302
+ glyph(icon, on ? hu.fg : hu.muted, 17),
3303
+ k.text(name, { fontSize: 13.5, fontWeight: "500", color: on ? hu.fg : hu.muted }),
3304
+ badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 7, right: 12, width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : null
3305
+ );
3306
+ };
3307
+ const onAccent = view === "home" && !plain;
3308
+ const ground = onAccent ? p.accent : hu.bg;
3309
+ const back = (to, label) => (0, import_react2.createElement)(
3071
3310
  RN3.Pressable,
3072
- { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 3 } },
3073
- glyph(icon, current === v ? p.accent : p.muted, 22),
3074
- k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.accent : p.muted }),
3075
- badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "28%", minWidth: 18, height: 18, borderRadius: 9, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(badge, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
3311
+ { key: "back", onPress: () => setView(to), accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
3312
+ glyph("next", hu.fg, 18)
3076
3313
  );
3077
- const onHome = view === "home";
3078
- const headFg = onHome ? ink : p.fg;
3079
3314
  const head = (0, import_react2.createElement)(
3080
3315
  RN3.View,
3081
- { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 10, paddingLeft: 16, paddingRight: 12, paddingTop: 16, paddingBottom: 12, backgroundColor: onHome ? p.accent : p.bg, borderBottomWidth: onHome ? 0 : 1, borderBottomColor: p.line } },
3082
- typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", hitSlop: 10 }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : view === "help" ? null : teamMark(title, onHome ? ink : p.accent, onHome ? p.accent : ink, onHome ? 28 : 36),
3083
- (0, import_react2.createElement)(
3316
+ { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 12, paddingRight: 12, paddingTop: 12, paddingBottom: 6, backgroundColor: ground } },
3317
+ typeof view !== "string" ? back("help", "All articles") : view === "messages" ? back("home", "Back to home") : view === "home" && (o.logo || title) ? o.logo ? (0, import_react2.createElement)(RN3.Image, { key: "logo", source: { uri: o.logo }, accessibilityIgnoresInvertColors: true, style: { width: 28, height: 28, borderRadius: 8, marginLeft: 4 } }) : teamMark(title, alpha(p.accent, 0.16), p.accent, 28) : null,
3318
+ // In the chat: who answers, in the middle.
3319
+ view === "messages" && title ? (0, import_react2.createElement)(
3084
3320
  RN3.View,
3085
- { style: { flex: 1, minWidth: 0 } },
3086
- typeof view !== "string" ? null : (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", numberOfLines: 1, style: { color: headFg, fontSize: 17, fontWeight: "700" } }, view === "help" ? "Help" : title),
3087
- view === "messages" ? k.muted(reply, { fontSize: 12 }) : null
3088
- ),
3089
- (0, import_react2.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onHome ? ink : p.muted, 18))
3321
+ { style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 } },
3322
+ teamMark(title, alpha(p.accent, 0.16), p.accent, 28),
3323
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { flexShrink: 1, color: hu.fg, fontSize: 15, fontWeight: "600" } }, title)
3324
+ ) : (0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
3325
+ (0, import_react2.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onAccent ? ink : p.muted, 18))
3090
3326
  );
3327
+ const powered = brand ? (0, import_react2.createElement)(
3328
+ RN3.View,
3329
+ { key: "brand", style: { paddingTop: 2, paddingBottom: 6, alignItems: "center", backgroundColor: hu.bg } },
3330
+ (0, import_react2.createElement)(
3331
+ RN3.Text,
3332
+ { onPress: () => void RN3.Linking.openURL(BRAND_URL).catch(() => void 0), accessibilityRole: "link", hitSlop: 8, style: { color: p.muted, fontSize: 11.5 } },
3333
+ "Powered by ",
3334
+ (0, import_react2.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket")
3335
+ )
3336
+ ) : null;
3091
3337
  return (0, import_react2.createElement)(
3092
3338
  RN3.Modal,
3093
3339
  { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: close },
3094
3340
  (0, import_react2.createElement)(
3095
3341
  RN3.KeyboardAvoidingView,
3096
- { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
3342
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: hu.bg } },
3097
3343
  head,
3098
3344
  content,
3099
- (0, import_react2.createElement)(
3345
+ view === "messages" ? null : (0, import_react2.createElement)(
3100
3346
  RN3.View,
3101
- { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingHorizontal: 8, paddingBottom: 22, backgroundColor: p.bg } },
3347
+ { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", marginHorizontal: 12, marginTop: 6, marginBottom: 6, padding: 4, borderRadius: 22, backgroundColor: hu.surface } },
3102
3348
  tab("home", "Home", "home"),
3103
- tab("messages", "Messages", "chat", view === "messages" ? 0 : chat.unseen.length),
3349
+ tab("messages", "Messages", "chat", chat.unseen.length),
3104
3350
  showHelp ? tab("help", "Help", "help") : null
3105
- )
3351
+ ),
3352
+ powered,
3353
+ (0, import_react2.createElement)(RN3.View, { style: { height: 16 } })
3106
3354
  )
3107
3355
  );
3108
3356
  };
@@ -3127,6 +3375,9 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3127
3375
  preview: look.preview,
3128
3376
  askEmail: look.askEmail,
3129
3377
  haptics: look.sound,
3378
+ corners: look.corners,
3379
+ header: look.header,
3380
+ logo: look.logo,
3130
3381
  ...defined
3131
3382
  } : given;
3132
3383
  const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
@@ -3155,37 +3406,42 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3155
3406
  const unseen = chat.unseen.length;
3156
3407
  const ink = inkOn(p.accent);
3157
3408
  const label = options.label?.trim() ?? "";
3158
- const side = options.position === "bottom-left" ? { left: 20 } : { right: 20 };
3409
+ const r = CORNERS[options.corners ?? "round"];
3410
+ const [edge, side] = (options.position ?? "bottom-right").split("-");
3411
+ const place = {
3412
+ ...edge === "top" ? { top: 60 } : edge === "bottom" ? { bottom: 34 } : { top: 0, bottom: 0, justifyContent: "center" },
3413
+ ...side === "left" ? { left: 20 } : side === "right" ? { right: 20 } : { left: 0, right: 0, alignItems: "center" }
3414
+ };
3159
3415
  const launcher = options.launcher ? (0, import_react2.createElement)(
3160
- RN3.Pressable,
3161
- {
3162
- key: "launcher",
3163
- onPress: () => openAt(unseen ? "messages" : "home"),
3164
- accessibilityRole: "button",
3165
- accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
3166
- style: {
3167
- position: "absolute",
3168
- ...side,
3169
- bottom: 34,
3170
- height: label ? 50 : 56,
3171
- minWidth: label ? 0 : 56,
3172
- paddingHorizontal: label ? 18 : 0,
3173
- borderRadius: 28,
3174
- flexDirection: "row",
3175
- gap: 8,
3176
- backgroundColor: p.accent,
3177
- alignItems: "center",
3178
- justifyContent: "center",
3179
- shadowColor: p.accent,
3180
- shadowOpacity: 0.4,
3181
- shadowRadius: 12,
3182
- shadowOffset: { width: 0, height: 6 },
3183
- elevation: 6
3184
- }
3185
- },
3186
- glyph("chat", ink, label ? 20 : 26),
3187
- label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
3188
- unseen ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
3416
+ RN3.View,
3417
+ { key: "launcher", pointerEvents: "box-none", style: { position: "absolute", ...place } },
3418
+ (0, import_react2.createElement)(
3419
+ RN3.Pressable,
3420
+ {
3421
+ onPress: () => openAt(unseen ? "messages" : "home"),
3422
+ accessibilityRole: "button",
3423
+ accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
3424
+ style: {
3425
+ height: label ? 50 : 56,
3426
+ minWidth: label ? 0 : 56,
3427
+ paddingHorizontal: label ? 18 : 0,
3428
+ borderRadius: 28,
3429
+ flexDirection: "row",
3430
+ gap: 8,
3431
+ backgroundColor: p.accent,
3432
+ alignItems: "center",
3433
+ justifyContent: "center",
3434
+ shadowColor: p.accent,
3435
+ shadowOpacity: 0.4,
3436
+ shadowRadius: 12,
3437
+ shadowOffset: { width: 0, height: 6 },
3438
+ elevation: 6
3439
+ }
3440
+ },
3441
+ glyph("chat", ink, label ? 20 : 26),
3442
+ label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
3443
+ unseen ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
3444
+ )
3189
3445
  ) : null;
3190
3446
  const toast = banner ? (0, import_react2.createElement)(
3191
3447
  RN3.Pressable,
@@ -3194,15 +3450,25 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3194
3450
  onPress: () => openAt("messages"),
3195
3451
  accessibilityRole: "button",
3196
3452
  accessibilityLiveRegion: "polite",
3197
- style: { position: "absolute", top: 54, left: 16, right: 16, flexDirection: "row", alignItems: "flex-start", gap: 12, backgroundColor: p.bg, borderRadius: 18, padding: 14, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.16, shadowRadius: 18, shadowOffset: { width: 0, height: 8 }, elevation: 8 }
3453
+ style: {
3454
+ position: "absolute",
3455
+ top: 54,
3456
+ left: 16,
3457
+ right: 16,
3458
+ gap: 3,
3459
+ backgroundColor: p.bg === DARK.bg ? p.soft : p.bg,
3460
+ borderRadius: r.card + 2,
3461
+ paddingHorizontal: 16,
3462
+ paddingVertical: 13,
3463
+ shadowColor: "#101218",
3464
+ shadowOpacity: 0.14,
3465
+ shadowRadius: 18,
3466
+ shadowOffset: { width: 0, height: 8 },
3467
+ elevation: 6
3468
+ }
3198
3469
  },
3199
- teamMark(options.title ?? "Chat with us", p.accent, ink),
3200
- (0, import_react2.createElement)(
3201
- RN3.View,
3202
- { style: { flex: 1, gap: 2 } },
3203
- k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3204
- (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
3205
- )
3470
+ k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3471
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
3206
3472
  ) : null;
3207
3473
  if (!launcher && !toast && !open) return null;
3208
3474
  return (0, import_react2.createElement)(
@@ -3210,7 +3476,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
3210
3476
  { pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
3211
3477
  launcher,
3212
3478
  toast,
3213
- open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
3479
+ open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView, brand: look?.branding !== false }) : null
3214
3480
  );
3215
3481
  };
3216
3482