fix(amber): send crew messages one signer intent at a time
Sending to a crew fired every recipient's gift wrap in parallel (Promise.all), and the sealed-relay crew chat encrypted to the whole roster in parallel. Amber is a single foreground activity: concurrent nostrsigner: intents mean only one is answered and the rest sit until the 60s native timeout rejects with 'Amber didn't respond'. Both paths now go sequentially for an interactive signer (parallel kept for silent signers). Also corrected the approval hint (2 per member: encrypt + sign, not 1 per recipient).
This commit is contained in:
parent
6c10d880c6
commit
27084a4e7b
5 changed files with 34 additions and 10 deletions
13
AGENTS.md
13
AGENTS.md
|
|
@ -512,7 +512,18 @@ session with **no other memory of this project**. So:
|
||||||
**legacy/unsigned** posts in a 30 s loop with **no interactive gate** — it now
|
**legacy/unsigned** posts in a 30 s loop with **no interactive gate** — it now
|
||||||
skips unsigned posts for interactive signers (pre-signed ones still publish).
|
skips unsigned posts for interactive signers (pre-signed ones still publish).
|
||||||
⚠️ If you add a new read hook that decrypts, gate it the same way or Amber
|
⚠️ If you add a new read hook that decrypts, gate it the same way or Amber
|
||||||
will storm on every navigation. (Not yet done: a persisted decrypt cache, so
|
will storm on every navigation. ⚠️ **Never fire concurrent signer intents with
|
||||||
|
an interactive signer (fixed 2026-10-13).** `useSendDirectMessage` created the
|
||||||
|
per-recipient gift wraps with `Promise.all`, and `sealGroupMessage`
|
||||||
|
(`lib/cordn/crypto.ts`) encrypted to the whole roster with `Promise.all`. Amber
|
||||||
|
is a single foreground activity: several `nostrsigner:` intents at once means
|
||||||
|
only one is answered, and the others sit until the **60 s** `TIMEOUT_MS` in
|
||||||
|
`android-templates/AmberIntentPlugin.java` rejects with the misleading *"Amber
|
||||||
|
didn't respond. Make sure Amber is installed"*. Both now send **sequentially
|
||||||
|
for an interactive signer** (parallel kept for silent ones); the NIP-17 crew
|
||||||
|
Chat therefore costs **2 approvals per member** (encrypt + sign). Steering
|
||||||
|
Amber users to the **MLS Private chat** (≈1 approval per action) is the real
|
||||||
|
cure. (Not yet done: a persisted decrypt cache, so
|
||||||
the first load per app start still decrypts everything.)
|
the first load per app start still decrypts everything.)
|
||||||
- **Sasquatch AI guide** (`src/components/SasquatchGuide.tsx`): the chat
|
- **Sasquatch AI guide** (`src/components/SasquatchGuide.tsx`): the chat
|
||||||
queries public relays **directly** (`useCVMClient.ts`, 3 fixed relays) and
|
queries public relays **directly** (`useCVMClient.ts`, 3 fixed relays) and
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,7 @@ function createRumor(
|
||||||
export function useSendDirectMessage() {
|
export function useSendDirectMessage() {
|
||||||
const { nostr } = useNostr();
|
const { nostr } = useNostr();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const { logins } = useNostrLogin();
|
||||||
const { config } = useAppContext();
|
const { config } = useAppContext();
|
||||||
const { write } = useRelays();
|
const { write } = useRelays();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -292,7 +293,18 @@ export function useSendDirectMessage() {
|
||||||
|
|
||||||
// 3. Create gift wraps for all recipients AND sender (so sender sees own messages)
|
// 3. Create gift wraps for all recipients AND sender (so sender sees own messages)
|
||||||
const allTargets = [...new Set([...recipients, user.pubkey])];
|
const allTargets = [...new Set([...recipients, user.pubkey])];
|
||||||
const giftWraps = await Promise.all(allTargets.map(createSealAndWrap));
|
// Each wrap costs TWO interactive signer ops (encrypt the rumor, sign the
|
||||||
|
// seal). An interactive signer (Amber) opens a foreground activity per op —
|
||||||
|
// firing them in parallel launches several intents at once, only one gets
|
||||||
|
// answered, and the rest time out ("Amber didn't respond"). So for Amber we
|
||||||
|
// MUST do them one at a time; silent signers keep the fast parallel path.
|
||||||
|
const interactive = isInteractiveSigner(user, logins);
|
||||||
|
const giftWraps: NostrEvent[] = [];
|
||||||
|
if (interactive) {
|
||||||
|
for (const target of allTargets) giftWraps.push(await createSealAndWrap(target));
|
||||||
|
} else {
|
||||||
|
giftWraps.push(...(await Promise.all(allTargets.map(createSealAndWrap))));
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Work out where to deliver. NIP-17: publish the wrap to each
|
// 4. Work out where to deliver. NIP-17: publish the wrap to each
|
||||||
// RECIPIENT's inbox relays (their kind 10050), plus our own inbox so we
|
// RECIPIENT's inbox relays (their kind 10050), plus our own inbox so we
|
||||||
|
|
@ -401,7 +413,7 @@ export function useSendDirectMessage() {
|
||||||
queryClient.invalidateQueries({ queryKey: ['group-conversations'] });
|
queryClient.invalidateQueries({ queryKey: ['group-conversations'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [user, nostr, config, write, queryClient]);
|
}, [user, logins, nostr, config, write, queryClient]);
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ recipients, content, replyTo, subject }: {
|
mutationFn: async ({ recipients, content, replyTo, subject }: {
|
||||||
|
|
|
||||||
|
|
@ -4523,7 +4523,7 @@ settingsUi: {
|
||||||
defaultAction: 'send',
|
defaultAction: 'send',
|
||||||
multiBefore: 'Your signer will ask you to approve',
|
multiBefore: 'Your signer will ask you to approve',
|
||||||
multiTimes: '{{approvals}} times',
|
multiTimes: '{{approvals}} times',
|
||||||
multiAfter: 'to {{action}} — that\'s normal for encrypted messages (one approval per recipient). Approve each.',
|
multiAfter: 'to {{action}} — that\'s normal for encrypted messages (an encrypt + a signature per member). Approve each.',
|
||||||
single: 'Your signer will ask you to approve to {{action}}.',
|
single: 'Your signer will ask you to approve to {{action}}.',
|
||||||
},
|
},
|
||||||
amberCallback: {
|
amberCallback: {
|
||||||
|
|
|
||||||
|
|
@ -4523,7 +4523,7 @@ settingsUi: {
|
||||||
defaultAction: 'enviar',
|
defaultAction: 'enviar',
|
||||||
multiBefore: 'Tu firmador te pedirá que apruebes',
|
multiBefore: 'Tu firmador te pedirá que apruebes',
|
||||||
multiTimes: '{{approvals}} veces',
|
multiTimes: '{{approvals}} veces',
|
||||||
multiAfter: 'para {{action}} — es normal en los mensajes cifrados (una aprobación por destinatario). Aprueba cada una.',
|
multiAfter: 'para {{action}} — es normal en los mensajes cifrados (un cifrado y una firma por miembro). Aprueba cada una.',
|
||||||
single: 'Tu firmador te pedirá que apruebes para {{action}}.',
|
single: 'Tu firmador te pedirá que apruebes para {{action}}.',
|
||||||
},
|
},
|
||||||
amberCallback: {
|
amberCallback: {
|
||||||
|
|
|
||||||
|
|
@ -109,13 +109,14 @@ export async function sealGroupMessage(
|
||||||
const json = JSON.stringify(plaintext);
|
const json = JSON.stringify(plaintext);
|
||||||
|
|
||||||
// Encrypt once per unique recipient (roster ∪ {sender}).
|
// Encrypt once per unique recipient (roster ∪ {sender}).
|
||||||
|
// ⚠️ Serial, not Promise.all: with an interactive signer (Amber) each encrypt
|
||||||
|
// opens a foreground activity, and concurrent intents collide/time out
|
||||||
|
// ("Amber didn't respond"). For a silent signer the cost is negligible.
|
||||||
const targets = Array.from(new Set([senderPk.toLowerCase(), ...roster.map((r) => r.toLowerCase())]));
|
const targets = Array.from(new Set([senderPk.toLowerCase(), ...roster.map((r) => r.toLowerCase())]));
|
||||||
const recipients: Record<string, string> = {};
|
const recipients: Record<string, string> = {};
|
||||||
await Promise.all(
|
for (const pk of targets) {
|
||||||
targets.map(async (pk) => {
|
recipients[pk] = await signer.nip44.encrypt(pk, json);
|
||||||
recipients[pk] = await signer.nip44.encrypt(pk, json);
|
}
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { v: 2, alg: 'nip44-group-v2', recipients };
|
return { v: 2, alg: 'nip44-group-v2', recipients };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue