diff --git a/mobile/lib/features/agents/agents_page.dart b/mobile/lib/features/agents/agents_page.dart index a1077c8a4a7..c5d531c54d3 100644 --- a/mobile/lib/features/agents/agents_page.dart +++ b/mobile/lib/features/agents/agents_page.dart @@ -14,6 +14,7 @@ import '../../shared/community/community_provider.dart'; import '../../shared/me/default_agent.dart'; import '../../shared/hosted/hosted_flow.dart'; import '../../shared/hosted/hosted_flow_provider.dart'; +import '../../shared/identity_archives/identity_archives.dart'; import '../../shared/identity_names/identity_names_provider.dart'; import '../../shared/mentions/agent_identity_provider.dart'; import '../../shared/profile/user_cache_provider.dart'; @@ -317,12 +318,22 @@ class AgentsPage extends HookConsumerWidget { final owned = registered.data ?? (lastOwned.value?.$1 == scope ? lastOwned.value?.$2 : null); - final keys = { - ...?owned, - for (final entry - in ownedDirectory.asData?.value ?? []) - entry.pubkey, - }.where((key) => !controller.deletedAgents.contains(key)).toList(); + // Archived agents are not offered, as on desktop, where archiving an + // agent takes it out of the library. + final archived = ref.watch(hiddenArchivedIdentitiesProvider); + final keys = + { + ...?owned, + for (final entry + in ownedDirectory.asData?.value ?? []) + entry.pubkey, + } + .where( + (key) => + !controller.deletedAgents.contains(key) && + !archived.contains(key), + ) + .toList(); final names = watchIdentityNames( ref, keys, @@ -363,6 +374,9 @@ class AgentsPage extends HookConsumerWidget { if (context.mounted) { attempt.value++; ref.invalidate(ownedAgentDirectoryProvider); + // A refresh also rereads archives: an agent restored elsewhere + // reappears, and one archived elsewhere leaves. + ref.invalidate(identityArchivesProvider); } } diff --git a/mobile/lib/features/channels/channel_actions_sheet.dart b/mobile/lib/features/channels/channel_actions_sheet.dart index de5d0c482b7..c8894123eb3 100644 --- a/mobile/lib/features/channels/channel_actions_sheet.dart +++ b/mobile/lib/features/channels/channel_actions_sheet.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:buzz/shared/theme/buzz_icons.dart'; import '../../shared/clipboard_utils.dart'; +import '../../shared/identity_archives/identity_archives.dart'; import '../../shared/identity_names/identity_names_provider.dart'; import 'dm_channel_labels.dart'; import '../../shared/mentions/agent_identity_provider.dart'; diff --git a/mobile/lib/features/channels/channel_detail_page.dart b/mobile/lib/features/channels/channel_detail_page.dart index 0790f1e4219..02b2383269f 100644 --- a/mobile/lib/features/channels/channel_detail_page.dart +++ b/mobile/lib/features/channels/channel_detail_page.dart @@ -27,7 +27,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../../shared/agents/owned_agent_directory.dart'; import '../../shared/emoji/emoji_burst.dart'; -import '../../shared/me/me_archived_agents.dart'; +import '../../shared/identity_archives/identity_archives.dart'; import '../../shared/me/me_assistants.dart'; import '../../shared/me/me_sync.dart'; import '../../shared/me/me_search_placement.dart'; diff --git a/mobile/lib/features/channels/channel_detail_page/huddle_agent_connection.dart b/mobile/lib/features/channels/channel_detail_page/huddle_agent_connection.dart index 40aeca288f8..3ba3b937a92 100644 --- a/mobile/lib/features/channels/channel_detail_page/huddle_agent_connection.dart +++ b/mobile/lib/features/channels/channel_detail_page/huddle_agent_connection.dart @@ -26,7 +26,7 @@ Future _openMobileHuddle({ final channel = ref.read(channelByIdProvider(invite.parentChannelId)); if (ref.read(dmVoiceCandidateProvider(invite.parentChannelId)) != null) { ref.invalidate(ownedAgentDirectoryProvider); - ref.invalidate(meArchivedAgentsProvider); + ref.invalidate(identityArchivesProvider); final agents = await ref.read(ownedCodexVoiceAgentsProvider.future); if (!context.mounted) return; final agent = dmVoiceAgent( @@ -52,7 +52,7 @@ Future _openMobileHuddle({ ); if (!confirmed || !context.mounted) return; ref.invalidate(ownedAgentDirectoryProvider); - ref.invalidate(meArchivedAgentsProvider); + ref.invalidate(identityArchivesProvider); final currentAgents = await ref.read( ownedCodexVoiceAgentsProvider.future, ); diff --git a/mobile/lib/features/channels/channel_details_page.dart b/mobile/lib/features/channels/channel_details_page.dart index 3bd79c05b0d..3f005bd1da5 100644 --- a/mobile/lib/features/channels/channel_details_page.dart +++ b/mobile/lib/features/channels/channel_details_page.dart @@ -151,7 +151,19 @@ class ChannelDetailsPage extends HookConsumerWidget { membersAsync.value?.length ?? resolvedChannel.memberCount; final memberLabel = '$memberCount ${memberCount == 1 ? 'member' : 'members'}'; - final previewMembers = members.take(_channelMemberPreviewLimit).toList(); + // People, agents, then known-archived people and agents, as in the + // members sheet. + final archived = ref.watch(hiddenArchivedIdentitiesProvider); + bool isArchived(ChannelMember member) => + archived.contains(member.pubkey.toLowerCase()); + final previewMembers = [ + for (final archivedPart in [false, true]) + for (final bot in [false, true]) + ...members.where( + (member) => + member.isBot == bot && isArchived(member) == archivedPart, + ), + ].take(_channelMemberPreviewLimit).toList(); final userCache = ref.watch(userCacheProvider); final memberLabels = watchChannelIdentityLabels( ref, @@ -450,6 +462,7 @@ class ChannelDetailsPage extends HookConsumerWidget { contextualLabel: memberLabels[member.pubkey.toLowerCase()], avatarUrl: userCache[member.pubkey.toLowerCase()]?.avatarUrl, + isArchived: isArchived(member), ), if (memberCount > _channelMemberPreviewLimit) AppListRowRaw( @@ -738,6 +751,7 @@ class _ChannelMemberPreviewRow extends StatelessWidget { required this.displayName, required this.contextualLabel, required this.avatarUrl, + this.isArchived = false, }); final ChannelMember member; @@ -749,6 +763,9 @@ class _ChannelMemberPreviewRow extends StatelessWidget { final String? contextualLabel; final String? avatarUrl; + /// Known-archived: the row says so, in text a screen reader reads too. + final bool isArchived; + @override Widget build(BuildContext context) { final isSelf = member.pubkey.toLowerCase() == currentPubkey?.toLowerCase(); @@ -765,7 +782,9 @@ class _ChannelMemberPreviewRow extends StatelessWidget { final initial = isSelf || hasName ? name[0].toUpperCase() : (member.pubkey.isNotEmpty ? member.pubkey[0].toUpperCase() : '?'); - final roleLabel = _channelMemberRoleLabel(member.role); + final roleLabel = isArchived + ? 'Archived \u00b7 ${_channelMemberRoleLabel(member.role)}' + : _channelMemberRoleLabel(member.role); final titleStyle = context.textTheme.bodyLarge; final roleStyle = context.textTheme.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, diff --git a/mobile/lib/features/channels/dm_voice_agent_provider.dart b/mobile/lib/features/channels/dm_voice_agent_provider.dart index bb4b25cf316..48b08bf64ea 100644 --- a/mobile/lib/features/channels/dm_voice_agent_provider.dart +++ b/mobile/lib/features/channels/dm_voice_agent_provider.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../shared/agents/owned_agent_directory.dart'; -import '../../shared/me/me_archived_agents.dart'; +import '../../shared/identity_archives/identity_archives.dart'; import '../../shared/me/me_sync.dart'; import '../../shared/relay/relay.dart'; import '../../shared/mentions/agent_identity_provider.dart'; @@ -19,7 +19,7 @@ import 'channels_provider.dart'; /// adds no inventory read. final ownedCodexVoiceAgentsProvider = FutureProvider>((ref) async { ref.watch(_voiceInventoryChangesProvider); - final archived = ref.watch(meArchivedAgentsProvider.future); + final archived = ref.watch(identityArchivesProvider.future); final owned = await ref.watch(ownedAgentDirectoryProvider.future); final hidden = await archived; return { @@ -57,7 +57,7 @@ final _voiceInventoryChangesProvider = Provider.autoDispose((ref) { NostrFilter(kinds: const [30177], authors: [sync.pubkey], limit: 0), (_) { if (!disposed) ref.invalidate(ownedAgentDirectoryProvider); - ref.invalidate(meArchivedAgentsProvider); + ref.invalidate(identityArchivesProvider); }, ); if (disposed) unsubscribe?.call(); @@ -103,7 +103,7 @@ Future resolveDmVoiceAgent(Ref ref, String parentChannelId) async { return null; } ref.invalidate(ownedAgentDirectoryProvider); - ref.invalidate(meArchivedAgentsProvider); + ref.invalidate(identityArchivesProvider); final agents = await ref.read(ownedCodexVoiceAgentsProvider.future); final agent = dmVoiceAgent(channel, ref.read(currentPubkeyProvider), agents); if (agent == null) { diff --git a/mobile/lib/features/channels/members_sheet.dart b/mobile/lib/features/channels/members_sheet.dart index 7650713200a..43fe47ff9d0 100644 --- a/mobile/lib/features/channels/members_sheet.dart +++ b/mobile/lib/features/channels/members_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:buzz/shared/theme/buzz_icons.dart'; +import '../../shared/identity_archives/identity_archives.dart'; import '../../shared/theme/theme.dart'; import '../../shared/widgets/sheet_action_section.dart'; import '../../shared/widgets/avatar_image.dart'; @@ -37,8 +38,24 @@ class MembersSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final membersAsync = ref.watch(channelMembersProvider(channel.id)); final allMembers = membersAsync.asData?.value ?? const []; - final people = allMembers.where((member) => !member.isBot).toList(); - final bots = allMembers.where((member) => member.isBot).toList(); + // Known-archived members leave People and Agents for their own section + // after both: archived people, then archived agents, in the relay's order. + final archived = ref.watch(hiddenArchivedIdentitiesProvider); + bool isArchived(ChannelMember member) => + archived.contains(member.pubkey.toLowerCase()); + final people = [ + for (final member in allMembers) + if (!member.isBot && !isArchived(member)) member, + ]; + final bots = [ + for (final member in allMembers) + if (member.isBot && !isArchived(member)) member, + ]; + final archivedMembers = [ + for (final bot in [false, true]) + for (final member in allMembers) + if (member.isBot == bot && isArchived(member)) member, + ]; final userCache = ref.watch(userCacheProvider); final typingBotPubkeys = ref.watch(workingBotPubkeysProvider(channel.id)); final statusCache = ref.watch(userStatusCacheProvider); @@ -89,6 +106,28 @@ class MembersSheet extends HookConsumerWidget { return null; }, [allMembers.length]); + Widget tile(ChannelMember member) { + final key = member.pubkey.toLowerCase(); + final working = member.isBot && typingBotPubkeys.contains(key); + return _MemberTile( + member: member, + currentPubkey: currentPubkey, + profile: userCache[key], + canManage: canManage, + isArchived: isArchived(member), + isSelf: !member.isBot && key == currentPubkey?.toLowerCase(), + channelId: channel.id, + userStatus: member.isBot ? null : statusCache[key], + isWorking: working, + onViewActivity: member.isBot ? () => openActivity(member) : null, + onActivityTap: member.isBot + ? (working ? () => openActivity(member) : null) + : onMemberTap == null + ? null + : () => onMemberTap!(context, member.pubkey), + ); + } + return Padding( key: const ValueKey('members-sheet-content-padding'), padding: EdgeInsets.fromLTRB(Grid.gutter, 0, Grid.gutter, 0), @@ -102,54 +141,19 @@ class MembersSheet extends HookConsumerWidget { shrinkWrap: true, slivers: [ const SliverToBoxAdapter(child: SizedBox(height: Grid.xxs)), - if (people.isNotEmpty) - _MemberSection( - label: 'People · ${people.length}', - itemCount: people.length, - itemBuilder: (context, index) { - final member = people[index]; - return _MemberTile( - member: member, - currentPubkey: currentPubkey, - profile: userCache[member.pubkey.toLowerCase()], - canManage: canManage, - isSelf: - member.pubkey.toLowerCase() == - currentPubkey?.toLowerCase(), - channelId: channel.id, - userStatus: statusCache[member.pubkey.toLowerCase()], - onActivityTap: onMemberTap == null - ? null - : () => onMemberTap!(context, member.pubkey), - ); - }, - ), - if (bots.isNotEmpty) - _MemberSection( - label: 'Agents · ${bots.length}', - semanticIndexOffset: people.length, - itemCount: bots.length, - itemBuilder: (context, index) { - final bot = bots[index]; - return _MemberTile( - member: bot, - currentPubkey: currentPubkey, - profile: userCache[bot.pubkey.toLowerCase()], - canManage: canManage, - isSelf: false, - channelId: channel.id, - isWorking: typingBotPubkeys.contains( - bot.pubkey.toLowerCase(), - ), - onViewActivity: () => openActivity(bot), - onActivityTap: - typingBotPubkeys.contains(bot.pubkey.toLowerCase()) - ? () => openActivity(bot) - : null, - ); - }, - ), - if (people.isEmpty && bots.isEmpty) + for (final (label, members, offset) in [ + ('People', people, 0), + ('Agents', bots, people.length), + ('Archived', archivedMembers, people.length + bots.length), + ]) + if (members.isNotEmpty) + _MemberSection( + label: '$label · ${members.length}', + semanticIndexOffset: offset, + itemCount: members.length, + itemBuilder: (context, index) => tile(members[index]), + ), + if (allMembers.isEmpty) SliverToBoxAdapter( child: Center( child: Text( @@ -196,6 +200,9 @@ class _MemberTile extends ConsumerWidget { final String? currentPubkey; final UserProfile? profile; final bool canManage; + + /// Known-archived: the row says so, in text a screen reader reads too. + final bool isArchived; final bool isSelf; final String channelId; final bool isWorking; @@ -208,6 +215,7 @@ class _MemberTile extends ConsumerWidget { required this.currentPubkey, required this.profile, required this.canManage, + this.isArchived = false, required this.isSelf, required this.channelId, this.isWorking = false, @@ -240,7 +248,7 @@ class _MemberTile extends ConsumerWidget { isAgent: member.isBot || profile?.isAgent == true, ), title: Text(label), - subtitle: isWorking + subtitle: isWorking && !isArchived ? Row( mainAxisSize: MainAxisSize.min, children: [ @@ -259,7 +267,7 @@ class _MemberTile extends ConsumerWidget { ), ], ) - : userStatus != null && !userStatus!.isEmpty + : !isArchived && userStatus != null && !userStatus!.isEmpty ? Text( '${userStatus!.emoji.isNotEmpty ? '${userStatus!.emoji} ' : ''}${userStatus!.text}', style: context.textTheme.bodySmall?.copyWith( @@ -269,7 +277,9 @@ class _MemberTile extends ConsumerWidget { overflow: TextOverflow.ellipsis, ) : Text( - _roleLabel(member.role), + isArchived + ? 'Archived \u00b7 ${_roleLabel(member.role)}' + : _roleLabel(member.role), style: context.textTheme.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, ), diff --git a/mobile/lib/features/channels/mentions/mention_candidates_provider.dart b/mobile/lib/features/channels/mentions/mention_candidates_provider.dart index 9826b3785cd..fa0015690a3 100644 --- a/mobile/lib/features/channels/mentions/mention_candidates_provider.dart +++ b/mobile/lib/features/channels/mentions/mention_candidates_provider.dart @@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../shared/agents/owned_agent_directory.dart'; import '../../../shared/crypto/nip_oa.dart'; +import '../../../shared/identity_archives/identity_archives.dart'; import '../../../shared/identity_names/identity_names.dart'; import '../../../shared/identity_names/identity_names_provider.dart'; import '../../../shared/mentions/agent_identity_provider.dart'; @@ -356,6 +357,9 @@ List _chooserCandidates( final userCache = ref.watch(userCacheProvider); final currentPubkey = ref.watch(currentPubkeyProvider); final searches = ref.watch(mentionSearchCacheProvider); + // Removed before labels are scoped, so an archived namesake never adds a + // qualifier to a live name. + final archived = ref.watch(hiddenArchivedIdentitiesProvider); final sharedChannelIds = { for (final channel in channels) @@ -384,7 +388,9 @@ List _chooserCandidates( offerOwnedAgents: offerOwnedAgents, offerUnsharedAgents: offerUnsharedAgents, )) - if (isMentionKey(candidate.pubkey)) candidate, + if (isMentionKey(candidate.pubkey) && + !archived.contains(candidate.pubkey)) + candidate, ]; // Admission does not gate the search: `@Mary J` can name a person diff --git a/mobile/lib/features/search/search_page.dart b/mobile/lib/features/search/search_page.dart index cc10d469582..cd852c94cfc 100644 --- a/mobile/lib/features/search/search_page.dart +++ b/mobile/lib/features/search/search_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:buzz/shared/theme/buzz_icons.dart'; +import '../../shared/identity_archives/identity_archives.dart'; import '../../shared/identity_names/identity_names_provider.dart'; import '../../shared/mentions/agent_identity_provider.dart'; import '../../shared/mentions/mention_tags.dart'; @@ -327,12 +328,19 @@ class _SearchBody extends ConsumerWidget { filter == _SearchFilter.all || filter == _SearchFilter.people; final showMessages = filter == _SearchFilter.all || filter == _SearchFilter.messages; + // Known-archived people leave the results. The search asks for more + // people than are shown, so they do not push live people out. + final archived = ref.watch(hiddenArchivedIdentitiesProvider); + final people = state.userResults + .where((user) => !archived.contains(user.pubkey.toLowerCase())) + .take(searchPeopleShown) + .toList(); // Only the active tab's results count, so a tab with none of its own // says so rather than showing a blank list. final hasAnyResults = (showChannels && state.channelResults.isNotEmpty) || - (showPeople && state.userResults.isNotEmpty) || + (showPeople && people.isNotEmpty) || (showMessages && state.messageResults.isNotEmpty); void recordResultSelection() => ref.read(recentSearchesProvider.notifier).record(state.query); @@ -388,9 +396,9 @@ class _SearchBody extends ConsumerWidget { SliverToBoxAdapter( child: _ChannelSearchFailed(onRetry: retryChannels), ), - if (showPeople && state.userResults.isNotEmpty) + if (showPeople && people.isNotEmpty) _PeopleSection( - users: state.userResults, + users: people, onResultSelected: recordResultSelection, ), if (showMessages && state.messageResults.isNotEmpty) diff --git a/mobile/lib/features/search/search_provider.dart b/mobile/lib/features/search/search_provider.dart index 3b920f47ab1..0f44c93cc8f 100644 --- a/mobile/lib/features/search/search_provider.dart +++ b/mobile/lib/features/search/search_provider.dart @@ -112,6 +112,9 @@ bool _searchesOpenChannels(String query) => _channelNeedle(query).isNotEmpty; /// Most channel rows one query shows, ranked best first. const searchChannelResultLimit = 8; +/// Most people rows one query shows. +const searchPeopleShown = 8; + /// How long a loaded open-channel directory serves searches before the next /// search reads it again, as desktop's `PUBLIC_CHANNEL_PAGE_TTL` does. const searchDirectoryMaxAge = Duration(seconds: 30); @@ -380,12 +383,16 @@ class SearchNotifier extends Notifier { } } + /// People asked of the directory search: more than [searchPeopleShown], + /// so archived people the page leaves out do not leave it short. + static const _peopleSearchLimit = 3 * searchPeopleShown; + Future _searchUsers(String query, int generation) async { List? users; try { users = await ref .read(channelActionsProvider) - .searchUsers(query, limit: 8); + .searchUsers(query, limit: _peopleSearchLimit); } catch (_) { // User search failure is non-critical — keep existing results. } diff --git a/mobile/lib/shared/me/me_archived_agents.dart b/mobile/lib/shared/identity_archives/identity_archives.dart similarity index 62% rename from mobile/lib/shared/me/me_archived_agents.dart rename to mobile/lib/shared/identity_archives/identity_archives.dart index 66b48944660..f1428f6b82d 100644 --- a/mobile/lib/shared/me/me_archived_agents.dart +++ b/mobile/lib/shared/identity_archives/identity_archives.dart @@ -7,19 +7,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart' as http; import '../relay/relay.dart'; -import 'me_sync.dart'; +import '../me/me_sync.dart'; -final meRelayInformationClientProvider = Provider((ref) { +final relayInformationClientProvider = Provider((ref) { final client = http.Client(); ref.onDispose(client.close); return client; }); -/// Matches desktop's known-archived filtering. Unknown archive evidence does -/// not hide identities; this is presentation, never ownership or admission. -final meArchivedAgentsProvider = FutureProvider>((ref) async { +/// The identities this relay's NIP-IA archive list (kind 13535) names, never +/// the viewer: an archive stays visible to its subject. Empty when the list +/// cannot be read. This is presentation, never ownership or admission. +final identityArchivesProvider = FutureProvider>((ref) async { final sync = ref.watch(meSyncProvider); - final client = ref.watch(meRelayInformationClientProvider); + final client = ref.watch(relayInformationClientProvider); try { final uri = Uri.parse(sync.community); final response = await client @@ -61,12 +62,36 @@ final meArchivedAgentsProvider = FutureProvider>((ref) async { timeout: const Duration(seconds: 3), ); sync.check(); - return await _parseArchives(events, authority); + return (await _parseArchives(events, authority))..remove(sync.pubkey); } catch (_) { return {}; } }); +/// [identityArchivesProvider] with the relay configuration (community and +/// account) it was read for. +final _scopedArchivesProvider = FutureProvider<(RelayConfig, Set)>(( + ref, +) async { + final config = ref.watch(relayConfigProvider); + return (config, await ref.watch(identityArchivesProvider.future)); +}); + +/// Identities to leave out of forward-looking choices (pickers, search and +/// the Agents page), as desktop's `archiveHides`: the known-archived ones. +/// None while the archive list loads or cannot be read. History (messages, +/// activity, profiles) keeps showing archived identities. +/// +/// A refresh keeps the last known set, but after a community or account +/// switch the previous set does not apply: nobody is hidden until the new +/// community's list arrives. +final hiddenArchivedIdentitiesProvider = Provider>((ref) { + final config = ref.watch(relayConfigProvider); + final known = ref.watch(_scopedArchivesProvider).value; + if (known == null || !identical(known.$1, config)) return const {}; + return known.$2; +}); + Future> _parseArchives(List events, String authority) => Isolate.run(() => _archived(events, authority)); diff --git a/mobile/lib/shared/me/me_assistants.dart b/mobile/lib/shared/me/me_assistants.dart index 1f3913cc96a..077a054ab57 100644 --- a/mobile/lib/shared/me/me_assistants.dart +++ b/mobile/lib/shared/me/me_assistants.dart @@ -10,7 +10,7 @@ import '../agents/remote_agents.dart'; import '../mentions/agent_identity_provider.dart'; import '../relay/relay.dart'; import 'me_agent_inventory.dart'; -import 'me_archived_agents.dart'; +import '../identity_archives/identity_archives.dart'; import 'me_sync.dart'; /// The owner's current inventory, with verified ownership and desktop's @@ -26,7 +26,7 @@ final meAssistantsProvider = FutureProvider>(( ref.watch(remoteAgentRegistrationRevisionProvider); // Refresh archive evidence with the inventory, including explicit retries. // Ordinary sheet openings reuse this provider's completed directory. - final archivedRequest = ref.refresh(meArchivedAgentsProvider.future); + final archivedRequest = ref.refresh(identityArchivesProvider.future); final inventory = await loadMeAgentInventory(sync); sync.check(); final profiles = await _loadProfiles(sync, inventory.keys.toList()); diff --git a/mobile/test/features/agents/agent_ordering_test.dart b/mobile/test/features/agents/agent_ordering_test.dart index 3fff34201aa..b6c82acbe90 100644 --- a/mobile/test/features/agents/agent_ordering_test.dart +++ b/mobile/test/features/agents/agent_ordering_test.dart @@ -1,7 +1,9 @@ import 'package:buzz/features/agents/agent_card.dart'; import 'package:buzz/features/agents/agents_page.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/profile/user_cache_provider.dart'; import 'package:buzz/shared/profile/user_profile.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -37,4 +39,46 @@ void main() { ['aardvark', 'alpha'], ); }); + + testWidgets('leaves archived agents out of the directory until restored', ( + tester, + ) async { + final h = await AgentsHarness.create(); + final existing = existingDesktopKeys.public; + var archived = {fakeAgent}; + await h.open( + tester, + directory: [existing, fakeAgent], + overrides: [ + identityArchivesProvider.overrideWith((ref) async => archived), + ], + ); + final container = ProviderScope.containerOf( + tester.element(find.byType(AgentsPage)), + ); + final profiles = container.read(userCacheProvider.notifier); + profiles.put(UserProfile(pubkey: existing, displayName: 'Zulu')); + profiles.put(UserProfile(pubkey: fakeAgent, displayName: 'alpha')); + await tester.pumpAndSettle(); + expect( + tester + .widgetList(find.byType(AgentCard)) + .map((card) => card.name), + ['Zulu'], + ); + // Restored elsewhere: pulling to refresh rereads the archive list. + archived = {}; + await tester.fling( + find.byType(CustomScrollView).first, + const Offset(0, 400), + 1000, + ); + await tester.pumpAndSettle(); + expect( + tester + .widgetList(find.byType(AgentCard)) + .map((card) => card.name), + ['alpha', 'Zulu'], + ); + }); } diff --git a/mobile/test/features/channels/add_members_sheet_names_test.dart b/mobile/test/features/channels/add_members_sheet_names_test.dart index 1a7e90e6830..5b357a5b123 100644 --- a/mobile/test/features/channels/add_members_sheet_names_test.dart +++ b/mobile/test/features/channels/add_members_sheet_names_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:buzz/features/channels/add_members_sheet.dart'; import 'package:buzz/features/channels/channel_management_provider.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/profile/user_cache_provider.dart'; import 'package:buzz/shared/profile/user_profile.dart'; import 'package:buzz/shared/relay/relay.dart'; @@ -190,4 +191,67 @@ void main() { ]); }, ); + + testWidgets( + 'leaves out an archived namesake, and shows both while archives are unknown', + (tester) async { + final live = 'a' * 64; + final archived = 'b' * 64; + Future> shownScouts( + Future> Function() archives, + ) async { + await tester.pumpWidget( + ProviderScope( + // A new scope per case, so no state is carried between them. + key: UniqueKey(), + overrides: [ + userCacheProvider.overrideWith(_RecordingUserCache.new), + channelMembersProvider('channel').overrideWith((ref) async => []), + relaySessionProvider.overrideWith( + () => RecipientDirectorySession( + {live: 'Scout', archived: 'Scout'}, + agents: {live, archived}, + ), + ), + identityArchivesProvider.overrideWith((ref) => archives()), + ], + child: MaterialApp( + theme: AppTheme.light(), + home: const Scaffold( + body: AddChannelMembersSheet( + channelId: 'channel', + existingPubkeys: {}, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.enterText( + find.byKey(const ValueKey('add-channel-members-search')), + 'Scout', + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + await tester.pumpAndSettle(); + return [ + for (final text in tester.widgetList(find.byType(Text))) + if (text.data?.startsWith('Scout') == true) text.data!, + ]; + } + + // The archived agent is not offered, and the live one keeps its + // plain name: a hidden namesake adds no qualifier. + expect(await shownScouts(() async => {archived}), ['Scout']); + + // A failed archive read hides no one: both agents are offered, told + // apart from each other. + final unknown = await shownScouts( + () => Future.error(StateError('relay information unavailable')), + ); + expect(unknown, hasLength(2)); + expect(unknown.toSet(), hasLength(2)); + expect(unknown, isNot(contains('Scout'))); + }, + ); } diff --git a/mobile/test/features/channels/channel_detail_page_test.dart b/mobile/test/features/channels/channel_detail_page_test.dart index 7757af969f0..bd23e37ae44 100644 --- a/mobile/test/features/channels/channel_detail_page_test.dart +++ b/mobile/test/features/channels/channel_detail_page_test.dart @@ -82,6 +82,7 @@ import 'package:buzz/shared/agents/owned_agent_directory.dart'; import 'package:buzz/shared/profile/user_cache_provider.dart'; import 'package:buzz/shared/profile/user_profile.dart'; import 'package:buzz/features/profile/user_profile_sheet.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/identity_names/identity_names.dart'; import 'package:buzz/shared/community/community_provider.dart'; import 'package:buzz/shared/emoji/emoji_burst.dart'; @@ -270,6 +271,7 @@ Widget _buildTestable({ Future> Function()? loadAgentOwners, UserCacheNotifier? userCacheNotifier, List members = const [], + Set? archived, List huddleMembers = const [], _MutableHuddleMembersNotifier? huddleMembersNotifier, Channel? channel, @@ -326,6 +328,8 @@ Widget _buildTestable({ retry: providerRetry ?? (disableRetries ? (_, _) => null : null), overrides: [ ownedCodexVoiceAgentsProvider.overrideWith((ref) async => voiceAgents), + if (archived != null) + identityArchivesProvider.overrideWith((ref) async => archived), if (bestie != null) defaultAgentProvider.overrideWith((ref) => Stream.value(bestie)), if (sender != null) sendMessageProvider.overrideWithValue(sender), @@ -4642,6 +4646,61 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('previews archived members after live ones, marked', ( + tester, + ) async { + await tester.pumpWidget( + _buildTestable( + messages: const [], + archived: {'alice'}, + members: [ + for (final (key, role, name) in [ + ('self', 'owner', 'Self'), + ('alice', 'member', 'Alice'), + ('agent', 'bot', 'Agent'), + ('bob', 'member', 'Bob'), + ]) + ChannelMember( + pubkey: key, + role: role, + joinedAt: DateTime(2025), + displayName: name, + ), + ], + ), + ); + await tester.pumpAndSettle(); + await tester.tap( + find.byKey(const ValueKey('channel-header-settings-trigger')), + ); + await tester.pumpAndSettle(); + + final rows = [ + // People, agents, then archived Alice, as in the members sheet. + for (final key in ['self', 'bob', 'agent', 'alice']) + find.byKey(ValueKey('channel-details-member-$key')), + ]; + final tops = [for (final row in rows) tester.getTopLeft(row).dy]; + expect(tops, orderedEquals([...tops]..sort())); + expect( + find.descendant( + of: rows[3], + matching: find.textContaining( + 'Archived \u00b7 Member', + findRichText: true, + ), + ), + findsOne, + ); + expect( + find.descendant( + of: rows[1], + matching: find.textContaining('Archived', findRichText: true), + ), + findsNothing, + ); + }); + testWidgets('previews five members before an icon-free See all row', ( tester, ) async { diff --git a/mobile/test/features/channels/members_sheet_test.dart b/mobile/test/features/channels/members_sheet_test.dart index e464d9efebe..da90fad951f 100644 --- a/mobile/test/features/channels/members_sheet_test.dart +++ b/mobile/test/features/channels/members_sheet_test.dart @@ -5,6 +5,7 @@ import 'package:buzz/features/channels/channel_management_provider.dart'; import 'package:buzz/features/channels/members_sheet.dart'; import 'package:buzz/features/profile/user_status.dart'; import 'package:buzz/features/profile/user_status_cache_provider.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/identity_names/identity_names.dart'; import 'package:buzz/shared/profile/user_cache_provider.dart'; import 'package:buzz/shared/profile/user_profile.dart'; @@ -34,6 +35,8 @@ Future _pump( WidgetTester tester, List members, { ValueChanged? onTap, + Set archived = const {}, + Set working = const {}, }) async { final names = IdentityNameSources.empty.scope( members.map((m) => m.pubkey), @@ -46,7 +49,8 @@ Future _pump( channelIdentityNamesProvider('channel').overrideWith((ref) => names), userCacheProvider.overrideWith(_Cache.new), userStatusCacheProvider.overrideWith(_Statuses.new), - workingBotPubkeysProvider('channel').overrideWith((ref) => {}), + workingBotPubkeysProvider('channel').overrideWith((ref) => working), + identityArchivesProvider.overrideWith((ref) async => archived), ], child: MaterialApp( theme: AppTheme.light(), @@ -72,7 +76,13 @@ Future _pump( ), ), ); - await tester.pumpAndSettle(); + if (working.isEmpty) { + await tester.pumpAndSettle(); + } else { + // A working indicator animates forever, so settle the data only. + await tester.pump(); + await tester.pump(); + } } List _roster(int people, int bots) => [ @@ -160,6 +170,54 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('archived members get their own section and say so', ( + tester, + ) async { + final semantics = tester.ensureSemantics(); + // Person 1 and Agent 3 are archived. + await _pump( + tester, + _roster(3, 2), + archived: {_key(1), _key(3)}, + working: {_key(3), _key(4)}, + ); + List titles() => [ + for (final tile in tester.widgetList(find.byType(ListTile))) + (tile.title! as Text).data!, + ]; + // Archived people, then archived agents, after People and Agents. + expect(find.text('People \u00b7 2'), findsOne); + expect(find.text('Agents \u00b7 1'), findsOne); + expect(find.text('Archived \u00b7 2'), findsOne); + expect(titles(), ['You', 'Person 2', 'Agent 4', 'Person 1', 'Agent 3']); + expect( + tester.getTopLeft(find.text('Archived \u00b7 2')).dy, + greaterThan(tester.getTopLeft(find.text('Agent 4')).dy), + ); + String subtitle(String name) => + ((tester + .widget( + find.ancestor( + of: find.text(name), + matching: find.byType(ListTile), + ), + ) + .subtitle)! + as Text) + .data!; + expect(subtitle('Person 1'), 'Archived \u00b7 Member'); + // An archived agent says Archived even while it reads as working. + expect(subtitle('Agent 3'), 'Archived \u00b7 Agent'); + expect(subtitle('Person 2'), 'Member'); + expect(find.text('Working\u2026'), findsOne); + expect(find.bySemanticsLabel(RegExp(r'Person 1[\s\S]*Archived')), findsOne); + expect( + find.bySemanticsLabel(RegExp(r'Person 2[\s\S]*Archived')), + findsNothing, + ); + semantics.dispose(); + }); + testWidgets('owner actions stay available after lazy scrolling', ( tester, ) async { diff --git a/mobile/test/features/search/search_channels_journey_test.dart b/mobile/test/features/search/search_channels_journey_test.dart index 09ae96f7652..8fbd5b793fc 100644 --- a/mobile/test/features/search/search_channels_journey_test.dart +++ b/mobile/test/features/search/search_channels_journey_test.dart @@ -7,6 +7,7 @@ import 'package:buzz/features/profile/profile_provider.dart'; import 'package:buzz/features/search/recent_searches_provider.dart'; import 'package:buzz/features/search/search_page.dart'; import 'package:buzz/features/search/search_provider.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/profile/user_profile.dart'; import 'package:buzz/shared/relay/relay.dart'; import 'package:buzz/shared/widgets/buzz_loading_indicator.dart'; @@ -602,6 +603,56 @@ void main() { expect(find.text('ops-room'), findsOneWidget); }, ); + + testWidgets('archived people do not crowd live people out of People', ( + tester, + ) async { + // Tall enough that every row is built. + tester.view.physicalSize = const Size(800, 4000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final session = _HeldSession(); + final actions = _HeldUserSearch(); + // The relay ranks more archived matches first than the page shows. + final archived = [ + for (var i = 0; i <= searchPeopleShown; i++) i.toRadixString(16) * 64, + ]; + final live = [for (var i = 0; i < searchPeopleShown; i++) 'f$i' * 32]; + + await tester.pumpWidget( + WidgetHelpers.testable( + overrides: [ + relayConfigProvider.overrideWith(_Community.new), + relaySessionProvider.overrideWith(() => session), + channelActionsProvider.overrideWith((ref) => actions), + channelsProvider.overrideWith(_NoMemberChannels.new), + recentSearchesProvider.overrideWith(_NoRecentSearches.new), + profileProvider.overrideWith(_Profile.new), + identityArchivesProvider.overrideWith((ref) async => {...archived}), + ], + child: const SearchPage(), + ), + ); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'scout'); + await tester.pump(const Duration(milliseconds: 300)); + for (final search in session.searches) { + search.complete(const []); + } + actions.searches.removeAt(0).complete([ + for (final key in [...archived, ...live]) + DirectoryUser(pubkey: key, displayName: 'Scout ${key.substring(0, 2)}'), + ]); + await tester.pumpAndSettle(); + + for (final key in archived) { + expect(find.byKey(ValueKey('search-person-row-$key')), findsNothing); + } + for (final key in live) { + expect(find.byKey(ValueKey('search-person-row-$key')), findsOneWidget); + } + }); } Channel _channel(String name, {required bool isMember}) => Channel( @@ -645,11 +696,12 @@ class _HeldSession extends RelaySessionNotifier { class _HeldUserSearch implements ChannelActions { final searches = >>[]; + /// Answers with at most [limit] people, as the relay does. @override Future> searchUsers(String query, {int limit = 8}) { final search = Completer>(); searches.add(search); - return search.future; + return search.future.then((users) => users.take(limit).toList()); } @override diff --git a/mobile/test/features/search/search_page_lazy_test.dart b/mobile/test/features/search/search_page_lazy_test.dart index 56f88b74664..db087e0daf7 100644 --- a/mobile/test/features/search/search_page_lazy_test.dart +++ b/mobile/test/features/search/search_page_lazy_test.dart @@ -174,7 +174,8 @@ void main() { await tester.pumpAndSettle(); final key = category == 'Channels' ? 'search-channel-row-channel-29' - : 'search-person-row-person-29'; + // The page shows at most [searchPeopleShown] people. + : 'search-person-row-person-${searchPeopleShown - 1}'; final last = find.byKey(ValueKey(key)); expect(last, findsNothing); await tester.scrollUntilVisible( diff --git a/mobile/test/features/search/search_page_test.dart b/mobile/test/features/search/search_page_test.dart index 4fa4b6c964c..291142ceec2 100644 --- a/mobile/test/features/search/search_page_test.dart +++ b/mobile/test/features/search/search_page_test.dart @@ -1,3 +1,4 @@ +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/widgets/buzz_agent_icon.dart'; import 'package:buzz/features/channels/channel.dart'; import 'package:buzz/features/channels/channel_detail_page.dart'; @@ -568,13 +569,20 @@ void main() { }); testWidgets('same-name people results get distinct labels', (tester) async { - final first = 'a' * 64, second = 'b' * 64; + final first = 'a' * 64, second = 'b' * 64, archived = 'c' * 64; + // Tall enough that every row is built. + tester.view.physicalSize = const Size(800, 4000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); final state = SearchState( query: 'scout', channelResults: const [], userResults: [ + DirectoryUser(pubkey: archived, displayName: 'Scout'), DirectoryUser(pubkey: first, displayName: 'Scout'), DirectoryUser(pubkey: second, displayName: 'Scout'), + for (var i = 0; i < 8; i++) + DirectoryUser(pubkey: '$i' * 64, displayName: 'Scout fan $i'), ], messageResults: const [], ); @@ -592,6 +600,7 @@ void main() { UserProfile(pubkey: first, displayName: 'Scout'), ), ), + identityArchivesProvider.overrideWith((ref) async => {archived}), ], child: const SearchPage(), ), @@ -603,6 +612,19 @@ void main() { .data!; expect(title(first), isNot(title(second))); expect(title(first), isNot('Scout')); + // An archived person is left out, and does not take a live person's + // place among the eight rows shown. + final rows = find.byWidgetPredicate( + (widget) => + widget.key is ValueKey && + (widget.key! as ValueKey).value.startsWith( + 'search-person-row-', + ), + ); + expect(find.byKey(ValueKey('search-person-row-$archived')), findsNothing); + expect(rows, findsNWidgets(searchPeopleShown)); + expect(find.byKey(ValueKey('search-person-row-${'5' * 64}')), findsOne); + expect(find.byKey(ValueKey('search-person-row-${'6' * 64}')), findsNothing); }); testWidgets('uses compact content styles and keeps message time by author', ( diff --git a/mobile/test/shared/agents/remote_agents_test.dart b/mobile/test/shared/agents/remote_agents_test.dart index 96ea0919cd2..68e35eb6b7c 100644 --- a/mobile/test/shared/agents/remote_agents_test.dart +++ b/mobile/test/shared/agents/remote_agents_test.dart @@ -11,7 +11,7 @@ import 'package:buzz/shared/hosted/account_identity_store.dart'; import 'package:buzz/shared/hosted/builderlab_account.dart'; import 'package:buzz/shared/hosted/hosted_environment.dart'; import 'package:buzz/shared/hosted/hosted_relay_auth.dart'; -import 'package:buzz/shared/me/me_archived_agents.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/me/me_assistants.dart'; import 'package:buzz/shared/me/me_sync.dart'; import 'package:buzz/shared/relay/relay.dart'; @@ -1020,7 +1020,7 @@ void main() { RelayConfig(baseUrl: 'https://relay.test', nsec: fakeOwner.nsec), ), ), - meArchivedAgentsProvider.overrideWith((ref) async => {}), + identityArchivesProvider.overrideWith((ref) async => {}), remoteAgentControllerProvider.overrideWithValue(controller), ], ); diff --git a/mobile/test/shared/me/me_archived_agents_test.dart b/mobile/test/shared/identity_archives/identity_archives_test.dart similarity index 55% rename from mobile/test/shared/me/me_archived_agents_test.dart rename to mobile/test/shared/identity_archives/identity_archives_test.dart index 99a53534ba7..230a4c61898 100644 --- a/mobile/test/shared/me/me_archived_agents_test.dart +++ b/mobile/test/shared/identity_archives/identity_archives_test.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:buzz/shared/me/me_archived_agents.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/me/me_sync.dart'; import 'package:buzz/shared/relay/relay.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -47,12 +47,12 @@ void main() { RelayConfig(baseUrl: 'https://relay.test', nsec: owner.nsec), ), ), - meRelayInformationClientProvider.overrideWithValue(client), + relayInformationClientProvider.overrideWithValue(client), ], ); addTearDown(container.dispose); addTearDown(client.close); - expect(await container.read(meArchivedAgentsProvider.future), {target}); + expect(await container.read(identityArchivesProvider.future), {target}); expect(relay.author, authority.public); for (final invalid in [ snapshot(impostor), @@ -60,12 +60,79 @@ void main() { NostrEvent.fromJson(snapshot(authority).toJson()..['sig'] = '0' * 128), ]) { relay.event = invalid; - container.invalidate(meArchivedAgentsProvider); - expect(await container.read(meArchivedAgentsProvider.future), isEmpty); + container.invalidate(identityArchivesProvider); + expect(await container.read(identityArchivesProvider.future), isEmpty); } relay.event = snapshot(authority); - container.invalidate(meArchivedAgentsProvider); - expect(await container.read(meArchivedAgentsProvider.future), {target}); + container.invalidate(identityArchivesProvider); + expect(await container.read(identityArchivesProvider.future), {target}); + }, + ); + + test( + 'hides archived identities for the current community only, never the viewer', + () async { + final viewer = nostr.Keys.generate(); + final other = nostr.Keys.generate(); + final authority = nostr.Keys.generate(); + final archivedInA = 'a' * 64; + final archivedInB = 'b' * 64; + NostrEvent snapshot(List keys) => NostrEvent.fromJson( + nostr.Event.from( + kind: 13535, + content: '', + secretKey: authority.secret, + tags: [ + ['-'], + for (final key in keys) ['p', key], + ], + ).toMap(), + ); + // Community A's list also names the viewer, who must stay visible. + final relay = _ArchiveRelay(snapshot([archivedInA, viewer.public])); + final communityB = Completer(); + final client = MockClient((request) async { + if (request.url.host == 'b.test') await communityB.future; + return http.Response(jsonEncode({'self': authority.public}), 200); + }); + final container = ProviderContainer( + overrides: [ + relayConfigProvider.overrideWith( + () => _Config( + RelayConfig(baseUrl: 'https://a.test', nsec: viewer.nsec), + ), + ), + meSyncProvider.overrideWith( + (ref) => MeSync(relay, ref.watch(relayConfigProvider)), + ), + relayInformationClientProvider.overrideWithValue(client), + ], + ); + addTearDown(container.dispose); + addTearDown(client.close); + container.listen(hiddenArchivedIdentitiesProvider, (_, _) {}); + Future> settled() async { + await container.read(identityArchivesProvider.future); + await pumpEventQueue(); + return container.read(hiddenArchivedIdentitiesProvider); + } + + expect(await settled(), {archivedInA}); + // A refresh in the same community keeps the known set meanwhile. + container.invalidate(identityArchivesProvider); + expect(container.read(hiddenArchivedIdentitiesProvider), {archivedInA}); + expect(await settled(), {archivedInA}); + + // Switch community and account: A's list no longer applies, so nobody + // is hidden until B's list arrives. + relay.event = snapshot([archivedInB]); + container + .read(relayConfigProvider.notifier) + .update(baseUrl: 'https://b.test', nsec: other.nsec); + await pumpEventQueue(); + expect(container.read(hiddenArchivedIdentitiesProvider), isEmpty); + communityB.complete(); + expect(await settled(), {archivedInB}); }, ); @@ -85,7 +152,7 @@ void main() { RelayConfig(baseUrl: 'https://relay.test', nsec: owner.nsec), ), ), - meRelayInformationClientProvider.overrideWithValue(client), + relayInformationClientProvider.overrideWithValue(client), ], ); addTearDown(container.dispose); @@ -96,7 +163,7 @@ void main() { }); addTearDown(trickle.cancel); final result = await container - .read(meArchivedAgentsProvider.future) + .read(identityArchivesProvider.future) .timeout(const Duration(seconds: 5)); expect(result, isEmpty); expect(cancelled, isTrue); @@ -119,12 +186,12 @@ void main() { RelayConfig(baseUrl: 'https://relay.test', nsec: owner.nsec), ), ), - meRelayInformationClientProvider.overrideWithValue(client), + relayInformationClientProvider.overrideWithValue(client), ], ); addTearDown(container.dispose); addTearDown(client.close); - expect(await container.read(meArchivedAgentsProvider.future), isEmpty); + expect(await container.read(identityArchivesProvider.future), isEmpty); expect(relay.author, isNull); }, ); @@ -144,3 +211,10 @@ class _ArchiveRelay extends RelaySessionNotifier { return [?event]; } } + +class _Config extends RelayConfigNotifier { + _Config(this.initial); + final RelayConfig initial; + @override + RelayConfig build() => initial; +} diff --git a/mobile/test/shared/me/me_assistants_test.dart b/mobile/test/shared/me/me_assistants_test.dart index b370edc86ac..90c805823a3 100644 --- a/mobile/test/shared/me/me_assistants_test.dart +++ b/mobile/test/shared/me/me_assistants_test.dart @@ -3,7 +3,7 @@ import 'package:buzz/features/channels/dm_voice_agent_provider.dart'; import 'dart:typed_data'; import 'package:buzz/shared/agents/owned_agent_directory.dart'; import 'package:buzz/shared/me/me_assistants.dart'; -import 'package:buzz/shared/me/me_archived_agents.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/me/me_agent_inventory.dart'; import 'package:buzz/shared/me/me_sync.dart'; import 'package:buzz/shared/relay/relay.dart'; @@ -139,7 +139,7 @@ void main() { overrides: [ relaySessionProvider.overrideWith(() => relay), meSyncProvider.overrideWithValue(sync), - meArchivedAgentsProvider.overrideWith((ref) async => archived), + identityArchivesProvider.overrideWith((ref) async => archived), if (viewer != null) myPubkeyProvider.overrideWithValue(viewer), ], ); diff --git a/mobile/test/shared/me/me_sync_test.dart b/mobile/test/shared/me/me_sync_test.dart index 718a8666af2..8cde0fed64d 100644 --- a/mobile/test/shared/me/me_sync_test.dart +++ b/mobile/test/shared/me/me_sync_test.dart @@ -9,7 +9,7 @@ import 'package:buzz/shared/crypto/nip44.dart'; import 'package:buzz/shared/me/me_groups.dart'; import 'package:buzz/shared/me/me_chat_creation.dart'; import 'package:buzz/shared/me/me_assistants.dart'; -import 'package:buzz/shared/me/me_archived_agents.dart'; +import 'package:buzz/shared/identity_archives/identity_archives.dart'; import 'package:buzz/shared/me/me_sync.dart'; import 'package:buzz/shared/me/me_recovery.dart'; import 'package:buzz/shared/relay/relay.dart'; @@ -204,7 +204,7 @@ void main() { final config = _Config(RelayConfig(baseUrl: origin, nsec: nsec)); final container = ProviderContainer( overrides: [ - meArchivedAgentsProvider.overrideWith((ref) async => {}), + identityArchivesProvider.overrideWith((ref) async => {}), relayConfigProvider.overrideWith(() => config), relaySessionProvider.overrideWith(() => relay), savedPrefsProvider.overrideWithValue(prefs),