@tapthrough/expo 0.1.1 → 0.1.4
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/dist/Bubble.d.ts +2 -0
- package/dist/Bubble.d.ts.map +1 -1
- package/dist/Bubble.js +9 -2
- package/dist/Bubble.js.map +1 -1
- package/dist/Feedback.d.ts.map +1 -1
- package/dist/Feedback.js +44 -6
- package/dist/Feedback.js.map +1 -1
- package/dist/Popover.d.ts +1 -0
- package/dist/Popover.d.ts.map +1 -1
- package/dist/Popover.js +25 -5
- package/dist/Popover.js.map +1 -1
- package/dist/core/feedback.d.ts +7 -1
- package/dist/core/feedback.d.ts.map +1 -1
- package/dist/core/feedback.js +12 -0
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/ideas.d.ts +17 -1
- package/dist/core/ideas.d.ts.map +1 -1
- package/dist/core/ideas.js +31 -2
- package/dist/core/ideas.js.map +1 -1
- package/package.json +1 -1
- package/src/Bubble.tsx +13 -2
- package/src/Feedback.tsx +81 -5
- package/src/Popover.tsx +76 -6
- package/src/core/feedback.ts +13 -2
- package/src/core/ideas.ts +41 -3
package/src/Feedback.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import type { Annotation } from './core/annotations.js'
|
|
|
24
24
|
import { defaultPosition, type Point } from './core/bubble.js'
|
|
25
25
|
import { createDiagnostics, installDiagnostics } from './core/diagnostics.js'
|
|
26
26
|
import {
|
|
27
|
+
buttonBadge,
|
|
27
28
|
type Checklist,
|
|
28
29
|
type ChecklistStep,
|
|
29
30
|
failedStepBody,
|
|
@@ -35,7 +36,6 @@ import {
|
|
|
35
36
|
type RecheckItem,
|
|
36
37
|
requestFix,
|
|
37
38
|
requestLaunch,
|
|
38
|
-
reviewBadge,
|
|
39
39
|
sendCause,
|
|
40
40
|
sendFeedback,
|
|
41
41
|
sendRecheck,
|
|
@@ -53,7 +53,9 @@ import {
|
|
|
53
53
|
ideaAction,
|
|
54
54
|
ideasNeedingYou,
|
|
55
55
|
isMoving,
|
|
56
|
+
newlyWaiting,
|
|
56
57
|
type OpenPr,
|
|
58
|
+
replyToIdea,
|
|
57
59
|
sendStaffReport,
|
|
58
60
|
submitIdea,
|
|
59
61
|
} from './core/ideas.js'
|
|
@@ -180,6 +182,10 @@ const IDEA_REFRESH_MS = 8_000
|
|
|
180
182
|
|
|
181
183
|
/** Update groups the reviewer has already been told about, so the toast shows once. */
|
|
182
184
|
const announcedBuilds = new Set<string>()
|
|
185
|
+
/** Ideas (and which round of questions) the reviewer has already been told about. */
|
|
186
|
+
const announcedIdeas = new Set<string>()
|
|
187
|
+
|
|
188
|
+
type Toast = { kind: 'build'; build: NewerBuild } | { kind: 'idea'; idea: Idea }
|
|
183
189
|
|
|
184
190
|
type Phase = 'idle' | 'loading' | 'open' | 'capturing' | 'composing'
|
|
185
191
|
|
|
@@ -223,7 +229,7 @@ export function ReportButton({
|
|
|
223
229
|
const [name, setName] = useState(rememberedName)
|
|
224
230
|
const [failing, setFailing] = useState<FailingStep | null>(null)
|
|
225
231
|
const [screenshot, setScreenshot] = useState<Screenshot | null>(null)
|
|
226
|
-
const [toast, setToast] = useState<
|
|
232
|
+
const [toast, setToast] = useState<Toast | null>(null)
|
|
227
233
|
const [ideas, setIdeas] = useState<Idea[]>([])
|
|
228
234
|
const [ideaBusy, setIdeaBusy] = useState<string | null>(null)
|
|
229
235
|
const [pulls, setPulls] = useState<OpenPr[] | null>(null)
|
|
@@ -262,10 +268,14 @@ export function ReportButton({
|
|
|
262
268
|
])
|
|
263
269
|
if (list) setChecklist(list)
|
|
264
270
|
setIdeas(recent)
|
|
271
|
+
const waiting = newlyWaiting(recent, announcedIdeas)
|
|
265
272
|
const newer = list?.newerBuild
|
|
266
273
|
if (newer && !announcedBuilds.has(newer.updateGroupId)) {
|
|
267
274
|
announcedBuilds.add(newer.updateGroupId)
|
|
268
|
-
setToast(newer)
|
|
275
|
+
setToast({ kind: 'build', build: newer })
|
|
276
|
+
} else if (waiting) {
|
|
277
|
+
haptics.light()
|
|
278
|
+
setToast({ kind: 'idea', idea: waiting })
|
|
269
279
|
}
|
|
270
280
|
return list
|
|
271
281
|
}
|
|
@@ -473,6 +483,15 @@ export function ReportButton({
|
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
486
|
+
/** From the idea banner: open the popover on that idea. */
|
|
487
|
+
function openIdea(id: string) {
|
|
488
|
+
setToast(null)
|
|
489
|
+
dismissKeyboard()
|
|
490
|
+
go({ view: 'idea', id })
|
|
491
|
+
setPhase('open')
|
|
492
|
+
void refresh()
|
|
493
|
+
}
|
|
494
|
+
|
|
476
495
|
function close() {
|
|
477
496
|
setPhase('idle')
|
|
478
497
|
setFull(false)
|
|
@@ -532,6 +551,24 @@ export function ReportButton({
|
|
|
532
551
|
replaceIdea(result.idea)
|
|
533
552
|
}
|
|
534
553
|
|
|
554
|
+
async function reply(id: string, text: string) {
|
|
555
|
+
if (!caller || !platform) return
|
|
556
|
+
rememberName()
|
|
557
|
+
setIdeaBusy(id)
|
|
558
|
+
const result = await replyToIdea(
|
|
559
|
+
{ caller, platform, id, reply: text, ...(rememberedName ? { by: rememberedName } : {}) },
|
|
560
|
+
{ apiUrl },
|
|
561
|
+
)
|
|
562
|
+
setIdeaBusy(null)
|
|
563
|
+
if (!result.ok) {
|
|
564
|
+
haptics.warning()
|
|
565
|
+
setError(result.message)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
haptics.success()
|
|
569
|
+
replaceIdea(result.idea)
|
|
570
|
+
}
|
|
571
|
+
|
|
535
572
|
async function act(id: string, action: 'build' | 'retry' | 'approve') {
|
|
536
573
|
if (!caller || !platform) return
|
|
537
574
|
rememberName()
|
|
@@ -600,7 +637,11 @@ export function ReportButton({
|
|
|
600
637
|
return (
|
|
601
638
|
<Overlay>
|
|
602
639
|
{toast && phase !== 'open' && phase !== 'composing' && phase !== 'capturing' ? (
|
|
603
|
-
|
|
640
|
+
toast.kind === 'build' ? (
|
|
641
|
+
<NewBuildToast build={toast.build} onPress={toggle} />
|
|
642
|
+
) : (
|
|
643
|
+
<IdeaToast idea={toast.idea} onPress={() => openIdea(toast.idea.id)} />
|
|
644
|
+
)
|
|
604
645
|
) : null}
|
|
605
646
|
{phase === 'open' && checklist ? (
|
|
606
647
|
<ReviewPopover
|
|
@@ -637,6 +678,7 @@ export function ReportButton({
|
|
|
637
678
|
ideas={ideas}
|
|
638
679
|
onShareIdea={() => void startReport(null, 'idea')}
|
|
639
680
|
onAnswerIdea={(id, answers) => void answer(id, answers)}
|
|
681
|
+
onReplyIdea={(id, text) => void reply(id, text)}
|
|
640
682
|
onIdeaAction={(id, action) => void act(id, action)}
|
|
641
683
|
ideaBusy={ideaBusy}
|
|
642
684
|
pulls={pulls}
|
|
@@ -653,7 +695,7 @@ export function ReportButton({
|
|
|
653
695
|
{showButton ? (
|
|
654
696
|
<Bubble
|
|
655
697
|
onPress={toggle}
|
|
656
|
-
badge={
|
|
698
|
+
badge={buttonBadge(checklist, ideasNeedingYou(ideas))}
|
|
657
699
|
busy={phase === 'loading'}
|
|
658
700
|
active={phase === 'open'}
|
|
659
701
|
onRest={setAnchor}
|
|
@@ -915,6 +957,38 @@ function NewBuildToast({ build, onPress }: { build: NewerBuild; onPress: () => v
|
|
|
915
957
|
)
|
|
916
958
|
}
|
|
917
959
|
|
|
960
|
+
/** "Claude has 3 questions" for a moment at the top; the bubble's count stays after. */
|
|
961
|
+
function IdeaToast({ idea, onPress }: { idea: Idea; onPress: () => void }) {
|
|
962
|
+
const colors = useColors()
|
|
963
|
+
const name = idea.title ?? idea.body.split(/\r?\n/)[0] ?? ''
|
|
964
|
+
return (
|
|
965
|
+
<View style={styles.toastWrap} pointerEvents="box-none">
|
|
966
|
+
<Pressable
|
|
967
|
+
onPress={onPress}
|
|
968
|
+
style={({ pressed }) => [pressed && styles.pressed]}
|
|
969
|
+
accessibilityRole="button"
|
|
970
|
+
accessibilityLiveRegion="polite"
|
|
971
|
+
accessibilityLabel={`${idea.statusLine}: ${name}. View`}
|
|
972
|
+
>
|
|
973
|
+
<Surface radius={24} style={[styles.toast, styles.ideaToast]}>
|
|
974
|
+
<View style={[styles.toastDot, { backgroundColor: COLORS.idea }]} />
|
|
975
|
+
<View style={styles.toastText}>
|
|
976
|
+
<Text style={[text.subhead, styles.strong, { color: colors.label }]} numberOfLines={1}>
|
|
977
|
+
{idea.statusLine}
|
|
978
|
+
</Text>
|
|
979
|
+
{name ? (
|
|
980
|
+
<Text style={[text.footnote, { color: colors.secondary }]} numberOfLines={1}>
|
|
981
|
+
{name}
|
|
982
|
+
</Text>
|
|
983
|
+
) : null}
|
|
984
|
+
</View>
|
|
985
|
+
<Text style={[text.subhead, styles.strong, { color: colors.tint }]}>View</Text>
|
|
986
|
+
</Surface>
|
|
987
|
+
</Pressable>
|
|
988
|
+
</View>
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
|
|
918
992
|
type Status =
|
|
919
993
|
| { kind: 'editing' }
|
|
920
994
|
| { kind: 'sending' }
|
|
@@ -1736,4 +1810,6 @@ const styles = StyleSheet.create({
|
|
|
1736
1810
|
paddingHorizontal: 18,
|
|
1737
1811
|
},
|
|
1738
1812
|
toastDot: { width: 8, height: 8, borderRadius: 4 },
|
|
1813
|
+
ideaToast: { maxWidth: 360, marginHorizontal: 16 },
|
|
1814
|
+
toastText: { flexShrink: 1 },
|
|
1739
1815
|
})
|
package/src/Popover.tsx
CHANGED
|
@@ -91,6 +91,7 @@ export interface ReviewPopoverProps {
|
|
|
91
91
|
ideas: Idea[]
|
|
92
92
|
onShareIdea: () => void
|
|
93
93
|
onAnswerIdea: (id: string, answers: string[]) => void
|
|
94
|
+
onReplyIdea: (id: string, reply: string) => void
|
|
94
95
|
onIdeaAction: (id: string, action: 'build' | 'retry' | 'approve') => void
|
|
95
96
|
/** The idea whose request is in flight, to show a spinner on its button. */
|
|
96
97
|
ideaBusy: string | null
|
|
@@ -320,8 +321,13 @@ function listView(p: ReviewPopoverProps, colors: Colors): Screen {
|
|
|
320
321
|
recheck.length ? `${recheck.length} to re-check` : null,
|
|
321
322
|
].filter(Boolean)
|
|
322
323
|
return {
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
// Say plainly which build this is: a PR's code, or the installed staging/TestFlight build.
|
|
325
|
+
title: p.staff ?? `PR #${checklist.pr}`,
|
|
326
|
+
subtitle: p.staff
|
|
327
|
+
? 'Not a PR build · send feedback and ideas'
|
|
328
|
+
: [checklist.sha ? checklist.sha.slice(0, 7) : null, counts.join(' · ') || 'Nothing planned']
|
|
329
|
+
.filter(Boolean)
|
|
330
|
+
.join(' · '),
|
|
325
331
|
body: (
|
|
326
332
|
<>
|
|
327
333
|
{checklist.newerBuild ? <NewerRow build={checklist.newerBuild} colors={colors} /> : null}
|
|
@@ -460,6 +466,15 @@ function ideaView(p: ReviewPopoverProps, idea: Idea, colors: Colors): Screen {
|
|
|
460
466
|
footer = (
|
|
461
467
|
<Capsule title="Build It" busy={busy} onPress={() => p.onIdeaAction(idea.id, 'build')} />
|
|
462
468
|
)
|
|
469
|
+
} else if (idea.actions.includes('approve') && p.staff) {
|
|
470
|
+
// A staging or TestFlight build can't load a PR's code; that takes the development build.
|
|
471
|
+
footer = idea.previewUrl ? (
|
|
472
|
+
<Capsule
|
|
473
|
+
title={`Open PR #${idea.prNumber} Page`}
|
|
474
|
+
kind="plain"
|
|
475
|
+
onPress={() => Linking.openURL(idea.previewUrl as string)}
|
|
476
|
+
/>
|
|
477
|
+
) : null
|
|
463
478
|
} else if (idea.actions.includes('approve')) {
|
|
464
479
|
footer = here ? (
|
|
465
480
|
<Capsule title="Approve" busy={busy} onPress={() => p.onIdeaAction(idea.id, 'approve')} />
|
|
@@ -533,7 +548,15 @@ function ideaView(p: ReviewPopoverProps, idea: Idea, colors: Colors): Screen {
|
|
|
533
548
|
colors={colors}
|
|
534
549
|
/>
|
|
535
550
|
) : null}
|
|
536
|
-
{idea.
|
|
551
|
+
{idea.actions.includes('reply') ? (
|
|
552
|
+
<IdeaReply
|
|
553
|
+
key={idea.id}
|
|
554
|
+
busy={busy}
|
|
555
|
+
onSend={(reply) => p.onReplyIdea(idea.id, reply)}
|
|
556
|
+
colors={colors}
|
|
557
|
+
/>
|
|
558
|
+
) : null}
|
|
559
|
+
{idea.steps.length && idea.status !== 'needs_answers' && idea.status !== 'declined' ? (
|
|
537
560
|
<Section title="How to test it" colors={colors}>
|
|
538
561
|
<Numbered
|
|
539
562
|
items={idea.steps.map((s) => ({ title: s.action, detail: s.expect }))}
|
|
@@ -543,9 +566,11 @@ function ideaView(p: ReviewPopoverProps, idea: Idea, colors: Colors): Screen {
|
|
|
543
566
|
) : null}
|
|
544
567
|
{idea.actions.includes('approve') ? (
|
|
545
568
|
<Text style={[text.footnote, { color: colors.secondary }]}>
|
|
546
|
-
{
|
|
547
|
-
?
|
|
548
|
-
:
|
|
569
|
+
{p.staff
|
|
570
|
+
? `You’re on ${p.staff}, not a PR build, so PR #${idea.prNumber} can’t open here. Open it on a phone with the development build, or in a simulator from the PR’s page. Approve it from there.`
|
|
571
|
+
: here
|
|
572
|
+
? 'You’re on this idea’s build. Try the steps; if it’s right, approve it.'
|
|
573
|
+
: 'Open its build, try the steps, then approve it from there.'}
|
|
549
574
|
</Text>
|
|
550
575
|
) : null}
|
|
551
576
|
<LinkRow
|
|
@@ -715,6 +740,51 @@ function IdeaAnswers({
|
|
|
715
740
|
)
|
|
716
741
|
}
|
|
717
742
|
|
|
743
|
+
/** After Claude stopped: one field to say how to go on, e.g. a smaller first version. */
|
|
744
|
+
function IdeaReply({
|
|
745
|
+
busy,
|
|
746
|
+
onSend,
|
|
747
|
+
colors,
|
|
748
|
+
}: {
|
|
749
|
+
busy: boolean
|
|
750
|
+
onSend: (reply: string) => void
|
|
751
|
+
colors: Colors
|
|
752
|
+
}) {
|
|
753
|
+
const [reply, setReply] = useState('')
|
|
754
|
+
return (
|
|
755
|
+
<View style={styles.sectionBlock}>
|
|
756
|
+
<View style={styles.question}>
|
|
757
|
+
<Text style={[text.subhead, styles.rowTitle, { color: colors.label }]}>
|
|
758
|
+
Tell Claude how to go on
|
|
759
|
+
</Text>
|
|
760
|
+
<Text style={[text.footnote, { color: colors.secondary }]}>
|
|
761
|
+
For example, a smaller first version it can build now. Claude writes it up again for you
|
|
762
|
+
to check before building.
|
|
763
|
+
</Text>
|
|
764
|
+
<TextInput
|
|
765
|
+
value={reply}
|
|
766
|
+
onChangeText={setReply}
|
|
767
|
+
placeholder="Your reply"
|
|
768
|
+
placeholderTextColor={colors.secondary}
|
|
769
|
+
multiline
|
|
770
|
+
style={[
|
|
771
|
+
text.subhead,
|
|
772
|
+
styles.nameInput,
|
|
773
|
+
{ color: colors.label, backgroundColor: colors.fill },
|
|
774
|
+
]}
|
|
775
|
+
accessibilityLabel="Tell Claude how to go on"
|
|
776
|
+
/>
|
|
777
|
+
</View>
|
|
778
|
+
<Capsule
|
|
779
|
+
title="Send to Claude"
|
|
780
|
+
busy={busy}
|
|
781
|
+
disabled={reply.trim() === ''}
|
|
782
|
+
onPress={() => onSend(reply.trim())}
|
|
783
|
+
/>
|
|
784
|
+
</View>
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
|
|
718
788
|
function prsView(p: ReviewPopoverProps, colors: Colors): Screen {
|
|
719
789
|
const { pulls } = p
|
|
720
790
|
return {
|
package/src/core/feedback.ts
CHANGED
|
@@ -292,8 +292,8 @@ export type Badge =
|
|
|
292
292
|
| { kind: 'failed' }
|
|
293
293
|
| { kind: 'remaining'; count: number }
|
|
294
294
|
| { kind: 'done' }
|
|
295
|
-
/**
|
|
296
|
-
| { kind: 'idea' }
|
|
295
|
+
/** Ideas waiting on someone: questions to answer, or a build to test. */
|
|
296
|
+
| { kind: 'idea'; count: number }
|
|
297
297
|
|
|
298
298
|
/** What the floating Review button shows: a new build wins, then failures, then steps left. */
|
|
299
299
|
export function reviewBadge(checklist: Checklist | null): Badge | null {
|
|
@@ -306,6 +306,17 @@ export function reviewBadge(checklist: Checklist | null): Badge | null {
|
|
|
306
306
|
return count === 0 ? { kind: 'done' } : { kind: 'remaining', count }
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
/**
|
|
310
|
+
* The button's badge with ideas counted in. An idea waiting on someone blocks
|
|
311
|
+
* Claude, so it outranks steps left; only a new build or a failed step wins.
|
|
312
|
+
*/
|
|
313
|
+
export function buttonBadge(checklist: Checklist | null, ideasWaiting: number): Badge | null {
|
|
314
|
+
const review = reviewBadge(checklist)
|
|
315
|
+
if (review?.kind === 'new-build' || review?.kind === 'failed') return review
|
|
316
|
+
if (ideasWaiting > 0) return { kind: 'idea', count: ideasWaiting }
|
|
317
|
+
return review
|
|
318
|
+
}
|
|
319
|
+
|
|
309
320
|
/** Marks an earlier report fixed or still broken on this build. Never throws. */
|
|
310
321
|
export async function sendRecheck(
|
|
311
322
|
input: {
|
package/src/core/ideas.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type IdeaStatus =
|
|
|
15
15
|
| 'failed'
|
|
16
16
|
| 'not_set_up'
|
|
17
17
|
|
|
18
|
-
export type IdeaAction = 'answer' | 'build' | 'approve' | 'retry'
|
|
18
|
+
export type IdeaAction = 'answer' | 'build' | 'approve' | 'retry' | 'reply'
|
|
19
19
|
|
|
20
20
|
/** One step from sent to merged: done, in progress, not reached yet, or where it stopped. */
|
|
21
21
|
export interface IdeaStage {
|
|
@@ -226,6 +226,26 @@ export function answerIdea(
|
|
|
226
226
|
)
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/** After Claude stopped: tell it how to go on, and it writes the idea up again. Never throws. */
|
|
230
|
+
export function replyToIdea(
|
|
231
|
+
input: { caller: Caller; platform: Platform; id: string; reply: string; by?: string },
|
|
232
|
+
options: Options,
|
|
233
|
+
) {
|
|
234
|
+
return call(
|
|
235
|
+
options,
|
|
236
|
+
`/api/v1/ideas/${encodeURIComponent(input.id)}/reply`,
|
|
237
|
+
{
|
|
238
|
+
method: 'POST',
|
|
239
|
+
body: {
|
|
240
|
+
...credential(input.caller, input.platform),
|
|
241
|
+
reply: input.reply,
|
|
242
|
+
...(input.by ? { by: input.by } : {}),
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
pickIdea,
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
229
249
|
/** Build It, Retry or Approve on one idea. Never throws. */
|
|
230
250
|
export function ideaAction(
|
|
231
251
|
input: {
|
|
@@ -259,10 +279,28 @@ export function ideaAction(
|
|
|
259
279
|
)
|
|
260
280
|
}
|
|
261
281
|
|
|
282
|
+
const WAITING: IdeaStatus[] = ['needs_answers', 'ready_to_test']
|
|
283
|
+
|
|
262
284
|
/** Ideas waiting on a person: questions to answer, or a build to test. */
|
|
263
285
|
export function ideasNeedingYou(ideas: Idea[]): number {
|
|
264
|
-
return ideas.filter((idea) =>
|
|
265
|
-
|
|
286
|
+
return ideas.filter((idea) => WAITING.includes(idea.status)).length
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The first idea that started waiting on someone since we last looked, to
|
|
291
|
+
* announce once. `seen` holds `id:status` pairs already announced; it's
|
|
292
|
+
* updated in place, so a new round of questions announces again.
|
|
293
|
+
*/
|
|
294
|
+
export function newlyWaiting(ideas: Idea[], seen: Set<string>): Idea | null {
|
|
295
|
+
let first: Idea | null = null
|
|
296
|
+
for (const idea of ideas) {
|
|
297
|
+
if (!WAITING.includes(idea.status)) continue
|
|
298
|
+
const key = `${idea.id}:${idea.status}:${idea.questions.join('\n')}`
|
|
299
|
+
if (seen.has(key)) continue
|
|
300
|
+
seen.add(key)
|
|
301
|
+
first ??= idea
|
|
302
|
+
}
|
|
303
|
+
return first
|
|
266
304
|
}
|
|
267
305
|
|
|
268
306
|
/** What Approve did, in plain words. */
|