From a8fe9abb4187cfba0c796c235b69c83988f471e3 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Wed, 7 Oct 2026 10:41:00 +0200 Subject: [PATCH 1/3] refactor(llc)!: route user blocking through the generated client Co-Authored-By: Claude Opus 5.5 --- migrations/v11-migration.md | 56 +++++ openapi-migration/09-users.md | 10 +- openapi-migration/19-user-blocking.md | 65 ++++++ openapi-migration/README.md | 7 +- openapi-migration/tool/generate_plan.py | 36 ++- packages/stream_chat/CHANGELOG.md | 8 + .../stream_chat/lib/src/client/client.dart | 92 ++++---- .../lib/src/core/api/responses.dart | 30 --- .../lib/src/core/api/responses.g.dart | 13 -- .../lib/src/core/api/user_api.dart | 28 --- .../models/response/block_user_response.dart | 33 +++ .../response/block_user_response.freezed.dart | 109 +++++++++ .../response/get_blocked_users_response.dart | 25 ++ .../get_blocked_users_response.freezed.dart | 86 +++++++ .../response/unblock_user_response.dart | 18 ++ .../unblock_user_response.freezed.dart | 76 ++++++ .../lib/src/core/models/user_block.dart | 75 +++--- .../src/core/models/user_block.freezed.dart | 112 +++++++++ .../lib/src/core/models/user_block.g.dart | 23 -- .../src/repository/mapper/user_mapper.dart | 42 ++++ .../lib/src/repository/users_repository.dart | 21 ++ packages/stream_chat/lib/stream_chat.dart | 3 + .../stream_chat/test/fixtures/user_block.json | 26 --- .../src/client/client_block_user_test.dart | 101 ++++++++ .../client/client_get_blocked_users_test.dart | 108 +++++++++ .../test/src/client/client_test.dart | 217 ------------------ .../src/client/client_unblock_user_test.dart | 89 +++++++ .../test/src/core/api/responses_test.dart | 51 ---- .../test/src/core/api/user_api_test.dart | 87 ------- .../test/src/core/models/user_block_test.dart | 42 ---- 30 files changed, 1059 insertions(+), 630 deletions(-) create mode 100644 openapi-migration/19-user-blocking.md create mode 100644 packages/stream_chat/lib/src/core/models/response/block_user_response.dart create mode 100644 packages/stream_chat/lib/src/core/models/response/block_user_response.freezed.dart create mode 100644 packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.dart create mode 100644 packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.freezed.dart create mode 100644 packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart create mode 100644 packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart create mode 100644 packages/stream_chat/lib/src/core/models/user_block.freezed.dart delete mode 100644 packages/stream_chat/lib/src/core/models/user_block.g.dart delete mode 100644 packages/stream_chat/test/fixtures/user_block.json create mode 100644 packages/stream_chat/test/src/client/client_block_user_test.dart create mode 100644 packages/stream_chat/test/src/client/client_get_blocked_users_test.dart create mode 100644 packages/stream_chat/test/src/client/client_unblock_user_test.dart delete mode 100644 packages/stream_chat/test/src/core/models/user_block_test.dart diff --git a/migrations/v11-migration.md b/migrations/v11-migration.md index a503673824..fda3315444 100644 --- a/migrations/v11-migration.md +++ b/migrations/v11-migration.md @@ -37,6 +37,7 @@ onto Stream's OpenAPI-generated API client. - [Channel Lifecycle](#channel-lifecycle) - [Read Receipts](#read-receipts) - [Unread Counts](#unread-counts) + - [User Blocking](#user-blocking) - [Migration Checklist](#migration-checklist) - [For AI Agents](#for-ai-agents) - [Contributing to this guide](#contributing-to-this-guide) @@ -86,6 +87,7 @@ from the spec, so don't subclass them or depend on their private constructors. | [**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 | +| [**User Blocking**](#user-blocking) | `blockUser`, `unblockUser` and `getBlockedUsers` (was `queryBlockedUsers`) return a `Result` instead of throwing; their responses are renamed `BlockUserResponse` and `GetBlockedUsersResponse`, `unblockUser` answers a new `UnblockUserResponse`, and `UserBlock`'s fields are all non-nullable | | _(filled in per feature as PRs land)_ | | --- @@ -263,6 +265,19 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `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 | +| `StreamChatClient.blockUser` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The blocked user is still added to `OwnUser.blockedUserIds` on success | +| `StreamChatClient.unblockUser` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The user is still removed from `OwnUser.blockedUserIds` on success | +| `StreamChatClient.queryBlockedUsers` | `StreamChatClient.getBlockedUsers` | `renamed` | Same call, the API's name | +| `StreamChatClient.queryBlockedUsers` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. `OwnUser.blockedUserIds` is still replaced on success | +| `UserBlockResponse` | `BlockUserResponse` | `renamed` | Same fields | +| `BlockedUsersResponse` | `GetBlockedUsersResponse` | `renamed` | Same fields | +| `UserBlockResponse.fromJson`, `BlockedUsersResponse.fromJson`, `UserBlock.fromJson` / `toJson` | — | `removed` | The responses and model are plain classes; construct them directly | +| `UserBlockResponse()..blockedUserId = …`, `BlockedUsersResponse()..blocks = …` and their other setters | `BlockUserResponse(duration: …, blockedUserId: …, …)`, `GetBlockedUsersResponse(duration: …, blocks: …)` | `retyped` | Plain classes with a const constructor and final fields | +| `UserBlockResponse`, `BlockedUsersResponse` identity `==` | value `==`, plus `copyWith` | `retyped` | Two instances with the same fields are now equal | +| `UserBlockResponse.duration`, `BlockedUsersResponse.duration` (`String?`) | `String` | `retyped` | Always present; drop any `!` or `?? ''` | +| `UserBlock extends Equatable`, `UserBlock.props` | `UserBlock` (value `==`, `copyWith`) | `removed` | Equality is unchanged; `props` is gone and `UserBlock` is no longer an `Equatable` | +| `UserBlock.blockedUser` (`User?`), `.userId` / `.blockedUserId` (`String?`), `.createdAt` (`DateTime?`) | `User`, `String`, `DateTime` — required in the constructor | `retyped` | The server always sends them; drop any `!`, `?.` or `?? …` | +| `StreamChatApi.user.blockUser` / `unblockUser` / `queryBlockedUsers` | `StreamChatClient.blockUser` / `unblockUser` / `getBlockedUsers` | `removed` | The endpoints moved to the generated client | | _(more added per feature as PRs land)_ | | | | --- @@ -838,6 +853,47 @@ always present. > **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. +### User Blocking + +**`blockUser`, `unblockUser` and `getBlockedUsers` return a `Result` instead of throwing, and `queryBlockedUsers` +is renamed `getBlockedUsers`.** They answer `BlockUserResponse` (renamed from `UserBlockResponse`), +`UnblockUserResponse` (new; `unblockUser` returned an `EmptyResponse`) and `GetBlockedUsersResponse` (renamed from +`BlockedUsersResponse`). A `try`/`catch` around them still compiles, but no longer catches a failed call: read the +returned `Result` instead. On success they still update the current user's `blockedUserIds`; a failure leaves it as +it was. + +```dart +// v10 +try { + final response = await client.queryBlockedUsers(); + showBlocked(response.blocks.map((block) => block.blockedUser!)); +} on StreamChatException catch (e) { + report(e); +} + +// v11 +final result = await client.getBlockedUsers(); +switch (result) { + case Success(:final data): showBlocked(data.blocks.map((block) => block.blockedUser)); + case Failure(:final error): report(error); +} +``` + +**`UserBlock.blockedUser`, `userId`, `blockedUserId` and `createdAt` are non-nullable** and required in the +constructor; drop any `!`, `?.` or `?? …` on them. `UserBlock` no longer decodes from or encodes to JSON and no +longer extends `Equatable`; it still compares by value and keeps `copyWith`. `BlockUserResponse` and +`GetBlockedUsersResponse` no longer decode from JSON, are plain classes with a const constructor, compare by value +and gain `copyWith`, and their `duration` is always present. + +**`StreamChatApi.user.blockUser`, `unblockUser` and `queryBlockedUsers` are removed.** Call `blockUser`, +`unblockUser` and `getBlockedUsers` on `StreamChatClient`. + +> **Why:** the endpoints moved onto the generated client, which returns a `Result` for every call. `blockUser` and +> `unblockUser` keep their names and answer the API's responses in the singular, because each takes one user id. +> `queryBlockedUsers` becomes `getBlockedUsers`, the API's name, because the call takes no filter, sort or +> pagination. The server always sends every `UserBlock` field, so the model no longer makes callers handle nulls +> that never arrive. + ### 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 2fe91f4e16..f33ead4ebe 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. [18](18-unread-counts.md) split the current user's unread counts out of it. +**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, and [19](19-user-blocking.md) blocking users. -**Size:** 8 hand-written method(s) across 1 file(s) → 8 generated operation(s). +**Size:** 5 hand-written method(s) across 1 file(s) → 5 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. @@ -16,9 +16,6 @@ | `user_api.dart` | `queryUsers` | `QueryUsersResponse` | | `user_api.dart` | `updateUsers` | `UpdateUsersResponse` | | `user_api.dart` | `partialUpdateUsers` | `UpdateUsersResponse` | -| `user_api.dart` | `blockUser` | `UserBlockResponse` | -| `user_api.dart` | `unblockUser` | `EmptyResponse` | -| `user_api.dart` | `queryBlockedUsers` | `BlockedUsersResponse` | | `user_api.dart` | `getActiveLiveLocations` | `GetActiveLiveLocationsResponse` | | `user_api.dart` | `updateLiveLocation` | `Location` | @@ -26,11 +23,8 @@ | Verb | Path | Operation | Response | | --- | --- | --- | --- | -| `POST` | `/api/v2/users/block` | `blockUsers` | `BlockUsersResponse` | -| `GET` | `/api/v2/users/block` | `getBlockedUsers` | `GetBlockedUsersResponse` | | `GET` | `/api/v2/users/live_locations` | `getUserLiveLocations` | `SharedLocationsResponse` | | `GET` | `/api/v2/users` | `queryUsers` | `QueryUsersResponse` | -| `POST` | `/api/v2/users/unblock` | `unblockUsers` | `UnblockUsersResponse` | | `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/19-user-blocking.md b/openapi-migration/19-user-blocking.md new file mode 100644 index 0000000000..3a5059cf38 --- /dev/null +++ b/openapi-migration/19-user-blocking.md @@ -0,0 +1,65 @@ +# 19 — User Blocking + +**Goal:** Move blocking and unblocking a user, and listing the blocked users, ahead of [09](09-users.md): they embed `User` only through the existing `user_mapper.dart`, and nothing persists them. + +**Size:** 0 hand-written method(s) across 0 file(s) → 3 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 | +| --- | --- | --- | --- | +| `POST` | `/api/v2/users/block` | `blockUsers` | `BlockUsersResponse` | +| `GET` | `/api/v2/users/block` | `getBlockedUsers` | `GetBlockedUsersResponse` | +| `POST` | `/api/v2/users/unblock` | `unblockUsers` | `UnblockUsersResponse` | + +## Decisions taken + +- **Split out of [09](09-users.md), ahead of it, as one slice.** Blocking and unblocking are a pair, + and the list refreshes the same `OwnUser.blockedUserIds`. No WebSocket or persisted model embeds + `UserBlock`, so it needs no converter and no storage codec. +- **The v2 routes are the v1 handlers.** `lib/chat/routes.go` mounts one `userBlockRoutes` table + (`v1.BlockUsers`, `v1.UnblockUsers`, `v1.GetBlockedUsers`) both at the root and under `/api/v2/`; + only the JSON encoding differs. None is gated, in beta or deprecated. +- **Moved off `UserApi`:** `blockUser`, `unblockUser` and `queryBlockedUsers`, now `StreamChatClient` + methods over `UsersRepository`. +- **`blockUser` and `unblockUser` keep their names,** rather than the spec's `blockUsers` and + `unblockUsers`: each takes one user id. Their envelopes take the spec's names in the singular, + `BlockUserResponse` (renamed from `UserBlockResponse`) and `UnblockUserResponse` (new; v10 returned + `EmptyResponse`), since the generated `UnblockUsersResponse` is a named response. +- **`queryBlockedUsers` is renamed `getBlockedUsers`,** answering `GetBlockedUsersResponse` (renamed + from `BlockedUsersResponse`), as the spec names them: the call takes no filter, sort or pagination. +- **`UserBlock` is a freezed plain model with every field non-nullable,** following the wire rather + than v10, which typed `blockedUser`, `userId`, `blockedUserId` and `createdAt` nullable. The + server always sends them; the break is approved for this group. +- **`OwnUser.blockedUserIds` is updated only on success,** through `Result.onSuccess`, as v10 did by + throwing before it reached the update. Failures are still logged, through `Result.onFailure`. + +## 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 7e16e7dc31..18a0ca366c 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 | 8 | 8 | ☐ | +| [09](09-users.md) | Users | 5 | 5 | ☐ | | [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 | ☑ | @@ -30,8 +30,9 @@ generated operations in scope, the decisions that group has to make, its risks, | [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 | ☑ | +| [19](19-user-blocking.md) | User Blocking — split out of 09 | 0 | 3 | ☑ | -**Coverage:** 69 hand-written methods across 10 files, and all 129 generated operations, each claimed by exactly +**Coverage:** 66 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). @@ -178,6 +179,8 @@ surfaces before it reaches `Message` and `ChannelState`: - **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. +- **19** is blocking and unblocking users and listing the blocked ones, split out of 09 because it embeds `User` + only through the existing mappers 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 afacdc8025..c7c844c614 100644 --- a/openapi-migration/tool/generate_plan.py +++ b/openapi-migration/tool/generate_plan.py @@ -516,9 +516,10 @@ def match(verb, path): dict( num='09', slug='users', title='Users', hand=['user_api.dart'], - match=owns('/api/v2/users'), + match=owns('/api/v2/users', unless=('/api/v2/users/block', '/api/v2/users/unblock')), 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.', + 'most. [18](18-unread-counts.md) split the current user\'s unread counts out of it, and ' + '[19](19-user-blocking.md) blocking users.', 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 ' @@ -944,6 +945,37 @@ def match(verb, path): risks=[], done=DONE.replace('- [ ]', '- [x]'), ), + dict( + num='19', slug='user-blocking', title='User Blocking', + hand=[], + match=only_ops('POST /api/v2/users/block', 'GET /api/v2/users/block', 'POST /api/v2/users/unblock'), + goal='Move blocking and unblocking a user, and listing the blocked users, ahead of [09](09-users.md): they ' + 'embed `User` only through the existing `user_mapper.dart`, and nothing persists them.', + decisions=[], + taken=textwrap.dedent("""\ + - **Split out of [09](09-users.md), ahead of it, as one slice.** Blocking and unblocking are a pair, + and the list refreshes the same `OwnUser.blockedUserIds`. No WebSocket or persisted model embeds + `UserBlock`, so it needs no converter and no storage codec. + - **The v2 routes are the v1 handlers.** `lib/chat/routes.go` mounts one `userBlockRoutes` table + (`v1.BlockUsers`, `v1.UnblockUsers`, `v1.GetBlockedUsers`) both at the root and under `/api/v2/`; + only the JSON encoding differs. None is gated, in beta or deprecated. + - **Moved off `UserApi`:** `blockUser`, `unblockUser` and `queryBlockedUsers`, now `StreamChatClient` + methods over `UsersRepository`. + - **`blockUser` and `unblockUser` keep their names,** rather than the spec's `blockUsers` and + `unblockUsers`: each takes one user id. Their envelopes take the spec's names in the singular, + `BlockUserResponse` (renamed from `UserBlockResponse`) and `UnblockUserResponse` (new; v10 returned + `EmptyResponse`), since the generated `UnblockUsersResponse` is a named response. + - **`queryBlockedUsers` is renamed `getBlockedUsers`,** answering `GetBlockedUsersResponse` (renamed + from `BlockedUsersResponse`), as the spec names them: the call takes no filter, sort or pagination. + - **`UserBlock` is a freezed plain model with every field non-nullable,** following the wire rather + than v10, which typed `blockedUser`, `userId`, `blockedUserId` and `createdAt` nullable. The + server always sends them; the break is approved for this group. + - **`OwnUser.blockedUserIds` is updated only on success,** through `Result.onSuccess`, as v10 did by + throwing before it reached the update. Failures are still logged, through `Result.onFailure`. + """), + risks=[], + done=DONE.replace('- [ ]', '- [x]'), + ), ] diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 9211a94289..a9c3743569 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -103,6 +103,14 @@ - `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. +- `StreamChatClient.blockUser` returns a `Result` and `unblockUser` a `Result` instead of throwing. +- `StreamChatClient.queryBlockedUsers` is renamed `getBlockedUsers` and returns a `Result` instead of throwing. +- `UserBlockResponse` is renamed `BlockUserResponse`, and `BlockedUsersResponse` is renamed `GetBlockedUsersResponse`. +- `BlockUserResponse` and `GetBlockedUsersResponse` no longer decode from JSON, are immutable, built through a const constructor, and their `duration` is a non-nullable `String`. +- `BlockUserResponse` and `GetBlockedUsersResponse` compare by value and gain `copyWith`. +- `UserBlock` no longer decodes from or encodes to JSON and no longer extends `Equatable`. +- `UserBlock.blockedUser`, `userId`, `blockedUserId` and `createdAt` are required and non-nullable. +- `StreamChatApi.user.blockUser`, `unblockUser` and `queryBlockedUsers` are removed; call `blockUser`, `unblockUser` and `getBlockedUsers` on `StreamChatClient` instead. 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 8ebc750319..aaf1a112e1 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -62,8 +62,10 @@ import '../core/models/reaction.dart'; import '../core/models/request/message_delivery.dart'; import '../core/models/response/add_user_group_members_response.dart'; import '../core/models/response/app_settings_response.dart'; +import '../core/models/response/block_user_response.dart'; import '../core/models/response/create_user_group_response.dart'; import '../core/models/response/delete_channel_response.dart'; +import '../core/models/response/get_blocked_users_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'; @@ -76,6 +78,7 @@ import '../core/models/response/remove_user_group_members_response.dart'; import '../core/models/response/search_roles_response.dart'; import '../core/models/response/search_user_groups_response.dart'; import '../core/models/response/show_channel_response.dart'; +import '../core/models/response/unblock_user_response.dart'; import '../core/models/response/update_channel_partial_response.dart'; import '../core/models/response/update_member_partial_response.dart'; import '../core/models/response/update_user_group_response.dart'; @@ -1790,64 +1793,49 @@ class StreamChatClient { final _userBlockLock = Lock(); - /// Blocks a user with the provided [userId]. - Future blockUser(String userId) async { - try { - final response = await _userBlockLock.synchronized( - () => _chatApi.user.blockUser(userId), - ); - - final blockedUserId = response.blockedUserId; - final currentBlockedUserIds = [...?state.currentUser?.blockedUserIds]; - if (!currentBlockedUserIds.contains(blockedUserId)) { - // Add the new blocked user to the blocked user list. - state.blockedUserIds = [...currentBlockedUserIds, blockedUserId]; - } - - return response; - } catch (e, stk) { - logger.e(() => 'Error blocking user', error: e, stackTrace: stk); - rethrow; - } + /// Blocks the user with the given [userId] for the current user. + /// + /// On success, the blocked user's id is added to [OwnUser.blockedUserIds] on [ClientState.currentUser]. A failure + /// leaves it unchanged. + Future> blockUser(String userId) async { + final result = await _userBlockLock.synchronized(() => _usersRepository.blockUser(userId)); + return result + .onSuccess((response) => _addBlockedUserId(response.blockedUserId)) + .onFailure((e, stk) => logger.e(() => 'Error blocking user', error: e, stackTrace: stk)); } - /// Unblocks a previously blocked user with the provided [userId]. - Future unblockUser(String userId) async { - try { - final response = await _userBlockLock.synchronized( - () => _chatApi.user.unblockUser(userId), - ); - - final unblockedUserId = userId; - final currentBlockedUserIds = [...?state.currentUser?.blockedUserIds]; - if (currentBlockedUserIds.contains(unblockedUserId)) { - // Remove the unblocked user from the blocked user list. - state.blockedUserIds = currentBlockedUserIds..remove(unblockedUserId); - } - - return response; - } catch (e, stk) { - logger.e(() => 'Error unblocking user', error: e, stackTrace: stk); - rethrow; - } + /// Unblocks the user with the given [userId] for the current user. + /// + /// On success, [userId] is removed from [OwnUser.blockedUserIds] on [ClientState.currentUser]. A failure leaves it + /// unchanged. + Future> unblockUser(String userId) async { + final result = await _userBlockLock.synchronized(() => _usersRepository.unblockUser(userId)); + return result + .onSuccess((_) => _removeBlockedUserId(userId)) + .onFailure((e, stk) => logger.e(() => 'Error unblocking user', error: e, stackTrace: stk)); } - /// Retrieves a list of all users that the current user has blocked. - Future queryBlockedUsers() async { - try { - final response = await _userBlockLock.synchronized( - () => _chatApi.user.queryBlockedUsers(), - ); + /// Gets the users the current user has blocked. + /// + /// On success, [OwnUser.blockedUserIds] on [ClientState.currentUser] is replaced with the ids of the returned + /// users. A failure leaves it unchanged. + Future> getBlockedUsers() async { + final result = await _userBlockLock.synchronized(_usersRepository.getBlockedUsers); + return result + .onSuccess((response) => state.blockedUserIds = [for (final block in response.blocks) block.blockedUserId]) + .onFailure((e, stk) => logger.e(() => 'Error getting blocked users', error: e, stackTrace: stk)); + } - // Update the blocked user IDs with the latest data. - final blockedUserIds = response.blocks.map((it) => it.blockedUserId); - state.blockedUserIds = [...blockedUserIds.nonNulls]; + void _addBlockedUserId(String userId) { + final blockedUserIds = [...?state.currentUser?.blockedUserIds]; + if (blockedUserIds.contains(userId)) return; + state.blockedUserIds = [...blockedUserIds, userId]; + } - return response; - } catch (e, stk) { - logger.e(() => 'Error querying blocked users', error: e, stackTrace: stk); - rethrow; - } + void _removeBlockedUserId(String userId) { + final blockedUserIds = [...?state.currentUser?.blockedUserIds]; + if (!blockedUserIds.remove(userId)) return; + state.blockedUserIds = blockedUserIds; } /// Gets how many unread messages and threads the current user has. diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 0c335a3a1b..81bf723f4e 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -18,7 +18,6 @@ import '../models/reaction.dart'; import '../models/read.dart'; import '../models/thread.dart'; import '../models/user.dart'; -import '../models/user_block.dart'; part 'responses.g.dart'; @@ -381,35 +380,6 @@ class ChannelStateResponse extends _BaseResponse { static ChannelStateResponse fromJson(Map json) => _$ChannelStateResponseFromJson(json); } -/// Contains information about a [User] that was banned from a [Channel] or App. -@JsonSerializable(createToJson: false) -class UserBlockResponse extends _BaseResponse { - /// User that banned the [user]. - @JsonKey(defaultValue: '') - late String blockedByUserId; - - /// Reason for the ban. - @JsonKey(defaultValue: '') - late String blockedUserId; - - /// Timestamp when the [user] was banned. - late DateTime createdAt; - - /// Create a new instance from a json - static UserBlockResponse fromJson(Map json) => _$UserBlockResponseFromJson(json); -} - -/// Model response for [StreamChatClient.queryBlockedUsers] api call -@JsonSerializable(createToJson: false) -class BlockedUsersResponse extends _BaseResponse { - /// Updated users - @JsonKey(defaultValue: []) - late List blocks; - - /// Create a new instance from a json - static BlockedUsersResponse fromJson(Map json) => _$BlockedUsersResponseFromJson(json); -} - /// Model response for [StreamChatClient.createPoll] api call @JsonSerializable(createToJson: false) class CreatePollResponse 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 8c1537d914..81fe1ac9b9 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -188,19 +188,6 @@ ChannelStateResponse _$ChannelStateResponseFromJson( ..watcherCount = (json['watcher_count'] as num?)?.toInt() ?? 0 ..read = (json['read'] as List?)?.map((e) => Read.fromJson(e as Map)).toList() ?? []; -UserBlockResponse _$UserBlockResponseFromJson(Map json) => UserBlockResponse() - ..duration = json['duration'] as String? - ..blockedByUserId = json['blocked_by_user_id'] as String? ?? '' - ..blockedUserId = json['blocked_user_id'] as String? ?? '' - ..createdAt = DateTime.parse(json['created_at'] as String); - -BlockedUsersResponse _$BlockedUsersResponseFromJson( - Map json, -) => BlockedUsersResponse() - ..duration = json['duration'] as String? - ..blocks = - (json['blocks'] as List?)?.map((e) => UserBlock.fromJson(e as Map)).toList() ?? []; - CreatePollResponse _$CreatePollResponseFromJson(Map json) => CreatePollResponse() ..duration = json['duration'] as String? ..poll = Poll.fromJson(json['poll'] as Map); 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 257d14d872..8940d993fa 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -64,34 +64,6 @@ class UserApi { return UpdateUsersResponse.fromJson(response.data); } - /// Blocks a user - Future blockUser(String userId) async { - final response = await _client.post( - '/users/block', - data: {'blocked_user_id': userId}, - ); - - return UserBlockResponse.fromJson(response.data); - } - - /// Unblocks a user - Future unblockUser(String userId) async { - final response = await _client.post( - '/users/unblock', - data: {'blocked_user_id': userId}, - ); - return EmptyResponse.fromJson(response.data); - } - - /// Requests blocked users. - Future queryBlockedUsers() async { - final response = await _client.get( - '/users/block', - ); - - return BlockedUsersResponse.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/block_user_response.dart b/packages/stream_chat/lib/src/core/models/response/block_user_response.dart new file mode 100644 index 0000000000..f370929c6c --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/block_user_response.dart @@ -0,0 +1,33 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'block_user_response.freezed.dart'; + +/// The block the current user placed on another user. +/// +/// Returned by [StreamChatClient.blockUser]. +@freezed +class BlockUserResponse with _$BlockUserResponse { + /// Creates a new [BlockUserResponse]. + const BlockUserResponse({ + required this.duration, + required this.blockedByUserId, + required this.blockedUserId, + required this.createdAt, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The id of the user who placed the block. + @override + final String blockedByUserId; + + /// The id of the blocked user. + @override + final String blockedUserId; + + /// The time the block was placed. + @override + final DateTime createdAt; +} diff --git a/packages/stream_chat/lib/src/core/models/response/block_user_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/block_user_response.freezed.dart new file mode 100644 index 0000000000..b081158c37 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/block_user_response.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 'block_user_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$BlockUserResponse { + String get duration; + String get blockedByUserId; + String get blockedUserId; + DateTime get createdAt; + + /// Create a copy of BlockUserResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $BlockUserResponseCopyWith get copyWith => _$BlockUserResponseCopyWithImpl( + this as BlockUserResponse, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is BlockUserResponse && + (identical(other.duration, duration) || other.duration == duration) && + (identical(other.blockedByUserId, blockedByUserId) || other.blockedByUserId == blockedByUserId) && + (identical(other.blockedUserId, blockedUserId) || other.blockedUserId == blockedUserId) && + (identical(other.createdAt, createdAt) || other.createdAt == createdAt)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + duration, + blockedByUserId, + blockedUserId, + createdAt, + ); + + @override + String toString() { + return 'BlockUserResponse(duration: $duration, blockedByUserId: $blockedByUserId, blockedUserId: $blockedUserId, createdAt: $createdAt)'; + } +} + +/// @nodoc +abstract mixin class $BlockUserResponseCopyWith<$Res> { + factory $BlockUserResponseCopyWith( + BlockUserResponse value, + $Res Function(BlockUserResponse) _then, + ) = _$BlockUserResponseCopyWithImpl; + @useResult + $Res call({ + String duration, + String blockedByUserId, + String blockedUserId, + DateTime createdAt, + }); +} + +/// @nodoc +class _$BlockUserResponseCopyWithImpl<$Res> implements $BlockUserResponseCopyWith<$Res> { + _$BlockUserResponseCopyWithImpl(this._self, this._then); + + final BlockUserResponse _self; + final $Res Function(BlockUserResponse) _then; + + /// Create a copy of BlockUserResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? duration = null, + Object? blockedByUserId = null, + Object? blockedUserId = null, + Object? createdAt = null, + }) { + return _then( + BlockUserResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + blockedByUserId: null == blockedByUserId + ? _self.blockedByUserId + : blockedByUserId // ignore: cast_nullable_to_non_nullable + as String, + blockedUserId: null == blockedUserId + ? _self.blockedUserId + : blockedUserId // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _self.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.dart b/packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.dart new file mode 100644 index 0000000000..64e0535542 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../user_block.dart'; + +part 'get_blocked_users_response.freezed.dart'; + +/// The users the current user has blocked. +/// +/// Returned by [StreamChatClient.getBlockedUsers]. +@freezed +class GetBlockedUsersResponse with _$GetBlockedUsersResponse { + /// Creates a new [GetBlockedUsersResponse]. + const GetBlockedUsersResponse({ + required this.duration, + required this.blocks, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// One block for each user the current user has blocked. + @override + final List blocks; +} diff --git a/packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.freezed.dart new file mode 100644 index 0000000000..06a265ed4b --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/get_blocked_users_response.freezed.dart @@ -0,0 +1,86 @@ +// 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_blocked_users_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$GetBlockedUsersResponse { + String get duration; + List get blocks; + + /// Create a copy of GetBlockedUsersResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $GetBlockedUsersResponseCopyWith get copyWith => + _$GetBlockedUsersResponseCopyWithImpl( + this as GetBlockedUsersResponse, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is GetBlockedUsersResponse && + (identical(other.duration, duration) || other.duration == duration) && + const DeepCollectionEquality().equals(other.blocks, blocks)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + duration, + const DeepCollectionEquality().hash(blocks), + ); + + @override + String toString() { + return 'GetBlockedUsersResponse(duration: $duration, blocks: $blocks)'; + } +} + +/// @nodoc +abstract mixin class $GetBlockedUsersResponseCopyWith<$Res> { + factory $GetBlockedUsersResponseCopyWith( + GetBlockedUsersResponse value, + $Res Function(GetBlockedUsersResponse) _then, + ) = _$GetBlockedUsersResponseCopyWithImpl; + @useResult + $Res call({String duration, List blocks}); +} + +/// @nodoc +class _$GetBlockedUsersResponseCopyWithImpl<$Res> implements $GetBlockedUsersResponseCopyWith<$Res> { + _$GetBlockedUsersResponseCopyWithImpl(this._self, this._then); + + final GetBlockedUsersResponse _self; + final $Res Function(GetBlockedUsersResponse) _then; + + /// Create a copy of GetBlockedUsersResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null, Object? blocks = null}) { + return _then( + GetBlockedUsersResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + blocks: null == blocks + ? _self.blocks + : blocks // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart b/packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart new file mode 100644 index 0000000000..b581493e6f --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'unblock_user_response.freezed.dart'; + +/// The outcome of the current user unblocking another user. +/// +/// Returned by [StreamChatClient.unblockUser]. +@freezed +class UnblockUserResponse with _$UnblockUserResponse { + /// Creates a new [UnblockUserResponse]. + const UnblockUserResponse({ + required this.duration, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; +} diff --git a/packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart new file mode 100644 index 0000000000..4b003e3c88 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart @@ -0,0 +1,76 @@ +// 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 'unblock_user_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UnblockUserResponse { + String get duration; + + /// Create a copy of UnblockUserResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UnblockUserResponseCopyWith get copyWith => + _$UnblockUserResponseCopyWithImpl( + this as UnblockUserResponse, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UnblockUserResponse && + (identical(other.duration, duration) || other.duration == duration)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration); + + @override + String toString() { + return 'UnblockUserResponse(duration: $duration)'; + } +} + +/// @nodoc +abstract mixin class $UnblockUserResponseCopyWith<$Res> { + factory $UnblockUserResponseCopyWith( + UnblockUserResponse value, + $Res Function(UnblockUserResponse) _then, + ) = _$UnblockUserResponseCopyWithImpl; + @useResult + $Res call({String duration}); +} + +/// @nodoc +class _$UnblockUserResponseCopyWithImpl<$Res> implements $UnblockUserResponseCopyWith<$Res> { + _$UnblockUserResponseCopyWithImpl(this._self, this._then); + + final UnblockUserResponse _self; + final $Res Function(UnblockUserResponse) _then; + + /// Create a copy of UnblockUserResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null}) { + return _then( + UnblockUserResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/user_block.dart b/packages/stream_chat/lib/src/core/models/user_block.dart index 8b0c3b0624..33b874b20f 100644 --- a/packages/stream_chat/lib/src/core/models/user_block.dart +++ b/packages/stream_chat/lib/src/core/models/user_block.dart @@ -1,63 +1,40 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + import 'user.dart'; -part 'user_block.g.dart'; +part 'user_block.freezed.dart'; -/// Contains information about a [User] blocked from a [Channel] or App. -@JsonSerializable() -class UserBlock extends Equatable { - /// Creates a new instance of [UserBlock] +/// A block one [User] placed on another. +/// +/// Listed in [GetBlockedUsersResponse.blocks]. +@freezed +class UserBlock with _$UserBlock { + /// Creates a new [UserBlock]. const UserBlock({ required this.user, - this.blockedUser, - this.userId, - this.blockedUserId, - this.createdAt, + required this.blockedUser, + required this.userId, + required this.blockedUserId, + required this.createdAt, }); - /// Create a new instance from a json - factory UserBlock.fromJson(Map json) => _$UserBlockFromJson(json); - - /// User that blocked the [blockedUser]. + /// The user who placed the block. + @override final User user; - /// User that was blocked by the [user]. - final User? blockedUser; - - /// ID of the [user]. - final String? userId; - - /// ID of the [blockedUser]. - final String? blockedUserId; - - /// Timestamp when the [user] was blocked. - final DateTime? createdAt; + /// The user [user] blocked. + @override + final User blockedUser; - /// Serialize to json - Map toJson() => _$UserBlockToJson(this); + /// The id of [user]. + @override + final String userId; - /// Returns a copy of this object with the given fields updated. - UserBlock copyWith({ - User? user, - User? blockedUser, - String? userId, - String? blockedUserId, - DateTime? createdAt, - }) => UserBlock( - user: user ?? this.user, - blockedUser: blockedUser ?? this.blockedUser, - userId: userId ?? this.userId, - blockedUserId: blockedUserId ?? this.blockedUserId, - createdAt: createdAt ?? this.createdAt, - ); + /// The id of [blockedUser]. + @override + final String blockedUserId; + /// The time [user] blocked [blockedUser]. @override - List get props => [ - user, - blockedUser, - userId, - blockedUserId, - createdAt, - ]; + final DateTime createdAt; } diff --git a/packages/stream_chat/lib/src/core/models/user_block.freezed.dart b/packages/stream_chat/lib/src/core/models/user_block.freezed.dart new file mode 100644 index 0000000000..a4d5d6ab69 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/user_block.freezed.dart @@ -0,0 +1,112 @@ +// 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 'user_block.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UserBlock { + User get user; + User get blockedUser; + String get userId; + String get blockedUserId; + DateTime get createdAt; + + /// Create a copy of UserBlock + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UserBlockCopyWith get copyWith => _$UserBlockCopyWithImpl(this as UserBlock, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UserBlock && + (identical(other.user, user) || other.user == user) && + (identical(other.blockedUser, blockedUser) || other.blockedUser == blockedUser) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.blockedUserId, blockedUserId) || other.blockedUserId == blockedUserId) && + (identical(other.createdAt, createdAt) || other.createdAt == createdAt)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + user, + blockedUser, + userId, + blockedUserId, + createdAt, + ); + + @override + String toString() { + return 'UserBlock(user: $user, blockedUser: $blockedUser, userId: $userId, blockedUserId: $blockedUserId, createdAt: $createdAt)'; + } +} + +/// @nodoc +abstract mixin class $UserBlockCopyWith<$Res> { + factory $UserBlockCopyWith(UserBlock value, $Res Function(UserBlock) _then) = _$UserBlockCopyWithImpl; + @useResult + $Res call({ + User user, + User blockedUser, + String userId, + String blockedUserId, + DateTime createdAt, + }); +} + +/// @nodoc +class _$UserBlockCopyWithImpl<$Res> implements $UserBlockCopyWith<$Res> { + _$UserBlockCopyWithImpl(this._self, this._then); + + final UserBlock _self; + final $Res Function(UserBlock) _then; + + /// Create a copy of UserBlock + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? blockedUser = null, + Object? userId = null, + Object? blockedUserId = null, + Object? createdAt = null, + }) { + return _then( + UserBlock( + user: null == user + ? _self.user + : user // ignore: cast_nullable_to_non_nullable + as User, + blockedUser: null == blockedUser + ? _self.blockedUser + : blockedUser // ignore: cast_nullable_to_non_nullable + as User, + userId: null == userId + ? _self.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + blockedUserId: null == blockedUserId + ? _self.blockedUserId + : blockedUserId // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _self.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/user_block.g.dart b/packages/stream_chat/lib/src/core/models/user_block.g.dart deleted file mode 100644 index 6e36cb806f..0000000000 --- a/packages/stream_chat/lib/src/core/models/user_block.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user_block.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UserBlock _$UserBlockFromJson(Map json) => UserBlock( - user: User.fromJson(json['user'] as Map), - blockedUser: json['blocked_user'] == null ? null : User.fromJson(json['blocked_user'] as Map), - userId: json['user_id'] as String?, - blockedUserId: json['blocked_user_id'] as String?, - createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), -); - -Map _$UserBlockToJson(UserBlock instance) => { - 'user': instance.user.toJson(), - 'blocked_user': instance.blockedUser?.toJson(), - 'user_id': instance.userId, - 'blocked_user_id': instance.blockedUserId, - 'created_at': instance.createdAt?.toIso8601String(), -}; 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..76176332cd 100644 --- a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart @@ -1,12 +1,16 @@ import '../../../open_api/api.dart' as api; import '../../core/models/own_user.dart'; import '../../core/models/privacy_settings.dart'; +import '../../core/models/response/block_user_response.dart'; import '../../core/models/response/create_guest_user_response.dart'; +import '../../core/models/response/get_blocked_users_response.dart'; import '../../core/models/response/get_unread_count_response.dart'; +import '../../core/models/response/unblock_user_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/models/user_block.dart'; import '../../core/util/extension.dart'; // TODO(openapi-migration): re-point these mappers in group 09. @@ -140,3 +144,41 @@ extension UnreadCountsChannelTypeMapper on api.UnreadCountsChannelType { unreadCount: unreadCount, ); } + +/// Maps a generated [api.BlockUsersResponse] to a [BlockUserResponse]. +extension BlockUsersResponseMapper on api.BlockUsersResponse { + /// Converts this response into a [BlockUserResponse]. + BlockUserResponse toModel() => BlockUserResponse( + duration: duration, + blockedByUserId: blockedByUserId, + blockedUserId: blockedUserId, + createdAt: createdAt, + ); +} + +/// Maps a generated [api.UnblockUsersResponse] to an [UnblockUserResponse]. +extension UnblockUsersResponseMapper on api.UnblockUsersResponse { + /// Converts this response into an [UnblockUserResponse]. + UnblockUserResponse toModel() => UnblockUserResponse(duration: duration); +} + +/// Maps a generated [api.GetBlockedUsersResponse] to a [GetBlockedUsersResponse]. +extension GetBlockedUsersResponseMapper on api.GetBlockedUsersResponse { + /// Converts this response into a [GetBlockedUsersResponse]. + GetBlockedUsersResponse toModel() => GetBlockedUsersResponse( + duration: duration, + blocks: blocks.map((block) => block.toModel()).toList(), + ); +} + +/// Maps a generated [api.BlockedUserResponse] to a [UserBlock]. +extension BlockedUserResponseMapper on api.BlockedUserResponse { + /// Converts this block into a [UserBlock]. + UserBlock toModel() => UserBlock( + user: user.toModel(), + blockedUser: blockedUser.toModel(), + userId: userId, + blockedUserId: blockedUserId, + createdAt: createdAt, + ); +} diff --git a/packages/stream_chat/lib/src/repository/users_repository.dart b/packages/stream_chat/lib/src/repository/users_repository.dart index ac381ec194..bdd9b704cc 100644 --- a/packages/stream_chat/lib/src/repository/users_repository.dart +++ b/packages/stream_chat/lib/src/repository/users_repository.dart @@ -1,7 +1,10 @@ import 'package:stream_core/stream_core.dart' show PatternMatching, Result; import '../../open_api/api.dart' as api; +import '../core/models/response/block_user_response.dart'; +import '../core/models/response/get_blocked_users_response.dart'; import '../core/models/response/get_unread_count_response.dart'; +import '../core/models/response/unblock_user_response.dart'; import 'mapper/user_mapper.dart'; /// Repository dedicated to user operations. @@ -16,4 +19,22 @@ class UsersRepository { final result = await _api.unreadCounts(); return result.map((response) => response.toModel()); } + + /// Blocks the user with the given [userId] for the current user. + Future> blockUser(String userId) async { + final result = await _api.blockUsers(blockUsersRequest: api.BlockUsersRequest(blockedUserId: userId)); + return result.map((response) => response.toModel()); + } + + /// Unblocks the user with the given [userId] for the current user. + Future> unblockUser(String userId) async { + final result = await _api.unblockUsers(unblockUsersRequest: api.UnblockUsersRequest(blockedUserId: userId)); + return result.map((response) => response.toModel()); + } + + /// Gets the users the current user has blocked. + Future> getBlockedUsers() async { + final result = await _api.getBlockedUsers(); + 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 77ffe93c1b..b8a66628f7 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -122,9 +122,11 @@ export 'src/core/models/read.dart'; export 'src/core/models/request/message_delivery.dart'; export 'src/core/models/response/add_user_group_members_response.dart'; export 'src/core/models/response/app_settings_response.dart'; +export 'src/core/models/response/block_user_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_blocked_users_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'; @@ -138,6 +140,7 @@ export 'src/core/models/response/remove_user_group_members_response.dart'; export 'src/core/models/response/search_roles_response.dart'; export 'src/core/models/response/search_user_groups_response.dart'; export 'src/core/models/response/show_channel_response.dart'; +export 'src/core/models/response/unblock_user_response.dart'; export 'src/core/models/response/unmute_users_response.dart'; export 'src/core/models/response/update_channel_partial_response.dart'; export 'src/core/models/response/update_member_partial_response.dart'; diff --git a/packages/stream_chat/test/fixtures/user_block.json b/packages/stream_chat/test/fixtures/user_block.json deleted file mode 100644 index 124781307e..0000000000 --- a/packages/stream_chat/test/fixtures/user_block.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "user": { - "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "role": "test-role", - "name": "John", - "image": "https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow", - "banned": true, - "online": true, - "created_at": "2021-08-03 12:39:21.817646", - "updated_at": "2021-08-04 12:39:21.817646", - "last_active": "2021-08-05 12:39:21.817646" - }, - "blocked_user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "user_id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "blocked_user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "created_at": "2020-01-28T22:17:30.83015Z" -} \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/client_block_user_test.dart b/packages/stream_chat/test/src/client/client_block_user_test.dart new file mode 100644 index 0000000000..b9297f1105 --- /dev/null +++ b/packages/stream_chat/test/src/client/client_block_user_test.dart @@ -0,0 +1,101 @@ +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.blockUser sends the user id and returns the block', () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.blockUsers(blockUsersRequest: _request)).thenAnswer( + (_) async => Result.success(_blockUsersResponse()), + ); + final client = _client(defaultApi); + + final res = await client.blockUser(_blockedUserId); + + expect( + res.getOrNull(), + BlockUserResponse( + duration: '4.21ms', + blockedByUserId: 'user-id', + blockedUserId: _blockedUserId, + createdAt: DateTime.utc(2026, 10, 7, 9, 30), + ), + ); + verify(() => defaultApi.blockUsers(blockUsersRequest: _request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test("StreamChatClient.blockUser adds the blocked user to the current user's blocked user ids", () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.blockUsers(blockUsersRequest: _request)).thenAnswer( + (_) async => Result.success(_blockUsersResponse()), + ); + final client = _clientWithOwnUser(defaultApi, blockedUserIds: const ['other-user']); + + await client.blockUser(_blockedUserId); + + expect(client.state.currentUser!.blockedUserIds, ['other-user', _blockedUserId]); + }); + + test('StreamChatClient.blockUser does not add an already blocked user again', () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.blockUsers(blockUsersRequest: _request)).thenAnswer( + (_) async => Result.success(_blockUsersResponse()), + ); + final client = _clientWithOwnUser(defaultApi, blockedUserIds: const [_blockedUserId]); + + await client.blockUser(_blockedUserId); + + expect(client.state.currentUser!.blockedUserIds, [_blockedUserId]); + }); + + test('StreamChatClient.blockUser returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when(() => defaultApi.blockUsers(blockUsersRequest: _request)).thenAnswer( + (_) async => const Result.failure(error), + ); + final client = _client(defaultApi); + + final res = await client.blockUser(_blockedUserId); + + expect(res.exceptionOrNull(), error); + }); + + test("StreamChatClient.blockUser leaves the current user's blocked user ids unchanged on failure", () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.blockUsers(blockUsersRequest: _request)).thenAnswer( + (_) async => const Result.failure(StreamClientException(message: 'boom')), + ); + final client = _clientWithOwnUser(defaultApi, blockedUserIds: const ['other-user']); + + await client.blockUser(_blockedUserId); + + expect(client.state.currentUser!.blockedUserIds, ['other-user']); + }); +} + +const _blockedUserId = 'blocked-user-id'; +const _request = api.BlockUsersRequest(blockedUserId: _blockedUserId); + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} + +StreamChatClient _clientWithOwnUser(api.DefaultApi defaultApi, {required List blockedUserIds}) { + final client = _client(defaultApi); + client.state.currentUser = OwnUser(id: 'user-id', blockedUserIds: blockedUserIds); + return client; +} + +api.BlockUsersResponse _blockUsersResponse() => api.BlockUsersResponse( + duration: '4.21ms', + blockedByUserId: 'user-id', + blockedUserId: _blockedUserId, + createdAt: DateTime.utc(2026, 10, 7, 9, 30), +); diff --git a/packages/stream_chat/test/src/client/client_get_blocked_users_test.dart b/packages/stream_chat/test/src/client/client_get_blocked_users_test.dart new file mode 100644 index 0000000000..ea3bd5caf1 --- /dev/null +++ b/packages/stream_chat/test/src/client/client_get_blocked_users_test.dart @@ -0,0 +1,108 @@ +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 '../fakes.dart'; +import '../mocks.dart'; + +void main() { + test('StreamChatClient.getBlockedUsers returns the blocks', () async { + final defaultApi = MockDefaultApi(); + when(defaultApi.getBlockedUsers).thenAnswer((_) async => Result.success(_getBlockedUsersResponse())); + final client = _client(defaultApi); + + final res = await client.getBlockedUsers(); + + expect( + res.getOrNull(), + GetBlockedUsersResponse( + duration: '4.21ms', + blocks: [ + UserBlock( + user: fakeUser('user-id'), + blockedUser: fakeUser('blocked-user-1'), + userId: 'user-id', + blockedUserId: 'blocked-user-1', + createdAt: DateTime.utc(2026, 10, 1), + ), + UserBlock( + user: fakeUser('user-id'), + blockedUser: fakeUser('blocked-user-2'), + userId: 'user-id', + blockedUserId: 'blocked-user-2', + createdAt: DateTime.utc(2026, 10, 2), + ), + ], + ), + ); + verify(defaultApi.getBlockedUsers).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test("StreamChatClient.getBlockedUsers replaces the current user's blocked user ids", () async { + final defaultApi = MockDefaultApi(); + when(defaultApi.getBlockedUsers).thenAnswer((_) async => Result.success(_getBlockedUsersResponse())); + final client = _clientWithOwnUser(defaultApi); + + await client.getBlockedUsers(); + + expect(client.state.currentUser!.blockedUserIds, ['blocked-user-1', 'blocked-user-2']); + }); + + test('StreamChatClient.getBlockedUsers returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when(defaultApi.getBlockedUsers).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.getBlockedUsers(); + + expect(res.exceptionOrNull(), error); + }); + + test("StreamChatClient.getBlockedUsers leaves the current user's blocked user ids unchanged on failure", () async { + final defaultApi = MockDefaultApi(); + when(defaultApi.getBlockedUsers).thenAnswer( + (_) async => const Result.failure(StreamClientException(message: 'boom')), + ); + final client = _clientWithOwnUser(defaultApi); + + await client.getBlockedUsers(); + + expect(client.state.currentUser!.blockedUserIds, ['stale-blocked-user']); + }); +} + +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 a blocked user the server no longer reports. +StreamChatClient _clientWithOwnUser(api.DefaultApi defaultApi) { + final client = _client(defaultApi); + client.state.currentUser = OwnUser(id: 'user-id', blockedUserIds: const ['stale-blocked-user']); + return client; +} + +api.GetBlockedUsersResponse _getBlockedUsersResponse() => api.GetBlockedUsersResponse( + duration: '4.21ms', + blocks: [ + api.BlockedUserResponse( + user: fakeUserResponse('user-id'), + blockedUser: fakeUserResponse('blocked-user-1'), + userId: 'user-id', + blockedUserId: 'blocked-user-1', + createdAt: DateTime.utc(2026, 10, 1), + ), + api.BlockedUserResponse( + user: fakeUserResponse('user-id'), + blockedUser: fakeUserResponse('blocked-user-2'), + userId: 'user-id', + blockedUserId: 'blocked-user-2', + createdAt: DateTime.utc(2026, 10, 2), + ), + ], +); diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 3fccb63b59..59da17c622 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -3374,223 +3374,6 @@ void main() { verifyNoMoreInteractions(fakeChatApi.user); }); - test('`.blockUser`', () async { - const userId = 'test-user-id'; - - when(() => fakeChatApi.user.blockUser(userId)).thenAnswer( - (_) async => UserBlockResponse.fromJson({ - 'blocked_by_user_id': 'deven', - 'blocked_user_id': 'jaap', - 'created_at': '2024-10-01 12:45:23.456', - }), - ); - - final res = await client.blockUser(userId); - - expect(res, isNotNull); - - verify( - () => fakeChatApi.user.blockUser(userId), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }); - - test('`.unblockUser`', () async { - const userId = 'test-user-id'; - - when(() => fakeChatApi.user.unblockUser(userId)).thenAnswer((_) async => EmptyResponse()); - - final res = await client.unblockUser(userId); - - expect(res, isNotNull); - - verify( - () => fakeChatApi.user.unblockUser(userId), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }); - - test('`.queryBlockedUsers`', () async { - final users = List.generate( - 3, - (index) => User(id: 'test-user-id-$index'), - ); - - when(() => fakeChatApi.user.queryBlockedUsers()).thenAnswer( - (_) async => BlockedUsersResponse() - ..blocks = [ - UserBlock(user: users[0], blockedUser: users[1]), - UserBlock(user: users[0], blockedUser: users[2]), - ], - ); - - final res = await client.queryBlockedUsers(); - expect(res, isNotNull); - expect(res.blocks.length, 2); - - verify(() => fakeChatApi.user.queryBlockedUsers()).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }); - - group('Block user state management', () { - test('blockUser should update blockedUserIds on client state', () async { - final testUser = OwnUser(id: 'test-user'); - const userId = 'blocked-user-id'; - - // Verify initial state - expect(client.state.currentUser?.blockedUserIds, isEmpty); - - when(() => fakeChatApi.user.blockUser(userId)).thenAnswer( - (_) async => UserBlockResponse() - ..blockedUserId = userId - ..blockedByUserId = testUser.id - ..createdAt = DateTime.now(), - ); - - await client.blockUser(userId); - - // Verify - should now include the blocked user ID - expect(client.state.currentUser?.blockedUserIds, contains(userId)); - verify(() => fakeChatApi.user.blockUser(userId)).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }); - - test( - 'blockUser should not duplicate existing blocked user IDs', - () async { - const userId = 'blocked-user-id'; - client.state.blockedUserIds = const [userId]; - - // Verify the user is already in the blocked list - expect(client.state.currentUser?.blockedUserIds, contains(userId)); - - when(() => fakeChatApi.user.blockUser(userId)).thenAnswer( - (_) async => UserBlockResponse() - ..blockedUserId = userId - ..blockedByUserId = client.state.currentUser!.id - ..createdAt = DateTime.now(), - ); - - await client.blockUser(userId); - - // Verify - should still have only one entry - expect(client.state.currentUser?.blockedUserIds, contains(userId)); - expect(client.state.currentUser?.blockedUserIds.length, 1); - verify(() => fakeChatApi.user.blockUser(userId)).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }, - ); - - test('unblockUser should remove user from blockedUserIds', () async { - const blockedUserId = 'blocked-user-id'; - const otherBlockedId = 'other-blocked-id'; - client.state.blockedUserIds = const [blockedUserId, otherBlockedId]; - - // Verify initial state includes both blocked IDs - expect( - client.state.currentUser?.blockedUserIds, - containsAll([blockedUserId, otherBlockedId]), - ); - - when(() => fakeChatApi.user.unblockUser(blockedUserId)).thenAnswer( - (_) async => EmptyResponse(), - ); - - await client.unblockUser(blockedUserId); - - // Verify - blockedUserId should be removed - expect( - client.state.currentUser?.blockedUserIds, - contains(otherBlockedId), - ); - - expect( - client.state.currentUser?.blockedUserIds, - isNot(contains(blockedUserId)), - ); - - verify(() => fakeChatApi.user.unblockUser(blockedUserId)).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }); - - test( - 'unblockUser should be resilient if user ID not in blocked list', - () async { - const nonBlockedUserId = 'not-in-list'; - const otherBlockedId = 'other-blocked-id'; - client.state.blockedUserIds = const [otherBlockedId]; - - // Verify initial state - expect( - client.state.currentUser?.blockedUserIds, - contains(otherBlockedId), - ); - - expect( - client.state.currentUser?.blockedUserIds, - isNot(contains(nonBlockedUserId)), - ); - - when(() => fakeChatApi.user.unblockUser(nonBlockedUserId)).thenAnswer( - (_) async => EmptyResponse(), - ); - - await client.unblockUser(nonBlockedUserId); - - // Verify - should remain unchanged - expect(client.state.currentUser?.blockedUserIds, contains(otherBlockedId)); - expect(client.state.currentUser?.blockedUserIds, isNot(contains(nonBlockedUserId))); - verify(() => fakeChatApi.user.unblockUser(nonBlockedUserId)).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }, - ); - - test( - 'queryBlockedUsers should update client state with blockedUserIds', - () async { - const blockedId1 = 'blocked-1'; - const blockedId2 = 'blocked-2'; - - // Verify initial state - expect(client.state.currentUser?.blockedUserIds, isEmpty); - - // Create mock users - final blockedUser1 = User(id: 'blocked-user-1'); - final blockedUser2 = User(id: 'blocked-user-2'); - - // Mock the queryBlockedUsers API call - when(() => fakeChatApi.user.queryBlockedUsers()).thenAnswer( - (_) async => BlockedUsersResponse() - ..blocks = [ - UserBlock( - user: user, - userId: user.id, - blockedUser: blockedUser1, - blockedUserId: blockedId1, - ), - UserBlock( - user: user, - userId: user.id, - blockedUser: blockedUser2, - blockedUserId: blockedId2, - ), - ], - ); - - await client.queryBlockedUsers(); - - // Verify - should now include both blocked IDs - expect( - client.state.currentUser?.blockedUserIds, - containsAll([blockedId1, blockedId2]), - ); - - verify(() => fakeChatApi.user.queryBlockedUsers()).called(1); - verifyNoMoreInteractions(fakeChatApi.user); - }, - ); - }); - test('`.getActiveLiveLocations`', () async { final locations = [ Location( diff --git a/packages/stream_chat/test/src/client/client_unblock_user_test.dart b/packages/stream_chat/test/src/client/client_unblock_user_test.dart new file mode 100644 index 0000000000..fc936cdaa6 --- /dev/null +++ b/packages/stream_chat/test/src/client/client_unblock_user_test.dart @@ -0,0 +1,89 @@ +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.unblockUser sends the user id and returns the response', () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).thenAnswer( + (_) async => const Result.success(api.UnblockUsersResponse(duration: '4.21ms')), + ); + final client = _client(defaultApi); + + final res = await client.unblockUser(_blockedUserId); + + expect(res.getOrNull(), const UnblockUserResponse(duration: '4.21ms')); + verify(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test("StreamChatClient.unblockUser removes the user from the current user's blocked user ids", () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).thenAnswer( + (_) async => const Result.success(api.UnblockUsersResponse(duration: '4.21ms')), + ); + final client = _clientWithOwnUser(defaultApi, blockedUserIds: const [_blockedUserId, 'other-user']); + + await client.unblockUser(_blockedUserId); + + expect(client.state.currentUser!.blockedUserIds, ['other-user']); + }); + + test( + "StreamChatClient.unblockUser leaves the current user's blocked user ids unchanged when the user was not blocked", + () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).thenAnswer( + (_) async => const Result.success(api.UnblockUsersResponse(duration: '4.21ms')), + ); + final client = _clientWithOwnUser(defaultApi, blockedUserIds: const ['other-user']); + + await client.unblockUser(_blockedUserId); + + expect(client.state.currentUser!.blockedUserIds, ['other-user']); + }, + ); + + test('StreamChatClient.unblockUser returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).thenAnswer( + (_) async => const Result.failure(error), + ); + final client = _client(defaultApi); + + final res = await client.unblockUser(_blockedUserId); + + expect(res.exceptionOrNull(), error); + }); + + test("StreamChatClient.unblockUser leaves the current user's blocked user ids unchanged on failure", () async { + final defaultApi = MockDefaultApi(); + when(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).thenAnswer( + (_) async => const Result.failure(StreamClientException(message: 'boom')), + ); + final client = _clientWithOwnUser(defaultApi, blockedUserIds: const [_blockedUserId, 'other-user']); + + await client.unblockUser(_blockedUserId); + + expect(client.state.currentUser!.blockedUserIds, [_blockedUserId, 'other-user']); + }); +} + +const _blockedUserId = 'blocked-user-id'; +const _request = api.UnblockUsersRequest(blockedUserId: _blockedUserId); + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} + +StreamChatClient _clientWithOwnUser(api.DefaultApi defaultApi, {required List blockedUserIds}) { + final client = _client(defaultApi); + client.state.currentUser = OwnUser(id: 'user-id', blockedUserIds: blockedUserIds); + return client; +} diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index db68051eeb..3ba17b59d7 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -4326,57 +4326,6 @@ void main() { expect(response.message, isA()); }); - test('UserBlockResponse', () { - const jsonExample = ''' - { - "blocked_user_id":"c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "blocked_by_user_id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "created_at": "2020-01-28T22:17:30.83015Z" - } - '''; - final response = UserBlockResponse.fromJson(json.decode(jsonExample)); - expect(response.blockedUserId, isA()); - expect(response.blockedByUserId, isA()); - expect(response.createdAt, isA()); - }); - - test('BlockedUsersResponse', () { - const jsonExample = ''' - { - "blocks": [ - { - "user": { - "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "role": "test-role", - "name": "John", - "image": "https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow", - "banned": true, - "online": true, - "created_at": "2021-08-03 12:39:21.817646", - "updated_at": "2021-08-04 12:39:21.817646", - "last_active": "2021-08-05 12:39:21.817646" - }, - "blocked_user": { - "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "role": "user", - "created_at": "2020-01-28T22:17:30.83015Z", - "updated_at": "2020-01-28T22:17:31.19435Z", - "banned": false, - "online": false, - "image": "https://randomuser.me/api/portraits/women/2.jpg", - "name": "Mia Denys" - }, - "user_id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", - "blocked_user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", - "created_at": "2020-01-28T22:17:30.83015Z" - } - ] - } - '''; - final response = BlockedUsersResponse.fromJson(json.decode(jsonExample)); - expect(response.blocks, isA>()); - }); - test('UpsertPushPreferencesResponse', () { const jsonExample = ''' { 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 c5ef7bff44..f9c1997e9f 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 @@ -137,93 +137,6 @@ void main() { verifyNoMoreInteractions(client); }); - test('blockUser', () async { - const targetUserId = 'test-target-user-id'; - - const path = '/users/block'; - - when( - () => client.post( - path, - data: { - 'blocked_user_id': targetUserId, - }, - ), - ).thenAnswer( - (_) async => successResponse( - path, - data: { - 'blocked_by_user_id': 'deven', - 'blocked_user_id': 'jaap', - 'created_at': '2024-10-01 12:45:23.456', - }, - ), - ); - - final res = await userApi.blockUser(targetUserId); - - expect(res, isNotNull); - - verify( - () => client.post( - path, - data: { - 'blocked_user_id': targetUserId, - }, - ), - ).called(1); - verifyNoMoreInteractions(client); - }); - - test('unblockUser', () async { - const targetUserId = 'test-target-user-id'; - - const path = '/users/unblock'; - - when( - () => client.post( - path, - data: { - 'blocked_user_id': targetUserId, - }, - ), - ).thenAnswer( - (_) async => successResponse( - path, - data: {}, - ), - ); - - final res = await userApi.unblockUser(targetUserId); - - expect(res, isNotNull); - - verify( - () => client.post( - path, - data: { - 'blocked_user_id': targetUserId, - }, - ), - ).called(1); - verifyNoMoreInteractions(client); - }); - - test('queryBlockedUsers', () async { - const path = '/users/block'; - - when(() => client.get(path)).thenAnswer( - (_) async => successResponse(path, data: {}), - ); - - final res = await userApi.queryBlockedUsers(); - - expect(res, isNotNull); - - 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/user_block_test.dart b/packages/stream_chat/test/src/core/models/user_block_test.dart deleted file mode 100644 index 18162de5d7..0000000000 --- a/packages/stream_chat/test/src/core/models/user_block_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:stream_chat/src/core/models/user.dart'; -import 'package:stream_chat/src/core/models/user_block.dart'; -import 'package:test/test.dart'; - -import '../../utils.dart'; - -void main() { - group('src/models/user_block', () { - test('should parse json correctly', () { - final userBlock = UserBlock.fromJson(jsonFixture('user_block.json')); - expect(userBlock.userId, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); - expect(userBlock.blockedUserId, 'c1c9b454-2bcc-402d-8bb0-2f3706ce1680'); - expect( - userBlock.createdAt, - DateTime.parse('2020-01-28T22:17:30.83015Z'), - ); - expect(userBlock.user, isNotNull); - expect(userBlock.blockedUser, isNotNull); - }); - - test('should serialize to json correctly', () { - final userBlock = UserBlock( - user: User(id: 'user-1'), - blockedUser: User(id: 'user-2'), - userId: 'user-1', - blockedUserId: 'user-2', - createdAt: DateTime.parse('2020-01-28T22:17:30.830150Z'), - ); - - expect( - userBlock.toJson(), - { - 'user': {'id': 'user-1', 'teams': [], 'online': false, 'banned': false}, - 'blocked_user': {'id': 'user-2', 'teams': [], 'online': false, 'banned': false}, - 'user_id': 'user-1', - 'blocked_user_id': 'user-2', - 'created_at': '2020-01-28T22:17:30.830150Z', - }, - ); - }); - }); -} From 714ebec575dbd9614cd2167eb3946c8cc613cf90 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Wed, 7 Oct 2026 11:02:17 +0200 Subject: [PATCH 2/3] refactor(llc)!: name the block envelopes after the spec's plural responses Co-Authored-By: Claude Opus 5.5 --- migrations/v11-migration.md | 20 +++++----- openapi-migration/19-user-blocking.md | 8 ++-- openapi-migration/tool/generate_plan.py | 8 ++-- packages/stream_chat/CHANGELOG.md | 8 ++-- .../stream_chat/lib/src/client/client.dart | 8 ++-- ...esponse.dart => block_users_response.dart} | 8 ++-- ...dart => block_users_response.freezed.dart} | 36 +++++++++--------- ...ponse.dart => unblock_users_response.dart} | 8 ++-- ...rt => unblock_users_response.freezed.dart} | 38 +++++++++---------- .../src/repository/mapper/user_mapper.dart | 16 ++++---- .../lib/src/repository/users_repository.dart | 8 ++-- packages/stream_chat/lib/stream_chat.dart | 4 +- .../src/client/client_block_user_test.dart | 2 +- .../src/client/client_unblock_user_test.dart | 2 +- 14 files changed, 89 insertions(+), 85 deletions(-) rename packages/stream_chat/lib/src/core/models/response/{block_user_response.dart => block_users_response.dart} (79%) rename packages/stream_chat/lib/src/core/models/response/{block_user_response.freezed.dart => block_users_response.freezed.dart} (74%) rename packages/stream_chat/lib/src/core/models/response/{unblock_user_response.dart => unblock_users_response.dart} (64%) rename packages/stream_chat/lib/src/core/models/response/{unblock_user_response.freezed.dart => unblock_users_response.freezed.dart} (63%) diff --git a/migrations/v11-migration.md b/migrations/v11-migration.md index fda3315444..a1fda30e95 100644 --- a/migrations/v11-migration.md +++ b/migrations/v11-migration.md @@ -87,7 +87,7 @@ from the spec, so don't subclass them or depend on their private constructors. | [**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 | -| [**User Blocking**](#user-blocking) | `blockUser`, `unblockUser` and `getBlockedUsers` (was `queryBlockedUsers`) return a `Result` instead of throwing; their responses are renamed `BlockUserResponse` and `GetBlockedUsersResponse`, `unblockUser` answers a new `UnblockUserResponse`, and `UserBlock`'s fields are all non-nullable | +| [**User Blocking**](#user-blocking) | `blockUser`, `unblockUser` and `getBlockedUsers` (was `queryBlockedUsers`) return a `Result` instead of throwing; their responses are renamed `BlockUsersResponse` and `GetBlockedUsersResponse`, `unblockUser` answers a new `UnblockUsersResponse`, and `UserBlock`'s fields are all non-nullable | | _(filled in per feature as PRs land)_ | | --- @@ -265,14 +265,14 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `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 | -| `StreamChatClient.blockUser` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The blocked user is still added to `OwnUser.blockedUserIds` on success | -| `StreamChatClient.unblockUser` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The user is still removed from `OwnUser.blockedUserIds` on success | +| `StreamChatClient.blockUser` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The blocked user is still added to `OwnUser.blockedUserIds` on success | +| `StreamChatClient.unblockUser` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. The user is still removed from `OwnUser.blockedUserIds` on success | | `StreamChatClient.queryBlockedUsers` | `StreamChatClient.getBlockedUsers` | `renamed` | Same call, the API's name | | `StreamChatClient.queryBlockedUsers` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. `OwnUser.blockedUserIds` is still replaced on success | -| `UserBlockResponse` | `BlockUserResponse` | `renamed` | Same fields | +| `UserBlockResponse` | `BlockUsersResponse` | `renamed` | Same fields | | `BlockedUsersResponse` | `GetBlockedUsersResponse` | `renamed` | Same fields | | `UserBlockResponse.fromJson`, `BlockedUsersResponse.fromJson`, `UserBlock.fromJson` / `toJson` | — | `removed` | The responses and model are plain classes; construct them directly | -| `UserBlockResponse()..blockedUserId = …`, `BlockedUsersResponse()..blocks = …` and their other setters | `BlockUserResponse(duration: …, blockedUserId: …, …)`, `GetBlockedUsersResponse(duration: …, blocks: …)` | `retyped` | Plain classes with a const constructor and final fields | +| `UserBlockResponse()..blockedUserId = …`, `BlockedUsersResponse()..blocks = …` and their other setters | `BlockUsersResponse(duration: …, blockedUserId: …, …)`, `GetBlockedUsersResponse(duration: …, blocks: …)` | `retyped` | Plain classes with a const constructor and final fields | | `UserBlockResponse`, `BlockedUsersResponse` identity `==` | value `==`, plus `copyWith` | `retyped` | Two instances with the same fields are now equal | | `UserBlockResponse.duration`, `BlockedUsersResponse.duration` (`String?`) | `String` | `retyped` | Always present; drop any `!` or `?? ''` | | `UserBlock extends Equatable`, `UserBlock.props` | `UserBlock` (value `==`, `copyWith`) | `removed` | Equality is unchanged; `props` is gone and `UserBlock` is no longer an `Equatable` | @@ -856,8 +856,8 @@ always present. ### User Blocking **`blockUser`, `unblockUser` and `getBlockedUsers` return a `Result` instead of throwing, and `queryBlockedUsers` -is renamed `getBlockedUsers`.** They answer `BlockUserResponse` (renamed from `UserBlockResponse`), -`UnblockUserResponse` (new; `unblockUser` returned an `EmptyResponse`) and `GetBlockedUsersResponse` (renamed from +is renamed `getBlockedUsers`.** They answer `BlockUsersResponse` (renamed from `UserBlockResponse`), +`UnblockUsersResponse` (new; `unblockUser` returned an `EmptyResponse`) and `GetBlockedUsersResponse` (renamed from `BlockedUsersResponse`). A `try`/`catch` around them still compiles, but no longer catches a failed call: read the returned `Result` instead. On success they still update the current user's `blockedUserIds`; a failure leaves it as it was. @@ -881,7 +881,7 @@ switch (result) { **`UserBlock.blockedUser`, `userId`, `blockedUserId` and `createdAt` are non-nullable** and required in the constructor; drop any `!`, `?.` or `?? …` on them. `UserBlock` no longer decodes from or encodes to JSON and no -longer extends `Equatable`; it still compares by value and keeps `copyWith`. `BlockUserResponse` and +longer extends `Equatable`; it still compares by value and keeps `copyWith`. `BlockUsersResponse` and `GetBlockedUsersResponse` no longer decode from JSON, are plain classes with a const constructor, compare by value and gain `copyWith`, and their `duration` is always present. @@ -889,8 +889,8 @@ and gain `copyWith`, and their `duration` is always present. `unblockUser` and `getBlockedUsers` on `StreamChatClient`. > **Why:** the endpoints moved onto the generated client, which returns a `Result` for every call. `blockUser` and -> `unblockUser` keep their names and answer the API's responses in the singular, because each takes one user id. -> `queryBlockedUsers` becomes `getBlockedUsers`, the API's name, because the call takes no filter, sort or +> `unblockUser` keep their names, because each takes one user id, and answer the API's responses under the API's +> names. `queryBlockedUsers` becomes `getBlockedUsers`, the API's name, because the call takes no filter, sort or > pagination. The server always sends every `UserBlock` field, so the model no longer makes callers handle nulls > that never arrive. diff --git a/openapi-migration/19-user-blocking.md b/openapi-migration/19-user-blocking.md index 3a5059cf38..4249adadbe 100644 --- a/openapi-migration/19-user-blocking.md +++ b/openapi-migration/19-user-blocking.md @@ -33,9 +33,11 @@ - **Moved off `UserApi`:** `blockUser`, `unblockUser` and `queryBlockedUsers`, now `StreamChatClient` methods over `UsersRepository`. - **`blockUser` and `unblockUser` keep their names,** rather than the spec's `blockUsers` and - `unblockUsers`: each takes one user id. Their envelopes take the spec's names in the singular, - `BlockUserResponse` (renamed from `UserBlockResponse`) and `UnblockUserResponse` (new; v10 returned - `EmptyResponse`), since the generated `UnblockUsersResponse` is a named response. + `unblockUsers`: each takes one user id, and a method that takes several would be added beside them. +- **Their envelopes take the spec's plural names,** `BlockUsersResponse` (renamed from + `UserBlockResponse`) and `UnblockUsersResponse` (new; v10 returned `EmptyResponse`, but the generated + response is a named one). If the API starts blocking several users in one call, these responses gain + fields without a rename, and a batch method can return the same envelope. - **`queryBlockedUsers` is renamed `getBlockedUsers`,** answering `GetBlockedUsersResponse` (renamed from `BlockedUsersResponse`), as the spec names them: the call takes no filter, sort or pagination. - **`UserBlock` is a freezed plain model with every field non-nullable,** following the wire rather diff --git a/openapi-migration/tool/generate_plan.py b/openapi-migration/tool/generate_plan.py index c7c844c614..dd7d4fe241 100644 --- a/openapi-migration/tool/generate_plan.py +++ b/openapi-migration/tool/generate_plan.py @@ -962,9 +962,11 @@ def match(verb, path): - **Moved off `UserApi`:** `blockUser`, `unblockUser` and `queryBlockedUsers`, now `StreamChatClient` methods over `UsersRepository`. - **`blockUser` and `unblockUser` keep their names,** rather than the spec's `blockUsers` and - `unblockUsers`: each takes one user id. Their envelopes take the spec's names in the singular, - `BlockUserResponse` (renamed from `UserBlockResponse`) and `UnblockUserResponse` (new; v10 returned - `EmptyResponse`), since the generated `UnblockUsersResponse` is a named response. + `unblockUsers`: each takes one user id, and a method that takes several would be added beside them. + - **Their envelopes take the spec's plural names,** `BlockUsersResponse` (renamed from + `UserBlockResponse`) and `UnblockUsersResponse` (new; v10 returned `EmptyResponse`, but the generated + response is a named one). If the API starts blocking several users in one call, these responses gain + fields without a rename, and a batch method can return the same envelope. - **`queryBlockedUsers` is renamed `getBlockedUsers`,** answering `GetBlockedUsersResponse` (renamed from `BlockedUsersResponse`), as the spec names them: the call takes no filter, sort or pagination. - **`UserBlock` is a freezed plain model with every field non-nullable,** following the wire rather diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index a9c3743569..a402ae2f3b 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -103,11 +103,11 @@ - `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. -- `StreamChatClient.blockUser` returns a `Result` and `unblockUser` a `Result` instead of throwing. +- `StreamChatClient.blockUser` returns a `Result` and `unblockUser` a `Result` instead of throwing. - `StreamChatClient.queryBlockedUsers` is renamed `getBlockedUsers` and returns a `Result` instead of throwing. -- `UserBlockResponse` is renamed `BlockUserResponse`, and `BlockedUsersResponse` is renamed `GetBlockedUsersResponse`. -- `BlockUserResponse` and `GetBlockedUsersResponse` no longer decode from JSON, are immutable, built through a const constructor, and their `duration` is a non-nullable `String`. -- `BlockUserResponse` and `GetBlockedUsersResponse` compare by value and gain `copyWith`. +- `UserBlockResponse` is renamed `BlockUsersResponse`, and `BlockedUsersResponse` is renamed `GetBlockedUsersResponse`. +- `BlockUsersResponse` and `GetBlockedUsersResponse` no longer decode from JSON, are immutable, built through a const constructor, and their `duration` is a non-nullable `String`. +- `BlockUsersResponse` and `GetBlockedUsersResponse` compare by value and gain `copyWith`. - `UserBlock` no longer decodes from or encodes to JSON and no longer extends `Equatable`. - `UserBlock.blockedUser`, `userId`, `blockedUserId` and `createdAt` are required and non-nullable. - `StreamChatApi.user.blockUser`, `unblockUser` and `queryBlockedUsers` are removed; call `blockUser`, `unblockUser` and `getBlockedUsers` on `StreamChatClient` instead. diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index aaf1a112e1..a78ec4d2e8 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -62,7 +62,7 @@ import '../core/models/reaction.dart'; import '../core/models/request/message_delivery.dart'; import '../core/models/response/add_user_group_members_response.dart'; import '../core/models/response/app_settings_response.dart'; -import '../core/models/response/block_user_response.dart'; +import '../core/models/response/block_users_response.dart'; import '../core/models/response/create_user_group_response.dart'; import '../core/models/response/delete_channel_response.dart'; import '../core/models/response/get_blocked_users_response.dart'; @@ -78,7 +78,7 @@ import '../core/models/response/remove_user_group_members_response.dart'; import '../core/models/response/search_roles_response.dart'; import '../core/models/response/search_user_groups_response.dart'; import '../core/models/response/show_channel_response.dart'; -import '../core/models/response/unblock_user_response.dart'; +import '../core/models/response/unblock_users_response.dart'; import '../core/models/response/update_channel_partial_response.dart'; import '../core/models/response/update_member_partial_response.dart'; import '../core/models/response/update_user_group_response.dart'; @@ -1797,7 +1797,7 @@ class StreamChatClient { /// /// On success, the blocked user's id is added to [OwnUser.blockedUserIds] on [ClientState.currentUser]. A failure /// leaves it unchanged. - Future> blockUser(String userId) async { + Future> blockUser(String userId) async { final result = await _userBlockLock.synchronized(() => _usersRepository.blockUser(userId)); return result .onSuccess((response) => _addBlockedUserId(response.blockedUserId)) @@ -1808,7 +1808,7 @@ class StreamChatClient { /// /// On success, [userId] is removed from [OwnUser.blockedUserIds] on [ClientState.currentUser]. A failure leaves it /// unchanged. - Future> unblockUser(String userId) async { + Future> unblockUser(String userId) async { final result = await _userBlockLock.synchronized(() => _usersRepository.unblockUser(userId)); return result .onSuccess((_) => _removeBlockedUserId(userId)) diff --git a/packages/stream_chat/lib/src/core/models/response/block_user_response.dart b/packages/stream_chat/lib/src/core/models/response/block_users_response.dart similarity index 79% rename from packages/stream_chat/lib/src/core/models/response/block_user_response.dart rename to packages/stream_chat/lib/src/core/models/response/block_users_response.dart index f370929c6c..caed1e6765 100644 --- a/packages/stream_chat/lib/src/core/models/response/block_user_response.dart +++ b/packages/stream_chat/lib/src/core/models/response/block_users_response.dart @@ -1,14 +1,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -part 'block_user_response.freezed.dart'; +part 'block_users_response.freezed.dart'; /// The block the current user placed on another user. /// /// Returned by [StreamChatClient.blockUser]. @freezed -class BlockUserResponse with _$BlockUserResponse { - /// Creates a new [BlockUserResponse]. - const BlockUserResponse({ +class BlockUsersResponse with _$BlockUsersResponse { + /// Creates a new [BlockUsersResponse]. + const BlockUsersResponse({ required this.duration, required this.blockedByUserId, required this.blockedUserId, diff --git a/packages/stream_chat/lib/src/core/models/response/block_user_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/block_users_response.freezed.dart similarity index 74% rename from packages/stream_chat/lib/src/core/models/response/block_user_response.freezed.dart rename to packages/stream_chat/lib/src/core/models/response/block_users_response.freezed.dart index b081158c37..632ec399b3 100644 --- a/packages/stream_chat/lib/src/core/models/response/block_user_response.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/response/block_users_response.freezed.dart @@ -3,7 +3,7 @@ // 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 'block_user_response.dart'; +part of 'block_users_response.dart'; // ************************************************************************** // FreezedGenerator @@ -12,18 +12,18 @@ part of 'block_user_response.dart'; T _$identity(T value) => value; /// @nodoc -mixin _$BlockUserResponse { +mixin _$BlockUsersResponse { String get duration; String get blockedByUserId; String get blockedUserId; DateTime get createdAt; - /// Create a copy of BlockUserResponse + /// Create a copy of BlockUsersResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $BlockUserResponseCopyWith get copyWith => _$BlockUserResponseCopyWithImpl( - this as BlockUserResponse, + $BlockUsersResponseCopyWith get copyWith => _$BlockUsersResponseCopyWithImpl( + this as BlockUsersResponse, _$identity, ); @@ -31,7 +31,7 @@ mixin _$BlockUserResponse { bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is BlockUserResponse && + other is BlockUsersResponse && (identical(other.duration, duration) || other.duration == duration) && (identical(other.blockedByUserId, blockedByUserId) || other.blockedByUserId == blockedByUserId) && (identical(other.blockedUserId, blockedUserId) || other.blockedUserId == blockedUserId) && @@ -49,16 +49,16 @@ mixin _$BlockUserResponse { @override String toString() { - return 'BlockUserResponse(duration: $duration, blockedByUserId: $blockedByUserId, blockedUserId: $blockedUserId, createdAt: $createdAt)'; + return 'BlockUsersResponse(duration: $duration, blockedByUserId: $blockedByUserId, blockedUserId: $blockedUserId, createdAt: $createdAt)'; } } /// @nodoc -abstract mixin class $BlockUserResponseCopyWith<$Res> { - factory $BlockUserResponseCopyWith( - BlockUserResponse value, - $Res Function(BlockUserResponse) _then, - ) = _$BlockUserResponseCopyWithImpl; +abstract mixin class $BlockUsersResponseCopyWith<$Res> { + factory $BlockUsersResponseCopyWith( + BlockUsersResponse value, + $Res Function(BlockUsersResponse) _then, + ) = _$BlockUsersResponseCopyWithImpl; @useResult $Res call({ String duration, @@ -69,13 +69,13 @@ abstract mixin class $BlockUserResponseCopyWith<$Res> { } /// @nodoc -class _$BlockUserResponseCopyWithImpl<$Res> implements $BlockUserResponseCopyWith<$Res> { - _$BlockUserResponseCopyWithImpl(this._self, this._then); +class _$BlockUsersResponseCopyWithImpl<$Res> implements $BlockUsersResponseCopyWith<$Res> { + _$BlockUsersResponseCopyWithImpl(this._self, this._then); - final BlockUserResponse _self; - final $Res Function(BlockUserResponse) _then; + final BlockUsersResponse _self; + final $Res Function(BlockUsersResponse) _then; - /// Create a copy of BlockUserResponse + /// Create a copy of BlockUsersResponse /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override @@ -86,7 +86,7 @@ class _$BlockUserResponseCopyWithImpl<$Res> implements $BlockUserResponseCopyWit Object? createdAt = null, }) { return _then( - BlockUserResponse( + BlockUsersResponse( duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable diff --git a/packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart b/packages/stream_chat/lib/src/core/models/response/unblock_users_response.dart similarity index 64% rename from packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart rename to packages/stream_chat/lib/src/core/models/response/unblock_users_response.dart index b581493e6f..88c24338ff 100644 --- a/packages/stream_chat/lib/src/core/models/response/unblock_user_response.dart +++ b/packages/stream_chat/lib/src/core/models/response/unblock_users_response.dart @@ -1,14 +1,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -part 'unblock_user_response.freezed.dart'; +part 'unblock_users_response.freezed.dart'; /// The outcome of the current user unblocking another user. /// /// Returned by [StreamChatClient.unblockUser]. @freezed -class UnblockUserResponse with _$UnblockUserResponse { - /// Creates a new [UnblockUserResponse]. - const UnblockUserResponse({ +class UnblockUsersResponse with _$UnblockUsersResponse { + /// Creates a new [UnblockUsersResponse]. + const UnblockUsersResponse({ required this.duration, }); diff --git a/packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/unblock_users_response.freezed.dart similarity index 63% rename from packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart rename to packages/stream_chat/lib/src/core/models/response/unblock_users_response.freezed.dart index 4b003e3c88..f19a40d4ec 100644 --- a/packages/stream_chat/lib/src/core/models/response/unblock_user_response.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/response/unblock_users_response.freezed.dart @@ -3,7 +3,7 @@ // 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 'unblock_user_response.dart'; +part of 'unblock_users_response.dart'; // ************************************************************************** // FreezedGenerator @@ -12,16 +12,16 @@ part of 'unblock_user_response.dart'; T _$identity(T value) => value; /// @nodoc -mixin _$UnblockUserResponse { +mixin _$UnblockUsersResponse { String get duration; - /// Create a copy of UnblockUserResponse + /// Create a copy of UnblockUsersResponse /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $UnblockUserResponseCopyWith get copyWith => - _$UnblockUserResponseCopyWithImpl( - this as UnblockUserResponse, + $UnblockUsersResponseCopyWith get copyWith => + _$UnblockUsersResponseCopyWithImpl( + this as UnblockUsersResponse, _$identity, ); @@ -29,7 +29,7 @@ mixin _$UnblockUserResponse { bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is UnblockUserResponse && + other is UnblockUsersResponse && (identical(other.duration, duration) || other.duration == duration)); } @@ -38,34 +38,34 @@ mixin _$UnblockUserResponse { @override String toString() { - return 'UnblockUserResponse(duration: $duration)'; + return 'UnblockUsersResponse(duration: $duration)'; } } /// @nodoc -abstract mixin class $UnblockUserResponseCopyWith<$Res> { - factory $UnblockUserResponseCopyWith( - UnblockUserResponse value, - $Res Function(UnblockUserResponse) _then, - ) = _$UnblockUserResponseCopyWithImpl; +abstract mixin class $UnblockUsersResponseCopyWith<$Res> { + factory $UnblockUsersResponseCopyWith( + UnblockUsersResponse value, + $Res Function(UnblockUsersResponse) _then, + ) = _$UnblockUsersResponseCopyWithImpl; @useResult $Res call({String duration}); } /// @nodoc -class _$UnblockUserResponseCopyWithImpl<$Res> implements $UnblockUserResponseCopyWith<$Res> { - _$UnblockUserResponseCopyWithImpl(this._self, this._then); +class _$UnblockUsersResponseCopyWithImpl<$Res> implements $UnblockUsersResponseCopyWith<$Res> { + _$UnblockUsersResponseCopyWithImpl(this._self, this._then); - final UnblockUserResponse _self; - final $Res Function(UnblockUserResponse) _then; + final UnblockUsersResponse _self; + final $Res Function(UnblockUsersResponse) _then; - /// Create a copy of UnblockUserResponse + /// Create a copy of UnblockUsersResponse /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? duration = null}) { return _then( - UnblockUserResponse( + UnblockUsersResponse( duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable 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 76176332cd..e21488f28d 100644 --- a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart @@ -1,11 +1,11 @@ import '../../../open_api/api.dart' as api; import '../../core/models/own_user.dart'; import '../../core/models/privacy_settings.dart'; -import '../../core/models/response/block_user_response.dart'; +import '../../core/models/response/block_users_response.dart'; import '../../core/models/response/create_guest_user_response.dart'; import '../../core/models/response/get_blocked_users_response.dart'; import '../../core/models/response/get_unread_count_response.dart'; -import '../../core/models/response/unblock_user_response.dart'; +import '../../core/models/response/unblock_users_response.dart'; import '../../core/models/unread_counts_channel.dart'; import '../../core/models/unread_counts_channel_type.dart'; import '../../core/models/unread_counts_thread.dart'; @@ -145,10 +145,10 @@ extension UnreadCountsChannelTypeMapper on api.UnreadCountsChannelType { ); } -/// Maps a generated [api.BlockUsersResponse] to a [BlockUserResponse]. +/// Maps a generated [api.BlockUsersResponse] to a [BlockUsersResponse]. extension BlockUsersResponseMapper on api.BlockUsersResponse { - /// Converts this response into a [BlockUserResponse]. - BlockUserResponse toModel() => BlockUserResponse( + /// Converts this response into a [BlockUsersResponse]. + BlockUsersResponse toModel() => BlockUsersResponse( duration: duration, blockedByUserId: blockedByUserId, blockedUserId: blockedUserId, @@ -156,10 +156,10 @@ extension BlockUsersResponseMapper on api.BlockUsersResponse { ); } -/// Maps a generated [api.UnblockUsersResponse] to an [UnblockUserResponse]. +/// Maps a generated [api.UnblockUsersResponse] to an [UnblockUsersResponse]. extension UnblockUsersResponseMapper on api.UnblockUsersResponse { - /// Converts this response into an [UnblockUserResponse]. - UnblockUserResponse toModel() => UnblockUserResponse(duration: duration); + /// Converts this response into an [UnblockUsersResponse]. + UnblockUsersResponse toModel() => UnblockUsersResponse(duration: duration); } /// Maps a generated [api.GetBlockedUsersResponse] to a [GetBlockedUsersResponse]. diff --git a/packages/stream_chat/lib/src/repository/users_repository.dart b/packages/stream_chat/lib/src/repository/users_repository.dart index bdd9b704cc..8f9bf5e319 100644 --- a/packages/stream_chat/lib/src/repository/users_repository.dart +++ b/packages/stream_chat/lib/src/repository/users_repository.dart @@ -1,10 +1,10 @@ import 'package:stream_core/stream_core.dart' show PatternMatching, Result; import '../../open_api/api.dart' as api; -import '../core/models/response/block_user_response.dart'; +import '../core/models/response/block_users_response.dart'; import '../core/models/response/get_blocked_users_response.dart'; import '../core/models/response/get_unread_count_response.dart'; -import '../core/models/response/unblock_user_response.dart'; +import '../core/models/response/unblock_users_response.dart'; import 'mapper/user_mapper.dart'; /// Repository dedicated to user operations. @@ -21,13 +21,13 @@ class UsersRepository { } /// Blocks the user with the given [userId] for the current user. - Future> blockUser(String userId) async { + Future> blockUser(String userId) async { final result = await _api.blockUsers(blockUsersRequest: api.BlockUsersRequest(blockedUserId: userId)); return result.map((response) => response.toModel()); } /// Unblocks the user with the given [userId] for the current user. - Future> unblockUser(String userId) async { + Future> unblockUser(String userId) async { final result = await _api.unblockUsers(unblockUsersRequest: api.UnblockUsersRequest(blockedUserId: userId)); 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 b8a66628f7..81757467c4 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -122,7 +122,7 @@ export 'src/core/models/read.dart'; export 'src/core/models/request/message_delivery.dart'; export 'src/core/models/response/add_user_group_members_response.dart'; export 'src/core/models/response/app_settings_response.dart'; -export 'src/core/models/response/block_user_response.dart'; +export 'src/core/models/response/block_users_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'; @@ -140,7 +140,7 @@ export 'src/core/models/response/remove_user_group_members_response.dart'; export 'src/core/models/response/search_roles_response.dart'; export 'src/core/models/response/search_user_groups_response.dart'; export 'src/core/models/response/show_channel_response.dart'; -export 'src/core/models/response/unblock_user_response.dart'; +export 'src/core/models/response/unblock_users_response.dart'; export 'src/core/models/response/unmute_users_response.dart'; export 'src/core/models/response/update_channel_partial_response.dart'; export 'src/core/models/response/update_member_partial_response.dart'; diff --git a/packages/stream_chat/test/src/client/client_block_user_test.dart b/packages/stream_chat/test/src/client/client_block_user_test.dart index b9297f1105..dc54d408ac 100644 --- a/packages/stream_chat/test/src/client/client_block_user_test.dart +++ b/packages/stream_chat/test/src/client/client_block_user_test.dart @@ -17,7 +17,7 @@ void main() { expect( res.getOrNull(), - BlockUserResponse( + BlockUsersResponse( duration: '4.21ms', blockedByUserId: 'user-id', blockedUserId: _blockedUserId, diff --git a/packages/stream_chat/test/src/client/client_unblock_user_test.dart b/packages/stream_chat/test/src/client/client_unblock_user_test.dart index fc936cdaa6..e0b2ed83e6 100644 --- a/packages/stream_chat/test/src/client/client_unblock_user_test.dart +++ b/packages/stream_chat/test/src/client/client_unblock_user_test.dart @@ -15,7 +15,7 @@ void main() { final res = await client.unblockUser(_blockedUserId); - expect(res.getOrNull(), const UnblockUserResponse(duration: '4.21ms')); + expect(res.getOrNull(), const UnblockUsersResponse(duration: '4.21ms')); verify(() => defaultApi.unblockUsers(unblockUsersRequest: _request)).called(1); verifyNoMoreInteractions(defaultApi); }); From ecf52fe48bd0d6904548c7121a113801cd8289ae Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Fri, 9 Oct 2026 16:32:55 +0200 Subject: [PATCH 3/3] refactor(llc): map the blocked users list like the other mappers Co-Authored-By: Claude Opus 5.5 --- packages/stream_chat/lib/src/repository/mapper/user_mapper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8285edf00c..19b0c59e7f 100644 --- a/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/user_mapper.dart @@ -167,7 +167,7 @@ extension GetBlockedUsersResponseMapper on api.GetBlockedUsersResponse { /// Converts this response into a [GetBlockedUsersResponse]. GetBlockedUsersResponse toModel() => GetBlockedUsersResponse( duration: duration, - blocks: blocks.map((block) => block.toModel()).toList(), + blocks: [for (final block in blocks) block.toModel()], ); }