diff --git a/.claude/skills/openapi-migration/SKILL.md b/.claude/skills/openapi-migration/SKILL.md index 4447b9e876..834ca71ae4 100644 --- a/.claude/skills/openapi-migration/SKILL.md +++ b/.claude/skills/openapi-migration/SKILL.md @@ -187,10 +187,13 @@ a public signature. **Per type, decide:** -- **Which v10 type it maps to, and under what name.** The v10 name is the default, even where the generated one - differs (`OGAttachmentResponse` for `GetOGResponse`, `AppSettings` for `AppResponseFields`). It is not a - requirement: the user may ask for another name, and you may propose one that fits clearly better. Renaming a - v10 type is a break, so propose it with the reason and wait for approval. +- **Which v10 type it maps to, and under what name.** The spec's name is the default, unless it is awkward or + describes something else, and sibling calls share one scheme: `UpdateChannelPartialResponse` and + `UpdateMemberPartialResponse` replaced v10's `PartialUpdateChannelResponse` and `PartialUpdateMemberResponse`, + and `updateMemberPartial` replaced `partialMemberUpdate`. v10's name stays where the generated one does not fit + (`OGAttachmentResponse` for `GetOGResponse`, `AppSettings` for `AppResponseFields`), and names an earlier group + settled stand. Renaming a v10 type or method is a break: list it with the reason, wait for approval, and call it + out in the PR description. - **Which fields it exposes.** v10's fields, with v10's defaults. A field only the generated type has (`AppResponseFields.id`, the twelve extra fields on `GetOGResponse`) stays out; exposing it later is additive. A field the spec marks optional is nullable by default, even if v10 typed it non-null; keeping it non-null needs a @@ -251,7 +254,7 @@ packages/stream_chat/lib/src/ - **The role decides the folder.** An envelope goes in `response/` (`CreateUserGroupResponse`). A type the caller passes in to shape a request goes in `request/`, whatever its name: `PartialUpdateUserRequest`, and also - `PaginationParams`, `ThreadOptions` and `MemberUpdatePayload`. Every other model stays in `models/` + `PaginationParams` and `ThreadOptions`. Every other model stays in `models/` (`UserGroup`). No `request/` type exists yet: the first arrives when a group migrates one out of `lib/src/core/api/requests.dart`. - **Models and envelopes** are `@freezed` classes with a const constructor and `@override final` fields. Copy @@ -344,10 +347,10 @@ Three data-shape traps while mapping: - **The model's `custom` is the wire's `custom`, verbatim** (README rule 9). A migrated model names it `custom` — `extraData` on one whose group has not run — and the mapper copies the generated map into it and sends it back as `custom`; never filter either. A typed wire field our model lacks is added as a field, with - approval, or left out — never parked in `extraData` behind a getter, as `ChannelModel.hidden` and `User.name` - still are until groups 11 and 09 promote them. A v1 decoder splits flat JSON by a key list — `topLevelFields` - while the model still has JSON, a private list beside the converter once it does not — and a v1 writer leaves a - custom entry named like a key out so it cannot overwrite the key. + approval, or left out — never parked in `extraData` behind a getter, as `User.name` still is until group 09 + promotes it. A v1 decoder splits flat JSON by a key list — `topLevelFields` while the model still has JSON, a + private list beside the converter once it does not — and a v1 writer leaves a custom entry named like a key out + so it cannot overwrite the key. - **Units on anything that takes a `Duration`.** The wire carries a bare integer and the field name rarely says in what. Read the Go payload type: `commonpayloads.DurationInMinutes` and `DurationMilliseconds` sit on sibling endpoints in the same feature — `ban.timeout` and `mute.timeout` are minutes, `muteChannel.expiration` is diff --git a/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart b/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart index 66db36946e..73f6fc787e 100644 --- a/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart +++ b/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart @@ -23,7 +23,7 @@ GetMessageResponse _makeSearchResult({ ..channel = ChannelModel( id: 'ch-$messageId', type: 'messaging', - extraData: {'name': channelName}, + custom: {'name': channelName}, ); return response; } diff --git a/docs/docs_screenshots/test/src/mocks.dart b/docs/docs_screenshots/test/src/mocks.dart index 73095cc83a..f773a37b99 100644 --- a/docs/docs_screenshots/test/src/mocks.dart +++ b/docs/docs_screenshots/test/src/mocks.dart @@ -155,8 +155,8 @@ void setupMockChannel({ when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); when(() => channel.isPinned).thenReturn(false); when(() => channel.isPinnedStream).thenAnswer((_) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': channelName})); - when(() => channel.extraData).thenReturn({'name': channelName}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': channelName})); + when(() => channel.custom).thenReturn({'name': channelName}); when(() => channel.name).thenReturn(channelName); when(() => channel.nameStream).thenAnswer((_) => Stream.value(channelName)); when(() => channel.image).thenReturn(null); diff --git a/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart b/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart index 6c998e9860..95a2ab4006 100644 --- a/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart +++ b/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart @@ -41,7 +41,7 @@ Thread _makeThread({ channel: ChannelModel( id: id, type: 'messaging', - extraData: {'name': channelName}, + custom: {'name': channelName}, ), parentMessage: parentMessage, latestReplies: [latestReply], diff --git a/migrations/v11-migration.md b/migrations/v11-migration.md index f691a00331..6e703d207a 100644 --- a/migrations/v11-migration.md +++ b/migrations/v11-migration.md @@ -34,6 +34,8 @@ onto Stream's OpenAPI-generated API client. - [File Upload](#file-upload) - [User Groups](#user-groups) - [Link Previews](#link-previews) + - [Partial Updates](#partial-updates) + - [Channel and Member Custom Data](#channel-and-member-custom-data) - [Migration Checklist](#migration-checklist) - [For AI Agents](#for-ai-agents) - [Contributing to this guide](#contributing-to-this-guide) @@ -80,6 +82,8 @@ from the spec, so don't subclass them or depend on their private constructors. | [**Guest Users**](#guest-users) | `connectGuestUser` is unchanged; `StreamChatApi.guest` and `ConnectGuestUserResponse` are removed | | [**Polls**](#polls) | Poll calls return a `Result`; eight responses become `PollResponse`, `PollOptionResponse` and `PollVoteResponse`; queries take `limit`/`next`/`prev`; `VotingVisibility` is an extension type; `extraData` is `custom` | | [**File Upload**](#file-upload) | Uploads return a `Result` and deletes a `Result` instead of throwing; `AttachmentFileUploaderProvider` receives a `Dio` | +| [**Partial Updates**](#partial-updates) | Channel and member partial updates — `updatePartial`, `updateName`, `updateImage`, slow mode, pin and archive — return a `Result` instead of throwing; their responses take the API's names, `UpdateChannelPartialResponse` and `UpdateMemberPartialResponse`, and `partialMemberUpdate` becomes `updateMemberPartial` | +| [**Channel and Member Custom Data**](#channel-and-member-custom-data) | `extraData` is `custom` on `ChannelModel`, `Member` and `Channel`, and holds the custom data verbatim; the channel and member flags it carried are fields | | _(filled in per feature as PRs land)_ | | --- @@ -121,7 +125,7 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `SortOption.ASC` / `.DESC` | `SortDirection.asc` / `.desc` | `retyped` | An enum carrying `value` (`1` / `-1`) rather than a bare `int` | | `const [SortOption.desc(f)]` | `[ChannelSort.desc(f)]` | `retyped` | **Drop the `const`.** A sort list can no longer be `const`: a `SortField` holds a closure that reads the value off the model, which is not a constant expression. Every v10 example wrote `const`, so expect this on the first line you touch | | `ChannelSortKey` / `MessageSortKey` / `UserSortKey` / … (extension types over `String`) | `ChannelSortField` / `MessageSearchSortField` / `UserSortField` / … | `retyped` | Same member names. A field is no longer a `String`: read `field.remote` for the wire name. `MessageSortKey` is named for its one endpoint: searching | -| `SortOption.desc('my_custom_field')` | `ChannelSort.desc(ChannelSortField.custom('my_custom_field'))` | `retyped` | Only for a field the model does not declare, and only on channel, member, user and message-search sorts — the other queries pin their sort to declared fields and reject a custom one. `custom` reads `extraData`, so pointing it at a declared field sends the right wire name but reads `null` locally — an offline or re-sorted list comes back unordered | +| `SortOption.desc('my_custom_field')` | `ChannelSort.desc(ChannelSortField.custom('my_custom_field'))` | `retyped` | Only for a field the model does not declare, and only on channel, member, user and message-search sorts — the other queries pin their sort to declared fields and reject a custom one. `custom` reads the model's custom data, so pointing it at a declared field sends the right wire name but reads `null` locally — an offline or re-sorted list comes back unordered | | `SortOrder` | `List`, `List`, … | `removed` | The typedef is gone; signatures name the model's sort type | | `SortOption.fromJson` | `ChannelSort.fromJson` | `moved` | `Sort` has no `fromJson`: the remote name has to resolve back to a declared field | | `SortOption(comparator:)` | — | `removed` | Declare a field whose value projects onto something orderable, or sort the list yourself | @@ -241,6 +245,28 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `SendAttachmentResponse`, `SendFileResponse`, `SendImageResponse`, `UploadImageResponse`, `UploadFileResponse` | `UploadedFile` (`stream_core`) | `removed` | `.file` becomes `.fileUrl`; `.thumbUrl` is unchanged; `duration` is gone | | `AttachmentFileUploaderProvider` = `AttachmentFileUploader Function(StreamHttpClient)` | `AttachmentFileUploader Function(Dio)` | `retyped` | Receives the client's `Dio`; `StreamAttachmentFileUploader(dio)` builds the default | | `StreamChatApi.fileUploader`, `StreamChatApi(attachmentFileUploaderProvider:)` | `StreamChatClient(attachmentFileUploaderProvider:)` | `removed` | The uploader belongs to `StreamChatClient` | +| `PartialUpdateChannelResponse` | `UpdateChannelPartialResponse` | `renamed` | The name of the API's response. Same fields: `duration`, `channel` and `members` | +| `PartialUpdateMemberResponse` | `UpdateMemberPartialResponse` | `renamed` | The name of the API's response. Same fields: `duration` and `channelMember` | +| `StreamChatClient.partialMemberUpdate` | `StreamChatClient.updateMemberPartial` | `renamed` | The name of the API's operation, matching `updateChannelPartial` | +| `StreamChatClient.enableSlowdown` / `disableSlowdown` | `StreamChatClient.enableSlowMode` / `disableSlowMode` | `renamed` | Matches `Channel.enableSlowMode` / `disableSlowMode` and `ChannelCapability.slowMode` | +| `StreamChatClient.updateChannelPartial` / `enableSlowdown` / `disableSlowdown` → `Future` | `updateChannelPartial` / `enableSlowMode` / `disableSlowMode` → `Future>` | `retyped` | Returns a `Result` instead of throwing | +| `Channel.updatePartial` / `updateName` / `updateImage` / `enableSlowMode` / `disableSlowMode` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing. Calling one before the channel is initialized still throws a `StateError` | +| `StreamChatClient.partialMemberUpdate` / `pinChannel` / `unpinChannel` / `archiveChannel` / `unarchiveChannel` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing | +| `Channel.pin` → `Future`, `unpin` / `archive` / `unarchive` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing, and answers the whole response: read the member off `.channelMember`. Calling one before the channel is initialized still throws a `StateError` | +| `PartialUpdateChannelResponse.channel` (`ChannelModel`) | `UpdateChannelPartialResponse.channel` (`ChannelModel?`) | `retyped` | Nullable, as the API declares it | +| `PartialUpdateChannelResponse.members` (`List?`) | `UpdateChannelPartialResponse.members` (`List`) | `retyped` | Always present, empty when the channel has no members; drop any `?.` or `?? []` | +| `PartialUpdateMemberResponse.channelMember` (`Member`) | `UpdateMemberPartialResponse.channelMember` (`Member?`) | `retyped` | Nullable, as the API declares it | +| `PartialUpdateChannelResponse.duration` / `PartialUpdateMemberResponse.duration` (`String?`) | `String` | `retyped` | Always present; drop any `!` or `?? ''` | +| `PartialUpdateChannelResponse.fromJson` / `PartialUpdateMemberResponse.fromJson` | — | `removed` | The responses are plain classes; construct them directly | +| `PartialUpdateChannelResponse()..channel = …`, `PartialUpdateMemberResponse()..channelMember = …` and their other setters | `UpdateChannelPartialResponse(duration: …, channel: …)` / `UpdateMemberPartialResponse(duration: …, channelMember: …)` | `retyped` | Plain classes with a const constructor and final fields | +| `PartialUpdateChannelResponse` / `PartialUpdateMemberResponse` identity `==` | value `==`, plus `copyWith` | `retyped` | Two instances with the same fields are now equal | +| `StreamChatApi.channel.updateChannelPartial` / `enableSlowdown` / `disableSlowdown` / `updateMemberPartial` | the `StreamChatClient` methods | `removed` | The endpoints moved to the generated client | +| `MemberUpdatePayload(pinned: true).toJson()` / `MemberUpdateType.pinned.name` | `{'pinned': true}` / `'pinned'` | `removed` | Same for `archived`. Or call `pinChannel` / `unpinChannel` / `archiveChannel` / `unarchiveChannel` | +| `ChannelModel.extraData` / `Member.extraData` | `ChannelModel.custom` / `Member.custom` | `renamed` | The wire's name for it; also the constructor and `copyWith` arguments | +| `Channel.extraData` / `Channel.extraDataStream` / `Channel(…, extraData:)` / `StreamChatClient.channel(…, extraData:)` | `Channel.custom` / `Channel.customStream` / `custom:` | `renamed` | Follows `ChannelModel.custom`; the setter is renamed too | +| `channel.extraData['disabled']` / `['hidden']` / `['muted']` / `['blocked']` / `['truncated_at']` / `['truncated_by']` / `['auto_translation_enabled']` / `['auto_translation_language']` | `ChannelModel.disabled` / `hidden` / `muted` / `blocked` / `truncatedAt` / `truncatedBy` / `autoTranslationEnabled` / `autoTranslationLanguage` | `retyped` | Fields now, not custom data; `copyWith(custom:)` no longer sets them, and `toJson` no longer writes them | +| `member.extraData['notifications_muted']` / `['status']` / `['ban_from_future_channels']` / `['future_channel_ban_expires']` / `['deleted_at']` | `Member.notificationsMuted` / `status` / `banFromFutureChannels` / `futureChannelBanExpires` / `deletedAt` | `retyped` | Fields now, not custom data; `copyWith(custom:)` no longer sets them, and `toJson` no longer writes them | +| `ChannelModel.custom` / `Member.custom` entry named like one of the model's own fields | the same entry, as custom data | `retyped` | Kept as the API returns it; `toJson` leaves it out so it cannot overwrite the field | | _(more added per feature as PRs land)_ | | | | --- @@ -433,8 +459,8 @@ counterpart: `code` is now a `StreamErrorCode` (an extension type over `int`, wi ## Offline Cache If you use `stream_chat_persistence`, the local database is **rebuilt from empty** the first time your app runs -on v11. The Drift schema version moves from `1035` to `1102`, and the upgrade strategy drops and recreates every -table rather than migrating rows. +on v11. The Drift schema version moves from v10's `1000 + n` (`1035` in v10.5.0) to `1106`, and the upgrade strategy +drops and recreates every table rather than migrating rows. Everything held on disk is discarded: channels, messages, members, reads, drafts, locations, polls, poll votes and reactions. **Messages that failed or were queued while offline are stored in the same table**, so they go @@ -630,6 +656,116 @@ result.fold( **`duration` is a non-nullable `String`**, where v10 typed it `String?`. +### Partial Updates + +**Channel and member partial updates return a `Result` instead of throwing.** On the channel side that covers +`StreamChatClient.updateChannelPartial`, `enableSlowMode` and `disableSlowMode` (v10's `enableSlowdown` and +`disableSlowdown`), and `Channel.updatePartial`, +`updateName`, `updateImage`, `enableSlowMode` and `disableSlowMode`. On the member side it covers +`StreamChatClient.updateMemberPartial` (v10's `partialMemberUpdate`), `pinChannel`, `unpinChannel`, +`archiveChannel` and `unarchiveChannel`, and `Channel.pin`, `unpin`, `archive` and `unarchive`. The full update, +`updateChannel` and `Channel.update`, still throws. A `try`/`catch` around a partial update still compiles, but +no longer catches a failed call: read the returned `Result` instead. + +```dart +// v10 +try { + final response = await channel.updatePartial(set: {'name': 'Support'}); + print(response.channel.name); +} on StreamChatException catch (e) { + report(e); +} + +// v11 +final result = await channel.updatePartial(set: {'name': 'Support'}); +result.fold( + onSuccess: (response) => print(response.channel?.name), + onFailure: (error, _) => report(error), +); +``` + +**The responses take the API's names.** `PartialUpdateChannelResponse` is now `UpdateChannelPartialResponse`, and +`PartialUpdateMemberResponse` is now `UpdateMemberPartialResponse`. Their fields keep their names. + +**`StreamChatClient.enableSlowdown` and `disableSlowdown` are renamed `enableSlowMode` and `disableSlowMode`,** the +names `Channel` already used. + +**`Channel.pin`, `unpin`, `archive` and `unarchive` answer the whole response**, where v10 returned the member: + +```dart +// v10 +final member = await channel.pin(); + +// v11 +final result = await channel.pin(); +final member = result.getOrThrow().channelMember; +``` + +**`UpdateChannelPartialResponse.channel` and `UpdateMemberPartialResponse.channelMember` are nullable**, where v10 +typed them non-null. `UpdateChannelPartialResponse.members` goes the other way: it is a `List`, empty when +the channel has none, where v10 typed it `List?`. + +**The responses no longer decode JSON.** They are plain classes; build them with their constructors — +`UpdateChannelPartialResponse(duration: '0ms', channel: channel)` where v10 wrote +`PartialUpdateChannelResponse()..channel = channel`. `duration` is a non-nullable `String`. + +**`StreamChatApi.channel.updateChannelPartial`, `enableSlowdown`, `disableSlowdown` and `updateMemberPartial` are +removed.** Call them on `StreamChatClient`. + +**`MemberUpdatePayload` and `MemberUpdateType` are removed.** They only named `pinned` and `archived`; pass the map +directly — `updateMemberPartial(channelId: …, channelType: …, set: {'pinned': true})` — or call `pinChannel`, +`archiveChannel` and their counterparts. + +> **Why:** the endpoints moved onto the generated client, which returns a `Result` for every call; the field types +> follow what the API declares, and the names follow the API's so the channel and member updates share one scheme. +> `Channel.pin` and its siblings answer the whole response because every migrated write returns its envelope, so a +> field the API adds later reaches you without another break. `MemberUpdatePayload` and `MemberUpdateType` named +> only two of the keys a membership accepts, so a plain map replaces them. + +### Channel and Member Custom Data + +**`extraData` is renamed `custom` on `ChannelModel`, `Member` and `Channel`.** That covers the fields, the +constructor and `copyWith` arguments, `Channel.extraData`, `Channel.extraDataStream` (now `customStream`), and the +`extraData` argument of `StreamChatClient.channel`. + +```dart +// v10 +final channel = client.channel('messaging', id: 'general', extraData: {'topic': 'support'}); +final topic = channel.extraData['topic']; + +// v11 +final channel = client.channel('messaging', id: 'general', custom: {'topic': 'support'}); +final topic = channel.custom['topic']; +``` + +**The channel and member flags are fields, not custom data.** `ChannelModel.disabled`, `hidden` and `truncatedAt` +read the same, and `muted`, `blocked`, `truncatedBy`, the auto-translation settings and `Member.notificationsMuted`, +`status`, `banFromFutureChannels`, `futureChannelBanExpires` and `deletedAt` are new fields. `custom` no longer holds +any of their keys, and an entry with one of those names in `custom` or in `copyWith(custom:)` no longer sets the +field: + +```dart +// v10 +final hidden = channel.extraData['hidden'] == true; +final shown = model.copyWith(extraData: {...model.extraData, 'hidden': false}); + +// v11 +final hidden = channel.hidden; +final shown = model.copyWith(hidden: false); +``` + +**`ChannelModel.toJson` and `Member.toJson` no longer write these fields**, where v10 wrote them at the root with the +rest of the custom data. Read them from the model rather than from its JSON. + +**`custom` is the custom data exactly as the API returns it.** An entry named like one of the model's own fields +stays in `custom` and leaves the field alone; `toJson` leaves it out, so it cannot overwrite the field. + +**Filter and sort on a flag through its declared field,** such as `ChannelFilterField.hidden`, rather than +`.custom('hidden')`, which reads `custom` locally and finds nothing there. + +> **Why:** the API calls it `custom`, and every migrated model takes that name. The flags arrive as fields of their +> own, so keeping them in the custom data let a custom entry with the same name overwrite them. + ### Moderation **Muting, banning and flagging return a `Result` instead of throwing**, on both `StreamChatClient` diff --git a/openapi-migration/05-polls.md b/openapi-migration/05-polls.md index 80fb91249f..0435af5538 100644 --- a/openapi-migration/05-polls.md +++ b/openapi-migration/05-polls.md @@ -94,8 +94,8 @@ and re-applies the `id` and timestamp defaults, recorded as a break. - **`extraData` is renamed `custom` on `Poll` and `PollOption`,** the wire's name, with no deprecated alias; `StreamPollController.addOption` and `updateOption` and the `polls.custom` column follow. - Every migrated model takes the name from here on (README rule 9); [group 15](15-custom-data.md) - renames whatever no group migrates. + Every migrated model takes the name from here on (README rule 9); [group 21](21-custom-data.md) + checks that none was missed. - **Verified live** against the demo app with a before/after harness covering every endpoint, the error cases and the WebSocket events, and in the sample app on web. diff --git a/openapi-migration/11-channels-and-members.md b/openapi-migration/11-channels-and-members.md index b3a3766bad..8bce522fd4 100644 --- a/openapi-migration/11-channels-and-members.md +++ b/openapi-migration/11-channels-and-members.md @@ -1,8 +1,8 @@ # 11 — Channels, Members & Sync -**Goal:** The biggest group, and the one every controller above it reads through `ChannelState`. +**Goal:** The biggest group, and the one every controller above it reads through `ChannelState`. [15](15-partial-updates.md) split the partial channel and member updates out of it. -**Size:** 27 hand-written method(s) across 2 file(s) → 24 generated operation(s). +**Size:** 23 hand-written method(s) across 2 file(s) → 22 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. @@ -17,9 +17,6 @@ | `channel_api.dart` | `queryChannels` | `QueryChannelsResponse` | | `channel_api.dart` | `markAllRead` | `EmptyResponse` | | `channel_api.dart` | `updateChannel` | `UpdateChannelResponse` | -| `channel_api.dart` | `updateChannelPartial` | `PartialUpdateChannelResponse` | -| `channel_api.dart` | `enableSlowdown` | `PartialUpdateChannelResponse` | -| `channel_api.dart` | `disableSlowdown` | `PartialUpdateChannelResponse` | | `channel_api.dart` | `acceptChannelInvite` | `AcceptInviteResponse` | | `channel_api.dart` | `rejectChannelInvite` | `RejectInviteResponse` | | `channel_api.dart` | `inviteChannelMembers` | `InviteMembersResponse` | @@ -36,7 +33,6 @@ | `channel_api.dart` | `markThreadRead` | `EmptyResponse` | | `channel_api.dart` | `markThreadUnread` | `EmptyResponse` | | `channel_api.dart` | `stopWatching` | `EmptyResponse` | -| `channel_api.dart` | `updateMemberPartial` | `PartialUpdateMemberResponse` | | `channel_api.dart` | `markChannelsDelivered` | `EmptyResponse` | | `general_api.dart` | `sync` | `SyncResponse` | | `general_api.dart` | `queryMembers` | `QueryMembersResponse` | @@ -67,21 +63,44 @@ | `POST` | `/api/v2/chat/sync` | `sync` | `SyncResponse` | | `POST` | `/api/v2/chat/channels/{type}/{id}/truncate` | `truncateChannel` | `TruncateChannelResponse` | | `POST` | `/api/v2/chat/channels/{type}/{id}` | `updateChannel` | `UpdateChannelResponse` | -| `PATCH` | `/api/v2/chat/channels/{type}/{id}` | `updateChannelPartial` | `UpdateChannelPartialResponse` | -| `PATCH` | `/api/v2/chat/channels/{type}/{id}/member` | `updateMemberPartial` | `UpdateMemberPartialResponse` | ## Decisions to make - `ChannelState`, `ChannelModel` and `Member` are public, persisted, and rebuilt from WebSocket events. Keep ours and map. -- **`ChannelModel` promotes its `extraData`-backed flags to fields** (README rule 9). `disabled`, `hidden`, `muted`, `blocked` and `truncatedAt`, plus the keys the channel mapper parks today — `truncatedBy`, `muteExpiresAt`, `hideMessagesBefore`, `autoTranslationEnabled` and `autoTranslationLanguage` — become fields with persistence columns, and the constructor stops writing them into the custom data, which is renamed `custom`. `name` and `image` stay in it: `ChannelResponse` has no such fields. Raised on [#2958](https://github.com/GetStream/stream-chat-flutter/pull/2958). +- **Whether `ChannelConfig` exposes the generated-only config fields.** `ChannelConfigWithInfo` carries `allowedFlagReasons`, `automodBehavior`, `automodThresholds`, `blocklist`, `blocklistBehavior`, `blocklists`, `countMessages`, `customEvents`, `grants`, `name`, `partitionSize`, `partitionTtl`, `quotes` and `reminders`, which `ChannelConfig` does not model and v1 drops as well. `blocklists` and `automodThresholds` would need hand-written models. The mapper marks the drop with a TODO. +- **Expose `hide_messages_before` on `ChannelState`.** `clear_history` writes it on the caller's member row, and the server returns it next to `channel` in each channel-state entry (`ChannelStateResponse.hideMessagesBefore`), never on the channel itself. v1 drops it today. - `sync` returns `SyncResponse` — one of the two models that needed the WSEvent generator patch. Verify it decodes before relying on it. +- **How `Channel` hands its creation data to `getOrCreateChannel`.** The `Channel` constructor and `StreamChatClient.channel` take one `custom` map, and v1 sends it whole as the request's flat `data`, so `client.channel('messaging', custom: {'members': [...]})` creates the channel with those members. v2's `ChannelInput` types `members`, `invites`, `team`, `frozen`, `disabled`, `createdBy`, `configOverrides` and the auto-translation settings, next to its own `custom`. Either the request mapper lifts those keys out of the map into the typed fields and sends the rest as `custom`, or `Channel` and `client.channel` take typed arguments such as `members`, a break. + +## Decisions taken + +- **The generated channel and member types map onto today's classes.** + `lib/src/repository/mapper/channels_mapper.dart` maps `ChannelResponse`, `ChannelConfigWithInfo`, + `ChannelMemberResponse`, `Command` and `ChatPreferences` onto the json_serializable `ChannelModel`, + `ChannelConfig`, `Member`, `Command` and `ChatPreferences`, the way `user_mapper.dart` does for + `User`. [15](15-partial-updates.md) uses it first; groups 06, 07 and 14 will reuse it. This group + restructures those classes and re-points the mapper. +- **Every field `ChannelResponse` and `ChannelMemberResponse` declare is reachable on ours,** as a + field: [15](15-partial-updates.md) promoted the ones v10 parked in `extraData` and renamed it `custom`. + Three are left out: the channel's `hide_messages_before`, which the server returns on the + channel-state entry rather than on the channel, its `mute_expires_at`, since a mute's expiry is + read from `ChannelMute.expires`, and the member's `role`, which neither the iOS nor the Android + SDK models. The nested config types drop `ChatPreferences.distinctChannelMessages` and + `Command`'s `createdAt` / `updatedAt`, which ours do not model. +- **The mapper copies `custom` verbatim** (README rule 9). A mapped channel or member still differs from + a v1 decode in two ways. `truncatedBy`, `createdBy` and each member's user go through + `user_mapper.dart`, which drops the user fields `User` does not model, for group 09 to settle. And + a v1 member keeps the wire's `role` in `custom`, where the mapper leaves the typed `role` out. +- **The mapper is tested through the client,** by [15](15-partial-updates.md)'s + `updateChannelPartial` tests: the first call that returns a channel through the generated client. ## Risks - `sync` and `queryMembers` live in `general_api.dart`, not `channel_api.dart` — this group reaches into that file. - `queryChannels` drives the channel list controllers and the offline cache; a shape change here is felt everywhere. -- A consumer reading `channel.extraData['hidden']` and friends loses the key once the flags are fields; none in this repo. -- Landing `ChannelResponse` -> `ChannelModel` is the last thing blocking [08](08-moderation-and-blocklists.md)'s `muteChannel`, which drops `channelMute`, `channelMutes` and `ownUser` because the generated `ChannelMute` carries a `ChannelResponse?` and a `UserResponse?` where ours needs a non-nullable `ChannelModel` and `User`. It needs 09 as well. Decide the null case there too — ours are non-nullable, the generated ones are not, the same question [14](14-banned-users.md) records for `BanResponse.user`. +- Sending `Channel`'s `custom` map as `ChannelInput.custom` as it stands turns `members` and the other typed keys into custom data, so `client.channel('messaging', custom: {'members': [...]})` creates a channel without its members, silently. +- **A zero timestamp decodes differently on v2.** v1 sends Go's zero time as `0001-01-01T00:00:00Z`; v2 encodes timestamps as epoch nanoseconds and sends `0`, which decodes as 1970-01-01. A truncated channel shows it: its `last_message_at` is zero time, so the same channel answers year 1 from a v1 query or WebSocket event and 1970 from `/api/v2/chat/channels`. Nothing in the SDK behaves differently today, since both precede `createdAt` (`lastUpdatedAt` falls back to it, and the read fallback and the `max` against new messages agree), but the two sources disagree on the value. Normalize it deliberately when `queryChannels` moves, and check the other timestamps the server can send as zero. +- [08](08-moderation-and-blocklists.md)'s `muteChannel` drops `channelMute`, `channelMutes` and `ownUser` because the generated `ChannelMute` carries a `ChannelResponse?` and a `UserResponse?` where ours needs a non-nullable `ChannelModel` and `User`. `channels_mapper.dart` covers the channel side now; it needs 09 as well. Decide the null case there too — ours are non-nullable, the generated ones are not, the same question [14](14-banned-users.md) records for `BanResponse.user`. ## Definition of done @@ -101,5 +120,5 @@ retyped but whose docs it left alone. - [ ] Tests follow [`TESTING.md`](../TESTING.md): no `group` organizing a file by method, each name states its subject and behaviour. -- [ ] `channel_mapper.dart` has no `_shadowedCustomKeys`: `ChannelModel.custom` is the wire's `custom`, +- [x] `channels_mapper.dart` has no `_shadowedCustomKeys`: `ChannelModel.custom` is the wire's `custom`, verbatim, and no flag is written into it. diff --git a/openapi-migration/15-partial-updates.md b/openapi-migration/15-partial-updates.md new file mode 100644 index 0000000000..668ce08446 --- /dev/null +++ b/openapi-migration/15-partial-updates.md @@ -0,0 +1,105 @@ +# 15 — Partial Updates + +**Goal:** Move the partial channel and member updates ahead of [11](11-channels-and-members.md): they answer a channel and its members through the mappers group 11 needs, without touching `ChannelState`, the channel list or the offline cache. + +**Size:** 0 hand-written method(s) across 0 file(s) → 2 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 | +| --- | --- | --- | --- | +| `PATCH` | `/api/v2/chat/channels/{type}/{id}` | `updateChannelPartial` | `UpdateChannelPartialResponse` | +| `PATCH` | `/api/v2/chat/channels/{type}/{id}/member` | `updateMemberPartial` | `UpdateMemberPartialResponse` | + +## Decisions taken + +- **Split out of [11](11-channels-and-members.md), ahead of it.** The partial updates read nothing + into client state — the `channel.updated` and `member.updated` events do that — so they can move + before the queries and prove `channels_mapper.dart` on real calls. +- **The full update stays in 11, so the pair is split on purpose.** `updateChannel` takes and + answers a `Message`, and the generated `UpdateChannelRequest.message` / `UpdateChannelResponse.message` + need the `MessageRequest` and `MessageResponse` mappers [10](10-messages.md) writes; its `data` is + a typed `ChannelInputRequest` rather than a map, too. Until then `updateChannel` and + `Channel.update` still throw while the partial update returns a `Result`, and + `migrations/v11-migration.md` says so. +- **Moved off `ChannelApi`:** `updateChannelPartial`, `enableSlowdown`, `disableSlowdown` and + `updateMemberPartial`, each now a `StreamChatClient` method over `ChannelsRepository`. +- **`enableSlowdown` and `disableSlowdown` move with it, renamed `enableSlowMode` and + `disableSlowMode`** to match the `Channel` helpers and `ChannelCapability.slowMode`. Both are + `updateChannelPartial` with a fixed `cooldown`, so they call the repository's one method rather than + an operation of their own. `disableSlowMode` sets `cooldown` to 0: unsetting it is refused on v1 and + v2, as it is a reserved field. +- **The v2 route is the v1 handler.** `lib/chat/routes.go` mounts `UpdateChannelPartial` in the + shared `coreRoutes`; it is neither gated nor in beta. On v2 it also accepts `custom.` paths + in `set` and `unset`, and still takes flat keys, so `{'name': …}` updates the same field. +- **The envelopes and the member method take the spec's names:** `PartialUpdateChannelResponse` becomes + `UpdateChannelPartialResponse`, `PartialUpdateMemberResponse` becomes `UpdateMemberPartialResponse`, + and `StreamChatClient.partialMemberUpdate` becomes `updateMemberPartial`. Hard renames, as + `GetAppSettingsResponse` → `AppSettingsResponse` was: every call site changes for `Result` anyway, + and the channel and member calls now share one naming scheme. + The other helpers with no spec counterpart keep their names (`pinChannel`, `archiveChannel`, + `Channel.pin` and the rest); the slow-mode pair is renamed above. +- **The envelopes are freezed, in `models/response/`.** `UpdateChannelPartialResponse.channel` and + `UpdateMemberPartialResponse.channelMember` are nullable, as the spec declares them, where v10 typed + them non-null. `UpdateChannelPartialResponse.members` is non-nullable, as the spec requires it, where + v10 typed it nullable: the server answers `[]` for a channel with no members. +- **`Channel.pin`, `unpin`, `archive` and `unarchive` answer the envelope,** where v10 returned the + bare `Member`: a write returns its envelope. +- **The v2 member route is a different handler from v1's, with the same behaviour.** v1 calls + `/member/{user_id}` (`UpdateMemberPartialDeprecated`), v2 calls `/member` (`UpdateMemberPartial`). + For a client-side request both take the user from the token, refuse `channel_role`, and run the same + update; checked live for pin, unpin, archive, unarchive and a custom field. +- **`MemberUpdatePayload` and `MemberUpdateType` are removed.** They only built the `set` and `unset` + values for pin and archive, named two of the fields the server accepts, and carried a public + `toJson` and an enum the model rules retire. The client writes `{'pinned': true}` / `['pinned']`. +- **The request always sends `set` and `unset`, `null` when not given,** where v1 left the key out. + The server reads both the same, and still rejects a call with neither. +- **`ChannelModel` and `Member` promote the fields v10 parked in `extraData`, and rename it `custom`** + (README rule 9), since these calls are the first to map them. `ChannelModel.disabled`, `hidden`, + `muted`, `blocked`, `truncatedAt`, `truncatedBy`, `autoTranslationEnabled` and + `autoTranslationLanguage`, and `Member.notificationsMuted`, `status`, `banFromFutureChannels`, + `futureChannelBanExpires` and `deletedAt`, are fields with persistence columns; `truncatedBy` is + read back through the users table. `toJson` leaves them out: nothing in the SDK sends a channel + or member as JSON. `Channel.extraData`, `extraDataStream`, and the `extraData` argument of the + `Channel` constructor and of `StreamChatClient.channel` are renamed with it. +- **`custom` is the wire's, verbatim,** and the v1 writers leave out a custom entry named like a key, + so it cannot overwrite the field. +- **`ChannelModel.merge` keeps a flag the incoming channel leaves out, and turns auto translation off + when it is missing,** as before. Checked live on v1 and v2: queries always send `hidden` and + `blocked`; events leave them out, except the `channel.hidden` a block sends; `muted` is never sent on + a channel; `truncated_at` and `truncated_by` come on every event once the channel is truncated; + `disabled` and `frozen` always. A client token cannot set `auto_translation_*`, so that rule rests + on the backend leaving the keys out when false or empty. + +## Risks + +- Until [11](11-channels-and-members.md) moves `updateChannel`, the partial and full updates report failures differently: a caller using both reads a `Result` from one and catches an exception from the other. + +## 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] `copywith_audit.py` was run for every model that loses a v10 `copyWith`, every site it reports is + handled, and the semantics change is in the CHANGELOG and the guide ([README rule 2](README.md#domain-models)). +- [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/15-custom-data.md b/openapi-migration/21-custom-data.md similarity index 60% rename from openapi-migration/15-custom-data.md rename to openapi-migration/21-custom-data.md index b04ed5afd8..7be8044cd4 100644 --- a/openapi-migration/15-custom-data.md +++ b/openapi-migration/21-custom-data.md @@ -1,6 +1,6 @@ -# 15 — Custom Data +# 21 — Custom Data Check -**Goal:** Every public model's custom data is named `custom` and is exactly the wire's `custom`. +**Goal:** Check that no model was missed: every public model's custom data is named `custom` and is exactly the wire's `custom`. **Size:** 0 hand-written method(s) across 0 file(s) → 0 generated operation(s). @@ -21,15 +21,18 @@ ## Decisions to make -- **Which models are left.** Each group renames the models it migrates, polls first; this pass renames whatever remains, and the persistence `extra_data` columns with them. -- **How to stage the remaining renames.** Polls did a hard rename; decide whether the widely used models (`Message`, `ChannelModel`, `User`) get a deprecated `extraData` alias for a release. - **What replaces `topLevelFields` and `Serializer`.** The v1 decoders still split flat JSON by a key list until the WebSocket moves to v2. A model that loses its JSON drops `topLevelFields` and its converter owns the list, as polls did; decide where `Serializer` goes once the last decoder does. +## Decisions taken + +- **Renames nothing of its own.** Each group renames the models it maps and their persistence + columns, and promotes the fields they park, as it goes (README rule 9); polls went first. A model + this check finds still saying `extraData` goes back to the group that maps it. + ## Risks - `Serializer`, the models' `topLevelFields` and the persistence `extra_data` columns name the concept too. -- A consumer reading `channel.extraData['hidden']` or `user.extraData['name']` loses the key; none in this repo. Filter fields over custom keys (`UserFilterField.email`) are unaffected. -- Runs after groups 09 and 11 at the earliest, once `User` and `ChannelModel` promote their parked fields. +- Runs last, after every group that maps a model. ## Definition of done @@ -38,6 +41,4 @@ - [ ] No public model exposes `topLevelFields`; each v1 decoder owns its key list. - [ ] Every public model names its custom data the same way, in the models, the mappers, the v1 decoders and the persistence columns. -- [ ] `migrations/v11-migration.md`: Symbol Map rows plus a feature section. -- [ ] CHANGELOG entry under `🛑️ Breaking` for each break; PR title `refactor(llc)!:`. - [ ] Decisions recorded in this file, and the status box ticked in `README.md`. diff --git a/openapi-migration/README.md b/openapi-migration/README.md index 3b6d92818b..9e1a6eb0e4 100644 --- a/openapi-migration/README.md +++ b/openapi-migration/README.md @@ -22,13 +22,14 @@ generated operations in scope, the decisions that group has to make, its risks, | [08](08-moderation-and-blocklists.md) | Moderation & Blocklists | 0 | 34 | ☑ | | [09](09-users.md) | Users | 9 | 9 | ☐ | | [10](10-messages.md) | Messages & Search | 14 | 12 | ☐ | -| [11](11-channels-and-members.md) | Channels, Members & Sync | 27 | 24 | ☐ | +| [11](11-channels-and-members.md) | Channels, Members & Sync | 23 | 22 | ☐ | | [12](12-uploads-cdn.md) | Uploads (CDN) | 8 | 8 | ☑ | | [13](13-push-preferences.md) | Push Preferences | 1 | 1 | ☐ | | [14](14-banned-users.md) | Banned Users — split out of 08 | 1 | 1 | ☐ | -| [15](15-custom-data.md) | Custom Data — `extraData` is the wire's `custom`, then renamed | — | — | ☐ | +| [15](15-partial-updates.md) | Partial Updates — split out of 11 | 0 | 2 | ☑ | +| [21](21-custom-data.md) | Custom Data Check — no model still says `extraData` | — | — | ☐ | -**Coverage:** 71 hand-written methods across 9 files, and all 129 generated operations, each claimed by exactly +**Coverage:** 67 hand-written methods across 9 files, and all 129 generated operations, each claimed by exactly one group. Verified mechanically — see [Keeping this plan honest](#keeping-this-plan-honest). @@ -113,10 +114,13 @@ includes a named response that carries only `duration` today, such as `HideChann public API change of its own. The semantics change goes in the CHANGELOG and the guide, listed per field under the rows of the audit's table. - **Names default to v10's,** even where the generated name differs: `OGAttachmentResponse` rather than - `GetOGResponse`, `AppSettings` rather than `AppResponseFields`, `UploadConfig` rather than `FileUploadConfig`. - That is a default, not a requirement. The user may ask for another name, or a migration may propose one that - fits clearly better; renaming a v10 type is a break, so it needs approval and a Symbol Map row. + **Names follow the spec's, unless the spec's name is awkward or describes something else,** and calls the + caller sees as siblings share one scheme: `updateChannelPartial` and `updateMemberPartial` answer + `UpdateChannelPartialResponse` and `UpdateMemberPartialResponse`, where v10 had `PartialUpdateChannelResponse`, + `partialMemberUpdate` and `PartialUpdateMemberResponse`. v10's name stays where the generated one does not fit: + `OGAttachmentResponse` rather than `GetOGResponse`, `AppSettings` rather than `AppResponseFields`. Names an + earlier group settled stand, such as group 04's `getAppSettings` and `UploadConfig`. Renaming a v10 type or + method is a break, so it needs approval, a Symbol Map row, and a line in the PR description. 3. **Responses keep their v10 envelopes,** as `@freezed` classes carrying a non-nullable `duration` and the payload. Every envelope lives in `lib/src/core/models/response/`, and every public type a caller passes in to shape a request — a `*Request` class, or a parameter type such as `PaginationParams` or `ThreadOptions` — in @@ -153,10 +157,11 @@ includes a named response that carries only `duration` today, such as `HideChann directions.** A response mapper copies the generated `custom` map into it and a request mapper sends it back as `custom`; neither filters it. A model whose group has not run still says `extraData`. A field the generated response types is never parked in `extraData` behind a getter: it becomes a field on the model, as - the owning group's break, or stays out. `ChannelModel.hidden` and `User.name` are parked today, and groups 11 - and 09 promote them. `topLevelFields` and `Serializer` serve only the v1 decoders, which still split flat JSON - by a key list, and go with the WebSocket v2 move; a model that loses its JSON drops `topLevelFields` and its - converter owns the list. A v1 writer leaves a custom entry named like a key out so it cannot overwrite the key. + the owning group's break, or stays out. `User.name` is parked today, and group 09 promotes it; group 15 + promoted `ChannelModel`'s and `Member`'s. `topLevelFields` and `Serializer` serve only the v1 decoders, which + still split flat JSON by a key list, and go with the WebSocket v2 move; a model that loses its JSON drops + `topLevelFields` and its converter owns the list. A v1 writer leaves a custom entry named like a key out so it + cannot overwrite the key. ### Temporary adapters @@ -168,6 +173,7 @@ includes a named response that carries only `duration` today, such as `HideChann | `PollV1JsonConverter` | `Message.poll`, `DraftMessage.poll`, `Event.poll` | WebSocket v2 (no group) | | `PollVoteV1JsonConverter` | `Event.pollVote` | WebSocket v2 (no group) | | `user_mapper.dart` (kept, re-pointed) | today's `User`, which still reads and writes JSON | [09](09-users.md) | +| `channels_mapper.dart` (kept, re-pointed) | today's `ChannelModel`, `ChannelConfig` and `Member`, which still read and write JSON | [11](11-channels-and-members.md) | How v1 JSON decodes `User` once it becomes a plain model is decided in [01-foundation](01-foundation.md): until group 09 restructures it, v1 payloads keep decoding through `User.fromJson`. @@ -186,15 +192,19 @@ surfaces before it reaches `Message` and `ChannelState`: - **08** is where we decide what *not* to expose: 34 generated operations against 10 hand-written methods. - **14** is `queryBannedUsers`, split out of 08 because it is the only moderation call that answers with a model. The `User` mappers it needs landed with group 04. -- **10–11** are the core of the SDK; 11 promotes the channel flags parked in `extraData` (rule 9). +- **10–11** are the core of the SDK. +- **15** is the partial channel and member updates, split out of 11 so `channels_mapper.dart` lands on calls that + read nothing into `ChannelState`. It landed first; the full update waits in 11 for group 10's message mappers. + As the first group to map `ChannelModel` and `Member`, it promoted their parked fields and renamed their custom + data `custom` (rule 9). - **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 user endpoints and restructures `User` and `OwnUser` themselves, when every parent that embeds them has moved. -- **15** is the custom-data pass: every model's custom data is named `custom` and is the wire's `custom`, with no - mapper filtering it and no typed field parked in it. Each group renames the models it migrates (polls went - first); 15 renames whatever is left and the persistence columns, and runs after 09 and 11, which promote their - parked fields. +- **21** checks the custom data once every group has run: each model's custom data is named `custom` and is the + wire's `custom`, with no mapper filtering it and no typed field parked in it. Each group does that for the + models it maps, persistence columns included (polls went first), so 21 renames nothing; a model it finds + still saying `extraData` goes back to the group that maps it. ## Prerequisites diff --git a/openapi-migration/tool/generate_plan.py b/openapi-migration/tool/generate_plan.py index fdc87e02de..d50328bd14 100644 --- a/openapi-migration/tool/generate_plan.py +++ b/openapi-migration/tool/generate_plan.py @@ -63,14 +63,29 @@ def read_generated(): ) -def owns(*prefixes, unless=()): - def match(path): - if any(path.startswith(x) for x in unless): +# Every 'VERB /path' named by only_ops or unless_ops, so the report can flag one that matches no operation. +EXACT_OPS = set() + + +def owns(*prefixes, unless=(), unless_ops=()): + """Claims every operation under one of the path prefixes, except those under an `unless` prefix and the + 'VERB /path' operations in `unless_ops`.""" + EXACT_OPS.update(unless_ops) + def match(verb, path): + if any(path.startswith(x) for x in unless) or f'{verb} {path}' in unless_ops: return False return any(path.startswith(x) for x in prefixes) return match +def only_ops(*operations): + """Claims exactly the given 'VERB /path' operations, for a group split out of another one's path.""" + EXACT_OPS.update(operations) + def match(verb, path): + return f'{verb} {path}' in operations + return match + + DONE = textwrap.dedent("""\ - [ ] Every method above either routes through `DefaultApi` or is listed here as deliberately left hand-written, with the reason. @@ -374,8 +389,8 @@ def match(path): and re-applies the `id` and timestamp defaults, recorded as a break. - **`extraData` is renamed `custom` on `Poll` and `PollOption`,** the wire's name, with no deprecated alias; `StreamPollController.addOption` and `updateOption` and the `polls.custom` column follow. - Every migrated model takes the name from here on (README rule 9); [group 15](15-custom-data.md) - renames whatever no group migrates. + Every migrated model takes the name from here on (README rule 9); [group 21](21-custom-data.md) + checks that none was missed. - **Verified live** against the demo app with a before/after harness covering every endpoint, the error cases and the WebSocket events, and in the sample app on web. """), @@ -643,37 +658,79 @@ def match(path): match=owns('/api/v2/chat/channels', '/api/v2/chat/members', '/api/v2/chat/sync', unless=('/api/v2/chat/channels/{type}/{id}/draft', '/api/v2/chat/channels/{type}/{id}/file', - '/api/v2/chat/channels/{type}/{id}/image')), - goal='The biggest group, and the one every controller above it reads through `ChannelState`.', + '/api/v2/chat/channels/{type}/{id}/image'), + unless_ops=('PATCH /api/v2/chat/channels/{type}/{id}', + 'PATCH /api/v2/chat/channels/{type}/{id}/member')), + goal='The biggest group, and the one every controller above it reads through `ChannelState`. ' + '[15](15-partial-updates.md) split the partial channel and member updates out of it.', decisions=[ '`ChannelState`, `ChannelModel` and `Member` are public, persisted, and rebuilt from WebSocket ' 'events. Keep ours and map.', - '**`ChannelModel` promotes its `extraData`-backed flags to fields** (README rule 9). `disabled`, ' - '`hidden`, `muted`, `blocked` and `truncatedAt`, plus the keys the channel mapper parks today — ' - '`truncatedBy`, `muteExpiresAt`, `hideMessagesBefore`, `autoTranslationEnabled` and ' - '`autoTranslationLanguage` — become fields with persistence columns, and the constructor stops writing ' - 'them into the custom data, which is renamed `custom`. `name` and `image` stay in it: `ChannelResponse` ' - 'has no such fields. ' - 'Raised on [#2958](https://github.com/GetStream/stream-chat-flutter/pull/2958).', + '**Whether `ChannelConfig` exposes the generated-only config fields.** `ChannelConfigWithInfo` ' + 'carries `allowedFlagReasons`, `automodBehavior`, `automodThresholds`, `blocklist`, ' + '`blocklistBehavior`, `blocklists`, `countMessages`, `customEvents`, `grants`, `name`, ' + '`partitionSize`, `partitionTtl`, `quotes` and `reminders`, which `ChannelConfig` does not model and ' + 'v1 drops as well. `blocklists` and `automodThresholds` would need hand-written models. The mapper ' + 'marks the drop with a TODO.', + '**Expose `hide_messages_before` on `ChannelState`.** `clear_history` writes it on the caller\'s ' + 'member row, and the server returns it next to `channel` in each channel-state entry ' + '(`ChannelStateResponse.hideMessagesBefore`), never on the channel itself. v1 drops it today.', '`sync` returns `SyncResponse` — one of the two models that needed the WSEvent generator patch. ' 'Verify it decodes before relying on it.', + '**How `Channel` hands its creation data to `getOrCreateChannel`.** The `Channel` constructor and ' + '`StreamChatClient.channel` take one `custom` map, and v1 sends it whole as the request\'s flat `data`, ' + 'so `client.channel(\'messaging\', custom: {\'members\': [...]})` creates the channel with those ' + 'members. v2\'s `ChannelInput` types `members`, `invites`, `team`, `frozen`, `disabled`, `createdBy`, ' + '`configOverrides` and the auto-translation settings, next to its own `custom`. Either the request ' + 'mapper lifts those keys out of the map into the typed fields and sends the rest as `custom`, or ' + '`Channel` and `client.channel` take typed arguments such as `members`, a break.', ], risks=[ '`sync` and `queryMembers` live in `general_api.dart`, not `channel_api.dart` — this group reaches ' 'into that file.', '`queryChannels` drives the channel list controllers and the offline cache; a shape change here is ' 'felt everywhere.', - 'A consumer reading `channel.extraData[\'hidden\']` and friends loses the key once the flags are fields; ' - 'none in this repo.', - 'Landing `ChannelResponse` -> `ChannelModel` is the last thing blocking ' - '[08](08-moderation-and-blocklists.md)\'s `muteChannel`, which drops `channelMute`, `channelMutes` ' - 'and `ownUser` because the generated `ChannelMute` carries a `ChannelResponse?` and a ' - '`UserResponse?` where ours needs a non-nullable `ChannelModel` and `User`. It needs 09 as well. ' + 'Sending `Channel`\'s `custom` map as `ChannelInput.custom` as it stands turns `members` and the other ' + 'typed keys into custom data, so `client.channel(\'messaging\', custom: {\'members\': [...]})` ' + 'creates a channel without its members, silently.', + '**A zero timestamp decodes differently on v2.** v1 sends Go\'s zero time as ' + '`0001-01-01T00:00:00Z`; v2 encodes timestamps as epoch nanoseconds and sends `0`, which decodes as ' + '1970-01-01. A truncated channel shows it: its `last_message_at` is zero time, so the same channel ' + 'answers year 1 from a v1 query or WebSocket event and 1970 from `/api/v2/chat/channels`. Nothing ' + 'in the SDK behaves differently today, since both precede `createdAt` (`lastUpdatedAt` falls back ' + 'to it, and the read fallback and the `max` against new messages agree), but the two sources disagree on ' + 'the value. Normalize it deliberately when `queryChannels` moves, and check the other ' + 'timestamps the server can send as zero.', + '[08](08-moderation-and-blocklists.md)\'s `muteChannel` drops `channelMute`, `channelMutes` and ' + '`ownUser` because the generated `ChannelMute` carries a `ChannelResponse?` and a `UserResponse?` ' + 'where ours needs a non-nullable `ChannelModel` and `User`. `channels_mapper.dart` covers the ' + 'channel side now; it needs 09 as well. ' 'Decide the null case there too — ours are non-nullable, the generated ones are not, the same ' 'question [14](14-banned-users.md) records for `BanResponse.user`.', ], + taken=textwrap.dedent("""\ + - **The generated channel and member types map onto today's classes.** + `lib/src/repository/mapper/channels_mapper.dart` maps `ChannelResponse`, `ChannelConfigWithInfo`, + `ChannelMemberResponse`, `Command` and `ChatPreferences` onto the json_serializable `ChannelModel`, + `ChannelConfig`, `Member`, `Command` and `ChatPreferences`, the way `user_mapper.dart` does for + `User`. [15](15-partial-updates.md) uses it first; groups 06, 07 and 14 will reuse it. This group + restructures those classes and re-points the mapper. + - **Every field `ChannelResponse` and `ChannelMemberResponse` declare is reachable on ours,** as a + field: [15](15-partial-updates.md) promoted the ones v10 parked in `extraData` and renamed it `custom`. + Three are left out: the channel's `hide_messages_before`, which the server returns on the + channel-state entry rather than on the channel, its `mute_expires_at`, since a mute's expiry is + read from `ChannelMute.expires`, and the member's `role`, which neither the iOS nor the Android + SDK models. The nested config types drop `ChatPreferences.distinctChannelMessages` and + `Command`'s `createdAt` / `updatedAt`, which ours do not model. + - **The mapper copies `custom` verbatim** (README rule 9). A mapped channel or member still differs from + a v1 decode in two ways. `truncatedBy`, `createdBy` and each member's user go through + `user_mapper.dart`, which drops the user fields `User` does not model, for group 09 to settle. And + a v1 member keeps the wire's `role` in `custom`, where the mapper leaves the typed `role` out. + - **The mapper is tested through the client,** by [15](15-partial-updates.md)'s + `updateChannelPartial` tests: the first call that returns a channel through the generated client. + """), done=DONE + ( - '- [ ] `channel_mapper.dart` has no `_shadowedCustomKeys`: `ChannelModel.custom` is the wire\'s `custom`,\n' + '- [x] `channels_mapper.dart` has no `_shadowedCustomKeys`: `ChannelModel.custom` is the wire\'s `custom`,\n' ' verbatim, and no flag is written into it.\n' ), ), @@ -778,36 +835,107 @@ def match(path): ], ), dict( - num='15', slug='custom-data', title='Custom Data', + num='15', slug='partial-updates', title='Partial Updates', + hand=[], + match=only_ops('PATCH /api/v2/chat/channels/{type}/{id}', 'PATCH /api/v2/chat/channels/{type}/{id}/member'), + goal='Move the partial channel and member updates ahead of [11](11-channels-and-members.md): they answer a ' + 'channel and its members through the mappers group 11 needs, without touching `ChannelState`, the ' + 'channel list or the offline cache.', + decisions=[], + taken=textwrap.dedent("""\ + - **Split out of [11](11-channels-and-members.md), ahead of it.** The partial updates read nothing + into client state — the `channel.updated` and `member.updated` events do that — so they can move + before the queries and prove `channels_mapper.dart` on real calls. + - **The full update stays in 11, so the pair is split on purpose.** `updateChannel` takes and + answers a `Message`, and the generated `UpdateChannelRequest.message` / `UpdateChannelResponse.message` + need the `MessageRequest` and `MessageResponse` mappers [10](10-messages.md) writes; its `data` is + a typed `ChannelInputRequest` rather than a map, too. Until then `updateChannel` and + `Channel.update` still throw while the partial update returns a `Result`, and + `migrations/v11-migration.md` says so. + - **Moved off `ChannelApi`:** `updateChannelPartial`, `enableSlowdown`, `disableSlowdown` and + `updateMemberPartial`, each now a `StreamChatClient` method over `ChannelsRepository`. + - **`enableSlowdown` and `disableSlowdown` move with it, renamed `enableSlowMode` and + `disableSlowMode`** to match the `Channel` helpers and `ChannelCapability.slowMode`. Both are + `updateChannelPartial` with a fixed `cooldown`, so they call the repository's one method rather than + an operation of their own. `disableSlowMode` sets `cooldown` to 0: unsetting it is refused on v1 and + v2, as it is a reserved field. + - **The v2 route is the v1 handler.** `lib/chat/routes.go` mounts `UpdateChannelPartial` in the + shared `coreRoutes`; it is neither gated nor in beta. On v2 it also accepts `custom.` paths + in `set` and `unset`, and still takes flat keys, so `{'name': …}` updates the same field. + - **The envelopes and the member method take the spec's names:** `PartialUpdateChannelResponse` becomes + `UpdateChannelPartialResponse`, `PartialUpdateMemberResponse` becomes `UpdateMemberPartialResponse`, + and `StreamChatClient.partialMemberUpdate` becomes `updateMemberPartial`. Hard renames, as + `GetAppSettingsResponse` → `AppSettingsResponse` was: every call site changes for `Result` anyway, + and the channel and member calls now share one naming scheme. + The other helpers with no spec counterpart keep their names (`pinChannel`, `archiveChannel`, + `Channel.pin` and the rest); the slow-mode pair is renamed above. + - **The envelopes are freezed, in `models/response/`.** `UpdateChannelPartialResponse.channel` and + `UpdateMemberPartialResponse.channelMember` are nullable, as the spec declares them, where v10 typed + them non-null. `UpdateChannelPartialResponse.members` is non-nullable, as the spec requires it, where + v10 typed it nullable: the server answers `[]` for a channel with no members. + - **`Channel.pin`, `unpin`, `archive` and `unarchive` answer the envelope,** where v10 returned the + bare `Member`: a write returns its envelope. + - **The v2 member route is a different handler from v1's, with the same behaviour.** v1 calls + `/member/{user_id}` (`UpdateMemberPartialDeprecated`), v2 calls `/member` (`UpdateMemberPartial`). + For a client-side request both take the user from the token, refuse `channel_role`, and run the same + update; checked live for pin, unpin, archive, unarchive and a custom field. + - **`MemberUpdatePayload` and `MemberUpdateType` are removed.** They only built the `set` and `unset` + values for pin and archive, named two of the fields the server accepts, and carried a public + `toJson` and an enum the model rules retire. The client writes `{'pinned': true}` / `['pinned']`. + - **The request always sends `set` and `unset`, `null` when not given,** where v1 left the key out. + The server reads both the same, and still rejects a call with neither. + - **`ChannelModel` and `Member` promote the fields v10 parked in `extraData`, and rename it `custom`** + (README rule 9), since these calls are the first to map them. `ChannelModel.disabled`, `hidden`, + `muted`, `blocked`, `truncatedAt`, `truncatedBy`, `autoTranslationEnabled` and + `autoTranslationLanguage`, and `Member.notificationsMuted`, `status`, `banFromFutureChannels`, + `futureChannelBanExpires` and `deletedAt`, are fields with persistence columns; `truncatedBy` is + read back through the users table. `toJson` leaves them out: nothing in the SDK sends a channel + or member as JSON. `Channel.extraData`, `extraDataStream`, and the `extraData` argument of the + `Channel` constructor and of `StreamChatClient.channel` are renamed with it. + - **`custom` is the wire's, verbatim,** and the v1 writers leave out a custom entry named like a key, + so it cannot overwrite the field. + - **`ChannelModel.merge` keeps a flag the incoming channel leaves out, and turns auto translation off + when it is missing,** as before. Checked live on v1 and v2: queries always send `hidden` and + `blocked`; events leave them out, except the `channel.hidden` a block sends; `muted` is never sent on + a channel; `truncated_at` and `truncated_by` come on every event once the channel is truncated; + `disabled` and `frozen` always. A client token cannot set `auto_translation_*`, so that rule rests + on the backend leaving the keys out when false or empty. + """), + risks=[ + 'Until [11](11-channels-and-members.md) moves `updateChannel`, the partial and full updates report ' + 'failures differently: a caller using both reads a `Result` from one and catches an exception from the ' + 'other.', + ], + done=DONE.replace('- [ ]', '- [x]'), + ), + dict( + num='21', slug='custom-data', title='Custom Data Check', hand=[], match=owns(), - goal='Every public model\'s custom data is named `custom` and is exactly the wire\'s `custom`.', + goal='Check that no model was missed: every public model\'s custom data is named `custom` and is exactly the ' + 'wire\'s `custom`.', decisions=[ - '**Which models are left.** Each group renames the models it migrates, polls first; this pass renames ' - 'whatever remains, and the persistence `extra_data` columns with them.', - '**How to stage the remaining renames.** Polls did a hard rename; decide whether the widely used models ' - '(`Message`, `ChannelModel`, `User`) get a deprecated `extraData` alias for a release.', '**What replaces `topLevelFields` and `Serializer`.** The v1 decoders still split flat JSON by a key ' 'list until the WebSocket moves to v2. A model that loses its JSON drops `topLevelFields` and its ' 'converter owns the list, as polls did; decide where `Serializer` goes once the last decoder does.', ], + taken=textwrap.dedent("""\ + - **Renames nothing of its own.** Each group renames the models it maps and their persistence + columns, and promotes the fields they park, as it goes (README rule 9); polls went first. A model + this check finds still saying `extraData` goes back to the group that maps it. + """), done=textwrap.dedent("""\ - [ ] No mapper under `lib/src/repository/mapper/` filters `custom`: `grep -rn _shadowedCustomKeys` is empty. - [ ] No model writes a typed field into `extraData`, and no getter reads a typed field out of it. - [ ] No public model exposes `topLevelFields`; each v1 decoder owns its key list. - [ ] Every public model names its custom data the same way, in the models, the mappers, the v1 decoders and the persistence columns. - - [ ] `migrations/v11-migration.md`: Symbol Map rows plus a feature section. - - [ ] CHANGELOG entry under `🛑️ Breaking` for each break; PR title `refactor(llc)!:`. - [ ] Decisions recorded in this file, and the status box ticked in `README.md`. """), risks=[ '`Serializer`, the models\' `topLevelFields` and the persistence `extra_data` columns name the ' 'concept too.', - 'A consumer reading `channel.extraData[\'hidden\']` or `user.extraData[\'name\']` loses the key; none in ' - 'this repo. Filter fields over custom keys (`UserFilterField.email`) are unaffected.', - 'Runs after groups 09 and 11 at the earliest, once `User` and `ChannelModel` promote their parked ' - 'fields.', + 'Runs last, after every group that maps a model.', ], ), ] @@ -830,7 +958,7 @@ def hand_for(group, hand): def render(g, hand, ops): hand_methods = hand_for(g, hand) - gops = [o for o in ops if g['match'](o[1])] + gops = [o for o in ops if g['match'](o[0], o[1])] L = [ f"# {g['num']} — {g['title']}\n", f"**Goal:** {g['goal']}\n", @@ -907,7 +1035,7 @@ def main(): claimed_ops = {} claimed_methods = {} for g in GROUPS: - for o in [o for o in ops if g['match'](o[1])]: + for o in [o for o in ops if g['match'](o[0], o[1])]: claimed_ops.setdefault((o[0], o[1]), []).append(g['num']) for f, n, _ in hand_for(g, hand): claimed_methods.setdefault((f, n), []).append(g['num']) @@ -925,6 +1053,8 @@ def main(): problems.append(f'method claimed by {v}: {k[0]}::{k[1]}') for k in sorted(all_methods - set(claimed_methods)): problems.append(f'method unclaimed: {k[0]}::{k[1]}') + for spec in sorted(EXACT_OPS - {f'{o[0]} {o[1]}' for o in ops}): + problems.append(f'exact operation matches nothing in the generated client: {spec}') for line in generated_exports(): problems.append(f'barrel exports the generated client: {line}') for f, line in generated_imports(): diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index a31e8f652a..d71658ade0 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -96,6 +96,17 @@ - `SendAttachmentResponse`, `SendFileResponse`, `SendImageResponse`, `UploadImageResponse` and `UploadFileResponse` are removed in favor of `UploadedFile`. - `AttachmentFileUploaderProvider` receives the client's `Dio` instead of a `StreamHttpClient`, and `StreamAttachmentFileUploader` is built from one. - `StreamChatApi.fileUploader` and `StreamChatApi`'s `attachmentFileUploaderProvider` are removed; pass `attachmentFileUploaderProvider` to `StreamChatClient` instead. +- The partial channel updates on `StreamChatClient` and `Channel`, slow mode included, return a `Result` instead of throwing. +- `PartialUpdateChannelResponse` is renamed `UpdateChannelPartialResponse`, and `PartialUpdateMemberResponse` is renamed `UpdateMemberPartialResponse`. +- `StreamChatClient.partialMemberUpdate` is renamed `updateMemberPartial`, and `enableSlowdown` and `disableSlowdown` are renamed `enableSlowMode` and `disableSlowMode`. +- The partial membership updates on `StreamChatClient` and `Channel`, pinning and archiving included, return a `Result` instead of throwing; the `Channel` ones no longer return the bare `Member`. +- `UpdateChannelPartialResponse` and `UpdateMemberPartialResponse` no longer decode from JSON, are immutable, built through a const constructor, compare by value, gain `copyWith`, and their `duration` is a non-nullable `String`. +- `UpdateChannelPartialResponse.channel` and `UpdateMemberPartialResponse.channelMember` are nullable, and `UpdateChannelPartialResponse.members` is a non-nullable list. +- `StreamChatApi.channel`'s partial update and slow mode methods are removed; use the `StreamChatClient` methods instead. +- `MemberUpdatePayload` and `MemberUpdateType` are removed; pass `set: {'pinned': true}` or `unset: ['pinned']` (and `archived`) to `updateMemberPartial`, or call `pinChannel`, `archiveChannel` and their counterparts. +- `ChannelModel.extraData` and `Member.extraData` are renamed `custom`, and so are `Channel.extraData`, `Channel.extraDataStream` (now `customStream`), and the `extraData` argument of the `Channel` constructor and of `StreamChatClient.channel`. +- Several channel and member values the custom data used to hold, such as `ChannelModel.hidden`, `ChannelModel.truncatedAt` and `Member.status`, are fields instead: `custom` no longer holds them, `toJson` no longer writes them, and an entry with one of their names in `custom` or in `copyWith(custom:)` no longer sets the field. +- `ChannelModel.custom` and `Member.custom` hold the custom data exactly as the API returns it, and `toJson` leaves out an entry named like one of the model's own fields. 🐞 Fixed @@ -125,6 +136,7 @@ - Added optional `reason` and `custom` arguments to `flagMessage` and `flagUser`, recorded with the flag. - Added an optional `timeout` to `StreamChatClient.moderation.muteUser` that expires the mute. - Added `StreamChatClient.moderation.muteUsers` and `unmuteUsers`, which mute and unmute several users in one call and report the ids that matched no user. +- Added `ChannelModel.muted`, `blocked`, `truncatedBy`, `autoTranslationEnabled` and `autoTranslationLanguage`, and `Member.notificationsMuted`, `status`, `banFromFutureChannels`, `futureChannelBanExpires` and `deletedAt`; the constructors and `copyWith` accept the new fields. - Added a `prev` argument to `queryPolls` and `queryPollVotes`, and a `prev` cursor on their responses, for paging backwards. 🔒 Security diff --git a/packages/stream_chat/lib/src/client/channel/channel.dart b/packages/stream_chat/lib/src/client/channel/channel.dart index 62c7ac30dc..6ce116e076 100644 --- a/packages/stream_chat/lib/src/client/channel/channel.dart +++ b/packages/stream_chat/lib/src/client/channel/channel.dart @@ -19,11 +19,11 @@ const incomingTypingStartEventTimeout = 7; /// /// {@template name} /// If an optional [name] argument is provided in the constructor then it -/// will be set on [extraData] with a key of 'name'. +/// will be set on [custom] with a key of 'name'. /// /// ```dart /// final channel = Channel(client, type, id, name: 'Channel name'); -/// print(channel.name == channel.extraData['name']); // true +/// print(channel.name == channel.custom['name']); // true /// ``` /// /// Before the channel is initialized the name can be set directly: @@ -43,11 +43,11 @@ const incomingTypingStartEventTimeout = 7; /// /// {@template image} /// If an optional [image] argument is provided in the constructor then it -/// will be set on [extraData] with a key of 'image'. +/// will be set on [custom] with a key of 'image'. /// /// ```dart /// final channel = Channel(client, type, id, image: 'https://getstream.io/image.png'); -/// print(channel.image == channel.extraData['image']); // true +/// print(channel.image == channel.custom['image']); // true /// ``` /// /// Before the channel is initialized the image can be set directly: @@ -65,18 +65,19 @@ const incomingTypingStartEventTimeout = 7; class Channel { /// Class that manages a specific channel. /// - /// Optional [extraData] and [image] properties can be provided. The [image] - /// is exposed to easily set a key of 'image' on [extraData]. + /// The channel starts with the custom data [custom]; a [name] or [image] is + /// stored in it under the `name` or `image` key, replacing any entry [custom] + /// has for that key. Channel( this._client, this._type, this._id, { String? name, String? image, - Map? extraData, + Map? custom, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = { - ...?extraData, + _custom = { + ...?custom, if (name != null) 'name': name, if (image != null) 'image': image, } { @@ -92,7 +93,7 @@ class Channel { _id = channelState.channel!.id, _type = channelState.channel!.type, _cid = channelState.channel!.cid, - _extraData = channelState.channel!.extraData { + _custom = channelState.channel!.custom { _initState(channelState); // Initialize the state immediately. } @@ -106,7 +107,7 @@ class Channel { String? _id; String? _cid; - final Map _extraData; + final Map _custom; /// Shortcut to set channel name. /// @@ -118,7 +119,7 @@ class Channel { 'to update the channel name', ); } - _extraData.addAll({'name': name}); + _custom.addAll({'name': name}); } /// Shortcut to set channel image. @@ -131,17 +132,17 @@ class Channel { 'to update the channel image', ); } - _extraData.addAll({'image': image}); + _custom.addAll({'image': image}); } - set extraData(Map extraData) { + set custom(Map custom) { if (_isInitialized) { throw StateError( 'Once the channel is initialized you should use `channel.update` ' 'to update channel data', ); } - _extraData.addAll(extraData); + _custom.addAll(custom); } /// Whether this channel is identified by its member set rather than an @@ -524,14 +525,11 @@ class Channel { return state!.channelState.channel?.team; } - /// Channel extra data. - Map get extraData { - var data = state?.channelState.channel?.extraData; - if (data == null || data.isEmpty) { - data = _extraData; - } - return data; - } + /// This channel's custom data. + /// + /// Before the channel is initialized, this is the custom data it was created + /// with, updated by any assignment to [name], [image] or [custom]. + Map get custom => state?.channelState.channel?.custom ?? _custom; /// List of user permissions on this channel List get ownCapabilities => state?.channelState.channel?.ownCapabilities ?? []; @@ -542,18 +540,18 @@ class Channel { return state!.channelStateStream.map((cs) => cs.channel?.ownCapabilities ?? []).distinct(); } - /// Channel extra data as a stream. - Stream> get extraDataStream { + /// Channel custom data as a stream. + Stream> get customStream { _checkInitialized(); return state!.channelStateStream.map( - (cs) => cs.channel?.extraData ?? _extraData, + (cs) => cs.channel?.custom ?? _custom, ); } /// Shortcut to get channel name. /// /// {@macro name} - String? get name => extraData['name'] as String?; + String? get name => custom['name'] as String?; /// Channel [name] as a stream. /// @@ -562,13 +560,13 @@ class Channel { /// {@macro name} Stream get nameStream { _checkInitialized(); - return extraDataStream.map((it) => it['name'] as String?).distinct(); + return customStream.map((it) => it['name'] as String?).distinct(); } /// Shortcut to get channel image. /// /// {@macro image} - String? get image => extraData['image'] as String?; + String? get image => custom['image'] as String?; /// Channel [image] as a stream. /// @@ -577,7 +575,7 @@ class Channel { /// {@macro image} Stream get imageStream { _checkInitialized(); - return extraDataStream.map((it) => it['image'] as String?).distinct(); + return customStream.map((it) => it['image'] as String?).distinct(); } /// The main Stream chat client. @@ -1712,39 +1710,15 @@ class Channel { ); } - /// Update the channel's [name]. - /// - /// This is the same as calling [updatePartial] and providing a map with a - /// 'name' key: - /// - /// ```dart - /// channel.updatePartial( - /// set: {'name': 'Updated channel name'} - /// ); - /// ``` + /// Sets this channel's [name], leaving every other field as it is. /// - /// Instead do: - /// ```dart - /// channel.updateName('Updated channel name'); - /// ``` - Future updateName(String name) => updatePartial(set: {'name': name}); + /// The same as [updatePartial] with `set: {'name': name}`. + Future> updateName(String name) => updatePartial(set: {'name': name}); - /// Update the channel's [image]. + /// Sets this channel's [image] URL, leaving every other field as it is. /// - /// This is the same as calling [updatePartial] and providing a map with an - /// 'image' key: - /// - /// ```dart - /// channel.updatePartial( - /// set: {'image': 'https://getstream.io/new-image'} - /// ); - /// ``` - /// - /// Instead do: - /// ```dart - /// channel.updateImage('https://getstream.io/new-image'); - /// ``` - Future updateImage(String image) => updatePartial(set: {'image': image}); + /// The same as [updatePartial] with `set: {'image': image}`. + Future> updateImage(String image) => updatePartial(set: {'image': image}); /// Update the channel custom data. This replaces all of the channel data /// with the given [channelData]. @@ -1766,19 +1740,13 @@ class Channel { ); } - /// A partial update can be used to set and unset specific custom data fields - /// when it is necessary to retain additional custom data fields on the - /// object. - /// - /// - [set] will add, or update existing attributes. - /// - [unset] will remove the attributes with the provided list of - /// values (keys). + /// Sets the fields in [set] and removes the fields named in [unset] on this + /// channel, leaving every other field as it is. /// - /// If you want to do a full update/replacement, use [update] instead. + /// At least one of [set] and [unset] is required. /// - /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart - /// for more information. - Future updatePartial({ + /// To replace every field, consider [update]. + Future> updatePartial({ Map? set, List? unset, }) async { @@ -1786,18 +1754,19 @@ class Channel { return _client.updateChannelPartial(id!, type, set: set, unset: unset); } - /// Enable slow mode - Future enableSlowMode({ + /// Enables slow mode on this channel, so members wait [cooldownInterval] + /// seconds between messages. + Future> enableSlowMode({ required int cooldownInterval, }) async { _checkInitialized(); - return _client.enableSlowdown(id!, type, cooldownInterval); + return _client.enableSlowMode(id!, type, cooldownInterval); } - /// Disable slow mode - Future disableSlowMode() async { + /// Disables slow mode on this channel. + Future> disableSlowMode() async { _checkInitialized(); - return _client.disableSlowdown(id!, type); + return _client.disableSlowMode(id!, type); } /// Delete this channel. Messages are permanently removed. @@ -2197,7 +2166,7 @@ class Channel { channelState ??= await _client.queryChannel( type, channelId: id, - channelData: _extraData, + channelData: _custom, state: state, watch: watch, presence: presence, @@ -2403,52 +2372,28 @@ class Channel { return _client.showChannel(id!, type); } - /// Pins the channel for the current user. - Future pin() async { + /// Pins this channel for the current user. + Future> pin() async { _checkInitialized(); - - final response = await _client.pinChannel( - channelId: id!, - channelType: type, - ); - - return response.channelMember; + return _client.pinChannel(channelId: id!, channelType: type); } - /// Unpins the channel. - Future unpin() async { + /// Unpins this channel for the current user. + Future> unpin() async { _checkInitialized(); - - final response = await _client.unpinChannel( - channelId: id!, - channelType: type, - ); - - return response.channelMember; + return _client.unpinChannel(channelId: id!, channelType: type); } - /// Archives the channel. - Future archive() async { + /// Archives this channel for the current user. + Future> archive() async { _checkInitialized(); - - final response = await _client.archiveChannel( - channelId: id!, - channelType: type, - ); - - return response.channelMember; + return _client.archiveChannel(channelId: id!, channelType: type); } - /// Unarchives the channel for the current user. - Future unarchive() async { + /// Unarchives this channel for the current user. + Future> unarchive() async { _checkInitialized(); - - final response = await _client.unarchiveChannel( - channelId: id!, - channelType: type, - ); - - return response.channelMember; + return _client.unarchiveChannel(channelId: id!, channelType: type); } /// Stream of [Event] coming from websocket connection specific for the diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index ab60fc33b7..70e266c111 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -75,6 +75,8 @@ import '../core/models/response/query_polls_response.dart'; 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/update_channel_partial_response.dart'; +import '../core/models/response/update_member_partial_response.dart'; import '../core/models/response/update_user_group_response.dart'; import '../core/models/role_type.dart'; import '../core/models/thread.dart'; @@ -86,6 +88,7 @@ import '../core/util/utils.dart'; import '../db/chat_persistence_client.dart'; import '../event_type.dart'; import '../repository/app_settings_repository.dart'; +import '../repository/channels_repository.dart'; import '../repository/devices_repository.dart'; import '../repository/general_repository.dart'; import '../repository/moderation_repository.dart'; @@ -196,6 +199,7 @@ class StreamChatClient { _pollsRepository = PollsRepository(api); _generalRepository = GeneralRepository(api); _moderationRepository = ModerationRepository(api); + _channelsRepository = ChannelsRepository(api); _appSettingsManager = AppSettingsManager(AppSettingsRepository(api)); moderation = ModerationClient(_moderationRepository); @@ -240,6 +244,7 @@ class StreamChatClient { late final PollsRepository _pollsRepository; late final GeneralRepository _generalRepository; late final ModerationRepository _moderationRepository; + late final ChannelsRepository _channelsRepository; late final AppSettingsManager _appSettingsManager; /// Muting, banning and flagging, for the connected user. @@ -1246,16 +1251,18 @@ class StreamChatClient { message: message, ); - /// Partial update for the [channelId] of type [ChannelType]. Sets the - /// data provided in [set], and removes the attributes given in [unset]. + /// Partially updates a channel: sets the fields in [set] and removes the + /// fields named in [unset], leaving every other field as it is. + /// + /// At least one of [set] and [unset] is required. /// /// Use [updateChannel] for a full update. - Future updateChannelPartial( + Future> updateChannelPartial( String channelId, String channelType, { Map? set, List? unset, - }) => _chatApi.channel.updateChannelPartial( + }) => _channelsRepository.updateChannelPartial( channelId, channelType, set: set, @@ -1367,12 +1374,12 @@ class StreamChatClient { Channel channel( String type, { String? id, - Map? extraData, + Map? custom, }) { if (id != null && state.channels.containsKey('$type:$id')) { return state.channels['$type:$id']!; } - return Channel(this, type, id, extraData: extraData); + return Channel(this, type, id, custom: custom); } /// Creates a new channel @@ -2158,24 +2165,26 @@ class StreamChatClient { ); } - /// Enables slow mode - Future enableSlowdown( + /// Enables slow mode on a channel, so members wait [cooldown] seconds between + /// messages. + Future> enableSlowMode( String channelId, String channelType, int cooldown, - ) async => _chatApi.channel.enableSlowdown( + ) => _channelsRepository.updateChannelPartial( channelId, channelType, - cooldown, + set: {'cooldown': cooldown}, ); - /// Disables slow mode - Future disableSlowdown( + /// Disables slow mode on a channel. + Future> disableSlowMode( String channelId, String channelType, - ) async => _chatApi.channel.disableSlowdown( + ) => _channelsRepository.updateChannelPartial( channelId, channelType, + set: const {'cooldown': 0}, ); /// Pins provided message @@ -2272,59 +2281,59 @@ class StreamChatClient { ); /// Pins the channel for the current user. - Future pinChannel({ + Future> pinChannel({ required String channelId, required String channelType, }) { - return partialMemberUpdate( + return updateMemberPartial( channelId: channelId, channelType: channelType, - set: const MemberUpdatePayload(pinned: true).toJson(), + set: const {'pinned': true}, ); } /// Unpins the channel for the current user. - Future unpinChannel({ + Future> unpinChannel({ required String channelId, required String channelType, }) { - return partialMemberUpdate( + return updateMemberPartial( channelId: channelId, channelType: channelType, - unset: [MemberUpdateType.pinned.name], + unset: const ['pinned'], ); } /// Archives the channel for the current user. - Future archiveChannel({ + Future> archiveChannel({ required String channelId, required String channelType, }) { - return partialMemberUpdate( + return updateMemberPartial( channelId: channelId, channelType: channelType, - set: const MemberUpdatePayload(archived: true).toJson(), + set: const {'archived': true}, ); } /// Unarchives the channel for the current user. - Future unarchiveChannel({ + Future> unarchiveChannel({ required String channelId, required String channelType, }) { - return partialMemberUpdate( + return updateMemberPartial( channelId: channelId, channelType: channelType, - unset: [MemberUpdateType.archived.name], + unset: const ['archived'], ); } - /// Partially updates the member of the given channel. + /// Partially updates the current user's membership of a channel: sets the + /// fields in [set] and removes the fields named in [unset], leaving every + /// other field as it is. /// - /// Use [set] to define values to be set. - /// Use [unset] to define values to be unset. - /// When [userId] is not provided, the current user will be used. - Future partialMemberUpdate({ + /// At least one of [set] and [unset] is required. + Future> updateMemberPartial({ required String channelId, required String channelType, Map? set, @@ -2332,9 +2341,9 @@ class StreamChatClient { }) { assert(set != null || unset != null, 'Set or unset must be provided.'); - return _chatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, + return _channelsRepository.updateMemberPartial( + channelId, + channelType, set: set, unset: unset, ); diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 45f077fa0b..14a762c818 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -115,54 +115,6 @@ class ChannelApi { return UpdateChannelResponse.fromJson(response.data); } - /// Updates the [channelId] of type [ChannelType] data with [data] - Future updateChannelPartial( - String channelId, - String channelType, { - Map? set, - List? unset, - }) async { - final response = await _client.patch( - _getChannelUrl(channelId, channelType), - data: { - if (set != null) 'set': set, - if (unset != null) 'unset': unset, - }, - ); - return PartialUpdateChannelResponse.fromJson(response.data); - } - - /// Enable slowdown - Future enableSlowdown( - String channelId, - String channelType, - int cooldown, - ) async { - final response = await updateChannelPartial( - channelId, - channelType, - set: { - 'cooldown': cooldown, - }, - ); - return response; - } - - /// Disable slowdown - Future disableSlowdown( - String channelId, - String channelType, - ) async { - final response = await updateChannelPartial( - channelId, - channelType, - set: { - 'cooldown': 0, - }, - ); - return response; - } - /// Accept invitation to the channel Future acceptChannelInvite( String channelId, @@ -406,26 +358,6 @@ class ChannelApi { return EmptyResponse.fromJson(response.data); } - /// Updates some of the member data - Future updateMemberPartial({ - required String channelId, - required String channelType, - Map? set, - List? unset, - }) async { - final response = await _client.patch( - // Note: user_id is not required for client side Apis as it can be fetched - // directly from the user token but, for the api path is built with it - // so we need to pass it as a placeholder. - '${_getChannelUrl(channelId, channelType)}/member/{user_id}', - data: { - if (set != null) 'set': set, - if (unset != null) 'unset': unset, - }, - ); - return PartialUpdateMemberResponse.fromJson(response.data); - } - /// Sends delivery receipts for the latest messages in multiple channels. /// /// Accepts up to 100 channels per call. diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 8d61e0434a..26ea16f9d7 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -206,31 +206,3 @@ class ThreadOptions extends Equatable { @override List get props => [watch, replyLimit, participantLimit, memberLimit]; } - -/// Payload for updating a member. -@JsonSerializable(createFactory: false, includeIfNull: false) -class MemberUpdatePayload { - /// Creates a new MemberUpdatePayload instance. - const MemberUpdatePayload({ - this.archived, - this.pinned, - }); - - /// Set to true to archive the channel for a user. - final bool? archived; - - /// Set to true to pin the channel for a user. - final bool? pinned; - - /// Serialize model to json - Map toJson() => _$MemberUpdatePayloadToJson(this); -} - -/// Type of member update to unset. -enum MemberUpdateType { - /// Unset the archived flag. - archived, - - /// Unset the pinned flag. - pinned, -} diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index 466725b861..8d2380876a 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -62,10 +62,3 @@ Map _$ThreadOptionsToJson(ThreadOptions instance) => _$MemberUpdatePayloadToJson( - MemberUpdatePayload instance, -) => { - 'archived': ?instance.archived, - 'pinned': ?instance.pinned, -}; diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 5e11c26205..27be49ab59 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -100,17 +100,6 @@ class QueryMembersResponse extends _BaseResponse { static QueryMembersResponse fromJson(Map json) => _$QueryMembersResponseFromJson(json); } -/// Model response for update member API calls, such as -/// [StreamChatClient.updateMemberPartial] -@JsonSerializable(createToJson: false) -class PartialUpdateMemberResponse extends _BaseResponse { - /// The updated member state - late Member channelMember; - - /// Create a new instance from a json - static PartialUpdateMemberResponse fromJson(Map json) => _$PartialUpdateMemberResponseFromJson(json); -} - /// Model response for [StreamChatClient.queryUsers] api call @JsonSerializable(createToJson: false) class QueryUsersResponse extends _BaseResponse { @@ -262,20 +251,6 @@ class UpdateChannelResponse extends _BaseResponse { static UpdateChannelResponse fromJson(Map json) => _$UpdateChannelResponseFromJson(json); } -/// Model response for [Channel.updatePartial] api call -@JsonSerializable(createToJson: false) -class PartialUpdateChannelResponse extends _BaseResponse { - /// Updated channel - late ChannelModel channel; - - /// Channel members - List? members; - - /// Create a new instance from a json - static PartialUpdateChannelResponse fromJson(Map json) => - _$PartialUpdateChannelResponseFromJson(json); -} - /// Model response for [Channel.inviteMembers] api call @JsonSerializable(createToJson: false) class InviteMembersResponse 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 1bf1e36e76..2626f9d87a 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -25,48 +25,59 @@ SyncResponse _$SyncResponseFromJson(Map json) => SyncResponse() ..duration = json['duration'] as String? ..events = (json['events'] as List?)?.map((e) => Event.fromJson(e as Map)).toList() ?? []; -QueryChannelsResponse _$QueryChannelsResponseFromJson(Map json) => QueryChannelsResponse() +QueryChannelsResponse _$QueryChannelsResponseFromJson( + Map json, +) => QueryChannelsResponse() ..duration = json['duration'] as String? ..channels = (json['channels'] as List?)?.map((e) => ChannelState.fromJson(e as Map)).toList() ?? [] ..predefinedFilter = json['predefined_filter'] == null ? null - : PredefinedFilter.fromJson(json['predefined_filter'] as Map); + : PredefinedFilter.fromJson( + json['predefined_filter'] as Map, + ); -TranslateMessageResponse _$TranslateMessageResponseFromJson(Map json) => TranslateMessageResponse() +TranslateMessageResponse _$TranslateMessageResponseFromJson( + Map json, +) => TranslateMessageResponse() ..duration = json['duration'] as String? ..message = Message.fromJson(json['message'] as Map); -QueryMembersResponse _$QueryMembersResponseFromJson(Map json) => QueryMembersResponse() +QueryMembersResponse _$QueryMembersResponseFromJson( + Map json, +) => QueryMembersResponse() ..duration = json['duration'] as String? ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? []; -PartialUpdateMemberResponse _$PartialUpdateMemberResponseFromJson(Map json) => - PartialUpdateMemberResponse() - ..duration = json['duration'] as String? - ..channelMember = Member.fromJson(json['channel_member'] as Map); - QueryUsersResponse _$QueryUsersResponseFromJson(Map json) => QueryUsersResponse() ..duration = json['duration'] as String? ..users = (json['users'] as List?)?.map((e) => User.fromJson(e as Map)).toList() ?? []; -QueryBannedUsersResponse _$QueryBannedUsersResponseFromJson(Map json) => QueryBannedUsersResponse() +QueryBannedUsersResponse _$QueryBannedUsersResponseFromJson( + Map json, +) => QueryBannedUsersResponse() ..duration = json['duration'] as String? ..bans = (json['bans'] as List?)?.map((e) => BannedUser.fromJson(e as Map)).toList() ?? []; -QueryReactionsResponse _$QueryReactionsResponseFromJson(Map json) => QueryReactionsResponse() +QueryReactionsResponse _$QueryReactionsResponseFromJson( + Map json, +) => QueryReactionsResponse() ..duration = json['duration'] as String? ..reactions = (json['reactions'] as List?)?.map((e) => Reaction.fromJson(e as Map)).toList() ?? [] ..next = json['next'] as String?; -QueryRepliesResponse _$QueryRepliesResponseFromJson(Map json) => QueryRepliesResponse() +QueryRepliesResponse _$QueryRepliesResponseFromJson( + Map json, +) => QueryRepliesResponse() ..duration = json['duration'] as String? ..messages = (json['messages'] as List?)?.map((e) => Message.fromJson(e as Map)).toList() ?? []; -SendReactionResponse _$SendReactionResponseFromJson(Map json) => SendReactionResponse() +SendReactionResponse _$SendReactionResponseFromJson( + Map json, +) => SendReactionResponse() ..duration = json['duration'] as String? ..message = Message.fromJson(json['message'] as Map) ..reaction = Reaction.fromJson(json['reaction'] as Map); @@ -74,10 +85,14 @@ SendReactionResponse _$SendReactionResponseFromJson(Map json) = UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) => UpdateUsersResponse() ..duration = json['duration'] as String? ..users = - (json['users'] as Map?)?.map((k, e) => MapEntry(k, User.fromJson(e as Map))) ?? + (json['users'] as Map?)?.map( + (k, e) => MapEntry(k, User.fromJson(e as Map)), + ) ?? {}; -UpdateMessageResponse _$UpdateMessageResponseFromJson(Map json) => UpdateMessageResponse() +UpdateMessageResponse _$UpdateMessageResponseFromJson( + Map json, +) => UpdateMessageResponse() ..duration = json['duration'] as String? ..message = Message.fromJson(json['message'] as Map); @@ -90,7 +105,9 @@ GetMessageResponse _$GetMessageResponseFromJson(Map json) => Ge ..message = Message.fromJson(json['message'] as Map) ..channel = json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map); -SearchMessagesResponse _$SearchMessagesResponseFromJson(Map json) => SearchMessagesResponse() +SearchMessagesResponse _$SearchMessagesResponseFromJson( + Map json, +) => SearchMessagesResponse() ..duration = json['duration'] as String? ..results = (json['results'] as List?) @@ -100,30 +117,32 @@ SearchMessagesResponse _$SearchMessagesResponseFromJson(Map jso ..next = json['next'] as String? ..previous = json['previous'] as String?; -GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(Map json) => GetMessagesByIdResponse() +GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson( + Map json, +) => GetMessagesByIdResponse() ..duration = json['duration'] as String? ..messages = (json['messages'] as List?)?.map((e) => Message.fromJson(e as Map)).toList() ?? []; -UpdateChannelResponse _$UpdateChannelResponseFromJson(Map json) => UpdateChannelResponse() +UpdateChannelResponse _$UpdateChannelResponseFromJson( + Map json, +) => UpdateChannelResponse() ..duration = json['duration'] as String? ..channel = ChannelModel.fromJson(json['channel'] as Map) ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); -PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson(Map json) => - PartialUpdateChannelResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList(); - -InviteMembersResponse _$InviteMembersResponseFromJson(Map json) => InviteMembersResponse() +InviteMembersResponse _$InviteMembersResponseFromJson( + Map json, +) => InviteMembersResponse() ..duration = json['duration'] as String? ..channel = ChannelModel.fromJson(json['channel'] as Map) ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); -RemoveMembersResponse _$RemoveMembersResponseFromJson(Map json) => RemoveMembersResponse() +RemoveMembersResponse _$RemoveMembersResponseFromJson( + Map json, +) => RemoveMembersResponse() ..duration = json['duration'] as String? ..channel = ChannelModel.fromJson(json['channel'] as Map) ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] @@ -139,13 +158,17 @@ AddMembersResponse _$AddMembersResponseFromJson(Map json) => Ad ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); -AcceptInviteResponse _$AcceptInviteResponseFromJson(Map json) => AcceptInviteResponse() +AcceptInviteResponse _$AcceptInviteResponseFromJson( + Map json, +) => AcceptInviteResponse() ..duration = json['duration'] as String? ..channel = ChannelModel.fromJson(json['channel'] as Map) ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); -RejectInviteResponse _$RejectInviteResponseFromJson(Map json) => RejectInviteResponse() +RejectInviteResponse _$RejectInviteResponseFromJson( + Map json, +) => RejectInviteResponse() ..duration = json['duration'] as String? ..channel = ChannelModel.fromJson(json['channel'] as Map) ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] @@ -154,7 +177,9 @@ RejectInviteResponse _$RejectInviteResponseFromJson(Map json) = EmptyResponse _$EmptyResponseFromJson(Map json) => EmptyResponse()..duration = json['duration'] as String?; -ChannelStateResponse _$ChannelStateResponseFromJson(Map json) => ChannelStateResponse() +ChannelStateResponse _$ChannelStateResponseFromJson( + Map json, +) => ChannelStateResponse() ..duration = json['duration'] as String? ..channel = ChannelModel.fromJson(json['channel'] as Map) ..messages = @@ -169,7 +194,9 @@ UserBlockResponse _$UserBlockResponseFromJson(Map json) => User ..blockedUserId = json['blocked_user_id'] as String? ?? '' ..createdAt = DateTime.parse(json['created_at'] as String); -BlockedUsersResponse _$BlockedUsersResponseFromJson(Map json) => BlockedUsersResponse() +BlockedUsersResponse _$BlockedUsersResponseFromJson( + Map json, +) => BlockedUsersResponse() ..duration = json['duration'] as String? ..blocks = (json['blocks'] as List?)?.map((e) => UserBlock.fromJson(e as Map)).toList() ?? []; @@ -178,11 +205,15 @@ GetThreadResponse _$GetThreadResponseFromJson(Map json) => GetT ..duration = json['duration'] as String? ..thread = Thread.fromJson(json['thread'] as Map); -UpdateThreadResponse _$UpdateThreadResponseFromJson(Map json) => UpdateThreadResponse() +UpdateThreadResponse _$UpdateThreadResponseFromJson( + Map json, +) => UpdateThreadResponse() ..duration = json['duration'] as String? ..thread = Thread.fromJson(json['thread'] as Map); -QueryThreadsResponse _$QueryThreadsResponseFromJson(Map json) => QueryThreadsResponse() +QueryThreadsResponse _$QueryThreadsResponseFromJson( + Map json, +) => QueryThreadsResponse() ..duration = json['duration'] as String? ..threads = (json['threads'] as List?)?.map((e) => Thread.fromJson(e as Map)).toList() ?? [] ..next = json['next'] as String?; @@ -200,22 +231,34 @@ QueryDraftsResponse _$QueryDraftsResponseFromJson(Map json) => ..drafts = (json['drafts'] as List?)?.map((e) => Draft.fromJson(e as Map)).toList() ?? [] ..next = json['next'] as String?; -CreateReminderResponse _$CreateReminderResponseFromJson(Map json) => CreateReminderResponse() +CreateReminderResponse _$CreateReminderResponseFromJson( + Map json, +) => CreateReminderResponse() ..duration = json['duration'] as String? - ..reminder = MessageReminder.fromJson(json['reminder'] as Map); + ..reminder = MessageReminder.fromJson( + json['reminder'] as Map, + ); -UpdateReminderResponse _$UpdateReminderResponseFromJson(Map json) => UpdateReminderResponse() +UpdateReminderResponse _$UpdateReminderResponseFromJson( + Map json, +) => UpdateReminderResponse() ..duration = json['duration'] as String? - ..reminder = MessageReminder.fromJson(json['reminder'] as Map); + ..reminder = MessageReminder.fromJson( + json['reminder'] as Map, + ); -QueryRemindersResponse _$QueryRemindersResponseFromJson(Map json) => QueryRemindersResponse() +QueryRemindersResponse _$QueryRemindersResponseFromJson( + Map json, +) => QueryRemindersResponse() ..duration = json['duration'] as String? ..reminders = (json['reminders'] as List?)?.map((e) => MessageReminder.fromJson(e as Map)).toList() ?? [] ..next = json['next'] as String?; -GetUnreadCountResponse _$GetUnreadCountResponseFromJson(Map json) => GetUnreadCountResponse() +GetUnreadCountResponse _$GetUnreadCountResponseFromJson( + Map json, +) => GetUnreadCountResponse() ..duration = json['duration'] as String? ..totalUnreadCount = (json['total_unread_count'] as num).toInt() ..totalUnreadThreadsCount = (json['total_unread_threads_count'] as num).toInt() @@ -232,24 +275,31 @@ GetUnreadCountResponse _$GetUnreadCountResponseFromJson(Map jso .map((e) => UnreadCountsThread.fromJson(e as Map)) .toList(); -UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson(Map json) => - UpsertPushPreferencesResponse() - ..duration = json['duration'] as String? - ..userPreferences = _userPreferencesFromJson(json['user_preferences'] as Map?) - ..userChannelPreferences = - (json['user_channel_preferences'] as Map?)?.map( +UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson( + Map json, +) => UpsertPushPreferencesResponse() + ..duration = json['duration'] as String? + ..userPreferences = _userPreferencesFromJson( + json['user_preferences'] as Map?, + ) + ..userChannelPreferences = + (json['user_channel_preferences'] as Map?)?.map( + (k, e) => MapEntry( + k, + (e as Map).map( (k, e) => MapEntry( k, - (e as Map).map( - (k, e) => MapEntry(k, ChannelPushPreference.fromJson(e as Map)), - ), + ChannelPushPreference.fromJson(e as Map), ), - ) ?? - {}; - -GetActiveLiveLocationsResponse _$GetActiveLiveLocationsResponseFromJson(Map json) => - GetActiveLiveLocationsResponse() - ..duration = json['duration'] as String? - ..activeLiveLocations = (json['active_live_locations'] as List) - .map((e) => Location.fromJson(e as Map)) - .toList(); + ), + ), + ) ?? + {}; + +GetActiveLiveLocationsResponse _$GetActiveLiveLocationsResponseFromJson( + Map json, +) => GetActiveLiveLocationsResponse() + ..duration = json['duration'] as String? + ..activeLiveLocations = (json['active_live_locations'] as List) + .map((e) => Location.fromJson(e as Map)) + .toList(); diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index df9bfc760e..d4015ad88c 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -11,7 +11,7 @@ part 'channel_model.g.dart'; /// The class that contains the information about a channel @JsonSerializable() class ChannelModel { - /// Constructor used for json serialization + /// Creates a channel model. ChannelModel({ String? id, String? type, @@ -26,16 +26,19 @@ class ChannelModel { this.deletedAt, this.memberCount = 0, this.members, - Map extraData = const {}, + Map custom = const {}, this.team, this.cooldown = 0, - bool? disabled, - bool? hidden, - bool? muted, - bool? blocked, - DateTime? truncatedAt, + this.disabled, + this.hidden, + this.muted, + this.blocked, + this.truncatedAt, this.messageCount, this.filterTags, + this.truncatedBy, + this.autoTranslationEnabled, + this.autoTranslationLanguage, }) : assert( (cid != null && cid.contains(':')) || (id != null && type != null), 'provide either a cid or an id and type', @@ -47,17 +50,7 @@ class ChannelModel { createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(), ownCapabilities = ownCapabilities?.map(ChannelCapability.new).toList(), - - // For backwards compatibility, set 'disabled', 'hidden', 'muted', - // 'blocked' and 'truncated_at' in [extraData]. - extraData = { - ...extraData, - if (disabled != null) 'disabled': disabled, - if (hidden != null) 'hidden': hidden, - if (muted != null) 'muted': muted, - if (blocked != null) 'blocked': blocked, - if (truncatedAt != null) 'truncated_at': truncatedAt.toIso8601String(), - }; + custom = {...custom}; /// Create a new instance from a json factory ChannelModel.fromJson(Map json) => _$ChannelModelFromJson( @@ -132,35 +125,44 @@ class ChannelModel { @JsonKey(includeIfNull: false) final int cooldown; - /// True if the channel is disabled - @JsonKey(includeToJson: false, includeFromJson: false) - bool? get disabled => extraData['disabled'].safeCast(); + /// Whether this channel is disabled. + @JsonKey(includeToJson: false) + final bool? disabled; - /// True if the channel is hidden - @JsonKey(includeToJson: false, includeFromJson: false) - bool? get hidden => extraData['hidden'].safeCast(); + /// Whether this channel is hidden for the current user. + @JsonKey(includeToJson: false) + final bool? hidden; - /// True if the channel is muted by the current user - @JsonKey(includeToJson: false, includeFromJson: false) - bool? get muted => extraData['muted'].safeCast(); + /// Whether this channel is muted for the current user. + @JsonKey(includeToJson: false) + final bool? muted; - /// True if the channel is blocked by the current user - @JsonKey(includeToJson: false, includeFromJson: false) - bool? get blocked => extraData['blocked'].safeCast(); + /// Whether this channel is blocked by the current user. + @JsonKey(includeToJson: false) + final bool? blocked; - /// The date of the last time channel got truncated - @JsonKey(includeToJson: false, includeFromJson: false) - DateTime? get truncatedAt { - final truncatedAt = extraData['truncated_at'].safeCast(); - if (truncatedAt != null && truncatedAt.isNotEmpty) { - return DateTime.parse(truncatedAt); - } + /// The date this channel was last truncated. + @JsonKey(includeToJson: false) + final DateTime? truncatedAt; - return null; - } + /// The user that last truncated this channel. + @JsonKey(includeToJson: false) + final User? truncatedBy; - /// Map of custom channel extraData - final Map extraData; + /// Whether messages in this channel are translated automatically. + @JsonKey(includeToJson: false) + final bool? autoTranslationEnabled; + + /// The language this channel's messages are translated into automatically. + @JsonKey(includeToJson: false) + final String? autoTranslationLanguage; + + /// This channel's custom data. + /// + /// It may hold a key that is also in [topLevelFields]; [toJson] leaves such + /// an entry out so it never overwrites the field. + @JsonKey(name: 'extra_data') + final Map custom; /// The team the channel belongs to @JsonKey(includeToJson: false) @@ -168,7 +170,7 @@ class ChannelModel { /// Shortcut for channel name String? get name { - final name = extraData['name'].safeCast(); + final name = custom['name'].safeCast(); if (name != null && name.isNotEmpty) return name; return null; @@ -187,8 +189,8 @@ class ChannelModel { @JsonKey(includeToJson: false) final List? filterTags; - /// Known top level fields. - /// Useful for [Serializer] methods. + /// The JSON keys [ChannelModel.fromJson] reads into named fields; every other + /// key is kept in [custom]. static const topLevelFields = [ 'id', 'type', @@ -207,11 +209,23 @@ class ChannelModel { 'cooldown', 'message_count', 'filter_tags', + 'disabled', + 'hidden', + 'muted', + 'blocked', + 'truncated_at', + 'truncated_by', + 'auto_translation_enabled', + 'auto_translation_language', ]; /// Serialize to json + /// + /// Writes the entries of [custom] at the root, leaving out any named like one + /// of [topLevelFields]. Map toJson() => Serializer.moveFromExtraDataToRoot( _$ChannelModelToJson(this), + topLevelFields: topLevelFields, ); /// Creates a copy of [ChannelModel] with specified attributes overridden. @@ -229,7 +243,7 @@ class ChannelModel { DateTime? deletedAt, int? memberCount, List? members, - Map? extraData, + Map? custom, String? team, int? cooldown, bool? disabled, @@ -239,6 +253,9 @@ class ChannelModel { DateTime? truncatedAt, int? messageCount, List? filterTags, + User? truncatedBy, + bool? autoTranslationEnabled, + String? autoTranslationLanguage, }) => ChannelModel( id: id ?? this.id, type: type ?? this.type, @@ -253,52 +270,55 @@ class ChannelModel { deletedAt: deletedAt ?? this.deletedAt, memberCount: memberCount ?? this.memberCount, members: members ?? this.members, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, team: team ?? this.team, cooldown: cooldown ?? this.cooldown, - disabled: disabled ?? extraData?['disabled'].safeCast() ?? this.disabled, - hidden: hidden ?? extraData?['hidden'].safeCast() ?? this.hidden, - muted: muted ?? extraData?['muted'].safeCast() ?? this.muted, - blocked: blocked ?? extraData?['blocked'].safeCast() ?? this.blocked, - truncatedAt: - truncatedAt ?? - (extraData?['truncated_at'] == null - ? null - // ignore: cast_nullable_to_non_nullable - : DateTime.parse(extraData?['truncated_at'] as String)) ?? - this.truncatedAt, + disabled: disabled ?? this.disabled, + hidden: hidden ?? this.hidden, + muted: muted ?? this.muted, + blocked: blocked ?? this.blocked, + truncatedAt: truncatedAt ?? this.truncatedAt, messageCount: messageCount ?? this.messageCount, filterTags: filterTags ?? this.filterTags, + truncatedBy: truncatedBy ?? this.truncatedBy, + autoTranslationEnabled: autoTranslationEnabled ?? this.autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage ?? this.autoTranslationLanguage, ); - /// Returns a new [ChannelModel] that is a combination of this channelModel - /// and the given [other] channelModel. + /// Returns a copy of this channel updated with the fields [other] carries. + /// + /// A field [other] leaves out keeps its value here, except the + /// auto-translation settings: when [other] leaves them out, auto translation + /// is off. ChannelModel merge(ChannelModel? other) { if (other == null) return this; - return copyWith( + return ChannelModel( id: other.id, type: other.type, cid: other.cid, - ownCapabilities: other.ownCapabilities, + ownCapabilities: other.ownCapabilities ?? ownCapabilities, config: other.config, - createdBy: other.createdBy, + createdBy: other.createdBy ?? createdBy, frozen: other.frozen, - lastMessageAt: other.lastMessageAt, + lastMessageAt: other.lastMessageAt ?? lastMessageAt, createdAt: other.createdAt, updatedAt: other.updatedAt, - deletedAt: other.deletedAt, + deletedAt: other.deletedAt ?? deletedAt, memberCount: other.memberCount, - members: other.members, - extraData: other.extraData, - team: other.team, + members: other.members ?? members, + custom: other.custom, + team: other.team ?? team, cooldown: other.cooldown, - disabled: other.disabled, - hidden: other.hidden, - muted: other.muted, - blocked: other.blocked, - truncatedAt: other.truncatedAt, - messageCount: other.messageCount, - filterTags: other.filterTags, + disabled: other.disabled ?? disabled, + hidden: other.hidden ?? hidden, + muted: other.muted ?? muted, + blocked: other.blocked ?? blocked, + truncatedAt: other.truncatedAt ?? truncatedAt, + messageCount: other.messageCount ?? messageCount, + filterTags: other.filterTags ?? filterTags, + truncatedBy: other.truncatedBy ?? truncatedBy, + autoTranslationEnabled: other.autoTranslationEnabled ?? false, + autoTranslationLanguage: other.autoTranslationLanguage, ); } } diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index d2c1ce62eb..7f07d7cb70 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -20,11 +20,19 @@ ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), memberCount: (json['member_count'] as num?)?.toInt() ?? 0, members: (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList(), - extraData: json['extra_data'] as Map? ?? const {}, + custom: json['extra_data'] as Map? ?? const {}, team: json['team'] as String?, cooldown: (json['cooldown'] as num?)?.toInt() ?? 0, + disabled: json['disabled'] as bool?, + hidden: json['hidden'] as bool?, + muted: json['muted'] as bool?, + blocked: json['blocked'] as bool?, + truncatedAt: json['truncated_at'] == null ? null : DateTime.parse(json['truncated_at'] as String), messageCount: (json['message_count'] as num?)?.toInt(), filterTags: (json['filter_tags'] as List?)?.map((e) => e as String).toList(), + truncatedBy: json['truncated_by'] == null ? null : User.fromJson(json['truncated_by'] as Map), + autoTranslationEnabled: json['auto_translation_enabled'] as bool?, + autoTranslationLanguage: json['auto_translation_language'] as String?, ); Map _$ChannelModelToJson(ChannelModel instance) => { @@ -32,5 +40,5 @@ Map _$ChannelModelToJson(ChannelModel instance) => { /// value off an instance with [value]. ChannelFilterField(super.remote, super.value); - /// Creates a field the SDK does not model, read from [ChannelModel.extraData]. + /// Creates a field the SDK does not model, read from [ChannelModel.custom]. /// /// The fields that depend on the calling user — `joined`, `has_unread`, /// `invite`, `distinct` and `app_banned` — are reached this way. They are @@ -155,7 +155,7 @@ class ChannelFilterField extends FilterField { /// **Supported operators:** `$eq`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, /// `$contains`, `$q`, `$autocomplete` factory ChannelFilterField.custom(String remote) { - return ChannelFilterField(remote, (it) => it.channel?.extraData[remote]); + return ChannelFilterField(remote, (it) => it.channel?.custom[remote]); } // Creates a channel filter field that declares its [collectionEquality]. @@ -166,7 +166,7 @@ class ChannelFilterField extends FilterField { // ```dart // ChannelFilterField._( // 'tags', - // (it) => it.channel?.extraData['tags'], + // (it) => it.channel?.custom['tags'], // collectionEquality: .containsExactly, // ); // ``` @@ -439,14 +439,14 @@ class ChannelSortField extends SortField { /// For a name the SDK has not modelled; prefer the fields declared here. ChannelSortField(super.remote, super.localValue); - /// Creates a field the SDK does not model, read from [ChannelModel.extraData]. + /// Creates a field the SDK does not model, read from [ChannelModel.custom]. /// /// Declared only where the API accepts a custom sort field. /// /// String values are compared as they are, without the case and diacritic /// folding a declared name field applies. factory ChannelSortField.custom(String remote) { - return ChannelSortField(remote, (it) => it.channel?.extraData[remote]); + return ChannelSortField(remote, (it) => it.channel?.custom[remote]); } /// Sorts channels by their last activity. diff --git a/packages/stream_chat/lib/src/core/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index 5b46017ee4..a3b1006baf 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -13,7 +13,7 @@ part 'member.g.dart'; /// in a channel @JsonSerializable() class Member extends Equatable { - /// Constructor used for json serialization + /// Creates a member. Member({ this.user, this.inviteAcceptedAt, @@ -30,19 +30,24 @@ class Member extends Equatable { this.pinnedAt, this.archivedAt, this.deletedMessages = const [], - this.extraData = const {}, + Map custom = const {}, + this.notificationsMuted, + this.status, + this.banFromFutureChannels, + this.futureChannelBanExpires, + this.deletedAt, }) : userId = userId ?? user?.id, createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + updatedAt = updatedAt ?? DateTime.now(), + custom = {...custom}; /// Create a new instance from a json factory Member.fromJson(Map json) => _$MemberFromJson( Serializer.moveToExtraDataFromRoot(json, _topLevelFields), ); - /// Known top level fields. - /// - /// Useful for [Serializer] methods. + // The JSON keys [Member.fromJson] reads into named fields; every other key is + // kept in [custom]. static const _topLevelFields = [ 'user', 'invite_accepted_at', @@ -59,6 +64,11 @@ class Member extends Equatable { 'pinned_at', 'archived_at', 'deleted_messages', + 'notifications_muted', + 'status', + 'ban_from_future_channels', + 'future_channel_ban_expires', + 'deleted_at', ]; /// The interested user @@ -109,8 +119,37 @@ class Member extends Equatable { /// visible to other channel members. final List deletedMessages; - /// Map of custom member extraData. - final Map extraData; + /// This member's custom data. + /// + /// It may hold a key named like one of the member's own fields; [toJson] + /// leaves such an entry out so it never overwrites the field. + @JsonKey(name: 'extra_data') + final Map custom; + + /// Whether this member muted notifications for the channel. + @JsonKey(includeToJson: false) + final bool? notificationsMuted; + + /// The status of this membership, such as `member`, or `pending` while an + /// invite is outstanding. + @JsonKey(includeToJson: false) + final String? status; + + /// Whether this member is banned from future channels. + @JsonKey(includeToJson: false) + final bool? banFromFutureChannels; + + /// The date at which this member's ban from future channels expires. + /// + /// Null when there is no such ban or it has no expiry. + @JsonKey(includeToJson: false) + final DateTime? futureChannelBanExpires; + + /// The date at which this membership was deleted. + /// + /// Null while the member belongs to the channel. + @JsonKey(includeToJson: false) + final DateTime? deletedAt; /// Creates a copy of [Member] with specified attributes overridden. Member copyWith({ @@ -130,7 +169,12 @@ class Member extends Equatable { DateTime? banExpires, bool? shadowBanned, List? deletedMessages, - Map? extraData, + Map? custom, + bool? notificationsMuted, + String? status, + bool? banFromFutureChannels, + DateTime? futureChannelBanExpires, + DateTime? deletedAt, }) => Member( user: user ?? this.user, inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, @@ -147,12 +191,21 @@ class Member extends Equatable { createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, deletedMessages: deletedMessages ?? this.deletedMessages, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, + notificationsMuted: notificationsMuted ?? this.notificationsMuted, + status: status ?? this.status, + banFromFutureChannels: banFromFutureChannels ?? this.banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires ?? this.futureChannelBanExpires, + deletedAt: deletedAt ?? this.deletedAt, ); /// Serialize to json + /// + /// Writes the entries of [custom] at the root, leaving out any named like one + /// of the member's own fields. Map toJson() => Serializer.moveFromExtraDataToRoot( _$MemberToJson(this), + topLevelFields: _topLevelFields, ); @override @@ -172,7 +225,12 @@ class Member extends Equatable { createdAt, updatedAt, deletedMessages, - extraData, + custom, + notificationsMuted, + status, + banFromFutureChannels, + futureChannelBanExpires, + deletedAt, ]; } @@ -191,12 +249,12 @@ class MemberFilterField extends FilterField { /// value off an instance with [value]. MemberFilterField(super.remote, super.value); - /// Creates a field the SDK does not model, read from [Member.extraData]. + /// Creates a field the SDK does not model, read from [Member.custom]. /// /// **Supported operators:** `$eq`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`, /// `$exists`, `$contains`, `$q`, `$autocomplete` factory MemberFilterField.custom(String remote) { - return MemberFilterField(remote, (it) => it.extraData[remote]); + return MemberFilterField(remote, (it) => it.custom[remote]); } /// Filters members by their user id. @@ -296,7 +354,7 @@ class MemberFilterField extends FilterField { /// **Supported operators:** `$eq` static final notificationsMuted = MemberFilterField( 'notifications_muted', - (it) => it.extraData['notifications_muted'].safeCast(), + (it) => it.notificationsMuted, ); /// Filters members by when they joined the channel. @@ -358,11 +416,11 @@ class MemberSortField extends SortField { /// For a name the SDK has not modelled; prefer the fields declared here. MemberSortField(super.remote, super.localValue); - /// Creates a field the SDK does not model, read from [Member.extraData]. + /// Creates a field the SDK does not model, read from [Member.custom]. /// /// Declared only where the API accepts a custom sort field. factory MemberSortField.custom(String remote) { - return MemberSortField(remote, (it) => it.extraData[remote]); + return MemberSortField(remote, (it) => it.custom[remote]); } /// Sorts members by their creation date in the channel. diff --git a/packages/stream_chat/lib/src/core/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart index 730051a2db..dd0364c3cd 100644 --- a/packages/stream_chat/lib/src/core/models/member.g.dart +++ b/packages/stream_chat/lib/src/core/models/member.g.dart @@ -22,7 +22,14 @@ Member _$MemberFromJson(Map json) => Member( pinnedAt: json['pinned_at'] == null ? null : DateTime.parse(json['pinned_at'] as String), archivedAt: json['archived_at'] == null ? null : DateTime.parse(json['archived_at'] as String), deletedMessages: (json['deleted_messages'] as List?)?.map((e) => e as String).toList() ?? const [], - extraData: json['extra_data'] as Map? ?? const {}, + custom: json['extra_data'] as Map? ?? const {}, + notificationsMuted: json['notifications_muted'] as bool?, + status: json['status'] as String?, + banFromFutureChannels: json['ban_from_future_channels'] as bool?, + futureChannelBanExpires: json['future_channel_ban_expires'] == null + ? null + : DateTime.parse(json['future_channel_ban_expires'] as String), + deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), ); Map _$MemberToJson(Member instance) => { @@ -41,5 +48,5 @@ Map _$MemberToJson(Member instance) => { 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_messages': instance.deletedMessages, - 'extra_data': instance.extraData, + 'extra_data': instance.custom, }; diff --git a/packages/stream_chat/lib/src/core/models/response/update_channel_partial_response.dart b/packages/stream_chat/lib/src/core/models/response/update_channel_partial_response.dart new file mode 100644 index 0000000000..72f9e32127 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/update_channel_partial_response.dart @@ -0,0 +1,31 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../channel_model.dart'; +import '../member.dart'; + +part 'update_channel_partial_response.freezed.dart'; + +/// The result of partially updating a channel, returned by +/// [StreamChatClient.updateChannelPartial] and the calls built on it, such as +/// [Channel.updatePartial] and [Channel.enableSlowMode]. +@freezed +class UpdateChannelPartialResponse with _$UpdateChannelPartialResponse { + /// Creates a new [UpdateChannelPartialResponse]. + const UpdateChannelPartialResponse({ + required this.duration, + this.channel, + this.members = const [], + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The updated channel. + @override + final ChannelModel? channel; + + /// A page of the channel's members after the update. + @override + final List members; +} diff --git a/packages/stream_chat/lib/src/core/models/response/update_channel_partial_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/update_channel_partial_response.freezed.dart new file mode 100644 index 0000000000..f408d1e3f0 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/update_channel_partial_response.freezed.dart @@ -0,0 +1,97 @@ +// 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 'update_channel_partial_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UpdateChannelPartialResponse { + String get duration; + ChannelModel? get channel; + List get members; + + /// Create a copy of UpdateChannelPartialResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UpdateChannelPartialResponseCopyWith get copyWith => + _$UpdateChannelPartialResponseCopyWithImpl( + this as UpdateChannelPartialResponse, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UpdateChannelPartialResponse && + (identical(other.duration, duration) || other.duration == duration) && + (identical(other.channel, channel) || other.channel == channel) && + const DeepCollectionEquality().equals(other.members, members)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + duration, + channel, + const DeepCollectionEquality().hash(members), + ); + + @override + String toString() { + return 'UpdateChannelPartialResponse(duration: $duration, channel: $channel, members: $members)'; + } +} + +/// @nodoc +abstract mixin class $UpdateChannelPartialResponseCopyWith<$Res> { + factory $UpdateChannelPartialResponseCopyWith( + UpdateChannelPartialResponse value, + $Res Function(UpdateChannelPartialResponse) _then, + ) = _$UpdateChannelPartialResponseCopyWithImpl; + @useResult + $Res call({String duration, ChannelModel? channel, List members}); +} + +/// @nodoc +class _$UpdateChannelPartialResponseCopyWithImpl<$Res> implements $UpdateChannelPartialResponseCopyWith<$Res> { + _$UpdateChannelPartialResponseCopyWithImpl(this._self, this._then); + + final UpdateChannelPartialResponse _self; + final $Res Function(UpdateChannelPartialResponse) _then; + + /// Create a copy of UpdateChannelPartialResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? duration = null, + Object? channel = freezed, + Object? members = null, + }) { + return _then( + UpdateChannelPartialResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + channel: freezed == channel + ? _self.channel + : channel // ignore: cast_nullable_to_non_nullable + as ChannelModel?, + members: null == members + ? _self.members + : members // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/update_member_partial_response.dart b/packages/stream_chat/lib/src/core/models/response/update_member_partial_response.dart new file mode 100644 index 0000000000..be6f90d9d7 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/update_member_partial_response.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../member.dart'; + +part 'update_member_partial_response.freezed.dart'; + +/// The result of partially updating the current user's membership of a channel, +/// returned by [StreamChatClient.updateMemberPartial] and the calls built on +/// it, such as [Channel.pin] and [Channel.archive]. +@freezed +class UpdateMemberPartialResponse with _$UpdateMemberPartialResponse { + /// Creates a new [UpdateMemberPartialResponse]. + const UpdateMemberPartialResponse({ + required this.duration, + this.channelMember, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The updated membership. + @override + final Member? channelMember; +} diff --git a/packages/stream_chat/lib/src/core/models/response/update_member_partial_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/update_member_partial_response.freezed.dart new file mode 100644 index 0000000000..0f8cfbf156 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/update_member_partial_response.freezed.dart @@ -0,0 +1,82 @@ +// 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 'update_member_partial_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UpdateMemberPartialResponse { + String get duration; + Member? get channelMember; + + /// Create a copy of UpdateMemberPartialResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UpdateMemberPartialResponseCopyWith get copyWith => + _$UpdateMemberPartialResponseCopyWithImpl( + this as UpdateMemberPartialResponse, + _$identity, + ); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UpdateMemberPartialResponse && + (identical(other.duration, duration) || other.duration == duration) && + (identical(other.channelMember, channelMember) || other.channelMember == channelMember)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration, channelMember); + + @override + String toString() { + return 'UpdateMemberPartialResponse(duration: $duration, channelMember: $channelMember)'; + } +} + +/// @nodoc +abstract mixin class $UpdateMemberPartialResponseCopyWith<$Res> { + factory $UpdateMemberPartialResponseCopyWith( + UpdateMemberPartialResponse value, + $Res Function(UpdateMemberPartialResponse) _then, + ) = _$UpdateMemberPartialResponseCopyWithImpl; + @useResult + $Res call({String duration, Member? channelMember}); +} + +/// @nodoc +class _$UpdateMemberPartialResponseCopyWithImpl<$Res> implements $UpdateMemberPartialResponseCopyWith<$Res> { + _$UpdateMemberPartialResponseCopyWithImpl(this._self, this._then); + + final UpdateMemberPartialResponse _self; + final $Res Function(UpdateMemberPartialResponse) _then; + + /// Create a copy of UpdateMemberPartialResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null, Object? channelMember = freezed}) { + return _then( + UpdateMemberPartialResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + channelMember: freezed == channelMember + ? _self.channelMember + : channelMember // ignore: cast_nullable_to_non_nullable + as Member?, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/util/serializer.dart b/packages/stream_chat/lib/src/core/util/serializer.dart index d1db1dedd3..b0f8b02de1 100644 --- a/packages/stream_chat/lib/src/core/util/serializer.dart +++ b/packages/stream_chat/lib/src/core/util/serializer.dart @@ -42,7 +42,9 @@ class Serializer { /// A copy of [json] with the entries of `extra_data` moved to the root. /// - /// An entry in `extra_data` replaces a root entry with the same key. + /// An entry in `extra_data` replaces a root entry with the same key, unless + /// the key is one of [topLevelFields]: such an entry is left out, so custom + /// data cannot overwrite a field. /// /// ```dart /// Serializer.moveFromExtraDataToRoot({ @@ -58,12 +60,16 @@ class Serializer { /// // } /// ``` static Map moveFromExtraDataToRoot( - Map json, - ) { - final extraData = json['extra_data']; + Map json, { + List topLevelFields = const [], + }) { + final knownFields = _knownFieldsCache[topLevelFields] ??= topLevelFields.toSet(); + final extraData = json['extra_data'] as Map?; return { ...json, - if (extraData != null) ...extraData, + if (extraData != null) + for (final MapEntry(:key, :value) in extraData.entries) + if (!knownFields.contains(key)) key: value, }..remove('extra_data'); } diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 084666cbfb..01d45da43c 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -491,6 +491,7 @@ abstract class ChatPersistenceClient { users.addAll( [ channel.createdBy, + channel.truncatedBy, ...?messages?.map((it) => it.user), ...?pinnedMessages?.map((it) => it.user), ...?reads?.map((it) => it.user), diff --git a/packages/stream_chat/lib/src/repository/channels_repository.dart b/packages/stream_chat/lib/src/repository/channels_repository.dart new file mode 100644 index 0000000000..c55d8836a4 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/channels_repository.dart @@ -0,0 +1,53 @@ +import 'package:stream_core/stream_core.dart' show PatternMatching, Result; + +import '../../open_api/api.dart' as api; +import '../core/models/response/update_channel_partial_response.dart'; +import '../core/models/response/update_member_partial_response.dart'; +import 'mapper/channels_mapper.dart'; + +/// Repository dedicated to channel operations. +class ChannelsRepository { + /// Creates a new channels repository. + const ChannelsRepository(this._api); + + final api.DefaultApi _api; + + /// Partially updates a channel: sets the fields in [set] and removes the + /// fields named in [unset], leaving every other field as it is. + /// + /// At least one of [set] and [unset] is required. + Future> updateChannelPartial( + String channelId, + String channelType, { + Map? set, + List? unset, + }) async { + final result = await _api.updateChannelPartial( + type: channelType, + id: channelId, + updateChannelPartialRequest: api.UpdateChannelPartialRequest(set: set, unset: unset), + ); + + return result.map((response) => response.toModel()); + } + + /// Partially updates the current user's membership of a channel: sets the + /// fields in [set] and removes the fields named in [unset], leaving every + /// other field as it is. + /// + /// At least one of [set] and [unset] is required. + Future> updateMemberPartial( + String channelId, + String channelType, { + Map? set, + List? unset, + }) async { + final result = await _api.updateMemberPartial( + type: channelType, + id: channelId, + updateMemberPartialRequest: api.UpdateMemberPartialRequest(set: set, unset: unset), + ); + + return result.map((response) => response.toModel()); + } +} diff --git a/packages/stream_chat/lib/src/repository/mapper/channels_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/channels_mapper.dart new file mode 100644 index 0000000000..85e2475d2d --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/channels_mapper.dart @@ -0,0 +1,158 @@ +import 'package:stream_core/stream_core.dart' show Standard; + +import '../../../open_api/api.dart' as api; +import '../../core/models/channel_config.dart'; +import '../../core/models/channel_model.dart'; +import '../../core/models/chat_preferences.dart'; +import '../../core/models/command.dart'; +import '../../core/models/member.dart'; +import '../../core/models/push_level.dart'; +import '../../core/models/response/update_channel_partial_response.dart'; +import '../../core/models/response/update_member_partial_response.dart'; +import 'user_mapper.dart'; + +// TODO(openapi-migration): re-point these mappers in group 11. + +/// Maps a generated [api.ChannelResponse] to a [ChannelModel]. +extension ChannelResponseMapper on api.ChannelResponse { + /// Converts this response into a [ChannelModel]. + ChannelModel toModel() => ChannelModel( + id: id, + type: type, + cid: cid, + ownCapabilities: ownCapabilities, + config: config?.toModel(), + createdBy: createdBy?.toModel(), + frozen: frozen, + lastMessageAt: lastMessageAt, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + memberCount: memberCount ?? 0, + members: members?.map((member) => member.toModel()).toList(), + custom: custom, + team: team, + cooldown: cooldown ?? 0, + disabled: disabled, + hidden: hidden, + muted: muted, + blocked: blocked, + truncatedAt: truncatedAt, + messageCount: messageCount, + filterTags: filterTags, + truncatedBy: truncatedBy?.toModel(), + autoTranslationEnabled: autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage, + ); +} + +/// Maps a generated [api.ChannelConfigWithInfo] to a [ChannelConfig]. +extension ChannelConfigWithInfoMapper on api.ChannelConfigWithInfo { + // TODO(openapi-migration): revisit exposing the config fields ChannelConfig + // does not model yet: allowedFlagReasons, automodBehavior, + // automodThresholds, blocklist, blocklistBehavior, blocklists, + // countMessages, customEvents, grants, name, partitionSize, partitionTtl, + // quotes and reminders. + + /// Converts this configuration into a [ChannelConfig]. + ChannelConfig toModel() => ChannelConfig( + automod: automod, + commands: [for (final command in commands) command.toModel()], + connectEvents: connectEvents, + createdAt: createdAt, + updatedAt: updatedAt, + maxMessageLength: maxMessageLength, + messageRetention: messageRetention, + mutes: mutes, + reactions: reactions, + readEvents: readEvents, + replies: replies, + search: search, + polls: polls, + pushLevel: pushLevel?.let(PushLevel.new), + pushNotifications: pushNotifications, + chatPreferences: chatPreferences?.toModel(), + typingEvents: typingEvents, + uploads: uploads, + urlEnrichment: urlEnrichment, + skipLastMsgUpdateForSystemMsgs: skipLastMsgUpdateForSystemMsgs, + userMessageReminders: userMessageReminders, + markMessagesPending: markMessagesPending, + deliveryEvents: deliveryEvents, + sharedLocations: sharedLocations, + ); +} + +/// Maps a generated [api.Command] to a [Command]. +extension CommandMapper on api.Command { + /// Converts this command into a [Command]. + Command toModel() => Command( + name: name, + description: description, + args: args, + set: CommandSet(set), + ); +} + +/// Maps a generated [api.ChatPreferences] to a [ChatPreferences]. +extension ChatPreferencesMapper on api.ChatPreferences { + /// Converts these preferences into [ChatPreferences]. + ChatPreferences toModel() => ChatPreferences( + channelMentions: channelMentions?.let(ChatPreferenceLevel.new), + defaultPreference: defaultPreference?.let(ChatPreferenceLevel.new), + directMentions: directMentions?.let(ChatPreferenceLevel.new), + groupMentions: groupMentions?.let(ChatPreferenceLevel.new), + hereMentions: hereMentions?.let(ChatPreferenceLevel.new), + roleMentions: roleMentions?.let(ChatPreferenceLevel.new), + threadReplies: threadReplies?.let(ChatPreferenceLevel.new), + ); +} + +/// Maps a generated [api.ChannelMemberResponse] to a [Member]. +extension ChannelMemberResponseMapper on api.ChannelMemberResponse { + /// Converts this response into a [Member]. + Member toModel() => Member( + user: user?.toModel(), + inviteAcceptedAt: inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt, + invited: invited ?? false, + channelRole: channelRole, + userId: userId, + isModerator: isModerator ?? false, + createdAt: createdAt, + updatedAt: updatedAt, + banned: banned, + banExpires: banExpires, + shadowBanned: shadowBanned, + pinnedAt: pinnedAt, + archivedAt: archivedAt, + deletedMessages: deletedMessages ?? const [], + custom: custom, + notificationsMuted: notificationsMuted, + status: status, + banFromFutureChannels: banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires, + deletedAt: deletedAt, + ); +} + +/// Maps a generated [api.UpdateChannelPartialResponse] to an +/// [UpdateChannelPartialResponse]. +extension UpdateChannelPartialResponseMapper on api.UpdateChannelPartialResponse { + /// Converts this response into an [UpdateChannelPartialResponse]. + UpdateChannelPartialResponse toModel() => UpdateChannelPartialResponse( + duration: duration, + channel: channel?.toModel(), + members: [for (final member in members) member.toModel()], + ); +} + +/// Maps a generated [api.UpdateMemberPartialResponse] to an +/// [UpdateMemberPartialResponse]. +extension UpdateMemberPartialResponseMapper on api.UpdateMemberPartialResponse { + /// Converts this response into an [UpdateMemberPartialResponse]. + UpdateMemberPartialResponse toModel() => UpdateMemberPartialResponse( + duration: duration, + channelMember: channelMember?.toModel(), + ); +} diff --git a/packages/stream_chat/lib/src/repository/mapper/devices_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/devices_mapper.dart index 0de090a582..b0edf4bc49 100644 --- a/packages/stream_chat/lib/src/repository/mapper/devices_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/devices_mapper.dart @@ -18,8 +18,9 @@ extension ListDevicesResponseMapper on api.ListDevicesResponse { ); } -/// Maps a [PushProvider] to the generated request enum. -extension PushProviderMapper on PushProvider { +/// Maps a [PushProvider] to the generated +/// [api.CreateDeviceRequestPushProvider]. +extension PushProviderRequestMapper on PushProvider { /// Converts this provider into an [api.CreateDeviceRequestPushProvider]. api.CreateDeviceRequestPushProvider toRequest() => api.CreateDeviceRequestPushProvider.fromJson(rawType); } diff --git a/packages/stream_chat/lib/src/repository/mapper/moderation_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/moderation_mapper.dart index bc55e12eb3..fd756d53a0 100644 --- a/packages/stream_chat/lib/src/repository/mapper/moderation_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/moderation_mapper.dart @@ -31,8 +31,8 @@ extension FlagItemResponseMapper on api.FlagItemResponse { ); } -/// Maps a [DeleteType] to the generated request type. -extension DeleteTypeMapper on DeleteType { - /// Converts this treatment into an [api.BanRequestDeleteMessages]. +/// Maps a [DeleteType] to the generated [api.BanRequestDeleteMessages]. +extension DeleteTypeRequestMapper on DeleteType { + /// Converts this delete type into an [api.BanRequestDeleteMessages]. api.BanRequestDeleteMessages toRequest() => api.BanRequestDeleteMessages.fromJson(rawType); } diff --git a/packages/stream_chat/lib/src/repository/mapper/roles_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/roles_mapper.dart index d6c9609701..8059e95dfb 100644 --- a/packages/stream_chat/lib/src/repository/mapper/roles_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/roles_mapper.dart @@ -4,7 +4,7 @@ import '../../core/models/role.dart'; /// Maps a generated [api.Role] to a [Role]. extension RoleMapper on api.Role { - /// Converts this generated role into a [Role]. + /// Converts this role into a [Role]. Role toModel() => Role( createdAt: createdAt, custom: custom, diff --git a/packages/stream_chat/lib/src/repository/mapper/user_groups_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/user_groups_mapper.dart index 4788c03e18..384d01d086 100644 --- a/packages/stream_chat/lib/src/repository/mapper/user_groups_mapper.dart +++ b/packages/stream_chat/lib/src/repository/mapper/user_groups_mapper.dart @@ -28,7 +28,7 @@ extension UserGroupResponseMapper on api.UserGroupResponse { /// Maps a generated [api.UserGroupMember] to a [UserGroupMember]. extension UserGroupMemberMapper on api.UserGroupMember { - /// Converts this generated member into a [UserGroupMember]. + /// Converts this member into a [UserGroupMember]. UserGroupMember toModel() => UserGroupMember( createdAt: createdAt, groupId: groupId, diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 9b2d92fe95..f27792f2a7 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -138,6 +138,8 @@ 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/unmute_users_response.dart'; +export 'src/core/models/response/update_channel_partial_response.dart'; +export 'src/core/models/response/update_member_partial_response.dart'; export 'src/core/models/response/update_user_group_response.dart'; export 'src/core/models/role.dart'; export 'src/core/models/role_type.dart'; diff --git a/packages/stream_chat/test/fixtures/channel.json b/packages/stream_chat/test/fixtures/channel.json index 2184cd6e92..913c7edcf7 100644 --- a/packages/stream_chat/test/fixtures/channel.json +++ b/packages/stream_chat/test/fixtures/channel.json @@ -2,6 +2,20 @@ "id": "test", "type": "livestream", "cid": "livestream:test", + "disabled": false, + "hidden": true, + "muted": true, + "blocked": false, + "truncated_at": "2020-01-29T03:22:47.636130Z", + "truncated_by": { + "id": "truncator", + "name": "Truncator" + }, + "auto_translation_enabled": true, + "auto_translation_language": "fr", "cats": true, - "fruit": ["bananas", "apples"] -} \ No newline at end of file + "fruit": [ + "bananas", + "apples" + ] +} diff --git a/packages/stream_chat/test/fixtures/member.json b/packages/stream_chat/test/fixtures/member.json index 63b7ddd175..3f9834bfab 100644 --- a/packages/stream_chat/test/fixtures/member.json +++ b/packages/stream_chat/test/fixtures/member.json @@ -17,5 +17,10 @@ "msg-2", "msg-3" ], + "notifications_muted": true, + "status": "member", + "ban_from_future_channels": true, + "future_channel_ban_expires": "2020-02-28T22:17:30.95443Z", + "deleted_at": "2020-01-29T22:17:30.95443Z", "some_custom_field": "with_custom_data" -} \ No newline at end of file +} diff --git a/packages/stream_chat/test/src/client/channel/channel_client_state_test.dart b/packages/stream_chat/test/src/client/channel/channel_client_state_test.dart index 08f4852be8..767f854d88 100644 --- a/packages/stream_chat/test/src/client/channel/channel_client_state_test.dart +++ b/packages/stream_chat/test/src/client/channel/channel_client_state_test.dart @@ -1918,14 +1918,14 @@ void main() { id: channelId, type: channelType, memberCount: 42, - extraData: const {'name': 'updated-name'}, + custom: const {'name': 'updated-name'}, ), ), ); await Future.delayed(Duration.zero); expect(channel.memberCount, 42); - expect(channel.extraData['name'], 'updated-name'); + expect(channel.custom['name'], 'updated-name'); }); test('replaces the member list with the event members', () async { diff --git a/packages/stream_chat/test/src/client/channel/channel_test.dart b/packages/stream_chat/test/src/client/channel/channel_test.dart index e6838e2d60..4fa59a965d 100644 --- a/packages/stream_chat/test/src/client/channel/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel/channel_test.dart @@ -58,49 +58,49 @@ void main() { channel.dispose(); }); - test('should be able to set `extraData`', () { - expect(channel.extraData.isEmpty, isTrue); + test('should be able to set `custom`', () { + expect(channel.custom.isEmpty, isTrue); expect( - () => channel.extraData = {'name': 'test-channel-name'}, + () => channel.custom = {'name': 'test-channel-name'}, returnsNormally, ); - expect(channel.extraData.isEmpty, isFalse); - expect(channel.extraData.containsKey('name'), isTrue); - expect(channel.extraData['name'], 'test-channel-name'); + expect(channel.custom.isEmpty, isFalse); + expect(channel.custom.containsKey('name'), isTrue); + expect(channel.custom['name'], 'test-channel-name'); }); test('should be able to get and set `image`', () { - expect(channel.extraData.isEmpty, isTrue); + expect(channel.custom.isEmpty, isTrue); const imageUrl = 'https://getstream.io/some-image'; channel.image = imageUrl; expect(channel.image, imageUrl); - expect(channel.extraData['image'], imageUrl); + expect(channel.custom['image'], imageUrl); const newImage = 'https://getstream.io/new-image'; final newChannelInstance = Channel(client, channelType, channelId, image: newImage); expect(newChannelInstance.image, newImage); - expect(newChannelInstance.extraData['image'], newImage); + expect(newChannelInstance.custom['image'], newImage); }); test('should be able to get and set `name`', () { - expect(channel.extraData.isEmpty, isTrue); + expect(channel.custom.isEmpty, isTrue); const name = 'Channel name'; channel.name = name; expect(channel.name, name); - expect(channel.extraData['name'], name); + expect(channel.custom['name'], name); const newName = 'New channel name'; final newChannelInstance = Channel(client, channelType, channelId, name: newName); expect(newChannelInstance.name, newName); - expect(newChannelInstance.extraData['name'], newName); + expect(newChannelInstance.custom['name'], newName); }); test('setters remain usable after a failed watch()', () async { @@ -232,8 +232,22 @@ void main() { clearInteractions(client); }); - test('should throw if trying to set `extraData`', () { - expect(() => channel.extraData = {'name': 'test-channel-name'}, throwsA(isA())); + test('should throw if trying to set `custom`', () { + expect(() => channel.custom = {'name': 'test-channel-name'}, throwsA(isA())); + }); + + test('Channel.custom reads the current channel, even when its custom data is empty', () { + final channel = Channel.fromState( + client, + ChannelState( + channel: ChannelModel(cid: channelCid, custom: const {'name': 'Old name'}), + ), + ); + addTearDown(channel.dispose); + + channel.state!.updateChannelState(ChannelState(channel: ChannelModel(cid: channelCid))); + + expect(channel.custom, isEmpty); }); test('should throw if trying to set `image`', () { @@ -3701,7 +3715,7 @@ void main() { final channelModel = ChannelModel( cid: channelCid, - extraData: channelData, + custom: channelData, ); when(() => client.updateChannel(channelId, channelType, channelData, message: any(named: 'message'))).thenAnswer( @@ -3717,18 +3731,18 @@ void main() { expect(res, isNotNull); expect(res.channel.cid, channelModel.cid); - expect(res.channel.extraData, channelData); + expect(res.channel.custom, channelData); expect(res.message?.id, updateMessage.id); verify(() => client.updateChannel(channelId, channelType, channelData, message: any(named: 'message'))).called(1); }); - test('`.updateImage`', () async { + test('Channel.updateImage sets the image through a partial update', () async { const image = 'https://getstream.io/new-image'; - final channelModel = ChannelModel( - cid: channelCid, - extraData: {'image': image}, + final response = UpdateChannelPartialResponse( + duration: '0.01ms', + channel: ChannelModel(cid: channelCid), ); when( @@ -3737,14 +3751,11 @@ void main() { channelType, set: {'image': image}, ), - ).thenAnswer( - (_) async => PartialUpdateChannelResponse()..channel = channelModel, - ); + ).thenAnswer((_) async => Result.success(response)); final res = await channel.updateImage(image); - expect(res, isNotNull); - expect(res.channel.extraData['image'], image); + expect(res.getOrNull(), response); verify( () => client.updateChannelPartial( @@ -3755,12 +3766,12 @@ void main() { ).called(1); }); - test('`.updateName`', () async { + test('Channel.updateName sets the name through a partial update', () async { const name = 'Name'; - final channelModel = ChannelModel( - cid: channelCid, - extraData: {'name': name}, + final response = UpdateChannelPartialResponse( + duration: '0.01ms', + channel: ChannelModel(cid: channelCid), ); when( @@ -3769,14 +3780,11 @@ void main() { channelType, set: {'name': name}, ), - ).thenAnswer( - (_) async => PartialUpdateChannelResponse()..channel = channelModel, - ); + ).thenAnswer((_) async => Result.success(response)); final res = await channel.updateName(name); - expect(res, isNotNull); - expect(res.channel.extraData['name'], name); + expect(res.getOrNull(), response); verify( () => client.updateChannelPartial( @@ -3787,7 +3795,7 @@ void main() { ).called(1); }); - test('`.updatePartial`', () async { + test('Channel.updatePartial sends the set and unset fields for this channel', () async { const set = { 'name': 'Stream Team', 'profile_image': 'test-profile-image', @@ -3795,12 +3803,9 @@ void main() { const unset = ['tag', 'last_name']; - final channelModel = ChannelModel( - cid: channelCid, - extraData: { - 'coolness': 999, - ...set, - }, + final response = UpdateChannelPartialResponse( + duration: '0.01ms', + channel: ChannelModel(cid: channelCid), ); when( @@ -3810,18 +3815,11 @@ void main() { set: set, unset: unset, ), - ).thenAnswer( - (_) async => PartialUpdateChannelResponse()..channel = channelModel, - ); + ).thenAnswer((_) async => Result.success(response)); final res = await channel.updatePartial(set: set, unset: unset); - expect(res, isNotNull); - expect(res.channel.cid, channelModel.cid); - expect( - res.channel.extraData, - {'coolness': 999, ...set}, - ); + expect(res.getOrNull(), response); verify( () => client.updateChannelPartial( @@ -4699,7 +4697,7 @@ void main() { ).called(1); }); - test('`.enableSlowMode`', () async { + test('Channel.enableSlowMode enables slow mode on this channel and returns the updated channel', () async { const cooldown = 10; final channelModel = ChannelModel( @@ -4708,19 +4706,21 @@ void main() { ); when( - () => client.enableSlowdown( + () => client.enableSlowMode( channelId, channelType, cooldown, ), - ).thenAnswer((_) async => PartialUpdateChannelResponse()..channel = channelModel); + ).thenAnswer( + (_) async => Result.success(UpdateChannelPartialResponse(duration: '0.01ms', channel: channelModel)), + ); final res = await channel.enableSlowMode(cooldownInterval: 10); - expect(res, isNotNull); + expect(res.getOrNull(), UpdateChannelPartialResponse(duration: '0.01ms', channel: channelModel)); verify( - () => client.enableSlowdown( + () => client.enableSlowMode( channelId, channelType, cooldown, @@ -4728,23 +4728,25 @@ void main() { ).called(1); }); - test('`.disableSlowMode`', () async { + test('Channel.disableSlowMode disables slow mode on this channel and returns the updated channel', () async { final channelModel = ChannelModel( cid: channelCid, ); when( - () => client.disableSlowdown( + () => client.disableSlowMode( channelId, channelType, ), - ).thenAnswer((_) async => PartialUpdateChannelResponse()..channel = channelModel); + ).thenAnswer( + (_) async => Result.success(UpdateChannelPartialResponse(duration: '0.01ms', channel: channelModel)), + ); final res = await channel.disableSlowMode(); - expect(res, isNotNull); + expect(res.getOrNull(), UpdateChannelPartialResponse(duration: '0.01ms', channel: channelModel)); - verify(() => client.disableSlowdown(channelId, channelType)).called(1); + verify(() => client.disableSlowMode(channelId, channelType)).called(1); }); test('`.banMember` scopes the ban to the channel cid', () async { @@ -4862,52 +4864,70 @@ void main() { verify(() => client.showChannel(channelId, channelType)).called(1); }); - // testing archiving - test('`.archive`', () async { + test('Channel.archive archives this channel and returns the updated membership', () async { + final response = UpdateMemberPartialResponse( + duration: '0.01ms', + channelMember: Member(userId: 'test-user-id'), + ); when(() => client.archiveChannel(channelId: channelId, channelType: channelType)).thenAnswer( - (_) async => FakePartialUpdateMemberResponse(), + (_) async => Result.success(response), ); final res = await channel.archive(); - expect(res, isNotNull); + expect(res.getOrNull(), response); verify(() => client.archiveChannel(channelId: channelId, channelType: channelType)).called(1); }); - test('`.unarchive`', () async { + test('Channel.unarchive unarchives this channel and returns the updated membership', () async { + final response = UpdateMemberPartialResponse( + duration: '0.01ms', + channelMember: Member(userId: 'test-user-id'), + ); when(() => client.unarchiveChannel(channelId: channelId, channelType: channelType)).thenAnswer( - (_) async => FakePartialUpdateMemberResponse(), + (_) async => Result.success(response), ); final res = await channel.unarchive(); - expect(res, isNotNull); + expect(res.getOrNull(), response); verify(() => client.unarchiveChannel(channelId: channelId, channelType: channelType)).called(1); }); - // testing pinning - test('`.pin`', () async { + test('Channel.pin pins this channel and returns the updated membership', () async { + final response = UpdateMemberPartialResponse( + duration: '0.01ms', + channelMember: Member(userId: 'test-user-id'), + ); when( () => client.pinChannel(channelId: channelId, channelType: channelType), - ).thenAnswer((_) async => FakePartialUpdateMemberResponse()); + ).thenAnswer( + (_) async => Result.success(response), + ); final res = await channel.pin(); - expect(res, isNotNull); + expect(res.getOrNull(), response); verify(() => client.pinChannel(channelId: channelId, channelType: channelType)).called(1); }); - test('`.unpin`', () async { + test('Channel.unpin unpins this channel and returns the updated membership', () async { + final response = UpdateMemberPartialResponse( + duration: '0.01ms', + channelMember: Member(userId: 'test-user-id'), + ); when( () => client.unpinChannel(channelId: channelId, channelType: channelType), - ).thenAnswer((_) async => FakePartialUpdateMemberResponse()); + ).thenAnswer( + (_) async => Result.success(response), + ); final res = await channel.unpin(); - expect(res, isNotNull); + expect(res.getOrNull(), response); verify(() => client.unpinChannel(channelId: channelId, channelType: channelType)).called(1); }); @@ -5524,7 +5544,7 @@ void main() { () async { // Set initial channel state with some properties final initialChannel = channel.state?.channelState.channel?.copyWith( - extraData: {'name': 'Test Channel'}, + custom: {'name': 'Test Channel'}, memberCount: 5, frozen: true, ); @@ -5691,7 +5711,7 @@ void main() { () async { // Set initial channel state with some properties final initialChannel = channel.state?.channelState.channel?.copyWith( - extraData: {'name': 'Test Channel'}, + custom: {'name': 'Test Channel'}, messageCount: 7, frozen: true, ); diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 8233e17d88..8aca0b1a45 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -1625,51 +1625,19 @@ void main() { ..channel = ChannelModel( id: channelId, type: channelType, - extraData: {...data}, + custom: {...data}, ), ); final res = await client.updateChannel(channelId, channelType, data); expect(res, isNotNull); expect(res.channel.cid, '$channelType:$channelId'); - expect(res.channel.extraData['name'], 'test-channel'); + expect(res.channel.custom['name'], 'test-channel'); verify(() => fakeChatApi.channel.updateChannel(channelId, channelType, data)).called(1); verifyNoMoreInteractions(fakeChatApi.channel); }); - test('`.updateChannelPartial`', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - const set = { - 'name': 'Stream Team', - 'profile_image': 'test-profile-image', - }; - const unset = ['tag', 'last_name']; - - when(() => fakeChatApi.channel.updateChannelPartial(channelId, channelType, set: set, unset: unset)).thenAnswer( - (invocation) async => PartialUpdateChannelResponse() - ..channel = ChannelModel( - id: channelId, - type: channelType, - extraData: {...set}, - ), - ); - - final res = await client.updateChannelPartial( - channelId, - channelType, - set: set, - unset: unset, - ); - expect(res, isNotNull); - expect(res.channel.cid, '$channelType:$channelId'); - expect(res.channel.extraData, set); - - verify(() => fakeChatApi.channel.updateChannelPartial(channelId, channelType, set: set, unset: unset)).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - test('StreamChatClient.addDevice sends the device and returns a success', () async { const id = 'test-device-id'; const request = api.CreateDeviceRequest( @@ -2530,14 +2498,14 @@ void main() { final channel = client.channel( channelType, id: channelId, - extraData: channelData, + custom: channelData, ); expect(channel, isNotNull); expect(channel.type, channelType); expect(channel.id, channelId); expect(channel.cid, '$channelType:$channelId'); - expect(channel.extraData, channelData); + expect(channel.custom, channelData); }); test('should return back in memory channel instance if available', () async { @@ -2549,7 +2517,7 @@ void main() { final channel = client.channel( channelType, id: channelId, - extraData: channelData, + custom: channelData, ); final channelState = ChannelState( @@ -2605,7 +2573,7 @@ void main() { const channelCid = '$channelType:$channelId'; final channelState = ChannelState( - channel: ChannelModel(cid: channelCid, extraData: channelData), + channel: ChannelModel(cid: channelCid, custom: channelData), ); when( @@ -2634,7 +2602,7 @@ void main() { expect(channel.type, channelType); expect(channel.id, channelId); expect(channel.cid, '$channelType:$channelId'); - expect(channel.extraData, channelData); + expect(channel.custom, channelData); verify( () => fakeChatApi.channel.queryChannel( @@ -2659,7 +2627,7 @@ void main() { const channelCid = '$channelType:$channelId'; final channelState = ChannelState( - channel: ChannelModel(cid: channelCid, extraData: channelData), + channel: ChannelModel(cid: channelCid, custom: channelData), ); when( @@ -2688,7 +2656,7 @@ void main() { expect(channel.type, channelType); expect(channel.id, channelId); expect(channel.cid, '$channelType:$channelId'); - expect(channel.extraData, channelData); + expect(channel.custom, channelData); verify( () => fakeChatApi.channel.queryChannel( @@ -2713,7 +2681,7 @@ void main() { const channelCid = '$channelType:$channelId'; final channelState = ChannelState( - channel: ChannelModel(cid: channelCid, extraData: channelData), + channel: ChannelModel(cid: channelCid, custom: channelData), ); when( @@ -2742,7 +2710,7 @@ void main() { expect(channel.type, channelType); expect(channel.id, channelId); expect(channel.cid, '$channelType:$channelId'); - expect(channel.extraData, channelData); + expect(channel.custom, channelData); verify( () => fakeChatApi.channel.queryChannel( @@ -2838,218 +2806,6 @@ void main() { verifyNoMoreInteractions(fakeChatApi.channel); }); - test('`.partialMemberUpdate with userId`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - const otherUserId = 'test-other-user-id'; - const set = {'pinned': true}; - const unset = ['pinned']; - - when( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - ), - ).thenAnswer( - (_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: otherUserId), - ), - ); - - final res = await client.partialMemberUpdate( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - ); - - expect(res, isNotNull); - expect(res.channelMember.userId, otherUserId); - - verify( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - ), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - - test('`.partialMemberUpdate with current user`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - const set = {'pinned': true}; - const unset = ['pinned']; - - when( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - ), - ).thenAnswer( - (_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId), - ), - ); - - final res = await client.partialMemberUpdate( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - ); - - expect(res, isNotNull); - expect(res.channelMember.userId, userId); - verify( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - ), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - - test('`.pinChannel`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - - when( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(pinned: true).toJson(), - ), - ).thenAnswer( - (_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, pinnedAt: DateTime.now()), - ), - ); - - final res = await client.pinChannel( - channelId: channelId, - channelType: channelType, - ); - - expect(res, isNotNull); - - verify( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(pinned: true).toJson(), - ), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - - test('`.unpinChannel`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - - when( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.pinned.name], - ), - ).thenAnswer( - (_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, pinnedAt: DateTime.now()), - ), - ); - - final res = await client.unpinChannel( - channelId: channelId, - channelType: channelType, - ); - - expect(res, isNotNull); - - verify( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.pinned.name], - ), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - - test('`.archiveChannel`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - - when( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(archived: true).toJson(), - ), - ).thenAnswer( - (_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, archivedAt: DateTime.now()), - ), - ); - - final res = await client.archiveChannel( - channelId: channelId, - channelType: channelType, - ); - - expect(res, isNotNull); - - verify( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(archived: true).toJson(), - ), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - - test('`.unarchiveChannel`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - - when( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.archived.name], - ), - ).thenAnswer( - (_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, pinnedAt: DateTime.now()), - ), - ); - - final res = await client.unarchiveChannel( - channelId: channelId, - channelType: channelType, - ); - - expect(res, isNotNull); - - verify( - () => fakeChatApi.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.archived.name], - ), - ).called(1); - verifyNoMoreInteractions(fakeChatApi.channel); - }); - test('`.acceptChannelInvite`', () async { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; diff --git a/packages/stream_chat/test/src/client/client_update_channel_partial_test.dart b/packages/stream_chat/test/src/client/client_update_channel_partial_test.dart new file mode 100644 index 0000000000..4c3ceefcec --- /dev/null +++ b/packages/stream_chat/test/src/client/client_update_channel_partial_test.dart @@ -0,0 +1,390 @@ +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() { + setUpAll(() => registerFallbackValue(const api.UpdateChannelPartialRequest())); + + test( + 'StreamChatClient.updateChannelPartial sends the set and unset fields and returns the updated members', + () async { + const request = api.UpdateChannelPartialRequest(set: {'name': 'General'}, unset: ['topic']); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateChannelPartialResponse())); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial( + 'general', + 'messaging', + set: const {'name': 'General'}, + unset: const ['topic'], + ); + + expect(res.getOrNull()!.duration, '0.01ms'); + expect(res.getOrNull()!.members, [fakeChannelMember()]); + verify( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }, + ); + + test('StreamChatClient.updateChannelPartial returns the channel with every field it models', () async { + const request = api.UpdateChannelPartialRequest(set: {'name': 'General'}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateChannelPartialResponse())); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial('general', 'messaging', set: const {'name': 'General'}); + + final channel = res.getOrNull()!.channel!; + expect(channel.id, 'general'); + expect(channel.type, 'messaging'); + expect(channel.cid, 'messaging:general'); + expect(channel.ownCapabilities, [ChannelCapability.sendMessage, ChannelCapability.readEvents]); + expect(channel.createdBy, fakeUser('creator')); + expect(channel.frozen, isTrue); + expect(channel.lastMessageAt, DateTime.utc(2026, 3, 4)); + expect(channel.createdAt, DateTime.utc(2026)); + expect(channel.updatedAt, DateTime.utc(2026, 2)); + expect(channel.deletedAt, DateTime.utc(2026, 3)); + expect(channel.memberCount, 2); + expect(channel.members, [fakeChannelMember()]); + expect(channel.team, 'blue'); + expect(channel.cooldown, 30); + expect(channel.disabled, isFalse); + expect(channel.hidden, isTrue); + expect(channel.muted, isFalse); + expect(channel.blocked, isTrue); + expect(channel.truncatedAt, DateTime.utc(2026, 3, 2)); + expect(channel.truncatedBy, fakeUser('truncator')); + expect(channel.autoTranslationEnabled, isTrue); + expect(channel.autoTranslationLanguage, 'fr'); + expect(channel.messageCount, 42); + expect(channel.filterTags, ['support']); + }); + + test('StreamChatClient.updateChannelPartial returns the channel custom data as the response sends it', () async { + const request = api.UpdateChannelPartialRequest(set: {'name': 'General'}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateChannelPartialResponse())); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial('general', 'messaging', set: const {'name': 'General'}); + + expect(res.getOrNull()!.channel!.custom, { + 'name': 'General', + 'image': 'https://example.com/general.png', + 'topic': 'anything', + }); + }); + + test('StreamChatClient.updateChannelPartial falls back to the defaults for channel fields left out', () async { + final response = _updateChannelPartialResponse( + channel: api.ChannelResponse( + cid: 'messaging:general', + id: 'general', + type: 'messaging', + createdAt: DateTime.utc(2026), + updatedAt: DateTime.utc(2026, 2), + custom: const {}, + disabled: false, + frozen: false, + ), + ); + const request = api.UpdateChannelPartialRequest(set: {'name': 'General'}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(response)); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial('general', 'messaging', set: const {'name': 'General'}); + + final channel = res.getOrNull()!.channel!; + expect(channel.memberCount, 0); + expect(channel.cooldown, 0); + expect(channel.config.commands, isEmpty); + expect(channel.members, isNull); + }); + + test('StreamChatClient.updateChannelPartial returns the channel config with every field it models', () async { + const request = api.UpdateChannelPartialRequest(set: {'name': 'General'}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateChannelPartialResponse())); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial('general', 'messaging', set: const {'name': 'General'}); + + final config = res.getOrNull()!.channel!.config; + expect(config.automod, 'simple'); + expect(config.commands.single.name, 'giphy'); + expect(config.commands.single.description, 'Post a random gif'); + expect(config.commands.single.args, '[text]'); + expect(config.commands.single.set, CommandSet.fun); + expect(config.connectEvents, isTrue); + expect(config.createdAt, DateTime.utc(2025)); + expect(config.updatedAt, DateTime.utc(2025, 2)); + expect(config.maxMessageLength, 5000); + expect(config.messageRetention, 'infinite'); + expect(config.mutes, isFalse); + expect(config.reactions, isTrue); + expect(config.readEvents, isFalse); + expect(config.replies, isTrue); + expect(config.search, isFalse); + expect(config.polls, isTrue); + expect(config.pushLevel, PushLevel.directMentions); + expect(config.pushNotifications, isFalse); + expect( + config.chatPreferences, + const ChatPreferences( + channelMentions: ChatPreferenceLevel.all, + defaultPreference: ChatPreferenceLevel.none, + directMentions: ChatPreferenceLevel.all, + groupMentions: ChatPreferenceLevel.none, + hereMentions: ChatPreferenceLevel.all, + roleMentions: ChatPreferenceLevel.none, + threadReplies: ChatPreferenceLevel.all, + ), + ); + expect(config.typingEvents, isTrue); + expect(config.uploads, isFalse); + expect(config.urlEnrichment, isTrue); + expect(config.skipLastMsgUpdateForSystemMsgs, isFalse); + expect(config.userMessageReminders, isTrue); + expect(config.markMessagesPending, isFalse); + expect(config.deliveryEvents, isTrue); + expect(config.sharedLocations, isFalse); + }); + + test('StreamChatClient.updateChannelPartial returns a null channel when the response has none', () async { + const response = api.UpdateChannelPartialResponse(duration: '0.01ms', members: []); + const request = api.UpdateChannelPartialRequest(set: {'name': 'General'}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => const Result.success(response)); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial('general', 'messaging', set: const {'name': 'General'}); + + expect(res, const Result.success(UpdateChannelPartialResponse(duration: '0.01ms'))); + }); + + test('StreamChatClient.updateChannelPartial returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateChannelPartialRequest: any(named: 'updateChannelPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.updateChannelPartial('general', 'messaging', set: const {'name': 'General'}); + + expect(res.exceptionOrNull(), error); + }); + + test('StreamChatClient.enableSlowMode sends the cooldown and returns the updated channel', () async { + const request = api.UpdateChannelPartialRequest(set: {'cooldown': 30}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateChannelPartialResponse(channel: _channelResponse(cooldown: 30)))); + final client = _client(defaultApi); + + final res = await client.enableSlowMode('general', 'messaging', 30); + + expect(res.getOrNull()!.channel!.cooldown, 30); + + verify( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.enableSlowMode returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateChannelPartialRequest: any(named: 'updateChannelPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.enableSlowMode('general', 'messaging', 30); + + expect(res.exceptionOrNull(), error); + }); + + test('StreamChatClient.disableSlowMode sends the cooldown set to 0 and returns the updated channel', () async { + const request = api.UpdateChannelPartialRequest(set: {'cooldown': 0}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateChannelPartialResponse(channel: _channelResponse(cooldown: 0)))); + final client = _client(defaultApi); + + final res = await client.disableSlowMode('general', 'messaging'); + + expect(res.getOrNull()!.channel!.cooldown, 0); + + verify( + () => defaultApi.updateChannelPartial(type: 'messaging', id: 'general', updateChannelPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.disableSlowMode returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateChannelPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateChannelPartialRequest: any(named: 'updateChannelPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.disableSlowMode('general', 'messaging'); + + expect(res.exceptionOrNull(), error); + }); +} + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} + +// A response with every field set. +api.UpdateChannelPartialResponse _updateChannelPartialResponse({api.ChannelResponse? channel}) => + api.UpdateChannelPartialResponse( + channel: channel ?? _channelResponse(), + duration: '0.01ms', + members: [fakeChannelMemberResponse()], + ); + +// A channel with every field set. +api.ChannelResponse _channelResponse({ + int cooldown = 30, + Map custom = const { + 'name': 'General', + 'image': 'https://example.com/general.png', + 'topic': 'anything', + }, +}) => api.ChannelResponse( + autoTranslationEnabled: true, + autoTranslationLanguage: 'fr', + blocked: true, + cid: 'messaging:general', + config: api.ChannelConfigWithInfo( + allowedFlagReasons: const ['spam'], + automod: api.ChannelConfigWithInfoAutomod.simple, + automodBehavior: api.ChannelConfigWithInfoAutomodBehavior.flag, + blocklist: 'profanity', + blocklistBehavior: api.ChannelConfigWithInfoBlocklistBehavior.block, + chatPreferences: const api.ChatPreferences( + channelMentions: 'all', + defaultPreference: 'none', + directMentions: 'all', + distinctChannelMessages: 'all', + groupMentions: 'none', + hereMentions: 'all', + roleMentions: 'none', + threadReplies: 'all', + ), + commands: [ + api.Command( + args: '[text]', + createdAt: DateTime.utc(2025), + description: 'Post a random gif', + name: 'giphy', + set: 'fun_set', + updatedAt: DateTime.utc(2025, 2), + ), + ], + connectEvents: true, + countMessages: true, + createdAt: DateTime.utc(2025), + customEvents: true, + deliveryEvents: true, + grants: const { + 'user': ['read-channel'], + }, + markMessagesPending: false, + maxMessageLength: 5000, + messageRetention: 'infinite', + mutes: false, + name: 'messaging', + partitionSize: 10, + partitionTtl: '24h', + polls: true, + pushLevel: api.ChannelConfigWithInfoPushLevel.directMentions, + pushNotifications: false, + quotes: true, + reactions: true, + readEvents: false, + reminders: true, + replies: true, + search: false, + sharedLocations: false, + skipLastMsgUpdateForSystemMsgs: false, + typingEvents: true, + updatedAt: DateTime.utc(2025, 2), + uploads: false, + urlEnrichment: true, + userMessageReminders: true, + ), + cooldown: cooldown, + createdAt: DateTime.utc(2026), + createdBy: fakeUserResponse('creator'), + custom: custom, + deletedAt: DateTime.utc(2026, 3), + disabled: false, + filterTags: const ['support'], + frozen: true, + hidden: true, + hideMessagesBefore: DateTime.utc(2026, 3, 1), + id: 'general', + lastMessageAt: DateTime.utc(2026, 3, 4), + memberCount: 2, + members: [fakeChannelMemberResponse()], + messageCount: 42, + muteExpiresAt: DateTime.utc(2026, 5), + muted: false, + ownCapabilities: const [api.ChannelOwnCapability.sendMessage, api.ChannelOwnCapability.readEvents], + team: 'blue', + truncatedAt: DateTime.utc(2026, 3, 2), + truncatedBy: fakeUserResponse('truncator'), + type: 'messaging', + updatedAt: DateTime.utc(2026, 2), +); diff --git a/packages/stream_chat/test/src/client/client_update_member_partial_test.dart b/packages/stream_chat/test/src/client/client_update_member_partial_test.dart new file mode 100644 index 0000000000..ddb4ff313e --- /dev/null +++ b/packages/stream_chat/test/src/client/client_update_member_partial_test.dart @@ -0,0 +1,245 @@ +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() { + setUpAll(() => registerFallbackValue(const api.UpdateMemberPartialRequest())); + + test('StreamChatClient.updateMemberPartial sends the set and unset fields and returns the updated member', () async { + const request = api.UpdateMemberPartialRequest(set: {'nickname': 'Mo'}, unset: ['status']); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateMemberPartialResponse())); + final client = _client(defaultApi); + + final res = await client.updateMemberPartial( + channelId: 'general', + channelType: 'messaging', + set: const {'nickname': 'Mo'}, + unset: const ['status'], + ); + + expect(res.getOrNull(), UpdateMemberPartialResponse(duration: '0.01ms', channelMember: fakeChannelMember())); + verify( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.updateMemberPartial returns a null member when the response has none', () async { + const response = api.UpdateMemberPartialResponse(duration: '0.01ms'); + const request = api.UpdateMemberPartialRequest(set: {'a': 1}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => const Result.success(response)); + final client = _client(defaultApi); + + final res = await client.updateMemberPartial(channelId: 'general', channelType: 'messaging', set: const {'a': 1}); + + expect(res, const Result.success(UpdateMemberPartialResponse(duration: '0.01ms'))); + }); + + test('StreamChatClient.updateMemberPartial returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateMemberPartialRequest: any(named: 'updateMemberPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.updateMemberPartial(channelId: 'general', channelType: 'messaging', set: const {'a': 1}); + + expect(res.exceptionOrNull(), error); + }); + + test('StreamChatClient.updateMemberPartial falls back to the defaults for member fields left out', () async { + final response = api.UpdateMemberPartialResponse( + duration: '0.01ms', + channelMember: api.ChannelMemberResponse( + banned: false, + channelRole: 'channel_member', + createdAt: DateTime.utc(2026), + updatedAt: DateTime.utc(2026, 2), + custom: const {}, + notificationsMuted: false, + shadowBanned: false, + ), + ); + const request = api.UpdateMemberPartialRequest(set: {'a': 1}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => Result.success(response)); + final client = _client(defaultApi); + + final res = await client.updateMemberPartial(channelId: 'general', channelType: 'messaging', set: const {'a': 1}); + + final member = res.getOrNull()!.channelMember!; + expect(member.invited, isFalse); + expect(member.isModerator, isFalse); + expect(member.deletedMessages, isEmpty); + }); + + test('StreamChatClient.pinChannel sends the pinned flag and returns the updated member', () async { + const request = api.UpdateMemberPartialRequest(set: {'pinned': true}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateMemberPartialResponse())); + final client = _client(defaultApi); + + final res = await client.pinChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.getOrNull(), UpdateMemberPartialResponse(duration: '0.01ms', channelMember: fakeChannelMember())); + verify( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.pinChannel returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateMemberPartialRequest: any(named: 'updateMemberPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.pinChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.exceptionOrNull(), error); + }); + + test('StreamChatClient.unpinChannel sends the pinned flag unset and returns the updated member', () async { + const request = api.UpdateMemberPartialRequest(unset: ['pinned']); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateMemberPartialResponse())); + final client = _client(defaultApi); + + final res = await client.unpinChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.getOrNull(), UpdateMemberPartialResponse(duration: '0.01ms', channelMember: fakeChannelMember())); + verify( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.unpinChannel returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateMemberPartialRequest: any(named: 'updateMemberPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.unpinChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.exceptionOrNull(), error); + }); + + test('StreamChatClient.archiveChannel sends the archived flag and returns the updated member', () async { + const request = api.UpdateMemberPartialRequest(set: {'archived': true}); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateMemberPartialResponse())); + final client = _client(defaultApi); + + final res = await client.archiveChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.getOrNull(), UpdateMemberPartialResponse(duration: '0.01ms', channelMember: fakeChannelMember())); + verify( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.archiveChannel returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateMemberPartialRequest: any(named: 'updateMemberPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.archiveChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.exceptionOrNull(), error); + }); + + test('StreamChatClient.unarchiveChannel sends the archived flag unset and returns the updated member', () async { + const request = api.UpdateMemberPartialRequest(unset: ['archived']); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).thenAnswer((_) async => Result.success(_updateMemberPartialResponse())); + final client = _client(defaultApi); + + final res = await client.unarchiveChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.getOrNull(), UpdateMemberPartialResponse(duration: '0.01ms', channelMember: fakeChannelMember())); + verify( + () => defaultApi.updateMemberPartial(type: 'messaging', id: 'general', updateMemberPartialRequest: request), + ).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.unarchiveChannel returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateMemberPartial( + type: any(named: 'type'), + id: any(named: 'id'), + updateMemberPartialRequest: any(named: 'updateMemberPartialRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.unarchiveChannel(channelId: 'general', channelType: 'messaging'); + + expect(res.exceptionOrNull(), error); + }); +} + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} + +// A response with every field set. +api.UpdateMemberPartialResponse _updateMemberPartialResponse() => + api.UpdateMemberPartialResponse(duration: '0.01ms', channelMember: fakeChannelMemberResponse()); diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 924ccbd0f3..7815a67a5b 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -288,7 +288,7 @@ void main() { final channelModel = ChannelModel( id: channelId, type: channelType, - extraData: data, + custom: data, ); when( @@ -324,50 +324,6 @@ void main() { verifyNoMoreInteractions(client); }); - test('updateChannelPartial', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - const set = { - 'name': 'Stream Team', - 'profile_image': 'test-profile-image', - }; - - const unset = ['tag', 'last_name']; - - final path = _getChannelUrl(channelId, channelType); - - final channelModel = ChannelModel( - id: channelId, - type: channelType, - extraData: set, - ); - - when( - () => client.patch(path, data: {'set': set, 'unset': unset}), - ).thenAnswer( - (_) async => successResponse( - path, - data: { - 'channel': channelModel.toJson(), - }, - ), - ); - - final res = await channelApi.updateChannelPartial( - channelId, - channelType, - set: set, - unset: unset, - ); - - expect(res, isNotNull); - - verify( - () => client.patch(path, data: {'set': set, 'unset': unset}), - ).called(1); - verifyNoMoreInteractions(client); - }); - test('acceptChannelInvite', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; @@ -890,142 +846,6 @@ void main() { verifyNoMoreInteractions(client); }); - test('archiveChannel', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - - final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; - const archivedAt = '2025-04-10 10:27:03.150349'; - - when( - () => client.patch( - path, - data: { - 'set': {'archived': true}, - }, - ), - ).thenAnswer( - (_) async => successResponse( - path, - data: { - 'channel_member': { - 'archived_at': archivedAt, - }, - }, - ), - ); - - final res = await channelApi.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(archived: true).toJson(), - ); - - expect(res, isNotNull); - expect(res.channelMember.archivedAt, DateTime.parse(archivedAt)); - - verify(() => client.patch(path, data: any(named: 'data'))).called(1); - verifyNoMoreInteractions(client); - }); - - test('unarchiveChannel', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - - final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; - - when( - () => client.patch( - path, - data: { - 'unset': ['archived'], - }, - ), - ).thenAnswer( - (_) async => successResponse(path, data: {'channel_member': {}}), - ); - - final res = await channelApi.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.archived.name], - ); - - expect(res, isNotNull); - expect(res.channelMember.archivedAt, null); - - verify(() => client.patch(path, data: any(named: 'data'))).called(1); - verifyNoMoreInteractions(client); - }); - - test('pinChannel', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - - final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; - const pinnedAt = '2025-04-10 10:27:03.150349'; - - when( - () => client.patch( - path, - data: { - 'set': {'pinned': true}, - }, - ), - ).thenAnswer( - (_) async => successResponse( - path, - data: { - 'channel_member': { - 'pinned_at': pinnedAt, - }, - }, - ), - ); - - final res = await channelApi.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(pinned: true).toJson(), - ); - - expect(res, isNotNull); - expect(res.channelMember.pinnedAt, DateTime.parse(pinnedAt)); - - verify(() => client.patch(path, data: any(named: 'data'))).called(1); - verifyNoMoreInteractions(client); - }); - - test('unpinChannel', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - - final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; - - when( - () => client.patch( - path, - data: { - 'unset': ['pinned'], - }, - ), - ).thenAnswer( - (_) async => successResponse(path, data: {'channel_member': {}}), - ); - - final res = await channelApi.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.pinned.name], - ); - - expect(res, isNotNull); - expect(res.channelMember.pinnedAt, null); - - verify(() => client.patch(path, data: any(named: 'data'))).called(1); - verifyNoMoreInteractions(client); - }); - test('stopWatching', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; @@ -1042,98 +862,6 @@ void main() { verifyNoMoreInteractions(client); }); - test('enableSlowdown', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - const cooldown = 10; - const set = { - 'cooldown': 10, - }; - - final path = _getChannelUrl(channelId, channelType); - - final channelModel = ChannelModel( - id: channelId, - type: channelType, - extraData: set, - ); - - when( - () => client.patch( - path, - data: { - 'set': set, - }, - ), - ).thenAnswer( - (_) async => successResponse( - path, - data: { - 'channel': channelModel.toJson(), - }, - ), - ); - - final res = await channelApi.enableSlowdown(channelId, channelType, cooldown); - - expect(res, isNotNull); - - verify( - () => client.patch( - path, - data: { - 'set': set, - }, - ), - ).called(1); - verifyNoMoreInteractions(client); - }); - - test('disableSlowdown', () async { - const channelId = 'test-channel-id'; - const channelType = 'test-channel-type'; - const set = { - 'cooldown': 0, - }; - - final path = _getChannelUrl(channelId, channelType); - - final channelModel = ChannelModel( - id: channelId, - type: channelType, - ); - - when( - () => client.patch( - path, - data: { - 'set': set, - }, - ), - ).thenAnswer( - (_) async => successResponse( - path, - data: { - 'channel': channelModel.toJson(), - }, - ), - ); - - final res = await channelApi.disableSlowdown(channelId, channelType); - - expect(res, isNotNull); - - verify( - () => client.patch( - path, - data: { - 'set': set, - }, - ), - ).called(1); - verifyNoMoreInteractions(client); - }); - test('markChannelsDelivered', () async { const path = '/channels/delivered'; diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 2738cfb023..d64ccc48f3 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -20,10 +20,10 @@ void main() { expect(channelState.channel?.updatedAt, DateTime.parse('2019-04-03T18:43:33.213374Z')); expect(channelState.channel?.createdBy, isA()); expect(channelState.channel?.frozen, true); - expect(channelState.channel?.extraData['example'], 1); - expect(channelState.channel?.extraData['name'], '#dev'); + expect(channelState.channel?.custom['example'], 1); + expect(channelState.channel?.custom['name'], '#dev'); expect( - channelState.channel?.extraData['image'], + channelState.channel?.custom['image'], 'https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png', ); expect(channelState.messages, isNotNull); @@ -151,11 +151,11 @@ void main() { ); }); - test('a custom field orders by the channel extra data', () { + test('a custom field orders by the channel custom data', () { expectOrders( ChannelSortField.custom('priority'), - createChannelState(id: 'low', extraData: const {'priority': 1}), - createChannelState(id: 'high', extraData: const {'priority': 10}), + createChannelState(id: 'low', custom: const {'priority': 1}), + createChannelState(id: 'high', custom: const {'priority': 10}), ); }); @@ -182,7 +182,7 @@ ChannelState createChannelState({ DateTime? updatedAt, DateTime? lastMessageAt, int? memberCount, - Map? extraData, + Map? custom, Draft? draft, }) { return ChannelState( @@ -194,7 +194,7 @@ ChannelState createChannelState({ createdAt: createdAt ?? DateTime(2023), updatedAt: updatedAt ?? DateTime(2023), memberCount: memberCount ?? 0, - extraData: extraData ?? {}, + custom: custom ?? {}, ), membership: Member(userId: 'user1'), draft: draft, diff --git a/packages/stream_chat/test/src/core/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart index 63bcd7f6ca..37dd08af1c 100644 --- a/packages/stream_chat/test/src/core/models/channel_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_test.dart @@ -1,6 +1,6 @@ // ignore_for_file: avoid_redundant_argument_values -import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import '../../utils.dart'; @@ -12,9 +12,18 @@ void main() { expect(channel.id, equals('test')); expect(channel.type, equals('livestream')); expect(channel.cid, equals('livestream:test')); - expect(channel.extraData['cats'], equals(true)); - expect(channel.extraData['fruit'], equals(['bananas', 'apples'])); + expect(channel.custom['cats'], equals(true)); + expect(channel.custom['fruit'], equals(['bananas', 'apples'])); expect(channel.cooldown, equals(0)); + expect(channel.disabled, isFalse); + expect(channel.hidden, isTrue); + expect(channel.muted, isTrue); + expect(channel.blocked, isFalse); + expect(channel.truncatedAt, DateTime.parse('2020-01-29T03:22:47.636130Z')); + expect(channel.truncatedBy?.id, 'truncator'); + expect(channel.truncatedBy?.name, 'Truncator'); + expect(channel.autoTranslationEnabled, isTrue); + expect(channel.autoTranslationLanguage, 'fr'); }); test('should serialize to json correctly', () { @@ -22,7 +31,7 @@ void main() { type: 'type', id: 'id', cid: 'a:a', - extraData: {'name': 'cool'}, + custom: {'name': 'cool'}, ); expect( @@ -42,7 +51,7 @@ void main() { type: 'type', id: 'id', cid: 'a:a', - extraData: {'name': 'cool'}, + custom: {'name': 'cool'}, ); expect( @@ -58,277 +67,143 @@ void main() { }); }); - test('hidden property and extraData manipulation', () { - final channel = ChannelModel(cid: 'test:cid', hidden: false); - - expect(channel.hidden, false); - expect(channel.extraData['hidden'], false); - expect(channel.toJson(), { - 'id': 'cid', - 'type': 'test', - 'frozen': false, - 'cooldown': 0, - 'hidden': false, - }); - expect(ChannelModel.fromJson(channel.toJson()).toJson(), { - 'id': 'cid', - 'type': 'test', - 'frozen': false, - 'cooldown': 0, - 'hidden': false, - }); - - var newChannel = channel.copyWith( - extraData: {'hidden': true}, - ); - - expect(newChannel.extraData['hidden'], true); - expect(newChannel.hidden, true); - - newChannel = channel.copyWith( - hidden: false, - ); - - expect(newChannel.extraData['hidden'], false); - expect(newChannel.hidden, false); - - newChannel = channel.copyWith( + test('ChannelModel.toJson leaves out custom data named like a field', () { + final channel = ChannelModel( + cid: 'test:cid', hidden: true, - extraData: {'hidden': true}, - ); - - expect(newChannel.extraData['hidden'], true); - expect(newChannel.hidden, true); - }); - - test('a non-boolean flag in replacement extraData falls back to the current value', () { - // The getters read these with `safeCast`, so `copyWith` has to as well — - // extra data is consumer-authored and can hold anything. - final channel = ChannelModel(cid: 'test:cid', muted: true, blocked: true); - - final newChannel = channel.copyWith( - extraData: {'muted': 'yes', 'blocked': 1}, + custom: const {'hidden': 'custom', 'id': 'custom', 'color': 'red'}, ); - expect(newChannel.muted, true); - expect(newChannel.blocked, true); - }); - - test('disabled property and extraData manipulation', () { - final channel = ChannelModel(cid: 'test:cid', disabled: false); - - expect(channel.disabled, false); - expect(channel.extraData['disabled'], false); expect(channel.toJson(), { 'id': 'cid', 'type': 'test', 'frozen': false, 'cooldown': 0, - 'disabled': false, - }); - expect(ChannelModel.fromJson(channel.toJson()).toJson(), { - 'id': 'cid', - 'type': 'test', - 'frozen': false, - 'cooldown': 0, - 'disabled': false, + 'color': 'red', }); - - var newChannel = channel.copyWith( - extraData: {'disabled': true}, - ); - - expect(newChannel.extraData['disabled'], true); - expect(newChannel.disabled, true); - - newChannel = channel.copyWith( - hidden: false, - ); - - expect(newChannel.extraData['disabled'], false); - expect(newChannel.disabled, false); - - newChannel = channel.copyWith( - hidden: true, - extraData: {'disabled': true}, - ); - - expect(newChannel.extraData['disabled'], true); - expect(newChannel.disabled, true); }); - test('truncatedAt property and extraData manipulation', () { - final currentDate = DateTime.now(); - final channel = ChannelModel(cid: 'test:cid', truncatedAt: currentDate); - - expect(channel.truncatedAt, currentDate); - expect(channel.extraData['truncated_at'], currentDate.toIso8601String()); - expect(channel.toJson(), { - 'id': 'cid', - 'type': 'test', - 'frozen': false, - 'cooldown': 0, - 'truncated_at': currentDate.toIso8601String(), - }); - expect(ChannelModel.fromJson(channel.toJson()).toJson(), { - 'id': 'cid', - 'type': 'test', - 'frozen': false, - 'cooldown': 0, - 'truncated_at': currentDate.toIso8601String(), - }); - - final dateOne = DateTime.now(); - var newChannel = channel.copyWith( - extraData: {'truncated_at': dateOne.toIso8601String()}, - ); - - expect(newChannel.extraData['truncated_at'], dateOne.toIso8601String()); - expect(newChannel.truncatedAt, dateOne); - - final dateTwo = DateTime.now(); - newChannel = channel.copyWith( - truncatedAt: dateTwo, - ); - - expect(newChannel.extraData['truncated_at'], dateTwo.toIso8601String()); - expect(newChannel.truncatedAt, dateTwo); - - final dateThree = DateTime.now(); - newChannel = channel.copyWith( - truncatedAt: dateThree, - extraData: {'truncated_at': dateThree.toIso8601String()}, - ); - - expect(newChannel.extraData['truncated_at'], dateThree.toIso8601String()); - expect(newChannel.truncatedAt, dateThree); - }); - - test('.name should fetch from extraData if available', () { + test('ChannelModel.copyWith keeps the flags it is not given', () { final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'name': 'Test Channel'}, + cid: 'test:cid', + hidden: true, + truncatedBy: User(id: 'truncator'), + autoTranslationEnabled: true, + autoTranslationLanguage: 'fr', ); - expect(channel.name, 'Test Channel'); - }); + final newChannel = channel.copyWith(team: 'blue'); - test('.name should return null if not available in extraData', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {}, - ); - - expect(channel.name, isNull); + expect(newChannel.hidden, isTrue); + expect(newChannel.truncatedBy?.id, 'truncator'); + expect(newChannel.autoTranslationEnabled, isTrue); + expect(newChannel.autoTranslationLanguage, 'fr'); }); - test('.name should return null if extraData value is not String', () { + test('ChannelModel.copyWith replaces the flags it is given', () { final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'name': true}, + cid: 'test:cid', + disabled: false, + hidden: false, + muted: false, + blocked: false, + truncatedAt: DateTime.utc(2026), + truncatedBy: User(id: 'truncator'), + autoTranslationEnabled: true, + autoTranslationLanguage: 'fr', ); - expect(channel.name, isNull); - }); - - test('.name should return null if extraData value is empty', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'name': ''}, + final newChannel = channel.copyWith( + disabled: true, + hidden: true, + muted: true, + blocked: true, + truncatedAt: DateTime.utc(2026, 2), + truncatedBy: User(id: 'other'), + autoTranslationEnabled: false, + autoTranslationLanguage: 'de', ); - expect(channel.name, isNull); + expect(newChannel.disabled, isTrue); + expect(newChannel.hidden, isTrue); + expect(newChannel.muted, isTrue); + expect(newChannel.blocked, isTrue); + expect(newChannel.truncatedAt, DateTime.utc(2026, 2)); + expect(newChannel.truncatedBy?.id, 'other'); + expect(newChannel.autoTranslationEnabled, isFalse); + expect(newChannel.autoTranslationLanguage, 'de'); }); - test('.disabled should fetch from extraData if available', () { + test('ChannelModel.merge keeps the flags the other channel leaves out', () { final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'disabled': true}, + cid: 'test:cid', + hidden: true, + muted: true, + blocked: true, + truncatedAt: DateTime.utc(2026), + truncatedBy: User(id: 'truncator'), ); - expect(channel.disabled, true); - }); - - test('.disabled should return null if not available in extraData', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {}, - ); + final merged = channel.merge(ChannelModel(cid: 'test:cid', custom: const {'color': 'red'})); - expect(channel.disabled, isNull); + expect(merged.hidden, isTrue); + expect(merged.muted, isTrue); + expect(merged.blocked, isTrue); + expect(merged.truncatedAt, DateTime.utc(2026)); + expect(merged.truncatedBy?.id, 'truncator'); }); - test('.disabled should return null if extraData value is not bool', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'disabled': 'true'}, - ); + test('ChannelModel.merge takes the custom data from the other channel', () { + final channel = ChannelModel(cid: 'test:cid', custom: const {'color': 'blue'}); - expect(channel.disabled, isNull); - }); - - test('.hidden should fetch from extraData if available', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'hidden': true}, - ); + final merged = channel.merge(ChannelModel(cid: 'test:cid', custom: const {'color': 'red'})); - expect(channel.hidden, true); + expect(merged.custom, {'color': 'red'}); }); - test('.hidden should return null if not available in extraData', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {}, - ); - - expect(channel.hidden, isNull); - }); + test('ChannelModel.merge turns auto translation off when the other channel leaves it out', () { + final channel = ChannelModel(cid: 'test:cid', autoTranslationEnabled: true, autoTranslationLanguage: 'fr'); - test('.hidden should return null if extraData value is not bool', () { - final channel = ChannelModel( - cid: 'test:channel', - extraData: const {'hidden': 'false'}, - ); + final merged = channel.merge(ChannelModel(cid: 'test:cid')); - expect(channel.hidden, isNull); + expect(merged.autoTranslationEnabled, isFalse); + expect(merged.autoTranslationLanguage, isNull); }); - test('.truncatedAt should fetch from extraData if available', () { - final testDate = DateTime.now(); + test('.name should fetch from custom if available', () { final channel = ChannelModel( cid: 'test:channel', - extraData: {'truncated_at': testDate.toIso8601String()}, + custom: const {'name': 'Test Channel'}, ); - expect(channel.truncatedAt, testDate); + expect(channel.name, 'Test Channel'); }); - test('.truncatedAt should return null if not available in extraData', () { + test('.name should return null if not available in custom', () { final channel = ChannelModel( cid: 'test:channel', - extraData: const {}, + custom: const {}, ); - expect(channel.truncatedAt, isNull); + expect(channel.name, isNull); }); - test('.truncatedAt should return null if extraData value is not String', () { + test('.name should return null if custom value is not String', () { final channel = ChannelModel( cid: 'test:channel', - extraData: const {'truncated_at': true}, + custom: const {'name': true}, ); - expect(channel.truncatedAt, isNull); + expect(channel.name, isNull); }); - test('.truncatedAt should return null if extraData value is empty', () { + test('.name should return null if custom value is empty', () { final channel = ChannelModel( cid: 'test:channel', - extraData: const {'truncated_at': ''}, + custom: const {'name': ''}, ); - expect(channel.truncatedAt, isNull); + expect(channel.name, isNull); }); test('should parse filterTags from json correctly', () { diff --git a/packages/stream_chat/test/src/core/models/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart index e8095d3a27..2b240ad6ef 100644 --- a/packages/stream_chat/test/src/core/models/member_test.dart +++ b/packages/stream_chat/test/src/core/models/member_test.dart @@ -1,5 +1,4 @@ -import 'package:stream_chat/src/core/models/member.dart'; -import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import '../../utils.dart'; @@ -13,7 +12,93 @@ void main() { expect(member.createdAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); expect(member.updatedAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); expect(member.deletedMessages, ['msg-1', 'msg-2', 'msg-3']); - expect(member.extraData['some_custom_field'], 'with_custom_data'); + expect(member.custom['some_custom_field'], 'with_custom_data'); + expect(member.notificationsMuted, isTrue); + expect(member.status, 'member'); + expect(member.banFromFutureChannels, isTrue); + expect(member.futureChannelBanExpires, DateTime.parse('2020-02-28T22:17:30.95443Z')); + expect(member.deletedAt, DateTime.parse('2020-01-29T22:17:30.95443Z')); + }); + + test('Member.toJson leaves out custom data named like a field', () { + final member = Member( + userId: 'user', + channelRole: 'channel_member', + custom: const {'channel_role': 'custom', 'color': 'red'}, + ); + + expect(member.toJson()['channel_role'], 'channel_member'); + expect(member.toJson()['color'], 'red'); + }); + + test('Member.copyWith replaces the fields it is given', () { + final member = Member( + userId: 'user', + notificationsMuted: false, + status: 'pending', + banFromFutureChannels: false, + futureChannelBanExpires: DateTime.utc(2021), + deletedAt: DateTime.utc(2020), + ); + + final newMember = member.copyWith( + notificationsMuted: true, + status: 'member', + banFromFutureChannels: true, + futureChannelBanExpires: DateTime.utc(2022), + deletedAt: DateTime.utc(2023), + ); + + expect(newMember.notificationsMuted, isTrue); + expect(newMember.status, 'member'); + expect(newMember.banFromFutureChannels, isTrue); + expect(newMember.futureChannelBanExpires, DateTime.utc(2022)); + expect(newMember.deletedAt, DateTime.utc(2023)); + }); + + test('Member equality includes notificationsMuted, status, the future channel ban and deletedAt', () { + final createdAt = DateTime.utc(2020); + Member member({ + bool? notificationsMuted, + String? status, + bool? banFromFutureChannels, + DateTime? expires, + DateTime? deletedAt, + }) => Member( + userId: 'user', + createdAt: createdAt, + updatedAt: createdAt, + notificationsMuted: notificationsMuted, + status: status, + banFromFutureChannels: banFromFutureChannels, + futureChannelBanExpires: expires, + deletedAt: deletedAt, + ); + + expect(member(notificationsMuted: true), isNot(member(notificationsMuted: false))); + expect(member(status: 'member'), isNot(member(status: 'pending'))); + expect(member(banFromFutureChannels: true), isNot(member(banFromFutureChannels: false))); + expect(member(expires: DateTime.utc(2021)), isNot(member(expires: DateTime.utc(2022)))); + expect(member(deletedAt: DateTime.utc(2021)), isNot(member(deletedAt: DateTime.utc(2022)))); + }); + + test('Member.copyWith keeps the fields it is not given', () { + final member = Member( + userId: 'user', + notificationsMuted: true, + status: 'member', + banFromFutureChannels: true, + futureChannelBanExpires: DateTime.utc(2021), + deletedAt: DateTime.utc(2020), + ); + + final newMember = member.copyWith(custom: const {'color': 'red'}); + + expect(newMember.notificationsMuted, isTrue); + expect(newMember.status, 'member'); + expect(newMember.banFromFutureChannels, isTrue); + expect(newMember.futureChannelBanExpires, DateTime.utc(2021)); + expect(newMember.deletedAt, DateTime.utc(2020)); }); group('MemberSortField', () { @@ -77,11 +162,11 @@ void main() { ); }); - test('a custom field orders by the member extra data', () { + test('a custom field orders by the member custom data', () { expectOrders( MemberSortField.custom('activityScore'), - createTestMember(userId: 'a', extraData: const {'activityScore': 10}), - createTestMember(userId: 'b', extraData: const {'activityScore': 75}), + createTestMember(userId: 'a', custom: const {'activityScore': 10}), + createTestMember(userId: 'b', custom: const {'activityScore': 75}), ); }); @@ -104,7 +189,7 @@ Member createTestMember({ DateTime? updatedAt, DateTime? userLastActive, bool includeUser = true, - Map? extraData, + Map? custom, }) { return Member( userId: userId, @@ -118,6 +203,6 @@ Member createTestMember({ channelRole: channelRole, createdAt: createdAt ?? DateTime(2023), updatedAt: updatedAt ?? DateTime(2023), - extraData: extraData ?? {}, + custom: custom ?? {}, ); } diff --git a/packages/stream_chat/test/src/core/models/sort_comparison_test.dart b/packages/stream_chat/test/src/core/models/sort_comparison_test.dart index 9094b2ed99..5131d88de4 100644 --- a/packages/stream_chat/test/src/core/models/sort_comparison_test.dart +++ b/packages/stream_chat/test/src/core/models/sort_comparison_test.dart @@ -207,7 +207,7 @@ void main() { id: 'red-team', type: 'messaging', createdAt: createdAt, - extraData: const {'team': 'red'}, + custom: const {'team': 'red'}, ), ), ]; @@ -224,7 +224,7 @@ void main() { id: 'red-team', type: 'messaging', createdAt: createdAt, - extraData: const {'team': 'red'}, + custom: const {'team': 'red'}, ), ), ChannelState( diff --git a/packages/stream_chat/test/src/core/models/sort_registry_test.dart b/packages/stream_chat/test/src/core/models/sort_registry_test.dart index 2c8005d183..08da89ee37 100644 --- a/packages/stream_chat/test/src/core/models/sort_registry_test.dart +++ b/packages/stream_chat/test/src/core/models/sort_registry_test.dart @@ -151,7 +151,7 @@ void main() { }); test('`fromRemote` falls back to a field the API added and the SDK has not', () { - // A channel key with no typed property deserializes into extraData, + // A channel key with no typed property deserializes into custom, // which is where the custom fallback looks — so the sort still works. ChannelState channelWith(Object? value) => ChannelState.fromJson({ 'channel': { diff --git a/packages/stream_chat/test/src/core/util/serializer_test.dart b/packages/stream_chat/test/src/core/util/serializer_test.dart index 77073f8a96..f34a8a4126 100644 --- a/packages/stream_chat/test/src/core/util/serializer_test.dart +++ b/packages/stream_chat/test/src/core/util/serializer_test.dart @@ -49,4 +49,16 @@ void main() { expect(json, {'name': 'extra'}); }); + + test('moveFromExtraDataToRoot leaves out an extra_data entry named like one of the top level fields', () { + final json = Serializer.moveFromExtraDataToRoot( + { + 'name': 'root', + 'extra_data': {'name': 'extra', 'role': 'extra', 'color': 'red'}, + }, + topLevelFields: ['name', 'role'], + ); + + expect(json, {'name': 'root', 'color': 'red'}); + }); } diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 5e35ffe050..6046fcb293 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -143,8 +143,11 @@ class TestPersistenceClient extends ChatPersistenceClient { @override Future updatePollVotes(List pollVotes) => Future.value(); + // The users passed to every [updateUsers] call, in order. + final updatedUsers = []; + @override - Future updateUsers(List users) => Future.value(); + Future updateUsers(List users) async => updatedUsers.addAll(users); @override Future bulkUpdateMembers(Map?> members) => Future.value(); @@ -290,5 +293,19 @@ void main() { ); persistenceClient.updateChannelStates([channelState]); }); + + test('updateChannelStates stores the users who created and last truncated the channel', () async { + final client = TestPersistenceClient(); + final creator = User(id: 'creator'); + final truncator = User(id: 'truncator'); + + await client.updateChannelStates([ + ChannelState( + channel: ChannelModel(cid: 'test:cid', createdBy: creator, truncatedBy: truncator), + ), + ]); + + expect(client.updatedUsers, containsAll([creator, truncator])); + }); }); } diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index f7e0985475..b9e329d2a7 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -246,12 +246,82 @@ class FakePollVote extends Fake implements PollVote {} class FakeChannelState extends Fake implements ChannelState {} -class FakePartialUpdateMemberResponse extends Fake implements PartialUpdateMemberResponse { - FakePartialUpdateMemberResponse({ - Member? channelMember, - }) : _channelMember = channelMember ?? Member(); +/// A generated channel member with every field set. +api.ChannelMemberResponse fakeChannelMemberResponse({Map custom = const {'nickname': 'Mo'}}) => + api.ChannelMemberResponse( + archivedAt: DateTime.utc(2026, 1, 5), + banExpires: DateTime.utc(2026, 6), + banFromFutureChannels: true, + banned: true, + channelRole: 'channel_moderator', + createdAt: DateTime.utc(2026), + custom: custom, + deletedAt: DateTime.utc(2026, 8), + deletedMessages: const ['message-1'], + futureChannelBanExpires: DateTime.utc(2026, 7), + inviteAcceptedAt: DateTime.utc(2026, 1, 2), + inviteRejectedAt: DateTime.utc(2026, 1, 3), + invited: true, + isModerator: false, + notificationsMuted: false, + pinnedAt: DateTime.utc(2026, 1, 4), + role: 'user', + shadowBanned: false, + status: 'member', + updatedAt: DateTime.utc(2026, 2), + user: fakeUserResponse('member'), + userId: 'member', + ); - final Member _channelMember; - @override - Member get channelMember => _channelMember; -} +/// A generated [api.UserResponse] with the id [id] and only the fields it +/// requires. +api.UserResponse fakeUserResponse(String id) => api.UserResponse( + banned: false, + blockedUserIds: const [], + createdAt: DateTime.utc(2025), + custom: const {}, + id: id, + language: 'en', + online: false, + role: 'user', + teams: const [], + updatedAt: DateTime.utc(2025), +); + +/// The [Member] that [fakeChannelMemberResponse] maps to, with its default +/// custom data. +Member fakeChannelMember() => Member( + user: fakeUser('member'), + userId: 'member', + inviteAcceptedAt: DateTime.utc(2026, 1, 2), + inviteRejectedAt: DateTime.utc(2026, 1, 3), + invited: true, + channelRole: 'channel_moderator', + isModerator: false, + createdAt: DateTime.utc(2026), + updatedAt: DateTime.utc(2026, 2), + banned: true, + banExpires: DateTime.utc(2026, 6), + shadowBanned: false, + pinnedAt: DateTime.utc(2026, 1, 4), + archivedAt: DateTime.utc(2026, 1, 5), + deletedMessages: const ['message-1'], + custom: const {'nickname': 'Mo'}, + notificationsMuted: false, + status: 'member', + banFromFutureChannels: true, + futureChannelBanExpires: DateTime.utc(2026, 7), + deletedAt: DateTime.utc(2026, 8), +); + +/// The [User] that [fakeUserResponse] maps to. +User fakeUser(String id) => User( + id: id, + role: 'user', + createdAt: DateTime.utc(2025), + updatedAt: DateTime.utc(2025), + online: false, + banned: false, + teams: const [], + language: 'en', +); diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart index b3a21e3522..22ed18a10c 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart @@ -172,7 +172,7 @@ class MessageSearchListTileTitle extends StatelessWidget { Widget build(BuildContext context) { final user = messageResponse.message.user!; final channel = messageResponse.channel; - final channelName = channel?.extraData['name']; + final channelName = channel?.custom['name']; return Text.rich( TextSpan( diff --git a/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart index 00e5f6e2b4..c89b14f92d 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart @@ -234,12 +234,12 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); when(() => channelState.unreadCount).thenReturn(1); diff --git a/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart index c6593b5f9c..0b74cf6a46 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart @@ -7,7 +7,7 @@ import '../mocks.dart'; void main() { testWidgets( - 'it should show the image in channel.extraData', + 'StreamChannelAvatar shows the channel image', (tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -98,7 +98,7 @@ void main() { ), }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart index c593fe15b6..7545ea927d 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_page_test.dart @@ -299,8 +299,8 @@ Future _pumpChannelPage( when(() => channel.imageStream).thenAnswer((_) => Stream.value(null)); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when(() => channelState.members).thenReturn([]); when(() => channelState.membersStream).thenAnswer((_) => Stream.value([])); diff --git a/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart b/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart index ac563654aa..cb3c34e5c8 100644 --- a/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/thread_page_test.dart @@ -249,8 +249,8 @@ Future _pumpThreadPage( when(() => channel.imageStream).thenAnswer((_) => Stream.value(null)); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when( () => channel.getReplies( any(), diff --git a/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart index bd85cee1a3..94faa2ce90 100644 --- a/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart @@ -27,12 +27,12 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); when(() => channelState.membersStream).thenAnswer( diff --git a/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart index d8bf20b406..6016f2f638 100644 --- a/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart @@ -22,12 +22,12 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/message_input/message_composer_content_insertion_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_composer_content_insertion_test.dart index e2535a6e7b..05a5defd7b 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_composer_content_insertion_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_composer_content_insertion_test.dart @@ -62,8 +62,8 @@ Future _pumpComposer( when(() => channel.client).thenReturn(client); when(channel.getRemainingCooldown).thenReturn(0); when(() => channel.lastMessageAt).thenReturn(DateTime.parse('2020-06-22 12:00:00')); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when(() => channelState.members).thenReturn([member]); when(() => channelState.membersStream).thenAnswer((_) => Stream.value([member])); when(() => channelState.messages).thenReturn([]); diff --git a/packages/stream_chat_flutter/test/src/message_input/message_composer_draft_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_composer_draft_test.dart index 38e8773254..8dffd88c43 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_composer_draft_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_composer_draft_test.dart @@ -34,8 +34,8 @@ void main() { when(() => channel.client).thenReturn(client); when(channel.getRemainingCooldown).thenReturn(0); when(() => channel.lastMessageAt).thenReturn(DateTime.parse('2020-06-22 12:00:00')); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', diff --git a/packages/stream_chat_flutter/test/src/message_input/message_composer_drop_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_composer_drop_test.dart index 5c582aba92..dedf8ef10a 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_composer_drop_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_composer_drop_test.dart @@ -60,8 +60,8 @@ Future pumpComposerWithSettings( when(() => channel.client).thenReturn(client); when(channel.getRemainingCooldown).thenReturn(0); when(() => channel.lastMessageAt).thenReturn(DateTime.parse('2020-06-22 12:00:00')); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', diff --git a/packages/stream_chat_flutter/test/src/message_input/message_composer_slow_mode_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_composer_slow_mode_test.dart index 88af2c4f4b..f289a70fb8 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_composer_slow_mode_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_composer_slow_mode_test.dart @@ -42,8 +42,8 @@ void main() { when(() => channel.isPinned).thenReturn(false); when(() => channel.isPinnedStream).thenAnswer((_) => Stream.value(false)); when(() => channel.isDistinct).thenReturn(false); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); - when(() => channel.extraData).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); when(() => channel.name).thenReturn('test'); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.image).thenReturn(null); diff --git a/packages/stream_chat_flutter/test/src/message_input/message_composer_surface_style_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_composer_surface_style_test.dart index e4c0a5a3eb..1b0a69256b 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_composer_surface_style_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_composer_surface_style_test.dart @@ -218,8 +218,8 @@ Future _pumpComposer( when(() => channel.client).thenReturn(client); when(channel.getRemainingCooldown).thenReturn(0); when(() => channel.lastMessageAt).thenReturn(null); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when(() => channelState.members).thenReturn([]); when(() => channelState.membersStream).thenAnswer((_) => Stream.value([])); when(() => channelState.messages).thenReturn([]); diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index 158344b361..589a2f6b76 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -409,8 +409,8 @@ void main() { when(channel.getRemainingCooldown).thenReturn(0); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); - when(() => channel.extraData).thenReturn({'name': 'test'}); - when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channel.custom).thenReturn({'name': 'test'}); + when(() => channel.customStream).thenAnswer((_) => Stream.value({'name': 'test'})); when(() => channelState.isUpToDate).thenReturn(true); when(() => channelState.members).thenReturn([ Member( @@ -1385,12 +1385,12 @@ MaterialApp buildWidget(Widget input) { when(channel.getRemainingCooldown).thenReturn(0); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); when(() => channelState.membersStream).thenAnswer( diff --git a/packages/stream_chat_flutter/test/src/misc/system_message_test.dart b/packages/stream_chat_flutter/test/src/misc/system_message_test.dart index 4c8a81f60f..c10d8a4a1e 100644 --- a/packages/stream_chat_flutter/test/src/misc/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/system_message_test.dart @@ -22,12 +22,12 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); @@ -81,12 +81,12 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); @@ -130,12 +130,12 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart b/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart index de78cbc7c0..3e5fe81e75 100644 --- a/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart @@ -142,12 +142,12 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer( + when(() => channel.customStream).thenAnswer( (i) => Stream.value({ 'name': 'test', }), ); - when(() => channel.extraData).thenReturn({ + when(() => channel.custom).thenReturn({ 'name': 'test', }); when(() => channelState.unreadCount).thenReturn(1); diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_tile_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_tile_test.dart index 690e21f151..fea5308324 100644 --- a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_tile_test.dart +++ b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_tile_test.dart @@ -15,7 +15,7 @@ void main() { channelCid: 'channel-type:channel-id', channel: ChannelModel( cid: 'channel-type:channel-id', - extraData: const {'name': 'Group ride'}, + custom: const {'name': 'Group ride'}, ), parentMessageId: 'parent-message-id', parentMessage: Message( diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 99d6c23d3e..d216518c39 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -15,6 +15,7 @@ 🐞 Fixed - A cached channel query given no sort came back in whatever order the lookup returned; it now applies `ChannelSort.defaultSort`, the ordering the API would have. +- A member's `banExpires` is now kept in the local cache. ## Upcoming diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index 98dab74418..bc65d46dbc 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -14,16 +14,21 @@ class ChannelDao extends DatabaseAccessor with _$ChannelDaoMi ChannelDao(super.db); /// Get channel by cid - Future getChannelByCid(String cid) async => (select(channels)..where((c) => c.cid.equals(cid))) - .join([ - leftOuterJoin(users, channels.createdById.equalsExp(users.id)), - ]) - .map((rows) { - final channel = rows.readTable(channels); - final createdBy = rows.readTableOrNull(users); - return channel.toChannelModel(createdBy: createdBy?.toUser()); - }) - .getSingleOrNull(); + Future getChannelByCid(String cid) async { + final truncatedByUsers = alias(users, 'truncated_by_users'); + return (select(channels)..where((c) => c.cid.equals(cid))) + .join([ + leftOuterJoin(users, channels.createdById.equalsExp(users.id)), + leftOuterJoin(truncatedByUsers, channels.truncatedById.equalsExp(truncatedByUsers.id)), + ]) + .map((rows) { + final channel = rows.readTable(channels); + final createdBy = rows.readTableOrNull(users); + final truncatedBy = rows.readTableOrNull(truncatedByUsers); + return channel.toChannelModel(createdBy: createdBy?.toUser(), truncatedBy: truncatedBy?.toUser()); + }) + .getSingleOrNull(); + } /// Delete all channels by matching cid in [cids] /// diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index 84fa499ead..03f2fdb0a1 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -120,16 +120,7 @@ class ChannelQueryDao extends DatabaseAccessor with _$Channel final cachedChannelCids = await getCachedChannelCids(filter); final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids)); - final cachedChannels = await query - .join([ - leftOuterJoin(users, channels.createdById.equalsExp(users.id)), - ]) - .map((row) { - final createdByEntity = row.readTableOrNull(users); - final channelEntity = row.readTable(channels); - return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser()); - }) - .get(); + final cachedChannels = await _withUsers(query).get(); return cachedChannels; } @@ -156,19 +147,27 @@ class ChannelQueryDao extends DatabaseAccessor with _$Channel return (const [], metadata?.filter, metadata?.sort); } - final cachedChannels = await (select(channels)..where((c) => c.cid.isIn(cachedCids))) + final cachedChannels = await _withUsers(select(channels)..where((c) => c.cid.isIn(cachedCids))).get(); + + return (cachedChannels, metadata?.filter, metadata?.sort); + } + + // Joins the users who created and last truncated each channel in [query]. + Selectable _withUsers(SimpleSelectStatement<$ChannelsTable, ChannelEntity> query) { + final truncatedByUsers = alias(users, 'truncated_by_users'); + return query .join([ leftOuterJoin(users, channels.createdById.equalsExp(users.id)), + leftOuterJoin(truncatedByUsers, channels.truncatedById.equalsExp(truncatedByUsers.id)), ]) .map((row) { final createdByEntity = row.readTableOrNull(users); + final truncatedByEntity = row.readTableOrNull(truncatedByUsers); final channelEntity = row.readTable(channels); return channelEntity.toChannelModel( createdBy: createdByEntity?.toUser(), + truncatedBy: truncatedByEntity?.toUser(), ); - }) - .get(); - - return (cachedChannels, metadata?.filter, metadata?.sort); + }); } } diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart index 43ef0b607c..685ee6b015 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart @@ -59,7 +59,7 @@ class DriftChatDatabase extends _$DriftChatDatabase { // Band the version by SDK major, and bump the offset whenever you change or // add a table definition. @override - int get schemaVersion => 1100 + 4; + int get schemaVersion => 1100 + 6; // Store DateTime as ISO-8601 text to preserve sub-second precision. @override diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index 1125f1cb31..00ac17f110 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -161,14 +161,109 @@ class $ChannelsTable extends Channels with TableInfo<$ChannelsTable, ChannelEnti type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _disabledMeta = const VerificationMeta( + 'disabled', + ); @override - late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn( - 'extra_data', + late final GeneratedColumn disabled = GeneratedColumn( + 'disabled', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("disabled" IN (0, 1))', + ), + ); + static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); + @override + late final GeneratedColumn hidden = GeneratedColumn( + 'hidden', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("hidden" IN (0, 1))', + ), + ); + static const VerificationMeta _mutedMeta = const VerificationMeta('muted'); + @override + late final GeneratedColumn muted = GeneratedColumn( + 'muted', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("muted" IN (0, 1))', + ), + ); + static const VerificationMeta _blockedMeta = const VerificationMeta( + 'blocked', + ); + @override + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("blocked" IN (0, 1))', + ), + ); + static const VerificationMeta _truncatedAtMeta = const VerificationMeta( + 'truncatedAt', + ); + @override + late final GeneratedColumn truncatedAt = GeneratedColumn( + 'truncated_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _truncatedByIdMeta = const VerificationMeta( + 'truncatedById', + ); + @override + late final GeneratedColumn truncatedById = GeneratedColumn( + 'truncated_by_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _autoTranslationEnabledMeta = const VerificationMeta('autoTranslationEnabled'); + @override + late final GeneratedColumn autoTranslationEnabled = GeneratedColumn( + 'auto_translation_enabled', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("auto_translation_enabled" IN (0, 1))', + ), + ); + static const VerificationMeta _autoTranslationLanguageMeta = const VerificationMeta('autoTranslationLanguage'); + @override + late final GeneratedColumn autoTranslationLanguage = GeneratedColumn( + 'auto_translation_language', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( + 'custom', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - ).withConverter?>($ChannelsTable.$converterextraDatan); + ).withConverter?>($ChannelsTable.$convertercustomn); @override List get $columns => [ id, @@ -186,7 +281,15 @@ class $ChannelsTable extends Channels with TableInfo<$ChannelsTable, ChannelEnti createdById, filterTags, team, - extraData, + disabled, + hidden, + muted, + blocked, + truncatedAt, + truncatedById, + autoTranslationEnabled, + autoTranslationLanguage, + custom, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -287,6 +390,66 @@ class $ChannelsTable extends Channels with TableInfo<$ChannelsTable, ChannelEnti team.isAcceptableOrUnknown(data['team']!, _teamMeta), ); } + if (data.containsKey('disabled')) { + context.handle( + _disabledMeta, + disabled.isAcceptableOrUnknown(data['disabled']!, _disabledMeta), + ); + } + if (data.containsKey('hidden')) { + context.handle( + _hiddenMeta, + hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta), + ); + } + if (data.containsKey('muted')) { + context.handle( + _mutedMeta, + muted.isAcceptableOrUnknown(data['muted']!, _mutedMeta), + ); + } + if (data.containsKey('blocked')) { + context.handle( + _blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta), + ); + } + if (data.containsKey('truncated_at')) { + context.handle( + _truncatedAtMeta, + truncatedAt.isAcceptableOrUnknown( + data['truncated_at']!, + _truncatedAtMeta, + ), + ); + } + if (data.containsKey('truncated_by_id')) { + context.handle( + _truncatedByIdMeta, + truncatedById.isAcceptableOrUnknown( + data['truncated_by_id']!, + _truncatedByIdMeta, + ), + ); + } + if (data.containsKey('auto_translation_enabled')) { + context.handle( + _autoTranslationEnabledMeta, + autoTranslationEnabled.isAcceptableOrUnknown( + data['auto_translation_enabled']!, + _autoTranslationEnabledMeta, + ), + ); + } + if (data.containsKey('auto_translation_language')) { + context.handle( + _autoTranslationLanguageMeta, + autoTranslationLanguage.isAcceptableOrUnknown( + data['auto_translation_language']!, + _autoTranslationLanguageMeta, + ), + ); + } return context; } @@ -362,10 +525,42 @@ class $ChannelsTable extends Channels with TableInfo<$ChannelsTable, ChannelEnti DriftSqlType.string, data['${effectivePrefix}team'], ), - extraData: $ChannelsTable.$converterextraDatan.fromSql( + disabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}disabled'], + ), + hidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}hidden'], + ), + muted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}muted'], + ), + blocked: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}blocked'], + ), + truncatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}truncated_at'], + ), + truncatedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}truncated_by_id'], + ), + autoTranslationEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}auto_translation_enabled'], + ), + autoTranslationLanguage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}auto_translation_language'], + ), + custom: $ChannelsTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), ); @@ -385,9 +580,9 @@ class $ChannelsTable extends Channels with TableInfo<$ChannelsTable, ChannelEnti static TypeConverter?, String?> $converterfilterTagsn = NullAwareTypeConverter.wrap( $converterfilterTags, ); - static TypeConverter, String> $converterextraData = MapConverter(); - static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( - $converterextraData, + static TypeConverter, String> $convertercustom = MapConverter(); + static TypeConverter?, String?> $convertercustomn = NullAwareTypeConverter.wrap( + $convertercustom, ); } @@ -437,8 +632,32 @@ class ChannelEntity extends DataClass implements Insertable { /// The team the channel belongs to final String? team; - /// Map of custom channel extraData - final Map? extraData; + /// Whether the channel is disabled. + final bool? disabled; + + /// Whether the channel is hidden. + final bool? hidden; + + /// Whether the current user muted the channel. + final bool? muted; + + /// Whether the current user blocked the channel. + final bool? blocked; + + /// The date the channel was last truncated. + final DateTime? truncatedAt; + + /// The id of the user that last truncated the channel. + final String? truncatedById; + + /// Whether messages in the channel are translated automatically. + final bool? autoTranslationEnabled; + + /// The language the channel's messages are translated into automatically. + final String? autoTranslationLanguage; + + /// Custom data attached to the channel. + final Map? custom; const ChannelEntity({ required this.id, required this.type, @@ -455,7 +674,15 @@ class ChannelEntity extends DataClass implements Insertable { this.createdById, this.filterTags, this.team, - this.extraData, + this.disabled, + this.hidden, + this.muted, + this.blocked, + this.truncatedAt, + this.truncatedById, + this.autoTranslationEnabled, + this.autoTranslationLanguage, + this.custom, }); @override Map toColumns(bool nullToAbsent) { @@ -497,9 +724,35 @@ class ChannelEntity extends DataClass implements Insertable { if (!nullToAbsent || team != null) { map['team'] = Variable(team); } - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $ChannelsTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || disabled != null) { + map['disabled'] = Variable(disabled); + } + if (!nullToAbsent || hidden != null) { + map['hidden'] = Variable(hidden); + } + if (!nullToAbsent || muted != null) { + map['muted'] = Variable(muted); + } + if (!nullToAbsent || blocked != null) { + map['blocked'] = Variable(blocked); + } + if (!nullToAbsent || truncatedAt != null) { + map['truncated_at'] = Variable(truncatedAt); + } + if (!nullToAbsent || truncatedById != null) { + map['truncated_by_id'] = Variable(truncatedById); + } + if (!nullToAbsent || autoTranslationEnabled != null) { + map['auto_translation_enabled'] = Variable(autoTranslationEnabled); + } + if (!nullToAbsent || autoTranslationLanguage != null) { + map['auto_translation_language'] = Variable( + autoTranslationLanguage, + ); + } + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $ChannelsTable.$convertercustomn.toSql(custom), ); } return map; @@ -528,7 +781,19 @@ class ChannelEntity extends DataClass implements Insertable { createdById: serializer.fromJson(json['createdById']), filterTags: serializer.fromJson?>(json['filterTags']), team: serializer.fromJson(json['team']), - extraData: serializer.fromJson?>(json['extraData']), + disabled: serializer.fromJson(json['disabled']), + hidden: serializer.fromJson(json['hidden']), + muted: serializer.fromJson(json['muted']), + blocked: serializer.fromJson(json['blocked']), + truncatedAt: serializer.fromJson(json['truncatedAt']), + truncatedById: serializer.fromJson(json['truncatedById']), + autoTranslationEnabled: serializer.fromJson( + json['autoTranslationEnabled'], + ), + autoTranslationLanguage: serializer.fromJson( + json['autoTranslationLanguage'], + ), + custom: serializer.fromJson?>(json['custom']), ); } @override @@ -550,7 +815,19 @@ class ChannelEntity extends DataClass implements Insertable { 'createdById': serializer.toJson(createdById), 'filterTags': serializer.toJson?>(filterTags), 'team': serializer.toJson(team), - 'extraData': serializer.toJson?>(extraData), + 'disabled': serializer.toJson(disabled), + 'hidden': serializer.toJson(hidden), + 'muted': serializer.toJson(muted), + 'blocked': serializer.toJson(blocked), + 'truncatedAt': serializer.toJson(truncatedAt), + 'truncatedById': serializer.toJson(truncatedById), + 'autoTranslationEnabled': serializer.toJson( + autoTranslationEnabled, + ), + 'autoTranslationLanguage': serializer.toJson( + autoTranslationLanguage, + ), + 'custom': serializer.toJson?>(custom), }; } @@ -570,7 +847,15 @@ class ChannelEntity extends DataClass implements Insertable { Value createdById = const Value.absent(), Value?> filterTags = const Value.absent(), Value team = const Value.absent(), - Value?> extraData = const Value.absent(), + Value disabled = const Value.absent(), + Value hidden = const Value.absent(), + Value muted = const Value.absent(), + Value blocked = const Value.absent(), + Value truncatedAt = const Value.absent(), + Value truncatedById = const Value.absent(), + Value autoTranslationEnabled = const Value.absent(), + Value autoTranslationLanguage = const Value.absent(), + Value?> custom = const Value.absent(), }) => ChannelEntity( id: id ?? this.id, type: type ?? this.type, @@ -587,7 +872,17 @@ class ChannelEntity extends DataClass implements Insertable { createdById: createdById.present ? createdById.value : this.createdById, filterTags: filterTags.present ? filterTags.value : this.filterTags, team: team.present ? team.value : this.team, - extraData: extraData.present ? extraData.value : this.extraData, + disabled: disabled.present ? disabled.value : this.disabled, + hidden: hidden.present ? hidden.value : this.hidden, + muted: muted.present ? muted.value : this.muted, + blocked: blocked.present ? blocked.value : this.blocked, + truncatedAt: truncatedAt.present ? truncatedAt.value : this.truncatedAt, + truncatedById: truncatedById.present ? truncatedById.value : this.truncatedById, + autoTranslationEnabled: autoTranslationEnabled.present ? autoTranslationEnabled.value : this.autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage.present + ? autoTranslationLanguage.value + : this.autoTranslationLanguage, + custom: custom.present ? custom.value : this.custom, ); ChannelEntity copyWithCompanion(ChannelsCompanion data) { return ChannelEntity( @@ -606,7 +901,19 @@ class ChannelEntity extends DataClass implements Insertable { createdById: data.createdById.present ? data.createdById.value : this.createdById, filterTags: data.filterTags.present ? data.filterTags.value : this.filterTags, team: data.team.present ? data.team.value : this.team, - extraData: data.extraData.present ? data.extraData.value : this.extraData, + disabled: data.disabled.present ? data.disabled.value : this.disabled, + hidden: data.hidden.present ? data.hidden.value : this.hidden, + muted: data.muted.present ? data.muted.value : this.muted, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + truncatedAt: data.truncatedAt.present ? data.truncatedAt.value : this.truncatedAt, + truncatedById: data.truncatedById.present ? data.truncatedById.value : this.truncatedById, + autoTranslationEnabled: data.autoTranslationEnabled.present + ? data.autoTranslationEnabled.value + : this.autoTranslationEnabled, + autoTranslationLanguage: data.autoTranslationLanguage.present + ? data.autoTranslationLanguage.value + : this.autoTranslationLanguage, + custom: data.custom.present ? data.custom.value : this.custom, ); } @@ -628,13 +935,21 @@ class ChannelEntity extends DataClass implements Insertable { ..write('createdById: $createdById, ') ..write('filterTags: $filterTags, ') ..write('team: $team, ') - ..write('extraData: $extraData') + ..write('disabled: $disabled, ') + ..write('hidden: $hidden, ') + ..write('muted: $muted, ') + ..write('blocked: $blocked, ') + ..write('truncatedAt: $truncatedAt, ') + ..write('truncatedById: $truncatedById, ') + ..write('autoTranslationEnabled: $autoTranslationEnabled, ') + ..write('autoTranslationLanguage: $autoTranslationLanguage, ') + ..write('custom: $custom') ..write(')')) .toString(); } @override - int get hashCode => Object.hash( + int get hashCode => Object.hashAll([ id, type, cid, @@ -650,8 +965,16 @@ class ChannelEntity extends DataClass implements Insertable { createdById, filterTags, team, - extraData, - ); + disabled, + hidden, + muted, + blocked, + truncatedAt, + truncatedById, + autoTranslationEnabled, + autoTranslationLanguage, + custom, + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -671,7 +994,15 @@ class ChannelEntity extends DataClass implements Insertable { other.createdById == this.createdById && other.filterTags == this.filterTags && other.team == this.team && - other.extraData == this.extraData); + other.disabled == this.disabled && + other.hidden == this.hidden && + other.muted == this.muted && + other.blocked == this.blocked && + other.truncatedAt == this.truncatedAt && + other.truncatedById == this.truncatedById && + other.autoTranslationEnabled == this.autoTranslationEnabled && + other.autoTranslationLanguage == this.autoTranslationLanguage && + other.custom == this.custom); } class ChannelsCompanion extends UpdateCompanion { @@ -690,7 +1021,15 @@ class ChannelsCompanion extends UpdateCompanion { final Value createdById; final Value?> filterTags; final Value team; - final Value?> extraData; + final Value disabled; + final Value hidden; + final Value muted; + final Value blocked; + final Value truncatedAt; + final Value truncatedById; + final Value autoTranslationEnabled; + final Value autoTranslationLanguage; + final Value?> custom; final Value rowid; const ChannelsCompanion({ this.id = const Value.absent(), @@ -708,7 +1047,15 @@ class ChannelsCompanion extends UpdateCompanion { this.createdById = const Value.absent(), this.filterTags = const Value.absent(), this.team = const Value.absent(), - this.extraData = const Value.absent(), + this.disabled = const Value.absent(), + this.hidden = const Value.absent(), + this.muted = const Value.absent(), + this.blocked = const Value.absent(), + this.truncatedAt = const Value.absent(), + this.truncatedById = const Value.absent(), + this.autoTranslationEnabled = const Value.absent(), + this.autoTranslationLanguage = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }); ChannelsCompanion.insert({ @@ -727,13 +1074,21 @@ class ChannelsCompanion extends UpdateCompanion { this.createdById = const Value.absent(), this.filterTags = const Value.absent(), this.team = const Value.absent(), - this.extraData = const Value.absent(), + this.disabled = const Value.absent(), + this.hidden = const Value.absent(), + this.muted = const Value.absent(), + this.blocked = const Value.absent(), + this.truncatedAt = const Value.absent(), + this.truncatedById = const Value.absent(), + this.autoTranslationEnabled = const Value.absent(), + this.autoTranslationLanguage = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), type = Value(type), cid = Value(cid), config = Value(config); - static Insertable custom({ + static Insertable createCustom({ Expression? id, Expression? type, Expression? cid, @@ -749,7 +1104,15 @@ class ChannelsCompanion extends UpdateCompanion { Expression? createdById, Expression? filterTags, Expression? team, - Expression? extraData, + Expression? disabled, + Expression? hidden, + Expression? muted, + Expression? blocked, + Expression? truncatedAt, + Expression? truncatedById, + Expression? autoTranslationEnabled, + Expression? autoTranslationLanguage, + Expression? custom, Expression? rowid, }) { return RawValuesInsertable({ @@ -768,7 +1131,15 @@ class ChannelsCompanion extends UpdateCompanion { if (createdById != null) 'created_by_id': createdById, if (filterTags != null) 'filter_tags': filterTags, if (team != null) 'team': team, - if (extraData != null) 'extra_data': extraData, + if (disabled != null) 'disabled': disabled, + if (hidden != null) 'hidden': hidden, + if (muted != null) 'muted': muted, + if (blocked != null) 'blocked': blocked, + if (truncatedAt != null) 'truncated_at': truncatedAt, + if (truncatedById != null) 'truncated_by_id': truncatedById, + if (autoTranslationEnabled != null) 'auto_translation_enabled': autoTranslationEnabled, + if (autoTranslationLanguage != null) 'auto_translation_language': autoTranslationLanguage, + if (custom != null) 'custom': custom, if (rowid != null) 'rowid': rowid, }); } @@ -789,7 +1160,15 @@ class ChannelsCompanion extends UpdateCompanion { Value? createdById, Value?>? filterTags, Value? team, - Value?>? extraData, + Value? disabled, + Value? hidden, + Value? muted, + Value? blocked, + Value? truncatedAt, + Value? truncatedById, + Value? autoTranslationEnabled, + Value? autoTranslationLanguage, + Value?>? custom, Value? rowid, }) { return ChannelsCompanion( @@ -808,7 +1187,15 @@ class ChannelsCompanion extends UpdateCompanion { createdById: createdById ?? this.createdById, filterTags: filterTags ?? this.filterTags, team: team ?? this.team, - extraData: extraData ?? this.extraData, + disabled: disabled ?? this.disabled, + hidden: hidden ?? this.hidden, + muted: muted ?? this.muted, + blocked: blocked ?? this.blocked, + truncatedAt: truncatedAt ?? this.truncatedAt, + truncatedById: truncatedById ?? this.truncatedById, + autoTranslationEnabled: autoTranslationEnabled ?? this.autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage ?? this.autoTranslationLanguage, + custom: custom ?? this.custom, rowid: rowid ?? this.rowid, ); } @@ -867,9 +1254,37 @@ class ChannelsCompanion extends UpdateCompanion { if (team.present) { map['team'] = Variable(team.value); } - if (extraData.present) { - map['extra_data'] = Variable( - $ChannelsTable.$converterextraDatan.toSql(extraData.value), + if (disabled.present) { + map['disabled'] = Variable(disabled.value); + } + if (hidden.present) { + map['hidden'] = Variable(hidden.value); + } + if (muted.present) { + map['muted'] = Variable(muted.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (truncatedAt.present) { + map['truncated_at'] = Variable(truncatedAt.value); + } + if (truncatedById.present) { + map['truncated_by_id'] = Variable(truncatedById.value); + } + if (autoTranslationEnabled.present) { + map['auto_translation_enabled'] = Variable( + autoTranslationEnabled.value, + ); + } + if (autoTranslationLanguage.present) { + map['auto_translation_language'] = Variable( + autoTranslationLanguage.value, + ); + } + if (custom.present) { + map['custom'] = Variable( + $ChannelsTable.$convertercustomn.toSql(custom.value), ); } if (rowid.present) { @@ -896,7 +1311,15 @@ class ChannelsCompanion extends UpdateCompanion { ..write('createdById: $createdById, ') ..write('filterTags: $filterTags, ') ..write('team: $team, ') - ..write('extraData: $extraData, ') + ..write('disabled: $disabled, ') + ..write('hidden: $hidden, ') + ..write('muted: $muted, ') + ..write('blocked: $blocked, ') + ..write('truncatedAt: $truncatedAt, ') + ..write('truncatedById: $truncatedById, ') + ..write('autoTranslationEnabled: $autoTranslationEnabled, ') + ..write('autoTranslationLanguage: $autoTranslationLanguage, ') + ..write('custom: $custom, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -9647,6 +10070,17 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> ), defaultValue: const Constant(false), ); + static const VerificationMeta _banExpiresMeta = const VerificationMeta( + 'banExpires', + ); + @override + late final GeneratedColumn banExpires = GeneratedColumn( + 'ban_expires', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); static const VerificationMeta _shadowBannedMeta = const VerificationMeta( 'shadowBanned', ); @@ -9702,13 +10136,66 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> defaultValue: const Constant(false), ); @override - late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn( - 'extra_data', + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( + 'custom', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($MembersTable.$convertercustomn); + static const VerificationMeta _notificationsMutedMeta = const VerificationMeta('notificationsMuted'); + @override + late final GeneratedColumn notificationsMuted = GeneratedColumn( + 'notifications_muted', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("notifications_muted" IN (0, 1))', + ), + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - ).withConverter?>($MembersTable.$converterextraDatan); + ); + static const VerificationMeta _banFromFutureChannelsMeta = const VerificationMeta('banFromFutureChannels'); + @override + late final GeneratedColumn banFromFutureChannels = GeneratedColumn( + 'ban_from_future_channels', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("ban_from_future_channels" IN (0, 1))', + ), + ); + static const VerificationMeta _futureChannelBanExpiresMeta = const VerificationMeta('futureChannelBanExpires'); + @override + late final GeneratedColumn futureChannelBanExpires = GeneratedColumn( + 'future_channel_ban_expires', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', ); @@ -9750,11 +10237,17 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> inviteRejectedAt, invited, banned, + banExpires, shadowBanned, pinnedAt, archivedAt, isModerator, - extraData, + custom, + notificationsMuted, + status, + banFromFutureChannels, + futureChannelBanExpires, + deletedAt, createdAt, updatedAt, deletedMessages, @@ -9826,6 +10319,12 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta), ); } + if (data.containsKey('ban_expires')) { + context.handle( + _banExpiresMeta, + banExpires.isAcceptableOrUnknown(data['ban_expires']!, _banExpiresMeta), + ); + } if (data.containsKey('shadow_banned')) { context.handle( _shadowBannedMeta, @@ -9856,7 +10355,46 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> ), ); } - if (data.containsKey('created_at')) { + if (data.containsKey('notifications_muted')) { + context.handle( + _notificationsMutedMeta, + notificationsMuted.isAcceptableOrUnknown( + data['notifications_muted']!, + _notificationsMutedMeta, + ), + ); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } + if (data.containsKey('ban_from_future_channels')) { + context.handle( + _banFromFutureChannelsMeta, + banFromFutureChannels.isAcceptableOrUnknown( + data['ban_from_future_channels']!, + _banFromFutureChannelsMeta, + ), + ); + } + if (data.containsKey('future_channel_ban_expires')) { + context.handle( + _futureChannelBanExpiresMeta, + futureChannelBanExpires.isAcceptableOrUnknown( + data['future_channel_ban_expires']!, + _futureChannelBanExpiresMeta, + ), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + if (data.containsKey('created_at')) { context.handle( _createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), @@ -9905,6 +10443,10 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> DriftSqlType.bool, data['${effectivePrefix}banned'], )!, + banExpires: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}ban_expires'], + ), shadowBanned: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}shadow_banned'], @@ -9921,12 +10463,32 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> DriftSqlType.bool, data['${effectivePrefix}is_moderator'], )!, - extraData: $MembersTable.$converterextraDatan.fromSql( + custom: $MembersTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), + notificationsMuted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}notifications_muted'], + ), + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + ), + banFromFutureChannels: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}ban_from_future_channels'], + ), + futureChannelBanExpires: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}future_channel_ban_expires'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -9949,9 +10511,9 @@ class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> return $MembersTable(attachedDatabase, alias); } - static TypeConverter, String> $converterextraData = MapConverter(); - static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( - $converterextraData, + static TypeConverter, String> $convertercustom = MapConverter(); + static TypeConverter?, String?> $convertercustomn = NullAwareTypeConverter.wrap( + $convertercustom, ); static TypeConverter, String> $converterdeletedMessages = ListConverter(); } @@ -9978,6 +10540,11 @@ class MemberEntity extends DataClass implements Insertable { /// True if the member is banned from the channel final bool banned; + /// The date at which the ban will expire. + /// + /// Null when the member is not banned or the ban has no expiry. + final DateTime? banExpires; + /// True if the member is shadow banned from the channel final bool shadowBanned; @@ -9990,8 +10557,23 @@ class MemberEntity extends DataClass implements Insertable { /// True if the user is a moderator of the channel final bool isModerator; - /// Map of custom member extraData - final Map? extraData; + /// Custom data attached to the member. + final Map? custom; + + /// Whether the member muted notifications for the channel. + final bool? notificationsMuted; + + /// The status of the membership. + final String? status; + + /// Whether the member is banned from future channels. + final bool? banFromFutureChannels; + + /// The date at which the member's ban from future channels expires. + final DateTime? futureChannelBanExpires; + + /// The date at which the membership was deleted. + final DateTime? deletedAt; /// The date of creation final DateTime createdAt; @@ -10012,11 +10594,17 @@ class MemberEntity extends DataClass implements Insertable { this.inviteRejectedAt, required this.invited, required this.banned, + this.banExpires, required this.shadowBanned, this.pinnedAt, this.archivedAt, required this.isModerator, - this.extraData, + this.custom, + this.notificationsMuted, + this.status, + this.banFromFutureChannels, + this.futureChannelBanExpires, + this.deletedAt, required this.createdAt, required this.updatedAt, required this.deletedMessages, @@ -10037,6 +10625,9 @@ class MemberEntity extends DataClass implements Insertable { } map['invited'] = Variable(invited); map['banned'] = Variable(banned); + if (!nullToAbsent || banExpires != null) { + map['ban_expires'] = Variable(banExpires); + } map['shadow_banned'] = Variable(shadowBanned); if (!nullToAbsent || pinnedAt != null) { map['pinned_at'] = Variable(pinnedAt); @@ -10045,11 +10636,28 @@ class MemberEntity extends DataClass implements Insertable { map['archived_at'] = Variable(archivedAt); } map['is_moderator'] = Variable(isModerator); - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $MembersTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $MembersTable.$convertercustomn.toSql(custom), ); } + if (!nullToAbsent || notificationsMuted != null) { + map['notifications_muted'] = Variable(notificationsMuted); + } + if (!nullToAbsent || status != null) { + map['status'] = Variable(status); + } + if (!nullToAbsent || banFromFutureChannels != null) { + map['ban_from_future_channels'] = Variable(banFromFutureChannels); + } + if (!nullToAbsent || futureChannelBanExpires != null) { + map['future_channel_ban_expires'] = Variable( + futureChannelBanExpires, + ); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); { @@ -10077,11 +10685,23 @@ class MemberEntity extends DataClass implements Insertable { ), invited: serializer.fromJson(json['invited']), banned: serializer.fromJson(json['banned']), + banExpires: serializer.fromJson(json['banExpires']), shadowBanned: serializer.fromJson(json['shadowBanned']), pinnedAt: serializer.fromJson(json['pinnedAt']), archivedAt: serializer.fromJson(json['archivedAt']), isModerator: serializer.fromJson(json['isModerator']), - extraData: serializer.fromJson?>(json['extraData']), + custom: serializer.fromJson?>(json['custom']), + notificationsMuted: serializer.fromJson( + json['notificationsMuted'], + ), + status: serializer.fromJson(json['status']), + banFromFutureChannels: serializer.fromJson( + json['banFromFutureChannels'], + ), + futureChannelBanExpires: serializer.fromJson( + json['futureChannelBanExpires'], + ), + deletedAt: serializer.fromJson(json['deletedAt']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), deletedMessages: serializer.fromJson>( @@ -10100,11 +10720,19 @@ class MemberEntity extends DataClass implements Insertable { 'inviteRejectedAt': serializer.toJson(inviteRejectedAt), 'invited': serializer.toJson(invited), 'banned': serializer.toJson(banned), + 'banExpires': serializer.toJson(banExpires), 'shadowBanned': serializer.toJson(shadowBanned), 'pinnedAt': serializer.toJson(pinnedAt), 'archivedAt': serializer.toJson(archivedAt), 'isModerator': serializer.toJson(isModerator), - 'extraData': serializer.toJson?>(extraData), + 'custom': serializer.toJson?>(custom), + 'notificationsMuted': serializer.toJson(notificationsMuted), + 'status': serializer.toJson(status), + 'banFromFutureChannels': serializer.toJson(banFromFutureChannels), + 'futureChannelBanExpires': serializer.toJson( + futureChannelBanExpires, + ), + 'deletedAt': serializer.toJson(deletedAt), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'deletedMessages': serializer.toJson>(deletedMessages), @@ -10119,11 +10747,17 @@ class MemberEntity extends DataClass implements Insertable { Value inviteRejectedAt = const Value.absent(), bool? invited, bool? banned, + Value banExpires = const Value.absent(), bool? shadowBanned, Value pinnedAt = const Value.absent(), Value archivedAt = const Value.absent(), bool? isModerator, - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), + Value notificationsMuted = const Value.absent(), + Value status = const Value.absent(), + Value banFromFutureChannels = const Value.absent(), + Value futureChannelBanExpires = const Value.absent(), + Value deletedAt = const Value.absent(), DateTime? createdAt, DateTime? updatedAt, List? deletedMessages, @@ -10135,11 +10769,19 @@ class MemberEntity extends DataClass implements Insertable { inviteRejectedAt: inviteRejectedAt.present ? inviteRejectedAt.value : this.inviteRejectedAt, invited: invited ?? this.invited, banned: banned ?? this.banned, + banExpires: banExpires.present ? banExpires.value : this.banExpires, shadowBanned: shadowBanned ?? this.shadowBanned, pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, archivedAt: archivedAt.present ? archivedAt.value : this.archivedAt, isModerator: isModerator ?? this.isModerator, - extraData: extraData.present ? extraData.value : this.extraData, + custom: custom.present ? custom.value : this.custom, + notificationsMuted: notificationsMuted.present ? notificationsMuted.value : this.notificationsMuted, + status: status.present ? status.value : this.status, + banFromFutureChannels: banFromFutureChannels.present ? banFromFutureChannels.value : this.banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires.present + ? futureChannelBanExpires.value + : this.futureChannelBanExpires, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, deletedMessages: deletedMessages ?? this.deletedMessages, @@ -10153,11 +10795,21 @@ class MemberEntity extends DataClass implements Insertable { inviteRejectedAt: data.inviteRejectedAt.present ? data.inviteRejectedAt.value : this.inviteRejectedAt, invited: data.invited.present ? data.invited.value : this.invited, banned: data.banned.present ? data.banned.value : this.banned, + banExpires: data.banExpires.present ? data.banExpires.value : this.banExpires, shadowBanned: data.shadowBanned.present ? data.shadowBanned.value : this.shadowBanned, pinnedAt: data.pinnedAt.present ? data.pinnedAt.value : this.pinnedAt, archivedAt: data.archivedAt.present ? data.archivedAt.value : this.archivedAt, isModerator: data.isModerator.present ? data.isModerator.value : this.isModerator, - extraData: data.extraData.present ? data.extraData.value : this.extraData, + custom: data.custom.present ? data.custom.value : this.custom, + notificationsMuted: data.notificationsMuted.present ? data.notificationsMuted.value : this.notificationsMuted, + status: data.status.present ? data.status.value : this.status, + banFromFutureChannels: data.banFromFutureChannels.present + ? data.banFromFutureChannels.value + : this.banFromFutureChannels, + futureChannelBanExpires: data.futureChannelBanExpires.present + ? data.futureChannelBanExpires.value + : this.futureChannelBanExpires, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, deletedMessages: data.deletedMessages.present ? data.deletedMessages.value : this.deletedMessages, @@ -10174,11 +10826,17 @@ class MemberEntity extends DataClass implements Insertable { ..write('inviteRejectedAt: $inviteRejectedAt, ') ..write('invited: $invited, ') ..write('banned: $banned, ') + ..write('banExpires: $banExpires, ') ..write('shadowBanned: $shadowBanned, ') ..write('pinnedAt: $pinnedAt, ') ..write('archivedAt: $archivedAt, ') ..write('isModerator: $isModerator, ') - ..write('extraData: $extraData, ') + ..write('custom: $custom, ') + ..write('notificationsMuted: $notificationsMuted, ') + ..write('status: $status, ') + ..write('banFromFutureChannels: $banFromFutureChannels, ') + ..write('futureChannelBanExpires: $futureChannelBanExpires, ') + ..write('deletedAt: $deletedAt, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('deletedMessages: $deletedMessages') @@ -10187,7 +10845,7 @@ class MemberEntity extends DataClass implements Insertable { } @override - int get hashCode => Object.hash( + int get hashCode => Object.hashAll([ userId, channelCid, channelRole, @@ -10195,15 +10853,21 @@ class MemberEntity extends DataClass implements Insertable { inviteRejectedAt, invited, banned, + banExpires, shadowBanned, pinnedAt, archivedAt, isModerator, - extraData, + custom, + notificationsMuted, + status, + banFromFutureChannels, + futureChannelBanExpires, + deletedAt, createdAt, updatedAt, deletedMessages, - ); + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -10215,11 +10879,17 @@ class MemberEntity extends DataClass implements Insertable { other.inviteRejectedAt == this.inviteRejectedAt && other.invited == this.invited && other.banned == this.banned && + other.banExpires == this.banExpires && other.shadowBanned == this.shadowBanned && other.pinnedAt == this.pinnedAt && other.archivedAt == this.archivedAt && other.isModerator == this.isModerator && - other.extraData == this.extraData && + other.custom == this.custom && + other.notificationsMuted == this.notificationsMuted && + other.status == this.status && + other.banFromFutureChannels == this.banFromFutureChannels && + other.futureChannelBanExpires == this.futureChannelBanExpires && + other.deletedAt == this.deletedAt && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && other.deletedMessages == this.deletedMessages); @@ -10233,11 +10903,17 @@ class MembersCompanion extends UpdateCompanion { final Value inviteRejectedAt; final Value invited; final Value banned; + final Value banExpires; final Value shadowBanned; final Value pinnedAt; final Value archivedAt; final Value isModerator; - final Value?> extraData; + final Value?> custom; + final Value notificationsMuted; + final Value status; + final Value banFromFutureChannels; + final Value futureChannelBanExpires; + final Value deletedAt; final Value createdAt; final Value updatedAt; final Value> deletedMessages; @@ -10250,11 +10926,17 @@ class MembersCompanion extends UpdateCompanion { this.inviteRejectedAt = const Value.absent(), this.invited = const Value.absent(), this.banned = const Value.absent(), + this.banExpires = const Value.absent(), this.shadowBanned = const Value.absent(), this.pinnedAt = const Value.absent(), this.archivedAt = const Value.absent(), this.isModerator = const Value.absent(), - this.extraData = const Value.absent(), + this.custom = const Value.absent(), + this.notificationsMuted = const Value.absent(), + this.status = const Value.absent(), + this.banFromFutureChannels = const Value.absent(), + this.futureChannelBanExpires = const Value.absent(), + this.deletedAt = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), this.deletedMessages = const Value.absent(), @@ -10268,11 +10950,17 @@ class MembersCompanion extends UpdateCompanion { this.inviteRejectedAt = const Value.absent(), this.invited = const Value.absent(), this.banned = const Value.absent(), + this.banExpires = const Value.absent(), this.shadowBanned = const Value.absent(), this.pinnedAt = const Value.absent(), this.archivedAt = const Value.absent(), this.isModerator = const Value.absent(), - this.extraData = const Value.absent(), + this.custom = const Value.absent(), + this.notificationsMuted = const Value.absent(), + this.status = const Value.absent(), + this.banFromFutureChannels = const Value.absent(), + this.futureChannelBanExpires = const Value.absent(), + this.deletedAt = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), required List deletedMessages, @@ -10280,7 +10968,7 @@ class MembersCompanion extends UpdateCompanion { }) : userId = Value(userId), channelCid = Value(channelCid), deletedMessages = Value(deletedMessages); - static Insertable custom({ + static Insertable createCustom({ Expression? userId, Expression? channelCid, Expression? channelRole, @@ -10288,11 +10976,17 @@ class MembersCompanion extends UpdateCompanion { Expression? inviteRejectedAt, Expression? invited, Expression? banned, + Expression? banExpires, Expression? shadowBanned, Expression? pinnedAt, Expression? archivedAt, Expression? isModerator, - Expression? extraData, + Expression? custom, + Expression? notificationsMuted, + Expression? status, + Expression? banFromFutureChannels, + Expression? futureChannelBanExpires, + Expression? deletedAt, Expression? createdAt, Expression? updatedAt, Expression? deletedMessages, @@ -10306,11 +11000,17 @@ class MembersCompanion extends UpdateCompanion { if (inviteRejectedAt != null) 'invite_rejected_at': inviteRejectedAt, if (invited != null) 'invited': invited, if (banned != null) 'banned': banned, + if (banExpires != null) 'ban_expires': banExpires, if (shadowBanned != null) 'shadow_banned': shadowBanned, if (pinnedAt != null) 'pinned_at': pinnedAt, if (archivedAt != null) 'archived_at': archivedAt, if (isModerator != null) 'is_moderator': isModerator, - if (extraData != null) 'extra_data': extraData, + if (custom != null) 'custom': custom, + if (notificationsMuted != null) 'notifications_muted': notificationsMuted, + if (status != null) 'status': status, + if (banFromFutureChannels != null) 'ban_from_future_channels': banFromFutureChannels, + if (futureChannelBanExpires != null) 'future_channel_ban_expires': futureChannelBanExpires, + if (deletedAt != null) 'deleted_at': deletedAt, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, if (deletedMessages != null) 'deleted_messages': deletedMessages, @@ -10326,11 +11026,17 @@ class MembersCompanion extends UpdateCompanion { Value? inviteRejectedAt, Value? invited, Value? banned, + Value? banExpires, Value? shadowBanned, Value? pinnedAt, Value? archivedAt, Value? isModerator, - Value?>? extraData, + Value?>? custom, + Value? notificationsMuted, + Value? status, + Value? banFromFutureChannels, + Value? futureChannelBanExpires, + Value? deletedAt, Value? createdAt, Value? updatedAt, Value>? deletedMessages, @@ -10344,11 +11050,17 @@ class MembersCompanion extends UpdateCompanion { inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, invited: invited ?? this.invited, banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, shadowBanned: shadowBanned ?? this.shadowBanned, pinnedAt: pinnedAt ?? this.pinnedAt, archivedAt: archivedAt ?? this.archivedAt, isModerator: isModerator ?? this.isModerator, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, + notificationsMuted: notificationsMuted ?? this.notificationsMuted, + status: status ?? this.status, + banFromFutureChannels: banFromFutureChannels ?? this.banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires ?? this.futureChannelBanExpires, + deletedAt: deletedAt ?? this.deletedAt, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, deletedMessages: deletedMessages ?? this.deletedMessages, @@ -10380,6 +11092,9 @@ class MembersCompanion extends UpdateCompanion { if (banned.present) { map['banned'] = Variable(banned.value); } + if (banExpires.present) { + map['ban_expires'] = Variable(banExpires.value); + } if (shadowBanned.present) { map['shadow_banned'] = Variable(shadowBanned.value); } @@ -10392,11 +11107,30 @@ class MembersCompanion extends UpdateCompanion { if (isModerator.present) { map['is_moderator'] = Variable(isModerator.value); } - if (extraData.present) { - map['extra_data'] = Variable( - $MembersTable.$converterextraDatan.toSql(extraData.value), + if (custom.present) { + map['custom'] = Variable( + $MembersTable.$convertercustomn.toSql(custom.value), ); } + if (notificationsMuted.present) { + map['notifications_muted'] = Variable(notificationsMuted.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (banFromFutureChannels.present) { + map['ban_from_future_channels'] = Variable( + banFromFutureChannels.value, + ); + } + if (futureChannelBanExpires.present) { + map['future_channel_ban_expires'] = Variable( + futureChannelBanExpires.value, + ); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -10424,11 +11158,17 @@ class MembersCompanion extends UpdateCompanion { ..write('inviteRejectedAt: $inviteRejectedAt, ') ..write('invited: $invited, ') ..write('banned: $banned, ') + ..write('banExpires: $banExpires, ') ..write('shadowBanned: $shadowBanned, ') ..write('pinnedAt: $pinnedAt, ') ..write('archivedAt: $archivedAt, ') ..write('isModerator: $isModerator, ') - ..write('extraData: $extraData, ') + ..write('custom: $custom, ') + ..write('notificationsMuted: $notificationsMuted, ') + ..write('status: $status, ') + ..write('banFromFutureChannels: $banFromFutureChannels, ') + ..write('futureChannelBanExpires: $futureChannelBanExpires, ') + ..write('deletedAt: $deletedAt, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('deletedMessages: $deletedMessages, ') @@ -12048,7 +12788,15 @@ typedef $$ChannelsTableCreateCompanionBuilder = Value createdById, Value?> filterTags, Value team, - Value?> extraData, + Value disabled, + Value hidden, + Value muted, + Value blocked, + Value truncatedAt, + Value truncatedById, + Value autoTranslationEnabled, + Value autoTranslationLanguage, + Value?> custom, Value rowid, }); typedef $$ChannelsTableUpdateCompanionBuilder = @@ -12068,7 +12816,15 @@ typedef $$ChannelsTableUpdateCompanionBuilder = Value createdById, Value?> filterTags, Value team, - Value?> extraData, + Value disabled, + Value hidden, + Value muted, + Value blocked, + Value truncatedAt, + Value truncatedById, + Value autoTranslationEnabled, + Value autoTranslationLanguage, + Value?> custom, Value rowid, }); @@ -12078,7 +12834,7 @@ final class $$ChannelsTableReferences extends BaseReferences<_$DriftChatDatabase static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.messages, - aliasName: $_aliasNameGenerator(db.channels.cid, db.messages.channelCid), + aliasName: 'channels__cid__messages__channel_cid', ); $$MessagesTableProcessedTableManager get messagesRefs { @@ -12097,10 +12853,7 @@ final class $$ChannelsTableReferences extends BaseReferences<_$DriftChatDatabase _$DriftChatDatabase db, ) => MultiTypedResultKey.fromTable( db.draftMessages, - aliasName: $_aliasNameGenerator( - db.channels.cid, - db.draftMessages.channelCid, - ), + aliasName: 'channels__cid__draft_messages__channel_cid', ); $$DraftMessagesTableProcessedTableManager get draftMessagesRefs { @@ -12118,7 +12871,7 @@ final class $$ChannelsTableReferences extends BaseReferences<_$DriftChatDatabase static MultiTypedResultKey<$LocationsTable, List> _locationsRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.locations, - aliasName: $_aliasNameGenerator(db.channels.cid, db.locations.channelCid), + aliasName: 'channels__cid__locations__channel_cid', ); $$LocationsTableProcessedTableManager get locationsRefs { @@ -12136,7 +12889,7 @@ final class $$ChannelsTableReferences extends BaseReferences<_$DriftChatDatabase static MultiTypedResultKey<$MembersTable, List> _membersRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.members, - aliasName: $_aliasNameGenerator(db.channels.cid, db.members.channelCid), + aliasName: 'channels__cid__members__channel_cid', ); $$MembersTableProcessedTableManager get membersRefs { @@ -12155,7 +12908,7 @@ final class $$ChannelsTableReferences extends BaseReferences<_$DriftChatDatabase _$DriftChatDatabase db, ) => MultiTypedResultKey.fromTable( db.reads, - aliasName: $_aliasNameGenerator(db.channels.cid, db.reads.channelCid), + aliasName: 'channels__cid__reads__channel_cid', ); $$ReadsTableProcessedTableManager get readsRefs { @@ -12254,11 +13007,50 @@ class $$ChannelsTableFilterComposer extends Composer<_$DriftChatDatabase, $Chann builder: (column) => ColumnFilters(column), ); - ColumnWithTypeConverterFilters?, Map, String> get extraData => - $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column), - ); + ColumnFilters get disabled => $composableBuilder( + column: $table.disabled, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get hidden => $composableBuilder( + column: $table.hidden, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get muted => $composableBuilder( + column: $table.muted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get blocked => $composableBuilder( + column: $table.blocked, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get truncatedAt => $composableBuilder( + column: $table.truncatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get truncatedById => $composableBuilder( + column: $table.truncatedById, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get autoTranslationEnabled => $composableBuilder( + column: $table.autoTranslationEnabled, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get autoTranslationLanguage => $composableBuilder( + column: $table.autoTranslationLanguage, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters?, Map, String> get custom => $composableBuilder( + column: $table.custom, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); Expression messagesRefs( Expression Function($$MessagesTableFilterComposer f) f, @@ -12464,8 +13256,48 @@ class $$ChannelsTableOrderingComposer extends Composer<_$DriftChatDatabase, $Cha builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get disabled => $composableBuilder( + column: $table.disabled, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get hidden => $composableBuilder( + column: $table.hidden, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get muted => $composableBuilder( + column: $table.muted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get blocked => $composableBuilder( + column: $table.blocked, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get truncatedAt => $composableBuilder( + column: $table.truncatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get truncatedById => $composableBuilder( + column: $table.truncatedById, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get autoTranslationEnabled => $composableBuilder( + column: $table.autoTranslationEnabled, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get autoTranslationLanguage => $composableBuilder( + column: $table.autoTranslationLanguage, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, builder: (column) => ColumnOrderings(column), ); } @@ -12527,8 +13359,36 @@ class $$ChannelsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $C GeneratedColumn get team => $composableBuilder(column: $table.team, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> get extraData => - $composableBuilder(column: $table.extraData, builder: (column) => column); + GeneratedColumn get disabled => $composableBuilder(column: $table.disabled, builder: (column) => column); + + GeneratedColumn get hidden => $composableBuilder(column: $table.hidden, builder: (column) => column); + + GeneratedColumn get muted => $composableBuilder(column: $table.muted, builder: (column) => column); + + GeneratedColumn get blocked => $composableBuilder(column: $table.blocked, builder: (column) => column); + + GeneratedColumn get truncatedAt => $composableBuilder( + column: $table.truncatedAt, + builder: (column) => column, + ); + + GeneratedColumn get truncatedById => $composableBuilder( + column: $table.truncatedById, + builder: (column) => column, + ); + + GeneratedColumn get autoTranslationEnabled => $composableBuilder( + column: $table.autoTranslationEnabled, + builder: (column) => column, + ); + + GeneratedColumn get autoTranslationLanguage => $composableBuilder( + column: $table.autoTranslationLanguage, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> get custom => + $composableBuilder(column: $table.custom, builder: (column) => column); Expression messagesRefs( Expression Function($$MessagesTableAnnotationComposer a) f, @@ -12697,7 +13557,15 @@ class $$ChannelsTableTableManager Value createdById = const Value.absent(), Value?> filterTags = const Value.absent(), Value team = const Value.absent(), - Value?> extraData = const Value.absent(), + Value disabled = const Value.absent(), + Value hidden = const Value.absent(), + Value muted = const Value.absent(), + Value blocked = const Value.absent(), + Value truncatedAt = const Value.absent(), + Value truncatedById = const Value.absent(), + Value autoTranslationEnabled = const Value.absent(), + Value autoTranslationLanguage = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => ChannelsCompanion( id: id, @@ -12715,7 +13583,15 @@ class $$ChannelsTableTableManager createdById: createdById, filterTags: filterTags, team: team, - extraData: extraData, + disabled: disabled, + hidden: hidden, + muted: muted, + blocked: blocked, + truncatedAt: truncatedAt, + truncatedById: truncatedById, + autoTranslationEnabled: autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage, + custom: custom, rowid: rowid, ), createCompanionCallback: @@ -12735,7 +13611,15 @@ class $$ChannelsTableTableManager Value createdById = const Value.absent(), Value?> filterTags = const Value.absent(), Value team = const Value.absent(), - Value?> extraData = const Value.absent(), + Value disabled = const Value.absent(), + Value hidden = const Value.absent(), + Value muted = const Value.absent(), + Value blocked = const Value.absent(), + Value truncatedAt = const Value.absent(), + Value truncatedById = const Value.absent(), + Value autoTranslationEnabled = const Value.absent(), + Value autoTranslationLanguage = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => ChannelsCompanion.insert( id: id, @@ -12753,13 +13637,21 @@ class $$ChannelsTableTableManager createdById: createdById, filterTags: filterTags, team: team, - extraData: extraData, + disabled: disabled, + hidden: hidden, + muted: muted, + blocked: blocked, + truncatedAt: truncatedAt, + truncatedById: truncatedById, + autoTranslationEnabled: autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage, + custom: custom, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$ChannelsTable, ChannelEntity>(table), $$ChannelsTableReferences(db, table, e), ), ) @@ -12968,9 +13860,8 @@ typedef $$MessagesTableUpdateCompanionBuilder = final class $$MessagesTableReferences extends BaseReferences<_$DriftChatDatabase, $MessagesTable, MessageEntity> { $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => db.channels.createAlias( - $_aliasNameGenerator(db.messages.channelCid, db.channels.cid), - ); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias('messages__channel_cid__channels__cid'); $$ChannelsTableProcessedTableManager get channelCid { final $_column = $_itemColumn('channel_cid')!; @@ -12990,10 +13881,7 @@ final class $$MessagesTableReferences extends BaseReferences<_$DriftChatDatabase _$DriftChatDatabase db, ) => MultiTypedResultKey.fromTable( db.draftMessages, - aliasName: $_aliasNameGenerator( - db.messages.id, - db.draftMessages.parentId, - ), + aliasName: 'messages__id__draft_messages__parent_id', ); $$DraftMessagesTableProcessedTableManager get draftMessagesRefs { @@ -13011,7 +13899,7 @@ final class $$MessagesTableReferences extends BaseReferences<_$DriftChatDatabase static MultiTypedResultKey<$LocationsTable, List> _locationsRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.locations, - aliasName: $_aliasNameGenerator(db.messages.id, db.locations.messageId), + aliasName: 'messages__id__locations__message_id', ); $$LocationsTableProcessedTableManager get locationsRefs { @@ -13029,7 +13917,7 @@ final class $$MessagesTableReferences extends BaseReferences<_$DriftChatDatabase static MultiTypedResultKey<$ReactionsTable, List> _reactionsRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.reactions, - aliasName: $_aliasNameGenerator(db.messages.id, db.reactions.messageId), + aliasName: 'messages__id__reactions__message_id', ); $$ReactionsTableProcessedTableManager get reactionsRefs { @@ -13981,7 +14869,7 @@ class $$MessagesTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$MessagesTable, MessageEntity>(table), $$MessagesTableReferences(db, table, e), ), ) @@ -14145,9 +15033,8 @@ final class $$DraftMessagesTableReferences super.$_typedResult, ); - static $MessagesTable _parentIdTable(_$DriftChatDatabase db) => db.messages.createAlias( - $_aliasNameGenerator(db.draftMessages.parentId, db.messages.id), - ); + static $MessagesTable _parentIdTable(_$DriftChatDatabase db) => + db.messages.createAlias('draft_messages__parent_id__messages__id'); $$MessagesTableProcessedTableManager? get parentId { final $_column = $_itemColumn('parent_id'); @@ -14163,9 +15050,8 @@ final class $$DraftMessagesTableReferences ); } - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => db.channels.createAlias( - $_aliasNameGenerator(db.draftMessages.channelCid, db.channels.cid), - ); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias('draft_messages__channel_cid__channels__cid'); $$ChannelsTableProcessedTableManager get channelCid { final $_column = $_itemColumn('channel_cid')!; @@ -14598,7 +15484,7 @@ class $$DraftMessagesTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$DraftMessagesTable, DraftMessageEntity>(table), $$DraftMessagesTableReferences(db, table, e), ), ) @@ -14699,9 +15585,8 @@ typedef $$LocationsTableUpdateCompanionBuilder = final class $$LocationsTableReferences extends BaseReferences<_$DriftChatDatabase, $LocationsTable, LocationEntity> { $$LocationsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => db.channels.createAlias( - $_aliasNameGenerator(db.locations.channelCid, db.channels.cid), - ); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias('locations__channel_cid__channels__cid'); $$ChannelsTableProcessedTableManager? get channelCid { final $_column = $_itemColumn('channel_cid'); @@ -14717,9 +15602,8 @@ final class $$LocationsTableReferences extends BaseReferences<_$DriftChatDatabas ); } - static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => db.messages.createAlias( - $_aliasNameGenerator(db.locations.messageId, db.messages.id), - ); + static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => + db.messages.createAlias('locations__message_id__messages__id'); $$MessagesTableProcessedTableManager? get messageId { final $_column = $_itemColumn('message_id'); @@ -15056,7 +15940,7 @@ class $$LocationsTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$LocationsTable, LocationEntity>(table), $$LocationsTableReferences(db, table, e), ), ) @@ -15221,10 +16105,7 @@ final class $$PinnedMessagesTableReferences static MultiTypedResultKey<$PinnedMessageReactionsTable, List> _pinnedMessageReactionsRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.pinnedMessageReactions, - aliasName: $_aliasNameGenerator( - db.pinnedMessages.id, - db.pinnedMessageReactions.messageId, - ), + aliasName: 'pinned_messages__id__pinned_message_reactions__message_id', ); $$PinnedMessageReactionsTableProcessedTableManager get pinnedMessageReactionsRefs { @@ -16028,7 +16909,7 @@ class $$PinnedMessagesTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$PinnedMessagesTable, PinnedMessageEntity>(table), $$PinnedMessagesTableReferences(db, table, e), ), ) @@ -16130,7 +17011,7 @@ final class $$PollsTableReferences extends BaseReferences<_$DriftChatDatabase, $ static MultiTypedResultKey<$PollVotesTable, List> _pollVotesRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( db.pollVotes, - aliasName: $_aliasNameGenerator(db.polls.id, db.pollVotes.pollId), + aliasName: 'polls__id__poll_votes__poll_id', ); $$PollVotesTableProcessedTableManager get pollVotesRefs { @@ -16597,7 +17478,10 @@ class $$PollsTableTableManager ), withReferenceMapper: (p0) => p0 .map( - (e) => (e.readTable(table), $$PollsTableReferences(db, table, e)), + (e) => ( + e.readTable<$PollsTable, PollEntity>(table), + $$PollsTableReferences(db, table, e), + ), ) .toList(), prefetchHooksCallback: ({pollVotesRefs = false}) { @@ -16666,8 +17550,7 @@ typedef $$PollVotesTableUpdateCompanionBuilder = final class $$PollVotesTableReferences extends BaseReferences<_$DriftChatDatabase, $PollVotesTable, PollVoteEntity> { $$PollVotesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $PollsTable _pollIdTable(_$DriftChatDatabase db) => - db.polls.createAlias($_aliasNameGenerator(db.pollVotes.pollId, db.polls.id)); + static $PollsTable _pollIdTable(_$DriftChatDatabase db) => db.polls.createAlias('poll_votes__poll_id__polls__id'); $$PollsTableProcessedTableManager? get pollId { final $_column = $_itemColumn('poll_id'); @@ -16938,7 +17821,7 @@ class $$PollVotesTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$PollVotesTable, PollVoteEntity>(table), $$PollVotesTableReferences(db, table, e), ), ) @@ -17032,12 +17915,8 @@ final class $$PinnedMessageReactionsTableReferences super.$_typedResult, ); - static $PinnedMessagesTable _messageIdTable(_$DriftChatDatabase db) => db.pinnedMessages.createAlias( - $_aliasNameGenerator( - db.pinnedMessageReactions.messageId, - db.pinnedMessages.id, - ), - ); + static $PinnedMessagesTable _messageIdTable(_$DriftChatDatabase db) => + db.pinnedMessages.createAlias('pinned_message_reactions__message_id__pinned_messages__id'); $$PinnedMessagesTableProcessedTableManager? get messageId { final $_column = $_itemColumn('message_id'); @@ -17319,7 +18198,7 @@ class $$PinnedMessageReactionsTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$PinnedMessageReactionsTable, PinnedMessageReactionEntity>(table), $$PinnedMessageReactionsTableReferences(db, table, e), ), ) @@ -17408,9 +18287,8 @@ typedef $$ReactionsTableUpdateCompanionBuilder = final class $$ReactionsTableReferences extends BaseReferences<_$DriftChatDatabase, $ReactionsTable, ReactionEntity> { $$ReactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => db.messages.createAlias( - $_aliasNameGenerator(db.reactions.messageId, db.messages.id), - ); + static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => + db.messages.createAlias('reactions__message_id__messages__id'); $$MessagesTableProcessedTableManager? get messageId { final $_column = $_itemColumn('message_id'); @@ -17676,7 +18554,7 @@ class $$ReactionsTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$ReactionsTable, ReactionEntity>(table), $$ReactionsTableReferences(db, table, e), ), ) @@ -18018,7 +18896,18 @@ class $$UsersTableTableManager extraData: extraData, rowid: rowid, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable<$UsersTable, UserEntity>(table), + BaseReferences<_$DriftChatDatabase, $UsersTable, UserEntity>( + db, + table, + e, + ), + ), + ) + .toList(), prefetchHooksCallback: null, ), ); @@ -18050,11 +18939,17 @@ typedef $$MembersTableCreateCompanionBuilder = Value inviteRejectedAt, Value invited, Value banned, + Value banExpires, Value shadowBanned, Value pinnedAt, Value archivedAt, Value isModerator, - Value?> extraData, + Value?> custom, + Value notificationsMuted, + Value status, + Value banFromFutureChannels, + Value futureChannelBanExpires, + Value deletedAt, Value createdAt, Value updatedAt, required List deletedMessages, @@ -18069,11 +18964,17 @@ typedef $$MembersTableUpdateCompanionBuilder = Value inviteRejectedAt, Value invited, Value banned, + Value banExpires, Value shadowBanned, Value pinnedAt, Value archivedAt, Value isModerator, - Value?> extraData, + Value?> custom, + Value notificationsMuted, + Value status, + Value banFromFutureChannels, + Value futureChannelBanExpires, + Value deletedAt, Value createdAt, Value updatedAt, Value> deletedMessages, @@ -18083,9 +18984,8 @@ typedef $$MembersTableUpdateCompanionBuilder = final class $$MembersTableReferences extends BaseReferences<_$DriftChatDatabase, $MembersTable, MemberEntity> { $$MembersTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => db.channels.createAlias( - $_aliasNameGenerator(db.members.channelCid, db.channels.cid), - ); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias('members__channel_cid__channels__cid'); $$ChannelsTableProcessedTableManager get channelCid { final $_column = $_itemColumn('channel_cid')!; @@ -18140,6 +19040,11 @@ class $$MembersTableFilterComposer extends Composer<_$DriftChatDatabase, $Member builder: (column) => ColumnFilters(column), ); + ColumnFilters get banExpires => $composableBuilder( + column: $table.banExpires, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get shadowBanned => $composableBuilder( column: $table.shadowBanned, builder: (column) => ColumnFilters(column), @@ -18160,11 +19065,35 @@ class $$MembersTableFilterComposer extends Composer<_$DriftChatDatabase, $Member builder: (column) => ColumnFilters(column), ); - ColumnWithTypeConverterFilters?, Map, String> get extraData => - $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column), - ); + ColumnWithTypeConverterFilters?, Map, String> get custom => $composableBuilder( + column: $table.custom, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get notificationsMuted => $composableBuilder( + column: $table.notificationsMuted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get banFromFutureChannels => $composableBuilder( + column: $table.banFromFutureChannels, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get futureChannelBanExpires => $composableBuilder( + column: $table.futureChannelBanExpires, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, @@ -18242,6 +19171,11 @@ class $$MembersTableOrderingComposer extends Composer<_$DriftChatDatabase, $Memb builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get banExpires => $composableBuilder( + column: $table.banExpires, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get shadowBanned => $composableBuilder( column: $table.shadowBanned, builder: (column) => ColumnOrderings(column), @@ -18262,8 +19196,33 @@ class $$MembersTableOrderingComposer extends Composer<_$DriftChatDatabase, $Memb builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get notificationsMuted => $composableBuilder( + column: $table.notificationsMuted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get status => $composableBuilder( + column: $table.status, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get banFromFutureChannels => $composableBuilder( + column: $table.banFromFutureChannels, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get futureChannelBanExpires => $composableBuilder( + column: $table.futureChannelBanExpires, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column), ); @@ -18334,6 +19293,11 @@ class $$MembersTableAnnotationComposer extends Composer<_$DriftChatDatabase, $Me GeneratedColumn get banned => $composableBuilder(column: $table.banned, builder: (column) => column); + GeneratedColumn get banExpires => $composableBuilder( + column: $table.banExpires, + builder: (column) => column, + ); + GeneratedColumn get shadowBanned => $composableBuilder( column: $table.shadowBanned, builder: (column) => column, @@ -18351,8 +19315,27 @@ class $$MembersTableAnnotationComposer extends Composer<_$DriftChatDatabase, $Me builder: (column) => column, ); - GeneratedColumnWithTypeConverter?, String> get extraData => - $composableBuilder(column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get custom => + $composableBuilder(column: $table.custom, builder: (column) => column); + + GeneratedColumn get notificationsMuted => $composableBuilder( + column: $table.notificationsMuted, + builder: (column) => column, + ); + + GeneratedColumn get status => $composableBuilder(column: $table.status, builder: (column) => column); + + GeneratedColumn get banFromFutureChannels => $composableBuilder( + column: $table.banFromFutureChannels, + builder: (column) => column, + ); + + GeneratedColumn get futureChannelBanExpires => $composableBuilder( + column: $table.futureChannelBanExpires, + builder: (column) => column, + ); + + GeneratedColumn get deletedAt => $composableBuilder(column: $table.deletedAt, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -18418,11 +19401,17 @@ class $$MembersTableTableManager Value inviteRejectedAt = const Value.absent(), Value invited = const Value.absent(), Value banned = const Value.absent(), + Value banExpires = const Value.absent(), Value shadowBanned = const Value.absent(), Value pinnedAt = const Value.absent(), Value archivedAt = const Value.absent(), Value isModerator = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), + Value notificationsMuted = const Value.absent(), + Value status = const Value.absent(), + Value banFromFutureChannels = const Value.absent(), + Value futureChannelBanExpires = const Value.absent(), + Value deletedAt = const Value.absent(), Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), Value> deletedMessages = const Value.absent(), @@ -18435,11 +19424,17 @@ class $$MembersTableTableManager inviteRejectedAt: inviteRejectedAt, invited: invited, banned: banned, + banExpires: banExpires, shadowBanned: shadowBanned, pinnedAt: pinnedAt, archivedAt: archivedAt, isModerator: isModerator, - extraData: extraData, + custom: custom, + notificationsMuted: notificationsMuted, + status: status, + banFromFutureChannels: banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires, + deletedAt: deletedAt, createdAt: createdAt, updatedAt: updatedAt, deletedMessages: deletedMessages, @@ -18454,11 +19449,17 @@ class $$MembersTableTableManager Value inviteRejectedAt = const Value.absent(), Value invited = const Value.absent(), Value banned = const Value.absent(), + Value banExpires = const Value.absent(), Value shadowBanned = const Value.absent(), Value pinnedAt = const Value.absent(), Value archivedAt = const Value.absent(), Value isModerator = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), + Value notificationsMuted = const Value.absent(), + Value status = const Value.absent(), + Value banFromFutureChannels = const Value.absent(), + Value futureChannelBanExpires = const Value.absent(), + Value deletedAt = const Value.absent(), Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), required List deletedMessages, @@ -18471,11 +19472,17 @@ class $$MembersTableTableManager inviteRejectedAt: inviteRejectedAt, invited: invited, banned: banned, + banExpires: banExpires, shadowBanned: shadowBanned, pinnedAt: pinnedAt, archivedAt: archivedAt, isModerator: isModerator, - extraData: extraData, + custom: custom, + notificationsMuted: notificationsMuted, + status: status, + banFromFutureChannels: banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires, + deletedAt: deletedAt, createdAt: createdAt, updatedAt: updatedAt, deletedMessages: deletedMessages, @@ -18484,7 +19491,7 @@ class $$MembersTableTableManager withReferenceMapper: (p0) => p0 .map( (e) => ( - e.readTable(table), + e.readTable<$MembersTable, MemberEntity>(table), $$MembersTableReferences(db, table, e), ), ) @@ -18572,7 +19579,7 @@ final class $$ReadsTableReferences extends BaseReferences<_$DriftChatDatabase, $ $$ReadsTableReferences(super.$_db, super.$_table, super.$_typedResult); static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => - db.channels.createAlias($_aliasNameGenerator(db.reads.channelCid, db.channels.cid)); + db.channels.createAlias('reads__channel_cid__channels__cid'); $$ChannelsTableProcessedTableManager get channelCid { final $_column = $_itemColumn('channel_cid')!; @@ -18831,7 +19838,10 @@ class $$ReadsTableTableManager ), withReferenceMapper: (p0) => p0 .map( - (e) => (e.readTable(table), $$ReadsTableReferences(db, table, e)), + (e) => ( + e.readTable<$ReadsTable, ReadEntity>(table), + $$ReadsTableReferences(db, table, e), + ), ) .toList(), prefetchHooksCallback: ({channelCid = false}) { @@ -19005,7 +20015,14 @@ class $$ChannelQueriesTableTableManager channelCid: channelCid, rowid: rowid, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable<$ChannelQueriesTable, ChannelQueryEntity>(table), + BaseReferences<_$DriftChatDatabase, $ChannelQueriesTable, ChannelQueryEntity>(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, ), ); @@ -19171,7 +20188,18 @@ class $$ChannelQueriesMetadataTableTableManager sort: sort, rowid: rowid, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable<$ChannelQueriesMetadataTable, ChannelQueryMetadataEntity>(table), + BaseReferences<_$DriftChatDatabase, $ChannelQueriesMetadataTable, ChannelQueryMetadataEntity>( + db, + table, + e, + ), + ), + ) + .toList(), prefetchHooksCallback: null, ), ); @@ -19403,7 +20431,16 @@ class $$ConnectionEventsTableTableManager lastEventAt: lastEventAt, lastSyncAt: lastSyncAt, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable<$ConnectionEventsTable, ConnectionEventEntity>( + table, + ), + BaseReferences<_$DriftChatDatabase, $ConnectionEventsTable, ConnectionEventEntity>(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, ), ); diff --git a/packages/stream_chat_persistence/lib/src/entity/channels.dart b/packages/stream_chat_persistence/lib/src/entity/channels.dart index 2b20d1d425..d511731722 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channels.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channels.dart @@ -50,8 +50,32 @@ class Channels extends Table { /// The team the channel belongs to TextColumn get team => text().nullable()(); - /// Map of custom channel extraData - TextColumn get extraData => text().nullable().map(MapConverter())(); + /// Whether the channel is disabled. + BoolColumn get disabled => boolean().nullable()(); + + /// Whether the channel is hidden. + BoolColumn get hidden => boolean().nullable()(); + + /// Whether the current user muted the channel. + BoolColumn get muted => boolean().nullable()(); + + /// Whether the current user blocked the channel. + BoolColumn get blocked => boolean().nullable()(); + + /// The date the channel was last truncated. + DateTimeColumn get truncatedAt => dateTime().nullable()(); + + /// The id of the user that last truncated the channel. + TextColumn get truncatedById => text().nullable()(); + + /// Whether messages in the channel are translated automatically. + BoolColumn get autoTranslationEnabled => boolean().nullable()(); + + /// The language the channel's messages are translated into automatically. + TextColumn get autoTranslationLanguage => text().nullable()(); + + /// Custom data attached to the channel. + TextColumn get custom => text().nullable().map(MapConverter())(); @override Set get primaryKey => {cid}; diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 920185a387..946aaf6b6a 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -28,6 +28,11 @@ class Members extends Table { /// True if the member is banned from the channel BoolColumn get banned => boolean().withDefault(const Constant(false))(); + /// The date at which the ban will expire. + /// + /// Null when the member is not banned or the ban has no expiry. + DateTimeColumn get banExpires => dateTime().nullable()(); + /// True if the member is shadow banned from the channel BoolColumn get shadowBanned => boolean().withDefault(const Constant(false))(); @@ -40,8 +45,23 @@ class Members extends Table { /// True if the user is a moderator of the channel BoolColumn get isModerator => boolean().withDefault(const Constant(false))(); - /// Map of custom member extraData - TextColumn get extraData => text().nullable().map(MapConverter())(); + /// Custom data attached to the member. + TextColumn get custom => text().nullable().map(MapConverter())(); + + /// Whether the member muted notifications for the channel. + BoolColumn get notificationsMuted => boolean().nullable()(); + + /// The status of the membership. + TextColumn get status => text().nullable()(); + + /// Whether the member is banned from future channels. + BoolColumn get banFromFutureChannels => boolean().nullable()(); + + /// The date at which the member's ban from future channels expires. + DateTimeColumn get futureChannelBanExpires => dateTime().nullable()(); + + /// The date at which the membership was deleted. + DateTimeColumn get deletedAt => dateTime().nullable()(); /// The date of creation DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart index ba5c5113f4..4a277e5fa0 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart @@ -4,7 +4,7 @@ import '../db/drift_chat_database.dart'; /// Useful mapping functions for [ChannelEntity] extension ChannelEntityX on ChannelEntity { /// Maps a [ChannelEntity] into [ChannelModel] - ChannelModel toChannelModel({User? createdBy}) { + ChannelModel toChannelModel({User? createdBy, User? truncatedBy}) { final config = ChannelConfig.fromJson(this.config); return ChannelModel( id: id, @@ -22,13 +22,22 @@ extension ChannelEntityX on ChannelEntity { createdBy: createdBy, filterTags: filterTags, team: team, - extraData: extraData ?? {}, + custom: custom ?? {}, + disabled: disabled, + hidden: hidden, + muted: muted, + blocked: blocked, + truncatedAt: truncatedAt, + truncatedBy: truncatedBy, + autoTranslationEnabled: autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage, ); } /// Maps a [ChannelEntity] into [ChannelState] ChannelState toChannelState({ User? createdBy, + User? truncatedBy, List members = const [], List reads = const [], List messages = const [], @@ -38,7 +47,7 @@ extension ChannelEntityX on ChannelEntity { read: reads, messages: messages, pinnedMessages: pinnedMessages, - channel: toChannelModel(createdBy: createdBy), + channel: toChannelModel(createdBy: createdBy, truncatedBy: truncatedBy), ); } @@ -61,6 +70,14 @@ extension ChannelModelX on ChannelModel { createdById: createdBy?.id, filterTags: filterTags, team: team, - extraData: extraData, + custom: custom, + disabled: disabled, + hidden: hidden, + muted: muted, + blocked: blocked, + truncatedAt: truncatedAt, + truncatedById: truncatedBy?.id, + autoTranslationEnabled: autoTranslationEnabled, + autoTranslationLanguage: autoTranslationLanguage, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart index 322c29ecb3..c1c50531f6 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart @@ -8,6 +8,7 @@ extension MemberEntityX on MemberEntity { user: user, userId: userId, banned: banned, + banExpires: banExpires, shadowBanned: shadowBanned, updatedAt: updatedAt, createdAt: createdAt, @@ -19,7 +20,12 @@ extension MemberEntityX on MemberEntity { archivedAt: archivedAt, isModerator: isModerator, deletedMessages: deletedMessages, - extraData: extraData ?? {}, + custom: custom ?? {}, + notificationsMuted: notificationsMuted, + status: status, + banFromFutureChannels: banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires, + deletedAt: deletedAt, ); } @@ -29,6 +35,7 @@ extension MemberX on Member { MemberEntity toEntity({required String cid}) => MemberEntity( userId: user!.id, banned: banned, + banExpires: banExpires, shadowBanned: shadowBanned, channelCid: cid, createdAt: createdAt, @@ -41,6 +48,11 @@ extension MemberX on Member { channelRole: channelRole, updatedAt: updatedAt, deletedMessages: deletedMessages, - extraData: extraData, + custom: custom, + notificationsMuted: notificationsMuted, + status: status, + banFromFutureChannels: banFromFutureChannels, + futureChannelBanExpires: futureChannelBanExpires, + deletedAt: deletedAt, ); } diff --git a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart index c14159dc2b..ee630fd3a6 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -24,12 +24,17 @@ void main() { expect(channel, isNull); // Saving a dummy channel + final createdBy = User(id: 'creatorId'); + final truncatedBy = User(id: 'truncatorId'); final dummyChannel = ChannelModel( id: id, type: type, cid: cid, config: ChannelConfig(), + createdBy: createdBy, + truncatedBy: truncatedBy, ); + await database.userDao.updateUsers([createdBy, truncatedBy]); await channelDao.updateChannels([dummyChannel]); // Should match the dummy channel @@ -38,6 +43,8 @@ void main() { expect(updatedChannel!.id, id); expect(updatedChannel.cid, cid); expect(updatedChannel.type, type); + expect(updatedChannel.createdBy?.id, createdBy.id); + expect(updatedChannel.truncatedBy?.id, truncatedBy.id); }); test('deleteChannelByCids', () async { diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index edd4245b68..83f1680954 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -74,6 +74,7 @@ void main() { final cids = List.generate(count, (index) => 'testCid$index'); final users = List.generate(count, (index) => User(id: 'testId$index')); + final truncators = List.generate(count, (index) => User(id: 'truncatorId$index')); final channels = List.generate( count, (index) => ChannelModel( @@ -81,9 +82,10 @@ void main() { type: 'testType$index', cid: cids[index], createdBy: users[index], + truncatedBy: truncators[index], config: ChannelConfig(), filterTags: ['tag$index'], - extraData: {'test_custom_field': index + 3}, + custom: {'test_custom_field': index + 3}, createdAt: now, memberCount: index + 3, lastMessageAt: now.add(Duration(hours: index)), @@ -91,7 +93,7 @@ void main() { ), ).toList(growable: false); - await userDao.updateUsers(users); + await userDao.updateUsers([...users, ...truncators]); await channelDao.updateChannels(channels); await channelQueryDao.updateChannelQueries(filter, cids); @@ -126,6 +128,8 @@ void main() { expect(updatedChannel.memberCount, insertedChannel.memberCount); expect(updatedChannel.filterTags, insertedChannel.filterTags); expect(updatedChannel.team, insertedChannel.team); + expect(updatedChannel.createdBy?.id, insertedChannel.createdBy?.id); + expect(updatedChannel.truncatedBy?.id, insertedChannel.truncatedBy?.id); // Should match createdAt date expect( diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index f6353f52d3..7c0b1d9727 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -25,6 +25,7 @@ void main() { (index) => Member( user: users[index], banned: math.Random().nextBool(), + banExpires: DateTime.now(), shadowBanned: math.Random().nextBool(), createdAt: DateTime.now(), pinnedAt: DateTime.now(), @@ -34,7 +35,7 @@ void main() { inviteAcceptedAt: DateTime.now(), channelRole: 'testRole', updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, ), ); await database.userDao.updateUsers(users); @@ -61,6 +62,7 @@ void main() { final fetchedMember = fetchedMembers[i]; expect(fetchedMember.user!.id, member.user!.id); expect(fetchedMember.banned, member.banned); + expect(fetchedMember.banExpires, isSameDateAs(member.banExpires)); expect(fetchedMember.shadowBanned, member.shadowBanned); expect(fetchedMember.createdAt, isSameDateAs(member.createdAt)); expect(fetchedMember.pinnedAt, isSameDateAs(member.pinnedAt)); @@ -73,7 +75,7 @@ void main() { fetchedMember.inviteAcceptedAt, isSameDateAs(member.inviteAcceptedAt), ); - expect(fetchedMember.extraData, member.extraData); + expect(fetchedMember.custom, member.custom); } }); @@ -162,7 +164,7 @@ void main() { fetchedMember.inviteAcceptedAt, isSameDateAs(member.inviteAcceptedAt), ); - expect(fetchedMember.extraData, member.extraData); + expect(fetchedMember.custom, member.custom); } // Modifying one of the member and also adding one new diff --git a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart index 4135607c11..f3c4af1e0e 100644 --- a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart @@ -10,6 +10,7 @@ import '../utils/date_matcher.dart'; void main() { group('ChannelEntity', () { final user = User(id: 'testUserId'); + final truncatedBy = User(id: 'truncatorId'); final entity = ChannelEntity( id: 'testId', type: 'testType', @@ -33,12 +34,20 @@ void main() { messageCount: 42, createdById: user.id, filterTags: ['tag1', 'tag2'], - extraData: {'test_extra_data': 'testData'}, + custom: {'test_extra_data': 'testData'}, team: 'testTeam', + disabled: false, + hidden: true, + muted: true, + blocked: false, + truncatedAt: DateTime.now(), + truncatedById: truncatedBy.id, + autoTranslationEnabled: true, + autoTranslationLanguage: 'fr', ); test('toChannelModel should map entity into ChannelModel', () { - final channelModel = entity.toChannelModel(createdBy: user); + final channelModel = entity.toChannelModel(createdBy: user, truncatedBy: truncatedBy); expect(channelModel, isA()); expect(channelModel.id, entity.id); expect(channelModel.ownCapabilities, entity.ownCapabilities); @@ -57,8 +66,16 @@ void main() { expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt)); expect(channelModel.createdBy!.id, entity.createdById); expect(channelModel.filterTags, entity.filterTags); - expect(channelModel.extraData, entity.extraData); + expect(channelModel.custom, entity.custom); expect(channelModel.team, entity.team); + expect(channelModel.disabled, entity.disabled); + expect(channelModel.hidden, entity.hidden); + expect(channelModel.muted, entity.muted); + expect(channelModel.blocked, entity.blocked); + expect(channelModel.truncatedAt, isSameDateAs(entity.truncatedAt)); + expect(channelModel.truncatedBy!.id, entity.truncatedById); + expect(channelModel.autoTranslationEnabled, entity.autoTranslationEnabled); + expect(channelModel.autoTranslationLanguage, entity.autoTranslationLanguage); }); test('toChannelState should map entity into ChannelState ', () { @@ -75,6 +92,7 @@ void main() { final channelState = entity.toChannelState( createdBy: user, + truncatedBy: truncatedBy, members: members, reads: reads, messages: messages, @@ -104,8 +122,16 @@ void main() { expect(channelModel.lastMessageAt, isSameDateAs(entity.lastMessageAt)); expect(channelModel.deletedAt, isSameDateAs(entity.deletedAt)); expect(channelModel.filterTags, entity.filterTags); - expect(channelModel.extraData, entity.extraData); + expect(channelModel.custom, entity.custom); expect(channelModel.createdBy!.id, entity.createdById); + expect(channelModel.disabled, entity.disabled); + expect(channelModel.hidden, entity.hidden); + expect(channelModel.muted, entity.muted); + expect(channelModel.blocked, entity.blocked); + expect(channelModel.truncatedAt, isSameDateAs(entity.truncatedAt)); + expect(channelModel.truncatedBy!.id, entity.truncatedById); + expect(channelModel.autoTranslationEnabled, entity.autoTranslationEnabled); + expect(channelModel.autoTranslationLanguage, entity.autoTranslationLanguage); }); }); @@ -134,8 +160,16 @@ void main() { messageCount: 75, createdBy: createdBy, filterTags: ['tag1', 'tag2'], - extraData: {'test_extra_data': 'testData'}, + custom: {'test_extra_data': 'testData'}, team: 'testTeam', + disabled: false, + hidden: true, + muted: true, + blocked: false, + truncatedAt: DateTime.now(), + truncatedBy: User(id: 'truncatorId'), + autoTranslationEnabled: true, + autoTranslationLanguage: 'fr', ); final channelEntity = model.toEntity(); @@ -164,8 +198,16 @@ void main() { expect(channelEntity.lastMessageAt, isSameDateAs(model.lastMessageAt)); expect(channelEntity.deletedAt, isSameDateAs(model.deletedAt)); expect(channelEntity.filterTags, model.filterTags); - expect(channelEntity.extraData, model.extraData); + expect(channelEntity.custom, model.custom); expect(channelEntity.createdById, model.createdBy!.id); expect(channelEntity.team, model.team); + expect(channelEntity.disabled, model.disabled); + expect(channelEntity.hidden, model.hidden); + expect(channelEntity.muted, model.muted); + expect(channelEntity.blocked, model.blocked); + expect(channelEntity.truncatedAt, isSameDateAs(model.truncatedAt)); + expect(channelEntity.truncatedById, model.truncatedBy!.id); + expect(channelEntity.autoTranslationEnabled, model.autoTranslationEnabled); + expect(channelEntity.autoTranslationLanguage, model.autoTranslationLanguage); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart index a577a6d078..a121bfcec4 100644 --- a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -25,7 +25,13 @@ void main() { archivedAt: DateTime.now(), isModerator: math.Random().nextBool(), deletedMessages: ['msg1', 'msg2', 'msg3'], - extraData: {'test_extra_data': 'testData'}, + custom: {'test_extra_data': 'testData'}, + banExpires: DateTime.now(), + notificationsMuted: true, + status: 'member', + banFromFutureChannels: true, + futureChannelBanExpires: DateTime.now(), + deletedAt: DateTime.now(), ); final member = entity.toMember(user: user); expect(member, isA()); @@ -42,7 +48,13 @@ void main() { expect(member.archivedAt, isSameDateAs(entity.archivedAt)); expect(member.isModerator, entity.isModerator); expect(member.deletedMessages, entity.deletedMessages); - expect(member.extraData, entity.extraData); + expect(member.custom, entity.custom); + expect(member.banExpires, isSameDateAs(entity.banExpires)); + expect(member.notificationsMuted, entity.notificationsMuted); + expect(member.status, entity.status); + expect(member.banFromFutureChannels, entity.banFromFutureChannels); + expect(member.futureChannelBanExpires, isSameDateAs(entity.futureChannelBanExpires)); + expect(member.deletedAt, isSameDateAs(entity.deletedAt)); }); test('toEntity show map member into MemberEntity', () { @@ -62,7 +74,13 @@ void main() { archivedAt: DateTime.now(), isModerator: math.Random().nextBool(), deletedMessages: const ['msg1', 'msg2', 'msg3'], - extraData: const {'test_extra_data': 'testData'}, + custom: const {'test_extra_data': 'testData'}, + banExpires: DateTime.now(), + notificationsMuted: true, + status: 'member', + banFromFutureChannels: true, + futureChannelBanExpires: DateTime.now(), + deletedAt: DateTime.now(), ); final entity = member.toEntity(cid: cid); expect(entity, isA()); @@ -80,6 +98,12 @@ void main() { expect(entity.archivedAt, isSameDateAs(member.archivedAt)); expect(entity.isModerator, member.isModerator); expect(entity.deletedMessages, member.deletedMessages); - expect(entity.extraData, member.extraData); + expect(entity.custom, member.custom); + expect(entity.banExpires, isSameDateAs(member.banExpires)); + expect(entity.notificationsMuted, member.notificationsMuted); + expect(entity.status, member.status); + expect(entity.banFromFutureChannels, member.banFromFutureChannels); + expect(entity.futureChannelBanExpires, isSameDateAs(member.futureChannelBanExpires)); + expect(entity.deletedAt, isSameDateAs(member.deletedAt)); }); } diff --git a/sample_app/benchmark/main_benchmark.dart b/sample_app/benchmark/main_benchmark.dart index e66e37700d..5f4e7d1dfc 100644 --- a/sample_app/benchmark/main_benchmark.dart +++ b/sample_app/benchmark/main_benchmark.dart @@ -64,7 +64,7 @@ class _BenchmarkLauncherState extends State<_BenchmarkLauncher> { final channel = client.channel( _benchmarkChannelType, id: _benchmarkChannelId, - extraData: const {'name': 'Perf benchmark'}, + custom: const {'name': 'Perf benchmark'}, ); await channel.watch( messagesPagination: const PaginationParams(limit: 25), diff --git a/sample_app/lib/pages/group_chat_details_screen.dart b/sample_app/lib/pages/group_chat_details_screen.dart index 255660f044..33384bd172 100644 --- a/sample_app/lib/pages/group_chat_details_screen.dart +++ b/sample_app/lib/pages/group_chat_details_screen.dart @@ -65,7 +65,7 @@ class _GroupChatDetailsScreenState extends State { final channel = client.channel( 'messaging', id: const Uuid().v4(), - extraData: { + custom: { 'members': [ client.state.currentUser!.id, ...widget.groupChatState.users.map((e) => e.id), diff --git a/sample_app/lib/pages/new_chat_screen.dart b/sample_app/lib/pages/new_chat_screen.dart index 4281dd3901..e771e7a2eb 100644 --- a/sample_app/lib/pages/new_chat_screen.dart +++ b/sample_app/lib/pages/new_chat_screen.dart @@ -106,7 +106,7 @@ class _NewChatScreenState extends State { } else { channel = chatState.client.channel( 'messaging', - extraData: { + custom: { 'members': [ ..._selectedUsers.map((e) => e.id), chatState.currentUser!.id, diff --git a/sample_app/lib/widgets/all_members_sheet.dart b/sample_app/lib/widgets/all_members_sheet.dart index 36f73dabe4..e9dc7cba23 100644 --- a/sample_app/lib/widgets/all_members_sheet.dart +++ b/sample_app/lib/widgets/all_members_sheet.dart @@ -426,7 +426,7 @@ Future _openDirectChannel(BuildContext context, User user) async { } else { channel = chat.client.channel( 'messaging', - extraData: { + custom: { 'members': [currentUser.id, user.id], }, ); diff --git a/sample_app/lib/widgets/edit_group_sheet.dart b/sample_app/lib/widgets/edit_group_sheet.dart index b1779c98c1..90a01d3c13 100644 --- a/sample_app/lib/widgets/edit_group_sheet.dart +++ b/sample_app/lib/widgets/edit_group_sheet.dart @@ -74,7 +74,7 @@ class _EditGroupSheetState extends State { final List _trackedUploads = []; String get _name => _nameController.text.trim(); - String get _initialName => (_channel.extraData['name'] as String?) ?? ''; + String get _initialName => (_channel.custom['name'] as String?) ?? ''; bool get _isDirty { if (_name != _initialName) return true; @@ -268,7 +268,12 @@ class _EditGroupSheetState extends State { } if (set.isNotEmpty || unset.isNotEmpty) { - await _channel.updatePartial(set: set, unset: unset); + final result = await _channel.updatePartial(set: set, unset: unset); + if (result case Failure(:final error)) { + messenger.showSnackBar(SnackBar(content: Text('Failed to save: $error'))); + if (mounted) setState(() => _saving = false); + return; + } } // Strip the saved URL from the orphan list so dispose() doesn't