Animate compact agent updates without tap expansion

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-21 21:47:43 -07:00
parent 7c60c0aee9
commit ea66f1fe1d
3 changed files with 73 additions and 23 deletions

View File

@@ -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(

View File

@@ -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<AgentReplyHandoff>(
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,

View File

@@ -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<MessageContent>(body()).bubble, isFalse);
expect(tester.widget<MessageContent>(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();