mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
fix(mobile): distinguish cancelled authorization from access denial
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -487,6 +487,37 @@ class ComposeBar extends HookConsumerWidget {
|
||||
visit == authorizationVisit.value &&
|
||||
authorizationRevision == draftRevision.value &&
|
||||
identical(config, ref.read(relayConfigProvider));
|
||||
void ensureAuthorizationCurrent() {
|
||||
if (!context.mounted) throw const _ComposeAuthorizationCancelled();
|
||||
if (visit != authorizationVisit.value ||
|
||||
!identical(config, ref.read(relayConfigProvider))) {
|
||||
throw StateError('Community changed during authorization');
|
||||
}
|
||||
if (authorizationRevision != draftRevision.value) {
|
||||
throw const _ComposeAuthorizationCancelled();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> authorize(Set<String> keys, {bool prepare = false}) async {
|
||||
if (keys.isEmpty) return;
|
||||
ensureAuthorizationCurrent();
|
||||
try {
|
||||
await authorizeAgentMentions(
|
||||
readAuthorization,
|
||||
keys,
|
||||
currentPubkey,
|
||||
channelId,
|
||||
isAuthorizationCurrent,
|
||||
prepare: prepare,
|
||||
);
|
||||
} catch (_) {
|
||||
// A stale request is cancellation, not an access decision.
|
||||
ensureAuthorizationCurrent();
|
||||
rethrow;
|
||||
}
|
||||
ensureAuthorizationCurrent();
|
||||
}
|
||||
|
||||
// Resolved before any await: see
|
||||
// `_reportSendCancelledByCommunitySwitch`.
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
@@ -533,14 +564,7 @@ class ComposeBar extends HookConsumerWidget {
|
||||
// Agent failures stop publication; the original draft keeps its keys.
|
||||
Future<void> addMentionedNonMembers() async {
|
||||
final keys = intendedAgentKeys.intersection(outgoing.pubkeys.toSet());
|
||||
await authorizeAgentMentions(
|
||||
readAuthorization,
|
||||
keys,
|
||||
currentPubkey,
|
||||
channelId,
|
||||
isAuthorizationCurrent,
|
||||
prepare: true,
|
||||
);
|
||||
await authorize(keys, prepare: true);
|
||||
await outgoing.addNonMembers(
|
||||
channelActions,
|
||||
scan: scan,
|
||||
@@ -551,13 +575,7 @@ class ComposeBar extends HookConsumerWidget {
|
||||
'Mention invitation failed. Draft kept; retry or remove the mention.',
|
||||
);
|
||||
}
|
||||
await authorizeAgentMentions(
|
||||
readAuthorization,
|
||||
keys,
|
||||
currentPubkey,
|
||||
channelId,
|
||||
isAuthorizationCurrent,
|
||||
);
|
||||
await authorize(keys);
|
||||
}
|
||||
|
||||
if (queuedAttachments.isEmpty) {
|
||||
@@ -643,9 +661,13 @@ class ComposeBar extends HookConsumerWidget {
|
||||
outgoing.pubkeys,
|
||||
mediaTags: [...payload.mediaTags, ...outgoing.referenceTags],
|
||||
);
|
||||
} on _ComposeAuthorizationCancelled {
|
||||
// Keep the newer draft without displaying a false access error.
|
||||
} catch (error) {
|
||||
if (cancellation.isCancelled) return;
|
||||
if (context.mounted) {
|
||||
if (error is StateError) {
|
||||
_reportSendCancelledByCommunitySwitch(messenger);
|
||||
} else if (context.mounted) {
|
||||
uploadError.value = _formatUploadError(error);
|
||||
}
|
||||
if (context.mounted &&
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
part of '../compose_bar.dart';
|
||||
|
||||
class _ComposeAuthorizationCancelled implements Exception {
|
||||
const _ComposeAuthorizationCancelled();
|
||||
}
|
||||
|
||||
Future<void> _sendTextOnlyDraft({
|
||||
required BuildContext context,
|
||||
required _MarkdownEditingController controller,
|
||||
@@ -49,6 +53,8 @@ Future<void> _sendTextOnlyDraft({
|
||||
outgoing.pubkeys,
|
||||
mediaTags: [...payload.mediaTags, ...outgoing.referenceTags],
|
||||
);
|
||||
} on _ComposeAuthorizationCancelled {
|
||||
restoreClearedDraft();
|
||||
} on StateError {
|
||||
restoreClearedDraft();
|
||||
_reportSendCancelledByCommunitySwitch(messenger);
|
||||
|
||||
@@ -70,57 +70,74 @@ void _publicationTests() {
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'editing while authorization waits cancels publication without overwriting new draft',
|
||||
(tester) async {
|
||||
final key = 'd' * 64;
|
||||
final signer = nostr.Keys.generate();
|
||||
final pending = Completer<List<AgentDirectoryEntry>>();
|
||||
var reads = 0;
|
||||
var sent = 0;
|
||||
await tester.pumpWidget(
|
||||
_buildComposeBar(
|
||||
uploadService: _testUploadService(signer.nsec),
|
||||
currentPubkey: signer.public,
|
||||
relayAgents: [_testAgent(key)],
|
||||
channels: [
|
||||
_makeCurrentChannel(channelType: 'dm'),
|
||||
_makeSharedMemberChannel(),
|
||||
],
|
||||
authorizationReader: (_, _, _, _) {
|
||||
reads++;
|
||||
return pending.future;
|
||||
},
|
||||
onSend: (_, _, {mediaTags = const []}) async {
|
||||
sent++;
|
||||
},
|
||||
),
|
||||
);
|
||||
await _expandComposer(tester);
|
||||
await tester.enterText(find.byType(TextField), '@hel');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Helper Bot'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'hello @Helper Bot');
|
||||
await tester.tap(find.byIcon(LucideIcons.arrowUp));
|
||||
await tester.pump();
|
||||
expect(reads, 1);
|
||||
await tester.enterText(find.byType(TextField), 'new intent');
|
||||
pending.complete([
|
||||
AgentDirectoryEntry(
|
||||
pubkey: key,
|
||||
respondTo: 'anyone',
|
||||
channelIds: ['channel-1'],
|
||||
),
|
||||
]);
|
||||
await tester.pumpAndSettle();
|
||||
expect(sent, 0);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField)).controller!.text,
|
||||
'new intent',
|
||||
);
|
||||
},
|
||||
);
|
||||
for (final switchCommunity in [false, true]) {
|
||||
testWidgets(
|
||||
'authorization cancellation reports scope change only: $switchCommunity',
|
||||
(tester) async {
|
||||
final key = 'd' * 64;
|
||||
final signer = nostr.Keys.generate();
|
||||
final pending = Completer<List<AgentDirectoryEntry>>();
|
||||
var reads = 0;
|
||||
var sent = 0;
|
||||
await tester.pumpWidget(
|
||||
_buildComposeBar(
|
||||
uploadService: _testUploadService(signer.nsec),
|
||||
currentPubkey: signer.public,
|
||||
relayConfig: () => _SwitchableRelayConfigNotifier(
|
||||
RelayConfig(baseUrl: 'https://relay.example', nsec: signer.nsec),
|
||||
),
|
||||
relayAgents: [_testAgent(key)],
|
||||
channels: [
|
||||
_makeCurrentChannel(channelType: 'dm'),
|
||||
_makeSharedMemberChannel(),
|
||||
],
|
||||
authorizationReader: (_, _, _, _) {
|
||||
reads++;
|
||||
return pending.future;
|
||||
},
|
||||
onSend: (_, _, {mediaTags = const []}) async {
|
||||
sent++;
|
||||
},
|
||||
),
|
||||
);
|
||||
await _expandComposer(tester);
|
||||
await tester.enterText(find.byType(TextField), '@hel');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Helper Bot'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'hello @Helper Bot');
|
||||
await tester.tap(find.byIcon(LucideIcons.arrowUp));
|
||||
await tester.pump();
|
||||
expect(reads, 1);
|
||||
if (switchCommunity) {
|
||||
ProviderScope.containerOf(tester.element(find.byType(ComposeBar)))
|
||||
.read(relayConfigProvider.notifier)
|
||||
.update(baseUrl: 'https://other.example', nsec: signer.nsec);
|
||||
await tester.pump();
|
||||
} else {
|
||||
await tester.enterText(find.byType(TextField), 'new intent');
|
||||
}
|
||||
pending.complete([
|
||||
AgentDirectoryEntry(
|
||||
pubkey: key,
|
||||
respondTo: 'anyone',
|
||||
channelIds: ['channel-1'],
|
||||
),
|
||||
]);
|
||||
await tester.pumpAndSettle();
|
||||
expect(sent, 0);
|
||||
expect(
|
||||
find.text('Message not sent: the community changed'),
|
||||
switchCommunity ? findsOneWidget : findsNothing,
|
||||
);
|
||||
expect(find.textContaining('Could not authorize'), findsNothing);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField)).controller!.text,
|
||||
switchCommunity ? '' : 'new intent',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
testWidgets('revocation after upload retains agent draft and attachment', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user