Files
buzz/mobile/lib/features/channels/inline_agent_typing.dart
klopez4212 120cbae8e5 fix(mobile): restore inline typing groups and keep working thread links (#8865)
Restores mobile’s shared inline typing bubble and animated avatar
cluster after #8765 replaced them with composer activity. Humans and
agents share the group, including the existing overflow badge and
reduced-motion behavior; fresh replies regain the bubble-to-message
handoff.

Keeps the working-agent reply link from #8754, including its current DM
support: a root message can show working agents and open its thread
before the first reply. Thread timelines now use the same inline typing
group after their last message, so reverting the redesign does not bring
back the old full-width typing bar. Archived and non-member
conversations retain their existing no-typing-indicator behavior.

This intentionally follows the requested mobile presentation rather than
buzz-app’s composer activity layout. VISION_MOBILE.md allows
platform-specific presentation. The composer’s “Working now” list is
removed with the redesign; the root-message thread links remain.

Validation:
- At `5df48ad4b`: `just mobile-install`, `just mobile-check` and `just
mobile-test` passed (4,036 Flutter tests passed, 30 skipped;
unconfigured-push checks and 27 crypto tests also passed). Repository
file-size checks passed.
- Required pre-push hooks passed, including a second full mobile
check/test run and the repository file-size gate. Hosted CI is pending.
- Extended the existing thread journey through opening an empty working
thread, mixed human/agent typing after a reply, clearing typing, and
Back. Existing coverage exercises avatar grouping, overflow, reduced
motion, reply handoff, and followed/detached thread scrolling.
- Agent self-review completed; no additional blockers found after
correcting inventory-only agent avatar shapes and waiting for timeline
layout before following new thread typing.
- Simulator component preview uses synthetic identities in a local,
uncommitted entrypoint. It is not evidence of a live relay exchange.

Human verification pending (draft):
1. In a channel or DM, have an agent work on a threaded reply. Confirm
the root’s working-agent reply link opens that thread before a reply
arrives.
2. Have multiple people/agents type. Confirm one inline avatar group and
dot bubble, including inside the opened thread, with no wide composer
bar.
3. Receive a fresh reply in the main conversation/Solo and check the
typing-to-message handoff. In a long thread, check typing while
following the latest message and while scrolled up, then navigate Back.

Component preview on iPhone 18 Pro (iOS 27.0 simulator). These are
production widgets in an uncommitted preview shell with synthetic
identities; surrounding screen chrome is not a full-app screenshot.

![Inline group together with the retained working thread
link](https://github.com/user-attachments/assets/28f57a27-9319-4d27-a965-24d0dca3b620)

![Five typers use the circular overflow
badge](https://github.com/user-attachments/assets/5912862e-ac5a-48f7-a79c-39c2e2231d24)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 09:09:30 -07:00

114 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../shared/mentions/agent_identity_provider.dart';
import '../../shared/profile/user_cache_provider.dart';
import '../../shared/theme/theme.dart';
import '../../shared/widgets/avatar_image.dart';
import 'agent_typing_bubble.dart';
import 'channel_identity_names_provider.dart';
import 'channel_typing_provider.dart';
import 'typing_avatar_cluster.dart';
/// One pending reply, shared by everyone currently typing.
class InlineAgentTyping extends ConsumerWidget {
const InlineAgentTyping({
super.key,
required this.channelId,
required this.entries,
this.previewAvatars = const {},
this.knownAgents = const {},
});
final String channelId;
final List<TypingEntry> entries;
final Map<String, Widget> previewAvatars;
/// Lowercase pubkeys known to be agents, such as the user's own.
final Set<String> knownAgents;
@override
Widget build(BuildContext context, WidgetRef ref) {
final keys = entries.map((entry) => entry.pubkey.toLowerCase()).toSet();
if (keys.isEmpty) return const SizedBox.shrink();
// Bound profile resolution and avatar construction independently of fanout.
final visibleKeys = keys.take(keys.length > 4 ? 3 : 4).toList();
final profiles = ref.watch(userCacheProvider);
final bots = ref.watch(agentMentionPubkeysProvider(channelId));
final labels = watchChannelIdentityLabels(
ref,
channelId,
visibleKeys.where((key) => !previewAvatars.containsKey(key)),
);
final name = previewAvatars.containsKey(visibleKeys.first)
? 'Demo agent'
: labels[visibleKeys.first] ?? 'Someone';
final title = keys.length == 1
? name
: '$name and ${keys.length - 1} ${keys.length == 2 ? 'other' : 'others'}';
return Semantics(
liveRegion: true,
label: '$title ${keys.length == 1 ? 'is' : 'are'} typing',
child: ExcludeSemantics(
child: Padding(
padding: const EdgeInsets.only(top: Grid.xs, bottom: Grid.xxs),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
TypingAvatarCluster(
count: keys.length,
avatars: {
for (final key in visibleKeys)
key:
previewAvatars[key] ??
AvatarImage(
key: ValueKey('inline-agent-typing-$key'),
imageUrl:
(profiles[key] ??
ref
.read(userCacheProvider.notifier)
.get(key))
?.avatarUrl,
radius: messageAvatarSize / 2,
isAgent:
knownAgents.contains(key) ||
bots.contains(key) ||
(profiles[key]?.isAgent ?? false),
backgroundColor: context.controlSurface,
fallback: Text(
profiles[key]?.initial ??
key.substring(0, 1).toUpperCase(),
),
),
},
),
const SizedBox(width: messageAvatarContentGap),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: Grid.xs),
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: messageUsernameTextStyle.copyWith(
color: context.colors.onSurface,
fontSize: 13.1,
height: 17 / 13.1,
),
),
),
const SizedBox(height: Grid.xxs),
const AgentTypingBubble(),
],
),
),
],
),
),
),
);
}
}