| … | … | @@ -24,6 +24,15 @@ export default function App () { |
| 24 | 24 | const [status, setStatus] = useState(null) |
| 25 | 25 | const [error, setError] = useState(null) |
| 26 | 26 | const [booting, setBooting] = useState('starting the node…') |
| 27 | // Pairing on demand, for a device that ALREADY has an identity. |
| 28 | // |
| 29 | // ⛔ HAVING AN IDENTITY IS NOT THE SAME AS BEING A MEMBER. The first cut of this shell showed the |
| 30 | // pairing screen only when there was no identity at all, which removed the one route to the ceremony |
| 31 | // for the case that actually needs it: a handset holding the DID and its signing key, but absent from |
| 32 | // the room's __members__, so every write is refused with "this room lists members and this device is |
| 33 | // not among them". Re-pairing from a roster-holding node is the FIX for that, and the app had no way |
| 34 | // to run it. The old UI reached pairing from settings for exactly this reason. |
| 35 | const [pairing, setPairing] = useState(false) |
| 27 | 36 | |
| 28 | 37 | // Boot is a named operation because it happens TWICE: once at launch, and again after pairing. The |
| 29 | 38 | // worklet reads `cfg.dids` only in bootMultiDevice (lib/node.js), so a node that has just been given |
| … | … | @@ -59,12 +68,14 @@ export default function App () { |
| 59 | 68 | // `signing` is false exactly when there is no identity config at all (lib/node.js only boots hpp when |
| 60 | 69 | // one exists), which is the fresh-install case this screen is for. |
| 61 | 70 | if (!status.favelaUrl) return <Center><Text style={s.err}>favela-web did not start — the UI has nothing to show</Text><Text style={s.dim}>{booting}</Text></Center> |
| 62 | | if (!status.signing) return <Pair client={client} onPaired={paired} /> |
| 71 | if (!status.signing || pairing) { |
| 72 | return <Pair client={client} onPaired={async (r) => { setPairing(false); await paired(r) }} /> |
| 73 | } |
| 63 | 74 | |
| 64 | | return <Web status={status} /> |
| 75 | return <Web status={status} onPair={() => setPairing(true)} /> |
| 65 | 76 | } |
| 66 | 77 | |
| 67 | | function Web ({ status }) { |
| 78 | function Web ({ status, onPair }) { |
| 68 | 79 | const ref = useRef(null) |
| 69 | 80 | const [canBack, setCanBack] = useState(false) |
| 70 | 81 | const [failed, setFailed] = useState(null) |
| … | … | @@ -132,6 +143,12 @@ function Web ({ status }) { |
| 132 | 143 | renderLoading={() => <Center><ActivityIndicator /></Center>} |
| 133 | 144 | style={s.fill} |
| 134 | 145 | /> |
| 146 | {/* The pairing affordance. Deliberately small and always present rather than hidden behind a state |
| 147 | this shell cannot observe: the app has no way to know whether the room has admitted it (that |
| 148 | lives in the worklet's StateBase), so gating the button on "not admitted" would need a new IPC |
| 149 | verb to answer a question the ceremony is harmless to run regardless. Goes away when pairing |
| 150 | moves into favela-web (design §4) and the web UI owns this screen like every other. */} |
| 151 | {onPair ? <Pressable onPress={onPair} hitSlop={12} style={s.pair}><Text style={s.pairTxt}>pair</Text></Pressable> : null} |
| 135 | 152 | </SafeAreaView> |
| 136 | 153 | ) |
| 137 | 154 | } |
| … | … | @@ -140,5 +157,9 @@ const s = StyleSheet.create({ |
| 140 | 157 | fill: { flex: 1 }, |
| 141 | 158 | err: { color: '#b00020', padding: 16, textAlign: 'center' }, |
| 142 | 159 | dim: { color: '#666', padding: 8, textAlign: 'center' }, |
| 143 | | retry: { color: '#2563eb', padding: 12, fontSize: 16 } |
| 160 | retry: { color: '#2563eb', padding: 12, fontSize: 16 }, |
| 161 | // Bottom-LEFT: compose and the feed's own controls live bottom-right/centre in favela-web, and an |
| 162 | // overlay that covers a button the page owns is worse than no overlay. |
| 163 | pair: { position: 'absolute', left: 10, bottom: 14, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 14, backgroundColor: 'rgba(120,120,128,0.28)' }, |
| 164 | pairTxt: { fontSize: 11, color: '#fff', opacity: 0.9 } |
| 144 | 165 | }) |