Skip to content

Commit bdf2b6c

Browse files
Hotragnclaude
andcommitted
Share: Send opens no sheet; Share is H4's main tap, navigator.share first; failures show Copy link and a toast
Rebased on main (keeps postedToTelegram). WIP: e2e re-run after rebase, share-matrix.md, budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4PRV5YZJ43W9E7E2HHCXS9F
1 parent 7fa8ad0 commit bdf2b6c

10 files changed

Lines changed: 407 additions & 32 deletions

File tree

‎apps/web/src/lib/screens/H4Sent.svelte‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
copied = false,
1313
ondone,
1414
onshare,
15+
linkOnly = false,
16+
shareFirst = false,
1517
oncopy,
1618
}: {
1719
theme?: Theme;
@@ -20,6 +22,10 @@
2022
copied?: boolean;
2123
ondone?: () => void;
2224
onshare?: () => void;
25+
/** No share sheet in this browser: Share copies the link. */
26+
linkOnly?: boolean;
27+
/** Share is the main button (not linked to Telegram). */
28+
shareFirst?: boolean;
2329
oncopy?: () => void;
2430
} = $props();
2531
</script>
@@ -38,9 +44,11 @@
3844
{#if copied}<p class="t-small" role="status">{t("sent.copied")}</p>{/if}
3945
</div>
4046
{#snippet actions()}
41-
<Button onclick={ondone}>{t("sent.pocket")}</Button>
47+
{@const share = t(linkOnly ? "share.copyLink" : "sent.shareAgain")}
48+
<!-- shareFirst: the Flare exists and the link is ready, so this tap opens the sheet at once. -->
49+
{#if shareFirst}<Button icon="share" onclick={onshare}>{share}</Button>{:else}<Button onclick={ondone}>{t("sent.pocket")}</Button>{/if}
4250
<div class="row">
43-
<Button variant="text" icon="share" onclick={onshare}>{t("sent.shareAgain")}</Button>
51+
{#if shareFirst}<Button variant="text" onclick={ondone}>{t("sent.pocket")}</Button>{:else}<Button variant="text" icon="share" onclick={onshare}>{share}</Button>{/if}
4452
<Button variant="text" onclick={oncopy}>{t("sent.copyText")}</Button>
4553
</div>
4654
{/snippet}

‎apps/web/src/lib/screens/H5HostKindling.svelte‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ontextyes,
2626
oncancel,
2727
onshare,
28+
linkOnly = false,
2829
onclose,
2930
}: {
3031
/** The Flare photo behind the screen (2026 direction); the warm light when there is none. */
@@ -45,6 +46,8 @@
4546
ontextyes?: (code: string) => void;
4647
oncancel?: () => void;
4748
onshare?: () => void;
49+
/** No share sheet in this browser: Share copies the link. */
50+
linkOnly?: boolean;
4851
onclose?: () => void;
4952
} = $props();
5053
// svelte-ignore state_referenced_locally
@@ -63,7 +66,7 @@
6366
{#if error}<p class="err" role="alert">{error}</p>{/if}
6467
{#snippet actions()}
6568
<div class="row">
66-
<Button variant="secondary" icon="share" onclick={onshare}>{t("sent.shareAgain")}</Button>
69+
<Button variant="secondary" icon="share" onclick={onshare}>{t(linkOnly ? "share.copyLink" : "sent.shareAgain")}</Button>
6770
<Button variant="secondary" onclick={() => (sheet = "text-yes")}>{t("host.textYes.open")}</Button>
6871
</div>
6972
<Button variant="text" onclick={() => (sheet = "cancel")}>{t("host.kindling.cancel")}</Button>

‎apps/web/src/lib/screens/H6HostLit.svelte‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
busy = false,
2020
onplusone,
2121
ontell,
22+
linkOnly = false,
2223
ondone,
2324
onclose,
2425
}: {
@@ -34,6 +35,8 @@
3435
onplusone?: (who: { id: string } | "someone_else") => void;
3536
/** Automations: tell the group chat it's on (share sheet, or copy). */
3637
ontell?: () => void;
38+
/** No share sheet in this browser: Tell the chat copies the link. */
39+
linkOnly?: boolean;
3740
ondone?: () => void;
3841
onclose?: () => void;
3942
} = $props();
@@ -56,7 +59,7 @@
5659
{#if error}<p class="err" role="alert">{error}</p>{/if}
5760
{#snippet actions()}
5861
<Button onclick={() => (sheet = "done")}>{t("host.lit.done")}</Button>
59-
{#if ontell}<Button variant="text" icon="share" onclick={ontell}>{t("host.lit.tell")}</Button>{/if}
62+
{#if ontell}<Button variant="text" icon="share" onclick={ontell}>{t(linkOnly ? "share.copyLink" : "host.lit.tell")}</Button>{/if}
6063
{/snippet}
6164
{#snippet overlay()}
6265
{#if sheet === "plusone"}

‎apps/web/src/lib/screens/HostPage.svelte‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@
151151
152152
async function send() {
153153
if (!flow.spot?.name && typedSpot.trim()) flow.typeSpot(typedSpot);
154-
const ok = await flow.send(kind);
155-
// The share sheet opens on its own (H4), unless the crew is linked to Telegram: then the server
156-
// already posted the card to the group, one tap fewer (automations, ADR-0030).
157-
if (ok && !flow.postedToTelegram) void flow.share();
154+
await flow.send(kind);
155+
// No share sheet here: Send awaits the seal and the POST, and iOS drops the tap by then, so the
156+
// sheet would fail with nothing on screen. H4's Share is the next tap, with the link ready.
157+
// Linked to Telegram, the server already posted the card to the group (automations, ADR-0030).
158158
}
159159
160160
// Plans: datetime-local values in this phone's zone.
@@ -212,11 +212,11 @@
212212
{:else if flow.screen === "send"}
213213
<H3Send view={flow.sending ? "sending" : flow.error ? "error" : "ready"} photo={flow.photo} scene={flow.alt} words={flow.words} spot={flow.spot?.name ?? ""} bind:quorum={flow.quorum} bind:hostLine={flow.hostLine} bind:hostName={flow.hostName} askName={flow.askName} askSpot={!flow.spot?.name} bind:typedSpot access={flow.access} nature={flow.nature} error={flow.error} onsend={send} onclose={exit} />
214214
{:else if flow.screen === "sent"}
215-
<H4Sent kind={flow.view?.kind ?? kind} deadline={flow.deadlineAt ? clock(flow.deadlineAt, flow.tz) : ""} copied={flow.copied} ondone={() => flow.pocket()} onshare={() => flow.share()} oncopy={() => flow.copyText()} />
215+
<H4Sent kind={flow.view?.kind ?? kind} deadline={flow.deadlineAt ? clock(flow.deadlineAt, flow.tz) : ""} copied={flow.copied} linkOnly={flow.linkOnly} shareFirst={!flow.postedToTelegram} ondone={() => flow.pocket()} onshare={() => flow.share()} oncopy={() => flow.copyText()} />
216216
{:else if flow.screen === "kindling"}
217-
<H5HostKindling photo={flow.photo} scene={flow.alt} have={flow.have} quorum={flow.view?.quorum ?? 2} sentMin={flow.sentMin} shrinkMin={flow.shrinkMin} hostLine={flow.hostLine} code={flow.view?.short_code ?? ""} textYesUsed={flow.textYesUsed} error={flow.error} busy={flow.busy} ontextyes={(c) => flow.textYes(c)} oncancel={() => flow.cancel()} onshare={() => flow.share()} onclose={exit} />
217+
<H5HostKindling photo={flow.photo} scene={flow.alt} have={flow.have} quorum={flow.view?.quorum ?? 2} sentMin={flow.sentMin} shrinkMin={flow.shrinkMin} hostLine={flow.hostLine} code={flow.view?.short_code ?? ""} textYesUsed={flow.textYesUsed} error={flow.error} busy={flow.busy} ontextyes={(c) => flow.textYes(c)} oncancel={() => flow.cancel()} onshare={() => flow.share()} linkOnly={flow.linkOnly} onclose={exit} />
218218
{:else if flow.screen === "lit"}
219-
<H6HostLit ontell={() => void flow.shareLit()} coming={Math.max(0, flow.have - 1)} spot={flow.spot?.name ?? ""} here={flow.view?.headcount ?? 0} members={flow.members} marked={flow.marked} error={flow.error} busy={flow.busy} onplusone={(w) => flow.plusOne(w)} ondone={() => flow.done()} onclose={exit} />
219+
<H6HostLit ontell={() => void flow.shareLit()} linkOnly={flow.linkOnly} coming={Math.max(0, flow.have - 1)} spot={flow.spot?.name ?? ""} here={flow.view?.headcount ?? 0} members={flow.members} marked={flow.marked} error={flow.error} busy={flow.busy} onplusone={(w) => flow.plusOne(w)} ondone={() => flow.done()} onclose={exit} />
220220
{:else if flow.screen === "outside"}
221221
<I12Outside view="end" sunset={flow.nature} minutes={flow.outsideMin} photo={flow.photo} onclose={exit} />
222222
{:else if flow.screen === "gone"}
@@ -249,6 +249,7 @@
249249
{#if flow.screen === "sent" && flow.postedToTelegram}
250250
<p class="posted" role="status">{t("sent.telegram")}</p>
251251
{/if}
252+
<p class="toast" role="status">{flow.note}</p>
252253

253254
<!-- Remember this (ADR-0031): the host turns it on; the recording sign shows here too. -->
254255
{#if (flow.screen === "kindling" || flow.screen === "lit") && flow.rememberCtx()}
@@ -263,4 +264,7 @@
263264
.row { width: 100%; min-height: 56px; display: flex; align-items: center; gap: 12px; padding: 8px 6px; border: 0; background: transparent; color: var(--text); text-decoration: none; font: inherit; text-align: left; border-radius: var(--r-md); }
264265
.txt { flex: 1; display: flex; flex-direction: column; }
265266
.foot { margin: 0; }
267+
.toast { position: fixed; top: calc(env(safe-area-inset-top, 0) + 72px); left: 16px; right: 16px; z-index: 30; margin: 0 auto; width: fit-content; max-width: 100%; overflow-wrap: anywhere;
268+
padding: 10px 18px; border-radius: 20px; background: var(--c-gold-400); color: #111; font-weight: 700; }
269+
.toast:empty { display: none; }
266270
</style>

‎apps/web/src/lib/screens/flow/host.svelte.ts‎

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,13 @@ export class HostFlow {
491491

492492
// ---------- H4: share ----------
493493

494-
/** The share sheet with the link (SRS-CRE-8). Call from a tap. */
495-
async share(): Promise<void> {
494+
/** No share sheet here, or it failed: the share buttons say Copy link. */
495+
linkOnly = $state<boolean>(typeof navigator !== "undefined" && !navigator.share);
496+
note = $state("");
497+
498+
/** The share sheet with the link (SRS-CRE-8). Call from a tap: navigator.share runs before any
499+
* await, or iOS drops the tap and nothing opens. */
500+
share(): Promise<void> {
496501
const host = this.hostName || storedName();
497502
const v = this.view;
498503
const kind = v?.kind ?? "flare";
@@ -501,24 +506,33 @@ export class HostFlow {
501506
kind === "plan"
502507
? t("share.plan.text", { ask: this.ask, deadline: this.deadlineAt ? clock(this.deadlineAt, this.#d.tz) : "" })
503508
: t("share.flare.text", { invite: this.words, n: v?.need ?? Number(this.quorum) - 1 });
504-
try {
505-
if (navigator.share) await navigator.share({ title, text, url: this.link });
506-
else await this.copyText();
507-
} catch {
508-
// The host closed the share sheet: nothing to do.
509-
}
509+
return this.#sheet({ title, text, url: this.link });
510510
}
511511

512512
/** Automations, "Tell the chat": once it's lit, one tap tells the group chat it's on and where.
513513
* The same link as the invite; the count only, never who. */
514-
async shareLit(): Promise<void> {
515-
const spot = this.spot?.name ?? "";
516-
const text = t("share.lit.text", { spot });
514+
shareLit(): Promise<void> {
515+
const text = t("share.lit.text", { spot: this.spot?.name ?? "" });
516+
return this.#sheet({ title: t("share.lit.title"), text, url: this.link });
517+
}
518+
519+
async #sheet(data: ShareData): Promise<void> {
520+
if (!this.linkOnly)
521+
try {
522+
return await navigator.share(data); // the first await in the tap
523+
} catch (e) {
524+
// AbortError: the host closed the sheet. Otherwise the next tap copies, in its own activation.
525+
if ((e as Error).name === "AbortError") return;
526+
this.linkOnly = true;
527+
this.note = t("share.failed");
528+
return;
529+
}
517530
try {
518-
if (navigator.share) await navigator.share({ title: t("share.lit.title"), text, url: this.link });
519-
else await navigator.clipboard?.writeText(`${text} ${this.link}`);
531+
await navigator.clipboard.writeText(`${data.text} ${data.url}`);
532+
this.note = t("share.linkCopied");
533+
setTimeout(() => (this.note = ""), 2500);
520534
} catch {
521-
// The host closed the share sheet.
535+
this.note = t("share.copyFailed", { url: data.url ?? "" }); // stays, so the link can be copied by hand
522536
}
523537
}
524538

0 commit comments

Comments
 (0)