From 18f87d8b418d2c975592cf1b3aea28b1c4f4eadb Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Wed, 7 Oct 2026 09:32:29 +0200 Subject: [PATCH 1/4] refactor(llc)!: route getUnreadCount through the generated client Co-Authored-By: Claude Opus 5.5 --- migrations/v11-migration.md | 42 +++++ openapi-migration/09-users.md | 6 +- openapi-migration/18-unread-counts.md | 58 +++++++ openapi-migration/README.md | 6 +- openapi-migration/tool/generate_plan.py | 30 +++- packages/stream_chat/CHANGELOG.md | 5 + .../stream_chat/lib/src/client/client.dart | 31 ++-- .../lib/src/core/api/responses.dart | 26 --- .../lib/src/core/api/responses.g.dart | 19 --- .../lib/src/core/api/user_api.dart | 7 - .../response/get_unread_count_response.dart | 55 +++++++ .../get_unread_count_response.freezed.dart | 147 +++++++++++++++++ .../lib/src/core/models/unread_counts.dart | 92 ----------- .../lib/src/core/models/unread_counts.g.dart | 51 ------ .../core/models/unread_counts_channel.dart | 28 ++++ .../models/unread_counts_channel.freezed.dart | 92 +++++++++++ .../models/unread_counts_channel_type.dart | 28 ++++ .../unread_counts_channel_type.freezed.dart | 92 +++++++++++ .../src/core/models/unread_counts_thread.dart | 33 ++++ .../models/unread_counts_thread.freezed.dart | 109 +++++++++++++ .../src/repository/mapper/user_mapper.dart | 49 ++++++ .../lib/src/repository/users_repository.dart | 19 +++ packages/stream_chat/lib/stream_chat.dart | 5 +- .../client/client_get_unread_count_test.dart | 154 ++++++++++++++++++ .../test/src/client/client_test.dart | 94 ----------- .../test/src/core/api/user_api_test.dart | 67 -------- .../src/core/models/unread_counts_test.dart | 127 --------------- 27 files changed, 967 insertions(+), 505 deletions(-) create mode 100644 openapi-migration/18-unread-counts.md create mode 100644 packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart create mode 100644 packages/stream_chat/lib/src/core/models/response/get_unread_count_response.freezed.dart delete mode 100644 packages/stream_chat/lib/src/core/models/unread_counts.dart delete mode 100644 packages/stream_chat/lib/src/core/models/unread_counts.g.dart create mode 100644 packages/stream_chat/lib/src/core/models/unread_counts_channel.dart create mode 100644 packages/stream_chat/lib/src/core/models/unread_counts_channel.freezed.dart create mode 100644 packages/stream_chat/lib/src/core/models/unread_counts_channel_type.dart create mode 100644 packages/stream_chat/lib/src/core/models/unread_counts_channel_type.freezed.dart create mode 100644 packages/stream_chat/lib/src/core/models/unread_counts_thread.dart create mode 100644 packages/stream_chat/lib/src/core/models/unread_counts_thread.freezed.dart create mode 100644 packages/stream_chat/lib/src/repository/users_repository.dart create mode 100644 packages/stream_chat/test/src/client/client_get_unread_count_test.dart delete mode 100644 packages/stream_chat/test/src/core/models/unread_counts_test.dart diff --git a/migrations/v11-migration.md b/migrations/v11-migration.md index 77d95d45da..a503673824 100644 --- a/migrations/v11-migration.md +++ b/migrations/v11-migration.md @@ -36,6 +36,7 @@ onto Stream's OpenAPI-generated API client. - [Partial Updates](#partial-updates) - [Channel Lifecycle](#channel-lifecycle) - [Read Receipts](#read-receipts) + - [Unread Counts](#unread-counts) - [Migration Checklist](#migration-checklist) - [For AI Agents](#for-ai-agents) - [Contributing to this guide](#contributing-to-this-guide) @@ -84,6 +85,7 @@ from the spec, so don't subclass them or depend on their private constructors. | [**Partial Updates**](#partial-updates) | Channel and member partial updates — `updatePartial`, `updateName`, `updateImage`, slow mode, pin and archive — return a `Result` instead of throwing; their responses take the API's names, `UpdateChannelPartialResponse` and `UpdateMemberPartialResponse`, and `partialMemberUpdate` becomes `updateMemberPartial` | | [**Channel Lifecycle**](#channel-lifecycle) | Hiding, showing and deleting a channel return a `Result` instead of throwing; stopping watching still throws | | [**Read Receipts**](#read-receipts) | Marking read, unread and delivered return a `Result` instead of throwing; `ChannelDeliveryReporter`'s callback returns a `Result` | +| [**Unread Counts**](#unread-counts) | `getUnreadCount` returns a `Result` instead of throwing; the response and its `UnreadCounts*` models no longer decode JSON and compare by value | | _(filled in per feature as PRs land)_ | | --- @@ -255,6 +257,12 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `MessageDelivery.toJson` | — | `removed` | `MessageDelivery` is a plain class | | `MessageDelivery` identity `==` | value `==`, plus `copyWith` | `retyped` | Two instances with the same fields are now equal | | `StreamChatApi.channel.markRead` / `markUnread` / `markUnreadByTimestamp` / `markThreadRead` / `markThreadUnread` / `markAllRead` / `markChannelsDelivered` | the `StreamChatClient` methods | `removed` | The endpoints moved to the generated client | +| `StreamChatClient.getUnreadCount` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The current user's unread counts are still updated on success | +| `GetUnreadCountResponse.fromJson`, `UnreadCountsChannel.fromJson` / `toJson`, `UnreadCountsThread.fromJson` / `toJson`, `UnreadCountsChannelType.fromJson` / `toJson` | — | `removed` | The response and models are plain classes; construct them directly | +| `GetUnreadCountResponse()..totalUnreadCount = …` and its other setters | `GetUnreadCountResponse(duration: …, totalUnreadCount: …, …)` | `retyped` | A plain class with a const constructor and final fields | +| `GetUnreadCountResponse`, `UnreadCountsChannel`, `UnreadCountsThread`, `UnreadCountsChannelType` identity `==` | value `==`, plus `copyWith` | `retyped` | Two instances with the same fields are now equal | +| `GetUnreadCountResponse.duration` (`String?`) | `String` | `retyped` | Always present; drop any `!` or `?? ''` | +| `StreamChatApi.user.getUnreadCount` | `StreamChatClient.getUnreadCount` | `removed` | The endpoint moved to the generated client | | _(more added per feature as PRs land)_ | | | | --- @@ -796,6 +804,40 @@ ChannelDeliveryReporter( > and sending receipts answer their own envelope, so a field the API adds later reaches you without another > break. Marking unread answers nothing the API could extend, so it carries no value. +### Unread Counts + +**`StreamChatClient.getUnreadCount` returns a `Result` instead of throwing.** A +`try`/`catch` around it still compiles, but no longer catches a failed call: read the returned `Result` instead. +On success it still updates the current user's `totalUnreadCount`, `unreadChannels` and `unreadThreads`; a +failure leaves them as they were. + +```dart +// v10 +try { + final counts = await client.getUnreadCount(); + showBadge(counts.totalUnreadCount); +} on StreamChatException catch (e) { + report(e); +} + +// v11 +final result = await client.getUnreadCount(); +switch (result) { + case Success(:final data): showBadge(data.totalUnreadCount); + case Failure(:final error): report(error); +} +``` + +**`UnreadCountsChannel`, `UnreadCountsThread` and `UnreadCountsChannelType` no longer decode from or encode to +JSON, and `GetUnreadCountResponse` no longer decodes from it.** All four compare by value and gain `copyWith`. +`GetUnreadCountResponse` is now a plain class with a const constructor and final fields, and its `duration` is +always present. + +**`StreamChatApi.user.getUnreadCount` is removed.** Call `StreamChatClient.getUnreadCount`. + +> **Why:** the endpoint moved onto the generated client, which returns a `Result` for every call. The response +> keeps its v10 name and fields; what changes is the error handling, the JSON codecs and value equality. + ### Moderation **Muting, banning and flagging return a `Result` instead of throwing**, on both `StreamChatClient` diff --git a/openapi-migration/09-users.md b/openapi-migration/09-users.md index 3df0231010..2fe91f4e16 100644 --- a/openapi-migration/09-users.md +++ b/openapi-migration/09-users.md @@ -1,8 +1,8 @@ # 09 — Users -**Goal:** `User` is the most widely referenced public model in the SDK; this is where keep-vs-adopt costs the most. +**Goal:** `User` is the most widely referenced public model in the SDK; this is where keep-vs-adopt costs the most. [18](18-unread-counts.md) split the current user's unread counts out of it. -**Size:** 9 hand-written method(s) across 1 file(s) → 9 generated operation(s). +**Size:** 8 hand-written method(s) across 1 file(s) → 8 generated operation(s). > This whole file is generated by `openapi-migration/tool/generate_plan.py`, prose included. > Edit its `GROUPS` entry and re-run the script — edits made here are lost on the next run. @@ -19,7 +19,6 @@ | `user_api.dart` | `blockUser` | `UserBlockResponse` | | `user_api.dart` | `unblockUser` | `EmptyResponse` | | `user_api.dart` | `queryBlockedUsers` | `BlockedUsersResponse` | -| `user_api.dart` | `getUnreadCount` | `GetUnreadCountResponse` | | `user_api.dart` | `getActiveLiveLocations` | `GetActiveLiveLocationsResponse` | | `user_api.dart` | `updateLiveLocation` | `Location` | @@ -32,7 +31,6 @@ | `GET` | `/api/v2/users/live_locations` | `getUserLiveLocations` | `SharedLocationsResponse` | | `GET` | `/api/v2/users` | `queryUsers` | `QueryUsersResponse` | | `POST` | `/api/v2/users/unblock` | `unblockUsers` | `UnblockUsersResponse` | -| `GET` | `/api/v2/chat/unread` | `unreadCounts` | `WrappedUnreadCountsResponse` | | `PUT` | `/api/v2/users/live_locations` | `updateLiveLocation` | `SharedLocationResponse` | | `POST` | `/api/v2/users` | `updateUsers` | `UpdateUsersResponse` | | `PATCH` | `/api/v2/users` | `updateUsersPartial` | `UpdateUsersResponse` | diff --git a/openapi-migration/18-unread-counts.md b/openapi-migration/18-unread-counts.md new file mode 100644 index 0000000000..7b3298f10e --- /dev/null +++ b/openapi-migration/18-unread-counts.md @@ -0,0 +1,58 @@ +# 18 — Unread Counts + +**Goal:** Move reading the current user's unread counts ahead of [09](09-users.md): it takes no parameters, answers only counts, and needs none of the `User` restructuring. + +**Size:** 0 hand-written method(s) across 0 file(s) → 1 generated operation(s). + +> This whole file is generated by `openapi-migration/tool/generate_plan.py`, prose included. +> Edit its `GROUPS` entry and re-run the script — edits made here are lost on the next run. + +## Scope + +### Hand-written today + +| File | Method | Returns | +| --- | --- | --- | + +### Generated operations that cover it + +| Verb | Path | Operation | Response | +| --- | --- | --- | --- | +| `GET` | `/api/v2/chat/unread` | `unreadCounts` | `WrappedUnreadCountsResponse` | + +## Decisions taken + +- **Split out of [09](09-users.md), ahead of it.** Nothing persists the response, and it embeds no + `User`. +- **The v2 route is the v1 handler.** `lib/chat/routes.go` mounts `/unread` and `/api/v2/chat/unread` + on the same `v1.UnreadCounts` in the shared `coreRoutes`. It is gated by + `ClassicUnreadCountsEnabled` on both, so the switch changes nothing about who may call it. It is + not in beta or deprecated. +- **Moved off `UserApi`:** `getUnreadCount`, now a `StreamChatClient` method over a new + `UsersRepository`, mirroring `UserApi`. +- **The v10 names stay:** `getUnreadCount` and `GetUnreadCountResponse`, rather than the spec's + `unreadCounts` and `WrappedUnreadCountsResponse`, which say nothing a caller needs. +- **`GetUnreadCountResponse` and the `UnreadCounts*` models are freezed plain models,** with v10's + fields and nullability; the generated types match them field for field. `duration` is non-null. +- **The current user's counts are updated only on success,** through `Result.onSuccess`, as v10 did + by throwing before it reached the update. + +## Risks + + +## Definition of done + +- [x] Every method above either routes through `DefaultApi` or is listed here as deliberately left + hand-written, with the reason. +- [x] Public methods return `Future>`; no `getOrThrow()` inside the SDK. +- [x] Hand-written request/response DTOs for this group are deleted, or their retention is justified. +- [x] `melos run analyze` clean, `melos run test:dart` green, persistence tests green if this group + persists anything. +- [x] `migrations/v11-migration.md`: Symbol Map rows plus a feature section for every break. +- [x] CHANGELOG entry under `🛑️ Breaking` for each break; PR title `refactor(llc)!:`. +- [x] Decisions recorded in this file, and the status box ticked in `README.md`. +- [x] Public dartdoc follows [`STYLE_GUIDE.md` § Documentation](../STYLE_GUIDE.md#documentation) + and [`EFFECTIVE_DART_DOC.md`](../EFFECTIVE_DART_DOC.md), including on symbols this group + retyped but whose docs it left alone. +- [x] Tests follow [`TESTING.md`](../TESTING.md): no `group` organizing a file by method, each + name states its subject and behaviour. diff --git a/openapi-migration/README.md b/openapi-migration/README.md index 03d4e990af..7e16e7dc31 100644 --- a/openapi-migration/README.md +++ b/openapi-migration/README.md @@ -20,7 +20,7 @@ generated operations in scope, the decisions that group has to make, its risks, | [06](06-reminders.md) | Message Reminders | 4 | 4 | ☐ | | [07](07-threads-and-drafts.md) | Threads & Drafts | 7 | 7 | ☐ | | [08](08-moderation-and-blocklists.md) | Moderation & Blocklists | 0 | 34 | ☑ | -| [09](09-users.md) | Users | 9 | 9 | ☐ | +| [09](09-users.md) | Users | 8 | 8 | ☐ | | [10](10-messages.md) | Messages & Search | 14 | 12 | ☐ | | [11](11-channels-and-members.md) | Channels, Members & Sync | 13 | 15 | ☐ | | [12](12-uploads-cdn.md) | Uploads (CDN) | 8 | 8 | ☑ | @@ -29,8 +29,9 @@ generated operations in scope, the decisions that group has to make, its risks, | [15](15-partial-updates.md) | Partial Updates — split out of 11 | 0 | 2 | ☑ | | [16](16-channel-lifecycle.md) | Channel Lifecycle — split out of 11 | 0 | 3 | ☑ | | [17](17-read-receipts.md) | Read Receipts — split out of 11 | 0 | 4 | ☑ | +| [18](18-unread-counts.md) | Unread Counts — split out of 09 | 0 | 1 | ☑ | -**Coverage:** 70 hand-written methods across 10 files, and all 129 generated operations, each claimed by exactly +**Coverage:** 69 hand-written methods across 10 files, and all 129 generated operations, each claimed by exactly one group. Verified mechanically — see [Keeping this plan honest](#keeping-this-plan-honest). @@ -176,6 +177,7 @@ surfaces before it reaches `Message` and `ChannelState`: `channel_mapper.dart` cannot map. - **17** is the read and delivery receipts, split out of 11 after 16. They answer only a `duration` and a read event, which waits for group 10's message mappers. +- **18** is the current user's unread counts, split out of 09 because it embeds no `User` and nothing persists it. - **12** comes late because it needs its own hand-written multipart client and is the highest-traffic path in the SDK. - **09** is last. Every group before it maps users through `user_mapper.dart` onto today's `User`; 09 migrates the diff --git a/openapi-migration/tool/generate_plan.py b/openapi-migration/tool/generate_plan.py index d8990e8c42..afacdc8025 100644 --- a/openapi-migration/tool/generate_plan.py +++ b/openapi-migration/tool/generate_plan.py @@ -516,9 +516,9 @@ def match(verb, path): dict( num='09', slug='users', title='Users', hand=['user_api.dart'], - match=owns('/api/v2/users', '/api/v2/chat/unread'), + match=owns('/api/v2/users'), goal='`User` is the most widely referenced public model in the SDK; this is where keep-vs-adopt costs the ' - 'most.', + 'most. [18](18-unread-counts.md) split the current user\'s unread counts out of it.', decisions=[ '`User` and `OwnUser` are public, persisted, and embedded in nearly every other response. This group ' 'restructures them, last: the mappers in `user_mapper.dart` already map the generated types onto the ' @@ -918,6 +918,32 @@ def match(verb, path): risks=[], done=DONE.replace('- [ ]', '- [x]'), ), + dict( + num='18', slug='unread-counts', title='Unread Counts', + hand=[], + match=only_ops('GET /api/v2/chat/unread'), + goal='Move reading the current user\'s unread counts ahead of [09](09-users.md): it takes no parameters, ' + 'answers only counts, and needs none of the `User` restructuring.', + decisions=[], + taken=textwrap.dedent("""\ + - **Split out of [09](09-users.md), ahead of it.** Nothing persists the response, and it embeds no + `User`. + - **The v2 route is the v1 handler.** `lib/chat/routes.go` mounts `/unread` and `/api/v2/chat/unread` + on the same `v1.UnreadCounts` in the shared `coreRoutes`. It is gated by + `ClassicUnreadCountsEnabled` on both, so the switch changes nothing about who may call it. It is + not in beta or deprecated. + - **Moved off `UserApi`:** `getUnreadCount`, now a `StreamChatClient` method over a new + `UsersRepository`, mirroring `UserApi`. + - **The v10 names stay:** `getUnreadCount` and `GetUnreadCountResponse`, rather than the spec's + `unreadCounts` and `WrappedUnreadCountsResponse`, which say nothing a caller needs. + - **`GetUnreadCountResponse` and the `UnreadCounts*` models are freezed plain models,** with v10's + fields and nullability; the generated types match them field for field. `duration` is non-null. + - **The current user's counts are updated only on success,** through `Result.onSuccess`, as v10 did + by throwing before it reached the update. + """), + risks=[], + done=DONE.replace('- [ ]', '- [x]'), + ), ] diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index af9146c7ec..9211a94289 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -98,6 +98,11 @@ - `StreamChatClient.markChannelsDelivered` returns a `Result` instead of throwing, and `MarkChannelsDelivered`, the callback `ChannelDeliveryReporter` takes, returns a `Future>`. - `MessageDelivery` no longer encodes to JSON, compares by value and gains `copyWith`. - `StreamChatApi.channel.markRead`, `markUnread`, `markUnreadByTimestamp`, `markThreadRead`, `markThreadUnread`, `markAllRead` and `markChannelsDelivered` are removed; call them on `StreamChatClient` instead. +- `StreamChatClient.getUnreadCount` returns a `Result` instead of throwing. +- `GetUnreadCountResponse` no longer decodes from JSON, is immutable, built through a const constructor, and its `duration` is a non-nullable `String`. +- `UnreadCountsChannel`, `UnreadCountsThread` and `UnreadCountsChannelType` no longer decode from or encode to JSON. +- `GetUnreadCountResponse`, `UnreadCountsChannel`, `UnreadCountsThread` and `UnreadCountsChannelType` compare by value and gain `copyWith`. +- `StreamChatApi.user.getUnreadCount` is removed; call `StreamChatClient.getUnreadCount` instead. 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 977e448616..13ffa623e4 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -64,6 +64,7 @@ import '../core/models/response/add_user_group_members_response.dart'; import '../core/models/response/app_settings_response.dart'; import '../core/models/response/create_user_group_response.dart'; import '../core/models/response/delete_channel_response.dart'; +import '../core/models/response/get_unread_count_response.dart'; import '../core/models/response/get_user_group_response.dart'; import '../core/models/response/hide_channel_response.dart'; import '../core/models/response/list_devices_response.dart'; @@ -94,6 +95,7 @@ import '../repository/general_repository.dart'; import '../repository/moderation_repository.dart'; import '../repository/roles_repository.dart'; import '../repository/user_groups_repository.dart'; +import '../repository/users_repository.dart'; import '../ws/connect_request.dart'; import '../ws/connection_manager.dart'; import '../ws/connection_status.dart'; @@ -195,6 +197,7 @@ class StreamChatClient { _rolesRepository = RolesRepository(api); _devicesRepository = DevicesRepository(api); _userGroupsRepository = UserGroupsRepository(api); + _usersRepository = UsersRepository(api); _generalRepository = GeneralRepository(api); _moderationRepository = ModerationRepository(api); _channelsRepository = ChannelsRepository(api); @@ -239,6 +242,7 @@ class StreamChatClient { late final RolesRepository _rolesRepository; late final DevicesRepository _devicesRepository; late final UserGroupsRepository _userGroupsRepository; + late final UsersRepository _usersRepository; late final GeneralRepository _generalRepository; late final ModerationRepository _moderationRepository; late final ChannelsRepository _channelsRepository; @@ -1846,21 +1850,22 @@ class StreamChatClient { } } - /// Returns the unread count information for the current user. - Future 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, + /// Gets how many unread messages and threads the current user has. + /// + /// On success, an event carrying the returned counts is added to [eventStream], which updates + /// [OwnUser.totalUnreadCount], [OwnUser.unreadChannels] and [OwnUser.unreadThreads] on [ClientState.currentUser]. + /// A failure adds no event and leaves them unchanged. + Future> getUnreadCount() async { + final result = await _usersRepository.getUnreadCount(); + return result.onSuccess( + (response) => handleEvent( + Event( + totalUnreadCount: response.totalUnreadCount, + unreadChannels: response.channels.length, + unreadThreads: response.threads.length, + ), ), ); - - return response; } /// Marks all of the current user's channels as read. diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 9e4234ad65..0c335a3a1b 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -17,7 +17,6 @@ import '../models/push_preference.dart'; import '../models/reaction.dart'; import '../models/read.dart'; import '../models/thread.dart'; -import '../models/unread_counts.dart'; import '../models/user.dart'; import '../models/user_block.dart'; @@ -621,31 +620,6 @@ class QueryRemindersResponse extends _BaseResponse { static QueryRemindersResponse fromJson(Map 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? totalUnreadCountByTeam; - - /// List of channels with unread messages - late List channels; - - /// Summary of unread counts grouped by channel type - late List channelType; - - /// List of threads with unread replies - late List threads; - - /// Create a new instance from a json - static GetUnreadCountResponse fromJson(Map json) => _$GetUnreadCountResponseFromJson(json); -} - /// Model response for [StreamChatClient.setPushPreferences] api call @JsonSerializable(createToJson: false) class UpsertPushPreferencesResponse extends _BaseResponse { 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 c1b22b4a20..8c1537d914 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -316,25 +316,6 @@ QueryRemindersResponse _$QueryRemindersResponseFromJson( [] ..next = json['next'] as String?; -GetUnreadCountResponse _$GetUnreadCountResponseFromJson( - Map json, -) => GetUnreadCountResponse() - ..duration = json['duration'] as String? - ..totalUnreadCount = (json['total_unread_count'] as num).toInt() - ..totalUnreadThreadsCount = (json['total_unread_threads_count'] as num).toInt() - ..totalUnreadCountByTeam = (json['total_unread_count_by_team'] as Map?)?.map( - (k, e) => MapEntry(k, (e as num).toInt()), - ) - ..channels = (json['channels'] as List) - .map((e) => UnreadCountsChannel.fromJson(e as Map)) - .toList() - ..channelType = (json['channel_type'] as List) - .map((e) => UnreadCountsChannelType.fromJson(e as Map)) - .toList() - ..threads = (json['threads'] as List) - .map((e) => UnreadCountsThread.fromJson(e as Map)) - .toList(); - UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson( Map json, ) => UpsertPushPreferencesResponse() diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index 44fb959526..257d14d872 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -92,13 +92,6 @@ class UserApi { return BlockedUsersResponse.fromJson(response.data); } - /// Requests the unread count information for the current user. - Future getUnreadCount() async { - final response = await _client.get('/unread'); - - return GetUnreadCountResponse.fromJson(response.data); - } - /// Retrieves all the active live locations of the current user. Future getActiveLiveLocations() async { final response = await _client.get( diff --git a/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart b/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart new file mode 100644 index 0000000000..ad011c2da4 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart @@ -0,0 +1,55 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../unread_counts_channel.dart'; +import '../unread_counts_channel_type.dart'; +import '../unread_counts_thread.dart'; + +part 'get_unread_count_response.freezed.dart'; + +/// How many unread messages and threads the current user has, in total and by channel, channel type and thread. +/// +/// Returned by [StreamChatClient.getUnreadCount]. +@freezed +class GetUnreadCountResponse with _$GetUnreadCountResponse { + /// Creates a new [GetUnreadCountResponse]. + const GetUnreadCountResponse({ + required this.duration, + required this.totalUnreadCount, + required this.totalUnreadThreadsCount, + this.totalUnreadCountByTeam, + required this.channels, + required this.channelType, + required this.threads, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The number of unread messages across all of the user's channels. + @override + final int totalUnreadCount; + + /// The number of threads with unread replies. + @override + final int totalUnreadThreadsCount; + + /// The number of unread messages in each of the user's teams, keyed by team. + /// + /// Null when unread messages are not counted per team, or the user belongs to no team. Otherwise each of the + /// user's teams has an entry, `0` when nothing in it is unread. + @override + final Map? totalUnreadCountByTeam; + + /// The channels that have unread messages. + @override + final List channels; + + /// The unread messages of each channel type that has any. + @override + final List channelType; + + /// The threads that have unread replies. + @override + final List threads; +} diff --git a/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.freezed.dart new file mode 100644 index 0000000000..cac9fbf780 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.freezed.dart @@ -0,0 +1,147 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'get_unread_count_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$GetUnreadCountResponse { + String get duration; + int get totalUnreadCount; + int get totalUnreadThreadsCount; + Map? get totalUnreadCountByTeam; + List get channels; + List get channelType; + List get threads; + + /// Create a copy of GetUnreadCountResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $GetUnreadCountResponseCopyWith get copyWith => + _$GetUnreadCountResponseCopyWithImpl( + this as GetUnreadCountResponse, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is GetUnreadCountResponse && + (identical(other.duration, duration) || other.duration == duration) && + (identical(other.totalUnreadCount, totalUnreadCount) || other.totalUnreadCount == totalUnreadCount) && + (identical( + other.totalUnreadThreadsCount, + totalUnreadThreadsCount, + ) || + other.totalUnreadThreadsCount == totalUnreadThreadsCount) && + const DeepCollectionEquality().equals( + other.totalUnreadCountByTeam, + totalUnreadCountByTeam, + ) && + const DeepCollectionEquality().equals(other.channels, channels) && + const DeepCollectionEquality().equals( + other.channelType, + channelType, + ) && + const DeepCollectionEquality().equals(other.threads, threads)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + duration, + totalUnreadCount, + totalUnreadThreadsCount, + const DeepCollectionEquality().hash(totalUnreadCountByTeam), + const DeepCollectionEquality().hash(channels), + const DeepCollectionEquality().hash(channelType), + const DeepCollectionEquality().hash(threads), + ); + + @override + String toString() { + return 'GetUnreadCountResponse(duration: $duration, totalUnreadCount: $totalUnreadCount, totalUnreadThreadsCount: $totalUnreadThreadsCount, totalUnreadCountByTeam: $totalUnreadCountByTeam, channels: $channels, channelType: $channelType, threads: $threads)'; + } +} + +/// @nodoc +abstract mixin class $GetUnreadCountResponseCopyWith<$Res> { + factory $GetUnreadCountResponseCopyWith( + GetUnreadCountResponse value, + $Res Function(GetUnreadCountResponse) _then, + ) = _$GetUnreadCountResponseCopyWithImpl; + @useResult + $Res call({ + String duration, + int totalUnreadCount, + int totalUnreadThreadsCount, + Map? totalUnreadCountByTeam, + List channels, + List channelType, + List threads, + }); +} + +/// @nodoc +class _$GetUnreadCountResponseCopyWithImpl<$Res> implements $GetUnreadCountResponseCopyWith<$Res> { + _$GetUnreadCountResponseCopyWithImpl(this._self, this._then); + + final GetUnreadCountResponse _self; + final $Res Function(GetUnreadCountResponse) _then; + + /// Create a copy of GetUnreadCountResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? duration = null, + Object? totalUnreadCount = null, + Object? totalUnreadThreadsCount = null, + Object? totalUnreadCountByTeam = freezed, + Object? channels = null, + Object? channelType = null, + Object? threads = null, + }) { + return _then( + GetUnreadCountResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + totalUnreadCount: null == totalUnreadCount + ? _self.totalUnreadCount + : totalUnreadCount // ignore: cast_nullable_to_non_nullable + as int, + totalUnreadThreadsCount: null == totalUnreadThreadsCount + ? _self.totalUnreadThreadsCount + : totalUnreadThreadsCount // ignore: cast_nullable_to_non_nullable + as int, + totalUnreadCountByTeam: freezed == totalUnreadCountByTeam + ? _self.totalUnreadCountByTeam + : totalUnreadCountByTeam // ignore: cast_nullable_to_non_nullable + as Map?, + channels: null == channels + ? _self.channels + : channels // ignore: cast_nullable_to_non_nullable + as List, + channelType: null == channelType + ? _self.channelType + : channelType // ignore: cast_nullable_to_non_nullable + as List, + threads: null == threads + ? _self.threads + : threads // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts.dart b/packages/stream_chat/lib/src/core/models/unread_counts.dart deleted file mode 100644 index 91a0b7dfe5..0000000000 --- a/packages/stream_chat/lib/src/core/models/unread_counts.dart +++ /dev/null @@ -1,92 +0,0 @@ -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 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 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 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 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 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 toJson() => _$UnreadCountsChannelTypeToJson(this); -} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts.g.dart b/packages/stream_chat/lib/src/core/models/unread_counts.g.dart deleted file mode 100644 index 941793799a..0000000000 --- a/packages/stream_chat/lib/src/core/models/unread_counts.g.dart +++ /dev/null @@ -1,51 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'unread_counts.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UnreadCountsChannel _$UnreadCountsChannelFromJson(Map json) => UnreadCountsChannel( - channelId: json['channel_id'] as String, - unreadCount: (json['unread_count'] as num).toInt(), - lastRead: DateTime.parse(json['last_read'] as String), -); - -Map _$UnreadCountsChannelToJson( - UnreadCountsChannel instance, -) => { - 'channel_id': instance.channelId, - 'unread_count': instance.unreadCount, - 'last_read': instance.lastRead.toIso8601String(), -}; - -UnreadCountsThread _$UnreadCountsThreadFromJson(Map json) => UnreadCountsThread( - unreadCount: (json['unread_count'] as num).toInt(), - lastRead: DateTime.parse(json['last_read'] as String), - lastReadMessageId: json['last_read_message_id'] as String, - parentMessageId: json['parent_message_id'] as String, -); - -Map _$UnreadCountsThreadToJson(UnreadCountsThread instance) => { - 'unread_count': instance.unreadCount, - 'last_read': instance.lastRead.toIso8601String(), - 'last_read_message_id': instance.lastReadMessageId, - 'parent_message_id': instance.parentMessageId, -}; - -UnreadCountsChannelType _$UnreadCountsChannelTypeFromJson( - Map json, -) => UnreadCountsChannelType( - channelType: json['channel_type'] as String, - channelCount: (json['channel_count'] as num).toInt(), - unreadCount: (json['unread_count'] as num).toInt(), -); - -Map _$UnreadCountsChannelTypeToJson( - UnreadCountsChannelType instance, -) => { - 'channel_type': instance.channelType, - 'channel_count': instance.channelCount, - 'unread_count': instance.unreadCount, -}; diff --git a/packages/stream_chat/lib/src/core/models/unread_counts_channel.dart b/packages/stream_chat/lib/src/core/models/unread_counts_channel.dart new file mode 100644 index 0000000000..fc6b7285dc --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/unread_counts_channel.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'unread_counts_channel.freezed.dart'; + +/// The current user's unread count in one channel. +/// +/// Listed in [GetUnreadCountResponse.channels]. +@freezed +class UnreadCountsChannel with _$UnreadCountsChannel { + /// Creates a new [UnreadCountsChannel]. + const UnreadCountsChannel({ + required this.channelId, + required this.unreadCount, + required this.lastRead, + }); + + /// The channel's cid, in the form `type:id` (for example `messaging:general`). + @override + final String channelId; + + /// The number of unread messages in the channel. + @override + final int unreadCount; + + /// The time the user last read the channel, or when they joined it if they never have. + @override + final DateTime lastRead; +} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts_channel.freezed.dart b/packages/stream_chat/lib/src/core/models/unread_counts_channel.freezed.dart new file mode 100644 index 0000000000..dfd4c5e82c --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/unread_counts_channel.freezed.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'unread_counts_channel.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UnreadCountsChannel { + String get channelId; + int get unreadCount; + DateTime get lastRead; + + /// Create a copy of UnreadCountsChannel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UnreadCountsChannelCopyWith get copyWith => + _$UnreadCountsChannelCopyWithImpl( + this as UnreadCountsChannel, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UnreadCountsChannel && + (identical(other.channelId, channelId) || other.channelId == channelId) && + (identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount) && + (identical(other.lastRead, lastRead) || other.lastRead == lastRead)); + } + + @override + int get hashCode => Object.hash(runtimeType, channelId, unreadCount, lastRead); + + @override + String toString() { + return 'UnreadCountsChannel(channelId: $channelId, unreadCount: $unreadCount, lastRead: $lastRead)'; + } +} + +/// @nodoc +abstract mixin class $UnreadCountsChannelCopyWith<$Res> { + factory $UnreadCountsChannelCopyWith( + UnreadCountsChannel value, + $Res Function(UnreadCountsChannel) _then, + ) = _$UnreadCountsChannelCopyWithImpl; + @useResult + $Res call({String channelId, int unreadCount, DateTime lastRead}); +} + +/// @nodoc +class _$UnreadCountsChannelCopyWithImpl<$Res> implements $UnreadCountsChannelCopyWith<$Res> { + _$UnreadCountsChannelCopyWithImpl(this._self, this._then); + + final UnreadCountsChannel _self; + final $Res Function(UnreadCountsChannel) _then; + + /// Create a copy of UnreadCountsChannel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channelId = null, + Object? unreadCount = null, + Object? lastRead = null, + }) { + return _then( + UnreadCountsChannel( + channelId: null == channelId + ? _self.channelId + : channelId // ignore: cast_nullable_to_non_nullable + as String, + unreadCount: null == unreadCount + ? _self.unreadCount + : unreadCount // ignore: cast_nullable_to_non_nullable + as int, + lastRead: null == lastRead + ? _self.lastRead + : lastRead // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts_channel_type.dart b/packages/stream_chat/lib/src/core/models/unread_counts_channel_type.dart new file mode 100644 index 0000000000..0cb8845d09 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/unread_counts_channel_type.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'unread_counts_channel_type.freezed.dart'; + +/// The current user's unread count across the channels of one channel type. +/// +/// Listed in [GetUnreadCountResponse.channelType]. +@freezed +class UnreadCountsChannelType with _$UnreadCountsChannelType { + /// Creates a new [UnreadCountsChannelType]. + const UnreadCountsChannelType({ + required this.channelType, + required this.channelCount, + required this.unreadCount, + }); + + /// The channel type, such as `messaging` or `livestream`. + @override + final String channelType; + + /// The number of channels of this type that have unread messages. + @override + final int channelCount; + + /// The number of unread messages across all channels of this type. + @override + final int unreadCount; +} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts_channel_type.freezed.dart b/packages/stream_chat/lib/src/core/models/unread_counts_channel_type.freezed.dart new file mode 100644 index 0000000000..da310dcc91 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/unread_counts_channel_type.freezed.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'unread_counts_channel_type.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UnreadCountsChannelType { + String get channelType; + int get channelCount; + int get unreadCount; + + /// Create a copy of UnreadCountsChannelType + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UnreadCountsChannelTypeCopyWith get copyWith => + _$UnreadCountsChannelTypeCopyWithImpl( + this as UnreadCountsChannelType, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UnreadCountsChannelType && + (identical(other.channelType, channelType) || other.channelType == channelType) && + (identical(other.channelCount, channelCount) || other.channelCount == channelCount) && + (identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount)); + } + + @override + int get hashCode => Object.hash(runtimeType, channelType, channelCount, unreadCount); + + @override + String toString() { + return 'UnreadCountsChannelType(channelType: $channelType, channelCount: $channelCount, unreadCount: $unreadCount)'; + } +} + +/// @nodoc +abstract mixin class $UnreadCountsChannelTypeCopyWith<$Res> { + factory $UnreadCountsChannelTypeCopyWith( + UnreadCountsChannelType value, + $Res Function(UnreadCountsChannelType) _then, + ) = _$UnreadCountsChannelTypeCopyWithImpl; + @useResult + $Res call({String channelType, int channelCount, int unreadCount}); +} + +/// @nodoc +class _$UnreadCountsChannelTypeCopyWithImpl<$Res> implements $UnreadCountsChannelTypeCopyWith<$Res> { + _$UnreadCountsChannelTypeCopyWithImpl(this._self, this._then); + + final UnreadCountsChannelType _self; + final $Res Function(UnreadCountsChannelType) _then; + + /// Create a copy of UnreadCountsChannelType + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channelType = null, + Object? channelCount = null, + Object? unreadCount = null, + }) { + return _then( + UnreadCountsChannelType( + channelType: null == channelType + ? _self.channelType + : channelType // ignore: cast_nullable_to_non_nullable + as String, + channelCount: null == channelCount + ? _self.channelCount + : channelCount // ignore: cast_nullable_to_non_nullable + as int, + unreadCount: null == unreadCount + ? _self.unreadCount + : unreadCount // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts_thread.dart b/packages/stream_chat/lib/src/core/models/unread_counts_thread.dart new file mode 100644 index 0000000000..a3353f882e --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/unread_counts_thread.dart @@ -0,0 +1,33 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'unread_counts_thread.freezed.dart'; + +/// The current user's unread count in one thread. +/// +/// Listed in [GetUnreadCountResponse.threads]. +@freezed +class UnreadCountsThread with _$UnreadCountsThread { + /// Creates a new [UnreadCountsThread]. + const UnreadCountsThread({ + required this.unreadCount, + required this.lastRead, + required this.lastReadMessageId, + required this.parentMessageId, + }); + + /// The number of unread replies in the thread. + @override + final int unreadCount; + + /// The time the user last read the thread, or when it was started if they never have. + @override + final DateTime lastRead; + + /// The id of the last reply the user read in the thread, or empty if they have not read one. + @override + final String lastReadMessageId; + + /// The id of the message the thread replies to. + @override + final String parentMessageId; +} diff --git a/packages/stream_chat/lib/src/core/models/unread_counts_thread.freezed.dart b/packages/stream_chat/lib/src/core/models/unread_counts_thread.freezed.dart new file mode 100644 index 0000000000..4a0c6b10ca --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/unread_counts_thread.freezed.dart @@ -0,0 +1,109 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'unread_counts_thread.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UnreadCountsThread { + int get unreadCount; + DateTime get lastRead; + String get lastReadMessageId; + String get parentMessageId; + + /// Create a copy of UnreadCountsThread + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UnreadCountsThreadCopyWith get copyWith => _$UnreadCountsThreadCopyWithImpl( + this as UnreadCountsThread, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UnreadCountsThread && + (identical(other.unreadCount, unreadCount) || other.unreadCount == unreadCount) && + (identical(other.lastRead, lastRead) || other.lastRead == lastRead) && + (identical(other.lastReadMessageId, lastReadMessageId) || other.lastReadMessageId == lastReadMessageId) && + (identical(other.parentMessageId, parentMessageId) || other.parentMessageId == parentMessageId)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + unreadCount, + lastRead, + lastReadMessageId, + parentMessageId, + ); + + @override + String toString() { + return 'UnreadCountsThread(unreadCount: $unreadCount, lastRead: $lastRead, lastReadMessageId: $lastReadMessageId, parentMessageId: $parentMessageId)'; + } +} + +/// @nodoc +abstract mixin class $UnreadCountsThreadCopyWith<$Res> { + factory $UnreadCountsThreadCopyWith( + UnreadCountsThread value, + $Res Function(UnreadCountsThread) _then, + ) = _$UnreadCountsThreadCopyWithImpl; + @useResult + $Res call({ + int unreadCount, + DateTime lastRead, + String lastReadMessageId, + String parentMessageId, + }); +} + +/// @nodoc +class _$UnreadCountsThreadCopyWithImpl<$Res> implements $UnreadCountsThreadCopyWith<$Res> { + _$UnreadCountsThreadCopyWithImpl(this._self, this._then); + + final UnreadCountsThread _self; + final $Res Function(UnreadCountsThread) _then; + + /// Create a copy of UnreadCountsThread + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? unreadCount = null, + Object? lastRead = null, + Object? lastReadMessageId = null, + Object? parentMessageId = null, + }) { + return _then( + UnreadCountsThread( + unreadCount: null == unreadCount + ? _self.unreadCount + : unreadCount // ignore: cast_nullable_to_non_nullable + as int, + lastRead: null == lastRead + ? _self.lastRead + : lastRead // ignore: cast_nullable_to_non_nullable + as DateTime, + lastReadMessageId: null == lastReadMessageId + ? _self.lastReadMessageId + : lastReadMessageId // ignore: cast_nullable_to_non_nullable + as String, + parentMessageId: null == parentMessageId + ? _self.parentMessageId + : parentMessageId // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart index 5a480ebb34..016ab65e98 100644 --- a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart @@ -2,6 +2,10 @@ import '../../../open_api/api.dart' as api; import '../../core/models/own_user.dart'; import '../../core/models/privacy_settings.dart'; import '../../core/models/response/create_guest_user_response.dart'; +import '../../core/models/response/get_unread_count_response.dart'; +import '../../core/models/unread_counts_channel.dart'; +import '../../core/models/unread_counts_channel_type.dart'; +import '../../core/models/unread_counts_thread.dart'; import '../../core/models/user.dart'; import '../../core/util/extension.dart'; @@ -91,3 +95,48 @@ extension CreateGuestResponseMapper on api.CreateGuestResponse { user: user.toModel(), ); } + +/// Maps a generated [api.WrappedUnreadCountsResponse] to a [GetUnreadCountResponse]. +extension WrappedUnreadCountsResponseMapper on api.WrappedUnreadCountsResponse { + /// Converts this response into a [GetUnreadCountResponse]. + GetUnreadCountResponse toModel() => GetUnreadCountResponse( + duration: duration, + totalUnreadCount: totalUnreadCount, + totalUnreadThreadsCount: totalUnreadThreadsCount, + totalUnreadCountByTeam: totalUnreadCountByTeam, + channels: channels.map((channel) => channel.toModel()).toList(), + channelType: channelType.map((type) => type.toModel()).toList(), + threads: threads.map((thread) => thread.toModel()).toList(), + ); +} + +/// Maps a generated [api.UnreadCountsChannel] to an [UnreadCountsChannel]. +extension UnreadCountsChannelMapper on api.UnreadCountsChannel { + /// Converts these counts into an [UnreadCountsChannel]. + UnreadCountsChannel toModel() => UnreadCountsChannel( + channelId: channelId, + unreadCount: unreadCount, + lastRead: lastRead, + ); +} + +/// Maps a generated [api.UnreadCountsThread] to an [UnreadCountsThread]. +extension UnreadCountsThreadMapper on api.UnreadCountsThread { + /// Converts these counts into an [UnreadCountsThread]. + UnreadCountsThread toModel() => UnreadCountsThread( + unreadCount: unreadCount, + lastRead: lastRead, + lastReadMessageId: lastReadMessageId, + parentMessageId: parentMessageId, + ); +} + +/// Maps a generated [api.UnreadCountsChannelType] to an [UnreadCountsChannelType]. +extension UnreadCountsChannelTypeMapper on api.UnreadCountsChannelType { + /// Converts these counts into an [UnreadCountsChannelType]. + UnreadCountsChannelType toModel() => UnreadCountsChannelType( + channelType: channelType, + channelCount: channelCount, + unreadCount: unreadCount, + ); +} diff --git a/packages/stream_chat/lib/src/repository/users_repository.dart b/packages/stream_chat/lib/src/repository/users_repository.dart new file mode 100644 index 0000000000..ac381ec194 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/users_repository.dart @@ -0,0 +1,19 @@ +import 'package:stream_core/stream_core.dart' show PatternMatching, Result; + +import '../../open_api/api.dart' as api; +import '../core/models/response/get_unread_count_response.dart'; +import 'mapper/user_mapper.dart'; + +/// Repository dedicated to user operations. +class UsersRepository { + /// Creates a new users repository. + const UsersRepository(this._api); + + final api.DefaultApi _api; + + /// Gets how many unread messages and threads the current user has. + Future> getUnreadCount() async { + final result = await _api.unreadCounts(); + return result.map((response) => response.toModel()); + } +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index ce12bc13d2..77ffe93c1b 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -125,6 +125,7 @@ export 'src/core/models/response/app_settings_response.dart'; export 'src/core/models/response/create_user_group_response.dart'; export 'src/core/models/response/delete_channel_response.dart'; export 'src/core/models/response/flag_response.dart'; +export 'src/core/models/response/get_unread_count_response.dart'; export 'src/core/models/response/get_user_group_response.dart'; export 'src/core/models/response/hide_channel_response.dart'; export 'src/core/models/response/list_devices_response.dart'; @@ -145,7 +146,9 @@ export 'src/core/models/role.dart'; export 'src/core/models/role_type.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/unread_counts_channel.dart'; +export 'src/core/models/unread_counts_channel_type.dart'; +export 'src/core/models/unread_counts_thread.dart'; export 'src/core/models/upload_config.dart'; export 'src/core/models/user.dart'; export 'src/core/models/user_block.dart'; diff --git a/packages/stream_chat/test/src/client/client_get_unread_count_test.dart b/packages/stream_chat/test/src/client/client_get_unread_count_test.dart new file mode 100644 index 0000000000..2d1efe5b82 --- /dev/null +++ b/packages/stream_chat/test/src/client/client_get_unread_count_test.dart @@ -0,0 +1,154 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/open_api/api.dart' as api; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + test('StreamChatClient.getUnreadCount returns the unread counts', () async { + final defaultApi = MockDefaultApi(); + when(defaultApi.unreadCounts).thenAnswer((_) async => Result.success(_unreadCountsResponse())); + final client = _client(defaultApi); + + final res = await client.getUnreadCount(); + + expect( + res.getOrNull(), + GetUnreadCountResponse( + duration: '4.21ms', + totalUnreadCount: 42, + totalUnreadThreadsCount: 8, + totalUnreadCountByTeam: const {'team-1': 15, 'team-2': 27}, + channels: [ + UnreadCountsChannel( + channelId: 'messaging:general', + unreadCount: 5, + lastRead: DateTime.utc(2024, 1, 15, 10, 30), + ), + ], + channelType: const [ + UnreadCountsChannelType(channelType: 'messaging', channelCount: 3, unreadCount: 25), + ], + threads: [ + UnreadCountsThread( + unreadCount: 7, + lastRead: DateTime.utc(2024, 1, 15, 9, 45), + lastReadMessageId: 'last-read-message-id', + parentMessageId: 'parent-message-id', + ), + ], + ), + ); + verify(defaultApi.unreadCounts).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test("StreamChatClient.getUnreadCount updates the current user's unread counts", () async { + final defaultApi = MockDefaultApi(); + when(defaultApi.unreadCounts).thenAnswer( + (_) async => Result.success( + _unreadCountsResponse( + totalUnreadCount: 25, + channels: [ + api.UnreadCountsChannel(channelId: 'messaging:one', lastRead: DateTime.utc(2024), unreadCount: 10), + api.UnreadCountsChannel(channelId: 'messaging:two', lastRead: DateTime.utc(2024), unreadCount: 15), + ], + threads: [ + api.UnreadCountsThread( + lastRead: DateTime.utc(2024), + lastReadMessageId: 'last-read-message-id', + parentMessageId: 'parent-message-id', + unreadCount: 3, + ), + ], + ), + ), + ); + final client = _clientWithOwnUser(defaultApi); + + await client.getUnreadCount(); + await pumpEventQueue(); + + final currentUser = client.state.currentUser!; + expect(currentUser.totalUnreadCount, 25); + expect(currentUser.unreadChannels, 2); + expect(currentUser.unreadThreads, 1); + }); + + test('StreamChatClient.getUnreadCount returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when(defaultApi.unreadCounts).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.getUnreadCount(); + + expect(res.exceptionOrNull(), error); + }); + + test("StreamChatClient.getUnreadCount leaves the current user's unread counts unchanged on failure", () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when(defaultApi.unreadCounts).thenAnswer((_) async => const Result.failure(error)); + final client = _clientWithOwnUser(defaultApi); + + await client.getUnreadCount(); + await pumpEventQueue(); + + final currentUser = client.state.currentUser!; + expect(currentUser.totalUnreadCount, 4); + expect(currentUser.unreadChannels, 3); + expect(currentUser.unreadThreads, 2); + }); +} + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} + +// A client whose current user starts with 4 unread messages in 3 channels and 2 unread threads. +StreamChatClient _clientWithOwnUser(api.DefaultApi defaultApi) { + final client = _client(defaultApi); + client.state + ..currentUser = OwnUser(id: 'user-id', totalUnreadCount: 4, unreadChannels: 3, unreadThreads: 2) + ..subscribeToEvents(); + return client; +} + +api.WrappedUnreadCountsResponse _unreadCountsResponse({ + int totalUnreadCount = 42, + List? channels, + List? threads, +}) { + return api.WrappedUnreadCountsResponse( + duration: '4.21ms', + totalUnreadCount: totalUnreadCount, + totalUnreadThreadsCount: 8, + totalUnreadCountByTeam: const {'team-1': 15, 'team-2': 27}, + channels: + channels ?? + [ + api.UnreadCountsChannel( + channelId: 'messaging:general', + lastRead: DateTime.utc(2024, 1, 15, 10, 30), + unreadCount: 5, + ), + ], + channelType: const [ + api.UnreadCountsChannelType(channelCount: 3, channelType: 'messaging', unreadCount: 25), + ], + threads: + threads ?? + [ + api.UnreadCountsThread( + lastRead: DateTime.utc(2024, 1, 15, 9, 45), + lastReadMessageId: 'last-read-message-id', + parentMessageId: 'parent-message-id', + unreadCount: 7, + ), + ], + ); +} diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index d88f60eb49..3fccb63b59 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -3591,100 +3591,6 @@ void main() { ); }); - test('`.getUnreadCount`', () async { - when(() => fakeChatApi.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(() => fakeChatApi.user.getUnreadCount()).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }); - - test( - '`.getUnreadCount` should also update user unread count as a side effect', - () async { - when(() => fakeChatApi.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(() => fakeChatApi.user.getUnreadCount()).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }, - ); - test('`.getActiveLiveLocations`', () async { final locations = [ Location( diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart index 7835e0d3ae..c5ef7bff44 100644 --- a/packages/stream_chat/test/src/core/api/user_api_test.dart +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -224,73 +224,6 @@ void main() { verifyNoMoreInteractions(client); }); - test('getUnreadCount', () async { - const path = '/unread'; - - when(() => client.get(path)).thenAnswer( - (_) async => successResponse( - path, - data: { - 'duration': '5.23ms', - 'total_unread_count': 42, - 'total_unread_threads_count': 8, - 'total_unread_count_by_team': {'team-1': 15, 'team-2': 27}, - 'channels': [ - { - 'channel_id': 'messaging:test-channel-1', - 'unread_count': 5, - 'last_read': '2024-01-15T10:30:00.000Z', - }, - { - 'channel_id': 'messaging:test-channel-2', - 'unread_count': 10, - 'last_read': '2024-01-15T09:15:00.000Z', - }, - ], - 'channel_type': [ - { - 'channel_type': 'messaging', - 'channel_count': 3, - 'unread_count': 25, - }, - { - 'channel_type': 'livestream', - 'channel_count': 1, - 'unread_count': 17, - }, - ], - 'threads': [ - { - 'unread_count': 3, - 'last_read': '2024-01-15T10:30:00.000Z', - 'last_read_message_id': 'message-1', - 'parent_message_id': 'parent-message-1', - }, - { - 'unread_count': 5, - 'last_read': '2024-01-15T09:45:00.000Z', - 'last_read_message_id': 'message-2', - 'parent_message_id': 'parent-message-2', - }, - ], - }, - ), - ); - - final res = await userApi.getUnreadCount(); - - expect(res, isNotNull); - expect(res.totalUnreadCount, 42); - expect(res.totalUnreadThreadsCount, 8); - expect(res.totalUnreadCountByTeam, {'team-1': 15, 'team-2': 27}); - expect(res.channels.length, 2); - expect(res.channelType.length, 2); - expect(res.threads.length, 2); - - verify(() => client.get(path)).called(1); - verifyNoMoreInteractions(client); - }); - test('getActiveLiveLocations', () async { const path = '/users/live_locations'; diff --git a/packages/stream_chat/test/src/core/models/unread_counts_test.dart b/packages/stream_chat/test/src/core/models/unread_counts_test.dart deleted file mode 100644 index 053b8db5f9..0000000000 --- a/packages/stream_chat/test/src/core/models/unread_counts_test.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:stream_chat/src/core/models/unread_counts.dart'; -import 'package:test/test.dart'; - -void main() { - group('UnreadCountsChannel', () { - const channelId = 'messaging:test-channel-id'; - const unreadCount = 5; - final lastRead = DateTime.parse('2024-01-15T10:30:00Z'); - - test('should parse json correctly', () { - final json = { - 'channel_id': channelId, - 'unread_count': unreadCount, - 'last_read': '2024-01-15T10:30:00.000Z', - }; - - final unreadCountsChannel = UnreadCountsChannel.fromJson(json); - - expect(unreadCountsChannel.channelId, channelId); - expect(unreadCountsChannel.unreadCount, unreadCount); - expect(unreadCountsChannel.lastRead, lastRead); - }); - - test('should serialize to json correctly', () { - final unreadCountsChannel = UnreadCountsChannel( - channelId: channelId, - unreadCount: unreadCount, - lastRead: lastRead, - ); - - final json = unreadCountsChannel.toJson(); - - expect(json['channel_id'], channelId); - expect(json['unread_count'], unreadCount); - expect(json['last_read'], lastRead.toIso8601String()); - }); - }); - - group('UnreadCountsThread', () { - const unreadCount = 3; - final lastRead = DateTime.parse('2024-01-15T10:30:00Z'); - const lastReadMessageId = 'last-read-message-id'; - const parentMessageId = 'parent-message-id'; - - test('should parse json correctly', () { - final json = { - 'unread_count': unreadCount, - 'last_read': '2024-01-15T10:30:00.000Z', - 'last_read_message_id': lastReadMessageId, - 'parent_message_id': parentMessageId, - }; - - final unreadCountsThread = UnreadCountsThread.fromJson(json); - - expect(unreadCountsThread.unreadCount, unreadCount); - expect(unreadCountsThread.lastRead, lastRead); - expect(unreadCountsThread.lastReadMessageId, lastReadMessageId); - expect(unreadCountsThread.parentMessageId, parentMessageId); - }); - - test('should serialize to json correctly', () { - final unreadCountsThread = UnreadCountsThread( - unreadCount: unreadCount, - lastRead: lastRead, - lastReadMessageId: lastReadMessageId, - parentMessageId: parentMessageId, - ); - - final json = unreadCountsThread.toJson(); - - expect(json['unread_count'], unreadCount); - expect(json['last_read'], lastRead.toIso8601String()); - expect(json['last_read_message_id'], lastReadMessageId); - expect(json['parent_message_id'], parentMessageId); - }); - }); - - group('UnreadCountsChannelType', () { - const channelType = 'messaging'; - const channelCount = 10; - const unreadCount = 25; - - test('should parse json correctly', () { - final json = { - 'channel_type': channelType, - 'channel_count': channelCount, - 'unread_count': unreadCount, - }; - - final unreadCountsChannelType = UnreadCountsChannelType.fromJson(json); - - expect(unreadCountsChannelType.channelType, channelType); - expect(unreadCountsChannelType.channelCount, channelCount); - expect(unreadCountsChannelType.unreadCount, unreadCount); - }); - - test('should serialize to json correctly', () { - const unreadCountsChannelType = UnreadCountsChannelType( - channelType: channelType, - channelCount: channelCount, - unreadCount: unreadCount, - ); - - final json = unreadCountsChannelType.toJson(); - - expect(json['channel_type'], channelType); - expect(json['channel_count'], channelCount); - expect(json['unread_count'], unreadCount); - }); - - test('should handle different channel types', () { - final channelTypes = ['messaging', 'livestream', 'team', 'commerce']; - - for (final type in channelTypes) { - final unreadCountsChannelType = UnreadCountsChannelType( - channelType: type, - channelCount: channelCount, - unreadCount: unreadCount, - ); - - expect(unreadCountsChannelType.channelType, type); - expect(unreadCountsChannelType.channelCount, channelCount); - expect(unreadCountsChannelType.unreadCount, unreadCount); - } - }); - }); -} From 314ebd8e4662dcbda8a727a15e7c21bf09f34549 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Wed, 7 Oct 2026 09:40:50 +0200 Subject: [PATCH 2/4] docs(llc): drop the event note from getUnreadCount's doc Co-Authored-By: Claude Opus 5.5 --- packages/stream_chat/lib/src/client/client.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 13ffa623e4..8ebc750319 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1851,10 +1851,6 @@ class StreamChatClient { } /// Gets how many unread messages and threads the current user has. - /// - /// On success, an event carrying the returned counts is added to [eventStream], which updates - /// [OwnUser.totalUnreadCount], [OwnUser.unreadChannels] and [OwnUser.unreadThreads] on [ClientState.currentUser]. - /// A failure adds no event and leaves them unchanged. Future> getUnreadCount() async { final result = await _usersRepository.getUnreadCount(); return result.onSuccess( From 4dd681f331d6eda9d5aec6d3ecae828096dac573 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Fri, 9 Oct 2026 16:31:03 +0200 Subject: [PATCH 3/4] refactor(llc): map the unread count lists like the other mappers Co-Authored-By: Claude Opus 5.5 --- .../stream_chat/lib/src/repository/mapper/user_mapper.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart index 016ab65e98..0c2ea1f46c 100644 --- a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart @@ -104,9 +104,9 @@ extension WrappedUnreadCountsResponseMapper on api.WrappedUnreadCountsResponse { totalUnreadCount: totalUnreadCount, totalUnreadThreadsCount: totalUnreadThreadsCount, totalUnreadCountByTeam: totalUnreadCountByTeam, - channels: channels.map((channel) => channel.toModel()).toList(), - channelType: channelType.map((type) => type.toModel()).toList(), - threads: threads.map((thread) => thread.toModel()).toList(), + channels: [for (final channel in channels) channel.toModel()], + channelType: [for (final type in channelType) type.toModel()], + threads: [for (final thread in threads) thread.toModel()], ); } From 6e1c995ea588e36a622229d4d663834fabf5fa5e Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Tue, 13 Oct 2026 11:58:26 +0200 Subject: [PATCH 4/4] docs(llc): document the getUnreadCount state update and fix the channelType doc Co-Authored-By: Claude Opus 5.5 --- packages/stream_chat/lib/src/client/client.dart | 3 +++ .../src/core/models/response/get_unread_count_response.dart | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 4ad1ca8c20..9503a89f40 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1851,6 +1851,9 @@ class StreamChatClient { } /// Gets how many unread messages and threads the current user has. + /// + /// On success, also updates [ClientState.totalUnreadCount], [ClientState.unreadChannels] and + /// [ClientState.unreadThreads]; a failure leaves them unchanged. Future> getUnreadCount() async { final result = await _usersRepository.getUnreadCount(); return result.onSuccess( diff --git a/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart b/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart index ad011c2da4..399f45e60b 100644 --- a/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart +++ b/packages/stream_chat/lib/src/core/models/response/get_unread_count_response.dart @@ -45,7 +45,7 @@ class GetUnreadCountResponse with _$GetUnreadCountResponse { @override final List channels; - /// The unread messages of each channel type that has any. + /// The channel types that have unread messages. @override final List channelType;