From b3cb7c379e1edf783a5356d47fbcae2e23aef6cb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 9 Aug 2025 01:11:49 +0200 Subject: [PATCH 1/7] feat(llc): add support for push preferences This commit introduces the ability to manage push notification preferences for users and channels. **New Models:** - `PushPreferenceInput`: Used for creating or updating preferences. - `PushPreference`: Represents user-level push preferences. - `ChannelPushPreference`: Represents channel-specific push preferences. - `ChatLevelPushPreference` enum: Defines chat notification levels (all, none, mentions, default). - `CallLevelPushPreference` enum: Defines call notification levels (all, none, default). **API Changes:** - `DeviceApi.setPushPreferences(List preferences)`: New method to set user or channel-specific push preferences. - `UpsertPushPreferencesResponse`: New response model for `setPushPreferences`. **Model Updates:** - `OwnUser`: Added `pushPreferences` field. - `ChannelState`: Added `pushPreferences` field. **Client Method:** - `StreamChatClient.setPushPreferences(List preferences)`: New client method to interact with the `setPushPreferences` API. --- .../stream_chat/lib/src/client/channel.dart | 1 + .../stream_chat/lib/src/client/client.dart | 49 +++++++ .../lib/src/core/api/device_api.dart | 33 +++++ .../lib/src/core/api/responses.dart | 15 ++ .../lib/src/core/api/responses.g.dart | 19 +++ .../lib/src/core/models/channel_state.dart | 9 +- .../lib/src/core/models/channel_state.g.dart | 5 + .../lib/src/core/models/own_user.dart | 9 ++ .../lib/src/core/models/own_user.g.dart | 4 + .../lib/src/core/models/push_preference.dart | 133 ++++++++++++++++++ .../src/core/models/push_preference.g.dart | 78 ++++++++++ packages/stream_chat/lib/stream_chat.dart | 1 + .../test/src/core/api/device_api_test.dart | 76 ++++++++++ .../test/src/core/api/responses_test.dart | 74 ++++++++++ .../src/core/models/push_preference_test.dart | 103 ++++++++++++++ 15 files changed, 608 insertions(+), 1 deletion(-) create mode 100644 packages/stream_chat/lib/src/core/models/push_preference.dart create mode 100644 packages/stream_chat/lib/src/core/models/push_preference.g.dart create mode 100644 packages/stream_chat/test/src/core/models/push_preference_test.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 6959d7181a..1849233552 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 073c36c84b..085e5d6164 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); 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 25a678ddbb..b450ddc18e 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 dbd8ed3bdf..9671392056 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,17 @@ class GetUnreadCountResponse extends _BaseResponse { static GetUnreadCountResponse fromJson(Map json) => _$GetUnreadCountResponseFromJson(json); } + +/// Model response for [DeviceApi.setPushPreferences] api call +@JsonSerializable(createToJson: false) +class UpsertPushPreferencesResponse extends _BaseResponse { + /// Mapping of user IDs to their push preferences + late Map userPreferences; + + /// Mapping of user IDs to their channel-specific push preferences + 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 3583ffdf3f..812f72ea87 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,22 @@ 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 ae7b9062b6..601ef679a2 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 88bd81e77e..e154f1235c 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 404e156342..ce300bd4e5 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, @@ -112,6 +113,7 @@ class OwnUser extends User { String? language, Map? teamsRole, int? avgResponseTime, + PushPreference? pushPreferences, }) => OwnUser( id: id ?? this.id, @@ -139,6 +141,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 +171,7 @@ class OwnUser extends User { language: other.language, teamsRole: other.teamsRole, avgResponseTime: other.avgResponseTime, + pushPreferences: other.pushPreferences, ); } @@ -199,6 +203,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 +218,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 df54bfa1d6..e6c30b8bdb 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 0000000000..63b3faefd2 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/push_preference.dart @@ -0,0 +1,133 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'push_preference.g.dart'; + +/// Chat level push preference type +enum ChatLevelPushPreference { + /// All messages + @JsonValue('all') + all, + + /// No messages + @JsonValue('none') + none, + + /// Only mentions + @JsonValue('mentions') + mentions, + + /// Use default system setting + @JsonValue('default') + defaultValue, +} + +/// Call level push preference type +enum CallLevelPushPreference { + /// All calls + @JsonValue('all') + all, + + /// No calls + @JsonValue('none') + none, + + /// Use default system setting + @JsonValue('default') + defaultValue, +} + +/// 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 CallLevelPushPreference? callLevel; + + /// Push preference for chat messages + final ChatLevelPushPreference? 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 { + 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 CallLevelPushPreference? callLevel; + + /// Push preference for chat messages + final ChatLevelPushPreference? 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 { + 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 ChatLevelPushPreference? 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 0000000000..0146db6f60 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/push_preference.g.dart @@ -0,0 +1,78 @@ +// 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 (_$CallLevelPushPreferenceEnumMap[instance.callLevel] + case final value?) + 'call_level': value, + if (_$ChatLevelPushPreferenceEnumMap[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, + }; + +const _$CallLevelPushPreferenceEnumMap = { + CallLevelPushPreference.all: 'all', + CallLevelPushPreference.none: 'none', + CallLevelPushPreference.defaultValue: 'default', +}; + +const _$ChatLevelPushPreferenceEnumMap = { + ChatLevelPushPreference.all: 'all', + ChatLevelPushPreference.none: 'none', + ChatLevelPushPreference.mentions: 'mentions', + ChatLevelPushPreference.defaultValue: 'default', +}; + +PushPreference _$PushPreferenceFromJson(Map json) => + PushPreference( + callLevel: $enumDecodeNullable( + _$CallLevelPushPreferenceEnumMap, json['call_level']), + chatLevel: $enumDecodeNullable( + _$ChatLevelPushPreferenceEnumMap, json['chat_level']), + disabledUntil: json['disabled_until'] == null + ? null + : DateTime.parse(json['disabled_until'] as String), + ); + +Map _$PushPreferenceToJson(PushPreference instance) => + { + if (_$CallLevelPushPreferenceEnumMap[instance.callLevel] + case final value?) + 'call_level': value, + if (_$ChatLevelPushPreferenceEnumMap[instance.chatLevel] + case final value?) + 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) + 'disabled_until': value, + }; + +ChannelPushPreference _$ChannelPushPreferenceFromJson( + Map json) => + ChannelPushPreference( + chatLevel: $enumDecodeNullable( + _$ChatLevelPushPreferenceEnumMap, json['chat_level']), + disabledUntil: json['disabled_until'] == null + ? null + : DateTime.parse(json['disabled_until'] as String), + ); + +Map _$ChannelPushPreferenceToJson( + ChannelPushPreference instance) => + { + if (_$ChatLevelPushPreferenceEnumMap[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 f296711b75..9e7e2372e1 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/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart index 4e04da6b1b..f87b9f47e7 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: ChatLevelPushPreference.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: ChatLevelPushPreference.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 b1e1e9efcd..2e6f256cbf 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, ChatLevelPushPreference.mentions); + expect(user1Prefs.callLevel, CallLevelPushPreference.all); + expect(user1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); + + // Test user2 preferences + final user2Prefs = response.userPreferences['user2']!; + expect(user2Prefs.chatLevel, ChatLevelPushPreference.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, ChatLevelPushPreference.all); + expect( + channel1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); + + final channel2Prefs = user1ChannelPrefs['channel2']!; + expect(channel2Prefs.chatLevel, ChatLevelPushPreference.none); + + // Test user2 channel preferences + final user2ChannelPrefs = response.userChannelPreferences['user2']!; + expect(user2ChannelPrefs, hasLength(1)); + + final channel3Prefs = user2ChannelPrefs['channel3']!; + expect(channel3Prefs.chatLevel, ChatLevelPushPreference.mentions); + }); }); } 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 0000000000..c03cd3ded0 --- /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: CallLevelPushPreference.all, + chatLevel: ChatLevelPushPreference.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: ChatLevelPushPreference.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', () { + final input = PushPreferenceInput( + chatLevel: ChatLevelPushPreference.defaultValue, + callLevel: CallLevelPushPreference.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, CallLevelPushPreference.all); + expect(pushPreference.chatLevel, ChatLevelPushPreference.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, CallLevelPushPreference.defaultValue); + expect(pushPreference.chatLevel, ChatLevelPushPreference.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, ChatLevelPushPreference.none); + expect( + channelPushPreference.disabledUntil, + DateTime.parse('2024-12-31T23:59:59Z'), + ); + }); + + test('should create correctly', () { + final channelPushPreference = ChannelPushPreference( + chatLevel: ChatLevelPushPreference.all, + disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), + ); + + expect(channelPushPreference.chatLevel, ChatLevelPushPreference.all); + expect( + channelPushPreference.disabledUntil, + DateTime.parse('2024-12-31T23:59:59Z'), + ); + }); + }); +} From 629dbdb4e9ffc5f47f6a0f0401b93dfc315dbe8f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 9 Aug 2025 01:15:11 +0200 Subject: [PATCH 2/7] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 7513795552..06226ee448 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 From 826c6ab723f965b8f209e6d53facf88cff5222b0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 9 Aug 2025 01:17:10 +0200 Subject: [PATCH 3/7] chore: minor doc update --- packages/stream_chat/lib/src/core/api/responses.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 9671392056..30190ca526 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -831,7 +831,7 @@ class GetUnreadCountResponse extends _BaseResponse { _$GetUnreadCountResponseFromJson(json); } -/// Model response for [DeviceApi.setPushPreferences] api call +/// Model response for [StreamChatClient.setPushPreferences] api call @JsonSerializable(createToJson: false) class UpsertPushPreferencesResponse extends _BaseResponse { /// Mapping of user IDs to their push preferences From e7209b82a86ca8b939d51887aef0ec5399a3fd78 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 9 Aug 2025 01:20:06 +0200 Subject: [PATCH 4/7] chore: fix lint --- .../lib/src/core/models/push_preference.dart | 2 ++ .../test/src/core/models/push_preference_test.dart | 2 +- .../test/src/db/chat_persistence_client_test.dart | 2 +- packages/stream_chat_flutter/test/src/mocks.dart | 2 +- .../test/message_list_core_test.dart | 2 +- .../test/stream_channel_test.dart | 12 ++++++------ 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/push_preference.dart b/packages/stream_chat/lib/src/core/models/push_preference.dart index 63b3faefd2..ca489c686f 100644 --- a/packages/stream_chat/lib/src/core/models/push_preference.dart +++ b/packages/stream_chat/lib/src/core/models/push_preference.dart @@ -81,6 +81,7 @@ class PushPreferenceInput { /// 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, @@ -110,6 +111,7 @@ class PushPreference extends Equatable { /// 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, 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 index c03cd3ded0..806baac159 100644 --- a/packages/stream_chat/test/src/core/models/push_preference_test.dart +++ b/packages/stream_chat/test/src/core/models/push_preference_test.dart @@ -35,7 +35,7 @@ void main() { }); test('should include default enum value', () { - final input = PushPreferenceInput( + const input = PushPreferenceInput( chatLevel: ChatLevelPushPreference.defaultValue, callLevel: CallLevelPushPreference.defaultValue, ); 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 a3036a2fde..3ebdfc32e4 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 99dc13826c..5418d29303 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 c3e9e77998..d49a31e8f5 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 a8d9c8c0fa..7dfcb93c03 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)); From 7a571d0f359fc65265fec94c03000ccc8d24f6ae Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 9 Aug 2025 01:28:18 +0200 Subject: [PATCH 5/7] test: fix test --- packages/stream_chat/test/fixtures/channel_state_to_json.json | 4 ++++ .../stream_chat/test/src/core/models/channel_state_test.dart | 4 ++++ 2 files changed, 8 insertions(+) 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 9eec788ae6..95f5d56933 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/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 6a436f1dcf..73325a3ab2 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: ChatLevelPushPreference.all, + disabledUntil: DateTime.parse('2020-01-30T13:43:41.062362Z'), + ), ); expect( From 31c4b9ff6bf4cb9a14c87a3d7b972431806e6421 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 9 Aug 2025 01:53:32 +0200 Subject: [PATCH 6/7] refactor: use extension types for push preference enums This commit replaces the `enum` types for `ChatLevelPushPreference` and `CallLevelPushPreference` with `extension type`s named `ChatLevel` and `CallLevel` respectively. This change allows for more flexibility in how these values are handled, particularly in JSON serialization and deserialization, while maintaining type safety. Key changes: - `ChatLevelPushPreference` and `CallLevelPushPreference` enums are removed. - `ChatLevel` and `CallLevel` extension types are introduced. - JSON serialization and deserialization logic for `PushPreference`, `ChannelPushPreference`, and `PushPreferenceInput` has been updated to use the new extension types directly as `String` values, removing the need for `EnumMap` lookups. - `UpsertPushPreferencesResponse` now includes a `defaultValue` of `{}` for `userPreferences` and `userChannelPreferences` to prevent potential null issues during deserialization. - Tests have been updated to reflect these changes. --- .../lib/src/core/api/responses.dart | 2 + .../lib/src/core/api/responses.g.dart | 30 +++++++------ .../lib/src/core/models/push_preference.dart | 35 +++++++--------- .../src/core/models/push_preference.g.dart | 42 ++++--------------- .../test/src/core/api/device_api_test.dart | 4 +- .../test/src/core/api/responses_test.dart | 12 +++--- .../src/core/models/channel_state_test.dart | 2 +- .../src/core/models/push_preference_test.dart | 24 +++++------ 8 files changed, 62 insertions(+), 89 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 30190ca526..cbd4505678 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -835,9 +835,11 @@ class GetUnreadCountResponse extends _BaseResponse { @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 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 812f72ea87..30dd445960 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -494,17 +494,21 @@ 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)), - ) + ..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)), - )), - ); + (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/push_preference.dart b/packages/stream_chat/lib/src/core/models/push_preference.dart index ca489c686f..00ba266a1d 100644 --- a/packages/stream_chat/lib/src/core/models/push_preference.dart +++ b/packages/stream_chat/lib/src/core/models/push_preference.dart @@ -4,37 +4,30 @@ import 'package:json_annotation/json_annotation.dart'; part 'push_preference.g.dart'; /// Chat level push preference type -enum ChatLevelPushPreference { +extension type const ChatLevel(String rawType) implements String { /// All messages - @JsonValue('all') - all, + static const all = ChatLevel('all'); /// No messages - @JsonValue('none') - none, + static const none = ChatLevel('none'); /// Only mentions - @JsonValue('mentions') - mentions, + static const mentions = ChatLevel('mentions'); /// Use default system setting - @JsonValue('default') - defaultValue, + static const defaultValue = ChatLevel('default'); } /// Call level push preference type -enum CallLevelPushPreference { +extension type const CallLevel(String rawType) implements String { /// All calls - @JsonValue('all') - all, + static const all = CallLevel('all'); /// No calls - @JsonValue('none') - none, + static const none = CallLevel('none'); /// Use default system setting - @JsonValue('default') - defaultValue, + static const defaultValue = CallLevel('default'); } /// Input for push preferences, used for creating or updating preferences @@ -63,10 +56,10 @@ class PushPreferenceInput { final String? channelCid; /// Push preference for calls - final CallLevelPushPreference? callLevel; + final CallLevel? callLevel; /// Push preference for chat messages - final ChatLevelPushPreference? chatLevel; + final ChatLevel? chatLevel; /// Disabled until this date (snooze functionality) final DateTime? disabledUntil; @@ -93,10 +86,10 @@ class PushPreference extends Equatable { _$PushPreferenceFromJson(json); /// Push preference for calls - final CallLevelPushPreference? callLevel; + final CallLevel? callLevel; /// Push preference for chat messages - final ChatLevelPushPreference? chatLevel; + final ChatLevel? chatLevel; /// Disabled until this date (snooze functionality) final DateTime? disabledUntil; @@ -122,7 +115,7 @@ class ChannelPushPreference extends Equatable { _$ChannelPushPreferenceFromJson(json); /// Push preference for chat messages - final ChatLevelPushPreference? chatLevel; + final ChatLevel? chatLevel; /// Disabled until this date (snooze functionality) final DateTime? 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 index 0146db6f60..971c0bd1f9 100644 --- a/packages/stream_chat/lib/src/core/models/push_preference.g.dart +++ b/packages/stream_chat/lib/src/core/models/push_preference.g.dart @@ -10,36 +10,17 @@ Map _$PushPreferenceInputToJson( PushPreferenceInput instance) => { if (instance.channelCid case final value?) 'channel_cid': value, - if (_$CallLevelPushPreferenceEnumMap[instance.callLevel] - case final value?) - 'call_level': value, - if (_$ChatLevelPushPreferenceEnumMap[instance.chatLevel] - case final value?) - 'chat_level': 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, }; -const _$CallLevelPushPreferenceEnumMap = { - CallLevelPushPreference.all: 'all', - CallLevelPushPreference.none: 'none', - CallLevelPushPreference.defaultValue: 'default', -}; - -const _$ChatLevelPushPreferenceEnumMap = { - ChatLevelPushPreference.all: 'all', - ChatLevelPushPreference.none: 'none', - ChatLevelPushPreference.mentions: 'mentions', - ChatLevelPushPreference.defaultValue: 'default', -}; - PushPreference _$PushPreferenceFromJson(Map json) => PushPreference( - callLevel: $enumDecodeNullable( - _$CallLevelPushPreferenceEnumMap, json['call_level']), - chatLevel: $enumDecodeNullable( - _$ChatLevelPushPreferenceEnumMap, json['chat_level']), + 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), @@ -47,12 +28,8 @@ PushPreference _$PushPreferenceFromJson(Map json) => Map _$PushPreferenceToJson(PushPreference instance) => { - if (_$CallLevelPushPreferenceEnumMap[instance.callLevel] - case final value?) - 'call_level': value, - if (_$ChatLevelPushPreferenceEnumMap[instance.chatLevel] - case final value?) - 'chat_level': 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, }; @@ -60,8 +37,7 @@ Map _$PushPreferenceToJson(PushPreference instance) => ChannelPushPreference _$ChannelPushPreferenceFromJson( Map json) => ChannelPushPreference( - chatLevel: $enumDecodeNullable( - _$ChatLevelPushPreferenceEnumMap, json['chat_level']), + chatLevel: json['chat_level'] as ChatLevel?, disabledUntil: json['disabled_until'] == null ? null : DateTime.parse(json['disabled_until'] as String), @@ -70,9 +46,7 @@ ChannelPushPreference _$ChannelPushPreferenceFromJson( Map _$ChannelPushPreferenceToJson( ChannelPushPreference instance) => { - if (_$ChatLevelPushPreferenceEnumMap[instance.chatLevel] - case final value?) - 'chat_level': value, + 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/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart index f87b9f47e7..c9d06a843d 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 @@ -137,7 +137,7 @@ void main() { const path = '/push_preferences'; const preferences = [ - PushPreferenceInput(chatLevel: ChatLevelPushPreference.mentions), + PushPreferenceInput(chatLevel: ChatLevel.mentions), ]; when(() => client.post(path, data: any(named: 'data'))).thenAnswer( @@ -190,7 +190,7 @@ void main() { const preferences = [ PushPreferenceInput.channel( channelCid: 'messaging:general', - chatLevel: ChatLevelPushPreference.none, + chatLevel: ChatLevel.none, ), ]; 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 2e6f256cbf..4f9d45c740 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -4486,13 +4486,13 @@ void main() { // Test user1 preferences final user1Prefs = response.userPreferences['user1']!; - expect(user1Prefs.chatLevel, ChatLevelPushPreference.mentions); - expect(user1Prefs.callLevel, CallLevelPushPreference.all); + 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, ChatLevelPushPreference.none); + expect(user2Prefs.chatLevel, ChatLevel.none); expect( response.userChannelPreferences, @@ -4505,19 +4505,19 @@ void main() { expect(user1ChannelPrefs, hasLength(2)); final channel1Prefs = user1ChannelPrefs['channel1']!; - expect(channel1Prefs.chatLevel, ChatLevelPushPreference.all); + expect(channel1Prefs.chatLevel, ChatLevel.all); expect( channel1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); final channel2Prefs = user1ChannelPrefs['channel2']!; - expect(channel2Prefs.chatLevel, ChatLevelPushPreference.none); + 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, ChatLevelPushPreference.mentions); + 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 73325a3ab2..e822e0ed1c 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 @@ -60,7 +60,7 @@ void main() { pinnedMessages: [], watchers: [], pushPreferences: ChannelPushPreference( - chatLevel: ChatLevelPushPreference.all, + chatLevel: ChatLevel.all, disabledUntil: DateTime.parse('2020-01-30T13:43:41.062362Z'), ), ); 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 index 806baac159..1faee8c29f 100644 --- a/packages/stream_chat/test/src/core/models/push_preference_test.dart +++ b/packages/stream_chat/test/src/core/models/push_preference_test.dart @@ -5,8 +5,8 @@ void main() { group('src/models/push_preference_input', () { test('should serialize to json correctly for user-level preferences', () { final input = PushPreferenceInput( - callLevel: CallLevelPushPreference.all, - chatLevel: ChatLevelPushPreference.mentions, + callLevel: CallLevel.all, + chatLevel: ChatLevel.mentions, disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), removeDisable: true, ); @@ -22,7 +22,7 @@ void main() { test('should serialize to json correctly for channel preferences', () { final input = PushPreferenceInput.channel( channelCid: 'messaging:general', - chatLevel: ChatLevelPushPreference.none, + chatLevel: ChatLevel.none, disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), ); @@ -36,8 +36,8 @@ void main() { test('should include default enum value', () { const input = PushPreferenceInput( - chatLevel: ChatLevelPushPreference.defaultValue, - callLevel: CallLevelPushPreference.defaultValue, + chatLevel: ChatLevel.defaultValue, + callLevel: CallLevel.defaultValue, ); final json = input.toJson(); @@ -54,8 +54,8 @@ void main() { 'disabled_until': '2024-12-31T23:59:59Z', }); - expect(pushPreference.callLevel, CallLevelPushPreference.all); - expect(pushPreference.chatLevel, ChatLevelPushPreference.mentions); + expect(pushPreference.callLevel, CallLevel.all); + expect(pushPreference.chatLevel, ChatLevel.mentions); expect( pushPreference.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z'), @@ -68,8 +68,8 @@ void main() { 'chat_level': 'default', }); - expect(pushPreference.callLevel, CallLevelPushPreference.defaultValue); - expect(pushPreference.chatLevel, ChatLevelPushPreference.defaultValue); + expect(pushPreference.callLevel, CallLevel.defaultValue); + expect(pushPreference.chatLevel, ChatLevel.defaultValue); }); }); @@ -80,7 +80,7 @@ void main() { 'disabled_until': '2024-12-31T23:59:59Z', }); - expect(channelPushPreference.chatLevel, ChatLevelPushPreference.none); + expect(channelPushPreference.chatLevel, ChatLevel.none); expect( channelPushPreference.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z'), @@ -89,11 +89,11 @@ void main() { test('should create correctly', () { final channelPushPreference = ChannelPushPreference( - chatLevel: ChatLevelPushPreference.all, + chatLevel: ChatLevel.all, disabledUntil: DateTime.parse('2024-12-31T23:59:59Z'), ); - expect(channelPushPreference.chatLevel, ChatLevelPushPreference.all); + expect(channelPushPreference.chatLevel, ChatLevel.all); expect( channelPushPreference.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z'), From 9e926c9a02b66e451311fed47199fa8ef5441260 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 11 Aug 2025 12:36:29 +0200 Subject: [PATCH 7/7] chore: preserve push preferences --- packages/stream_chat/lib/src/client/client.dart | 1 + packages/stream_chat/lib/src/core/models/own_user.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 085e5d6164..7897b83ec5 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -2188,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/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index ce300bd4e5..6e1840991a 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -69,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