mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
fix(mobile): draw Huddle agents with agent avatars
The Huddle roster counted record-only and `bot` agents as agents, but the call avatars, the spotlight and the overflow roster still chose the agent avatar only from a NIP-OA profile or a directory label. Such an agent rendered as a person. All three surfaces now also use the Huddle agent set. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -12,6 +12,7 @@ class _HuddleCallAvatar extends HookConsumerWidget {
|
||||
required this.preparingResponse,
|
||||
required this.onTap,
|
||||
this.contextualLabel,
|
||||
this.isHuddleAgent = false,
|
||||
this.isSelf = false,
|
||||
this.frameSize = _huddleAvatarFrameSize,
|
||||
});
|
||||
@@ -22,6 +23,9 @@ class _HuddleCallAvatar extends HookConsumerWidget {
|
||||
|
||||
/// The huddle-scoped identity label, when resolved.
|
||||
final String? contextualLabel;
|
||||
|
||||
/// Whether the Huddle roster counts this participant as an agent.
|
||||
final bool isHuddleAgent;
|
||||
final bool active;
|
||||
final double speakerLevel;
|
||||
final bool preparingResponse;
|
||||
@@ -129,7 +133,8 @@ class _HuddleCallAvatar extends HookConsumerWidget {
|
||||
contextualLabel: contextualLabel,
|
||||
isSelf: isSelf,
|
||||
);
|
||||
final isAgent = profile?.isAgent == true || fallbackLabel != null;
|
||||
final isAgent =
|
||||
isHuddleAgent || profile?.isAgent == true || fallbackLabel != null;
|
||||
|
||||
final semanticStates = [
|
||||
label,
|
||||
|
||||
@@ -12,6 +12,7 @@ class _HuddleCallParticipants extends StatelessWidget {
|
||||
required this.activeSpeakerPubkeys,
|
||||
required this.speakerLevels,
|
||||
required this.workingAgentPubkeys,
|
||||
required this.agentPubkeys,
|
||||
required this.retryTooltip,
|
||||
required this.retryIcon,
|
||||
required this.onRetry,
|
||||
@@ -31,6 +32,9 @@ class _HuddleCallParticipants extends StatelessWidget {
|
||||
final Set<String> activeSpeakerPubkeys;
|
||||
final Map<String, double> speakerLevels;
|
||||
final Set<String> workingAgentPubkeys;
|
||||
|
||||
/// Lowercase pubkeys that the Huddle roster counts as agents.
|
||||
final Set<String> agentPubkeys;
|
||||
final String retryTooltip;
|
||||
final IconData retryIcon;
|
||||
final VoidCallback onRetry;
|
||||
@@ -154,6 +158,7 @@ class _HuddleCallParticipants extends StatelessWidget {
|
||||
activeSpeakerPubkeys: activeSpeakerPubkeys,
|
||||
speakerLevels: speakerLevels,
|
||||
workingAgentPubkeys: workingAgentPubkeys,
|
||||
agentPubkeys: agentPubkeys,
|
||||
movementDuration: movementDuration,
|
||||
entryDuration: entryDuration,
|
||||
exitDuration: exitDuration,
|
||||
|
||||
@@ -42,6 +42,7 @@ class _HuddleParticipantCluster extends HookWidget {
|
||||
required this.activeSpeakerPubkeys,
|
||||
required this.speakerLevels,
|
||||
required this.workingAgentPubkeys,
|
||||
required this.agentPubkeys,
|
||||
required this.movementDuration,
|
||||
required this.entryDuration,
|
||||
required this.exitDuration,
|
||||
@@ -58,6 +59,9 @@ class _HuddleParticipantCluster extends HookWidget {
|
||||
final Set<String> activeSpeakerPubkeys;
|
||||
final Map<String, double> speakerLevels;
|
||||
final Set<String> workingAgentPubkeys;
|
||||
|
||||
/// Lowercase pubkeys that the Huddle roster counts as agents.
|
||||
final Set<String> agentPubkeys;
|
||||
final Duration movementDuration;
|
||||
final Duration entryDuration;
|
||||
final Duration exitDuration;
|
||||
@@ -147,6 +151,7 @@ class _HuddleParticipantCluster extends HookWidget {
|
||||
active: activeSpeakerPubkeys.contains(pubkey),
|
||||
speakerLevel: speakerLevels[pubkey] ?? 0,
|
||||
preparingResponse: workingAgentPubkeys.contains(pubkey),
|
||||
isHuddleAgent: agentPubkeys.contains(pubkey.toLowerCase()),
|
||||
slot: lastSlots[pubkey]!,
|
||||
present: visiblePubkeySet.contains(pubkey),
|
||||
movementDuration: movementDuration,
|
||||
@@ -183,6 +188,7 @@ class _HuddleAnimatedParticipant extends StatelessWidget {
|
||||
required this.active,
|
||||
required this.speakerLevel,
|
||||
required this.preparingResponse,
|
||||
required this.isHuddleAgent,
|
||||
required this.slot,
|
||||
required this.present,
|
||||
required this.movementDuration,
|
||||
@@ -198,6 +204,7 @@ class _HuddleAnimatedParticipant extends StatelessWidget {
|
||||
final bool active;
|
||||
final double speakerLevel;
|
||||
final bool preparingResponse;
|
||||
final bool isHuddleAgent;
|
||||
final _HuddleClusterSlot slot;
|
||||
final bool present;
|
||||
final Duration movementDuration;
|
||||
@@ -251,6 +258,7 @@ class _HuddleAnimatedParticipant extends StatelessWidget {
|
||||
active: active,
|
||||
speakerLevel: speakerLevel,
|
||||
preparingResponse: preparingResponse,
|
||||
isHuddleAgent: isHuddleAgent,
|
||||
frameSize: frameSize,
|
||||
onTap: onTap,
|
||||
),
|
||||
|
||||
@@ -175,7 +175,13 @@ class _HuddleParticipantSpotlight extends ConsumerWidget {
|
||||
),
|
||||
isSelf: isSelf,
|
||||
);
|
||||
final isAgent = profile?.isAgent == true || fallbackLabel != null;
|
||||
final isHuddleAgent = ref.watch(
|
||||
huddleAgentPubkeysProvider(
|
||||
ephemeralChannelId,
|
||||
).select((agents) => agents.contains(pubkey.toLowerCase())),
|
||||
);
|
||||
final isAgent =
|
||||
isHuddleAgent || profile?.isAgent == true || fallbackLabel != null;
|
||||
|
||||
return Semantics(
|
||||
label: active ? '$label, speaking' : label,
|
||||
@@ -274,6 +280,9 @@ class _HuddleParticipantRoster extends ConsumerWidget {
|
||||
}
|
||||
final profiles = ref.watch(userCacheProvider);
|
||||
final fallbackLabels = ref.watch(agentDirectoryDisplayNamesProvider);
|
||||
final agentPubkeys = ref.watch(
|
||||
huddleAgentPubkeysProvider(ephemeralChannelId),
|
||||
);
|
||||
final names = ref.watch(_huddleIdentityNamesProvider(ephemeralChannelId));
|
||||
final activeSpeakerPubkeys = ref.watch(
|
||||
huddleSessionProvider.select((session) => session.activeSpeakerPubkeys),
|
||||
@@ -362,6 +371,9 @@ class _HuddleParticipantRoster extends ConsumerWidget {
|
||||
color: context.colors.onPrimaryContainer,
|
||||
),
|
||||
isAgent:
|
||||
agentPubkeys.contains(
|
||||
pubkey.toLowerCase(),
|
||||
) ||
|
||||
profile?.isAgent == true ||
|
||||
fallbackLabels[pubkey] != null,
|
||||
),
|
||||
|
||||
@@ -718,6 +718,7 @@ class _MobileHuddleCallPage extends ConsumerWidget {
|
||||
activeSpeakerPubkeys: session.activeSpeakerPubkeys,
|
||||
speakerLevels: session.speakerLevels,
|
||||
workingAgentPubkeys: workingAgentPubkeys,
|
||||
agentPubkeys: huddleBotPubkeys,
|
||||
retryTooltip: retryTooltip,
|
||||
retryIcon: retryIcon,
|
||||
onRetry: onRetry,
|
||||
|
||||
@@ -7452,15 +7452,37 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Join'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
AvatarImage avatarIn(Finder owner) => tester.widget<AvatarImage>(
|
||||
find.descendant(of: owner, matching: find.byType(AvatarImage)),
|
||||
);
|
||||
final agentAvatar = find.byKey(
|
||||
const ValueKey('huddle-participant-avatar-agent'),
|
||||
);
|
||||
expect(agentAvatar, findsOneWidget);
|
||||
// The record makes it an agent, so it renders as one too.
|
||||
expect(avatarIn(agentAvatar).isAgent, isTrue);
|
||||
expect(
|
||||
find.byKey(const ValueKey('huddle-participant-avatar-agent')),
|
||||
findsOneWidget,
|
||||
avatarIn(
|
||||
find.byKey(const ValueKey('huddle-participant-avatar-self')),
|
||||
).isAgent,
|
||||
isFalse,
|
||||
);
|
||||
// A person who is not on audio is not in the roster after admission.
|
||||
expect(
|
||||
find.byKey(const ValueKey('huddle-participant-avatar-person')),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.tap(agentAvatar);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
avatarIn(
|
||||
find.byKey(
|
||||
const ValueKey('huddle-participant-spotlight-avatar-agent'),
|
||||
),
|
||||
).isAgent,
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8534,6 +8556,20 @@ void main() {
|
||||
),
|
||||
);
|
||||
expect(membershipAgentRow, findsOneWidget);
|
||||
// The `bot` role alone makes it an agent: no profile attestation or
|
||||
// directory label is needed for the agent avatar.
|
||||
AvatarImage rowAvatar(Finder row) => tester.widget<AvatarImage>(
|
||||
find.descendant(of: row, matching: find.byType(AvatarImage)),
|
||||
);
|
||||
expect(rowAvatar(membershipAgentRow).isAgent, isTrue);
|
||||
expect(
|
||||
rowAvatar(
|
||||
find.byKey(
|
||||
const ValueKey('huddle-participant-roster-row-guest-23'),
|
||||
),
|
||||
).isAgent,
|
||||
isFalse,
|
||||
);
|
||||
await tester.tapAt(const Offset(8, 8));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user