diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 751379555..06226ee44 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -8,6 +8,11 @@ an `User` instance. - Fixed `Client.currentUser` specific fields getting reset on `user.updated` events. +✅ Added + +- Added support for `Client.setPushPreferences` which allows setting PushPreferences for the + current user or for a specific channel. + ## 9.15.0 ✅ Added diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 6959d7181..184923355 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -3320,6 +3320,7 @@ class ChannelClientState { read: newReads, draft: updatedState.draft, pinnedMessages: updatedState.pinnedMessages, + pushPreferences: updatedState.pushPreferences, ); } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 073c36c84..7897b83ec 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -31,6 +31,7 @@ import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/src/core/models/poll.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/push_preference.dart'; import 'package:stream_chat/src/core/models/thread.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/util/utils.dart'; @@ -989,6 +990,54 @@ class StreamChatClient { Future removeDevice(String id) => _chatApi.device.removeDevice(id); + /// Set push preferences for the current user. + /// + /// This method allows you to configure push notification settings + /// at both global and channel-specific levels. + /// + /// [preferences] - List of push preferences to apply + /// + /// Returns [UpsertPushPreferencesResponse] with the updated preferences. + /// + /// Example: + /// ```dart + /// // Set global push preferences + /// await client.setPushPreferences([ + /// const PushPreferenceInput( + /// chatLevel: ChatLevelPushPreference.mentions, + /// callLevel: CallLevelPushPreference.all, + /// ), + /// ]); + /// + /// // Set channel-specific preferences + /// await client.setPushPreferences([ + /// const PushPreferenceInput.channel( + /// channelCid: 'messaging:general', + /// chatLevel: ChatLevelPushPreference.none, + /// ), + /// const PushPreferenceInput.channel( + /// channelCid: 'messaging:support', + /// chatLevel: ChatLevelPushPreference.mentions, + /// ), + /// ]); + /// + /// // Mix global and channel-specific preferences + /// await client.setPushPreferences([ + /// const PushPreferenceInput( + /// chatLevel: ChatLevelPushPreference.all, + /// ), // Global default + /// const PushPreferenceInput.channel( + /// channelCid: 'messaging:spam', + /// chatLevel: ChatLevelPushPreference.none, + /// ), + /// ]); + /// ``` + Future setPushPreferences( + List preferences, + ) { + return _chatApi.device.setPushPreferences(preferences); + } + /// Get a development token Token devToken(String userId) => Token.development(userId); @@ -2139,6 +2188,7 @@ class ClientState { unreadChannels: currentUser?.unreadChannels, unreadThreads: currentUser?.unreadThreads, blockedUserIds: currentUser?.blockedUserIds, + pushPreferences: currentUser?.pushPreferences, ); } diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart index 25a678ddb..b450ddc18 100644 --- a/packages/stream_chat/lib/src/core/api/device_api.dart +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/push_preference.dart'; /// Provider used to send push notifications. enum PushProvider { @@ -57,4 +60,34 @@ class DeviceApi { ); return EmptyResponse.fromJson(response.data); } + + /// Set push preferences for the current user. + /// + /// This method allows you to configure push notification settings + /// at both global and channel-specific levels. + /// + /// [preferences] - List of [PushPreferenceInput] to apply. Use the default + /// constructor for user-level preferences or [PushPreferenceInput.channel] + /// for channel-specific preferences. + /// + /// Returns [UpsertPushPreferencesResponse] with the updated preferences. + /// + /// Throws [ArgumentError] if preferences list is empty. + Future setPushPreferences( + List preferences, + ) async { + if (preferences.isEmpty) { + throw ArgumentError.value( + preferences, + 'preferences', + 'Cannot be empty. At least one preference must be provided.', + ); + } + + final response = await _client.post( + '/push_preferences', + data: jsonEncode({'preferences': preferences}), + ); + return UpsertPushPreferencesResponse.fromJson(response.data); + } } diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index dbd8ed3bd..cbd450567 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -15,6 +15,7 @@ import 'package:stream_chat/src/core/models/message_reminder.dart'; import 'package:stream_chat/src/core/models/poll.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/push_preference.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/thread.dart'; @@ -829,3 +830,19 @@ class GetUnreadCountResponse extends _BaseResponse { static GetUnreadCountResponse fromJson(Map json) => _$GetUnreadCountResponseFromJson(json); } + +/// Model response for [StreamChatClient.setPushPreferences] api call +@JsonSerializable(createToJson: false) +class UpsertPushPreferencesResponse extends _BaseResponse { + /// Mapping of user IDs to their push preferences + @JsonKey(defaultValue: {}) + late Map userPreferences; + + /// Mapping of user IDs to their channel-specific push preferences + @JsonKey(defaultValue: {}) + late Map> userChannelPreferences; + + /// Create a new instance from a json + static UpsertPushPreferencesResponse fromJson(Map json) => + _$UpsertPushPreferencesResponseFromJson(json); +} diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 3583ffdf3..30dd44596 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -489,3 +489,26 @@ GetUnreadCountResponse _$GetUnreadCountResponseFromJson( ..threads = (json['threads'] as List) .map((e) => UnreadCountsThread.fromJson(e as Map)) .toList(); + +UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson( + Map json) => + UpsertPushPreferencesResponse() + ..duration = json['duration'] as String? + ..userPreferences = (json['user_preferences'] as Map?) + ?.map( + (k, e) => + MapEntry(k, PushPreference.fromJson(e as Map)), + ) ?? + {} + ..userChannelPreferences = + (json['user_channel_preferences'] as Map?)?.map( + (k, e) => MapEntry( + k, + (e as Map).map( + (k, e) => MapEntry( + k, + ChannelPushPreference.fromJson( + e as Map)), + )), + ) ?? + {}; 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 ae7b9062b..601ef679a 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -4,6 +4,7 @@ import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/draft.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/push_preference.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/user.dart'; @@ -19,7 +20,7 @@ const _nullConst = _NullConst(); @JsonSerializable() class ChannelState implements ComparableFieldProvider { /// Constructor used for json serialization - ChannelState({ + const ChannelState({ this.channel, this.messages, this.members, @@ -29,6 +30,7 @@ class ChannelState implements ComparableFieldProvider { this.read, this.membership, this.draft, + this.pushPreferences, }); /// The channel to which this state belongs @@ -58,6 +60,9 @@ class ChannelState implements ComparableFieldProvider { /// The draft message for this channel if it exists. final Draft? draft; + /// The push preferences for this channel if it exists. + final ChannelPushPreference? pushPreferences; + /// Create a new instance from a json static ChannelState fromJson(Map json) => _$ChannelStateFromJson(json); @@ -76,6 +81,7 @@ class ChannelState implements ComparableFieldProvider { List? read, Member? membership, Object? draft = _nullConst, + ChannelPushPreference? pushPreferences, }) => ChannelState( channel: channel ?? this.channel, @@ -87,6 +93,7 @@ class ChannelState implements ComparableFieldProvider { read: read ?? this.read, membership: membership ?? this.membership, draft: draft == _nullConst ? this.draft : draft as Draft?, + pushPreferences: pushPreferences ?? this.pushPreferences, ); @override diff --git a/packages/stream_chat/lib/src/core/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart index 88bd81e77..e154f1235 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.g.dart @@ -32,6 +32,10 @@ ChannelState _$ChannelStateFromJson(Map json) => ChannelState( draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), + pushPreferences: json['push_preferences'] == null + ? null + : ChannelPushPreference.fromJson( + json['push_preferences'] as Map), ); Map _$ChannelStateToJson(ChannelState instance) => @@ -46,4 +50,5 @@ Map _$ChannelStateToJson(ChannelState instance) => 'read': instance.read?.map((e) => e.toJson()).toList(), 'membership': instance.membership?.toJson(), 'draft': instance.draft?.toJson(), + 'push_preferences': instance.pushPreferences?.toJson(), }; diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 404e15634..6e1840991 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -19,6 +19,7 @@ class OwnUser extends User { this.channelMutes = const [], this.unreadThreads = 0, this.blockedUserIds = const [], + this.pushPreferences, required super.id, super.role, super.name, @@ -68,6 +69,7 @@ class OwnUser extends User { unreadChannels: user.extraData['unread_channels'].safeCast(), unreadThreads: user.extraData['unread_threads'].safeCast(), blockedUserIds: user.extraData['blocked_user_ids'].safeCast(), + pushPreferences: user.extraData['push_preferences'].safeCast(), ); // Once we are done working with the extraData, we have to clean it up @@ -112,6 +114,7 @@ class OwnUser extends User { String? language, Map? teamsRole, int? avgResponseTime, + PushPreference? pushPreferences, }) => OwnUser( id: id ?? this.id, @@ -139,6 +142,7 @@ class OwnUser extends User { language: language ?? this.language, teamsRole: teamsRole ?? this.teamsRole, avgResponseTime: avgResponseTime ?? this.avgResponseTime, + pushPreferences: pushPreferences ?? this.pushPreferences, ); /// Returns a new [OwnUser] that is a combination of this ownUser @@ -168,6 +172,7 @@ class OwnUser extends User { language: other.language, teamsRole: other.teamsRole, avgResponseTime: other.avgResponseTime, + pushPreferences: other.pushPreferences, ); } @@ -199,6 +204,10 @@ class OwnUser extends User { @JsonKey(includeIfNull: false) final List blockedUserIds; + /// Push preferences for the user if set. + @JsonKey(includeIfNull: false) + final PushPreference? pushPreferences; + /// Known top level fields. /// /// Useful for [Serializer] methods. @@ -210,6 +219,7 @@ class OwnUser extends User { 'channel_mutes', 'unread_threads', 'blocked_user_ids', + 'push_preferences', ...User.topLevelFields, ]; } diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index df54bfa1d..e6c30b8bd 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -26,6 +26,10 @@ OwnUser _$OwnUserFromJson(Map json) => OwnUser( ?.map((e) => e as String) .toList() ?? const [], + pushPreferences: json['push_preferences'] == null + ? null + : PushPreference.fromJson( + json['push_preferences'] as Map), id: json['id'] as String, role: json['role'] as String?, createdAt: json['created_at'] == null diff --git a/packages/stream_chat/lib/src/core/models/push_preference.dart b/packages/stream_chat/lib/src/core/models/push_preference.dart new file mode 100644 index 000000000..00ba266a1 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/push_preference.dart @@ -0,0 +1,128 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'push_preference.g.dart'; + +/// Chat level push preference type +extension type const ChatLevel(String rawType) implements String { + /// All messages + static const all = ChatLevel('all'); + + /// No messages + static const none = ChatLevel('none'); + + /// Only mentions + static const mentions = ChatLevel('mentions'); + + /// Use default system setting + static const defaultValue = ChatLevel('default'); +} + +/// Call level push preference type +extension type const CallLevel(String rawType) implements String { + /// All calls + static const all = CallLevel('all'); + + /// No calls + static const none = CallLevel('none'); + + /// Use default system setting + static const defaultValue = CallLevel('default'); +} + +/// Input for push preferences, used for creating or updating preferences +/// for a user or a specific channel. +@JsonSerializable(createFactory: false, includeIfNull: false) +class PushPreferenceInput { + /// Creates a new push preference input + const PushPreferenceInput({ + this.callLevel, + this.chatLevel, + this.disabledUntil, + this.removeDisable, + }) : channelCid = null; + + /// Creates a new push preference input for a specific channel + /// with the given [channelCid]. + const PushPreferenceInput.channel({ + required String this.channelCid, + this.callLevel, + this.chatLevel, + this.disabledUntil, + this.removeDisable, + }); + + /// If not null, creates a push preference for a specific channel + final String? channelCid; + + /// Push preference for calls + final CallLevel? callLevel; + + /// Push preference for chat messages + final ChatLevel? chatLevel; + + /// Disabled until this date (snooze functionality) + final DateTime? disabledUntil; + + /// Temporary flag for resetting disabledUntil + final bool? removeDisable; + + /// Serialize model to json + Map toJson() => _$PushPreferenceInputToJson(this); +} + +/// The class that contains push preferences for a user +@JsonSerializable(includeIfNull: false) +class PushPreference extends Equatable { + /// Creates a new push preference instance + const PushPreference({ + this.callLevel, + this.chatLevel, + this.disabledUntil, + }); + + /// Create a new instance from a json + factory PushPreference.fromJson(Map json) => + _$PushPreferenceFromJson(json); + + /// Push preference for calls + final CallLevel? callLevel; + + /// Push preference for chat messages + final ChatLevel? chatLevel; + + /// Disabled until this date (snooze functionality) + final DateTime? disabledUntil; + + /// Serialize model to json + Map toJson() => _$PushPreferenceToJson(this); + + @override + List get props => [callLevel, chatLevel, disabledUntil]; +} + +/// The class that contains push preferences for a specific channel +@JsonSerializable(includeIfNull: false) +class ChannelPushPreference extends Equatable { + /// Creates a new channel push preference instance + const ChannelPushPreference({ + this.chatLevel, + this.disabledUntil, + }); + + /// Create a new instance from a json + factory ChannelPushPreference.fromJson(Map json) => + _$ChannelPushPreferenceFromJson(json); + + /// Push preference for chat messages + final ChatLevel? chatLevel; + + /// Disabled until this date (snooze functionality) + final DateTime? disabledUntil; + + /// Serialize model to json + Map toJson() => _$ChannelPushPreferenceToJson(this); + + @override + List get props => [chatLevel, disabledUntil]; +} diff --git a/packages/stream_chat/lib/src/core/models/push_preference.g.dart b/packages/stream_chat/lib/src/core/models/push_preference.g.dart new file mode 100644 index 000000000..971c0bd1f --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/push_preference.g.dart @@ -0,0 +1,52 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'push_preference.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Map _$PushPreferenceInputToJson( + PushPreferenceInput instance) => + { + if (instance.channelCid case final value?) 'channel_cid': value, + if (instance.callLevel case final value?) 'call_level': value, + if (instance.chatLevel case final value?) 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) + 'disabled_until': value, + if (instance.removeDisable case final value?) 'remove_disable': value, + }; + +PushPreference _$PushPreferenceFromJson(Map json) => + PushPreference( + callLevel: json['call_level'] as CallLevel?, + chatLevel: json['chat_level'] as ChatLevel?, + disabledUntil: json['disabled_until'] == null + ? null + : DateTime.parse(json['disabled_until'] as String), + ); + +Map _$PushPreferenceToJson(PushPreference instance) => + { + if (instance.callLevel case final value?) 'call_level': value, + if (instance.chatLevel case final value?) 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) + 'disabled_until': value, + }; + +ChannelPushPreference _$ChannelPushPreferenceFromJson( + Map json) => + ChannelPushPreference( + chatLevel: json['chat_level'] as ChatLevel?, + disabledUntil: json['disabled_until'] == null + ? null + : DateTime.parse(json['disabled_until'] as String), + ); + +Map _$ChannelPushPreferenceToJson( + ChannelPushPreference instance) => + { + if (instance.chatLevel case final value?) 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) + 'disabled_until': value, + }; diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index f296711b7..9e7e2372e 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -54,6 +54,7 @@ export 'src/core/models/poll.dart'; export 'src/core/models/poll_option.dart'; export 'src/core/models/poll_vote.dart'; export 'src/core/models/poll_voting_mode.dart'; +export 'src/core/models/push_preference.dart'; export 'src/core/models/reaction.dart'; export 'src/core/models/reaction_group.dart'; export 'src/core/models/read.dart'; diff --git a/packages/stream_chat/test/fixtures/channel_state_to_json.json b/packages/stream_chat/test/fixtures/channel_state_to_json.json index 9eec788ae..95f5d5693 100644 --- a/packages/stream_chat/test/fixtures/channel_state_to_json.json +++ b/packages/stream_chat/test/fixtures/channel_state_to_json.json @@ -12,6 +12,10 @@ "read": [], "membership": null, "draft": null, + "push_preferences": { + "chat_level": "all", + "disabled_until": "2020-01-30T13:43:41.062362Z" + }, "messages": [ { "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", diff --git a/packages/stream_chat/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart index 4e04da6b1..c9d06a843 100644 --- a/packages/stream_chat/test/src/core/api/device_api_test.dart +++ b/packages/stream_chat/test/src/core/api/device_api_test.dart @@ -132,4 +132,80 @@ void main() { ).called(1); verifyNoMoreInteractions(client); }); + + test('setPushPreferences', () async { + const path = '/push_preferences'; + + const preferences = [ + PushPreferenceInput(chatLevel: ChatLevel.mentions), + ]; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: { + 'user_preferences': {}, + 'user_channel_preferences': {}, + }), + ); + + final res = await deviceApi.setPushPreferences(preferences); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('setPushPreferences throws on empty list', () async { + expect( + () => deviceApi.setPushPreferences([]), + throwsA(isA()), + ); + }); + + test('setPushPreferences allows removeDisable as only field', () async { + const path = '/push_preferences'; + + const preferences = [ + PushPreferenceInput(removeDisable: true), + ]; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: { + 'user_preferences': {}, + 'user_channel_preferences': {}, + }), + ); + + final res = await deviceApi.setPushPreferences(preferences); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('setPushPreferences with channel-specific preference', () async { + const path = '/push_preferences'; + + const preferences = [ + PushPreferenceInput.channel( + channelCid: 'messaging:general', + chatLevel: ChatLevel.none, + ), + ]; + + when(() => client.post(path, data: any(named: 'data'))).thenAnswer( + (_) async => successResponse(path, data: { + 'user_preferences': {}, + 'user_channel_preferences': {}, + }), + ); + + final res = await deviceApi.setPushPreferences(preferences); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); } diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index b1e1e9efc..4f9d45c74 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -4445,5 +4445,79 @@ void main() { final response = BlockedUsersResponse.fromJson(json.decode(jsonExample)); expect(response.blocks, isA>()); }); + + test('UpsertPushPreferencesResponse', () { + const jsonExample = ''' + { + "user_preferences": { + "user1": { + "chat_level": "mentions", + "call_level": "all", + "disabled_until": "2024-12-31T23:59:59Z" + }, + "user2": { + "chat_level": "none" + } + }, + "user_channel_preferences": { + "user1": { + "channel1": { + "chat_level": "all", + "disabled_until": "2024-12-31T23:59:59Z" + }, + "channel2": { + "chat_level": "none" + } + }, + "user2": { + "channel3": { + "chat_level": "mentions" + } + } + } + } + '''; + final response = UpsertPushPreferencesResponse.fromJson( + json.decode(jsonExample), + ); + + expect(response.userPreferences, isA>()); + expect(response.userPreferences, hasLength(2)); + + // Test user1 preferences + final user1Prefs = response.userPreferences['user1']!; + expect(user1Prefs.chatLevel, ChatLevel.mentions); + expect(user1Prefs.callLevel, CallLevel.all); + expect(user1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); + + // Test user2 preferences + final user2Prefs = response.userPreferences['user2']!; + expect(user2Prefs.chatLevel, ChatLevel.none); + + expect( + response.userChannelPreferences, + isA>>(), + ); + expect(response.userChannelPreferences, hasLength(2)); + + // Test user1 channel preferences + final user1ChannelPrefs = response.userChannelPreferences['user1']!; + expect(user1ChannelPrefs, hasLength(2)); + + final channel1Prefs = user1ChannelPrefs['channel1']!; + expect(channel1Prefs.chatLevel, ChatLevel.all); + expect( + channel1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); + + final channel2Prefs = user1ChannelPrefs['channel2']!; + expect(channel2Prefs.chatLevel, ChatLevel.none); + + // Test user2 channel preferences + final user2ChannelPrefs = response.userChannelPreferences['user2']!; + expect(user2ChannelPrefs, hasLength(1)); + + final channel3Prefs = user2ChannelPrefs['channel3']!; + expect(channel3Prefs.chatLevel, ChatLevel.mentions); + }); }); } 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 6a436f1dc..e822e0ed1 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 @@ -59,6 +59,10 @@ void main() { watcherCount: 5, pinnedMessages: [], watchers: [], + pushPreferences: ChannelPushPreference( + chatLevel: ChatLevel.all, + disabledUntil: DateTime.parse('2020-01-30T13:43:41.062362Z'), + ), ); expect( diff --git a/packages/stream_chat/test/src/core/models/push_preference_test.dart b/packages/stream_chat/test/src/core/models/push_preference_test.dart new file mode 100644 index 000000000..1faee8c29 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/push_preference_test.dart @@ -0,0 +1,103 @@ +import 'package:stream_chat/src/core/models/push_preference.dart'; +import 'package:test/test.dart'; + +void main() { + group('src/models/push_preference_input', () { + test('should serialize to json correctly for user-level preferences', () { + final input = PushPreferenceInput( + callLevel: CallLevel.all, + chatLevel: ChatLevel.mentions, + disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), + removeDisable: true, + ); + + final json = input.toJson(); + expect(json['call_level'], 'all'); + expect(json['chat_level'], 'mentions'); + expect(json['disabled_until'], '2024-12-31T23:59:59.000Z'); + expect(json['remove_disable'], true); + expect(json['channel_cid'], isNull); + }); + + test('should serialize to json correctly for channel preferences', () { + final input = PushPreferenceInput.channel( + channelCid: 'messaging:general', + chatLevel: ChatLevel.none, + disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), + ); + + final json = input.toJson(); + expect(json['channel_cid'], 'messaging:general'); + expect(json['chat_level'], 'none'); + expect(json['disabled_until'], '2024-12-31T23:59:59.000Z'); + expect(json['call_level'], isNull); + expect(json['remove_disable'], isNull); + }); + + test('should include default enum value', () { + const input = PushPreferenceInput( + chatLevel: ChatLevel.defaultValue, + callLevel: CallLevel.defaultValue, + ); + + final json = input.toJson(); + expect(json['chat_level'], 'default'); + expect(json['call_level'], 'default'); + }); + }); + + group('src/models/push_preference', () { + test('should parse json correctly', () { + final pushPreference = PushPreference.fromJson(const { + 'call_level': 'all', + 'chat_level': 'mentions', + 'disabled_until': '2024-12-31T23:59:59Z', + }); + + expect(pushPreference.callLevel, CallLevel.all); + expect(pushPreference.chatLevel, ChatLevel.mentions); + expect( + pushPreference.disabledUntil, + DateTime.parse('2024-12-31T23:59:59Z'), + ); + }); + + test('should parse default enum values', () { + final pushPreference = PushPreference.fromJson(const { + 'call_level': 'default', + 'chat_level': 'default', + }); + + expect(pushPreference.callLevel, CallLevel.defaultValue); + expect(pushPreference.chatLevel, ChatLevel.defaultValue); + }); + }); + + group('src/models/channel_push_preference', () { + test('should parse json correctly', () { + final channelPushPreference = ChannelPushPreference.fromJson(const { + 'chat_level': 'none', + 'disabled_until': '2024-12-31T23:59:59Z', + }); + + expect(channelPushPreference.chatLevel, ChatLevel.none); + expect( + channelPushPreference.disabledUntil, + DateTime.parse('2024-12-31T23:59:59Z'), + ); + }); + + test('should create correctly', () { + final channelPushPreference = ChannelPushPreference( + chatLevel: ChatLevel.all, + disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), + ); + + expect(channelPushPreference.chatLevel, ChatLevel.all); + expect( + channelPushPreference.disabledUntil, + DateTime.parse('2024-12-31T23:59:59Z'), + ); + }); + }); +} 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 a3036a2fd..3ebdfc32e 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 @@ -251,7 +251,7 @@ void main() { }); test('updateChannelState', () async { - final channelState = ChannelState(); + const channelState = ChannelState(); persistenceClient.updateChannelState(channelState); }); diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 99dc13826..5418d2930 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -53,7 +53,7 @@ class MockChannel extends Mock implements Channel { PaginationParams? membersPagination, PaginationParams? watchersPagination, }) { - return Future.value(ChannelState()); + return Future.value(const ChannelState()); } @override diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index c3e9e7799..d49a31e8f 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -310,7 +310,7 @@ void main() { messagesPagination: any(named: 'messagesPagination'), preferOffline: any(named: 'preferOffline'), watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer((_) async => ChannelState()); + )).thenAnswer((_) async => const ChannelState()); const messages = []; when(() => mockChannel.state.messagesStream) diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index a8d9c8c0f..7dfcb93c0 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -184,7 +184,7 @@ void main() { final nonInitializedMockChannel = NonInitializedMockChannel(); when(() => nonInitializedMockChannel.cid).thenReturn('test:channel'); when(nonInitializedMockChannel.watch).thenAnswer( - (_) async => ChannelState(), + (_) async => const ChannelState(), ); // A simple widget that provides StreamChannel @@ -221,7 +221,7 @@ void main() { final nonInitializedMockChannel = NonInitializedMockChannel(); when(() => nonInitializedMockChannel.cid).thenReturn('test:channel'); when(nonInitializedMockChannel.watch).thenAnswer( - (_) async => ChannelState(), + (_) async => const ChannelState(), ); // A simple widget that provides StreamChannel @@ -362,7 +362,7 @@ void main() { messagesPagination: any(named: 'messagesPagination'), preferOffline: any(named: 'preferOffline'), ), - ).thenAnswer((_) async => ChannelState()); + ).thenAnswer((_) async => const ChannelState()); // Build a widget with initialMessageId final testWidget = MaterialApp( @@ -411,7 +411,7 @@ void main() { // Second channel final mockChannel2 = NonInitializedMockChannel(); when(() => mockChannel2.cid).thenReturn('test:channel2'); - when(mockChannel2.watch).thenAnswer((_) async => ChannelState()); + when(mockChannel2.watch).thenAnswer((_) async => const ChannelState()); // Update widget with second channel await tester.pumpWidget( @@ -448,7 +448,7 @@ void main() { messagesPagination: any(named: 'messagesPagination'), preferOffline: any(named: 'preferOffline'), ), - ).thenAnswer((_) async => ChannelState()); + ).thenAnswer((_) async => const ChannelState()); // First initial message ID const initialMessageId1 = 'test-message-id-1'; @@ -510,7 +510,7 @@ void main() { preferOffline: any(named: 'preferOffline'), messagesPagination: any(named: 'messagesPagination'), ), - ).thenAnswer((_) async => ChannelState()); + ).thenAnswer((_) async => const ChannelState()); }); tearDown(() => reset(mockChannel));