diff --git a/mobile/lib/features/channels/channel_detail_page/message_bubble.dart b/mobile/lib/features/channels/channel_detail_page/message_bubble.dart index 8e6d38aac5..0d18be649c 100644 --- a/mobile/lib/features/channels/channel_detail_page/message_bubble.dart +++ b/mobile/lib/features/channels/channel_detail_page/message_bubble.dart @@ -39,7 +39,6 @@ class _MessageBubble extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final coordinationExpanded = useState(false); final messageSnapshotKey = useMemoized(GlobalKey.new, const []); final swipeContentKey = useMemoized(GlobalKey.new, const []); final mediaOffset = useMessageMediaOffset(message.content, message.tags); @@ -141,7 +140,7 @@ class _MessageBubble extends HookConsumerWidget { ); } - if (message.isAgentCoordination && !coordinationExpanded.value) { + if (message.isAgentCoordination) { final collapsed = Padding( key: swipeContentKey, padding: const EdgeInsets.symmetric(vertical: Grid.xxs), @@ -151,13 +150,10 @@ class _MessageBubble extends HookConsumerWidget { key: ValueKey('message-row-${message.id}'), snapshotKey: messageSnapshotKey, onLongPressDetails: openMessageActions, - onTap: () => coordinationExpanded.value = true, child: RepaintBoundary( key: messageSnapshotKey, child: Semantics( - button: true, - expanded: false, - label: 'Agent update from $displayName. Show message', + label: 'Agent update from $displayName', child: Row( children: [ SizedBox( @@ -208,7 +204,11 @@ class _MessageBubble extends HookConsumerWidget { currentPubkey: currentPubkey, isMember: isMember, isArchived: isArchived, - child: collapsed, + child: AgentReplyHandoff( + key: ValueKey('reply-handoff-${message.id}'), + animate: typingHandoff, + child: collapsed, + ), ); } @@ -334,17 +334,6 @@ class _MessageBubble extends HookConsumerWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (message.isAgentCoordination) - TextButton( - key: ValueKey( - 'collapse-update-${message.id}', - ), - onPressed: () => - coordinationExpanded.value = false, - child: const Text( - 'Collapse agent update', - ), - ), if (showAuthor) Padding( padding: EdgeInsets.only( diff --git a/mobile/test/features/channels/channel_detail_page_test.dart b/mobile/test/features/channels/channel_detail_page_test.dart index 118e49bfe9..055030692e 100644 --- a/mobile/test/features/channels/channel_detail_page_test.dart +++ b/mobile/test/features/channels/channel_detail_page_test.dart @@ -11078,6 +11078,71 @@ void main() { expect(findRichText('Here is the reply'), findsOneWidget); }); + testWidgets('Me compact reply consumes typing and animates', ( + tester, + ) async { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final prompt = _textMsg( + id: threadIdRoot, + pubkey: 'self', + content: 'Hello', + createdAt: now - 3, + ); + final typing = _FakeTypingNotifier(const []); + await tester.pumpWidget( + _buildTestable( + personalChat: true, + messages: [prompt], + typingNotifier: typing, + users: const { + 'alice': UserProfile(pubkey: 'alice', displayName: 'Alice'), + }, + ), + ); + await tester.pumpAndSettle(); + typing.setEntries([ + TypingEntry( + pubkey: 'alice', + threadHeadId: threadIdRoot, + expiresAtMs: (now - 1) * 1000 + 8000, + ), + ]); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + expect(find.byKey(const ValueKey('agent-typing-bubble')), findsOneWidget); + final container = ProviderScope.containerOf( + tester.element(find.byType(ChannelDetailPage)), + ); + final messages = + container.read(meMessagesProvider(_channelId).notifier) + as _FakeMessagesNotifier; + messages.setMessages([ + prompt, + _textMsg( + id: 'reply', + pubkey: 'alice', + content: 'Here is the reply', + createdAt: now, + extraTags: [ + ['e', threadIdRoot, '', 'reply'], + ['audience', 'agents'], + ], + ), + ]); + await tester.pump(); + expect( + find.byKey(const ValueKey('inline-agent-typing-alice')), + findsNothing, + ); + final handoff = tester.widget( + find.byKey(const ValueKey('reply-handoff-reply')), + ); + expect(handoff.animate, isTrue); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('agent-typing-bubble')), findsNothing); + expect(find.text('Here is the reply'), findsOneWidget); + }); + for (final personal in [true, false]) { testWidgets('reply-scoped typing is visible only in flat Me: $personal', ( tester, diff --git a/mobile/test/features/channels/channel_detail_page_test/bubble_tests.dart b/mobile/test/features/channels/channel_detail_page_test/bubble_tests.dart index 9ea049df04..047eb5e235 100644 --- a/mobile/test/features/channels/channel_detail_page_test/bubble_tests.dart +++ b/mobile/test/features/channels/channel_detail_page_test/bubble_tests.dart @@ -307,7 +307,7 @@ void bubbleLayoutTests() { } testWidgets( - 'agent audience updates collapse without a bubble and expand on tap', + 'agent audience updates stay compact on tap and preserve swipe reply', (tester) async { await tester.binding.setSurfaceSize(const Size(390, 844)); addTearDown(() => tester.binding.setSurfaceSize(null)); @@ -362,10 +362,6 @@ void bubbleLayoutTests() { } await tester.tap(row); await tester.pumpAndSettle(); - expect(tester.widget(body()).bubble, isFalse); - expect(tester.widget(body()).content, update); - await tester.tap(find.byKey(const ValueKey('collapse-update-update'))); - await tester.pumpAndSettle(); expect(body(), findsNothing); await tester.drag(row, const Offset(150, 0)); await tester.pumpAndSettle();