mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
fix(mobile): show agent DM typing and balance reply bubble padding (#8731)
Messages with a reply footer now keep even vertical text padding unless they have actual overlapping reactions. Agent typing indicators are now visible in both one-to-one and group DMs when the response is thread-scoped, regardless of Bestie selection. The typing fix originated in #8665 and now applies to every agent DM using the existing agent classification. It retains main's `visibleTypingProvider` filtering. Ordinary channels and human thread-typing signals retain their existing scope; voice recording is not included. Validation: required pre-push hooks passed, including mobile formatting, analysis, tests, native crypto package tests, and file-size checks. Flutter analysis and the full mobile suite pass (3,995 passed, 28 skipped). Widget coverage compares bubble heights with and without a reply footer and checks typing across Solo, ordinary channels, agent DMs, human DMs, and group DMs with agents. Marked ready for review at the owner’s request. Device verification remains pending. To try: open a DM with any agent, including one not selected as Bestie, while it handles a thread-scoped response and check for its avatar and three dots; repeat in a group DM with an agent; compare equal text in messages with and without a reply footer, with no reactions, and confirm equal bubble heights. The broad `just ci` check was stopped during the unrelated desktop Rust dependency build after the mobile pre-push checks passed; no full-repository CI pass is claimed. ### Before / after Flutter widget-harness snapshots with identical synthetic data; not native device captures. Before: the reply-footer bubble has excess bottom padding and the thread-scoped Bestie typing signal is hidden. After: both text bubbles have equal height, and Bestie’s avatar and three dots are visible. | Before | After | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -362,17 +362,6 @@ class _ResolvedChannelDetailPage extends HookConsumerWidget {
|
||||
initialForcedUnreadMessageIdsRef.value;
|
||||
// Ownership comes from the signing identity, not reloadable profile metadata.
|
||||
final currentPubkey = ref.watch(currentPubkeyProvider);
|
||||
// Me flattens replies into this timeline, so include their typing signals.
|
||||
// Ordinary channels keep thread activity inside the thread view.
|
||||
final typingEntries = ref
|
||||
.watch(
|
||||
visibleTypingProvider(
|
||||
channel.id,
|
||||
selfPubkey: currentPubkey,
|
||||
include: (e) => personalChat || e.threadHeadId == null,
|
||||
),
|
||||
)
|
||||
.entries;
|
||||
final baseChannel = listedChannel ?? channel;
|
||||
final resolvedChannel =
|
||||
detailsAsync.whenData(baseChannel.mergeDetails).value ?? baseChannel;
|
||||
@@ -453,6 +442,21 @@ class _ResolvedChannelDetailPage extends HookConsumerWidget {
|
||||
channelBotPubkeys:
|
||||
channelBotPubkeysState.asData?.value ?? const <String>{},
|
||||
);
|
||||
// Agent work is visible in every DM, even when its response is threaded.
|
||||
// Ordinary channels keep thread activity inside the thread view.
|
||||
final typingEntries = ref
|
||||
.watch(
|
||||
visibleTypingProvider(
|
||||
channel.id,
|
||||
selfPubkey: currentPubkey,
|
||||
include: (e) =>
|
||||
personalChat ||
|
||||
e.threadHeadId == null ||
|
||||
(resolvedChannel.isDm &&
|
||||
agentPubkeys.contains(e.pubkey.toLowerCase())),
|
||||
),
|
||||
)
|
||||
.entries;
|
||||
useEffect(() {
|
||||
if (sessionStatus != SessionStatus.connected ||
|
||||
identitySubscriptionPubkeys.isEmpty) {
|
||||
|
||||
@@ -349,8 +349,7 @@ class _MessageBubble extends HookConsumerWidget {
|
||||
content: message.content,
|
||||
bubble: true,
|
||||
hasReactions:
|
||||
message.reactions.isNotEmpty ||
|
||||
replyFooter != null,
|
||||
message.reactions.isNotEmpty,
|
||||
bubbleEnd: showAvatar,
|
||||
bubbleStart: showAuthor,
|
||||
ownMessage: ownMessage,
|
||||
|
||||
@@ -11120,14 +11120,39 @@ void main() {
|
||||
expect(findRichText('Here is the reply'), findsOneWidget);
|
||||
});
|
||||
|
||||
for (final personal in [true, false]) {
|
||||
testWidgets('reply-scoped typing is visible only in flat Me: $personal', (
|
||||
for (final scenario in [
|
||||
(personal: true, dm: false, agent: true, group: false, visible: true),
|
||||
(personal: false, dm: false, agent: true, group: false, visible: false),
|
||||
(personal: false, dm: true, agent: true, group: false, visible: true),
|
||||
(personal: false, dm: true, agent: false, group: false, visible: false),
|
||||
(personal: false, dm: true, agent: true, group: true, visible: true),
|
||||
]) {
|
||||
testWidgets('reply-scoped typing follows conversation scope: $scenario', (
|
||||
tester,
|
||||
) async {
|
||||
final typing = _FakeTypingNotifier(const []);
|
||||
await tester.pumpWidget(
|
||||
_buildTestable(
|
||||
personalChat: personal,
|
||||
personalChat: scenario.personal,
|
||||
knownAgentPubkeys: scenario.agent ? {'alice'} : <String>{},
|
||||
channel: scenario.dm
|
||||
? Channel(
|
||||
id: _channelId,
|
||||
name: 'DM',
|
||||
channelType: 'dm',
|
||||
visibility: 'private',
|
||||
description: '',
|
||||
createdBy: 'self',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: scenario.group ? 3 : 2,
|
||||
participantPubkeys: [
|
||||
'self',
|
||||
'alice',
|
||||
if (scenario.group) 'bob',
|
||||
],
|
||||
isMember: true,
|
||||
)
|
||||
: null,
|
||||
messages: [
|
||||
_textMsg(id: 'prompt', pubkey: 'self', content: 'Hello'),
|
||||
],
|
||||
@@ -11149,7 +11174,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byKey(const ValueKey('inline-agent-typing-alice')),
|
||||
personal ? findsOneWidget : findsNothing,
|
||||
scenario.visible ? findsOneWidget : findsNothing,
|
||||
);
|
||||
expect(find.text('Alice is typing…'), findsNothing);
|
||||
typing.setEntries(const []);
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
part of '../channel_detail_page_test.dart';
|
||||
|
||||
void bubbleLayoutTests() {
|
||||
testWidgets('reply footer does not add reaction padding inside the bubble', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestable(
|
||||
messages: [
|
||||
_textMsg(id: 'plain', pubkey: 'alice', content: 'Same text'),
|
||||
_textMsg(id: threadIdRoot, pubkey: 'alice', content: 'Same text'),
|
||||
_textMsg(
|
||||
id: threadIdR1,
|
||||
pubkey: 'bob',
|
||||
content: 'Reply',
|
||||
extraTags: [
|
||||
['e', threadIdRoot, '', 'reply'],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
Finder bubble(String id) => find
|
||||
.descendant(
|
||||
of: find.byKey(ValueKey('message-row-$id')),
|
||||
matching: find.byType(LocalSendBubbleSurface),
|
||||
)
|
||||
.first;
|
||||
expect(find.text('1 reply'), findsOneWidget);
|
||||
expect(
|
||||
tester.getSize(bubble(threadIdRoot)).height,
|
||||
closeTo(tester.getSize(bubble('plain')).height, 0.01),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final author in ['self', 'alice']) {
|
||||
testWidgets('date divider owns equal space around $author messages', (
|
||||
tester,
|
||||
|
||||
Reference in New Issue
Block a user