mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
🤖 ## Summary When two identities share a display name, Buzz mobile shows the same label for both. A reader cannot tell a person from an agent, or one person's agent from another's. For example, a channel can show three agents and one human all named "murderbot". This PR adds the naming rules that fix this, without changing any screen yet. It ports the contextual identity-name contract (v1) from the desktop app to Dart. Given the identities shown together, the resolver gives each public key a distinct label: - A human keeps the plain name. Agents that share it get a qualifier. - The viewer's own agents are named before other people's agents. - An agent is qualified by its owner's name first ("Wes's Honey"). A short public-key suffix is added only if that is still ambiguous ("baxen's murderbot · xa8v"). It also adds two providers that screens will use: - `channelIdentityNamesProvider` compares names against a channel's members, plus any referenced non-member such as an old author or an outside mention. - `watchIdentityNames` compares a set of identities shown together outside a channel, and loads their profiles. The next PRs in this stack connect these to the screens. Part 1 of 3: #7894 (resolver) → #7895 (conversations) → #7896 (lists, Search, Pulse). ### Related issue Related to #2910 (default "Fizz" agents from different users collide in one channel). ### Testing - The contract's 31 portable fixture cases run against the production resolver. The fixture file is copied unchanged from the contract, so reviewers can skip it. - Unit tests cover owner and suffix qualification, the viewer's own identities, and invalid public keys. - Provider tests check that a channel keeps its names while its roster reloads, that an offline cached bot still counts as an agent, and that changing relay or account never shows the old roster. - `flutter analyze` is clean. No UI change, so there are no screenshots. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
84 lines
2.9 KiB
Dart
84 lines
2.9 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import '../../shared/identity_names/identity_names.dart';
|
|
import '../../shared/identity_names/identity_names_provider.dart';
|
|
import '../../shared/mentions/agent_identity_provider.dart';
|
|
import 'channel_management_provider.dart';
|
|
import 'channels_provider.dart';
|
|
|
|
/// Contextual identity labels for one channel, thread, or DM.
|
|
///
|
|
/// The comparison context is the channel's members; a referenced non-member
|
|
/// (historical author, outside mention) is compared against the members plus
|
|
/// itself. Watch one label with
|
|
/// `ref.watch(channelIdentityNamesProvider(id).select((n) => n.labelFor(pk)))`.
|
|
final channelIdentityNamesProvider = Provider.autoDispose
|
|
.family<IdentityNames, String>((ref, channelId) {
|
|
final sources = ref.watch(identityNameSourcesProvider);
|
|
final members =
|
|
watchLatestChannelMembers(ref, channelId) ??
|
|
(ref.watch(channelsProvider).asData == null
|
|
? const <ChannelMember>[]
|
|
: ref
|
|
.read(channelsProvider.notifier)
|
|
.cachedMembersForChannel(channelId));
|
|
final names = sources.scope(
|
|
[for (final member in members) member.pubkey],
|
|
// The roster's own bot roles stay available offline, when the
|
|
// relay-backed bot lookup and agent directory are empty.
|
|
agentPubkeys: {
|
|
...ref.watch(agentMentionPubkeysProvider(channelId)),
|
|
for (final member in members)
|
|
if (member.isBot) member.pubkey,
|
|
},
|
|
fallbackNames: {
|
|
for (final member in members) member.pubkey: ?member.displayName,
|
|
},
|
|
);
|
|
loadIdentityNameOwners(ref, sources, names.candidates);
|
|
return names;
|
|
});
|
|
|
|
/// Watches one identity's contextual label in [channelId].
|
|
String watchChannelIdentityLabel(
|
|
WidgetRef ref,
|
|
String channelId,
|
|
String pubkey,
|
|
) => ref.watch(
|
|
channelIdentityNamesProvider(
|
|
channelId,
|
|
).select((names) => names.labelFor(pubkey)),
|
|
);
|
|
|
|
/// Watches contextual labels for [pubkeys] in [channelId], keyed by
|
|
/// lowercase pubkey. Rebuilds only when one of these labels changes.
|
|
Map<String, String> watchChannelIdentityLabels(
|
|
WidgetRef ref,
|
|
String channelId,
|
|
Iterable<String> pubkeys,
|
|
) {
|
|
final keys = {for (final pubkey in pubkeys) pubkey.toLowerCase()};
|
|
if (keys.isEmpty) return const {};
|
|
return ref.watch(
|
|
channelIdentityNamesProvider(channelId).select(
|
|
(names) => _LabelMap({for (final key in keys) key: names.labelFor(key)}),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _LabelMap extends UnmodifiableMapView<String, String> {
|
|
_LabelMap(super.labels);
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
other is Map<String, String> && mapEquals(this, other);
|
|
|
|
@override
|
|
int get hashCode => Object.hashAllUnordered(
|
|
entries.map((entry) => Object.hash(entry.key, entry.value)),
|
|
);
|
|
}
|