@simon-says-build/ssb-overlay 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +6 -0
- package/package.json +2 -1
- package/src/SsbHost.js +41 -10
- package/src/core.js +18 -1
package/index.d.ts
CHANGED
|
@@ -41,5 +41,11 @@ export function SsbHost(props: {
|
|
|
41
41
|
voice?: { Room: new (opts?: object) => any; startAudio?: () => Promise<void> | void; stopAudio?: () => Promise<void> | void };
|
|
42
42
|
/** Keep the button off the home indicator / tab bar. Default {top: 60, bottom: 40}. */
|
|
43
43
|
insets?: { top?: number; bottom?: number };
|
|
44
|
+
/**
|
|
45
|
+
* Which build this is: the origin the app's JS bundle was loaded from (a
|
|
46
|
+
* SimonSays preview's tunnel). Read off the bundle when omitted; Ready lists
|
|
47
|
+
* only what this build holds, and ▶ Demo plays it here.
|
|
48
|
+
*/
|
|
49
|
+
bundle?: string;
|
|
44
50
|
children?: ReactNode;
|
|
45
51
|
}): ReactNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simon-says-build/ssb-overlay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The SSB button inside a native app SimonSays controls: screenshot the screen, mark it, and ask SSB for a change. Renders nothing for anyone SSB does not let in.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"peerDependencies": {
|
|
25
|
+
"@simon-says-build/demo-player": ">=0.1.0",
|
|
25
26
|
"react": ">=18",
|
|
26
27
|
"react-native": ">=0.76"
|
|
27
28
|
},
|
package/src/SsbHost.js
CHANGED
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
22
22
|
import {
|
|
23
23
|
ActivityIndicator, Image, Keyboard, KeyboardAvoidingView, Linking, PanResponder, Platform, Pressable,
|
|
24
|
-
ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions,
|
|
24
|
+
NativeModules, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions,
|
|
25
25
|
} from 'react-native';
|
|
26
|
+
// The app's demo player (a peer): ▶ Demo hands it the demo to play in this
|
|
27
|
+
// build. An older player has no playDemo, and the button is not offered.
|
|
28
|
+
import { playDemo } from '@simon-says-build/demo-player';
|
|
26
29
|
import {
|
|
27
30
|
FAB, addPoint, clampFab, demoBanner, fabHome, isDrag, describeHost, draftsOf, fieldTop, inAppApi, namedHosts, newReply, offerOf,
|
|
28
|
-
STATE_WORD, boxItem, workView, drawingBox, drawingMark, drawingPoint, itemRect, overlayConfig, pollEvery, SNAPSHOT_TOPIC, snapshotOf, talkLine, segmentsOf, sendBody, smallestAt, ssbLinkOf,
|
|
31
|
+
STATE_WORD, boxItem, bundleOriginOf, elsewhereLine, workView, drawingBox, drawingMark, drawingPoint, itemRect, overlayConfig, pollEvery, SNAPSHOT_TOPIC, snapshotOf, talkLine, segmentsOf, sendBody, smallestAt, ssbLinkOf,
|
|
29
32
|
} from './core.js';
|
|
30
33
|
|
|
31
34
|
const h = React.createElement;
|
|
@@ -65,11 +68,16 @@ const frames = (n = 2) => new Promise((resolve) => {
|
|
|
65
68
|
step(n);
|
|
66
69
|
});
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
// Where this app's JS was loaded from: a preview's tunnel, or a file.
|
|
72
|
+
const scriptURL = () => {
|
|
73
|
+
try { const sc = NativeModules.SourceCode; return sc?.getConstants?.()?.scriptURL ?? sc?.scriptURL ?? ''; } catch { return ''; }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function SsbHost({ appId, backend, api, getToken, sessionKey, route, capture, storage, pushToken, notifications, voice, insets, bundle, children }) {
|
|
69
77
|
const config = useMemo(() => overlayConfig({ appId, backend, api }), [appId, backend, api]);
|
|
70
78
|
useSsbLinks(notifications);
|
|
71
79
|
if (!config || typeof getToken !== 'function') return children ?? null;
|
|
72
|
-
return h(Host, { config, getToken, sessionKey, route, capture, storage, pushToken, voice, insets: { ...DEFAULT_INSETS, ...(insets || {}) } }, children);
|
|
80
|
+
return h(Host, { config, getToken, sessionKey, route, capture, storage, pushToken, voice, bundle, insets: { ...DEFAULT_INSETS, ...(insets || {}) } }, children);
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
// A tapped "Ready to see" notification (functions/in-app-notify.mjs) opens
|
|
@@ -85,7 +93,12 @@ function useSsbLinks(notifications) {
|
|
|
85
93
|
}, [notifications]);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
function Host({ config, getToken, sessionKey, route, capture, storage, pushToken, voice, insets, children }) {
|
|
96
|
+
function Host({ config, getToken, sessionKey, route, capture, storage, pushToken, voice, insets, bundle, children }) {
|
|
97
|
+
// WHICH BUILD THIS IS (docs/IN-APP-SSB.md "Ready is this build"): Ready is
|
|
98
|
+
// only what this bundle holds. `bundle` overrides (the harness); else the
|
|
99
|
+
// origin the JS came from.
|
|
100
|
+
const here = useMemo(() => (bundle !== undefined ? String(bundle || '') : bundleOriginOf(scriptURL())), [bundle]);
|
|
101
|
+
const [elsewhere, setElsewhere] = useState([]);
|
|
89
102
|
const screen = useWindowDimensions();
|
|
90
103
|
const tokenRef = useRef(getToken);
|
|
91
104
|
tokenRef.current = getToken;
|
|
@@ -181,7 +194,18 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
181
194
|
const open = mode !== 'idle';
|
|
182
195
|
useEffect(() => { if (open) load(); }, [open, chatId]);
|
|
183
196
|
// The top of the panel: the app's work, asked when it opens and while it stays open.
|
|
184
|
-
const loadWork = useCallback(async () => {
|
|
197
|
+
const loadWork = useCallback(async () => {
|
|
198
|
+
try { const r = await ssb.work(here); setWork(r?.inFlight || []); setElsewhere(r?.readyElsewhere || []); } catch { /* the last list stands */ }
|
|
199
|
+
}, [ssb, here]);
|
|
200
|
+
// ▶ Demo: one demo of everything Ready in this build, played by this build.
|
|
201
|
+
const startDemo = useCallback(async () => {
|
|
202
|
+
setBusy(true); setError(null);
|
|
203
|
+
try {
|
|
204
|
+
const r = await ssb.demo(here);
|
|
205
|
+
if (!playDemo({ id: r.demoId, token: r.token, api: config.api, here: true })) throw new Error('the demo could not start');
|
|
206
|
+
Keyboard.dismiss(); setMode('idle');
|
|
207
|
+
} catch (e) { setError(e.message); } finally { setBusy(false); }
|
|
208
|
+
}, [ssb, here, config.api]);
|
|
185
209
|
useEffect(() => {
|
|
186
210
|
if (mode !== 'sheet' || !gate) return undefined;
|
|
187
211
|
loadWork();
|
|
@@ -445,7 +469,8 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
445
469
|
h(Pressable, { testID: 'ssb-draw-send', onPress: sendDrawing, disabled: !note.trim() || busy, accessibilityRole: 'button', accessibilityLabel: 'Send to SSB', style: [s.sendRound, (!note.trim() || busy) && s.disabled] }, h(Text, { style: s.sendRoundText }, '→'))) : null);
|
|
446
470
|
}
|
|
447
471
|
if (mode === 'sheet') return h(Panel, {
|
|
448
|
-
gate, chat, work, screen, insets, draft, setDraft, busy, error,
|
|
472
|
+
gate, chat, work, screen, insets, draft, setDraft, busy, error, elsewhere,
|
|
473
|
+
onDemo: typeof playDemo === 'function' ? startDemo : null,
|
|
449
474
|
onSend: sendDraft, onDraw: () => { Keyboard.dismiss(); setMode('drawing'); }, onClose: close,
|
|
450
475
|
canTalk: !!voice?.Room, talk, onTalk: startTalk, onEndTalk: endTalk, onMute: toggleMute,
|
|
451
476
|
onNew: () => { rememberChat(null); setChat(null); }, onDecide: decide, onStartAll: startAll, onOpenLink: openLink,
|
|
@@ -458,14 +483,16 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
458
483
|
h(View, { ref: chromeRef, collapsable: false, pointerEvents: 'box-none', style: StyleSheet.absoluteFill }, chrome));
|
|
459
484
|
}
|
|
460
485
|
|
|
461
|
-
function Panel({ gate, chat, work, screen, insets, draft, setDraft, busy, error, onSend, onDraw, onClose, onNew, onDecide, onStartAll, onOpenLink, canTalk, talk, onTalk, onEndTalk, onMute }) {
|
|
486
|
+
function Panel({ gate, chat, work, elsewhere, onDemo, screen, insets, draft, setDraft, busy, error, onSend, onDraw, onClose, onNew, onDecide, onStartAll, onOpenLink, canTalk, talk, onTalk, onEndTalk, onMute }) {
|
|
462
487
|
const scroll = useRef(null);
|
|
463
488
|
const [open, setOpen] = useState(null); // the draft whose detail is shown
|
|
464
489
|
const { flying, waiting, sent } = workView({ inFlight: work, chat });
|
|
465
490
|
const offer = offerOf(chat);
|
|
466
491
|
const banner = demoBanner(chat);
|
|
467
492
|
const msgs = chat?.messages || [];
|
|
468
|
-
const
|
|
493
|
+
const ready = flying.filter((x) => x.state === 'ready');
|
|
494
|
+
const away = elsewhereLine(elsewhere);
|
|
495
|
+
const hasWork = flying.length || waiting.length || sent.length || banner || away;
|
|
469
496
|
const showMe = (x) => (x.state === 'ready' && banner?.url ? banner.url : null);
|
|
470
497
|
return h(View, { style: [s.panel, { paddingTop: insets.top }], testID: 'ssb-panel' },
|
|
471
498
|
h(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, style: { flex: 1 } },
|
|
@@ -482,7 +509,10 @@ function Panel({ gate, chat, work, screen, insets, draft, setDraft, busy, error,
|
|
|
482
509
|
testID: 'ssb-demo', disabled: !banner.url, onPress: () => onOpenLink(banner.url),
|
|
483
510
|
accessibilityRole: banner.url ? 'button' : 'text', style: [s.demo, banner.url && s.demoReady],
|
|
484
511
|
}, h(Text, { style: [s.demoText, banner.url && { color: '#fff' }] }, banner.text), banner.url ? h(Text, { style: s.demoGo }, '▶ See it') : null) : null,
|
|
485
|
-
flying.length ? h(
|
|
512
|
+
flying.length ? h(View, { style: s.sectionRow },
|
|
513
|
+
h(Text, { style: [s.section, { flex: 1 }], testID: 'ssb-section-flight' }, 'In flight'),
|
|
514
|
+
ready.length && onDemo ? h(Pressable, { testID: 'ssb-demo-ready', onPress: onDemo, disabled: busy, accessibilityRole: 'button', accessibilityLabel: `Demo what is ready in this build (${ready.length})`, style: [s.primary, s.small, busy && s.disabled] },
|
|
515
|
+
h(Text, { style: s.primaryText }, `▶ Demo (${ready.length})`)) : null) : null,
|
|
486
516
|
// A row opens its job in the SimonSays app, the console when that app
|
|
487
517
|
// is not on the phone (functions/in-app.mjs jobLinksFor) — for those
|
|
488
518
|
// who can open it there; anyone else's rows stay plain.
|
|
@@ -494,6 +524,7 @@ function Panel({ gate, chat, work, screen, insets, draft, setDraft, busy, error,
|
|
|
494
524
|
showMe(x)
|
|
495
525
|
? h(Pressable, { onPress: () => onOpenLink(showMe(x)), accessibilityRole: 'button', accessibilityLabel: `Show me: ${x.title}`, style: [s.primary, s.small] }, h(Text, { style: s.primaryText }, '▶ Show me'))
|
|
496
526
|
: h(Text, { style: [s.state, s[`state_${x.state}`]] }, STATE_WORD[x.state] || x.state))),
|
|
527
|
+
away ? h(Text, { style: [s.draftBody, { paddingHorizontal: 16, paddingVertical: 8 }], testID: 'ssb-ready-elsewhere' }, away) : null,
|
|
497
528
|
waiting.length ? h(View, { style: s.sectionRow },
|
|
498
529
|
h(Text, { style: [s.section, { flex: 1 }] }, 'Drafts'),
|
|
499
530
|
waiting.length > 1 ? h(Pressable, { testID: 'ssb-start-all', onPress: onStartAll, disabled: busy, accessibilityRole: 'button', accessibilityLabel: 'Start all', style: [s.primary, s.small, busy && s.disabled] }, h(Text, { style: s.primaryText }, `Start all (${waiting.length})`)) : null) : null,
|
package/src/core.js
CHANGED
|
@@ -43,7 +43,10 @@ export function inAppApi(config, getToken, fetchImpl = globalThis.fetch) {
|
|
|
43
43
|
decide: (chatId, proposalId, action, body) => post(`/chats/${encodeURIComponent(chatId)}/proposals/${encodeURIComponent(proposalId)}/${action}`, body),
|
|
44
44
|
pushToken: (token, platform) => post('/push-token', { token, platform }),
|
|
45
45
|
voice: (body) => post('/voice', body),
|
|
46
|
-
|
|
46
|
+
// `bundle`: where this app's JS came from (a preview's tunnel), so SSB
|
|
47
|
+
// scopes Ready to this build; undefined = the whole list, as before.
|
|
48
|
+
work: (bundle) => call(bundle === undefined ? '/work' : `/work?bundle=${encodeURIComponent(bundle)}`),
|
|
49
|
+
demo: (bundle) => post('/demo', { bundle }),
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -308,3 +311,17 @@ export function workView({ inFlight = [], chat = null } = {}) {
|
|
|
308
311
|
const { waiting, sent } = draftsOf(chat);
|
|
309
312
|
return { flying, waiting, sent: sent.filter((p) => !p.jobId || !flyingIds.has(p.jobId)) };
|
|
310
313
|
}
|
|
314
|
+
|
|
315
|
+
// READY IS THIS BUILD (2026-10-20, docs/IN-APP-SSB.md): the origin the app's
|
|
316
|
+
// JS bundle was loaded from — a preview's tunnel — or "" for a build that
|
|
317
|
+
// loads from a file (a store or local build, which holds no SSB preview).
|
|
318
|
+
export function bundleOriginOf(scriptURL) {
|
|
319
|
+
const m = /^(https?:\/\/[^/?#]+)/i.exec(String(scriptURL || ''));
|
|
320
|
+
return m ? m[1] : '';
|
|
321
|
+
}
|
|
322
|
+
// What the overlay says about Ready work this build does not hold.
|
|
323
|
+
export function elsewhereLine(readyElsewhere = []) {
|
|
324
|
+
const n = (readyElsewhere || []).reduce((t, x) => t + (Number(x?.count) || 0), 0);
|
|
325
|
+
if (!n) return '';
|
|
326
|
+
return `${n === 1 ? '1 more change is' : `${n} more changes are`} ready in another build — open its preview to see ${n === 1 ? 'it' : 'them'}.`;
|
|
327
|
+
}
|