@simon-says-build/ssb-overlay 0.9.0 → 0.9.2
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/package.json +1 -1
- package/src/SsbHost.js +62 -15
- package/src/core.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simon-says-build/ssb-overlay",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "SSB inside a native app SimonSays controls: the SSB button for the people who change the app (screenshot, mark, ask for a change), and Help for everyone who uses it.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/SsbHost.js
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
// make it an option"). **Draw** puts a pencil and a box on the live screen:
|
|
14
14
|
// scribble on or box what should change (as much as it takes, Undo), say what,
|
|
15
15
|
// send — the drawing is captured ON the screen, with the field and our chrome
|
|
16
|
-
// hidden.
|
|
16
|
+
// hidden. The screen is FROZEN while they draw (2026-10-25): a picture of it,
|
|
17
|
+
// taken as Draw opens, covers the app, so the keyboard the field raises (an
|
|
18
|
+
// app's KeyboardAvoidingView), a scroll or a live update can't slide the app
|
|
19
|
+
// out from under the drawing. (It was **Mark**, one pin or one box, until 0.2.0.) SSB replies on the thread and proposes jobs; the drafts bar
|
|
17
20
|
// starts them. The same conversation is on the console.
|
|
18
21
|
//
|
|
19
22
|
// No JSX, so it needs nothing from the app's Babel setup beyond React Native's.
|
|
@@ -70,6 +73,16 @@ const frames = (n = 2) => new Promise((resolve) => {
|
|
|
70
73
|
step(n);
|
|
71
74
|
});
|
|
72
75
|
|
|
76
|
+
// Until the keyboard is down (and the app back where it sits without it), or
|
|
77
|
+
// half a second: a picture taken mid-slide would freeze the app shifted.
|
|
78
|
+
const keyboardGone = (up) => new Promise((resolve) => {
|
|
79
|
+
if (!up.current) { resolve(); return; }
|
|
80
|
+
let sub = null;
|
|
81
|
+
const done = () => { sub?.remove(); clearTimeout(t); frames(2).then(resolve); };
|
|
82
|
+
const t = setTimeout(done, 500);
|
|
83
|
+
sub = Keyboard.addListener('keyboardDidHide', done);
|
|
84
|
+
});
|
|
85
|
+
|
|
73
86
|
// Where this app's JS was loaded from: a preview's tunnel, or a file.
|
|
74
87
|
const scriptURL = () => {
|
|
75
88
|
try { const sc = NativeModules.SourceCode; return sc?.getConstants?.()?.scriptURL ?? sc?.scriptURL ?? ''; } catch { return ''; }
|
|
@@ -131,7 +144,9 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
131
144
|
const [gate, setGate] = useState(null); // {show, person, app, canJob} once SSB has answered
|
|
132
145
|
const [mode, setMode] = useState('idle'); // idle | sheet | drawing | capturing
|
|
133
146
|
const [shot, setShot] = useState(null); // the screen as it was when the button was tapped
|
|
147
|
+
const [frozen, setFrozen] = useState(null); // while drawing: { pic, hosts } — the screen held still under the pencil
|
|
134
148
|
const [items, setItems] = useState([]); // what is drawn: strokes and boxes, in screen points (core.js)
|
|
149
|
+
const [openShot, setOpenShot] = useState(null); // the message whose full screen is showing (its preview otherwise)
|
|
135
150
|
const [live, setLive] = useState(null); // the item under the finger
|
|
136
151
|
const liveRef = useRef(null);
|
|
137
152
|
const [tool, setTool] = useState('pencil'); // pencil | box
|
|
@@ -154,6 +169,8 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
154
169
|
const [work, setWork] = useState([]); // the app's work in flight (GET /api/in-app/work)
|
|
155
170
|
const [seen, setSeen] = useState(null);
|
|
156
171
|
const [kbTop, setKbTop] = useState(screen.height);
|
|
172
|
+
const kbUpRef = useRef(null);
|
|
173
|
+
kbUpRef.current = kbTop < screen.height; // is a keyboard up
|
|
157
174
|
|
|
158
175
|
// ---- the gate: ask once per login --------------------------------------
|
|
159
176
|
useEffect(() => {
|
|
@@ -197,7 +214,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
197
214
|
setShot(null);
|
|
198
215
|
if (mode === 'drawing') {
|
|
199
216
|
if (note.trim()) setDraft((d) => (d ? `${d}\n${note}` : note));
|
|
200
|
-
setItems([]); setNote(''); setMode('sheet');
|
|
217
|
+
setItems([]); setNote(''); setFrozen(null); setMode('sheet');
|
|
201
218
|
}
|
|
202
219
|
}, [route]);
|
|
203
220
|
|
|
@@ -269,7 +286,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
269
286
|
Keyboard.dismiss();
|
|
270
287
|
setSeen(lastReplyId(chat));
|
|
271
288
|
setSeenDemo(chat?.demo?.url ?? null);
|
|
272
|
-
setItems([]); setNote('');
|
|
289
|
+
setItems([]); setNote(''); setFrozen(null);
|
|
273
290
|
setMode('idle');
|
|
274
291
|
}, [chat]);
|
|
275
292
|
|
|
@@ -296,28 +313,51 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
296
313
|
}, [draft, busy, deliver, chatId, route, screen, shot]);
|
|
297
314
|
|
|
298
315
|
// What the app has under the scribbles, named the way the app names it.
|
|
299
|
-
|
|
300
|
-
|
|
316
|
+
// Measured as the screen freezes, so they match the picture the drawing is on.
|
|
317
|
+
const measureHosts = useCallback(async () => {
|
|
301
318
|
try {
|
|
302
319
|
const hosts = namedHosts(fiberOf(rootRef.current), fiberOf(chromeRef.current));
|
|
303
|
-
|
|
304
|
-
|
|
320
|
+
return await Promise.all(hosts.map(async (fiber) => ({ fiber, rect: await measure(fiber) })));
|
|
321
|
+
} catch { return null; }
|
|
322
|
+
}, []);
|
|
323
|
+
const elementUnder = useCallback(async (pt, held) => {
|
|
324
|
+
if (!pt) return null;
|
|
325
|
+
try {
|
|
326
|
+
const hit = smallestAt(held || (await measureHosts()) || [], pt);
|
|
305
327
|
return hit ? describeHost(hit.fiber) : null;
|
|
306
328
|
} catch { return null; }
|
|
329
|
+
}, [measureHosts]);
|
|
330
|
+
|
|
331
|
+
// ✎ Draw: the sheet steps aside, the keyboard goes (its app may have moved
|
|
332
|
+
// for it), and the screen is taken and held under the pencil. With no
|
|
333
|
+
// `capture`, they draw on the live screen as before.
|
|
334
|
+
const startDrawing = useCallback(async () => {
|
|
335
|
+
setError(null);
|
|
336
|
+
Keyboard.dismiss();
|
|
337
|
+
setMode('capturing');
|
|
338
|
+
await keyboardGone(kbUpRef);
|
|
339
|
+
const [pic, hosts] = await Promise.all([grab(), measureHosts()]);
|
|
340
|
+
setFrozen(pic ? { pic, hosts } : null);
|
|
341
|
+
if (pic) setShot(pic);
|
|
342
|
+
setMode('drawing');
|
|
343
|
+
}, [grab, measureHosts]);
|
|
344
|
+
const stopDrawing = useCallback(() => {
|
|
345
|
+
Keyboard.dismiss(); setItems([]); setFrozen(null); setMode('sheet');
|
|
307
346
|
}, []);
|
|
308
347
|
|
|
309
348
|
const sendDrawing = useCallback(async () => {
|
|
310
349
|
const text = note.trim();
|
|
311
350
|
if (!text || busy || !items.length) return;
|
|
312
351
|
Keyboard.dismiss();
|
|
313
|
-
const element = await elementUnder(drawingPoint(items));
|
|
314
|
-
setMode('capturing'); // only the drawing
|
|
352
|
+
const element = await elementUnder(drawingPoint(items), frozen?.hosts);
|
|
353
|
+
setMode('capturing'); // only the drawing (on the held screen) stays
|
|
315
354
|
const pic = await grab();
|
|
316
355
|
setMode('sheet');
|
|
356
|
+
setFrozen(null);
|
|
317
357
|
const ok = await deliver(sendBody({ chatId, text, route, platform: Platform.OS, screen, mark: drawingMark(items, screen), element, screenshot: pic || shot }));
|
|
318
358
|
if (ok) { setItems([]); setNote(''); }
|
|
319
359
|
else setDraft((d) => d || text);
|
|
320
|
-
}, [note, busy, items, elementUnder, grab, deliver, chatId, route, screen, shot]);
|
|
360
|
+
}, [note, busy, items, frozen, elementUnder, grab, deliver, chatId, route, screen, shot]);
|
|
321
361
|
|
|
322
362
|
// ---- 🎙 Talk ------------------------------------------------------------------
|
|
323
363
|
const endTalk = useCallback(async () => {
|
|
@@ -447,7 +487,9 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
447
487
|
|
|
448
488
|
const chrome = (() => {
|
|
449
489
|
if (!gate) return null;
|
|
450
|
-
|
|
490
|
+
// The held screen, under the drawing, in place of the live app.
|
|
491
|
+
const still = frozen ? h(Image, { source: { uri: frozen.pic }, style: StyleSheet.absoluteFill, resizeMode: 'stretch', testID: 'ssb-draw-frozen' }) : null;
|
|
492
|
+
if (mode === 'capturing') return items.length || still ? h(View, { style: StyleSheet.absoluteFill, pointerEvents: 'none' }, still, ...drawingViews(items)) : null;
|
|
451
493
|
if (mode === 'idle' && talk) {
|
|
452
494
|
return h(View, { style: [s.pill, { top: insets.top + 8 }], testID: 'ssb-talk-pill' },
|
|
453
495
|
h(View, { style: [s.talkDot, talk.status !== 'live' && { backgroundColor: MUTED }] }),
|
|
@@ -470,7 +512,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
470
512
|
return h(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none' },
|
|
471
513
|
// The surface first, so the hint and the field above it still take their own taps.
|
|
472
514
|
h(View, { style: StyleSheet.absoluteFill, ...pan.panHandlers, testID: 'ssb-draw-surface' },
|
|
473
|
-
...drawingViews(live ? [...items, live] : items)),
|
|
515
|
+
still, ...drawingViews(live ? [...items, live] : items)),
|
|
474
516
|
h(View, { style: [s.hint, { top: insets.top + 8 }] },
|
|
475
517
|
...['pencil', 'box'].map((t) => h(Pressable, {
|
|
476
518
|
key: t, onPress: () => setTool(t), accessibilityRole: 'button', accessibilityState: { selected: tool === t },
|
|
@@ -478,7 +520,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
478
520
|
}, h(Text, { style: [s.toolText, tool === t && s.toolTextOn] }, t === 'box' ? '▭ Box' : '✎ Pencil'))),
|
|
479
521
|
h(View, { style: s.hintSep }),
|
|
480
522
|
items.length ? h(Pressable, { onPress: () => setItems((all) => all.slice(0, -1)), accessibilityRole: 'button', accessibilityLabel: 'Undo the last one', style: s.hintBtn }, h(Text, { style: s.hintLink }, 'Undo')) : null,
|
|
481
|
-
h(Pressable, { onPress:
|
|
523
|
+
h(Pressable, { onPress: stopDrawing, accessibilityRole: 'button', accessibilityLabel: 'Stop drawing', style: s.hintBtn }, h(Text, { style: s.hintLink }, 'Cancel'))),
|
|
482
524
|
top != null ? h(View, { style: [s.field, { top }] },
|
|
483
525
|
h(TextInput, {
|
|
484
526
|
testID: 'ssb-draw-note', value: note, onChangeText: setNote, placeholder: 'What should change here?',
|
|
@@ -489,7 +531,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
489
531
|
if (mode === 'sheet') return h(Panel, {
|
|
490
532
|
gate, chat, work, screen, insets, draft, setDraft, busy, error, elsewhere,
|
|
491
533
|
onDemo: typeof playDemo === 'function' ? startDemo : null,
|
|
492
|
-
onSend: sendDraft, onDraw:
|
|
534
|
+
onSend: sendDraft, onDraw: startDrawing, onClose: close,
|
|
493
535
|
canTalk: !!voice?.Room, talk, onTalk: startTalk, onEndTalk: endTalk, onMute: toggleMute,
|
|
494
536
|
onNew: () => { rememberChat(null); setChat(null); }, onDecide: decide, onStartAll: startAll, onOpenLink: openLink,
|
|
495
537
|
});
|
|
@@ -626,7 +668,11 @@ function Panel({ gate, chat, work, elsewhere, onDemo, screen, insets, draft, set
|
|
|
626
668
|
h(BottomSheetScrollView, { ref: scroll, style: s.thread, contentContainerStyle: { paddingTop: 8, paddingBottom: footH + 8 }, keyboardShouldPersistTaps: 'handled', onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }) },
|
|
627
669
|
msgs.length ? null : h(Text, { style: s.empty }, 'Say what should change on this screen, or tap Draw to scribble on it.'),
|
|
628
670
|
...msgs.map((m) => h(View, { key: m.id, style: [s.bubble, m.role === 'user' ? s.mine : s.theirs] },
|
|
629
|
-
|
|
671
|
+
// The marked part as the preview; a tap shows the full screen
|
|
672
|
+
// (2026-10-23, functions/shot-crop.mjs — the console and the
|
|
673
|
+
// extension do the same).
|
|
674
|
+
m.image ? h(Pressable, { onPress: () => setOpenShot(openShot === m.id ? null : m.id), accessibilityRole: 'imagebutton', accessibilityLabel: openShot === m.id ? 'Show the marked part' : 'Show the full screen' },
|
|
675
|
+
h(Image, { source: { uri: openShot === m.id || !m.preview ? m.image : m.preview }, style: openShot === m.id ? s.shotFull : s.thumb, resizeMode: 'contain' })) : null,
|
|
630
676
|
h(Text, { style: m.role === 'user' ? s.mineText : s.theirsText }, m.text),
|
|
631
677
|
m.id === offer ? h(Pressable, { onPress: onStartAll, disabled: busy, style: [s.primary, { marginTop: 8, alignSelf: 'flex-start' }] }, h(Text, { style: s.primaryText }, 'Confirm and start work')) : null)),
|
|
632
678
|
chat?.thinking ? h(View, { style: [s.bubble, s.theirs, s.row] }, h(ActivityIndicator, { size: 'small', color: MUTED }), h(Text, { style: [s.theirsText, { marginLeft: 8 }] }, 'SSB is thinking…')) : null,
|
|
@@ -707,6 +753,7 @@ const s = StyleSheet.create({
|
|
|
707
753
|
mineText: { color: '#fff', fontSize: 15, lineHeight: 21 },
|
|
708
754
|
theirsText: { color: TEXT, fontSize: 15, lineHeight: 21 },
|
|
709
755
|
thumb: { width: 140, height: 220, borderRadius: 8, marginBottom: 6, backgroundColor: '#0006' },
|
|
756
|
+
shotFull: { width: 260, height: 460, borderRadius: 8, marginBottom: 6, backgroundColor: '#0006' },
|
|
710
757
|
error: { color: RED, fontSize: 13, paddingHorizontal: 16, paddingTop: 6 },
|
|
711
758
|
composer: { paddingHorizontal: 12, paddingTop: 10, borderTopWidth: 1, borderTopColor: LINE, backgroundColor: SURFACE },
|
|
712
759
|
input: { minHeight: 44, maxHeight: 120, fontSize: 16, color: TEXT, backgroundColor: BG, borderWidth: 1, borderColor: LINE, borderRadius: 14, paddingHorizontal: 13, paddingTop: 11, paddingBottom: 11 },
|
package/src/core.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export const DEFAULT_API = 'https://simonsays.build';
|
|
6
6
|
// What this overlay is, said when it registers for pushes (clientInfo below).
|
|
7
7
|
// Kept equal to package.json by test/core.test.mjs.
|
|
8
|
-
export const OVERLAY_VERSION = '0.9.
|
|
8
|
+
export const OVERLAY_VERSION = '0.9.2';
|
|
9
9
|
export const HOST = 5;
|
|
10
10
|
export const TEXT = 6;
|
|
11
11
|
const PIN_SLOP = 12; // a drag shorter than this is a tap: a pin, not a box
|