Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 @@ -2197,6 +2197,8 @@ class ChannelClientState {

_startCleaningStalePinnedMessages();

_listenChannelPushPreferenceUpdated();

_channel._client.chatPersistenceClient
?.getChannelThreads(_channel.cid!)
.then((threads) {
Expand Down Expand Up @@ -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();
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 @@ -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', () {
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