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
5 changes: 5 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
- Fixed cached messages are cleared from channels with unread messages when accessed
offline. [[#2083]](https://github.com/GetStream/stream-chat-flutter/issues/2083)

✅ Added

- Added support for `client.getUnreadCount()`, which returns the unread count information for the
current user.

🔄 Changed

- Deprecated `SortOption.new` constructor in favor of `SortOption.desc` and `SortOption.asc`.
Expand Down
15 changes: 15 additions & 0 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,21 @@ class StreamChatClient {
}
}

/// Returns the unread count information for the current user.
Future<GetUnreadCountResponse> getUnreadCount() async {
final response = await _chatApi.user.getUnreadCount();

// Emit an local event with the unread count information as a side effect
// in order to update the current user state.
handleEvent(Event(
totalUnreadCount: response.totalUnreadCount,
unreadChannels: response.channels.length,
unreadThreads: response.threads.length,
));

return response;
}

/// Mutes a user
Future<EmptyResponse> muteUser(String userId) =>
_chatApi.moderation.muteUser(userId);
Expand Down
27 changes: 27 additions & 0 deletions packages/stream_chat/lib/src/core/api/responses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:stream_chat/src/core/models/poll_vote.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';
import 'package:stream_chat/src/core/models/unread_counts.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/models/user_block.dart';

Expand Down Expand Up @@ -802,3 +803,29 @@ class QueryRemindersResponse extends _BaseResponse {
static QueryRemindersResponse fromJson(Map<String, dynamic> json) =>
_$QueryRemindersResponseFromJson(json);
}

/// Model response for [StreamChatClient.getUnreadCount] api call
@JsonSerializable(createToJson: false)
class GetUnreadCountResponse extends _BaseResponse {
/// Total number of unread messages across all channels
late int totalUnreadCount;

/// Total number of threads with unread replies
late int totalUnreadThreadsCount;

/// Total number of unread messages grouped by team
late Map<String, int>? totalUnreadCountByTeam;

/// List of channels with unread messages
late List<UnreadCountsChannel> channels;

/// Summary of unread counts grouped by channel type
late List<UnreadCountsChannelType> channelType;

/// List of threads with unread replies
late List<UnreadCountsThread> threads;

/// Create a new instance from a json
static GetUnreadCountResponse fromJson(Map<String, dynamic> json) =>
_$GetUnreadCountResponseFromJson(json);
}
22 changes: 22 additions & 0 deletions packages/stream_chat/lib/src/core/api/responses.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/core/api/user_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,11 @@ class UserApi {

return BlockedUsersResponse.fromJson(response.data);
}

/// Requests the unread count information for the current user.
Future<GetUnreadCountResponse> getUnreadCount() async {
final response = await _client.get('/unread');

return GetUnreadCountResponse.fromJson(response.data);
}
}
95 changes: 95 additions & 0 deletions packages/stream_chat/lib/src/core/models/unread_counts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:json_annotation/json_annotation.dart';

part 'unread_counts.g.dart';

/// {@template unreadCountsChannel}
/// A model class representing information for a specific channel.
/// {@endtemplate}
@JsonSerializable()
class UnreadCountsChannel {
/// {@macro unreadCountsChannel}
const UnreadCountsChannel({
required this.channelId,
required this.unreadCount,
required this.lastRead,
});

/// Create a new instance from a json.
factory UnreadCountsChannel.fromJson(Map<String, dynamic> json) =>
_$UnreadCountsChannelFromJson(json);

/// The unique identifier of the channel (format: "type:id").
final String channelId;

/// Number of unread messages in this channel.
final int unreadCount;

/// Timestamp when the channel was last read by the user.
final DateTime lastRead;

/// Serializes this instance to a JSON map.
Map<String, dynamic> toJson() => _$UnreadCountsChannelToJson(this);
}

/// {@template unreadCountsThread}
/// A model class representing unread count information for a specific thread.
/// {@endtemplate}
@JsonSerializable()
class UnreadCountsThread {
/// {@macro unreadCountsThread}
const UnreadCountsThread({
required this.unreadCount,
required this.lastRead,
required this.lastReadMessageId,
required this.parentMessageId,
});

/// Create a new instance from a json.
factory UnreadCountsThread.fromJson(Map<String, dynamic> json) =>
_$UnreadCountsThreadFromJson(json);

/// Number of unread messages in this thread.
final int unreadCount;

/// Timestamp when the thread was last read by the user.
final DateTime lastRead;

/// ID of the last message that was read in this thread.
final String lastReadMessageId;

/// ID of the parent message that started this thread.
final String parentMessageId;

/// Serializes this instance to a JSON map.
Map<String, dynamic> toJson() => _$UnreadCountsThreadToJson(this);
}

/// {@template unreadCountsChannelType}
/// A model class representing aggregated unread count information for a
/// specific channel type.
/// {@endtemplate}
@JsonSerializable()
class UnreadCountsChannelType {
/// {@macro unreadCountsChannelType}
const UnreadCountsChannelType({
required this.channelType,
required this.channelCount,
required this.unreadCount,
});

/// Create a new instance from a json.
factory UnreadCountsChannelType.fromJson(Map<String, dynamic> json) =>
_$UnreadCountsChannelTypeFromJson(json);

/// The type of channel (e.g., "messaging", "livestream", "team").
final String channelType;

/// Number of channels of this type that have unread messages.
final int channelCount;

/// Total number of unread messages across all channels of this type.
final int unreadCount;

/// Serializes this instance to a JSON map.
Map<String, dynamic> toJson() => _$UnreadCountsChannelTypeToJson(this);
}
54 changes: 54 additions & 0 deletions packages/stream_chat/lib/src/core/models/unread_counts.g.dart

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

1 change: 1 addition & 0 deletions packages/stream_chat/lib/stream_chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export 'src/core/models/reaction_group.dart';
export 'src/core/models/read.dart';
export 'src/core/models/thread.dart';
export 'src/core/models/thread_participant.dart';
export 'src/core/models/unread_counts.dart';
export 'src/core/models/user.dart';
export 'src/core/models/user_block.dart';
export 'src/core/platform_detector/platform_detector.dart';
Expand Down
94 changes: 94 additions & 0 deletions packages/stream_chat/test/src/client/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2631,6 +2631,100 @@ void main() {
);
});

test('`.getUnreadCount`', () async {
when(() => api.user.getUnreadCount()).thenAnswer(
(_) async => GetUnreadCountResponse()
..totalUnreadCount = 42
..totalUnreadThreadsCount = 8
..channelType = []
..channels = [
UnreadCountsChannel(
channelId: 'messaging:test-channel-1',
unreadCount: 10,
lastRead: DateTime.now(),
),
UnreadCountsChannel(
channelId: 'messaging:test-channel-2',
unreadCount: 15,
lastRead: DateTime.now(),
),
]
..threads = [
UnreadCountsThread(
unreadCount: 3,
lastRead: DateTime.now(),
lastReadMessageId: 'message-1',
parentMessageId: 'parent-message-1',
),
UnreadCountsThread(
unreadCount: 5,
lastRead: DateTime.now(),
lastReadMessageId: 'message-2',
parentMessageId: 'parent-message-2',
),
],
);

final res = await client.getUnreadCount();

expect(res, isNotNull);
expect(res.totalUnreadCount, 42);
expect(res.totalUnreadThreadsCount, 8);

verify(() => api.user.getUnreadCount()).called(1);
verifyNoMoreInteractions(api.user);
});

test(
'`.getUnreadCount` should also update user unread count as a side effect',
() async {
when(() => api.user.getUnreadCount()).thenAnswer(
(_) async => GetUnreadCountResponse()
..totalUnreadCount = 25
..totalUnreadThreadsCount = 2
..channelType = []
..channels = [
UnreadCountsChannel(
channelId: 'messaging:test-channel-1',
unreadCount: 10,
lastRead: DateTime.now(),
),
UnreadCountsChannel(
channelId: 'messaging:test-channel-2',
unreadCount: 15,
lastRead: DateTime.now(),
),
]
..threads = [
UnreadCountsThread(
unreadCount: 3,
lastRead: DateTime.now(),
lastReadMessageId: 'message-1',
parentMessageId: 'parent-message-1',
),
UnreadCountsThread(
unreadCount: 5,
lastRead: DateTime.now(),
lastReadMessageId: 'message-2',
parentMessageId: 'parent-message-2',
),
],
);

client.getUnreadCount().ignore();

// Wait for the local side effect event to be processed
await Future.delayed(Duration.zero);

expect(client.state.currentUser?.totalUnreadCount, 25);
expect(client.state.currentUser?.unreadChannels, 2); // channels.length
expect(client.state.currentUser?.unreadThreads, 2); // threads.length

verify(() => api.user.getUnreadCount()).called(1);
verifyNoMoreInteractions(api.user);
},
);

test('`.shadowBan`', () async {
const userId = 'test-user-id';

Expand Down
Loading