import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/user_mention_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Builder function for building a mention tile. /// /// Use [UserMentionTile] for the default implementation. typedef MentionTileBuilder = Widget Function( BuildContext context, User user, ); /// Overlay for displaying users that can be mentioned. class UserMentionsOverlay extends StatefulWidget { /// Constructor for creating a [UserMentionsOverlay]. UserMentionsOverlay({ Key? key, required this.query, required this.channel, required this.size, this.client, this.limit = 10, this.mentionAllAppUsers = false, this.mentionsTileBuilder, this.onMentionUserTap, }) : assert( channel.state != null, 'Channel ${channel.cid} is not yet initialized', ), assert( !mentionAllAppUsers || (mentionAllAppUsers && client != null), 'StreamChatClient is required in order to use mentionAllAppUsers', ), super(key: key); /// Query for searching users. final String query; /// Limit applied on user search results. final int limit; /// The size of the overlay. final Size size; /// The channel to search for users. final Channel channel; /// The client to search for users in case [mentionAllAppUsers] is True. final StreamChatClient? client; /// When enabled mentions search users across the entire app. /// /// Defaults to false. final bool mentionAllAppUsers; /// Customize the tile for the mentions overlay. final MentionTileBuilder? mentionsTileBuilder; /// Callback called when a user is selected. final void Function(User user)? onMentionUserTap; @override _UserMentionsOverlayState createState() => _UserMentionsOverlayState(); } class _UserMentionsOverlayState extends State { late Future> userMentionsFuture; late List initialMentions = membersAndWatchers; @override void initState() { super.initState(); userMentionsFuture = queryMentions(widget.query); } @override void didUpdateWidget(covariant UserMentionsOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (widget.channel != oldWidget.channel || widget.query != oldWidget.query || widget.mentionAllAppUsers != oldWidget.mentionAllAppUsers || widget.limit != oldWidget.limit) { userMentionsFuture = queryMentions(widget.query); } } @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); return Card( margin: const EdgeInsets.all(8), elevation: 2, color: theme.colorTheme.barsBg, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), clipBehavior: Clip.hardEdge, child: Container( constraints: BoxConstraints.loose(widget.size), decoration: BoxDecoration(color: theme.colorTheme.barsBg), child: FutureBuilder>( future: userMentionsFuture, initialData: initialMentions, builder: (context, snapshot) { final users = snapshot.data!; return ListView.builder( padding: const EdgeInsets.all(0), shrinkWrap: true, itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return Material( color: theme.colorTheme.barsBg, child: InkWell( onTap: () => widget.onMentionUserTap?.call(user), child: widget.mentionsTileBuilder?.call(context, user) ?? UserMentionTile(user), ), ); }, ); }, ), ), ); } List get membersAndWatchers { final state = widget.channel.state!; return { ...state.watchers, ...state.members.map((it) => it.user), }.whereType().toList(growable: false); } Future> queryMentions(String query) async { if (widget.mentionAllAppUsers) { return _queryUsers(query); } var channelState = widget.channel.state; channelState = channelState!; final members = channelState.members; // By default, we return maximum 100 members via queryChannels api call. // Thus it is safe to assume, that if number of members in channel.state // is < 100, then all the members are already available on client side // and we don't need to make any api call to queryMembers endpoint. if (members.length < 100) { final matchingUsers = membersAndWatchers.search(query); return matchingUsers.toList(growable: false); } final result = await _queryMembers(query); return result .map((it) => it.user) .whereType() .toList(growable: false); } Future> _queryMembers(String query) async { final response = await widget.channel.queryMembers( pagination: PaginationParams(limit: widget.limit), filter: query.isEmpty ? const Filter.empty() : Filter.autoComplete('name', query), ); return response.members; } Future> _queryUsers(String query) async { assert( widget.client != null, 'StreamChatClient is required in order to query all app users', ); final response = await widget.client!.queryUsers( pagination: PaginationParams(limit: widget.limit), filter: query.isEmpty ? const Filter.empty() : Filter.or([ Filter.autoComplete('id', query), Filter.autoComplete('name', query), ]), sort: [const SortOption('id', direction: SortOption.ASC)], ); return response.users; } }