-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Hide Huddles in mobile agent DMs #6676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6f729de
8dc0655
61918cc
91bc8b5
e2130c4
7ffb04b
2f02577
1149640
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,20 +107,59 @@ Future<void> _loadDeepLinkEvents( | |
| } | ||
|
|
||
| /// Fetch channel members and preload their profiles into the user cache. | ||
| Future<void> _preloadMembers(WidgetRef ref, String channelId) async { | ||
| /// Returns whether identity resolution completed successfully. | ||
| Future<bool> _preloadMembers( | ||
| WidgetRef ref, | ||
| String channelId, | ||
| List<String> participantPubkeys, | ||
| ) async { | ||
| // Capture references before async gap to avoid using disposed ref. | ||
| final notifier = ref.read(userCacheProvider.notifier); | ||
| try { | ||
| final members = await ref.read(channelMembersProvider(channelId).future); | ||
| final pubkeys = members.map((m) => m.pubkey).toList(); | ||
| final pubkeys = { | ||
| ...members.map((member) => member.pubkey), | ||
| ...participantPubkeys, | ||
| }.toList(); | ||
| if (pubkeys.isNotEmpty) { | ||
| notifier.preload(pubkeys); | ||
| return notifier.preload(pubkeys); | ||
| } | ||
| return true; | ||
| } catch (_) { | ||
| // Non-fatal — mentions will just fall back to cache from messages. | ||
| // Identity remains unresolved, so agent-only actions stay hidden. | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| Future<void Function()> _subscribeToDmIdentityUpdates( | ||
| WidgetRef ref, | ||
| List<String> participantPubkeys, { | ||
| required VoidCallback onFailure, | ||
| }) async { | ||
| final session = ref.read(relaySessionProvider.notifier); | ||
| return session.subscribe( | ||
| NostrFilter( | ||
| kinds: const [0, 10100], | ||
| authors: participantPubkeys, | ||
| limit: 100, | ||
| ).copyWithSince(DateTime.now().millisecondsSinceEpoch ~/ 1000 - 5), | ||
| (event) { | ||
| if (event.kind == 0) { | ||
| try { | ||
| ref.read(userCacheProvider.notifier).cacheProfileEvent(event); | ||
| } catch (error) { | ||
| debugPrint('[DmIdentity] invalid live profile: $error'); | ||
| onFailure(); | ||
| } | ||
| } else if (event.kind == 10100) { | ||
| ref.invalidate(agentDirectoryProvider); | ||
| ref.invalidate(agentOwnersProvider); | ||
| } | ||
| }, | ||
| onClosed: (_) => onFailure(), | ||
| ); | ||
| } | ||
|
|
||
| int? _channelReadTimestamp({ | ||
| required Channel channel, | ||
| required AsyncValue<List<NostrEvent>> messagesState, | ||
|
|
@@ -146,6 +185,16 @@ int? _channelReadTimestamp({ | |
| return dateTimeToUnixSeconds(channel.lastMessageAt); | ||
| } | ||
|
|
||
| bool _isOneToOneAgentDm(Channel channel, Set<String> agentPubkeys) { | ||
| final participants = channel.participantPubkeys | ||
| .map((pubkey) => pubkey.trim().toLowerCase()) | ||
| .where((pubkey) => pubkey.isNotEmpty) | ||
| .toSet(); | ||
| return channel.isDm && | ||
| participants.length == 2 && | ||
| participants.any(agentPubkeys.contains); | ||
| } | ||
|
|
||
| /// Controls how a hydrated initial thread is added to the navigation stack. | ||
| enum InitialThreadRouteBehavior { | ||
| /// Keep the channel route beneath the thread. | ||
|
|
@@ -256,10 +305,124 @@ class ChannelDetailPage extends HookConsumerWidget { | |
| channel; | ||
| final resolvedChannel = | ||
| detailsAsync.whenData(baseChannel.mergeDetails).value ?? baseChannel; | ||
| final memberProfilesPreload = useMemoized( | ||
| () => _preloadMembers( | ||
| ref, | ||
| resolvedChannel.id, | ||
| resolvedChannel.participantPubkeys, | ||
| ), | ||
| [ | ||
| resolvedChannel.id, | ||
| sessionStatus, | ||
| Object.hashAll(resolvedChannel.participantPubkeys), | ||
| ], | ||
| ); | ||
|
klopez4212 marked this conversation as resolved.
|
||
| final memberProfilesPreloadState = useFuture(memberProfilesPreload); | ||
| final showsComposer = | ||
| !resolvedChannel.isForum && | ||
| resolvedChannel.isMember && | ||
| !resolvedChannel.isArchived; | ||
| final profileOwnedAgentPubkeys = <String>[]; | ||
| for (final participantPubkey in resolvedChannel.participantPubkeys) { | ||
| final normalized = participantPubkey.trim().toLowerCase(); | ||
| final isProfileOwnedAgent = ref.watch( | ||
| userCacheProvider.select( | ||
| (cache) => cache[normalized]?.ownerPubkey != null, | ||
| ), | ||
| ); | ||
| if (isProfileOwnedAgent) profileOwnedAgentPubkeys.add(normalized); | ||
| } | ||
| final agentDirectoryState = ref.watch(agentDirectoryProvider); | ||
| final agentOwnersState = ref.watch(agentOwnersProvider); | ||
|
klopez4212 marked this conversation as resolved.
|
||
| final channelBotPubkeysState = ref.watch( | ||
| channelBotPubkeysProvider(resolvedChannel.id), | ||
| ); | ||
|
klopez4212 marked this conversation as resolved.
|
||
| final agentPubkeys = agentPubkeysWithChannelBots( | ||
| knownAgentPubkeys: agentPubkeysWithProfileOwners( | ||
| knownAgentPubkeys: ref.watch(knownAgentPubkeysProvider), | ||
| profileOwnedAgentPubkeys: profileOwnedAgentPubkeys, | ||
| ), | ||
| channelBotPubkeys: | ||
| channelBotPubkeysState.asData?.value ?? const <String>{}, | ||
| ); | ||
| final participantCount = resolvedChannel.participantPubkeys | ||
| .map((pubkey) => pubkey.trim().toLowerCase()) | ||
| .where((pubkey) => pubkey.isNotEmpty) | ||
| .toSet() | ||
| .length; | ||
| final isOneToOneDm = resolvedChannel.isDm && participantCount == 2; | ||
| final identitySubscriptionPubkeys = isOneToOneDm | ||
| ? (resolvedChannel.participantPubkeys | ||
| .map((pubkey) => pubkey.trim().toLowerCase()) | ||
| .where((pubkey) => pubkey.isNotEmpty) | ||
| .toSet() | ||
| .toList() | ||
| ..sort()) | ||
| : const <String>[]; | ||
| final identitySubscriptionKey = Object.hashAll(identitySubscriptionPubkeys); | ||
| final identitySubscriptionReady = useValueNotifier(false, [ | ||
| sessionStatus, | ||
| resolvedChannel.id, | ||
| identitySubscriptionKey, | ||
| ]); | ||
| final isIdentitySubscriptionReady = useValueListenable( | ||
| identitySubscriptionReady, | ||
| ); | ||
| useEffect(() { | ||
| if (sessionStatus != SessionStatus.connected || | ||
| identitySubscriptionPubkeys.isEmpty) { | ||
| return null; | ||
| } | ||
| var disposed = false; | ||
| var subscriptionFailed = false; | ||
| void markFailed() { | ||
| subscriptionFailed = true; | ||
| if (!disposed) identitySubscriptionReady.value = false; | ||
| } | ||
|
|
||
| void Function()? unsubscribe; | ||
| Future.microtask(() async { | ||
| try { | ||
| final cleanup = await _subscribeToDmIdentityUpdates( | ||
| ref, | ||
| identitySubscriptionPubkeys, | ||
| onFailure: markFailed, | ||
| ); | ||
| if (disposed) { | ||
| cleanup(); | ||
| } else { | ||
| unsubscribe = cleanup; | ||
| if (!subscriptionFailed) identitySubscriptionReady.value = true; | ||
| } | ||
| } catch (error) { | ||
| if (!disposed) { | ||
| debugPrint('[DmIdentity] live subscription failed: $error'); | ||
| markFailed(); | ||
| } | ||
| } | ||
| }); | ||
| return () { | ||
| disposed = true; | ||
| unsubscribe?.call(); | ||
| }; | ||
| }, [sessionStatus, resolvedChannel.id, identitySubscriptionKey]); | ||
| final isAgentIdentityUnresolved = | ||
| isOneToOneDm && | ||
| ((sessionStatus == SessionStatus.connected && | ||
| !isIdentitySubscriptionReady) || | ||
| agentDirectoryState.isLoading || | ||
| agentDirectoryState.hasError || | ||
| agentOwnersState.isLoading || | ||
| agentOwnersState.hasError || | ||
| channelBotPubkeysState.isLoading || | ||
| channelBotPubkeysState.hasError || | ||
|
Comment on lines
+417
to
+418
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the kind:39002 live subscription fails to start or is closed terminally after the initial history fetch succeeds, this state remains successful and the Huddle action is enabled for a currently-human peer. Fresh evidence beyond the history-fetch error fix is that Useful? React with 👍 / 👎. |
||
| memberProfilesPreloadState.connectionState != | ||
| ConnectionState.done || | ||
| memberProfilesPreloadState.data != true); | ||
| final showsHuddleAction = | ||
| showsComposer && | ||
| !isAgentIdentityUnresolved && | ||
| !_isOneToOneAgentDm(resolvedChannel, agentPubkeys); | ||
| final messagesNotifier = ref.read( | ||
| channelMessagesProvider(channel.id).notifier, | ||
| ); | ||
|
|
@@ -301,12 +464,6 @@ class ChannelDetailPage extends HookConsumerWidget { | |
| return session.registerVisibleChannel(channel.id); | ||
| }, [channel.id]); | ||
|
|
||
| // Preload channel member profiles so @mentions resolve correctly. | ||
| useEffect(() { | ||
| _preloadMembers(ref, channel.id); | ||
| return null; | ||
| }, [channel.id]); | ||
|
|
||
| useEffect( | ||
| () { | ||
| if (channel.isForum) return null; | ||
|
|
@@ -394,7 +551,7 @@ class ChannelDetailPage extends HookConsumerWidget { | |
| ), | ||
| actions: resolvedChannel.isDm | ||
| ? [ | ||
| if (showsComposer) | ||
| if (showsHuddleAction) | ||
| _HuddleButton( | ||
| channel: resolvedChannel, | ||
| events: [ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.