@simon-says-build/ssb-overlay 0.9.1 → 0.9.3
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 -17
- 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.3",
|
|
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,6 +144,7 @@ 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)
|
|
135
149
|
const [openShot, setOpenShot] = useState(null); // the message whose full screen is showing (its preview otherwise)
|
|
136
150
|
const [live, setLive] = useState(null); // the item under the finger
|
|
@@ -155,6 +169,8 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
155
169
|
const [work, setWork] = useState([]); // the app's work in flight (GET /api/in-app/work)
|
|
156
170
|
const [seen, setSeen] = useState(null);
|
|
157
171
|
const [kbTop, setKbTop] = useState(screen.height);
|
|
172
|
+
const kbUpRef = useRef(null);
|
|
173
|
+
kbUpRef.current = kbTop < screen.height; // is a keyboard up
|
|
158
174
|
|
|
159
175
|
// ---- the gate: ask once per login --------------------------------------
|
|
160
176
|
useEffect(() => {
|
|
@@ -198,7 +214,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
198
214
|
setShot(null);
|
|
199
215
|
if (mode === 'drawing') {
|
|
200
216
|
if (note.trim()) setDraft((d) => (d ? `${d}\n${note}` : note));
|
|
201
|
-
setItems([]); setNote(''); setMode('sheet');
|
|
217
|
+
setItems([]); setNote(''); setFrozen(null); setMode('sheet');
|
|
202
218
|
}
|
|
203
219
|
}, [route]);
|
|
204
220
|
|
|
@@ -270,7 +286,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
270
286
|
Keyboard.dismiss();
|
|
271
287
|
setSeen(lastReplyId(chat));
|
|
272
288
|
setSeenDemo(chat?.demo?.url ?? null);
|
|
273
|
-
setItems([]); setNote('');
|
|
289
|
+
setItems([]); setNote(''); setFrozen(null);
|
|
274
290
|
setMode('idle');
|
|
275
291
|
}, [chat]);
|
|
276
292
|
|
|
@@ -297,28 +313,51 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
297
313
|
}, [draft, busy, deliver, chatId, route, screen, shot]);
|
|
298
314
|
|
|
299
315
|
// What the app has under the scribbles, named the way the app names it.
|
|
300
|
-
|
|
301
|
-
|
|
316
|
+
// Measured as the screen freezes, so they match the picture the drawing is on.
|
|
317
|
+
const measureHosts = useCallback(async () => {
|
|
302
318
|
try {
|
|
303
319
|
const hosts = namedHosts(fiberOf(rootRef.current), fiberOf(chromeRef.current));
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
306
327
|
return hit ? describeHost(hit.fiber) : null;
|
|
307
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');
|
|
308
346
|
}, []);
|
|
309
347
|
|
|
310
348
|
const sendDrawing = useCallback(async () => {
|
|
311
349
|
const text = note.trim();
|
|
312
350
|
if (!text || busy || !items.length) return;
|
|
313
351
|
Keyboard.dismiss();
|
|
314
|
-
const element = await elementUnder(drawingPoint(items));
|
|
315
|
-
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
|
|
316
354
|
const pic = await grab();
|
|
317
355
|
setMode('sheet');
|
|
356
|
+
setFrozen(null);
|
|
318
357
|
const ok = await deliver(sendBody({ chatId, text, route, platform: Platform.OS, screen, mark: drawingMark(items, screen), element, screenshot: pic || shot }));
|
|
319
358
|
if (ok) { setItems([]); setNote(''); }
|
|
320
359
|
else setDraft((d) => d || text);
|
|
321
|
-
}, [note, busy, items, elementUnder, grab, deliver, chatId, route, screen, shot]);
|
|
360
|
+
}, [note, busy, items, frozen, elementUnder, grab, deliver, chatId, route, screen, shot]);
|
|
322
361
|
|
|
323
362
|
// ---- 🎙 Talk ------------------------------------------------------------------
|
|
324
363
|
const endTalk = useCallback(async () => {
|
|
@@ -448,7 +487,9 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
448
487
|
|
|
449
488
|
const chrome = (() => {
|
|
450
489
|
if (!gate) return null;
|
|
451
|
-
|
|
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;
|
|
452
493
|
if (mode === 'idle' && talk) {
|
|
453
494
|
return h(View, { style: [s.pill, { top: insets.top + 8 }], testID: 'ssb-talk-pill' },
|
|
454
495
|
h(View, { style: [s.talkDot, talk.status !== 'live' && { backgroundColor: MUTED }] }),
|
|
@@ -471,15 +512,18 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
471
512
|
return h(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none' },
|
|
472
513
|
// The surface first, so the hint and the field above it still take their own taps.
|
|
473
514
|
h(View, { style: StyleSheet.absoluteFill, ...pan.panHandlers, testID: 'ssb-draw-surface' },
|
|
474
|
-
...drawingViews(live ? [...items, live] : items)),
|
|
515
|
+
still, ...drawingViews(live ? [...items, live] : items)),
|
|
475
516
|
h(View, { style: [s.hint, { top: insets.top + 8 }] },
|
|
476
517
|
...['pencil', 'box'].map((t) => h(Pressable, {
|
|
477
518
|
key: t, onPress: () => setTool(t), accessibilityRole: 'button', accessibilityState: { selected: tool === t },
|
|
478
519
|
accessibilityLabel: t === 'box' ? 'Box: drag a box round it' : 'Pencil: scribble on it', style: [s.toolBtn, tool === t && s.toolOn],
|
|
479
520
|
}, h(Text, { style: [s.toolText, tool === t && s.toolTextOn] }, t === 'box' ? '▭ Box' : '✎ Pencil'))),
|
|
480
521
|
h(View, { style: s.hintSep }),
|
|
481
|
-
|
|
482
|
-
|
|
522
|
+
// Undo and Cancel are buttons, always there (Michael, 2026-10-26: two
|
|
523
|
+
// small links, Undo only after a mark, read as missing). Undo takes
|
|
524
|
+
// the last mark off; Cancel drops them all and goes back to the panel.
|
|
525
|
+
h(Pressable, { testID: 'ssb-draw-undo', onPress: () => setItems((all) => all.slice(0, -1)), disabled: !items.length, accessibilityRole: 'button', accessibilityLabel: 'Undo the last one', accessibilityState: { disabled: !items.length }, style: [s.hintBtn, !items.length && s.disabled] }, h(Text, { style: s.hintBtnText }, '↶ Undo')),
|
|
526
|
+
h(Pressable, { testID: 'ssb-draw-cancel', onPress: stopDrawing, accessibilityRole: 'button', accessibilityLabel: 'Stop drawing', style: [s.hintBtn, s.hintCancel] }, h(Text, { style: s.hintBtnText }, '✕ Cancel'))),
|
|
483
527
|
top != null ? h(View, { style: [s.field, { top }] },
|
|
484
528
|
h(TextInput, {
|
|
485
529
|
testID: 'ssb-draw-note', value: note, onChangeText: setNote, placeholder: 'What should change here?',
|
|
@@ -490,7 +534,7 @@ function Host({ config, getToken, sessionKey, route, capture, storage, pushToken
|
|
|
490
534
|
if (mode === 'sheet') return h(Panel, {
|
|
491
535
|
gate, chat, work, screen, insets, draft, setDraft, busy, error, elsewhere,
|
|
492
536
|
onDemo: typeof playDemo === 'function' ? startDemo : null,
|
|
493
|
-
onSend: sendDraft, onDraw:
|
|
537
|
+
onSend: sendDraft, onDraw: startDrawing, onClose: close,
|
|
494
538
|
canTalk: !!voice?.Room, talk, onTalk: startTalk, onEndTalk: endTalk, onMute: toggleMute,
|
|
495
539
|
onNew: () => { rememberChat(null); setChat(null); }, onDecide: decide, onStartAll: startAll, onOpenLink: openLink,
|
|
496
540
|
});
|
|
@@ -659,8 +703,9 @@ const s = StyleSheet.create({
|
|
|
659
703
|
toolText: { color: TEXT, fontSize: 14, fontWeight: '600' },
|
|
660
704
|
toolTextOn: { color: '#fff' },
|
|
661
705
|
hintSep: { width: 1, height: 18, backgroundColor: LINE, marginHorizontal: 6 },
|
|
662
|
-
|
|
663
|
-
|
|
706
|
+
hintBtn: { paddingVertical: 5, paddingHorizontal: 10, borderRadius: 12, marginLeft: 4, borderWidth: 1, borderColor: LINE, backgroundColor: SURFACE },
|
|
707
|
+
hintCancel: { borderColor: 'rgba(255,77,90,0.5)' },
|
|
708
|
+
hintBtnText: { color: TEXT, fontSize: 14, fontWeight: '600' },
|
|
664
709
|
field: { position: 'absolute', left: 12, right: 12, height: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: 14, paddingRight: 6, borderRadius: 16, backgroundColor: SURFACE, borderWidth: 1, borderColor: LINE, ...shadow },
|
|
665
710
|
fieldInput: { flex: 1, fontSize: 16, color: TEXT },
|
|
666
711
|
sendRound: { width: 40, height: 40, borderRadius: 20, backgroundColor: BLUE, alignItems: 'center', justifyContent: 'center', marginLeft: 4, ...glow(BLUE, 10) },
|
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.3';
|
|
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
|