feat(ui): improve add support for mentionAllAppUsers in UserMentionsOverlay.
Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
@@ -24,6 +25,12 @@ extension StringExtension on String {
|
||||
final characters = trim().characters;
|
||||
return characters.every(_emojiChars.contains);
|
||||
}
|
||||
|
||||
/// Removes accents and diacritics from the given String.
|
||||
String get diacriticsInsensitive => removeDiacritics(this);
|
||||
|
||||
/// Levenshtein distance between this and [t].
|
||||
int levenshteinDistance(String t) => levenshtein(this, t);
|
||||
}
|
||||
|
||||
/// List extension
|
||||
@@ -170,3 +177,49 @@ extension IconButtonX on IconButton {
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
/// Extensions on List<User>
|
||||
extension UserListX on List<User> {
|
||||
/// It does an search on a list of [User] and returns users with
|
||||
/// `id` or `name` containing the [query].
|
||||
///
|
||||
/// Results are returned sorted by their edit distance from the
|
||||
/// searched string, distance is calculated using the [levenshtein] algorithm.
|
||||
List<User> search(String query) {
|
||||
String normalize(String input) => input.toLowerCase().diacriticsInsensitive;
|
||||
|
||||
final normalizedQuery = normalize(query);
|
||||
|
||||
final matchingUsers = <User, int>{}; // User:lDistance
|
||||
|
||||
for (final user in this) {
|
||||
final normalizedId = normalize(user.id);
|
||||
final normalizedUserName = normalize(user.name);
|
||||
final lDistance = normalizedUserName.levenshteinDistance(normalizedQuery);
|
||||
final containsId = normalizedId.contains(normalizedQuery);
|
||||
final containsName = normalizedUserName.contains(normalizedQuery);
|
||||
if (lDistance < 3 || containsId || containsName) {
|
||||
matchingUsers[user] = lDistance;
|
||||
}
|
||||
}
|
||||
|
||||
final entries = matchingUsers.entries.toList(growable: false)
|
||||
..sort((prev, curr) {
|
||||
bool containsQuery(User user) =>
|
||||
normalize(user.id).contains(normalizedQuery) ||
|
||||
normalize(user.name).contains(normalizedQuery);
|
||||
|
||||
final containsInPrev = containsQuery(prev.key);
|
||||
final containsInCurr = containsQuery(curr.key);
|
||||
|
||||
if (containsInPrev && !containsInCurr) {
|
||||
return -1;
|
||||
} else if (!containsInPrev && containsInCurr) {
|
||||
return 1;
|
||||
}
|
||||
return prev.value.compareTo(curr.value);
|
||||
});
|
||||
|
||||
return entries.map((e) => e.key).toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// This widget is used for showing user tiles for mentions
|
||||
/// Use [title], [subtitle], [leading], [trailing] for
|
||||
/// substituting widgets in respective positions
|
||||
@Deprecated(
|
||||
"'MentionTile' widget is deprecated and shouldn't be used. "
|
||||
"Use 'UserMentionTile' instead",
|
||||
)
|
||||
class MentionTile extends StatelessWidget {
|
||||
/// Constructor for creating a [MentionTile] widget
|
||||
const MentionTile(
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Overlay for displaying users that can be mentioned
|
||||
class MentionsOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [MentionsOverlay]
|
||||
const MentionsOverlay({
|
||||
required this.query,
|
||||
required this.onMentionResult,
|
||||
required this.size,
|
||||
required this.channel,
|
||||
this.mentionsTileBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The size of the overlay
|
||||
final Size size;
|
||||
|
||||
/// Query for searching users
|
||||
final String query;
|
||||
|
||||
/// The channel to search for users
|
||||
final Channel channel;
|
||||
|
||||
/// Callback called when a user is selected
|
||||
final ValueChanged<User?> onMentionResult;
|
||||
|
||||
/// Customize the tile for the mentions overlay
|
||||
final MentionTileBuilder? mentionsTileBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
Future<List<Member>>? queryMembers;
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
queryMembers = channel
|
||||
.queryMembers(filter: Filter.autoComplete('name', query))
|
||||
.then((res) => res.members);
|
||||
}
|
||||
|
||||
final members = channel.state?.members
|
||||
.where((m) => m.user?.name.toLowerCase().contains(query) == true)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (members.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
elevation: 2,
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
child: FutureBuilder<List<Member>>(
|
||||
future: queryMembers ?? Future.value(members),
|
||||
initialData: members,
|
||||
builder: (context, snapshot) => ListView(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
...snapshot.data!
|
||||
.where((it) => it.user != null)
|
||||
.map(
|
||||
(m) => Material(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onMentionResult(m.user);
|
||||
},
|
||||
child: mentionsTileBuilder != null
|
||||
? mentionsTileBuilder!(context, m)
|
||||
: MentionTile(m, key: ValueKey(m.user?.id)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/mentions_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/overlays.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
@@ -193,6 +193,7 @@ class MessageInput extends StatefulWidget {
|
||||
this.attachmentButtonBuilder,
|
||||
this.commandButtonBuilder,
|
||||
this.customOverlays = const [],
|
||||
this.mentionAllAppUsers = false,
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
@@ -303,6 +304,11 @@ class MessageInput extends StatefulWidget {
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? commandButtonBuilder;
|
||||
|
||||
/// When enabled mentions search users across the entire app.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool mentionAllAppUsers;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -1159,16 +1165,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final renderObject = context.findRenderObject() as RenderBox;
|
||||
|
||||
return MentionsOverlay(
|
||||
return UserMentionsOverlay(
|
||||
query: query,
|
||||
mentionAllAppUsers: widget.mentionAllAppUsers,
|
||||
client: StreamChat.of(context).client,
|
||||
channel: StreamChannel.of(context).channel,
|
||||
size: Size(renderObject.size.width - 16, 400),
|
||||
query: query,
|
||||
onMentionResult: (user) {
|
||||
if (user != null) {
|
||||
_mentionedUsers.add(user);
|
||||
}
|
||||
|
||||
splits[splits.length - 1] = user!.name;
|
||||
onMentionUserTap: (user) {
|
||||
_mentionedUsers.add(user);
|
||||
splits[splits.length - 1] = user.name;
|
||||
final rejoin = splits.join('@');
|
||||
|
||||
textEditingController.value = TextEditingValue(
|
||||
@@ -1180,9 +1185,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
);
|
||||
_onChangedDebounced.cancel();
|
||||
setState(() {
|
||||
_showMentionsOverlay = false;
|
||||
});
|
||||
setState(() => _showMentionsOverlay = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// This widget is used for showing user tiles for mentions
|
||||
/// Use [title], [subtitle], [leading], [trailing] for
|
||||
/// substituting widgets in respective positions
|
||||
class UserMentionTile extends StatelessWidget {
|
||||
/// Constructor for creating a [UserMentionTile] widget
|
||||
const UserMentionTile(
|
||||
this.user, {
|
||||
Key? key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
}) : super(key: key);
|
||||
|
||||
/// User to display in the tile
|
||||
final User user;
|
||||
|
||||
/// Widget to display as title
|
||||
final Widget? title;
|
||||
|
||||
/// Widget to display below [title]
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Widget at the start of the tile
|
||||
final Widget? leading;
|
||||
|
||||
/// Widget at the end of tile
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
leading ??
|
||||
UserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.tight(const Size(40, 40)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title ??
|
||||
Text(
|
||||
user.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: chatThemeData.textTheme.bodyBold,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
subtitle ??
|
||||
Text(
|
||||
'@${user.id}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: chatThemeData.textTheme.footnoteBold.copyWith(
|
||||
color: chatThemeData.colorTheme.textLowEmphasis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 18,
|
||||
left: 8,
|
||||
),
|
||||
child: StreamSvgIcon.mentions(
|
||||
color: chatThemeData.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
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 && 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<UserMentionsOverlay> {
|
||||
late Future<List<User>> userMentionsFuture;
|
||||
late List<User> 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<List<User>>(
|
||||
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<User> get membersAndWatchers {
|
||||
final state = widget.channel.state!;
|
||||
return {
|
||||
...state.watchers,
|
||||
...state.members.map((it) => it.user),
|
||||
}.whereType<User>().toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<User>> 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<User>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<Member>> _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<List<User>> _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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Launch URL
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
@@ -388,3 +389,39 @@ class Tuple2<T1, T2> {
|
||||
@override
|
||||
int get hashCode => item1.hashCode ^ item2.hashCode;
|
||||
}
|
||||
|
||||
/// Levenshtein algorithm implementation based on:
|
||||
/// http://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
int levenshtein(String s, String t, {bool caseSensitive = true}) {
|
||||
if (!caseSensitive) {
|
||||
// ignore: parameter_assignments
|
||||
s = s.toLowerCase();
|
||||
// ignore: parameter_assignments
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (s == t) return 0;
|
||||
if (s.isEmpty) return t.length;
|
||||
if (t.isEmpty) return s.length;
|
||||
|
||||
final v0 = List<int>.filled(t.length + 1, 0);
|
||||
final v1 = List<int>.filled(t.length + 1, 0);
|
||||
|
||||
for (var i = 0; i < t.length + 1; i < i++) {
|
||||
v0[i] = i;
|
||||
}
|
||||
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
v1[0] = i + 1;
|
||||
|
||||
for (var j = 0; j < t.length; j++) {
|
||||
final cost = (s[i] == t[j]) ? 0 : 1;
|
||||
v1[j + 1] = math.min(v1[j] + 1, math.min(v0[j + 1] + 1, v0[j] + cost));
|
||||
}
|
||||
|
||||
for (var j = 0; j < t.length + 1; j++) {
|
||||
v0[j] = v1[j];
|
||||
}
|
||||
}
|
||||
|
||||
return v1[t.length];
|
||||
}
|
||||
|
||||
@@ -44,5 +44,6 @@ export 'src/unread_indicator.dart';
|
||||
export 'src/user_avatar.dart';
|
||||
export 'src/user_item.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/user_mention_tile.dart';
|
||||
export 'src/utils.dart';
|
||||
export 'src/visible_footnote.dart';
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies:
|
||||
characters: ^1.1.0
|
||||
chewie: ^1.2.0
|
||||
collection: ^1.15.0
|
||||
diacritic: ^0.1.3
|
||||
dio: ^4.0.0
|
||||
ezanimation: ^0.5.0
|
||||
file_picker: ^3.0.1
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
void main() {
|
||||
group('List<User>.search', () {
|
||||
test('should work fine', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final thierry = User(id: 'thierry', name: 'Thierry');
|
||||
final users = [tommaso, thierry];
|
||||
|
||||
final a = users.search('Tom');
|
||||
expect(users.search('Tom'), [tommaso]);
|
||||
expect(users.search('Thier'), [thierry]);
|
||||
});
|
||||
|
||||
test('should search using UpperCased', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final thierry = User(id: 'thierry', name: 'Thierry');
|
||||
final users = [tommaso, thierry];
|
||||
|
||||
expect(users.search('tom'), [tommaso]);
|
||||
expect(users.search('thier'), [thierry]);
|
||||
});
|
||||
|
||||
test('should search by .id or .name', () {
|
||||
final user1 = User(id: 'searchingThis');
|
||||
final user2 = User(id: 'x', name: 'searchingThis');
|
||||
|
||||
expect([user1].search('sear'), [user1]);
|
||||
expect([user2].search('sear'), [user2]);
|
||||
});
|
||||
|
||||
test('should search transliterated', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final thierry = User(id: 'thierry', name: 'Thierry');
|
||||
final users = [tommaso, thierry];
|
||||
|
||||
expect(users.search('tóm'), [tommaso]);
|
||||
expect(users.search('thíer'), [thierry]);
|
||||
});
|
||||
|
||||
test('search and sorted by distance', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final tomas = User(id: 'tomas', name: 'Tomas');
|
||||
final users = [tommaso, tomas];
|
||||
|
||||
expect(users.search('tom'), [tomas, tommaso]);
|
||||
});
|
||||
|
||||
test('should work fine with cyrillic diacritics', () {
|
||||
final petyo = User(id: '42', name: 'Петьо');
|
||||
final anastasia = User(id: '13', name: 'Анастасiя');
|
||||
final dmitriy = User(id: '99', name: 'Дмитрий');
|
||||
final users = [petyo, anastasia, dmitriy];
|
||||
|
||||
expect(users.search('petyo'), []);
|
||||
expect(users.search('Пе'), [petyo]);
|
||||
expect(users.search('Ана'), [anastasia]);
|
||||
expect(users.search('Дмитри'), [dmitriy]);
|
||||
expect(users.search('Дмитрии'), [dmitriy]);
|
||||
});
|
||||
|
||||
test('should work fine with french diacritics', () {
|
||||
final user = User(id: 'fra', name: 'françois');
|
||||
|
||||
expect([user].search('françois'), [user]);
|
||||
expect([user].search('franc'), [user]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -152,13 +152,9 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
int limit = 20,
|
||||
}) {
|
||||
if (direction == QueryDirection.top) {
|
||||
return _queryTopMessages(
|
||||
limit: limit,
|
||||
);
|
||||
return _queryTopMessages(limit: limit);
|
||||
}
|
||||
return _queryBottomMessages(
|
||||
limit: limit,
|
||||
);
|
||||
return _queryBottomMessages(limit: limit);
|
||||
}
|
||||
|
||||
/// Calls [channel.getReplies] updating [queryMessage] stream
|
||||
@@ -311,6 +307,20 @@ class StreamChannelState extends State<StreamChannel> {
|
||||
return message;
|
||||
}
|
||||
|
||||
/// Query channel members.
|
||||
Future<List<Member>> queryMembers({
|
||||
Filter? filter,
|
||||
List<SortOption>? sort,
|
||||
PaginationParams? pagination,
|
||||
}) async {
|
||||
final response = await channel.queryMembers(
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
pagination: pagination,
|
||||
);
|
||||
return response.members;
|
||||
}
|
||||
|
||||
/// Reloads the channel with latest message
|
||||
Future<void> reloadChannel() => _queryAtMessage(before: 30);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user