9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
108 lines
4.0 KiB
Dart
108 lines
4.0 KiB
Dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import '../../../shared/crypto/nip_oa.dart';
|
|
import '../../../shared/mentions/agent_identity_provider.dart';
|
|
import '../../../shared/relay/relay.dart';
|
|
import '../../profile/user_cache_provider.dart';
|
|
import '../../profile/user_profile.dart';
|
|
import '../channel.dart';
|
|
import '../channel_management_provider.dart';
|
|
import '../channels_provider.dart';
|
|
import 'mention_candidates.dart';
|
|
import 'mention_ranking.dart';
|
|
|
|
/// Debounce before a mention query hits the relay search endpoint.
|
|
const _mentionSearchDebounce = Duration(milliseconds: 250);
|
|
|
|
/// Global user search for mention autocomplete — kind:0 prefix search via
|
|
/// the relay HTTP bridge. Mirrors desktop's `useInfiniteUserSearchQuery`
|
|
/// feeding `useMentions` (source 4: people and agents outside the channel).
|
|
///
|
|
/// Debounced: the provider waits [_mentionSearchDebounce] before querying;
|
|
/// keystrokes dispose the stale family member so its request never fires.
|
|
final mentionUserSearchProvider = FutureProvider.autoDispose
|
|
.family<List<UserProfile>, String>((ref, query) async {
|
|
final trimmed = query.trim();
|
|
if (trimmed.isEmpty) return const [];
|
|
|
|
var disposed = false;
|
|
ref.onDispose(() => disposed = true);
|
|
await Future<void>.delayed(_mentionSearchDebounce);
|
|
if (disposed) return const [];
|
|
|
|
final session = ref.read(relaySessionProvider.notifier);
|
|
final events = await session.queryRelay([
|
|
NostrFilters.searchUsers(trimmed),
|
|
]);
|
|
|
|
// Keep only the latest kind:0 event per pubkey (the bridge does not
|
|
// honor the `kinds` filter under search, and may return several
|
|
// profile revisions — mirrors desktop's `list_user_search_results`).
|
|
final latestByPubkey = <String, NostrEvent>{};
|
|
for (final event in events) {
|
|
if (event.kind != 0) continue;
|
|
final pk = event.pubkey.toLowerCase();
|
|
final current = latestByPubkey[pk];
|
|
if (current == null || event.createdAt > current.createdAt) {
|
|
latestByPubkey[pk] = event;
|
|
}
|
|
}
|
|
|
|
return [
|
|
for (final event in latestByPubkey.values) _profileFromEvent(event),
|
|
];
|
|
});
|
|
|
|
UserProfile _profileFromEvent(NostrEvent event) {
|
|
final data = ProfileData.fromEvent(event);
|
|
return UserProfile(
|
|
pubkey: event.pubkey.toLowerCase(),
|
|
displayName: data.displayName,
|
|
avatarUrl: data.avatarUrl,
|
|
about: data.about,
|
|
nip05Handle: data.nip05,
|
|
ownerPubkey: verifiedOaOwnerPubkey(event.tags, event.pubkey),
|
|
);
|
|
}
|
|
|
|
/// Ranked mention candidates for a channel + query. Channel members first,
|
|
/// then non-member relay agents the user can actually reach, then global
|
|
/// search results; ordering matches desktop's `rankMentionCandidates`.
|
|
final mentionCandidatesProvider = Provider.family
|
|
.autoDispose<List<MentionCandidate>, ({String channelId, String query})>((
|
|
ref,
|
|
args,
|
|
) {
|
|
final members =
|
|
ref.watch(channelMembersProvider(args.channelId)).asData?.value ??
|
|
const <ChannelMember>[];
|
|
final relayAgents =
|
|
ref.watch(agentDirectoryProvider).asData?.value ??
|
|
const <AgentDirectoryEntry>[];
|
|
final owners = ref.watch(agentOwnersProvider).asData?.value ?? const {};
|
|
final channels =
|
|
ref.watch(channelsProvider).asData?.value ?? const <Channel>[];
|
|
final userCache = ref.watch(userCacheProvider);
|
|
final currentPubkey = ref.watch(currentPubkeyProvider);
|
|
final searchResults =
|
|
ref.watch(mentionUserSearchProvider(args.query)).asData?.value ??
|
|
const <UserProfile>[];
|
|
|
|
final sharedChannelIds = {
|
|
for (final channel in channels)
|
|
if (channel.isMember && !channel.isArchived) channel.id,
|
|
};
|
|
|
|
final candidates = buildMentionCandidates(
|
|
members: members,
|
|
relayAgents: relayAgents,
|
|
sharedChannelIds: sharedChannelIds,
|
|
userCache: userCache,
|
|
ownerByAgentPubkey: owners,
|
|
searchResults: searchResults,
|
|
currentPubkey: currentPubkey,
|
|
);
|
|
|
|
return rankMentionCandidates(candidates, args.query);
|
|
});
|