mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(mobile): disambiguate same-named agents
Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
43bd104de5
commit
941c50d04c
@@ -22,6 +22,7 @@ import 'channels_provider.dart';
|
||||
import 'emoji_picker.dart';
|
||||
import 'mentions/mention_candidates.dart';
|
||||
import 'mentions/mention_candidates_provider.dart';
|
||||
import 'mentions/mention_display_name.dart';
|
||||
import 'mentions/mention_ranking.dart';
|
||||
|
||||
part 'compose_bar/helpers.dart';
|
||||
|
||||
@@ -17,6 +17,10 @@ class _MentionSuggestions extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nameCounts = countVisibleMentionDisplayNames(
|
||||
suggestions.map((candidate) => candidate.label),
|
||||
);
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 240),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@@ -41,6 +45,22 @@ class _MentionSuggestions extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final candidate = suggestions[index];
|
||||
final name = candidate.label;
|
||||
final ownerLabel = candidate.isAgent
|
||||
? formatOwnerLabel(
|
||||
candidate.ownerPubkey,
|
||||
currentPubkey,
|
||||
userCache,
|
||||
)
|
||||
: null;
|
||||
final displayName = formatDisambiguatedMentionDisplayName(
|
||||
displayName: name,
|
||||
hasNameCollision: hasVisibleMentionDisplayNameCollision(
|
||||
name,
|
||||
nameCounts,
|
||||
),
|
||||
isAgent: candidate.isAgent,
|
||||
ownerLabel: ownerLabel,
|
||||
);
|
||||
final avatarUrl =
|
||||
candidate.avatarUrl ?? userCache[candidate.pubkey]?.avatarUrl;
|
||||
|
||||
@@ -58,7 +78,7 @@ class _MentionSuggestions extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(name, style: context.textTheme.bodyMedium),
|
||||
title: Text(displayName, style: context.textTheme.bodyMedium),
|
||||
subtitle: _MentionSuggestionInfo.build(
|
||||
context,
|
||||
candidate: candidate,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/// Case-insensitive display-name counts for the currently visible suggestions.
|
||||
Map<String, int> countVisibleMentionDisplayNames(
|
||||
Iterable<String> displayNames,
|
||||
) {
|
||||
final counts = <String, int>{};
|
||||
for (final displayName in displayNames) {
|
||||
final normalizedName = displayName.trim().toLowerCase();
|
||||
if (normalizedName.isEmpty) continue;
|
||||
counts[normalizedName] = (counts[normalizedName] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
bool hasVisibleMentionDisplayNameCollision(
|
||||
String displayName,
|
||||
Map<String, int> counts,
|
||||
) {
|
||||
return (counts[displayName.trim().toLowerCase()] ?? 0) > 1;
|
||||
}
|
||||
|
||||
/// Adds an agent's owner only when its name collides with another visible
|
||||
/// suggestion. The original candidate label remains unchanged for insertion.
|
||||
String formatDisambiguatedMentionDisplayName({
|
||||
required String displayName,
|
||||
required bool hasNameCollision,
|
||||
required bool isAgent,
|
||||
required String? ownerLabel,
|
||||
}) {
|
||||
final normalizedOwnerLabel = ownerLabel?.trim();
|
||||
if (!hasNameCollision ||
|
||||
!isAgent ||
|
||||
normalizedOwnerLabel == null ||
|
||||
normalizedOwnerLabel.isEmpty) {
|
||||
return displayName;
|
||||
}
|
||||
return '$displayName ($normalizedOwnerLabel)';
|
||||
}
|
||||
@@ -116,6 +116,7 @@ Widget _buildComposeBar({
|
||||
List<ChannelMember> members = const <ChannelMember>[],
|
||||
Future<List<ChannelMember>>? membersFuture,
|
||||
List<AgentDirectoryEntry> relayAgents = const <AgentDirectoryEntry>[],
|
||||
Map<String, String> agentOwners = const <String, String>{},
|
||||
List<Channel> channels = const <Channel>[],
|
||||
String? currentPubkey,
|
||||
bool? supportsShowingSystemContextMenu,
|
||||
@@ -128,7 +129,7 @@ Widget _buildComposeBar({
|
||||
'channel-1',
|
||||
).overrideWith((ref) => membersFuture ?? Future.value(members)),
|
||||
agentDirectoryProvider.overrideWith((ref) async => relayAgents),
|
||||
agentOwnersProvider.overrideWith((ref) async => const <String, String>{}),
|
||||
agentOwnersProvider.overrideWith((ref) async => agentOwners),
|
||||
relayClientProvider.overrideWithValue(
|
||||
RelayClient(baseUrl: 'http://localhost:3000'),
|
||||
),
|
||||
@@ -844,6 +845,62 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'disambiguates colliding agent titles without changing selection text',
|
||||
(tester) async {
|
||||
final currentPubkey = 'a' * 64;
|
||||
final ownedAgentPubkey = 'b' * 64;
|
||||
final remoteAgentPubkey = 'c' * 64;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildComposeBar(
|
||||
uploadService: _testUploadService(nostr.Keys.generate().nsec),
|
||||
currentPubkey: currentPubkey,
|
||||
relayAgents: [
|
||||
AgentDirectoryEntry(
|
||||
pubkey: ownedAgentPubkey,
|
||||
displayName: 'Bumble',
|
||||
respondTo: 'anyone',
|
||||
channelIds: const ['shared-channel'],
|
||||
),
|
||||
AgentDirectoryEntry(
|
||||
pubkey: remoteAgentPubkey,
|
||||
displayName: 'bUmBlE',
|
||||
respondTo: 'anyone',
|
||||
channelIds: const ['shared-channel'],
|
||||
),
|
||||
],
|
||||
agentOwners: {
|
||||
ownedAgentPubkey: currentPubkey,
|
||||
remoteAgentPubkey: 'd' * 64,
|
||||
},
|
||||
channels: [_makeCurrentChannel(), _makeSharedMemberChannel()],
|
||||
onSend:
|
||||
(
|
||||
content,
|
||||
mentionPubkeys, {
|
||||
mediaTags = const <List<String>>[],
|
||||
}) async {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(find.byType(TextField), '@bum');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Bumble (you)'), findsOneWidget);
|
||||
expect(find.text('bUmBlE (dddddddd…)'), findsOneWidget);
|
||||
expect(find.text('owned by you · not in channel'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Bumble (you)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField)).controller?.text,
|
||||
'@Bumble ',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('adds a selected non-member agent as a bot before sending', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:buzz/features/channels/mentions/mention_display_name.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('appends owner labels to colliding agents', () {
|
||||
expect(
|
||||
formatDisambiguatedMentionDisplayName(
|
||||
displayName: 'Bumble',
|
||||
hasNameCollision: true,
|
||||
isAgent: true,
|
||||
ownerLabel: 'sergior',
|
||||
),
|
||||
'Bumble (sergior)',
|
||||
);
|
||||
expect(
|
||||
formatDisambiguatedMentionDisplayName(
|
||||
displayName: 'Bumble',
|
||||
hasNameCollision: true,
|
||||
isAgent: true,
|
||||
ownerLabel: 'you',
|
||||
),
|
||||
'Bumble (you)',
|
||||
);
|
||||
});
|
||||
|
||||
test('leaves humans and unique agents unchanged', () {
|
||||
expect(
|
||||
formatDisambiguatedMentionDisplayName(
|
||||
displayName: 'Bumble',
|
||||
hasNameCollision: true,
|
||||
isAgent: false,
|
||||
ownerLabel: null,
|
||||
),
|
||||
'Bumble',
|
||||
);
|
||||
expect(
|
||||
formatDisambiguatedMentionDisplayName(
|
||||
displayName: 'Bumble',
|
||||
hasNameCollision: false,
|
||||
isAgent: true,
|
||||
ownerLabel: 'sergior',
|
||||
),
|
||||
'Bumble',
|
||||
);
|
||||
});
|
||||
|
||||
test('detects visible display-name collisions case-insensitively', () {
|
||||
final counts = countVisibleMentionDisplayNames([
|
||||
'Bumble',
|
||||
' bUmBlE ',
|
||||
'Fizz',
|
||||
]);
|
||||
|
||||
expect(hasVisibleMentionDisplayNameCollision('bumble', counts), isTrue);
|
||||
expect(hasVisibleMentionDisplayNameCollision('Fizz', counts), isFalse);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user