Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Upcoming

🐞 Fixed

- Fixed `currentUser.pushPreferences` not updating immediately after calling `setPushPreferences`.

## 9.16.0

🐞 Fixed
Expand Down
20 changes: 20 additions & 0 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,8 @@ class ChannelClientState {

_startCleaningStalePinnedMessages();

_listenChannelPushPreferenceUpdated();

_channel._client.chatPersistenceClient
?.getChannelThreads(_channel.cid!)
.then((threads) {
Expand Down Expand Up @@ -3497,6 +3499,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();
Expand Down
40 changes: 38 additions & 2 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1034,8 +1034,39 @@ class StreamChatClient {
/// ```
Future<UpsertPushPreferencesResponse> setPushPreferences(
List<PushPreferenceInput> 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
Expand Down Expand Up @@ -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);
}
}),
);

Expand Down
15 changes: 15 additions & 0 deletions packages/stream_chat/lib/src/core/models/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<String, Object?> extraData;

Expand Down Expand Up @@ -193,6 +201,8 @@ class Event {
'last_read_message_id',
'draft',
'reminder',
'push_preference',
'channel_push_preference',
];

/// Serialize to json
Expand Down Expand Up @@ -234,6 +244,8 @@ class Event {
String? lastReadMessageId,
Draft? draft,
MessageReminder? reminder,
PushPreference? pushPreference,
ChannelPushPreference? channelPushPreference,
Map<String, Object?>? extraData,
}) =>
Event(
Expand Down Expand Up @@ -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,
);
Expand Down
12 changes: 12 additions & 0 deletions packages/stream_chat/lib/src/core/models/event.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/stream_chat/lib/src/event_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
93 changes: 93 additions & 0 deletions packages/stream_chat/test/src/client/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4814,6 +4814,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', () {
Expand Down
82 changes: 82 additions & 0 deletions packages/stream_chat/test/src/client/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event>().having(
(e) => e.type,
'push_preference.updated event',
EventType.pushPreferenceUpdated,
),
isA<Event>().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';

Expand Down