diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 04e3d31f7..d7b841457 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -2,6 +2,7 @@ 🐞 Fixed +- Fixed `currentUser.pushPreferences` not updating immediately after calling `setPushPreferences`. - Fixed `Channel.sendMessage` to prevent sending empty messages when all attachments are cancelled during upload. - Fixed `toDraftMessage` to only include successfully uploaded attachments in draft messages. diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 8c1420a6f..54f163c0f 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -2197,6 +2197,8 @@ class ChannelClientState { _startCleaningStalePinnedMessages(); + _listenChannelPushPreferenceUpdated(); + _channel._client.chatPersistenceClient ?.getChannelThreads(_channel.cid!) .then((threads) { @@ -3515,6 +3517,24 @@ class ChannelClientState { ); } + // Listens to channel push preference update events and updates the state + void _listenChannelPushPreferenceUpdated() { + _subscriptions.add( + _channel.on(EventType.channelPushPreferenceUpdated).listen( + (event) { + final pushPreferences = event.channelPushPreference; + if (pushPreferences == null) return; + + updateChannelState( + channelState.copyWith( + pushPreferences: pushPreferences, + ), + ); + }, + ), + ); + } + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7897b83ec..fee7eef0e 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1034,8 +1034,39 @@ class StreamChatClient { /// ``` Future setPushPreferences( List preferences, - ) { - return _chatApi.device.setPushPreferences(preferences); + ) async { + final res = await _chatApi.device.setPushPreferences(preferences); + + final currentUser = state.currentUser; + final currentUserId = currentUser?.id; + if (currentUserId == null) return res; + + // Emit events for updated preferences + final updatedPushPreference = res.userPreferences[currentUserId]; + if (updatedPushPreference != null) { + final pushPreferenceUpdatedEvent = Event( + type: EventType.pushPreferenceUpdated, + pushPreference: updatedPushPreference, + ); + + handleEvent(pushPreferenceUpdatedEvent); + } + + // Emit events for updated channel-specific preferences + final channelPushPreferences = res.userChannelPreferences[currentUserId]; + if (channelPushPreferences != null) { + for (final MapEntry(:key, :value) in channelPushPreferences.entries) { + final pushPreferenceUpdatedEvent = Event( + type: EventType.channelPushPreferenceUpdated, + cid: key, + channelPushPreference: value, + ); + + handleEvent(pushPreferenceUpdatedEvent); + } + } + + return res; } /// Get a development token @@ -2129,6 +2160,11 @@ class ClientState { if (event.unreadThreads case final count?) { currentUser = currentUser?.copyWith(unreadThreads: count); } + + // Update the push preferences. + if (event.pushPreference case final preferences?) { + currentUser = currentUser?.copyWith(pushPreferences: preferences); + } }), ); diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index d331e4ff4..75e0211b4 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -41,6 +41,8 @@ class Event { this.lastReadMessageId, this.draft, this.reminder, + this.pushPreference, + this.channelPushPreference, this.extraData = const {}, this.isLocal = true, }) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc(); @@ -154,6 +156,12 @@ class Event { /// The message reminder sent with the event. final MessageReminder? reminder; + /// Push notification preferences for the current user. + final PushPreference? pushPreference; + + /// Push notification preferences for the current user for this channel. + final ChannelPushPreference? channelPushPreference; + /// Map of custom channel extraData final Map extraData; @@ -193,6 +201,8 @@ class Event { 'last_read_message_id', 'draft', 'reminder', + 'push_preference', + 'channel_push_preference', ]; /// Serialize to json @@ -234,6 +244,8 @@ class Event { String? lastReadMessageId, Draft? draft, MessageReminder? reminder, + PushPreference? pushPreference, + ChannelPushPreference? channelPushPreference, Map? extraData, }) => Event( @@ -269,6 +281,9 @@ class Event { lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId, draft: draft ?? this.draft, reminder: reminder ?? this.reminder, + pushPreference: pushPreference ?? this.pushPreference, + channelPushPreference: + channelPushPreference ?? this.channelPushPreference, isLocal: isLocal, extraData: extraData ?? this.extraData, ); diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index c81ddeeda..1623dc143 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -68,6 +68,14 @@ Event _$EventFromJson(Map json) => Event( reminder: json['reminder'] == null ? null : MessageReminder.fromJson(json['reminder'] as Map), + pushPreference: json['push_preference'] == null + ? null + : PushPreference.fromJson( + json['push_preference'] as Map), + channelPushPreference: json['channel_push_preference'] == null + ? null + : ChannelPushPreference.fromJson( + json['channel_push_preference'] as Map), extraData: json['extra_data'] as Map? ?? const {}, isLocal: json['is_local'] as bool? ?? false, ); @@ -112,6 +120,10 @@ Map _$EventToJson(Event instance) => { 'last_read_message_id': value, if (instance.draft?.toJson() case final value?) 'draft': value, if (instance.reminder?.toJson() case final value?) 'reminder': value, + if (instance.pushPreference?.toJson() case final value?) + 'push_preference': value, + if (instance.channelPushPreference?.toJson() case final value?) + 'channel_push_preference': value, 'extra_data': instance.extraData, }; diff --git a/packages/stream_chat/lib/src/event_type.dart b/packages/stream_chat/lib/src/event_type.dart index 29ab5b1de..63c3c3f8e 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -170,4 +170,11 @@ class EventType { /// Event sent when a message reminder is due. static const String notificationReminderDue = 'notification.reminder_due'; + + /// Local event sent when push notification preference is updated. + static const String pushPreferenceUpdated = 'push_preference.updated'; + + /// Local event sent when channel push notification preference is updated. + static const String channelPushPreferenceUpdated = + 'channel.push_preference.updated'; } diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 46ec0930d..8034264ae 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -5102,6 +5102,99 @@ void main() { expect(updatedMessage?.reminder, isNull); }); }); + + group('Channel push preference events', () { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUp(() { + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + test('should handle channel.push_preference.updated event', () async { + // Verify initial state + expect(channel.state?.channelState.pushPreferences, isNull); + + // Create channel push preference + final channelPushPreference = ChannelPushPreference( + chatLevel: ChatLevel.mentions, + disabledUntil: DateTime.now().add(const Duration(hours: 1)), + ); + + // Create channel.push_preference.updated event + final channelPushPreferenceUpdatedEvent = Event( + cid: channel.cid, + type: EventType.channelPushPreferenceUpdated, + channelPushPreference: channelPushPreference, + ); + + // Dispatch event + client.addEvent(channelPushPreferenceUpdatedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify channel push preferences were updated + final updatedPreferences = channel.state?.channelState.pushPreferences; + expect(updatedPreferences, isNotNull); + expect(updatedPreferences?.chatLevel, ChatLevel.mentions); + expect( + updatedPreferences?.disabledUntil, + channelPushPreference.disabledUntil, + ); + }); + + test('should update existing channel push preferences', () async { + // Set initial push preferences + const initialPushPreference = ChannelPushPreference( + chatLevel: ChatLevel.all, + ); + + channel.state?.updateChannelState( + channel.state!.channelState.copyWith( + pushPreferences: initialPushPreference, + ), + ); + + // Verify initial state + final pushPreferences = channel.state?.channelState.pushPreferences; + expect(pushPreferences?.chatLevel, ChatLevel.all); + expect(pushPreferences?.disabledUntil, isNull); + + // Create updated channel push preference + final updatedPushPreference = ChannelPushPreference( + chatLevel: ChatLevel.none, + disabledUntil: DateTime.now().add(const Duration(hours: 2)), + ); + + // Create channel.push_preference.updated event + final channelPushPreferenceUpdatedEvent = Event( + cid: channel.cid, + type: EventType.channelPushPreferenceUpdated, + channelPushPreference: updatedPushPreference, + ); + + // Dispatch event + client.addEvent(channelPushPreferenceUpdatedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify channel push preferences were updated + final updatedPreferences = channel.state?.channelState.pushPreferences; + expect(updatedPreferences?.chatLevel, ChatLevel.none); + expect( + updatedPreferences?.disabledUntil, + updatedPushPreference.disabledUntil, + ); + }); + }); }); group('ChannelCapabilityCheck', () { diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 4eaad9f83..7fc654b5d 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -1242,6 +1242,88 @@ void main() { verifyNoMoreInteractions(api.device); }); + test('`.setPushPreferences`', () async { + const pushPreferenceInput = PushPreferenceInput( + chatLevel: ChatLevel.mentions, + ); + + const channelCid = 'messaging:123'; + const channelPreferenceInput = PushPreferenceInput.channel( + channelCid: channelCid, + chatLevel: ChatLevel.mentions, + ); + + const preferences = [pushPreferenceInput, channelPreferenceInput]; + + final currentUser = client.state.currentUser; + when(() => api.device.setPushPreferences(preferences)).thenAnswer( + (_) async => UpsertPushPreferencesResponse() + ..userPreferences = { + '${currentUser?.id}': PushPreference( + chatLevel: pushPreferenceInput.chatLevel, + ), + } + ..userChannelPreferences = { + '${currentUser?.id}': { + channelCid: ChannelPushPreference( + chatLevel: channelPreferenceInput.chatLevel, + ), + }, + }, + ); + + expect( + client.eventStream, + emitsInOrder([ + isA().having( + (e) => e.type, + 'push_preference.updated event', + EventType.pushPreferenceUpdated, + ), + isA().having( + (e) => e.type, + 'channel.push_preference.updated event', + EventType.channelPushPreferenceUpdated, + ), + ]), + ); + + final res = await client.setPushPreferences(preferences); + expect(res, isNotNull); + + verify(() => api.device.setPushPreferences(preferences)).called(1); + verifyNoMoreInteractions(api.device); + }); + + test('should handle push_preference.updated event', () async { + final pushPreference = PushPreference( + chatLevel: ChatLevel.mentions, + callLevel: CallLevel.all, + disabledUntil: DateTime.now().add(const Duration(hours: 1)), + ); + + final event = Event( + type: EventType.pushPreferenceUpdated, + pushPreference: pushPreference, + ); + + // Initially null + expect(client.state.currentUser?.pushPreferences, isNull); + + // Trigger the event + client.handleEvent(event); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + // Should update currentUser.pushPreferences + final pushPreferences = client.state.currentUser?.pushPreferences; + expect(pushPreferences, isNotNull); + expect(pushPreferences?.chatLevel, ChatLevel.mentions); + expect(pushPreferences?.callLevel, CallLevel.all); + expect(pushPreferences?.disabledUntil, pushPreference.disabledUntil); + }); + test('`.devToken`', () async { const userId = 'test-user-id';