From cafe72d8768e1ecaffdc62b94a70056d005a0369 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 4 Apr 2025 18:27:05 +0200 Subject: [PATCH 1/9] refactor!(llc, core): Improve query sorts and provide defaults. --- .../stream_chat/lib/src/client/channel.dart | 7 +- .../stream_chat/lib/src/client/client.dart | 19 +- .../lib/src/core/api/channel_api.dart | 2 +- .../lib/src/core/api/general_api.dart | 4 +- .../lib/src/core/api/moderation_api.dart | 3 +- .../lib/src/core/api/polls_api.dart | 4 +- .../lib/src/core/api/requests.dart | 129 ++++++++-- .../lib/src/core/api/requests.g.dart | 6 +- .../lib/src/core/api/user_api.dart | 2 +- .../lib/src/core/models/banned_user.dart | 24 +- .../lib/src/core/models/channel_model.dart | 4 + .../lib/src/core/models/channel_state.dart | 49 +++- .../lib/src/core/models/comparable_field.dart | 46 ++++ .../lib/src/core/models/member.dart | 36 ++- .../lib/src/core/models/message.dart | 32 ++- .../stream_chat/lib/src/core/models/poll.dart | 42 ++- .../lib/src/core/models/poll_vote.dart | 38 ++- .../stream_chat/lib/src/core/models/user.dart | 54 +++- .../lib/src/db/chat_persistence_client.dart | 2 +- .../test/src/client/client_test.dart | 4 +- .../test/src/core/api/channel_api_test.dart | 2 +- .../test/src/core/api/general_api_test.dart | 10 +- .../test/src/core/api/requests_test.dart | 242 +++++++++++++++++- .../test/src/core/api/user_api_test.dart | 2 +- .../src/core/models/channel_state_test.dart | 164 ++++++++++++ .../core/models/comparable_field_test.dart | 158 ++++++++++++ .../test/src/core/models/member_test.dart | 193 ++++++++++++++ .../test/src/core/models/user_test.dart | 164 ++++++++++++ .../src/db/chat_persistence_client_test.dart | 2 +- .../lib/src/stream_channel.dart | 2 +- .../src/stream_channel_list_controller.dart | 28 +- .../src/stream_member_list_controller.dart | 32 ++- ...stream_message_search_list_controller.dart | 6 +- .../src/stream_poll_vote_list_controller.dart | 32 ++- .../lib/src/stream_user_list_controller.dart | 29 ++- .../lib/src/dao/channel_query_dao.dart | 47 +--- .../src/stream_chat_persistence_client.dart | 49 +--- .../test/src/dao/channel_query_dao_test.dart | 86 ------- 38 files changed, 1493 insertions(+), 262 deletions(-) create mode 100644 packages/stream_chat/lib/src/core/models/comparable_field.dart create mode 100644 packages/stream_chat/test/src/core/models/comparable_field_test.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 3552417ed1..f1d264b245 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_queue.dart'; +import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:synchronized/synchronized.dart'; @@ -1223,7 +1224,7 @@ class Channel { Future queryPollVotes( String pollId, { Filter? filter, - List? sort, + Sort? sort, PaginationParams pagination = const PaginationParams(), }) { _checkInitialized(); @@ -1782,7 +1783,7 @@ class Channel { /// Query channel members. Future queryMembers({ Filter? filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) => _client.queryMembers( @@ -1797,7 +1798,7 @@ class Channel { /// Query channel banned users. Future queryBannedUsers({ Filter? filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) { _checkInitialized(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index ea113adf53..cce2ae9b88 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -17,6 +17,7 @@ import 'package:stream_chat/src/core/http/system_environment_manager.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; @@ -608,7 +609,7 @@ class StreamChatClient { /// Requests channels with a given query. Stream> queryChannels({ Filter? filter, - List>? channelStateSort, + Sort? channelStateSort, bool state = true, bool watch = true, bool presence = false, @@ -717,7 +718,7 @@ class StreamChatClient { /// Requests channels with a given query from the API. Future> queryChannelsOnline({ Filter? filter, - List? sort, + Sort? sort, bool state = true, bool watch = true, bool presence = false, @@ -791,7 +792,7 @@ class StreamChatClient { /// Requests channels with a given query from the Persistence client. Future> queryChannelsOffline({ Filter? filter, - List>? channelStateSort, + Sort? channelStateSort, PaginationParams paginationParams = const PaginationParams(), }) async { final offlineChannels = (await chatPersistenceClient?.getChannelStates( @@ -830,7 +831,7 @@ class StreamChatClient { Future queryUsers({ bool? presence, Filter? filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) async { final response = await _chatApi.user.queryUsers( @@ -846,7 +847,7 @@ class StreamChatClient { /// Query banned users. Future queryBannedUsers({ required Filter filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) => _chatApi.moderation.queryBannedUsers( @@ -859,7 +860,7 @@ class StreamChatClient { Future search( Filter filter, { String? query, - List? sort, + Sort? sort, PaginationParams? paginationParams, Filter? messageFilters, }) => @@ -1064,7 +1065,7 @@ class StreamChatClient { Filter? filter, String? channelId, List? members, - List? sort, + Sort? sort, PaginationParams? pagination, }) => _chatApi.general.queryMembers( @@ -1385,7 +1386,7 @@ class StreamChatClient { /// Queries Polls with the given [filter] and [sort] options. Future queryPolls({ Filter? filter, - List? sort, + Sort? sort, PaginationParams pagination = const PaginationParams(), }) => _chatApi.polls.queryPolls( @@ -1399,7 +1400,7 @@ class StreamChatClient { Future queryPollVotes( String pollId, { Filter? filter, - List? sort, + Sort? sort, PaginationParams pagination = const PaginationParams(), }) => _chatApi.polls.queryPollVotes( diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index bfe2d8a638..59b6a6223c 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -50,7 +50,7 @@ class ChannelApi { /// Requests channels with a given query from the API. Future queryChannels({ Filter? filter, - List? sort, + Sort? sort, int? memberLimit, int? messageLimit, bool state = true, diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index 9a2e773c9a..f2174c870b 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -32,7 +32,7 @@ class GeneralApi { Future searchMessages( Filter filter, { String? query, - List? sort, + Sort? sort, PaginationParams? pagination, Filter? messageFilters, }) async { @@ -75,7 +75,7 @@ class GeneralApi { Filter? filter, String? channelId, List? members, - List? sort, + Sort? sort, PaginationParams? pagination, }) async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/core/api/moderation_api.dart b/packages/stream_chat/lib/src/core/api/moderation_api.dart index 10a4c8a03a..0fd06d2a30 100644 --- a/packages/stream_chat/lib/src/core/api/moderation_api.dart +++ b/packages/stream_chat/lib/src/core/api/moderation_api.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/stream_chat.dart'; /// Defines the api dedicated to moderation operations @@ -130,7 +131,7 @@ class ModerationApi { /// Queries banned users. Future queryBannedUsers({ Filter? filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/core/api/polls_api.dart b/packages/stream_chat/lib/src/core/api/polls_api.dart index c87c20bc1c..14c3e5e16b 100644 --- a/packages/stream_chat/lib/src/core/api/polls_api.dart +++ b/packages/stream_chat/lib/src/core/api/polls_api.dart @@ -162,7 +162,7 @@ class PollsApi { /// parameters. Future queryPolls({ Filter? filter, - List? sort, + Sort? sort, PaginationParams pagination = const PaginationParams(), }) async { final response = await _client.post( @@ -181,7 +181,7 @@ class PollsApi { Future queryPollVotes( String pollId, { Filter? filter, - List? sort, + Sort? sort, PaginationParams pagination = const PaginationParams(), }) async { final response = await _client.post( diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 0ca9a007ca..a0943e8edb 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -1,47 +1,140 @@ +// ignore_for_file: constant_identifier_names + import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; part 'requests.g.dart'; -/// Sorting options +/// A list of [SortOption]s that define a sorting order for elements of type [T] +/// +/// When multiple sort options are provided, they are applied in sequence +/// until a non-equal comparison is found. +/// +/// Example: `Sort([pinnedAtSort, lastMessageAtSort])` +typedef Sort = List>; + +/// Extension that allows a [Sort] to be used as a comparator function. +extension CompositeComparator on Sort { + /// Compares two objects using all sort options in sequence. + /// + /// Returns the first non-zero comparison result, or 0 if all comparisons + /// result in equality. + /// + /// ```dart + /// channels.sort(mySort.compare); + /// ``` + int compare(T a, T b) { + for (final sortOption in this) { + final comparison = sortOption.compare(a, b); + if (comparison != 0) return comparison; + } + + return 0; // All comparisons were equal + } +} + +/// A sort specification for objects that implement [ComparableFieldProvider]. +/// +/// Defines a field to sort by and a direction (ascending or descending). +/// Can use a custom comparator or create a default one based on the field name. +/// +/// Example: +/// ```dart +/// // Sort channels by last message date in descending order +/// final sort = SortOption("last_message_at"); +/// ``` @JsonSerializable(includeIfNull: false) -class SortOption { - /// Creates a new SortOption instance +class SortOption { + /// Creates a new SortOption instance with the specified field and direction. /// - /// For example: /// ```dart - /// // Sort channels by the last message date: - /// final sorting = SortOption("last_message_at") + /// final sorting = SortOption("last_message_at") // Default: descending order /// ``` const SortOption( this.field, { this.direction = SortOption.DESC, - this.comparator, - }); + Comparator? comparator, + }) : _comparator = comparator; - /// Create a new instance from a json + /// Creates a SortOption for descending order sorting by the specified field. + /// + /// Example: + /// ```dart + /// // Sort channels by last message date in descending order + /// final sort = SortOption.desc("last_message_at"); + /// ``` + const SortOption.desc( + this.field, { + Comparator? comparator, + }) : direction = SortOption.DESC, + _comparator = comparator; + + /// Creates a SortOption for ascending order sorting by the specified field. + /// + /// Example: + /// ```dart + /// // Sort channels by name in ascending order + /// final sort = SortOption.asc("name"); + /// ``` + const SortOption.asc( + this.field, { + Comparator? comparator, + }) : direction = SortOption.ASC, + _comparator = comparator; + + /// Create a new instance from JSON. factory SortOption.fromJson(Map json) => _$SortOptionFromJson(json); - /// Ascending order - // ignore: constant_identifier_names + /// Ascending order (1) static const ASC = 1; - /// Descending order - // ignore: constant_identifier_names + /// Descending order (-1) static const DESC = -1; - /// A sorting field name + /// The field name to sort by final String field; - /// A sorting direction + /// The sort direction (ASC or DESC) final int direction; - /// Sorting field Comparator required for offline sorting + /// Compares two objects of type T using the specified field and direction. + /// + /// Returns: + /// - 0 if both objects are equal + /// - 1 if the first object is greater than the second + /// - -1 if the first object is less than the second + /// + /// Handles null values by treating null as less than any non-null value. + /// + /// ```dart + /// final sortOption = SortOption("last_message_at"); + /// final sortedChannels = channels.sort(sortOption.compare); + /// ``` + int compare(T a, T b) => direction * comparator(a, b); + + /// Returns a comparator function for sorting objects of type T. @JsonKey(includeToJson: false, includeFromJson: false) - final Comparator? comparator; + Comparator get comparator { + if (_comparator case final comparator?) return comparator; - /// Serialize model to json + return (T a, T b) { + final aValue = a.getComparableField(field); + final bValue = b.getComparableField(field); + + // Handle null values + if (aValue == null && bValue == null) return 0; + if (aValue == null) return -1; + if (bValue == null) return 1; + + return aValue.compareTo(bValue); + }; + } + + final Comparator? _comparator; + + /// Converts this option to JSON. Map toJson() => _$SortOptionToJson(this); } diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index cffffb6d69..3896ed2cf4 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -6,13 +6,15 @@ part of 'requests.dart'; // JsonSerializableGenerator // ************************************************************************** -SortOption _$SortOptionFromJson(Map json) => +SortOption _$SortOptionFromJson( + Map json) => SortOption( json['field'] as String, direction: (json['direction'] as num?)?.toInt() ?? SortOption.DESC, ); -Map _$SortOptionToJson(SortOption instance) => +Map _$SortOptionToJson( + SortOption instance) => { 'field': instance.field, 'direction': instance.direction, diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index bc60ceda3d..df046b443c 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -17,7 +17,7 @@ class UserApi { Future queryUsers({ bool presence = false, Filter? filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/core/models/banned_user.dart b/packages/stream_chat/lib/src/core/models/banned_user.dart index d557661576..519cb5eb7b 100644 --- a/packages/stream_chat/lib/src/core/models/banned_user.dart +++ b/packages/stream_chat/lib/src/core/models/banned_user.dart @@ -1,13 +1,14 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'banned_user.g.dart'; /// Contains information about a [User] that was banned from a [Channel] or App. @JsonSerializable() -class BannedUser extends Equatable { +class BannedUser extends Equatable with ComparableFieldProvider { /// Creates a new instance of [BannedUser] const BannedUser({ required this.user, @@ -77,4 +78,25 @@ class BannedUser extends Equatable { shadow, reason, ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + BannedUserSortKey.createdAt => createdAt, + _ => null, + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [BannedUser]. +/// +/// This type provides type-safe keys that can be used for sorting banned users +/// in queries. Each constant represents a field that can be sorted on. +extension type const BannedUserSortKey(String key) implements String { + /// Sort banned users by their creation date. + /// + /// This is the default sort field (in descending order). + static const createdAt = BannedUserSortKey('created_at'); } diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 96fe82d469..07efebe06a 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -92,6 +92,10 @@ class ChannelModel { @JsonKey(includeToJson: false) final DateTime createdAt; + /// The date at which the channel was last updated. + @JsonKey(includeToJson: false, includeFromJson: false) + DateTime? get lastUpdatedAt => lastMessageAt ?? createdAt; + /// The date of the last channel update @JsonKey(includeToJson: false) final DateTime updatedAt; diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 65e22f38e6..d6e317a40a 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/read.dart'; @@ -9,7 +10,7 @@ part 'channel_state.g.dart'; /// The class that contains the information about a channel @JsonSerializable() -class ChannelState { +class ChannelState with ComparableFieldProvider { /// Constructor used for json serialization ChannelState({ this.channel, @@ -74,4 +75,50 @@ class ChannelState { read: read ?? this.read, membership: membership ?? this.membership, ); + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + ChannelSortKey.lastUpdated => channel?.lastUpdatedAt, + ChannelSortKey.createdAt => channel?.createdAt, + ChannelSortKey.updatedAt => channel?.updatedAt, + ChannelSortKey.lastMessageAt => channel?.lastMessageAt, + ChannelSortKey.memberCount => channel?.memberCount, + // TODO: Support providing default value for hasUnread, unreadCount + ChannelSortKey.hasUnread => null, + ChannelSortKey.unreadCount => null, + _ => channel?.extraData[sortKey], + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [ChannelState]. +/// +/// This type provides type-safe keys that can be used for sorting channels +/// in queries. Each constant represents a field that can be sorted on. +extension type const ChannelSortKey(String key) implements String { + /// The default sorting is by the last message date or a channel created date + /// if no messages. + static const lastUpdated = ChannelSortKey('last_updated'); + + /// Sort channels by the date they were created. + static const createdAt = ChannelSortKey('created_at'); + + /// Sort channels by the date they were updated. + static const updatedAt = ChannelSortKey('updated_at'); + + /// Sort channels by the timestamp of the last message. + static const lastMessageAt = ChannelSortKey('last_message_at'); + + /// Sort channels by the number of members. + static const memberCount = ChannelSortKey('member_count'); + + /// Sort channels by whether they have unread messages. + /// Useful for grouping read and unread channels. + static const hasUnread = ChannelSortKey('has_unread'); + + /// Sort channels by the count of unread messages. + static const unreadCount = ChannelSortKey('unread_count'); } diff --git a/packages/stream_chat/lib/src/core/models/comparable_field.dart b/packages/stream_chat/lib/src/core/models/comparable_field.dart new file mode 100644 index 0000000000..cb75b99a64 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/comparable_field.dart @@ -0,0 +1,46 @@ +/// A wrapper class for values that implements [Comparable]. +/// +/// This class is used to compare values of different types in a way that +/// allows for consistent ordering. +/// +/// This is useful when sorting or comparing values in a consistent manner. +/// +/// For example, when sorting a list of objects with different types of fields, +/// using this class will ensure that all values are compared correctly +/// regardless of their type. +class ComparableField implements Comparable> { + const ComparableField._(this.value); + + /// Creates a new [ComparableField] instance from a [value]. + static ComparableField? fromValue(T? value) { + if (value == null) return null; + return ComparableField._(value); + } + + /// The value to be compared. + final T value; + + @override + int compareTo(ComparableField other) { + return switch ((value, other.value)) { + (final num a, final num b) => a.compareTo(b), + (final String a, final String b) => a.compareTo(b), + (final DateTime a, final DateTime b) => a.compareTo(b), + (final bool a, final bool b) when a == b => 0, + (final bool a, final bool b) => a && !b ? 1 : -1, // true > false + _ => 0 // All comparisons were equal or incomparable types + }; + } +} + +/// A mixin that provides a way to access comparable fields by string keys. +/// +/// Classes that implement this mixin can be used in sorting operations +/// where the sort key is determined at runtime. +mixin ComparableFieldProvider { + /// Gets a comparable field value for the given [sortKey]. + /// + /// Returns a [ComparableField] or null if no comparable field with the given + /// sort key exists. + ComparableField? getComparableField(String sortKey); +} diff --git a/packages/stream_chat/lib/src/core/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index abb9f5b131..2834611831 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; @@ -8,7 +9,7 @@ part 'member.g.dart'; /// The class that contains the information about the user membership /// in a channel @JsonSerializable() -class Member extends Equatable { +class Member extends Equatable implements ComparableFieldProvider { /// Constructor used for json serialization Member({ this.user, @@ -144,4 +145,37 @@ class Member extends Equatable { updatedAt, extraData, ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + MemberSortKey.createdAt => createdAt, + MemberSortKey.userId => userId, + MemberSortKey.name => user?.name, + MemberSortKey.channelRole => channelRole, + _ => extraData[sortKey], + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [Member]. +/// +/// This type provides type-safe keys that can be used for sorting members +/// in queries. Each constant represents a field that can be sorted on. +extension type const MemberSortKey(String key) implements String { + /// Sort members by their creation date in the channel. + static const createdAt = MemberSortKey('created_at'); + + /// Sort members by the user ID. + static const userId = MemberSortKey('user_id'); + + /// Sort members by user name. + /// + /// Note: This requires additional database joins and might be slower. + static const name = MemberSortKey('name'); + + /// Sort members by the channel role. + static const channelRole = MemberSortKey('channel_role'); } diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index aad6966af4..d207326f05 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/message_state.dart'; import 'package:stream_chat/src/core/models/moderation.dart'; import 'package:stream_chat/src/core/models/poll.dart'; @@ -19,7 +20,7 @@ const _nullConst = _NullConst(); /// The class that contains the information about a message. @JsonSerializable() -class Message extends Equatable { +class Message extends Equatable with ComparableFieldProvider { /// Constructor used for json serialization. Message({ String? id, @@ -531,6 +532,35 @@ class Message extends Equatable { restrictedVisibility, moderation, ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + MessageSortKey.id => id, + MessageSortKey.createdAt => createdAt, + MessageSortKey.updatedAt => updatedAt, + _ => extraData[sortKey], + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [Message]. +/// +/// This type provides type-safe keys that can be used for sorting messages +/// in queries. Each constant represents a field that can be sorted on. +extension type const MessageSortKey(String key) implements String { + /// Sort messages by their unique ID. + static const id = MessageSortKey('id'); + + /// Sort messages by their creation date. + /// + /// This is the default sort field (in descending order). + static const createdAt = MessageSortKey('created_at'); + + /// Sort messages by their last update date. + static const updatedAt = MessageSortKey('updated_at'); } /// {@template messageType} diff --git a/packages/stream_chat/lib/src/core/models/poll.dart b/packages/stream_chat/lib/src/core/models/poll.dart index 4b9ed91a05..b46d66fc13 100644 --- a/packages/stream_chat/lib/src/core/models/poll.dart +++ b/packages/stream_chat/lib/src/core/models/poll.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/poll_option.dart'; import 'package:stream_chat/src/core/models/poll_vote.dart'; import 'package:stream_chat/src/core/models/user.dart'; @@ -32,7 +33,7 @@ enum VotingVisibility { /// A model class representing a poll. /// {@endtemplate} @JsonSerializable() -class Poll extends Equatable { +class Poll extends Equatable with ComparableFieldProvider { /// {@macro streamPoll} Poll({ String? id, @@ -270,6 +271,45 @@ class Poll extends Equatable { createdAt, updatedAt, ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + PollSortKey.id => id, + PollSortKey.name => name, + PollSortKey.createdAt => createdAt, + PollSortKey.updatedAt => updatedAt, + PollSortKey.isClosed => isClosed, + _ => extraData[sortKey], + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [Poll]. +/// +/// This type provides type-safe keys that can be used for sorting polls +/// in queries. Each constant represents a field that can be sorted on. +extension type const PollSortKey(String key) implements String { + /// Sort polls by their unique ID. + static const id = PollSortKey('id'); + + /// Sort polls by their name. + static const name = PollSortKey('name'); + + /// Sort polls by their creation date. + /// + /// This is the default sort field (in ascending order). + static const createdAt = PollSortKey('created_at'); + + /// Sort polls by their last update date. + static const updatedAt = PollSortKey('updated_at'); + + /// Sort polls by whether they are closed or not. + /// + /// Closed polls will appear first when sorting in ascending order. + static const isClosed = PollSortKey('is_closed'); } /// Helper extension for [Poll] model. diff --git a/packages/stream_chat/lib/src/core/models/poll_vote.dart b/packages/stream_chat/lib/src/core/models/poll_vote.dart index c7a2bb40b1..0db4707f52 100644 --- a/packages/stream_chat/lib/src/core/models/poll_vote.dart +++ b/packages/stream_chat/lib/src/core/models/poll_vote.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/user.dart'; part 'poll_vote.g.dart'; @@ -8,7 +9,7 @@ part 'poll_vote.g.dart'; /// A model class representing a poll vote. /// {@endtemplate} @JsonSerializable() -class PollVote extends Equatable { +class PollVote extends Equatable with ComparableFieldProvider { /// {@macro streamPollVote} PollVote({ this.id, @@ -104,4 +105,39 @@ class PollVote extends Equatable { userId, user, ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + PollVoteSortKey.id => id, + PollVoteSortKey.createdAt => createdAt, + PollVoteSortKey.updatedAt => updatedAt, + PollVoteSortKey.answerText => answerText, + _ => null, + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [PollVote]. +/// +/// This type provides type-safe keys that can be used for sorting poll votes +/// in queries. Each constant represents a field that can be sorted on. +extension type const PollVoteSortKey(String key) implements String { + /// Sort poll votes by their ID. + static const id = PollVoteSortKey('id'); + + /// Sort poll votes by their creation date. + /// + /// This is the default sort field (in ascending order). + static const createdAt = PollVoteSortKey('created_at'); + + /// Sort poll votes by their last update date. + static const updatedAt = PollVoteSortKey('updated_at'); + + /// Sort poll votes by their answer text. + /// + /// Only applicable for votes that have answer text. + static const answerText = PollVoteSortKey('answer_text'); } diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index dd0b77048e..b57928426c 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -1,12 +1,13 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; part 'user.g.dart'; /// Class that defines a Stream Chat User. @JsonSerializable() -class User extends Equatable { +class User extends Equatable with ComparableFieldProvider { /// Creates a new user. /// /// {@template name} @@ -188,4 +189,55 @@ class User extends Equatable { teams, language, ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + UserSortKey.id => id, + UserSortKey.createdAt => createdAt, + UserSortKey.updatedAt => updatedAt, + UserSortKey.name => name, + UserSortKey.role => role, + UserSortKey.banned => banned, + UserSortKey.lastActive => lastActive, + _ => extraData[sortKey], + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [User]. +/// +/// This type provides type-safe keys that can be used for sorting users +/// in queries. Each constant represents a field that can be sorted on. +extension type const UserSortKey(String key) implements String { + /// Sort users by their ID. + static const id = UserSortKey('id'); + + /// Sort users by their creation date. + /// + /// This is part of the default sort (in descending order). + static const createdAt = UserSortKey('created_at'); + + /// Sort users by their last update date. + static const updatedAt = UserSortKey('updated_at'); + + /// Sort users by their name. + /// + /// Useful for alphabetical sorting of users. + static const name = UserSortKey('name'); + + /// Sort users by their role. + static const role = UserSortKey('role'); + + /// Sort users by whether they are banned. + /// + /// Banned users will appear first when sorting in ascending order. + static const banned = UserSortKey('banned'); + + /// Sort users by their last active date. + /// + /// Useful for sorting users by recent activity. + static const lastActive = UserSortKey('last_active'); } diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 0da0225f39..da83d4949a 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -104,7 +104,7 @@ abstract class ChatPersistenceClient { /// for filtering out states. Future> getChannelStates({ Filter? filter, - List>? channelStateSort, + Sort? channelStateSort, PaginationParams? paginationParams, }); diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 7d796fb4a5..c1ee572071 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2067,7 +2067,7 @@ void main() { test('`.queryPolls`', () async { final filter = Filter.in_('id', const ['test-poll-id']); - final sort = [const SortOption('created_at')]; + final sort = [const SortOption('created_at')]; const pagination = PaginationParams(limit: 20); final polls = List.generate( @@ -2109,7 +2109,7 @@ void main() { test('`.queryPollVotes`', () async { const pollId = 'test-poll-id'; final filter = Filter.in_('id', const ['test-vote-id']); - final sort = [const SortOption('created_at')]; + final sort = [const SortOption('created_at')]; const pagination = PaginationParams(limit: 20); final votes = List.generate( diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 74c4c5c563..abe9de82c9 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -122,7 +122,7 @@ void main() { const channelType = 'test-channel-type'; final filter = Filter.in_('cid', const ['test-cid']); - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; const memberLimit = 33; const messageLimit = 33; diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index 0a27cce4d6..e620d25904 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -86,7 +86,7 @@ void main() { 'should throw if `pagination.offset` and `sort` both are provided', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; const pagination = PaginationParams(offset: 10); try { await generalApi.searchMessages( @@ -103,7 +103,7 @@ void main() { test('should run successfully with `query`', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const query = 'test-query'; - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; const pagination = PaginationParams(); const path = '/search'; @@ -142,7 +142,7 @@ void main() { test('should run successfully with `messageFilter`', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; final messageFilter = Filter.query('key', 'text'); const pagination = PaginationParams(); @@ -187,7 +187,7 @@ void main() { const channelId = 'test-channel-id'; final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const pagination = PaginationParams(); - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; const path = '/members'; @@ -234,7 +234,7 @@ void main() { const channelType = 'test-channel-type'; final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const pagination = PaginationParams(); - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; const path = '/members'; diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 0bfdca7e88..d6f2ef2e40 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -1,12 +1,246 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; +/// Simple test model that implements ComparableFieldProvider +class TestModel with ComparableFieldProvider { + const TestModel({ + this.name, + this.age, + this.createdAt, + this.active, + }); + + final String? name; + final int? age; + final DateTime? createdAt; + final bool? active; + + @override + ComparableField? getComparableField(String sortKey) { + return switch (sortKey) { + 'name' => ComparableField.fromValue(name), + 'age' => ComparableField.fromValue(age), + 'created_at' => ComparableField.fromValue(createdAt), + 'active' => ComparableField.fromValue(active), + _ => null, + }; + } +} + void main() { group('src/api/requests', () { - test('SortOption', () { - const option = SortOption('name'); - final j = option.toJson(); - expect(j, {'field': 'name', 'direction': -1}); + group('SortOption', () { + test('serialization', () { + const option = SortOption.desc('name'); + final j = option.toJson(); + expect(j, {'field': 'name', 'direction': -1}); + }); + + test('should create a SortOption with default DESC direction', () { + const option = SortOption('name'); + expect(option.field, 'name'); + expect(option.direction, SortOption.DESC); + }); + + test('should create a SortOption with ASC direction', () { + const option = SortOption.asc('age'); + expect(option.field, 'age'); + expect(option.direction, SortOption.ASC); + }); + + test('should create a SortOption with DESC direction', () { + const option = SortOption.desc('age'); + expect(option.field, 'age'); + expect(option.direction, SortOption.DESC); + }); + + test('should correctly deserialize from JSON', () { + final json = {'field': 'age', 'direction': 1}; + final option = SortOption.fromJson(json); + expect(option.field, 'age'); + expect(option.direction, SortOption.ASC); + }); + + test('should compare two objects in descending order', () { + const option = SortOption.desc('age'); + const a = TestModel(age: 30); + const b = TestModel(age: 25); + + // In descending order, 30 should come before 25 + expect(option.compare(a, b), lessThan(0)); + }); + + test('should compare two objects in ascending order', () { + const option = SortOption.asc('age'); + const a = TestModel(age: 25); + const b = TestModel(age: 30); + + // In ascending order, 25 should come before 30 + expect(option.compare(a, b), lessThan(0)); + }); + + test('should handle null values correctly', () { + const option = SortOption.desc('age'); + const a = TestModel(age: null); + const b = TestModel(age: 25); + const c = TestModel(age: null); + + // Null values should come after non-null values + expect(option.compare(a, b), greaterThan(0)); + expect(option.compare(b, a), lessThan(0)); + + // Two null values should be equal + expect(option.compare(a, c), equals(0)); + }); + + test('should compare date fields correctly', () { + const option = SortOption.desc('created_at'); + final now = DateTime.now(); + final earlier = now.subtract(const Duration(days: 1)); + + final a = TestModel(createdAt: now); + final b = TestModel(createdAt: earlier); + + // In descending order, now should come before earlier + expect(option.compare(a, b), lessThan(0)); + }); + + test('should compare boolean fields correctly', () { + const option = SortOption.desc('active'); + const a = TestModel(active: true); + const b = TestModel(active: false); + const c = TestModel(active: true); + + // In descending order, true should come before false + expect(option.compare(a, b), lessThan(0)); + expect(option.compare(b, a), greaterThan(0)); + + // Two true values should be equal + expect(option.compare(a, c), equals(0)); + }); + + test('should handle custom comparator', () { + // Custom comparator that sorts by name length + final option = SortOption.desc( + 'name', + comparator: (a, b) { + final aLength = a.name?.length ?? 0; + final bLength = b.name?.length ?? 0; + return aLength.compareTo(bLength); + }, + ); + + const a = TestModel(name: 'longer_name'); + const b = TestModel(name: 'short'); + + // With custom comparator, longer name should come before shorter name + expect(option.compare(a, b), lessThan(0)); + }); + }); + + group('Composite Sorting', () { + test('should sort list using multiple sort criteria', () { + final models = [ + const TestModel(name: 'Alice', age: 30), + const TestModel(name: 'Bob', age: 30), + const TestModel(name: 'Charlie', age: 25), + const TestModel(name: 'David', age: 40), + ]; + + // Sort by age (DESC) then name (ASC) + final sortOptions = >[ + const SortOption.desc('age'), + const SortOption.asc('name'), + ]; + + // Use the compare extension + models.sort(sortOptions.compare); + + // Expected order: David (40), Alice (30), Bob (30), Charlie (25) + expect(models[0].name, 'David'); + // Same age as Bob, but name is alphabetically first + expect(models[1].name, 'Alice'); + // Same age as Alice, but name is alphabetically second + expect(models[2].name, 'Bob'); + expect(models[3].name, 'Charlie'); + }); + + test('should handle null values in multi-sort', () { + final models = [ + const TestModel(name: 'Alice', age: null), + const TestModel(name: 'Bob', age: 30), + const TestModel(name: 'Charlie', age: null), + const TestModel(name: null, age: 40), + ]; + + // Sort by age (DESC) then name (ASC) + final sortOptions = >[ + const SortOption.desc('age'), + const SortOption.asc('name'), + ]; + + models.sort(sortOptions.compare); + + // Expected order: + // 1. null name, age 40 + // 2. Bob, age 30 + // 3. Alice, null age + // 4. Charlie, null age + expect(models[0].name, null); + expect(models[1].name, 'Bob'); + // Null age, but name comes before Charlie alphabetically + expect(models[2].name, 'Alice'); + // Null age, but name comes after Alice alphabetically + expect(models[3].name, 'Charlie'); + }); + + test('should handle empty sort options', () { + final models = [ + const TestModel(name: 'Alice', age: 30), + const TestModel(name: 'Bob', age: 25), + ]; + + // Empty sort options + final sortOptions = >[]; + + // Should not change the order + final originalOrder = [...models]; + models.sort(sortOptions.compare); + + expect(models, equals(originalOrder)); + }); + + test('should sort with different data types in sequence', () { + final now = DateTime.now(); + final yesterday = now.subtract(const Duration(days: 1)); + + final models = [ + TestModel(name: 'Alice', active: true, createdAt: yesterday), + TestModel(name: 'Bob', active: false, createdAt: now), + TestModel(name: 'Charlie', active: true, createdAt: now), + ]; + + // Sort by created_at (DESC), active (DESC), then name (ASC) + final sortOptions = >[ + const SortOption.desc('created_at'), + const SortOption.desc('active'), + const SortOption.asc('name'), + ]; + + models.sort(sortOptions.compare); + + // Expected order: + // 1. Charlie - newest and active + // 2. Bob - newest but not active + // 3. Alice - older but active + expect(models[0].name, 'Charlie'); + expect(models[1].name, 'Bob'); + expect(models[2].name, 'Alice'); + }); }); group('PaginationParams', () { diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart index 9feb68e71f..6356ce46b2 100644 --- a/packages/stream_chat/test/src/core/api/user_api_test.dart +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -25,7 +25,7 @@ void main() { test('queryUsers', () async { const presence = true; final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); - const sort = [SortOption('test-field')]; + const sort = [SortOption('test-field')]; const pagination = PaginationParams(); const path = '/users'; diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 6441ef4651..cf93706a67 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -66,5 +66,169 @@ void main() { jsonFixture('channel_state_to_json.json'), ); }); + + group('ComparableFieldProvider', () { + test('should return ComparableField for channel.lastMessageAt', () { + final channelState = createChannelState( + id: 'test-channel', + lastMessageAt: DateTime(2023, 6, 15), + ); + + final field = channelState.getComparableField( + ChannelSortKey.lastMessageAt, + ); + + expect(field, isNotNull); + expect(field!.value, equals(DateTime(2023, 6, 15))); + }); + + test('should return ComparableField for channel.createdAt', () { + final channelState = createChannelState( + id: 'test-channel', + createdAt: DateTime(2023, 6, 10), + ); + + final field = channelState.getComparableField(ChannelSortKey.createdAt); + expect(field, isNotNull); + expect(field!.value, equals(DateTime(2023, 6, 10))); + }); + + test('should return ComparableField for channel.updatedAt', () { + final channelState = createChannelState( + id: 'test-channel', + updatedAt: DateTime(2023, 6, 12), + ); + + final field = channelState.getComparableField(ChannelSortKey.updatedAt); + expect(field, isNotNull); + expect(field!.value, equals(DateTime(2023, 6, 12))); + }); + + test('should return ComparableField for channel.memberCount', () { + final channelState = createChannelState( + id: 'test-channel', + memberCount: 42, + ); + + final field = + channelState.getComparableField(ChannelSortKey.memberCount); + expect(field, isNotNull); + expect(field!.value, equals(42)); + }); + + test('should return ComparableField for channel.extraData', () { + final channelState = createChannelState( + id: 'test-channel', + extraData: {'priority': 5}, + ); + + final field = channelState.getComparableField('priority'); + expect(field, isNotNull); + expect(field!.value, equals(5)); + }); + + test('should return null for non-existent extraData keys', () { + final channelState = createChannelState( + id: 'test-channel', + ); + + final field = channelState.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two channel states correctly using createdAt', () { + final newerChannel = createChannelState( + id: 'newer', + createdAt: DateTime(2023, 6, 15), + ); + + final olderChannel = createChannelState( + id: 'older', + createdAt: DateTime(2023, 6, 10), + ); + + final newerField = newerChannel.getComparableField( + ChannelSortKey.createdAt, + ); + + final olderField = olderChannel.getComparableField( + ChannelSortKey.createdAt, + ); + + expect(newerField!.compareTo(olderField!), greaterThan(0)); + expect(olderField.compareTo(newerField), lessThan(0)); + }); + + test('should compare two channel states correctly using memberCount', () { + final largerChannel = createChannelState( + id: 'larger', + memberCount: 100, + ); + + final smallerChannel = createChannelState( + id: 'smaller', + memberCount: 50, + ); + + final largerField = largerChannel.getComparableField( + ChannelSortKey.memberCount, + ); + + final smallerField = smallerChannel.getComparableField( + ChannelSortKey.memberCount, + ); + + expect(largerField!.compareTo(smallerField!), greaterThan(0)); + expect(smallerField.compareTo(largerField), lessThan(0)); + }); + + test('should compare two channel states correctly using extraData', () { + final highPriorityChannel = createChannelState( + id: 'high-priority', + extraData: {'priority': 10}, + ); + + final lowPriorityChannel = createChannelState( + id: 'low-priority', + extraData: {'priority': 1}, + ); + + final highPriorityField = highPriorityChannel.getComparableField( + 'priority', + ); + + final lowPriorityField = lowPriorityChannel.getComparableField( + 'priority', + ); + + expect(highPriorityField!.compareTo(lowPriorityField!), greaterThan(0)); + expect(lowPriorityField.compareTo(highPriorityField), lessThan(0)); + }); + }); }); } + +/// Helper function to create a ChannelState for testing +ChannelState createChannelState({ + required String id, + String type = 'messaging', + DateTime? createdAt, + DateTime? updatedAt, + DateTime? lastMessageAt, + int? memberCount, + Map? extraData, +}) { + return ChannelState( + channel: ChannelModel( + cid: '$type:$id', + id: id, + type: type, + lastMessageAt: lastMessageAt, + createdAt: createdAt ?? DateTime(2023), + updatedAt: updatedAt ?? DateTime(2023), + memberCount: memberCount ?? 0, + extraData: extraData ?? {}, + ), + membership: Member(userId: 'user1'), + ); +} diff --git a/packages/stream_chat/test/src/core/models/comparable_field_test.dart b/packages/stream_chat/test/src/core/models/comparable_field_test.dart new file mode 100644 index 0000000000..7a817d2f78 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/comparable_field_test.dart @@ -0,0 +1,158 @@ +import 'package:stream_chat/src/core/models/comparable_field.dart'; +import 'package:test/test.dart'; + +void main() { + group('ComparableField', () { + group('fromValue', () { + test('should create a comparable field from a non-null value', () { + final field = ComparableField.fromValue('test'); + expect(field, isNotNull); + }); + + test('should return null when value is null', () { + final field = ComparableField.fromValue(null); + expect(field, isNull); + }); + }); + + group('compare numbers', () { + test('should compare integers correctly', () { + final a = ComparableField.fromValue(10); + final b = ComparableField.fromValue(5); + + expect(a!.compareTo(b!), greaterThan(0)); + expect(b.compareTo(a), lessThan(0)); + expect(a.compareTo(ComparableField.fromValue(10)!), equals(0)); + }); + + test('should compare doubles correctly', () { + final a = ComparableField.fromValue(10.5); + final b = ComparableField.fromValue(10.2); + + expect(a!.compareTo(b!), greaterThan(0)); + expect(b.compareTo(a), lessThan(0)); + expect(a.compareTo(ComparableField.fromValue(10.5)!), equals(0)); + }); + + test('should compare mixed number types correctly', () { + final a = ComparableField.fromValue(10); + final b = ComparableField.fromValue(10.0); + + expect(a!.compareTo(b!), equals(0)); + }); + }); + + group('compare strings', () { + test('should compare strings alphabetically', () { + final a = ComparableField.fromValue('banana'); + final b = ComparableField.fromValue('apple'); + + expect(a!.compareTo(b!), greaterThan(0)); + expect(b.compareTo(a), lessThan(0)); + expect(a.compareTo(ComparableField.fromValue('banana')!), equals(0)); + }); + + test('should respect case sensitivity in string comparison', () { + final a = ComparableField.fromValue('Apple'); + final b = ComparableField.fromValue('apple'); + + // Uppercase comes before lowercase in ASCII + expect(a!.compareTo(b!), lessThan(0)); + }); + }); + + group('compare dates', () { + test('should compare dates correctly', () { + final now = DateTime.now(); + final earlier = now.subtract(const Duration(days: 1)); + final later = now.add(const Duration(days: 1)); + + final a = ComparableField.fromValue(now); + final b = ComparableField.fromValue(earlier); + final c = ComparableField.fromValue(later); + + expect(a!.compareTo(b!), greaterThan(0)); + expect(b.compareTo(a), lessThan(0)); + expect(c!.compareTo(a), greaterThan(0)); + expect(a.compareTo(ComparableField.fromValue(now)!), equals(0)); + }); + }); + + group('compare booleans', () { + test('should treat true as greater than false', () { + final a = ComparableField.fromValue(true); + final b = ComparableField.fromValue(false); + + expect(a!.compareTo(b!), greaterThan(0)); + expect(b.compareTo(a), lessThan(0)); + }); + + test('should treat equal booleans as equal', () { + final a = ComparableField.fromValue(true); + final b = ComparableField.fromValue(true); + final c = ComparableField.fromValue(false); + final d = ComparableField.fromValue(false); + + expect(a!.compareTo(b!), equals(0)); + expect(c!.compareTo(d!), equals(0)); + }); + }); + + group('compare different types', () { + test('should handle custom objects gracefully', () { + final a = ComparableField.fromValue(Object()); + final b = ComparableField.fromValue(Object()); + + expect(a!.compareTo(b!), equals(0)); + }); + }); + + group('ComparableFieldProvider', () { + test('should retrieve comparable fields from implementation', () { + final provider = TestProvider(); + + final nameField = provider.getComparableField('name'); + final ageField = provider.getComparableField('age'); + final missingField = provider.getComparableField('missing'); + + expect(nameField, isNotNull); + expect(ageField, isNotNull); + expect(missingField, isNull); + }); + + test('should compare fields correctly via provider', () { + final provider1 = TestProvider(name: 'Adam', age: 30); + final provider2 = TestProvider(name: 'Bob', age: 25); + + final name1 = provider1.getComparableField('name'); + final name2 = provider2.getComparableField('name'); + + final age1 = provider1.getComparableField('age'); + final age2 = provider2.getComparableField('age'); + + expect(name1!.compareTo(name2!), lessThan(0)); // Adam < Bob + expect(age1!.compareTo(age2!), greaterThan(0)); // 30 > 25 + }); + }); + }); +} + +/// Test implementation of ComparableFieldProvider +class TestProvider with ComparableFieldProvider { + TestProvider({ + this.name = 'test', + this.age = 0, + }); + + final String name; + final int age; + + @override + ComparableField? getComparableField(String sortKey) { + return switch (sortKey) { + 'name' => ComparableField.fromValue(name), + 'age' => ComparableField.fromValue(age), + _ => null, + }; + } +} diff --git a/packages/stream_chat/test/src/core/models/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart index 7e531718a9..752ff46b60 100644 --- a/packages/stream_chat/test/src/core/models/member_test.dart +++ b/packages/stream_chat/test/src/core/models/member_test.dart @@ -14,5 +14,198 @@ void main() { expect(member.updatedAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); expect(member.extraData['some_custom_field'], 'with_custom_data'); }); + + group('ComparableFieldProvider', () { + test('should return ComparableField for member.createdAt', () { + final createdAt = DateTime(2023, 6, 15); + final member = createTestMember( + userId: 'test-user', + createdAt: createdAt, + ); + + final field = member.getComparableField(MemberSortKey.createdAt); + expect(field, isNotNull); + expect(field!.value, equals(createdAt)); + }); + + test('should return ComparableField for member.userId', () { + final member = createTestMember( + userId: 'test-user', + ); + + final field = member.getComparableField(MemberSortKey.userId); + expect(field, isNotNull); + expect(field!.value, equals('test-user')); + }); + + test('should return ComparableField for member user.name', () { + final member = createTestMember( + userId: 'test-user', + userName: 'Test User', + ); + + final field = member.getComparableField(MemberSortKey.name); + expect(field, isNotNull); + expect(field!.value, equals('Test User')); + }); + + test('should return ComparableField for member.channelRole', () { + final member = createTestMember( + userId: 'test-user', + channelRole: 'owner', + ); + + final field = member.getComparableField(MemberSortKey.channelRole); + expect(field, isNotNull); + expect(field!.value, equals('owner')); + }); + + test('should return ComparableField for member.extraData', () { + final member = createTestMember( + userId: 'test-user', + extraData: {'activityScore': 75}, + ); + + final field = member.getComparableField('activityScore'); + expect(field, isNotNull); + expect(field!.value, equals(75)); + }); + + test('should return null for non-existent extraData keys', () { + final member = createTestMember( + userId: 'test-user', + ); + + final field = member.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should return null when user is null for name field', () { + final member = createTestMember( + userId: 'test-user', + includeUser: false, + ); + + final field = member.getComparableField(MemberSortKey.name); + expect(field, isNull); + }); + + test('should compare two members correctly using createdAt', () { + final recentMember = createTestMember( + userId: 'recent', + createdAt: DateTime(2023, 6, 15), + ); + + final olderMember = createTestMember( + userId: 'older', + createdAt: DateTime(2023, 6, 10), + ); + + final field1 = recentMember.getComparableField(MemberSortKey.createdAt); + final field2 = olderMember.getComparableField(MemberSortKey.createdAt); + + expect(field1!.compareTo(field2!), + greaterThan(0)); // More recent > Less recent + expect( + field2.compareTo(field1), lessThan(0)); // Less recent < More recent + }); + + test('should compare two members correctly using userId', () { + final member1 = createTestMember( + userId: 'alice', + ); + + final member2 = createTestMember( + userId: 'bob', + ); + + final field1 = member1.getComparableField(MemberSortKey.userId); + final field2 = member2.getComparableField(MemberSortKey.userId); + + expect(field1!.compareTo(field2!), lessThan(0)); // alice < bob + expect(field2.compareTo(field1), greaterThan(0)); // bob > alice + }); + + test('should compare two members correctly using user name', () { + final member1 = createTestMember( + userId: 'user1', + userName: 'Alice', + ); + + final member2 = createTestMember( + userId: 'user2', + userName: 'Bob', + ); + + final field1 = member1.getComparableField(MemberSortKey.name); + final field2 = member2.getComparableField(MemberSortKey.name); + + expect(field1!.compareTo(field2!), lessThan(0)); // Alice < Bob + expect(field2.compareTo(field1), greaterThan(0)); // Bob > Alice + }); + + test('should compare two members correctly using channelRole', () { + final owner = createTestMember( + userId: 'owner', + channelRole: 'owner', + ); + + final moderator = createTestMember( + userId: 'moderator', + channelRole: 'moderator', + ); + + final field1 = owner.getComparableField(MemberSortKey.channelRole); + final field2 = moderator.getComparableField(MemberSortKey.channelRole); + + expect(field1!.compareTo(field2!), + greaterThan(0)); // 'owner' > 'moderator' alphabetically + expect(field2.compareTo(field1), + lessThan(0)); // 'moderator' < 'owner' alphabetically + }); + + test('should compare two members correctly using extraData', () { + final highScore = createTestMember( + userId: 'high', + extraData: {'score': 100}, + ); + + final lowScore = createTestMember( + userId: 'low', + extraData: {'score': 50}, + ); + + final field1 = highScore.getComparableField('score'); + final field2 = lowScore.getComparableField('score'); + + expect(field1!.compareTo(field2!), greaterThan(0)); // 100 > 50 + expect(field2.compareTo(field1), lessThan(0)); // 50 < 100 + }); + }); }); } + +/// Helper function to create a Member for testing +Member createTestMember({ + required String userId, + String? userName, + String? channelRole, + DateTime? createdAt, + DateTime? updatedAt, + bool includeUser = true, + Map? extraData, +}) { + return Member( + userId: userId, + user: includeUser + ? User( + id: userId, + name: userName, + ) + : null, + channelRole: channelRole, + createdAt: createdAt ?? DateTime(2023), + updatedAt: updatedAt ?? DateTime(2023), + extraData: extraData ?? {}, + ); +} diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index fe44dc8711..c7d024cae1 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values, lines_longer_than_80_chars + import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; @@ -217,5 +219,167 @@ void main() { expect(user.createdAt, null); expect(user.updatedAt, null); }); + + group('ComparableFieldProvider', () { + test('should return ComparableField for user.id', () { + final user = createTestUser( + id: 'test-user', + ); + + final field = user.getComparableField(UserSortKey.id); + expect(field, isNotNull); + expect(field!.value, equals('test-user')); + }); + + test('should return ComparableField for user.name', () { + final user = createTestUser( + id: 'test-user', + name: 'Test User', + ); + + final field = user.getComparableField(UserSortKey.name); + expect(field, isNotNull); + expect(field!.value, equals('Test User')); + }); + + test('should return ComparableField for user.role', () { + final user = createTestUser( + id: 'test-user', + role: 'admin', + ); + + final field = user.getComparableField(UserSortKey.role); + expect(field, isNotNull); + expect(field!.value, equals('admin')); + }); + + test('should return ComparableField for user.banned', () { + final user = createTestUser( + id: 'test-user', + banned: true, + ); + + final field = user.getComparableField(UserSortKey.banned); + expect(field, isNotNull); + expect(field!.value, isTrue); + }); + + test('should return ComparableField for user.lastActive', () { + final lastActive = DateTime(2023, 6, 15); + final user = createTestUser( + id: 'test-user', + lastActive: lastActive, + ); + + final field = user.getComparableField(UserSortKey.lastActive); + expect(field, isNotNull); + expect(field!.value, equals(lastActive)); + }); + + test('should return ComparableField for user.extraData', () { + final user = createTestUser( + id: 'test-user', + extraData: {'score': 42}, + ); + + final field = user.getComparableField('score'); + expect(field, isNotNull); + expect(field!.value, equals(42)); + }); + + test('should return null for non-existent extraData keys', () { + final user = createTestUser( + id: 'test-user', + ); + + final field = user.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two users correctly using name', () { + final user1 = createTestUser( + id: 'user1', + name: 'Alice', + ); + + final user2 = createTestUser( + id: 'user2', + name: 'Bob', + ); + + final field1 = user1.getComparableField(UserSortKey.name); + final field2 = user2.getComparableField(UserSortKey.name); + + expect(field1!.compareTo(field2!), lessThan(0)); // Alice < Bob + expect(field2.compareTo(field1), greaterThan(0)); // Bob > Alice + }); + + test('should compare two users correctly using lastActive', () { + final recentlyActive = createTestUser( + id: 'recent', + lastActive: DateTime(2023, 6, 15), + ); + + final lessRecentlyActive = createTestUser( + id: 'old', + lastActive: DateTime(2023, 6, 10), + ); + + final field1 = recentlyActive.getComparableField(UserSortKey.lastActive); + final field2 = lessRecentlyActive.getComparableField(UserSortKey.lastActive); + + expect(field1!.compareTo(field2!), greaterThan(0)); // More recent > Less recent + expect(field2.compareTo(field1), lessThan(0)); // Less recent < More recent + }); + + test('should compare two users correctly using banned status', () { + final bannedUser = createTestUser( + id: 'banned', + banned: true, + ); + + final notBannedUser = createTestUser( + id: 'not-banned', + banned: false, + ); + + final field1 = bannedUser.getComparableField(UserSortKey.banned); + final field2 = notBannedUser.getComparableField(UserSortKey.banned); + + expect(field1!.compareTo(field2!), greaterThan(0)); // true > false + expect(field2.compareTo(field1), lessThan(0)); // false < true + }); + + test('should fallback to user id when name is null', () { + // The User implementation fallbacks to id when name is null + final user = createTestUser( + id: 'without-name', + name: null, + ); + + final field = user.getComparableField(UserSortKey.name); + expect(field, isNotNull); + expect(field!.value, equals('without-name')); // Fallback to user id + }); + }); }); } + +/// Helper function to create a User for testing +User createTestUser({ + required String id, + String? name, + String? role, + bool? banned, + DateTime? lastActive, + Map? extraData, +}) { + return User( + id: id, + name: name, + role: role, + banned: banned ?? false, + lastActive: lastActive, + extraData: extraData ?? {}, + ); +} diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index cd5313698f..843d3b5074 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -67,7 +67,7 @@ class TestPersistenceClient extends ChatPersistenceClient { @override Future> getChannelStates( {Filter? filter, - List>? channelStateSort, + Sort? channelStateSort, PaginationParams? paginationParams}) => throw UnimplementedError(); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index e6988540f0..a626418401 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -395,7 +395,7 @@ class StreamChannelState extends State { /// Query channel members. Future> queryMembers({ Filter? filter, - List? sort, + Sort? sort, PaginationParams? pagination, }) async { final response = await channel.queryMembers( diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index 0d61e06d2b..2eb8fd5066 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; import 'package:stream_chat_flutter_core/src/stream_channel_list_event_handler.dart'; @@ -8,6 +9,11 @@ import 'package:stream_chat_flutter_core/src/stream_channel_list_event_handler.d /// The default channel page limit to load. const defaultChannelPagedLimit = 10; +/// The default sort used for the channel list. +const defaultChannelListSort = [ + SortOption(ChannelSortKey.lastUpdated), +]; + const _kDefaultBackendPaginationLimit = 30; /// A controller for a Channel list. @@ -44,7 +50,7 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, StreamChannelListEventHandler? eventHandler, this.filter, - this.channelStateSort, + this.channelStateSort = defaultChannelListSort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, @@ -58,7 +64,7 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, StreamChannelListEventHandler? eventHandler, this.filter, - this.channelStateSort, + this.channelStateSort = defaultChannelListSort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, @@ -87,7 +93,7 @@ class StreamChannelListController extends PagedValueNotifier { /// created_at or member_count. /// /// Direction can be ascending or descending. - final List>? channelStateSort; + final Sort? channelStateSort; /// If true you’ll receive user presence updates via the websocket events final bool presence; @@ -102,6 +108,22 @@ class StreamChannelListController extends PagedValueNotifier { /// Number of members to fetch in each channel. final int? memberLimit; + @override + set value(PagedValue newValue) { + super.value = switch (channelStateSort) { + null => newValue, + final channelSort => newValue.maybeMap( + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sortedByCompare( + (it) => it.state!.channelState, + channelSort.compare, + ), + ), + ), + }; + } + @override Future doInitialLoad() async { final limit = min( diff --git a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart index 989aa87786..c0dbe2af7f 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart @@ -1,12 +1,21 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; /// The default channel page limit to load. const defaultMemberPagedLimit = 10; +/// The default sort used for the member list. +const defaultMemberListSort = [ + SortOption( + MemberSortKey.createdAt, + direction: SortOption.ASC, + ), +]; + const _kDefaultBackendPaginationLimit = 30; /// A controller for a member list. @@ -28,7 +37,7 @@ class StreamMemberListController extends PagedValueNotifier { StreamMemberListController({ required this.channel, this.filter, - this.sort, + this.sort = defaultMemberListSort, this.limit = defaultMemberPagedLimit, }) : _activeFilter = filter, _activeSort = sort, @@ -39,7 +48,7 @@ class StreamMemberListController extends PagedValueNotifier { super.value, { required this.channel, this.filter, - this.sort, + this.sort = defaultMemberListSort, this.limit = defaultMemberPagedLimit, }) : _activeFilter = filter, _activeSort = sort; @@ -61,8 +70,8 @@ class StreamMemberListController extends PagedValueNotifier { /// can be provided. /// /// Direction can be ascending or descending. - final List? sort; - List? _activeSort; + final Sort? sort; + Sort? _activeSort; /// The limit to apply to the member list. The default is set to /// [defaultMemberPagedLimit]. @@ -78,7 +87,20 @@ class StreamMemberListController extends PagedValueNotifier { /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(List? value) => _activeSort = value; + set sort(Sort? value) => _activeSort = value; + + @override + set value(PagedValue newValue) { + super.value = switch (_activeSort) { + null => newValue, + final memberSort => newValue.maybeMap( + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(memberSort.compare), + ), + ), + }; + } @override Future doInitialLoad() async { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart index b41a99f041..eff12e68f8 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart @@ -101,8 +101,8 @@ class StreamMessageSearchListController /// can be provided. /// /// Direction can be ascending or descending. - final List? sort; - List? _activeSort; + final Sort? sort; + Sort? _activeSort; /// The limit to apply to the user list. The default is set to /// [defaultUserPagedLimit]. @@ -130,7 +130,7 @@ class StreamMessageSearchListController /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(List? value) => _activeSort = value; + set sort(Sort? value) => _activeSort = value; @override Future doInitialLoad() async { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart index 3ac1cfd209..a502627064 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart @@ -1,12 +1,21 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; /// The default channel page limit to load. const defaultPollVotePagedLimit = 10; +/// The default sort used for the poll vote list. +const defaultPollVoteListSort = [ + SortOption( + PollVoteSortKey.createdAt, + direction: SortOption.ASC, + ), +]; + const _kDefaultBackendPaginationLimit = 30; /// A controller for a poll vote list. @@ -28,7 +37,7 @@ class StreamPollVoteListController required this.pollId, StreamPollVoteEventHandler? eventHandler, this.filter, - this.sort, + this.sort = defaultPollVoteListSort, this.limit = defaultPollVotePagedLimit, }) : _eventHandler = eventHandler ?? StreamPollVoteEventHandler(), _activeFilter = filter, @@ -42,7 +51,7 @@ class StreamPollVoteListController required this.pollId, StreamPollVoteEventHandler? eventHandler, this.filter, - this.sort, + this.sort = defaultPollVoteListSort, this.limit = defaultPollVotePagedLimit, }) : _eventHandler = eventHandler ?? StreamPollVoteEventHandler(), _activeFilter = filter, @@ -70,8 +79,8 @@ class StreamPollVoteListController /// can be provided. /// /// Direction can be ascending or descending. - final List? sort; - List? _activeSort; + final Sort? sort; + Sort? _activeSort; /// The limit to apply to the poll vote list. The default is set to /// [defaultPollVotePagedLimit]. @@ -87,7 +96,20 @@ class StreamPollVoteListController /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(List? value) => _activeSort = value; + set sort(Sort? value) => _activeSort = value; + + @override + set value(PagedValue newValue) { + super.value = switch (_activeSort) { + null => newValue, + final pollVoteSort => newValue.maybeMap( + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(pollVoteSort.compare), + ), + ), + }; + } @override Future doInitialLoad() async { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart index 466b61776d..ef273908c8 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart @@ -1,12 +1,18 @@ import 'dart:async'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; /// The default channel page limit to load. const defaultUserPagedLimit = 10; +/// The default sort used for the user list. +const defaultUserListSort = [ + SortOption(UserSortKey.createdAt), +]; + const _kDefaultBackendPaginationLimit = 30; /// A controller for a user list. @@ -31,7 +37,7 @@ class StreamUserListController extends PagedValueNotifier { StreamUserListController({ required this.client, this.filter, - this.sort, + this.sort = defaultUserListSort, this.presence = true, this.limit = defaultUserPagedLimit, }) : _activeFilter = filter, @@ -43,7 +49,7 @@ class StreamUserListController extends PagedValueNotifier { super.value, { required this.client, this.filter, - this.sort, + this.sort = defaultUserListSort, this.presence = true, this.limit = defaultUserPagedLimit, }) : _activeFilter = filter, @@ -66,8 +72,8 @@ class StreamUserListController extends PagedValueNotifier { /// can be provided. /// /// Direction can be ascending or descending. - final List? sort; - List? _activeSort; + final Sort? sort; + Sort? _activeSort; /// If true you’ll receive user presence updates via the websocket events final bool presence; @@ -86,7 +92,20 @@ class StreamUserListController extends PagedValueNotifier { /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(List? value) => _activeSort = value; + set sort(Sort? value) => _activeSort = value; + + @override + set value(PagedValue newValue) { + super.value = switch (_activeSort) { + null => newValue, + final userSort => newValue.maybeMap( + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(userSort.compare), + ), + ), + }; + } @override Future doInitialLoad() async { diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index e7a74f5845..b0931ee3be 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -64,20 +64,7 @@ class ChannelQueryDao extends DatabaseAccessor } /// Get list of channels by filter, sort and paginationParams - Future> getChannels({ - Filter? filter, - List>? sort, - PaginationParams? paginationParams, - }) async { - assert(() { - if (sort != null && sort.any((it) => it.comparator == null)) { - throw ArgumentError( - 'SortOption requires a comparator in order to sort', - ); - } - return true; - }(), ''); - + Future> getChannels({Filter? filter}) async { final cachedChannelCids = await getCachedChannelCids(filter); final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids)); @@ -89,38 +76,6 @@ class ChannelQueryDao extends DatabaseAccessor return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser()); }).get(); - var chainedComparator = (ChannelModel a, ChannelModel b) { - final dateA = a.lastMessageAt ?? a.createdAt; - final dateB = b.lastMessageAt ?? b.createdAt; - return dateB.compareTo(dateA); - }; - - if (sort != null && sort.isNotEmpty) { - chainedComparator = (a, b) { - int result; - for (final comparator in sort.map((it) => it.comparator)) { - try { - result = comparator!(a, b); - } catch (e) { - result = 0; - } - if (result != 0) return result; - } - return 0; - }; - } - - cachedChannels.sort(chainedComparator); - - final offset = paginationParams?.offset; - if (offset != null && offset > 0 && cachedChannels.isNotEmpty) { - cachedChannels.removeRange(0, offset); - } - - if (paginationParams?.limit != null) { - return cachedChannels.take(paginationParams!.limit).toList(); - } - return cachedChannels; } } diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 089b1544c6..25cd084f7e 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -251,19 +251,10 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { @override Future> getChannelStates({ Filter? filter, - List>? channelStateSort, + Sort? channelStateSort, PaginationParams? paginationParams, }) async { assert(_debugIsConnected, ''); - assert(() { - if (channelStateSort?.any((it) => it.comparator == null) ?? false) { - throw ArgumentError( - 'SortOption requires a comparator in order to sort', - ); - } - return true; - }(), ''); - _logger.info('getChannelStates'); final channels = await db!.channelQueryDao.getChannels(filter: filter); @@ -273,13 +264,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); // Sort the channel states - var comparator = _defaultChannelStateComparator; if (channelStateSort != null && channelStateSort.isNotEmpty) { - comparator = _combineComparators( - channelStateSort.map((it) => it.comparator).withNullifyer, - ); + channelStates.sort(channelStateSort.compare); } - channelStates.sort(comparator); final offset = paginationParams?.offset; if (offset != null && offset > 0 && channelStates.isNotEmpty) { @@ -446,35 +433,3 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } } } - -// Creates a new combined [Comparator] which sorts items -// by the given [comparators]. -Comparator _combineComparators(Iterable> comparators) { - return (T a, T b) { - for (final comparator in comparators) { - try { - final result = comparator(a, b); - if (result != 0) return result; - } catch (e) { - // If the comparator throws an exception, we ignore it and - // continue with the next comparator. - continue; - } - } - return 0; - }; -} - -// The default [Comparator] used to sort [ChannelState]s. -int _defaultChannelStateComparator(ChannelState a, ChannelState b) { - final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt; - final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt; - - if (dateA == null && dateB == null) return 0; - if (dateA == null) return 1; - if (dateB == null) { - return -1; - } else { - return dateB.compareTo(dateA); - } -} diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index 91c7ad7555..5adbc99063 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -136,92 +136,6 @@ void main() { ); } }); - - test('should return sorted channels using member count', () async { - int sortComparator(ChannelModel a, ChannelModel b) => - b.memberCount.compareTo(a.memberCount); - - // Inserting test data for get channels - final insertedChannels = await _insertTestDataForGetChannel(filter); - insertedChannels.sort(sortComparator); - - // Should match with the inserted channels - final updatedChannels = await channelQueryDao.getChannels( - filter: filter, - sort: [ - SortOption( - 'member_count', - comparator: sortComparator, - ) - ], - ); - - expect(updatedChannels.length, insertedChannels.length); - for (var i = 0; i < updatedChannels.length; i++) { - final updatedChannel = updatedChannels[i]; - final insertedChannel = insertedChannels[i]; - - // Should match all the basic details - expect(updatedChannel.id, insertedChannel.id); - expect(updatedChannel.type, insertedChannel.type); - expect(updatedChannel.cid, insertedChannel.cid); - expect(updatedChannel.memberCount, insertedChannel.memberCount); - - // Should match createdAt date - expect( - updatedChannel.createdAt, - isSameDateAs(insertedChannel.createdAt), - ); - - // Should match lastMessageAt date - expect( - updatedChannel.lastMessageAt, - isSameDateAs(insertedChannel.lastMessageAt), - ); - } - }); - - test('should return sorted channels using custom field', () async { - int sortComparator(ChannelModel a, ChannelModel b) { - final aData = int.parse(a.extraData['test_custom_field'].toString()); - final bData = int.parse(b.extraData['test_custom_field'].toString()); - return bData.compareTo(aData); - } - - // Inserting test data for get channels - final insertedChannels = await _insertTestDataForGetChannel(filter); - insertedChannels.sort(sortComparator); - - // Should match with the inserted channels - final updatedChannels = await channelQueryDao.getChannels( - filter: filter, - sort: [SortOption('test_custom_field', comparator: sortComparator)], - ); - - expect(updatedChannels.length, insertedChannels.length); - for (var i = 0; i < updatedChannels.length; i++) { - final updatedChannel = updatedChannels[i]; - final insertedChannel = insertedChannels[i]; - - // Should match all the basic details - expect(updatedChannel.id, insertedChannel.id); - expect(updatedChannel.type, insertedChannel.type); - expect(updatedChannel.cid, insertedChannel.cid); - expect(updatedChannel.memberCount, insertedChannel.memberCount); - - // Should match createdAt date - expect( - updatedChannel.createdAt, - isSameDateAs(insertedChannel.createdAt), - ); - - // Should match lastMessageAt date - expect( - updatedChannel.lastMessageAt, - isSameDateAs(insertedChannel.lastMessageAt), - ); - } - }); }); tearDown(() async { From b97126695f22aec07e80ec11ace07cc4c5dfbadb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Apr 2025 23:40:14 +0200 Subject: [PATCH 2/9] chore: rename Sort to SortOrder --- .../stream_chat/lib/src/client/channel.dart | 6 +++--- .../stream_chat/lib/src/client/client.dart | 18 +++++++++--------- .../lib/src/core/api/channel_api.dart | 2 +- .../lib/src/core/api/general_api.dart | 4 ++-- .../lib/src/core/api/moderation_api.dart | 2 +- .../lib/src/core/api/polls_api.dart | 4 ++-- .../stream_chat/lib/src/core/api/requests.dart | 7 ++++--- .../stream_chat/lib/src/core/api/user_api.dart | 2 +- .../lib/src/db/chat_persistence_client.dart | 2 +- .../src/db/chat_persistence_client_test.dart | 2 +- .../lib/src/stream_channel.dart | 2 +- .../src/stream_channel_list_controller.dart | 2 +- .../lib/src/stream_member_list_controller.dart | 6 +++--- .../stream_message_search_list_controller.dart | 6 +++--- .../src/stream_poll_vote_list_controller.dart | 6 +++--- .../lib/src/stream_user_list_controller.dart | 6 +++--- .../src/stream_chat_persistence_client.dart | 2 +- 17 files changed, 40 insertions(+), 39 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index f1d264b245..104a321327 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1224,7 +1224,7 @@ class Channel { Future queryPollVotes( String pollId, { Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams pagination = const PaginationParams(), }) { _checkInitialized(); @@ -1783,7 +1783,7 @@ class Channel { /// Query channel members. Future queryMembers({ Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) => _client.queryMembers( @@ -1798,7 +1798,7 @@ class Channel { /// Query channel banned users. Future queryBannedUsers({ Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) { _checkInitialized(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index cce2ae9b88..6334215570 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -609,7 +609,7 @@ class StreamChatClient { /// Requests channels with a given query. Stream> queryChannels({ Filter? filter, - Sort? channelStateSort, + SortOrder? channelStateSort, bool state = true, bool watch = true, bool presence = false, @@ -718,7 +718,7 @@ class StreamChatClient { /// Requests channels with a given query from the API. Future> queryChannelsOnline({ Filter? filter, - Sort? sort, + SortOrder? sort, bool state = true, bool watch = true, bool presence = false, @@ -792,7 +792,7 @@ class StreamChatClient { /// Requests channels with a given query from the Persistence client. Future> queryChannelsOffline({ Filter? filter, - Sort? channelStateSort, + SortOrder? channelStateSort, PaginationParams paginationParams = const PaginationParams(), }) async { final offlineChannels = (await chatPersistenceClient?.getChannelStates( @@ -831,7 +831,7 @@ class StreamChatClient { Future queryUsers({ bool? presence, Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) async { final response = await _chatApi.user.queryUsers( @@ -847,7 +847,7 @@ class StreamChatClient { /// Query banned users. Future queryBannedUsers({ required Filter filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) => _chatApi.moderation.queryBannedUsers( @@ -860,7 +860,7 @@ class StreamChatClient { Future search( Filter filter, { String? query, - Sort? sort, + SortOrder? sort, PaginationParams? paginationParams, Filter? messageFilters, }) => @@ -1065,7 +1065,7 @@ class StreamChatClient { Filter? filter, String? channelId, List? members, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) => _chatApi.general.queryMembers( @@ -1386,7 +1386,7 @@ class StreamChatClient { /// Queries Polls with the given [filter] and [sort] options. Future queryPolls({ Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams pagination = const PaginationParams(), }) => _chatApi.polls.queryPolls( @@ -1400,7 +1400,7 @@ class StreamChatClient { Future queryPollVotes( String pollId, { Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams pagination = const PaginationParams(), }) => _chatApi.polls.queryPollVotes( diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 59b6a6223c..b9b59f52fd 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -50,7 +50,7 @@ class ChannelApi { /// Requests channels with a given query from the API. Future queryChannels({ Filter? filter, - Sort? sort, + SortOrder? sort, int? memberLimit, int? messageLimit, bool state = true, diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index f2174c870b..3cf77ef95b 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -32,7 +32,7 @@ class GeneralApi { Future searchMessages( Filter filter, { String? query, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, Filter? messageFilters, }) async { @@ -75,7 +75,7 @@ class GeneralApi { Filter? filter, String? channelId, List? members, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/core/api/moderation_api.dart b/packages/stream_chat/lib/src/core/api/moderation_api.dart index 0fd06d2a30..1da82fab8c 100644 --- a/packages/stream_chat/lib/src/core/api/moderation_api.dart +++ b/packages/stream_chat/lib/src/core/api/moderation_api.dart @@ -131,7 +131,7 @@ class ModerationApi { /// Queries banned users. Future queryBannedUsers({ Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/core/api/polls_api.dart b/packages/stream_chat/lib/src/core/api/polls_api.dart index 14c3e5e16b..1e92aea7ff 100644 --- a/packages/stream_chat/lib/src/core/api/polls_api.dart +++ b/packages/stream_chat/lib/src/core/api/polls_api.dart @@ -162,7 +162,7 @@ class PollsApi { /// parameters. Future queryPolls({ Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams pagination = const PaginationParams(), }) async { final response = await _client.post( @@ -181,7 +181,7 @@ class PollsApi { Future queryPollVotes( String pollId, { Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams pagination = const PaginationParams(), }) async { final response = await _client.post( diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index a0943e8edb..1cb4af9092 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -12,10 +12,11 @@ part 'requests.g.dart'; /// until a non-equal comparison is found. /// /// Example: `Sort([pinnedAtSort, lastMessageAtSort])` -typedef Sort = List>; +typedef SortOrder = List>; -/// Extension that allows a [Sort] to be used as a comparator function. -extension CompositeComparator on Sort { +/// Extension that allows a [SortOrder] to be used as a comparator function. +extension CompositeComparator + on SortOrder { /// Compares two objects using all sort options in sequence. /// /// Returns the first non-zero comparison result, or 0 if all comparisons diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index df046b443c..9702d68026 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -17,7 +17,7 @@ class UserApi { Future queryUsers({ bool presence = false, Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index da83d4949a..ab9079db35 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -104,7 +104,7 @@ abstract class ChatPersistenceClient { /// for filtering out states. Future> getChannelStates({ Filter? filter, - Sort? channelStateSort, + SortOrder? channelStateSort, PaginationParams? paginationParams, }); diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 843d3b5074..fcaa29b867 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -67,7 +67,7 @@ class TestPersistenceClient extends ChatPersistenceClient { @override Future> getChannelStates( {Filter? filter, - Sort? channelStateSort, + SortOrder? channelStateSort, PaginationParams? paginationParams}) => throw UnimplementedError(); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index a626418401..d5f666a689 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -395,7 +395,7 @@ class StreamChannelState extends State { /// Query channel members. Future> queryMembers({ Filter? filter, - Sort? sort, + SortOrder? sort, PaginationParams? pagination, }) async { final response = await channel.queryMembers( diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index 2eb8fd5066..90c13ad55e 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -93,7 +93,7 @@ class StreamChannelListController extends PagedValueNotifier { /// created_at or member_count. /// /// Direction can be ascending or descending. - final Sort? channelStateSort; + final SortOrder? channelStateSort; /// If true you’ll receive user presence updates via the websocket events final bool presence; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart index c0dbe2af7f..c4957b4cce 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart @@ -70,8 +70,8 @@ class StreamMemberListController extends PagedValueNotifier { /// can be provided. /// /// Direction can be ascending or descending. - final Sort? sort; - Sort? _activeSort; + final SortOrder? sort; + SortOrder? _activeSort; /// The limit to apply to the member list. The default is set to /// [defaultMemberPagedLimit]. @@ -87,7 +87,7 @@ class StreamMemberListController extends PagedValueNotifier { /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(Sort? value) => _activeSort = value; + set sort(SortOrder? value) => _activeSort = value; @override set value(PagedValue newValue) { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart index eff12e68f8..7b1f7e9c15 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart @@ -101,8 +101,8 @@ class StreamMessageSearchListController /// can be provided. /// /// Direction can be ascending or descending. - final Sort? sort; - Sort? _activeSort; + final SortOrder? sort; + SortOrder? _activeSort; /// The limit to apply to the user list. The default is set to /// [defaultUserPagedLimit]. @@ -130,7 +130,7 @@ class StreamMessageSearchListController /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(Sort? value) => _activeSort = value; + set sort(SortOrder? value) => _activeSort = value; @override Future doInitialLoad() async { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart index a502627064..735cacda03 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart @@ -79,8 +79,8 @@ class StreamPollVoteListController /// can be provided. /// /// Direction can be ascending or descending. - final Sort? sort; - Sort? _activeSort; + final SortOrder? sort; + SortOrder? _activeSort; /// The limit to apply to the poll vote list. The default is set to /// [defaultPollVotePagedLimit]. @@ -96,7 +96,7 @@ class StreamPollVoteListController /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(Sort? value) => _activeSort = value; + set sort(SortOrder? value) => _activeSort = value; @override set value(PagedValue newValue) { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart index ef273908c8..66c5e22ebc 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart @@ -72,8 +72,8 @@ class StreamUserListController extends PagedValueNotifier { /// can be provided. /// /// Direction can be ascending or descending. - final Sort? sort; - Sort? _activeSort; + final SortOrder? sort; + SortOrder? _activeSort; /// If true you’ll receive user presence updates via the websocket events final bool presence; @@ -92,7 +92,7 @@ class StreamUserListController extends PagedValueNotifier { /// /// Use this if you need to support runtime sort changes, /// through custom sort UI. - set sort(Sort? value) => _activeSort = value; + set sort(SortOrder? value) => _activeSort = value; @override set value(PagedValue newValue) { diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 25cd084f7e..f8b72fdd48 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -251,7 +251,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { @override Future> getChannelStates({ Filter? filter, - Sort? channelStateSort, + SortOrder? channelStateSort, PaginationParams? paginationParams, }) async { assert(_debugIsConnected, ''); From a26f38000feaf8b77ac6a8bbad64315cd0b2ef19 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Apr 2025 23:41:26 +0200 Subject: [PATCH 3/9] refactor: convert ComparableFieldProvider to an interface --- packages/stream_chat/lib/src/core/models/banned_user.dart | 2 +- packages/stream_chat/lib/src/core/models/channel_state.dart | 2 +- packages/stream_chat/lib/src/core/models/comparable_field.dart | 2 +- packages/stream_chat/lib/src/core/models/message.dart | 2 +- packages/stream_chat/lib/src/core/models/poll.dart | 2 +- packages/stream_chat/lib/src/core/models/poll_vote.dart | 2 +- packages/stream_chat/lib/src/core/models/user.dart | 2 +- packages/stream_chat/test/src/core/api/requests_test.dart | 2 +- .../stream_chat/test/src/core/models/comparable_field_test.dart | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/banned_user.dart b/packages/stream_chat/lib/src/core/models/banned_user.dart index 519cb5eb7b..4f25ac75fe 100644 --- a/packages/stream_chat/lib/src/core/models/banned_user.dart +++ b/packages/stream_chat/lib/src/core/models/banned_user.dart @@ -8,7 +8,7 @@ part 'banned_user.g.dart'; /// Contains information about a [User] that was banned from a [Channel] or App. @JsonSerializable() -class BannedUser extends Equatable with ComparableFieldProvider { +class BannedUser extends Equatable implements ComparableFieldProvider { /// Creates a new instance of [BannedUser] const BannedUser({ required this.user, diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index d6e317a40a..108810aa8f 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -10,7 +10,7 @@ part 'channel_state.g.dart'; /// The class that contains the information about a channel @JsonSerializable() -class ChannelState with ComparableFieldProvider { +class ChannelState implements ComparableFieldProvider { /// Constructor used for json serialization ChannelState({ this.channel, diff --git a/packages/stream_chat/lib/src/core/models/comparable_field.dart b/packages/stream_chat/lib/src/core/models/comparable_field.dart index cb75b99a64..334e694f9c 100644 --- a/packages/stream_chat/lib/src/core/models/comparable_field.dart +++ b/packages/stream_chat/lib/src/core/models/comparable_field.dart @@ -37,7 +37,7 @@ class ComparableField implements Comparable> { /// /// Classes that implement this mixin can be used in sorting operations /// where the sort key is determined at runtime. -mixin ComparableFieldProvider { +abstract interface class ComparableFieldProvider { /// Gets a comparable field value for the given [sortKey]. /// /// Returns a [ComparableField] or null if no comparable field with the given diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index d207326f05..d1e04468b0 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -20,7 +20,7 @@ const _nullConst = _NullConst(); /// The class that contains the information about a message. @JsonSerializable() -class Message extends Equatable with ComparableFieldProvider { +class Message extends Equatable implements ComparableFieldProvider { /// Constructor used for json serialization. Message({ String? id, diff --git a/packages/stream_chat/lib/src/core/models/poll.dart b/packages/stream_chat/lib/src/core/models/poll.dart index b46d66fc13..dd61feac2b 100644 --- a/packages/stream_chat/lib/src/core/models/poll.dart +++ b/packages/stream_chat/lib/src/core/models/poll.dart @@ -33,7 +33,7 @@ enum VotingVisibility { /// A model class representing a poll. /// {@endtemplate} @JsonSerializable() -class Poll extends Equatable with ComparableFieldProvider { +class Poll extends Equatable implements ComparableFieldProvider { /// {@macro streamPoll} Poll({ String? id, diff --git a/packages/stream_chat/lib/src/core/models/poll_vote.dart b/packages/stream_chat/lib/src/core/models/poll_vote.dart index 0db4707f52..a48bb03d9e 100644 --- a/packages/stream_chat/lib/src/core/models/poll_vote.dart +++ b/packages/stream_chat/lib/src/core/models/poll_vote.dart @@ -9,7 +9,7 @@ part 'poll_vote.g.dart'; /// A model class representing a poll vote. /// {@endtemplate} @JsonSerializable() -class PollVote extends Equatable with ComparableFieldProvider { +class PollVote extends Equatable implements ComparableFieldProvider { /// {@macro streamPollVote} PollVote({ this.id, diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index b57928426c..68fa910042 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -7,7 +7,7 @@ part 'user.g.dart'; /// Class that defines a Stream Chat User. @JsonSerializable() -class User extends Equatable with ComparableFieldProvider { +class User extends Equatable implements ComparableFieldProvider { /// Creates a new user. /// /// {@template name} diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index d6f2ef2e40..0f6498b9fb 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -6,7 +6,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; /// Simple test model that implements ComparableFieldProvider -class TestModel with ComparableFieldProvider { +class TestModel implements ComparableFieldProvider { const TestModel({ this.name, this.age, diff --git a/packages/stream_chat/test/src/core/models/comparable_field_test.dart b/packages/stream_chat/test/src/core/models/comparable_field_test.dart index 7a817d2f78..22ad363607 100644 --- a/packages/stream_chat/test/src/core/models/comparable_field_test.dart +++ b/packages/stream_chat/test/src/core/models/comparable_field_test.dart @@ -138,7 +138,7 @@ void main() { } /// Test implementation of ComparableFieldProvider -class TestProvider with ComparableFieldProvider { +class TestProvider implements ComparableFieldProvider { TestProvider({ this.name = 'test', this.age = 0, From a2d787bed4dad4140b22d237c5034b09d2738e2e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Apr 2025 23:42:26 +0200 Subject: [PATCH 4/9] chore: remove `one_member_abstracts` lint rule --- analysis_options.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 90c65a7ebd..d5ac4a48f4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -68,7 +68,6 @@ linter: - missing_whitespace_between_adjacent_strings - non_constant_identifier_names - null_closures - - one_member_abstracts - only_throw_errors - package_prefixed_library_names - parameter_assignments From a35350afea2b5b9bd0419a4c4809b132c3573dd0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Apr 2025 23:43:06 +0200 Subject: [PATCH 5/9] chore: update doc --- .../stream_chat/lib/src/core/models/comparable_field.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/comparable_field.dart b/packages/stream_chat/lib/src/core/models/comparable_field.dart index 334e694f9c..f43df58a33 100644 --- a/packages/stream_chat/lib/src/core/models/comparable_field.dart +++ b/packages/stream_chat/lib/src/core/models/comparable_field.dart @@ -33,9 +33,9 @@ class ComparableField implements Comparable> { } } -/// A mixin that provides a way to access comparable fields by string keys. +/// A interface that provides a way to access comparable fields by string keys. /// -/// Classes that implement this mixin can be used in sorting operations +/// Classes that implement this class can be used in sorting operations /// where the sort key is determined at runtime. abstract interface class ComparableFieldProvider { /// Gets a comparable field value for the given [sortKey]. From afcba431053058ffb91aaa4f83bbeab6f97b5712 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 8 Apr 2025 23:49:59 +0200 Subject: [PATCH 6/9] refactor: move sortOrder out of requests --- .../stream_chat/lib/src/client/client.dart | 1 + .../lib/src/core/api/channel_api.dart | 1 + .../lib/src/core/api/general_api.dart | 1 + .../lib/src/core/api/polls_api.dart | 1 + .../lib/src/core/api/requests.dart | 136 ----------------- .../lib/src/core/api/requests.g.dart | 14 -- .../lib/src/core/api/sort_order.dart | 139 ++++++++++++++++++ .../lib/src/core/api/sort_order.g.dart | 21 +++ .../lib/src/core/api/user_api.dart | 1 + .../lib/src/db/chat_persistence_client.dart | 1 + packages/stream_chat/lib/stream_chat.dart | 1 + .../test/src/core/api/polls_api_test.dart | 1 + .../test/src/core/api/requests_test.dart | 1 - .../src/db/chat_persistence_client_test.dart | 1 + 14 files changed, 169 insertions(+), 151 deletions(-) create mode 100644 packages/stream_chat/lib/src/core/api/sort_order.dart create mode 100644 packages/stream_chat/lib/src/core/api/sort_order.g.dart diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 6334215570..ed88d9f556 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -9,6 +9,7 @@ import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/api/stream_chat_api.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index b9b59f52fd..b4d9df213a 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index 3cf77ef95b..c364674463 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/member.dart'; diff --git a/packages/stream_chat/lib/src/core/api/polls_api.dart b/packages/stream_chat/lib/src/core/api/polls_api.dart index 1e92aea7ff..fda60106ee 100644 --- a/packages/stream_chat/lib/src/core/api/polls_api.dart +++ b/packages/stream_chat/lib/src/core/api/polls_api.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/poll.dart'; diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 1cb4af9092..2e0bc5e6e7 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -1,144 +1,8 @@ -// ignore_for_file: constant_identifier_names - import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/models/comparable_field.dart'; part 'requests.g.dart'; -/// A list of [SortOption]s that define a sorting order for elements of type [T] -/// -/// When multiple sort options are provided, they are applied in sequence -/// until a non-equal comparison is found. -/// -/// Example: `Sort([pinnedAtSort, lastMessageAtSort])` -typedef SortOrder = List>; - -/// Extension that allows a [SortOrder] to be used as a comparator function. -extension CompositeComparator - on SortOrder { - /// Compares two objects using all sort options in sequence. - /// - /// Returns the first non-zero comparison result, or 0 if all comparisons - /// result in equality. - /// - /// ```dart - /// channels.sort(mySort.compare); - /// ``` - int compare(T a, T b) { - for (final sortOption in this) { - final comparison = sortOption.compare(a, b); - if (comparison != 0) return comparison; - } - - return 0; // All comparisons were equal - } -} - -/// A sort specification for objects that implement [ComparableFieldProvider]. -/// -/// Defines a field to sort by and a direction (ascending or descending). -/// Can use a custom comparator or create a default one based on the field name. -/// -/// Example: -/// ```dart -/// // Sort channels by last message date in descending order -/// final sort = SortOption("last_message_at"); -/// ``` -@JsonSerializable(includeIfNull: false) -class SortOption { - /// Creates a new SortOption instance with the specified field and direction. - /// - /// ```dart - /// final sorting = SortOption("last_message_at") // Default: descending order - /// ``` - const SortOption( - this.field, { - this.direction = SortOption.DESC, - Comparator? comparator, - }) : _comparator = comparator; - - /// Creates a SortOption for descending order sorting by the specified field. - /// - /// Example: - /// ```dart - /// // Sort channels by last message date in descending order - /// final sort = SortOption.desc("last_message_at"); - /// ``` - const SortOption.desc( - this.field, { - Comparator? comparator, - }) : direction = SortOption.DESC, - _comparator = comparator; - - /// Creates a SortOption for ascending order sorting by the specified field. - /// - /// Example: - /// ```dart - /// // Sort channels by name in ascending order - /// final sort = SortOption.asc("name"); - /// ``` - const SortOption.asc( - this.field, { - Comparator? comparator, - }) : direction = SortOption.ASC, - _comparator = comparator; - - /// Create a new instance from JSON. - factory SortOption.fromJson(Map json) => - _$SortOptionFromJson(json); - - /// Ascending order (1) - static const ASC = 1; - - /// Descending order (-1) - static const DESC = -1; - - /// The field name to sort by - final String field; - - /// The sort direction (ASC or DESC) - final int direction; - - /// Compares two objects of type T using the specified field and direction. - /// - /// Returns: - /// - 0 if both objects are equal - /// - 1 if the first object is greater than the second - /// - -1 if the first object is less than the second - /// - /// Handles null values by treating null as less than any non-null value. - /// - /// ```dart - /// final sortOption = SortOption("last_message_at"); - /// final sortedChannels = channels.sort(sortOption.compare); - /// ``` - int compare(T a, T b) => direction * comparator(a, b); - - /// Returns a comparator function for sorting objects of type T. - @JsonKey(includeToJson: false, includeFromJson: false) - Comparator get comparator { - if (_comparator case final comparator?) return comparator; - - return (T a, T b) { - final aValue = a.getComparableField(field); - final bValue = b.getComparableField(field); - - // Handle null values - if (aValue == null && bValue == null) return 0; - if (aValue == null) return -1; - if (bValue == null) return 1; - - return aValue.compareTo(bValue); - }; - } - - final Comparator? _comparator; - - /// Converts this option to JSON. - Map toJson() => _$SortOptionToJson(this); -} - /// Pagination options. @JsonSerializable(includeIfNull: false) class PaginationParams extends Equatable { diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index 3896ed2cf4..d0200535df 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -6,20 +6,6 @@ part of 'requests.dart'; // JsonSerializableGenerator // ************************************************************************** -SortOption _$SortOptionFromJson( - Map json) => - SortOption( - json['field'] as String, - direction: (json['direction'] as num?)?.toInt() ?? SortOption.DESC, - ); - -Map _$SortOptionToJson( - SortOption instance) => - { - 'field': instance.field, - 'direction': instance.direction, - }; - PaginationParams _$PaginationParamsFromJson(Map json) => PaginationParams( limit: (json['limit'] as num?)?.toInt() ?? 10, diff --git a/packages/stream_chat/lib/src/core/api/sort_order.dart b/packages/stream_chat/lib/src/core/api/sort_order.dart new file mode 100644 index 0000000000..c30aa2274b --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/sort_order.dart @@ -0,0 +1,139 @@ +// ignore_for_file: constant_identifier_names + +import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; + +part 'sort_order.g.dart'; + +/// A list of [SortOption]s that define a sorting order for elements of type [T] +/// +/// When multiple sort options are provided, they are applied in sequence +/// until a non-equal comparison is found. +/// +/// Example: `Sort([pinnedAtSort, lastMessageAtSort])` +typedef SortOrder = List>; + +/// A sort specification for objects that implement [ComparableFieldProvider]. +/// +/// Defines a field to sort by and a direction (ascending or descending). +/// Can use a custom comparator or create a default one based on the field name. +/// +/// Example: +/// ```dart +/// // Sort channels by last message date in descending order +/// final sort = SortOption("last_message_at"); +/// ``` +@JsonSerializable(includeIfNull: false) +class SortOption { + /// Creates a new SortOption instance with the specified field and direction. + /// + /// ```dart + /// final sorting = SortOption("last_message_at") // Default: descending order + /// ``` + const SortOption( + this.field, { + this.direction = SortOption.DESC, + Comparator? comparator, + }) : _comparator = comparator; + + /// Creates a SortOption for descending order sorting by the specified field. + /// + /// Example: + /// ```dart + /// // Sort channels by last message date in descending order + /// final sort = SortOption.desc("last_message_at"); + /// ``` + const SortOption.desc( + this.field, { + Comparator? comparator, + }) : direction = SortOption.DESC, + _comparator = comparator; + + /// Creates a SortOption for ascending order sorting by the specified field. + /// + /// Example: + /// ```dart + /// // Sort channels by name in ascending order + /// final sort = SortOption.asc("name"); + /// ``` + const SortOption.asc( + this.field, { + Comparator? comparator, + }) : direction = SortOption.ASC, + _comparator = comparator; + + /// Create a new instance from JSON. + factory SortOption.fromJson(Map json) => + _$SortOptionFromJson(json); + + /// Ascending order (1) + static const ASC = 1; + + /// Descending order (-1) + static const DESC = -1; + + /// The field name to sort by + final String field; + + /// The sort direction (ASC or DESC) + final int direction; + + /// Compares two objects of type T using the specified field and direction. + /// + /// Returns: + /// - 0 if both objects are equal + /// - 1 if the first object is greater than the second + /// - -1 if the first object is less than the second + /// + /// Handles null values by treating null as less than any non-null value. + /// + /// ```dart + /// final sortOption = SortOption("last_message_at"); + /// final sortedChannels = channels.sort(sortOption.compare); + /// ``` + int compare(T a, T b) => direction * comparator(a, b); + + /// Returns a comparator function for sorting objects of type T. + @JsonKey(includeToJson: false, includeFromJson: false) + Comparator get comparator { + if (_comparator case final comparator?) return comparator; + + return (T a, T b) { + final aValue = a.getComparableField(field); + final bValue = b.getComparableField(field); + + // Handle null values + if (aValue == null && bValue == null) return 0; + if (aValue == null) return -1; + if (bValue == null) return 1; + + return aValue.compareTo(bValue); + }; + } + + final Comparator? _comparator; + + /// Converts this option to JSON. + Map toJson() => _$SortOptionToJson(this); +} + +/// Extension that allows a [SortOrder] to be used as a comparator function. +extension CompositeComparator + on SortOrder { + /// Compares two objects using all sort options in sequence. + /// + /// Returns the first non-zero comparison result, or 0 if all comparisons + /// result in equality. + /// + /// ```dart + /// channels.sort(mySort.compare); + /// ``` + int compare(T a, T b) { + for (final sortOption in this) { + final comparison = sortOption.compare(a, b); + if (comparison != 0) return comparison; + } + + return 0; // All comparisons were equal + } +} diff --git a/packages/stream_chat/lib/src/core/api/sort_order.g.dart b/packages/stream_chat/lib/src/core/api/sort_order.g.dart new file mode 100644 index 0000000000..1a4e70f1fe --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/sort_order.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sort_order.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SortOption _$SortOptionFromJson( + Map json) => + SortOption( + json['field'] as String, + direction: (json['direction'] as num?)?.toInt() ?? SortOption.DESC, + ); + +Map _$SortOptionToJson( + SortOption instance) => + { + 'field': instance.field, + 'direction': instance.direction, + }; diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index 9702d68026..c94c344bec 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/user.dart'; diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index ab9079db35..85e25989d0 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -1,4 +1,5 @@ import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index d4a4ce356f..7835914e20 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -22,6 +22,7 @@ export 'src/client/key_stroke_handler.dart'; export 'src/core/api/attachment_file_uploader.dart'; export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; +export 'src/core/api/sort_order.dart'; export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; export 'src/core/http/interceptor/logging_interceptor.dart'; diff --git a/packages/stream_chat/test/src/core/api/polls_api_test.dart b/packages/stream_chat/test/src/core/api/polls_api_test.dart index 91e7629e74..77faed0044 100644 --- a/packages/stream_chat/test/src/core/api/polls_api_test.dart +++ b/packages/stream_chat/test/src/core/api/polls_api_test.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/polls_api.dart'; import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/poll.dart'; import 'package:stream_chat/src/core/models/poll_option.dart'; diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 0f6498b9fb..a9f04f6162 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: avoid_redundant_argument_values -import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index fcaa29b867..78fd790034 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -1,4 +1,5 @@ import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; From 8edcc3c3ceb44775404bfeb6de3d1e1f07a914b4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 9 Apr 2025 00:10:19 +0200 Subject: [PATCH 7/9] chore: fix formatting --- .../test/src/core/models/user_test.dart | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index c7d024cae1..1b7b6f43ac 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -219,13 +219,13 @@ void main() { expect(user.createdAt, null); expect(user.updatedAt, null); }); - + group('ComparableFieldProvider', () { test('should return ComparableField for user.id', () { final user = createTestUser( id: 'test-user', ); - + final field = user.getComparableField(UserSortKey.id); expect(field, isNotNull); expect(field!.value, equals('test-user')); @@ -236,7 +236,7 @@ void main() { id: 'test-user', name: 'Test User', ); - + final field = user.getComparableField(UserSortKey.name); expect(field, isNotNull); expect(field!.value, equals('Test User')); @@ -247,7 +247,7 @@ void main() { id: 'test-user', role: 'admin', ); - + final field = user.getComparableField(UserSortKey.role); expect(field, isNotNull); expect(field!.value, equals('admin')); @@ -258,7 +258,7 @@ void main() { id: 'test-user', banned: true, ); - + final field = user.getComparableField(UserSortKey.banned); expect(field, isNotNull); expect(field!.value, isTrue); @@ -270,7 +270,7 @@ void main() { id: 'test-user', lastActive: lastActive, ); - + final field = user.getComparableField(UserSortKey.lastActive); expect(field, isNotNull); expect(field!.value, equals(lastActive)); @@ -281,7 +281,7 @@ void main() { id: 'test-user', extraData: {'score': 42}, ); - + final field = user.getComparableField('score'); expect(field, isNotNull); expect(field!.value, equals(42)); @@ -291,7 +291,7 @@ void main() { final user = createTestUser( id: 'test-user', ); - + final field = user.getComparableField('non_existent_key'); expect(field, isNull); }); @@ -301,15 +301,15 @@ void main() { id: 'user1', name: 'Alice', ); - + final user2 = createTestUser( id: 'user2', name: 'Bob', ); - + final field1 = user1.getComparableField(UserSortKey.name); final field2 = user2.getComparableField(UserSortKey.name); - + expect(field1!.compareTo(field2!), lessThan(0)); // Alice < Bob expect(field2.compareTo(field1), greaterThan(0)); // Bob > Alice }); @@ -319,17 +319,21 @@ void main() { id: 'recent', lastActive: DateTime(2023, 6, 15), ); - + final lessRecentlyActive = createTestUser( id: 'old', lastActive: DateTime(2023, 6, 10), ); - - final field1 = recentlyActive.getComparableField(UserSortKey.lastActive); - final field2 = lessRecentlyActive.getComparableField(UserSortKey.lastActive); - - expect(field1!.compareTo(field2!), greaterThan(0)); // More recent > Less recent - expect(field2.compareTo(field1), lessThan(0)); // Less recent < More recent + + final field1 = + recentlyActive.getComparableField(UserSortKey.lastActive); + final field2 = + lessRecentlyActive.getComparableField(UserSortKey.lastActive); + + expect(field1!.compareTo(field2!), + greaterThan(0)); // More recent > Less recent + expect( + field2.compareTo(field1), lessThan(0)); // Less recent < More recent }); test('should compare two users correctly using banned status', () { @@ -337,15 +341,15 @@ void main() { id: 'banned', banned: true, ); - + final notBannedUser = createTestUser( id: 'not-banned', banned: false, ); - + final field1 = bannedUser.getComparableField(UserSortKey.banned); final field2 = notBannedUser.getComparableField(UserSortKey.banned); - + expect(field1!.compareTo(field2!), greaterThan(0)); // true > false expect(field2.compareTo(field1), lessThan(0)); // false < true }); @@ -356,7 +360,7 @@ void main() { id: 'without-name', name: null, ); - + final field = user.getComparableField(UserSortKey.name); expect(field, isNotNull); expect(field!.value, equals('without-name')); // Fallback to user id From c0f2daa7a589eec692dc08312ec8d99c52d6a1d5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 9 Apr 2025 00:51:23 +0200 Subject: [PATCH 8/9] test: fix tests --- .../test/src/dao/channel_query_dao_test.dart | 2 +- .../test/stream_chat_persistence_client_test.dart | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index 5adbc99063..6a12371b09 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -87,7 +87,7 @@ void main() { memberCount: index + 3, lastMessageAt: now.add(Duration(hours: index)), ), - ).reversed.toList(growable: false); + ).toList(growable: false); await userDao.updateUsers(users); await channelDao.updateChannels(channels); diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index efb5608c77..3143654e4c 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -248,20 +248,6 @@ void main() { }); group('getChannelState', () { - test('should throw if sort is provided without comparator', () async { - final sort = [ - const SortOption( - 'testField', - direction: SortOption.ASC, - ), - ]; - - expect( - () => client.getChannelStates(channelStateSort: sort), - throwsA(isA()), - ); - }); - test('should work fine', () async { const cid = 'testType:testId'; final channels = List.generate(3, (index) => ChannelModel(cid: cid)); From 8b73b52f0ba181ffc46ad437db71a8f65c6f2dda Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 9 Apr 2025 02:21:46 +0200 Subject: [PATCH 9/9] test: add more tests --- .../src/core/models/banned_user_test.dart | 87 +++++++ .../test/src/core/models/message_test.dart | 151 ++++++++++++ .../test/src/core/models/poll_test.dart | 217 ++++++++++++++++++ .../test/src/core/models/poll_vote_test.dart | 153 ++++++++++++ 4 files changed, 608 insertions(+) create mode 100644 packages/stream_chat/test/src/core/models/banned_user_test.dart create mode 100644 packages/stream_chat/test/src/core/models/poll_vote_test.dart diff --git a/packages/stream_chat/test/src/core/models/banned_user_test.dart b/packages/stream_chat/test/src/core/models/banned_user_test.dart new file mode 100644 index 0000000000..466b8540f0 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/banned_user_test.dart @@ -0,0 +1,87 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:stream_chat/src/core/models/banned_user.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:test/test.dart'; + +void main() { + group('src/models/banned_user', () { + group('ComparableFieldProvider', () { + test('should return ComparableField for banned_user.createdAt', () { + final createdAt = DateTime(2023, 6, 15); + final bannedUser = createTestBannedUser( + userId: 'banned-user-id', + createdAt: createdAt, + ); + + final field = bannedUser.getComparableField( + BannedUserSortKey.createdAt, + ); + + expect(field, isNotNull); + expect(field!.value, equals(createdAt)); + }); + + test('should return null for non-existent field keys', () { + final bannedUser = createTestBannedUser( + userId: 'banned-user-id', + ); + + final field = bannedUser.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two banned users correctly using createdAt', () { + final recentBan = createTestBannedUser( + userId: 'recent-ban', + createdAt: DateTime(2023, 6, 15), + ); + + final olderBan = createTestBannedUser( + userId: 'older-ban', + createdAt: DateTime(2023, 6, 10), + ); + + final field1 = recentBan.getComparableField( + BannedUserSortKey.createdAt, + ); + + final field2 = olderBan.getComparableField( + BannedUserSortKey.createdAt, + ); + + // More recent > Less recent + expect(field1!.compareTo(field2!), greaterThan(0)); + // Less recent < More recent + expect(field2.compareTo(field1), lessThan(0)); + }); + }); + }); +} + +/// Helper function to create a BannedUser for testing +BannedUser createTestBannedUser({ + required String userId, + DateTime? createdAt, + DateTime? expires, + String? reason, + bool shadow = false, + String? channelId, + String? channelType, +}) { + return BannedUser( + user: User(id: userId), + bannedBy: User(id: 'moderator-user'), + channel: channelId != null && channelType != null + ? ChannelModel( + id: channelId, + type: channelType, + ) + : null, + createdAt: createdAt, + expires: expires, + shadow: shadow, + reason: reason, + ); +} diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index f0403f0370..5910f0de5b 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -68,6 +68,134 @@ void main() { jsonFixture('message_to_json.json'), ); }); + + group('ComparableFieldProvider', () { + test('should return ComparableField for message.id', () { + final message = createTestMessage( + id: 'test-message-id', + text: 'Hello world', + ); + + final field = message.getComparableField(MessageSortKey.id); + expect(field, isNotNull); + expect(field!.value, equals('test-message-id')); + }); + + test('should return ComparableField for message.createdAt', () { + final createdAt = DateTime(2023, 6, 15); + final message = createTestMessage( + id: 'test-message-id', + text: 'Hello world', + createdAt: createdAt, + ); + + final field = message.getComparableField(MessageSortKey.createdAt); + expect(field, isNotNull); + expect(field!.value, equals(createdAt)); + }); + + test('should return ComparableField for message.updatedAt', () { + final updatedAt = DateTime(2023, 6, 20); + final message = createTestMessage( + id: 'test-message-id', + text: 'Hello world', + updatedAt: updatedAt, + ); + + final field = message.getComparableField(MessageSortKey.updatedAt); + expect(field, isNotNull); + expect(field!.value, equals(updatedAt)); + }); + + test('should return ComparableField for message.extraData', () { + final message = createTestMessage( + id: 'test-message-id', + text: 'Hello world', + extraData: {'priority': 5}, + ); + + final field = message.getComparableField('priority'); + expect(field, isNotNull); + expect(field!.value, equals(5)); + }); + + test('should return null for non-existent extraData keys', () { + final message = createTestMessage( + id: 'test-message-id', + text: 'Hello world', + ); + + final field = message.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two messages correctly using id', () { + final message1 = createTestMessage( + id: 'message-a', + text: 'Message A', + ); + + final message2 = createTestMessage( + id: 'message-b', + text: 'Message B', + ); + + final field1 = message1.getComparableField(MessageSortKey.id); + final field2 = message2.getComparableField(MessageSortKey.id); + + // message-a < message-b + expect(field1!.compareTo(field2!), lessThan(0)); + // message-b > message-a + expect(field2.compareTo(field1), greaterThan(0)); + }); + + test('should compare two messages correctly using createdAt', () { + final newerMessage = createTestMessage( + id: 'newer', + text: 'Newer Message', + createdAt: DateTime(2023, 6, 15), + ); + + final olderMessage = createTestMessage( + id: 'older', + text: 'Older Message', + createdAt: DateTime(2023, 6, 10), + ); + + final field1 = newerMessage.getComparableField( + MessageSortKey.createdAt, + ); + + final field2 = olderMessage.getComparableField( + MessageSortKey.createdAt, + ); + + // More recent > Less recent + expect(field1!.compareTo(field2!), greaterThan(0)); + // Less recent < More recent + expect(field2.compareTo(field1), lessThan(0)); + }); + + test('should compare two messages correctly using extraData', () { + final highPriorityMessage = createTestMessage( + id: 'high', + text: 'High Priority Message', + extraData: {'priority': 10}, + ); + + final lowPriorityMessage = createTestMessage( + id: 'low', + text: 'Low Priority Message', + extraData: {'priority': 1}, + ); + + final field1 = highPriorityMessage.getComparableField('priority'); + final field2 = lowPriorityMessage.getComparableField('priority'); + + expect(field1!.compareTo(field2!), greaterThan(0)); // 10 > 1 + expect(field2.compareTo(field1), lessThan(0)); // 1 < 10 + }); + }); }); group('MessageVisibility Extension Tests', () { @@ -301,3 +429,26 @@ void main() { }); }); } + +/// Helper function to create a Message for testing +Message createTestMessage({ + String? id, + required String text, + String type = 'regular', + DateTime? createdAt, + DateTime? updatedAt, + User? user, + Map? extraData, + List? restrictedVisibility, +}) { + return Message( + id: id, + text: text, + type: type, + localCreatedAt: createdAt, + localUpdatedAt: updatedAt, + user: user, + extraData: extraData ?? {}, + restrictedVisibility: restrictedVisibility, + ); +} diff --git a/packages/stream_chat/test/src/core/models/poll_test.dart b/packages/stream_chat/test/src/core/models/poll_test.dart index f1e4fead0b..bb3ba1c0a1 100644 --- a/packages/stream_chat/test/src/core/models/poll_test.dart +++ b/packages/stream_chat/test/src/core/models/poll_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'package:stream_chat/src/core/models/poll.dart'; import 'package:stream_chat/src/core/models/poll_option.dart'; import 'package:test/test.dart'; @@ -70,5 +72,220 @@ void main() { expect(json['allow_answers'], false); expect(json['is_closed'], false); }); + + group('ComparableFieldProvider', () { + test('should return ComparableField for poll.id', () { + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + ); + + final field = poll.getComparableField(PollSortKey.id); + expect(field, isNotNull); + expect(field!.value, equals('test-poll-id')); + }); + + test('should return ComparableField for poll.name', () { + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + ); + + final field = poll.getComparableField(PollSortKey.name); + expect(field, isNotNull); + expect(field!.value, equals('Test Poll')); + }); + + test('should return ComparableField for poll.createdAt', () { + final createdAt = DateTime(2023, 6, 15); + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + createdAt: createdAt, + ); + + final field = poll.getComparableField(PollSortKey.createdAt); + expect(field, isNotNull); + expect(field!.value, equals(createdAt)); + }); + + test('should return ComparableField for poll.updatedAt', () { + final updatedAt = DateTime(2023, 6, 20); + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + updatedAt: updatedAt, + ); + + final field = poll.getComparableField(PollSortKey.updatedAt); + expect(field, isNotNull); + expect(field!.value, equals(updatedAt)); + }); + + test('should return ComparableField for poll.isClosed', () { + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + isClosed: true, + ); + + final field = poll.getComparableField(PollSortKey.isClosed); + expect(field, isNotNull); + expect(field!.value, isTrue); + }); + + test('should return ComparableField for poll.extraData', () { + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + extraData: {'priority': 5}, + ); + + final field = poll.getComparableField('priority'); + expect(field, isNotNull); + expect(field!.value, equals(5)); + }); + + test('should return null for non-existent extraData keys', () { + final poll = createTestPoll( + id: 'test-poll-id', + name: 'Test Poll', + ); + + final field = poll.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two polls correctly using id', () { + final poll1 = createTestPoll( + id: 'poll-a', + name: 'Poll A', + ); + + final poll2 = createTestPoll( + id: 'poll-b', + name: 'Poll B', + ); + + final field1 = poll1.getComparableField(PollSortKey.id); + final field2 = poll2.getComparableField(PollSortKey.id); + + expect(field1!.compareTo(field2!), lessThan(0)); // poll-a < poll-b + expect(field2.compareTo(field1), greaterThan(0)); // poll-b > poll-a + }); + + test('should compare two polls correctly using name', () { + final poll1 = createTestPoll( + id: 'test-poll-1', + name: 'Apple Poll', + ); + + final poll2 = createTestPoll( + id: 'test-poll-2', + name: 'Banana Poll', + ); + + final field1 = poll1.getComparableField(PollSortKey.name); + final field2 = poll2.getComparableField(PollSortKey.name); + + expect(field1!.compareTo(field2!), lessThan(0)); // Apple < Banana + expect(field2.compareTo(field1), greaterThan(0)); // Banana > Apple + }); + + test('should compare two polls correctly using createdAt', () { + final newerPoll = createTestPoll( + id: 'newer', + name: 'New Poll', + createdAt: DateTime(2023, 6, 15), + ); + + final olderPoll = createTestPoll( + id: 'older', + name: 'Old Poll', + createdAt: DateTime(2023, 6, 10), + ); + + final field1 = newerPoll.getComparableField(PollSortKey.createdAt); + final field2 = olderPoll.getComparableField(PollSortKey.createdAt); + + // More recent > Less recent + expect(field1!.compareTo(field2!), greaterThan(0)); + // Less recent < More recent + expect(field2.compareTo(field1), lessThan(0)); + }); + + test('should compare two polls correctly using isClosed', () { + final closedPoll = createTestPoll( + id: 'closed', + name: 'Closed Poll', + isClosed: true, + ); + + final openPoll = createTestPoll( + id: 'open', + name: 'Open Poll', + isClosed: false, + ); + + final field1 = closedPoll.getComparableField(PollSortKey.isClosed); + final field2 = openPoll.getComparableField(PollSortKey.isClosed); + + expect(field1!.compareTo(field2!), greaterThan(0)); // true > false + expect(field2.compareTo(field1), lessThan(0)); // false < true + }); + + test('should compare two polls correctly using extraData', () { + final highPriorityPoll = createTestPoll( + id: 'high', + name: 'High Priority Poll', + extraData: {'priority': 10}, + ); + + final lowPriorityPoll = createTestPoll( + id: 'low', + name: 'Low Priority Poll', + extraData: {'priority': 1}, + ); + + final field1 = highPriorityPoll.getComparableField('priority'); + final field2 = lowPriorityPoll.getComparableField('priority'); + + expect(field1!.compareTo(field2!), greaterThan(0)); // 10 > 1 + expect(field2.compareTo(field1), lessThan(0)); // 1 < 10 + }); + }); }); } + +/// Helper function to create a Poll for testing +Poll createTestPoll({ + String? id, + required String name, + String? description, + List? options, + VotingVisibility votingVisibility = VotingVisibility.public, + bool enforceUniqueVote = true, + int? maxVotesAllowed, + bool allowUserSuggestedOptions = false, + bool allowAnswers = false, + bool isClosed = false, + DateTime? createdAt, + DateTime? updatedAt, + Map? extraData, +}) { + return Poll( + id: id, + name: name, + description: description, + options: options ?? [const PollOption(text: 'Option 1')], + votingVisibility: votingVisibility, + enforceUniqueVote: enforceUniqueVote, + maxVotesAllowed: maxVotesAllowed, + allowUserSuggestedOptions: allowUserSuggestedOptions, + allowAnswers: allowAnswers, + isClosed: isClosed, + createdAt: createdAt ?? DateTime(2023), + updatedAt: updatedAt ?? DateTime(2023), + extraData: extraData ?? {}, + ); +} diff --git a/packages/stream_chat/test/src/core/models/poll_vote_test.dart b/packages/stream_chat/test/src/core/models/poll_vote_test.dart new file mode 100644 index 0000000000..20fc18c5e9 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/poll_vote_test.dart @@ -0,0 +1,153 @@ +import 'package:stream_chat/src/core/models/poll_vote.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:test/test.dart'; + +void main() { + group('src/models/poll_vote', () { + group('ComparableFieldProvider', () { + test('should return ComparableField for poll_vote.id', () { + final pollVote = createTestPollVote( + id: 'test-vote-id', + optionId: 'option-1', + ); + + final field = pollVote.getComparableField(PollVoteSortKey.id); + expect(field, isNotNull); + expect(field!.value, equals('test-vote-id')); + }); + + test('should return ComparableField for poll_vote.createdAt', () { + final createdAt = DateTime(2023, 6, 15); + final pollVote = createTestPollVote( + id: 'test-vote-id', + optionId: 'option-1', + createdAt: createdAt, + ); + + final field = pollVote.getComparableField(PollVoteSortKey.createdAt); + expect(field, isNotNull); + expect(field!.value, equals(createdAt)); + }); + + test('should return ComparableField for poll_vote.updatedAt', () { + final updatedAt = DateTime(2023, 6, 20); + final pollVote = createTestPollVote( + id: 'test-vote-id', + optionId: 'option-1', + updatedAt: updatedAt, + ); + + final field = pollVote.getComparableField(PollVoteSortKey.updatedAt); + expect(field, isNotNull); + expect(field!.value, equals(updatedAt)); + }); + + test('should return ComparableField for poll_vote.answerText', () { + final pollVote = createTestPollVote( + id: 'test-vote-id', + answerText: 'This is my answer', + ); + + final field = pollVote.getComparableField(PollVoteSortKey.answerText); + expect(field, isNotNull); + expect(field!.value, equals('This is my answer')); + }); + + test('should return null for non-existent field keys', () { + final pollVote = createTestPollVote( + id: 'test-vote-id', + optionId: 'option-1', + ); + + final field = pollVote.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two poll votes correctly using createdAt', () { + final recentVote = createTestPollVote( + id: 'recent', + optionId: 'option-1', + createdAt: DateTime(2023, 6, 15), + ); + + final olderVote = createTestPollVote( + id: 'older', + optionId: 'option-1', + createdAt: DateTime(2023, 6, 10), + ); + + final field1 = recentVote.getComparableField(PollVoteSortKey.createdAt); + final field2 = olderVote.getComparableField(PollVoteSortKey.createdAt); + + // More recent > Less recent + expect(field1!.compareTo(field2!), greaterThan(0)); + // Less recent < More recent + expect(field2.compareTo(field1), lessThan(0)); + }); + + test('should compare two poll votes correctly using id', () { + final vote1 = createTestPollVote( + id: 'abc', + optionId: 'option-1', + ); + + final vote2 = createTestPollVote( + id: 'xyz', + optionId: 'option-1', + ); + + final field1 = vote1.getComparableField(PollVoteSortKey.id); + final field2 = vote2.getComparableField(PollVoteSortKey.id); + + expect(field1!.compareTo(field2!), lessThan(0)); // abc < xyz + expect(field2.compareTo(field1), greaterThan(0)); // xyz > abc + }); + + test('should compare two poll votes correctly using answerText', () { + final vote1 = createTestPollVote( + id: 'answer1', + answerText: 'Answer A', + ); + + final vote2 = createTestPollVote( + id: 'answer2', + answerText: 'Answer B', + ); + + final field1 = vote1.getComparableField(PollVoteSortKey.answerText); + final field2 = vote2.getComparableField(PollVoteSortKey.answerText); + + expect(field1!.compareTo(field2!), lessThan(0)); // Answer A < Answer B + expect(field2.compareTo(field1), greaterThan(0)); // Answer B > Answer A + }); + }); + }); +} + +/// Helper function to create a PollVote for testing +PollVote createTestPollVote({ + String? id, + String? pollId, + String? optionId, + String? answerText, + DateTime? createdAt, + DateTime? updatedAt, + String? userId, + User? user, +}) { + assert( + optionId != null || answerText != null, + 'Either optionId or answerText must be provided', + ); + + return PollVote( + id: id, + pollId: pollId, + optionId: optionId, + answerText: answerText, + createdAt: createdAt ?? DateTime(2023), + updatedAt: updatedAt ?? DateTime(2023), + userId: userId, + user: user, + ); +}