diff --git a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart index 6653c22c13..e1f3129c6c 100644 --- a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart +++ b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart @@ -262,7 +262,7 @@ void main() { type: 'voiceRecording', assetUrl: 'https://example.com/recording.m4a', uploadState: const UploadState.success(), - extraData: const { + custom: const { 'duration': 15.0, 'waveform_data': [0.1, 0.5, 0.9, 0.4, 0.2], }, @@ -294,7 +294,7 @@ void main() { type: 'voiceRecording', assetUrl: 'https://example.com/recording.m4a', uploadState: const UploadState.success(), - extraData: const { + custom: const { 'duration': 15.0, 'waveform_data': [0.1, 0.5, 0.9, 0.4, 0.2], }, @@ -328,7 +328,7 @@ void main() { type: 'voiceRecording', assetUrl: 'https://example.com/recording.m4a', uploadState: const UploadState.success(), - extraData: const { + custom: const { 'duration': 15.0, 'waveform_data': [ 0.1, @@ -423,7 +423,7 @@ void main() { type: 'voiceRecording', assetUrl: 'https://example.com/recording.m4a', uploadState: const UploadState.success(), - extraData: const { + custom: const { 'duration': 15.0, 'waveform_data': [ 0.1, @@ -516,7 +516,7 @@ void main() { type: 'voiceRecording', assetUrl: 'https://example.com/recording.m4a', uploadState: const UploadState.success(), - extraData: const { + custom: const { 'duration': 15.0, 'waveform_data': [ 0.1, diff --git a/migrations/v11-migration.md b/migrations/v11-migration.md index 5d30772970..f67a0c5bfa 100644 --- a/migrations/v11-migration.md +++ b/migrations/v11-migration.md @@ -31,7 +31,9 @@ onto Stream's OpenAPI-generated API client. - [App Settings](#app-settings) - [Guest Users](#guest-users) - [Polls](#polls) + - [Message Reminders](#message-reminders) - [File Upload](#file-upload) + - [Messages](#messages) - [User Groups](#user-groups) - [Link Previews](#link-previews) - [Partial Updates](#partial-updates) @@ -42,6 +44,7 @@ onto Stream's OpenAPI-generated API client. - [User Updates](#user-updates) - [Channel and Member Custom Data](#channel-and-member-custom-data) - [User Custom Data](#user-custom-data) + - [Message Custom Data](#message-custom-data) - [Migration Checklist](#migration-checklist) - [For AI Agents](#for-ai-agents) - [Contributing to this guide](#contributing-to-this-guide) @@ -87,15 +90,18 @@ from the spec, so don't subclass them or depend on their private constructors. | [**App Settings**](#app-settings) | `getAppSettings` returns a `Result` instead of throwing; `GetAppSettingsResponse` is renamed `AppSettingsResponse` | | [**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` | +| [**Message Reminders**](#message-reminders) | Reminder calls return a `Result`; `deleteReminder` answers a new `DeleteReminderResponse`; `queryReminders` takes `limit`/`next`/`prev`; `MessageReminder` no longer decodes JSON | | [**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 Lifecycle**](#channel-lifecycle) | Hiding, showing and deleting a channel return a `Result` instead of throwing | | [**Read Receipts**](#read-receipts) | Marking read, unread and delivered return a `Result` instead of throwing; `ChannelDeliveryReporter`'s callback returns a `Result` | | [**Unread Counts**](#unread-counts) | `getUnreadCount` returns a `Result` instead of throwing; the response and its `UnreadCounts*` models lose their JSON codecs and compare by value | | [**User Blocking**](#user-blocking) | `blockUser`, `unblockUser` and `getBlockedUsers` (was `queryBlockedUsers`) return a `Result` instead of throwing; their responses are renamed `BlockUsersResponse` and `GetBlockedUsersResponse`, `unblockUser` answers a new `UnblockUsersResponse`, and `UserBlock`'s fields are all non-nullable | +| [**Messages**](#messages) | `Moderation`, `ReactionGroup`, `Action`, `Reaction` and `Location` no longer decode from or encode to JSON; `Action` compares by value | | [**User Updates**](#user-updates) | `updateUser` and `updateUsers` return a `Result` instead of throwing; `partialUpdateUser(s)` becomes `updateUserPartial` / `updateUsersPartial` and `PartialUpdateUserRequest` becomes `UpdateUserPartialRequest`; `updateUser` no longer sends `role`, `teams` or `teamsRole`, and the returned users no longer carry their private fields in `extraData` | | [**Channel and Member Custom Data**](#channel-and-member-custom-data) | `extraData` is `custom` on `ChannelModel`, `Member` and `Channel`; the channel and member flags it carried are fields | | [**User Custom Data**](#user-custom-data) | `extraData` is `custom` on `User` and `OwnUser`; `name` is nullable and `nameOrId` falls back to the id; the user fields it carried are fields | +| [**Message Custom Data**](#message-custom-data) | `extraData` is `custom` on `Message`, `Attachment`, `Reaction` and `DraftMessage`; a message's channel id, HTML, markup and deleted reply count, and an attachment's Giphy renditions, are fields | | _(filled in per feature as PRs land)_ | | --- @@ -308,6 +314,16 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `UserBlock.blockedUser` (`User?`), `.userId` / `.blockedUserId` (`String?`), `.createdAt` (`DateTime?`) | `User`, `String`, `DateTime` — required in the constructor | `retyped` | The server always sends them; drop any `!`, `?.` or `?? …` | | `UserBlock.copyWith(user: User?, …)`, keeping a field when passed `null` | `copyWith(user: User, …)` | `retyped` | Every argument is non-nullable, so a call that passed a nullable value no longer compiles; pass the value or leave the argument out | | `StreamChatApi.user.blockUser` / `unblockUser` / `queryBlockedUsers` | `StreamChatClient.blockUser` / `unblockUser` / `getBlockedUsers` | `removed` | The endpoints moved to the generated client | +| `Moderation.fromJson` / `toJson` | — | `removed` | A plain class; construct it directly. `Message.fromJson` still reads a message's moderation | +| `Moderation extends Equatable`, `Moderation.props` | `Moderation` (value `==`, `copyWith`) | `removed` | Equality is unchanged; `props` is gone and `Moderation` is no longer an `Equatable` | +| `ReactionGroup.fromJson` / `toJson` | — | `removed` | A plain class; construct it directly. `Message.fromJson` still reads a message's reaction groups. `fromData` / `toData` are the offline database's format, not API JSON | +| `ReactionGroup extends Equatable`, `ReactionGroup.props` | `ReactionGroup` (value `==`) | `removed` | Equality is unchanged; `props` is gone and `ReactionGroup` is no longer an `Equatable` | +| `Action.fromJson` / `toJson` | — | `removed` | A plain class; construct it directly. `Attachment.fromJson` and `toJson` still read and write an attachment's actions | +| `Action` identity `==` | value `==`, plus `copyWith` | `retyped` | Two actions with the same fields are now equal, and so are attachments that differ only in holding separate copies of them | +| `Reaction.fromJson` / `toJson` | — | `removed` | A plain class; construct it directly. Messages, events and `sendReaction` still read and write reactions | +| `Reaction extends Equatable`, `Reaction.props` | `Reaction` (value `==`) | `removed` | Equality and `merge` are unchanged; `props` is gone and `Reaction` is no longer an `Equatable` | +| `Location.fromJson` / `toJson` | — | `removed` | A plain class; construct it directly. Messages, channel states and the live location calls still read and write locations | +| `Location extends Equatable`, `Location.props` | `Location` (value `==`) | `removed` | Equality is unchanged; `props` is gone and `Location` is no longer an `Equatable` | | `StreamChatClient.updateUser` / `updateUsers` → `Future` | `Future>` | `retyped` | Returns a `Result` instead of throwing | | `StreamChatClient.partialUpdateUser` | `StreamChatClient.updateUserPartial` | `renamed` | Same arguments | | `StreamChatClient.partialUpdateUser` → `Future` | `updateUserPartial` → `Future>` | `retyped` | Returns a `Result` instead of throwing | @@ -334,6 +350,24 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed` | `OwnUser.fromUser(user)` reading devices, mutes, unread counts, push preferences and privacy settings out of `user.extraData` | — | `removed` | A `User` no longer carries them; they are left at their defaults | | `User.custom` entry named like one of the user'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 | | `MessageType.rawType` / `AttachmentType.rawType` / `PushLevel.rawType` / `ChatPreferenceLevel.rawType` / `ChatLevel.rawType` / `CallLevel.rawType` / `RoleType.rawType`, `ModerationAction.action`, `ChannelCapability.capability` | the value itself | `removed` | Each is a `String`, so use it directly: `message.type.rawType` becomes `message.type` | +| `StreamChatClient.createReminder` / `.updateReminder` / `.queryReminders` → `Future` and the like | `Future>` and the like | `retyped` | Return a `Result` instead of throwing; the same holds for `Channel.createReminder` and `.updateReminder` | +| `StreamChatClient.deleteReminder` / `Channel.deleteReminder` → `Future` | `Future>` | `retyped` | A new response carrying `duration` | +| `queryReminders(pagination: PaginationParams(limit: l, next: n))` | `queryReminders(limit: l, next: n)` | `retyped` | `limit` is still 10 when omitted. Pass a response's `prev` as `prev` to page backwards. The other `PaginationParams` fields never had an effect on this query | +| `CreateReminderResponse()..reminder = …`, `UpdateReminderResponse()..reminder = …`, `QueryRemindersResponse()..reminders = …` and their other setters | `CreateReminderResponse(duration: …, reminder: …)` and the like | `retyped` | The responses are plain classes with a const constructor and final fields | +| `CreateReminderResponse.fromJson`, `UpdateReminderResponse.fromJson`, `QueryRemindersResponse.fromJson` | — | `removed` | Construct the responses directly | +| reminder responses' `duration` (`String?`) | `String` | `retyped` | Always present; drop any `!` or `?? ''` | +| reminder responses' identity `==` | value `==`, plus `copyWith` | `retyped` | Two responses with the same fields are now equal | +| `MessageReminderResponse` | — | `removed` | The shared base class of the create and update responses | +| `MessageReminder.fromJson` / `toJson` | — | `removed` | A plain class; construct it directly. Messages and events still read reminders | +| `MessageReminder extends Equatable`, `MessageReminder.props` | `MessageReminder` (value `==`) | `removed` | Equality and `merge` are unchanged; `props` is gone and `MessageReminder` is no longer an `Equatable` | +| `Location.copyWith` / `MessageReminder.copyWith` / `Reaction.copyWith` / `ReactionGroup.copyWith` keeping a field passed as `null` | freezed's `copyWith` | `retyped` | `null` clears a nullable field and resets `createdAt`, `updatedAt`, `firstReactionAt` and `lastReactionAt` to now; `MessageReminder.remindAt` already cleared | +| `StreamChatApi.reminders` (`RemindersApi`) | `StreamChatClient` reminder methods | `removed` | The endpoints moved to the generated client | +| `Message.extraData` / `Attachment.extraData` / `Reaction.extraData` / `DraftMessage.extraData` | `Message.custom` / `Attachment.custom` / `Reaction.custom` / `DraftMessage.custom` | `renamed` | The wire's name for it; also the constructor and `copyWith` arguments | +| `Channel.sendStaticLocation(extraData: …)` / `startLiveLocationSharing(extraData: …)` | `sendStaticLocation(custom: …)` / `startLiveLocationSharing(custom: …)` | `renamed` | The message's custom data | +| `message.extraData['cid']` / `['html']` / `['mml']` / `['deleted_reply_count']` | `Message.cid` / `html` / `mml` / `deletedReplyCount` | `retyped` | Fields now, not custom data; `toJson` does not write them, so a message can no longer be sent with markup | +| `attachment.extraData['giphy']` | `Attachment.giphy` | `retyped` | A field now, not custom data | +| `Message.custom` / `Attachment.custom` / `Reaction.custom` / `DraftMessage.custom` entry named like one of the model's own fields | the same entry, as custom data | `retyped` | Kept as the API returns it; left out when the model is sent, so it cannot overwrite the field | +| `Reaction.topLevelFields` | — | `removed` | It listed the JSON keys of a model that no longer has JSON | | _(more added per feature as PRs land)_ | | | | --- @@ -1094,6 +1128,40 @@ no longer carries them. > **Why:** the API calls it `custom`, and every migrated model takes that name. A name that answered the id could > not tell an unnamed user from a named one, so initials and sorts read the id as a name. +### Message Custom Data + +**`extraData` is renamed `custom` on `Message`, `Attachment`, `Reaction` and `DraftMessage`**, along with the +constructor and `copyWith` arguments and the `extraData` argument of `Channel.sendStaticLocation` and +`startLiveLocationSharing`. + +```dart +// v10 +final priority = message.extraData['priority']; +final duration = attachment.extraData['duration']; + +// v11 +final priority = message.custom['priority']; +final duration = attachment.custom['duration']; +``` + +**A message's channel id, HTML, markup and deleted reply count are fields**, `Message.cid`, `html`, `mml` and +`deletedReplyCount`, and so are an attachment's Giphy renditions, `Attachment.giphy`. `custom` no longer holds +their keys, and an entry with one of those names in `custom` no longer sets the field. `Message.toJson` does not +write any of them, so a message can no longer be sent with markup. + +```dart +// v10 +final html = message.extraData['html'] as String?; + +// v11 +final html = message.html; +``` + +**An entry of `custom` named like one of the model's own fields stays in `custom`** and leaves the field alone; it +is left out when the model is sent, so it cannot overwrite the field. A reaction on a reminder's message keeps its emoji code in its `custom` as well as in `Reaction.emojiCode`. + +> **Why:** the API calls it `custom`, and every migrated model takes that name. + ### Moderation **Muting, banning and flagging return a `Result` instead of throwing**, on both `StreamChatClient` @@ -1329,6 +1397,54 @@ poll.copyWith(name: 'Dinner?'); **`StreamChatApi.polls` is removed.** Call the poll methods on `StreamChatClient` or `Channel` instead. +### Message Reminders + +**Every reminder call returns a `Result` instead of throwing,** on `StreamChatClient` and on `Channel`. Calling a +`Channel` method on a channel that is not initialized still throws a `StateError`, as in v10. + +```dart +// v10 +try { + final response = await client.createReminder(messageId, remindAt: remindAt); + showReminder(response.reminder); +} on StreamChatException catch (e) { + report(e); +} + +// v11 +final result = await client.createReminder(messageId, remindAt: remindAt); +result.fold( + onSuccess: (response) => showReminder(response.reminder), + onFailure: (error, _) => report(error), +); +``` + +**`deleteReminder` answers a `DeleteReminderResponse`** where v10 answered an `EmptyResponse`. It carries only +`duration` today. + +**`queryReminders` takes `limit`, `next` and `prev`** instead of a `PaginationParams`. `limit` is still 10 when +omitted. The offset and id-based fields of `PaginationParams` never had an effect on this query. The response now +carries a `prev` cursor next to `next`; pass it as `prev` to fetch the page before. + +```dart +// v10 +await client.queryReminders(pagination: PaginationParams(limit: 10, next: cursor)); + +// v11 +await client.queryReminders(limit: 10, next: cursor); +``` + +**`MessageReminder` and the reminder responses no longer decode JSON.** Build them with their constructors — +`CreateReminderResponse(duration: '0ms', reminder: reminder)` where v10 wrote +`CreateReminderResponse()..reminder = reminder`. `Message.reminder` and the reminder events still decode from the +same keys. `MessageReminder` is no longer an `Equatable`: equality and `merge` are unchanged, and `props` is +gone. Its `copyWith` is freezed's: `null` clears a nullable field and resets a date to now. + +**`StreamChatApi.reminders` is removed.** Call the reminder methods on `StreamChatClient` or `Channel` instead. + +> **Why:** a failure is a value the caller handles where it happens, and the public models stop being wire shapes, +> so the API payload can change without changing the type a caller holds. + ### File Upload **Uploads and deletes return a `Result` instead of throwing.** `sendImage`, `sendFile`, `uploadImage` and @@ -1369,6 +1485,34 @@ StreamChatClient(apiKey, attachmentFileUploaderProvider: (httpClient) => MyUploa StreamChatClient(apiKey, attachmentFileUploaderProvider: (dio) => MyUploader(dio)); ``` +### Messages + +**`Moderation` and `ReactionGroup` are plain classes.** Neither decodes from or encodes to JSON any more, and +neither is an `Equatable`: equality is unchanged and `props` is gone. `Moderation` gains `copyWith`. A message +still carries them in `Message.moderation` and `Message.reactionGroups`, read from the API as before. +`ReactionGroup.fromData` and `toData` read and write the offline database's format; they are not a codec for API +JSON. + +**`Action` is a plain class too, and compares by value.** It no longer decodes from or encodes to JSON, and it gains +`copyWith`. Two actions with the same fields are now equal, where v10 compared them by identity; an `Attachment` +holding them follows suit. + +**`Reaction` and `Location` are plain classes.** Neither decodes from or encodes to JSON any more, and neither is an +`Equatable`; equality is unchanged, as is `Reaction.merge`. Messages, events, channel states, `sendReaction` and +the live location calls still carry them as before. `copyWith` on `Reaction`, `Location` and `ReactionGroup` is +freezed's: `null` clears a nullable field and resets a date to now. + +```dart +// v10 +final moderation = Moderation.fromJson(json); + +// v11 +const moderation = Moderation(action: ModerationAction.flag, originalText: 'original text'); +``` + +> **Why:** the public models stop being wire shapes, so the API payload can change without changing the type a +> caller holds. + --- ## Migration Checklist diff --git a/openapi-migration/06-reminders.md b/openapi-migration/06-reminders.md index 91be7b453e..3bc82ff111 100644 --- a/openapi-migration/06-reminders.md +++ b/openapi-migration/06-reminders.md @@ -2,7 +2,7 @@ **Goal:** Small and self-contained, and it exercises the `PATCH` shape our hand-written layer expresses differently. -**Size:** 4 hand-written method(s) across 1 file(s) → 4 generated operation(s). +**Size:** 0 hand-written method(s) across 0 file(s) → 4 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. @@ -13,10 +13,6 @@ | File | Method | Returns | | --- | --- | --- | -| `reminders_api.dart` | `queryReminders` | `QueryRemindersResponse` | -| `reminders_api.dart` | `createReminder` | `CreateReminderResponse` | -| `reminders_api.dart` | `updateReminder` | `UpdateReminderResponse` | -| `reminders_api.dart` | `deleteReminder` | `EmptyResponse` | ### Generated operations that cover it @@ -27,9 +23,35 @@ | `POST` | `/api/v2/chat/reminders/query` | `queryReminders` | `QueryRemindersResponse` | | `PATCH` | `/api/v2/chat/messages/{message_id}/reminders` | `updateReminder` | `UpdateReminderResponse` | -## Decisions to make +## Decisions taken -- `MessageReminder` is public and persisted; decide keep-vs-adopt with persistence in the same PR. +- **`MessageReminder` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; + equality is unchanged. Its constructor stays non-const (dates default to now). It takes freezed's + `copyWith` (audited: only `merge` needed `?? channel`, `?? message` and `?? user` to keep what an + event leaves out) and keeps v10's `merge`. `expiresAt` stays out; adding it + is additive. It is not persisted after all: `Message.reminder` is never written to JSON and + `stream_chat_persistence` has no reminder column, so no codec is needed. +- **v1 JSON keeps decoding through `MessageReminderV1JsonConverter`** on `Message.reminder` and + `Event.reminder`. `Event` writes the field too, so the converter runs both ways and writes the keys + v10 did (ids and dates, without the channel, message or user). +- **The four methods return a `Result`,** on `StreamChatClient` and `Channel`. `deleteReminder` + answers the named `DeleteReminderResponse`, so it returns a new `DeleteReminderResponse` envelope + rather than `Result`. `CreateReminderResponse`, `UpdateReminderResponse` and + `QueryRemindersResponse` keep their names; `reminder` is non-null, as the spec requires it. Their + shared base class `MessageReminderResponse` is removed (approved). +- **`queryReminders` takes `limit`, `next` and `prev`** instead of `PaginationParams` (approved). The + backend reads only those (default limit 10, at most 100, both cursors together rejected), so + the other `PaginationParams` fields were never honoured. `limit` defaults to 10, and the response + gains `prev`. +- **A null `remindAt` on `updateReminder` clears the due date,** as in v10: the backend treats an + absent and an explicit null `remind_at` alike, and the update is not a partial patch. +- **The v1 and v2 routes reach the same controllers** (`lib/chat/routes.go` mounts `coreRoutes` under + both), with no feature flag or beta gate; create still checks the channel's + `user_message_reminders` setting. +- **The reminder's message maps through the message mapper** of [10](10-messages.md), its first + consumer. Create and update answer without the channel; a query includes it. +- **Verified live** against the demo app: create, bookmark, update (set and clear), query with + `next`/`prev`, both cursors rejected, delete and a second delete (404). ## Risks @@ -37,19 +59,19 @@ ## Definition of done -- [ ] Every method above either routes through `DefaultApi` or is listed here as deliberately left +- [x] Every method above either routes through `DefaultApi` or is listed here as deliberately left hand-written, with the reason. -- [ ] Public methods return `Future>`; no `getOrThrow()` inside the SDK. -- [ ] Hand-written request/response DTOs for this group are deleted, or their retention is justified. -- [ ] `copywith_audit.py` was run for every model that loses a v10 `copyWith`, every site it reports is +- [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)). -- [ ] `melos run analyze` clean, `melos run test:dart` green, persistence tests green if this group +- [x] `melos run analyze` clean, `melos run test:dart` green, persistence tests green if this group persists anything. -- [ ] `migrations/v11-migration.md`: Symbol Map rows plus a feature section for every break. -- [ ] CHANGELOG entry under `🛑️ Breaking` for each break; PR title `refactor(llc)!:`. -- [ ] Decisions recorded in this file, and the status box ticked in `README.md`. -- [ ] Public dartdoc follows [`STYLE_GUIDE.md` § Documentation](../STYLE_GUIDE.md#documentation) +- [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. -- [ ] Tests follow [`TESTING.md`](../TESTING.md): no `group` organizing a file by method, each +- [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/07-threads-and-drafts.md b/openapi-migration/07-threads-and-drafts.md index eb757b9e15..0819f46a5c 100644 --- a/openapi-migration/07-threads-and-drafts.md +++ b/openapi-migration/07-threads-and-drafts.md @@ -39,7 +39,7 @@ ## Risks -- `DraftMessage.poll` is never decoded from JSON: `poll` is not in `DraftMessage.topLevelFields`, so the key would land in `extraData`, and the draft payload carries `poll_id` only. Its `PollV1JsonConverter` exists so the generated code compiles. Decide here whether the field stays constructor-only or goes. +- `DraftMessage.poll` is never decoded from JSON: `poll` is not in `DraftMessage.topLevelFields`, so the key would land in `custom`, and the draft payload carries `poll_id` only. Its `PollV1JsonConverter` exists so the generated code compiles. Decide here whether the field stays constructor-only or goes. - The four draft methods live in `message_api.dart`, not `threads_api.dart` — this group reaches into that file, and group 10 must leave those four alone. - `Draft` and `DraftMessage` are public, persisted, and read by `StreamDraftListController` in `stream_chat_flutter_core`. diff --git a/openapi-migration/10-messages.md b/openapi-migration/10-messages.md index cda3684b02..634b42024a 100644 --- a/openapi-migration/10-messages.md +++ b/openapi-migration/10-messages.md @@ -48,15 +48,89 @@ ## Decisions to make - `Message` is public, persisted, WebSocket-delivered and the most customised type in the SDK. Keep ours; treat the generated `MessageResponse` as a mapping source only. -- `Attachment`: the generated model defines fields our `extraData` currently absorbs. Decide the promotion rules before writing the mapper. -- Replace the temporary `@DataSerializable` storage codec (`UserGroup`, `UserGroupMember`): decide between dedicated tables and codecs owned by `stream_chat_persistence` before `Message` and `Attachment` become plain models, then delete the typedef and every `fromData`/`toData` it generates. -- `Action` is still the v10 json_serializable class, embedded in `Attachment.actions`, which `Attachment.toJson` writes when sending and `toData` writes to persistence. It becomes a plain model with `Attachment`; group 04 left it alone. +- Replace the temporary `@DataSerializable` storage codec (`UserGroup`, `UserGroupMember`, `ReactionGroup`): decide between dedicated tables and codecs owned by `stream_chat_persistence` before `Message` and `Attachment` become plain models, then delete the typedef and every `fromData`/`toData` it generates. - Expose `MarkReadResponse.event`. [17](17-read-receipts.md) dropped it because its thread carries a `MessageResponse`; the mapper marks it with a TODO. +## Decisions taken + +The models `Message` embeds become plain ahead of the endpoints, one PR each, leaves first. None routes +an endpoint, so the definition of done below stays open. + +- **`Moderation` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; equality + is unchanged. `Message.moderation` decodes through `moderationFromV1Json`, a temporary decode-only + function in `v1_json_converters.dart`: `Message.toJson` never writes the field. It keeps the + `moderation_details` fallback and the legacy `MESSAGE_RESPONSE_ACTION_*` names, and reads a missing + `platform_circumvented` as `false`. `stream_chat_persistence` does not store moderation, so no codec + is needed. `ModerationAction` keeps its `fromJson`/`toJson` statics, as `MessageType` does, until + `Message` stops decoding v1 JSON. `ModerationV2Response` maps beside the message, in + `messages_mapper.dart`. +- **`ReactionGroup` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; + equality is unchanged. Its constructor stays non-const, defaulting both dates to now, and it takes + freezed's `copyWith` (audited: no site passes a `null` date). `Message.reactionGroups` decodes through + `reactionGroupsFromV1Json`, + decode-only; `reactionGroupsFromCounts` builds the groups from `reaction_counts` and + `reaction_scores` when `reaction_groups` is missing. Dates go through `StreamDateTimeConverter`. + `messages.reaction_groups` and `pinned_messages.reaction_groups` store the groups through the + temporary `@DataSerializable` codec, whose output is byte-identical to v10's `toJson`, so no + `schemaVersion` bump. +- **`Action` is a plain `@freezed` model,** ahead of `Attachment` rather than with it. It loses + `fromJson` and `toJson`, gains `copyWith` and `const`, and compares by value where v10 compared by + identity, so attachments holding equal actions now compare equal. `Attachment.actions` reads and + writes through `ActionV1JsonConverter`: `Attachment.toJson` sends the actions and `toData` stores + them, and the converter writes the same keys v10 did, `value` included when null, so requests and + the stored `attachments` columns are unchanged. `Action` needs no codec of its own. +- **`Reaction` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; equality + is unchanged. Its constructor stays non-const (`userId` from `user`, dates default to now) and it + takes freezed's `copyWith` (audited: `merge` keeps a `null` message id, user, user id and emoji code + with `??`, and `sendReaction` keeps the reaction's user when there is no current user). + `Reaction.topLevelFields` is removed; the converter owns the key list. `ReactionV1JsonConverter` reads + and writes a reaction everywhere v1 JSON carries one: the message's latest and own reactions, + `Event.reaction`, the two hand-written reaction responses, and the `sendReaction` body, which keeps + v10's request shape (type, score, emoji code, custom data at the root). `stream_chat_persistence` + stores reactions as table rows, so no codec is needed. +- **`Location` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; equality + is unchanged. Its constructor stays non-const (`endAt` normalised to UTC, dates default to now) and + it takes freezed's `copyWith` (audited: no site changes). Its `channel` and `message` + stay json_serializable `ChannelModel` and `Message`, which `LocationV1JsonConverter` decodes through + their own `fromJson`; the API nests them only in active live locations, one level deep, so the + message-location cycle ends there. The converter reads and writes `Message.sharedLocation` (the write + keeps v10's request shape: coordinates, device and end date), `ChannelState.activeLiveLocations`, + `GetActiveLiveLocationsResponse` and `updateLiveLocation`'s response. `stream_chat_persistence` stores + locations as table rows, so no codec is needed. +- **`messages_mapper.dart` maps `MessageResponse` onto today's json_serializable `Message`** through + its constructor, with the generated attachment, reaction, + reaction group, moderation, shared location, reminder, draft and draft payload types, each mapped in + its own file (`attachments_mapper.dart`, `reactions_mapper.dart`, `locations_mapper.dart`, + `drafts_mapper.dart`, `reminders_mapper.dart`), the moderation beside the message itself. Its first + consumer is [06](06-reminders.md). `Message.fromJson` and the mapper share the type and state + derivation and the reaction groups built from counts and scores when a payload has none + (`lib/src/core/util/message_decoding.dart`, internal). +- **`Message`, `Attachment`, `Reaction` and `DraftMessage` rename `extraData` to `custom`** (README + rule 9), since these mappers are the first to map them. The response mappers copy the generated + `custom` as is; the v1 decoders keep splitting flat JSON by `topLevelFields` (the reaction converter + owns its key list, as `Reaction` has no JSON), and the v1 writers leave out a custom entry named like + a key. Persistence renames the `extra_data` columns `custom`. +- **The values v10 parked in `extraData` that the spec types are fields:** `Message.cid`, `html`, `mml` + and `deletedReplyCount`, with persistence columns (`cid` reads the message's channel), and + `Attachment.giphy`, stored through the attachment's `toData`. `Message.toJson` writes none of the + message's; `Attachment.toJson` writes `giphy`, so an edited Giphy message keeps its renditions. +- **What the SDK has no feature for stays out:** `image_labels` and `mentioned_channel_members` on a + message, and `html` and `mml` on a draft message, which iOS and Android drop too. The mappers leave + them out; the v1 decoders split flat JSON by `topLevelFields` alone, so they keep them in `custom`, + as v10 did. +- **A reaction's emoji code arrives in `custom`,** which keeps it, and also maps to `Reaction.emojiCode`. +- **Moderation actions go through `ModerationAction.fromJson`,** so legacy names read as current ones. +- **Attachments map at parity with `Attachment.fromJson`:** each gets a new local `id`, and the id the + attachment was sent with stays in `custom`; `giphy` and `fields` are written in their v1 JSON shape, + which `GiphyInfo` and the UI read. Making `Attachment` plain, and typing the renditions and + `fields`, is left to this group. +- **A reminder or location nests its message one level deep,** so the recursion ends there. +- **`MessageWithChannelResponse` gets its mapper with its first consumer** (getMessage, search), not + ahead of it. + ## Risks - `message_api.dart` also holds the four draft methods, which belong to group 07 — leave them alone here. -- Attachment `custom`/`extraData` promotion is the known hard part of the whole migration. - Message send has offline and retry paths through `stream_chat_persistence` that must keep working. - `MessageDeleteScope` has to be reconciled with `DeleteType`, which [08](08-moderation-and-blocklists.md) added. It is named for the scope of a delete — `deleteForMe` vs `deleteForAll` — but carries a `hard` bool, which is the same axis `DeleteType` models, in the same words, minus `pruning`. `deleteMessage(hard: true)` therefore cannot express a pruning delete at all, and `softDeleteForAll` / `hardDeleteForAll` read as two spellings of `DeleteType.soft` / `DeleteType.hard`. Decide whether the scope keeps a `DeleteType` field or the two stay separate arguments; either way the public type changes, so it belongs in this group rather than a later fix. diff --git a/openapi-migration/README.md b/openapi-migration/README.md index 8f0462c1fe..644a3437e4 100644 --- a/openapi-migration/README.md +++ b/openapi-migration/README.md @@ -17,7 +17,7 @@ generated operations in scope, the decisions that group has to make, its risks, | [03](03-user-groups.md) | User Groups | 0 | 8 | ☑ | | [04](04-roles-guest-and-app.md) | Roles, Guest & App Settings | 0 | 5 | ☑ | | [05](05-polls.md) | Polls | 0 | 13 | ☑ | -| [06](06-reminders.md) | Message Reminders | 4 | 4 | ☐ | +| [06](06-reminders.md) | Message Reminders | 0 | 4 | ☑ | | [07](07-threads-and-drafts.md) | Threads & Drafts | 7 | 7 | ☐ | | [08](08-moderation-and-blocklists.md) | Moderation & Blocklists | 0 | 34 | ☑ | | [09](09-users.md) | Users | 3 | 3 | ☐ | @@ -34,7 +34,7 @@ generated operations in scope, the decisions that group has to make, its risks, | [20](20-user-updates.md) | User Updates — split out of 09 | 0 | 2 | ☑ | | [21](21-custom-data.md) | Custom Data Check — no model still says `extraData` | — | — | ☐ | -**Coverage:** 51 hand-written methods across 9 files, and all 129 generated operations, each claimed by exactly +**Coverage:** 47 hand-written methods across 8 files, and all 129 generated operations, each claimed by exactly one group. Verified mechanically — see [Keeping this plan honest](#keeping-this-plan-honest). @@ -162,11 +162,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. Group 15 promoted `ChannelModel`'s and `Member`'s, and group 20 - `User`'s and `OwnUser`'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. + the owning group's break, or stays out. Group 15 promoted `ChannelModel`'s and `Member`'s, group 20 + `User`'s and `OwnUser`'s, and group 06 `Message`'s, `Attachment`'s, `Reaction`'s and `DraftMessage`'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 @@ -174,12 +174,19 @@ includes a named response that carries only `duration` today, such as `HideChann | --- | --- | --- | | `DeviceV1JsonConverter` | `OwnUser.devices` | [09](09-users.md) | | `userGroupsFromV1Json` | `Message.mentionedGroups` | [10](10-messages.md) | -| `DataSerializable` | `UserGroup`, `UserGroupMember`, `PollOption` (`fromData`, `toData`) | [10](10-messages.md) | +| `moderationFromV1Json` | `Message.moderation` | [10](10-messages.md) | +| `reactionGroupsFromV1Json` | `Message.reactionGroups` | [10](10-messages.md) | +| `ActionV1JsonConverter` | `Attachment.actions` | [10](10-messages.md) | +| `LocationV1JsonConverter` | `Message.sharedLocation`, `ChannelState.activeLiveLocations`, `GetActiveLiveLocationsResponse.activeLiveLocations`, `updateLiveLocation`'s response | [10](10-messages.md) | +| `MessageReminderV1JsonConverter` | `Message.reminder`, `Event.reminder` | WebSocket v2 (no group) | +| `ReactionV1JsonConverter` | `Message.latestReactions` / `ownReactions`, `Event.reaction`, `QueryReactionsResponse.reactions`, `SendReactionResponse.reaction`, the `sendReaction` body | WebSocket v2 (no group) | +| `DataSerializable` | `UserGroup`, `UserGroupMember`, `ReactionGroup`, `PollOption` (`fromData`, `toData`) | [10](10-messages.md) | | `PollV1JsonConverter` | `Message.poll`, `DraftMessage.poll`, `Event.poll` | WebSocket v2 (no group) | | `PollVoteV1JsonConverter` | `Event.pollVote` | WebSocket v2 (no group) | | `Event.fromJson`'s `user.updated` re-decode | `Event.user`, decoded as a `User` and again as an `OwnUser` | WebSocket v2 (no group) | | `users_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) | +| `messages_mapper.dart`, `attachments_mapper.dart`, `reactions_mapper.dart`, `locations_mapper.dart`, `drafts_mapper.dart` (kept, re-pointed) | today's `Message`, `Attachment`, `Reaction`, `Location`, `Draft` and `DraftMessage`, which still read and write JSON | [10](10-messages.md) | The `user.updated` re-decode keeps the privacy settings the current user's copy of the event carries, which `User` does not model, so the handler can apply them as v10 did. When the WebSocket moves to v2, map the generated diff --git a/openapi-migration/tool/generate_plan.py b/openapi-migration/tool/generate_plan.py index b02192937d..7dae65ce9c 100644 --- a/openapi-migration/tool/generate_plan.py +++ b/openapi-migration/tool/generate_plan.py @@ -414,13 +414,41 @@ def match(verb, path): ), dict( num='06', slug='reminders', title='Message Reminders', - hand=['reminders_api.dart'], + hand=[], match=owns('/api/v2/chat/messages/{message_id}/reminders', '/api/v2/chat/reminders'), goal='Small and self-contained, and it exercises the `PATCH` shape our hand-written layer expresses ' 'differently.', - decisions=[ - '`MessageReminder` is public and persisted; decide keep-vs-adopt with persistence in the same PR.', - ], + decisions=[], + taken=textwrap.dedent("""\ + - **`MessageReminder` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; + equality is unchanged. Its constructor stays non-const (dates default to now). It takes freezed's + `copyWith` (audited: only `merge` needed `?? channel`, `?? message` and `?? user` to keep what an + event leaves out) and keeps v10's `merge`. `expiresAt` stays out; adding it + is additive. It is not persisted after all: `Message.reminder` is never written to JSON and + `stream_chat_persistence` has no reminder column, so no codec is needed. + - **v1 JSON keeps decoding through `MessageReminderV1JsonConverter`** on `Message.reminder` and + `Event.reminder`. `Event` writes the field too, so the converter runs both ways and writes the keys + v10 did (ids and dates, without the channel, message or user). + - **The four methods return a `Result`,** on `StreamChatClient` and `Channel`. `deleteReminder` + answers the named `DeleteReminderResponse`, so it returns a new `DeleteReminderResponse` envelope + rather than `Result`. `CreateReminderResponse`, `UpdateReminderResponse` and + `QueryRemindersResponse` keep their names; `reminder` is non-null, as the spec requires it. Their + shared base class `MessageReminderResponse` is removed (approved). + - **`queryReminders` takes `limit`, `next` and `prev`** instead of `PaginationParams` (approved). The + backend reads only those (default limit 10, at most 100, both cursors together rejected), so + the other `PaginationParams` fields were never honoured. `limit` defaults to 10, and the response + gains `prev`. + - **A null `remindAt` on `updateReminder` clears the due date,** as in v10: the backend treats an + absent and an explicit null `remind_at` alike, and the update is not a partial patch. + - **The v1 and v2 routes reach the same controllers** (`lib/chat/routes.go` mounts `coreRoutes` under + both), with no feature flag or beta gate; create still checks the channel's + `user_message_reminders` setting. + - **The reminder's message maps through the message mapper** of [10](10-messages.md), its first + consumer. Create and update answer without the channel; a query includes it. + - **Verified live** against the demo app: create, bookmark, update (set and clear), query with + `next`/`prev`, both cursors rejected, delete and a second delete (404). + """), + done=DONE.replace('- [ ]', '- [x]'), risks=['Reminder events also arrive over the WebSocket.'], ), dict( @@ -437,7 +465,7 @@ def match(verb, path): ], risks=[ '`DraftMessage.poll` is never decoded from JSON: `poll` is not in `DraftMessage.topLevelFields`, so the key ' - 'would land in `extraData`, and the draft payload carries `poll_id` only. Its `PollV1JsonConverter` exists so ' + 'would land in `custom`, and the draft payload carries `poll_id` only. Its `PollV1JsonConverter` exists so ' 'the generated code compiles. Decide here whether the field stays constructor-only or goes.', 'The four draft methods live in `message_api.dart`, not `threads_api.dart` — this group reaches into ' 'that file, and group 10 must leave those four alone.', @@ -638,24 +666,94 @@ def match(verb, path): decisions=[ '`Message` is public, persisted, WebSocket-delivered and the most customised type in the SDK. Keep ' 'ours; treat the generated `MessageResponse` as a mapping source only.', - '`Attachment`: the generated model defines fields our `extraData` currently absorbs. Decide the ' - 'promotion rules before writing the mapper.', - 'Replace the temporary `@DataSerializable` storage codec (`UserGroup`, `UserGroupMember`): decide ' + 'Replace the temporary `@DataSerializable` storage codec (`UserGroup`, `UserGroupMember`, `ReactionGroup`): decide ' 'between dedicated tables and codecs owned by `stream_chat_persistence` before `Message` and ' '`Attachment` become plain models, then delete the typedef and every `fromData`/`toData` it generates.', - '`Action` is still the v10 json_serializable class, embedded in `Attachment.actions`, which ' - '`Attachment.toJson` writes when sending and `toData` writes to persistence. It becomes a plain model ' - 'with `Attachment`; group 04 left it alone.', 'Expose `MarkReadResponse.event`. [17](17-read-receipts.md) dropped it because its thread carries a ' '`MessageResponse`; the mapper marks it with a TODO.', ], risks=[ '`message_api.dart` also holds the four draft methods, which belong to group 07 — leave them alone ' 'here.', - 'Attachment `custom`/`extraData` promotion is the known hard part of the whole migration.', 'Message send has offline and retry paths through `stream_chat_persistence` that must keep working.', '`MessageDeleteScope` has to be reconciled with `DeleteType`, which [08](08-moderation-and-blocklists.md) added. It is named for the scope of a delete — `deleteForMe` vs `deleteForAll` — but carries a `hard` bool, which is the same axis `DeleteType` models, in the same words, minus `pruning`. `deleteMessage(hard: true)` therefore cannot express a pruning delete at all, and `softDeleteForAll` / `hardDeleteForAll` read as two spellings of `DeleteType.soft` / `DeleteType.hard`. Decide whether the scope keeps a `DeleteType` field or the two stay separate arguments; either way the public type changes, so it belongs in this group rather than a later fix.', ], + taken=textwrap.dedent("""\ + The models `Message` embeds become plain ahead of the endpoints, one PR each, leaves first. None routes + an endpoint, so the definition of done below stays open. + + - **`Moderation` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; equality + is unchanged. `Message.moderation` decodes through `moderationFromV1Json`, a temporary decode-only + function in `v1_json_converters.dart`: `Message.toJson` never writes the field. It keeps the + `moderation_details` fallback and the legacy `MESSAGE_RESPONSE_ACTION_*` names, and reads a missing + `platform_circumvented` as `false`. `stream_chat_persistence` does not store moderation, so no codec + is needed. `ModerationAction` keeps its `fromJson`/`toJson` statics, as `MessageType` does, until + `Message` stops decoding v1 JSON. `ModerationV2Response` maps beside the message, in + `messages_mapper.dart`. + - **`ReactionGroup` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; + equality is unchanged. Its constructor stays non-const, defaulting both dates to now, and it takes + freezed's `copyWith` (audited: no site passes a `null` date). `Message.reactionGroups` decodes through + `reactionGroupsFromV1Json`, + decode-only; `reactionGroupsFromCounts` builds the groups from `reaction_counts` and + `reaction_scores` when `reaction_groups` is missing. Dates go through `StreamDateTimeConverter`. + `messages.reaction_groups` and `pinned_messages.reaction_groups` store the groups through the + temporary `@DataSerializable` codec, whose output is byte-identical to v10's `toJson`, so no + `schemaVersion` bump. + - **`Action` is a plain `@freezed` model,** ahead of `Attachment` rather than with it. It loses + `fromJson` and `toJson`, gains `copyWith` and `const`, and compares by value where v10 compared by + identity, so attachments holding equal actions now compare equal. `Attachment.actions` reads and + writes through `ActionV1JsonConverter`: `Attachment.toJson` sends the actions and `toData` stores + them, and the converter writes the same keys v10 did, `value` included when null, so requests and + the stored `attachments` columns are unchanged. `Action` needs no codec of its own. + - **`Reaction` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; equality + is unchanged. Its constructor stays non-const (`userId` from `user`, dates default to now) and it + takes freezed's `copyWith` (audited: `merge` keeps a `null` message id, user, user id and emoji code + with `??`, and `sendReaction` keeps the reaction's user when there is no current user). + `Reaction.topLevelFields` is removed; the converter owns the key list. `ReactionV1JsonConverter` reads + and writes a reaction everywhere v1 JSON carries one: the message's latest and own reactions, + `Event.reaction`, the two hand-written reaction responses, and the `sendReaction` body, which keeps + v10's request shape (type, score, emoji code, custom data at the root). `stream_chat_persistence` + stores reactions as table rows, so no codec is needed. + - **`Location` is a plain `@freezed` model.** It loses `fromJson`, `toJson` and `Equatable`; equality + is unchanged. Its constructor stays non-const (`endAt` normalised to UTC, dates default to now) and + it takes freezed's `copyWith` (audited: no site changes). Its `channel` and `message` + stay json_serializable `ChannelModel` and `Message`, which `LocationV1JsonConverter` decodes through + their own `fromJson`; the API nests them only in active live locations, one level deep, so the + message-location cycle ends there. The converter reads and writes `Message.sharedLocation` (the write + keeps v10's request shape: coordinates, device and end date), `ChannelState.activeLiveLocations`, + `GetActiveLiveLocationsResponse` and `updateLiveLocation`'s response. `stream_chat_persistence` stores + locations as table rows, so no codec is needed. + - **`messages_mapper.dart` maps `MessageResponse` onto today's json_serializable `Message`** through + its constructor, with the generated attachment, reaction, + reaction group, moderation, shared location, reminder, draft and draft payload types, each mapped in + its own file (`attachments_mapper.dart`, `reactions_mapper.dart`, `locations_mapper.dart`, + `drafts_mapper.dart`, `reminders_mapper.dart`), the moderation beside the message itself. Its first + consumer is [06](06-reminders.md). `Message.fromJson` and the mapper share the type and state + derivation and the reaction groups built from counts and scores when a payload has none + (`lib/src/core/util/message_decoding.dart`, internal). + - **`Message`, `Attachment`, `Reaction` and `DraftMessage` rename `extraData` to `custom`** (README + rule 9), since these mappers are the first to map them. The response mappers copy the generated + `custom` as is; the v1 decoders keep splitting flat JSON by `topLevelFields` (the reaction converter + owns its key list, as `Reaction` has no JSON), and the v1 writers leave out a custom entry named like + a key. Persistence renames the `extra_data` columns `custom`. + - **The values v10 parked in `extraData` that the spec types are fields:** `Message.cid`, `html`, `mml` + and `deletedReplyCount`, with persistence columns (`cid` reads the message's channel), and + `Attachment.giphy`, stored through the attachment's `toData`. `Message.toJson` writes none of the + message's; `Attachment.toJson` writes `giphy`, so an edited Giphy message keeps its renditions. + - **What the SDK has no feature for stays out:** `image_labels` and `mentioned_channel_members` on a + message, and `html` and `mml` on a draft message, which iOS and Android drop too. The mappers leave + them out; the v1 decoders split flat JSON by `topLevelFields` alone, so they keep them in `custom`, + as v10 did. + - **A reaction's emoji code arrives in `custom`,** which keeps it, and also maps to `Reaction.emojiCode`. + - **Moderation actions go through `ModerationAction.fromJson`,** so legacy names read as current ones. + - **Attachments map at parity with `Attachment.fromJson`:** each gets a new local `id`, and the id the + attachment was sent with stays in `custom`; `giphy` and `fields` are written in their v1 JSON shape, + which `GiphyInfo` and the UI read. Making `Attachment` plain, and typing the renditions and + `fields`, is left to this group. + - **A reminder or location nests its message one level deep,** so the recursion ends there. + - **`MessageWithChannelResponse` gets its mapper with its first consumer** (getMessage, search), not + ahead of it. + """), ), dict( num='11', slug='channels-and-members', title='Channels, Members & Sync', diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index cce82a2852..f602cdafd9 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -121,6 +121,12 @@ - `UserBlock` no longer decodes from or encodes to JSON and no longer extends `Equatable`, so `props` is removed; it still compares by value. - `UserBlock.blockedUser`, `userId`, `blockedUserId` and `createdAt` are required and non-nullable, and `UserBlock.copyWith` takes every field as a non-nullable value. - `StreamChatApi.user`'s blocking methods are removed; use `blockUser`, `unblockUser` and `getBlockedUsers` on `StreamChatClient` instead. +- `Moderation`, `ReactionGroup`, `Action`, `Reaction`, `Location` and `MessageReminder` no longer decode from or encode to JSON. +- `Moderation`, `ReactionGroup`, `Reaction`, `Location` and `MessageReminder` no longer extend `Equatable`, so `props` is removed; they still compare by value. +- `Action` compares by value instead of by identity. +- `Moderation` and `Action` gain `copyWith`. +- `ReactionGroup` gains `fromData` and `toData`, for the offline cache only. +- `copyWith` on `Location`, `MessageReminder`, `Reaction` and `ReactionGroup` clears a nullable field passed as `null`, and resets a date passed as `null` to now. - `StreamChatClient.updateUser` and `updateUsers` return a `Result` instead of throwing. - `StreamChatClient.partialUpdateUser` and `partialUpdateUsers` are renamed `updateUserPartial` and `updateUsersPartial`, and return a `Result` instead of throwing. - `PartialUpdateUserRequest` is renamed `UpdateUserPartialRequest`. @@ -129,6 +135,11 @@ - The users in an `UpdateUsersResponse` no longer carry their devices, mutes, channel mutes, privacy settings, unread counts, hidden channels or token revocation time in their custom data. - `updateUser` and `updateUsers` no longer send the user's `role`, `teams` and `teamsRole`. - `StreamChatApi.user.updateUsers` and `partialUpdateUsers` are removed; call `updateUsers` and `updateUsersPartial` on `StreamChatClient` instead. +- The reminder calls on `StreamChatClient` and `Channel` return a `Result` instead of throwing. +- `deleteReminder` returns a `DeleteReminderResponse` instead of an `EmptyResponse`. +- `queryReminders` takes `limit`, `next` and `prev` instead of `PaginationParams`. +- `CreateReminderResponse`, `UpdateReminderResponse` and `QueryRemindersResponse` are immutable, compare by value, and their `duration` is non-nullable. +- `MessageReminderResponse` and `StreamChatApi.reminders` are removed. - `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`. - The channel and member values `custom` used to hold, such as `ChannelModel.hidden` and `Member.status`, are fields now: `custom` and `toJson` leave them out, and setting one through `custom` has no effect. - An entry of `ChannelModel.custom` or `Member.custom` named like one of the model's own fields stays in `custom` and leaves the field alone, and `toJson` leaves it out. @@ -139,6 +150,11 @@ - An entry of `User.custom` named like one of the user's own fields stays in `custom` and leaves the field alone, and `toJson` leaves it out. - `OwnUser.fromUser` no longer reads devices, mutes, unread counts, push preferences or privacy settings out of the user's custom data; they are left at their defaults. - `rawType` is removed from `MessageType`, `AttachmentType`, `PushLevel`, `ChatPreferenceLevel`, `ChatLevel`, `CallLevel` and `RoleType`, as are `ModerationAction.action` and `ChannelCapability.capability`; use the value itself instead. +- `extraData` is renamed `custom` on `Message`, `Attachment`, `Reaction` and `DraftMessage`, and on `Channel.sendStaticLocation` and `startLiveLocationSharing`. +- A custom entry named like one of the model's own fields is no longer sent. +- A message's channel id, HTML, markup and deleted reply count, and an attachment's Giphy renditions, are fields instead of custom data. +- `Message.toJson` no longer writes the message's channel id, HTML, markup or deleted reply count. +- `Reaction.topLevelFields` is removed. 🐞 Fixed @@ -175,6 +191,8 @@ - Added `StreamChatClient.moderation.muteUsers` and `unmuteUsers`, which mute and unmute several users in one call and report the ids that matched no user. - Added `User.deactivatedAt`, `deletedAt`, `shadowBanned`, `blockedUserIds` and `nameOrId`; the constructors and `copyWith` accept the new fields. - 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 `Message.cid`, `html`, `mml` and `deletedReplyCount`, and `Attachment.giphy`. +- Added a `prev` cursor to `QueryRemindersResponse`. - 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 e0a248049c..4a1d68cd09 100644 --- a/packages/stream_chat/lib/src/client/channel/channel.dart +++ b/packages/stream_chat/lib/src/client/channel/channel.dart @@ -694,7 +694,7 @@ class Channel { it.file!, onSendProgress: onSendProgress, cancelToken: cancelToken, - extraData: it.extraData, + extraData: it.custom, ); throttledUpdateAttachment.cancel(); @@ -1270,19 +1270,19 @@ class Channel { /// Sends a static location to this channel. /// - /// Optionally, provide a [messageText] and [extraData] to send along with - /// the location. + /// A [messageText] and [custom] data, when given, are sent in the message + /// with the location. Future sendStaticLocation({ String? id, String? messageText, String? createdByDeviceId, required LocationCoordinate location, - Map extraData = const {}, + Map custom = const {}, }) { final message = Message( id: id, text: messageText, - extraData: extraData, + custom: custom, ); final currentUserId = _client.state.currentUser?.id; @@ -1302,20 +1302,20 @@ class Channel { /// Sends a live location sharing message to this channel. /// - /// Optionally, provide a [messageText] and [extraData] to send along with - /// the location. + /// A [messageText] and [custom] data, when given, are sent in the message + /// with the location. Future startLiveLocationSharing({ String? id, String? messageText, String? createdByDeviceId, required DateTime endSharingAt, required LocationCoordinate location, - Map extraData = const {}, + Map custom = const {}, }) { final message = Message( id: id, text: messageText, - extraData: extraData, + custom: custom, ); final currentUserId = _client.state.currentUser?.id; @@ -1592,12 +1592,13 @@ class Channel { ); } - /// Create a reminder for the given [messageId]. + /// Creates a reminder on the message with the id [messageId]. + /// + /// The reminder is due at [remindAt]. Without it, the reminder is a bookmark, + /// which is never due. /// - /// Optionally, provide a [remindAt] date to set when the reminder should - /// be triggered. If not provided, the reminder will be created as a - /// bookmark type instead. - Future createReminder( + /// Throws a [StateError] if this channel is not initialized. + Future> createReminder( String messageId, { DateTime? remindAt, }) { @@ -1608,12 +1609,13 @@ class Channel { ); } - /// Update an existing reminder with the given [reminderId]. + /// Updates the reminder on the message with the id [messageId]. /// - /// Optionally, provide a [remindAt] date to set when the reminder should - /// be triggered. If not provided, the reminder will be updated as a - /// bookmark type instead. - Future updateReminder( + /// The reminder becomes due at [remindAt]. Without it, the reminder becomes a + /// bookmark, which is never due. + /// + /// Throws a [StateError] if this channel is not initialized. + Future> updateReminder( String messageId, { DateTime? remindAt, }) { @@ -1624,8 +1626,10 @@ class Channel { ); } - /// Remove the reminder for the given [messageId]. - Future deleteReminder(String messageId) { + /// Deletes the reminder on the message with the id [messageId]. + /// + /// Throws a [StateError] if this channel is not initialized. + Future> deleteReminder(String messageId) { _checkInitialized(); return _client.deleteReminder(messageId); } @@ -1645,7 +1649,7 @@ class Channel { // ignore: parameter_assignments reaction = reaction.copyWith( messageId: messageId, - user: _client.state.currentUser, + user: _client.state.currentUser ?? reaction.user, ); final updatedMessage = message.addMyReaction( diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 230b56a41f..8f48d1b903 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -64,8 +64,10 @@ import '../core/models/request/update_user_partial_request.dart'; import '../core/models/response/add_user_group_members_response.dart'; import '../core/models/response/app_settings_response.dart'; import '../core/models/response/block_users_response.dart'; +import '../core/models/response/create_reminder_response.dart'; import '../core/models/response/create_user_group_response.dart'; import '../core/models/response/delete_channel_response.dart'; +import '../core/models/response/delete_reminder_response.dart'; import '../core/models/response/get_blocked_users_response.dart'; import '../core/models/response/get_unread_count_response.dart'; import '../core/models/response/get_user_group_response.dart'; @@ -80,6 +82,7 @@ import '../core/models/response/poll_response.dart'; import '../core/models/response/poll_vote_response.dart'; import '../core/models/response/query_poll_votes_response.dart'; import '../core/models/response/query_polls_response.dart'; +import '../core/models/response/query_reminders_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'; @@ -87,6 +90,7 @@ import '../core/models/response/show_channel_response.dart'; import '../core/models/response/unblock_users_response.dart'; import '../core/models/response/update_channel_partial_response.dart'; import '../core/models/response/update_member_partial_response.dart'; +import '../core/models/response/update_reminder_response.dart'; import '../core/models/response/update_user_group_response.dart'; import '../core/models/response/update_users_response.dart'; import '../core/models/role_type.dart'; @@ -104,6 +108,7 @@ import '../repository/devices_repository.dart'; import '../repository/general_repository.dart'; import '../repository/moderation_repository.dart'; import '../repository/polls_repository.dart'; +import '../repository/reminders_repository.dart'; import '../repository/roles_repository.dart'; import '../repository/user_groups_repository.dart'; import '../repository/users_repository.dart'; @@ -210,6 +215,7 @@ class StreamChatClient { _userGroupsRepository = UserGroupsRepository(api); _usersRepository = UsersRepository(api); _pollsRepository = PollsRepository(api); + _remindersRepository = RemindersRepository(api); _generalRepository = GeneralRepository(api); _moderationRepository = ModerationRepository(api); _channelsRepository = ChannelsRepository(api); @@ -256,6 +262,7 @@ class StreamChatClient { late final UserGroupsRepository _userGroupsRepository; late final UsersRepository _usersRepository; late final PollsRepository _pollsRepository; + late final RemindersRepository _remindersRepository; late final GeneralRepository _generalRepository; late final ModerationRepository _moderationRepository; late final ChannelsRepository _channelsRepository; @@ -2363,52 +2370,42 @@ class StreamChatClient { ); } - /// Queries reminders for the current user. + /// Fetches one page of the current user's reminders matching [filter], + /// ordered by [sort]. /// - /// Optionally, pass [filter], [sort] and [pagination] to filter, sort and - /// paginate the reminders. - Future queryReminders({ + /// Up to [limit] reminders are returned. The next page is fetched by passing + /// a response's [QueryRemindersResponse.next] as [next], and the previous + /// page by passing its [QueryRemindersResponse.prev] as [prev]. At most one + /// of [next] and [prev] may be given. + Future> queryReminders({ MessageReminderFilter? filter, List? sort, - PaginationParams pagination = const PaginationParams(), - }) { - return _chatApi.reminders.queryReminders( - filter: filter, - sort: sort, - pagination: pagination, - ); - } + int limit = 10, + String? next, + String? prev, + }) => _remindersRepository.queryReminders(filter: filter, sort: sort, limit: limit, next: next, prev: prev); - /// Creates a reminder for the given [messageId]. + /// Creates a reminder on the message with the id [messageId]. /// - /// Optionally, pass [remindAt] to set the reminder time. - Future createReminder( + /// The reminder is due at [remindAt]. Without it, the reminder is a bookmark, + /// which is never due. + Future> createReminder( String messageId, { DateTime? remindAt, - }) { - return _chatApi.reminders.createReminder( - messageId, - remindAt: remindAt, - ); - } + }) => _remindersRepository.createReminder(messageId, remindAt: remindAt); - /// Updates a reminder for the given [messageId]. + /// Updates the reminder on the message with the id [messageId]. /// - /// Optionally, pass [remindAt] to set the new reminder time. - Future updateReminder( + /// The reminder becomes due at [remindAt]. Without it, the reminder becomes a + /// bookmark, which is never due. + Future> updateReminder( String messageId, { DateTime? remindAt, - }) { - return _chatApi.reminders.updateReminder( - messageId, - remindAt: remindAt, - ); - } + }) => _remindersRepository.updateReminder(messageId, remindAt: remindAt); - /// Deletes a reminder for the given [messageId]. - Future deleteReminder(String messageId) { - return _chatApi.reminders.deleteReminder(messageId); - } + /// Deletes the reminder on the message with the id [messageId]. + Future> deleteReminder(String messageId) => + _remindersRepository.deleteReminder(messageId); /// Lists user groups with cursor-based pagination. /// diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 05d4f451ee..464b2d2a3e 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import '../http/stream_http_client.dart'; +import '../models/converters/v1_json_converters.dart'; import '../models/draft.dart'; import '../models/draft_message.dart'; import '../models/message.dart'; @@ -222,7 +223,7 @@ class MessageApi { final response = await _client.post( '/messages/$messageId/reaction', data: json.encode({ - 'reaction': reaction.toJson(), + 'reaction': const ReactionV1JsonConverter().toJson(reaction), 'skip_push': skipPush, 'enforce_unique': enforceUnique, }), diff --git a/packages/stream_chat/lib/src/core/api/reminders_api.dart b/packages/stream_chat/lib/src/core/api/reminders_api.dart deleted file mode 100644 index f4965dc3b7..0000000000 --- a/packages/stream_chat/lib/src/core/api/reminders_api.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:convert'; - -import '../http/stream_http_client.dart'; -import '../models/message_reminder.dart'; -import 'requests.dart'; -import 'responses.dart'; - -/// Defines the api dedicated to message reminders operations -class RemindersApi { - /// Initialize a new reminders api - const RemindersApi(this._client); - - final StreamHttpClient _client; - - /// Retrieves the list of reminders for the current user. - /// - /// Optionally, you can filter and sort the reminders using the [filter] and - /// [sort] parameters respectively. You can also paginate the results using - /// [pagination]. - /// - /// Returns a [QueryRemindersResponse] containing the list of reminders. - Future queryReminders({ - MessageReminderFilter? filter, - List? sort, - PaginationParams? pagination, - }) async { - final response = await _client.post( - '/reminders/query', - data: jsonEncode({ - if (filter != null) 'filter': filter, - if (sort != null) 'sort': sort, - if (pagination != null) ...pagination.toJson(), - }), - ); - - return QueryRemindersResponse.fromJson(response.data); - } - - /// Creates a new reminder for the specified [messageId]. - /// - /// You can specify the time to remind using the [remindAt] parameter. - /// - /// Returns a [CreateReminderResponse] containing the created reminder. - Future createReminder( - String messageId, { - DateTime? remindAt, - }) async { - final response = await _client.post( - '/messages/$messageId/reminders', - data: jsonEncode({ - if (remindAt != null) 'remind_at': remindAt.toUtc().toIso8601String(), - }), - ); - - return CreateReminderResponse.fromJson(response.data); - } - - /// Updates an existing reminder for the specified [messageId]. - /// - /// You can change the reminder time using the [remindAt] parameter. - /// - /// Returns an [UpdateReminderResponse] containing the updated reminder. - Future updateReminder( - String messageId, { - DateTime? remindAt, - }) async { - final response = await _client.patch( - '/messages/$messageId/reminders', - data: jsonEncode({ - if (remindAt != null) 'remind_at': remindAt.toUtc().toIso8601String(), - }), - ); - - return UpdateReminderResponse.fromJson(response.data); - } - - /// Deletes a reminder for the specified [messageId]. - /// - /// Returns an [EmptyResponse] indicating the deletion was successful. - Future deleteReminder( - String messageId, - ) async { - final response = await _client.delete( - '/messages/$messageId/reminders', - ); - - return EmptyResponse.fromJson(response.data); - } -} diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 3e5647a14e..df18b1c25a 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -4,11 +4,11 @@ import '../../ws/events/event.dart'; import '../models/banned_user.dart'; import '../models/channel_model.dart'; import '../models/channel_state.dart'; +import '../models/converters/v1_json_converters.dart'; import '../models/draft.dart'; import '../models/location.dart'; import '../models/member.dart'; import '../models/message.dart'; -import '../models/message_reminder.dart'; import '../models/predefined_filter.dart'; import '../models/push_preference.dart'; import '../models/reaction.dart'; @@ -125,6 +125,7 @@ class QueryBannedUsersResponse extends _BaseResponse { class QueryReactionsResponse extends _BaseResponse { /// List of reactions returned by the query @JsonKey(defaultValue: []) + @ReactionV1JsonConverter() late List reactions; /// The cursor for the next page of results. @@ -151,6 +152,7 @@ class QueryRepliesResponse extends _BaseResponse { @JsonSerializable(createToJson: false) class SendReactionResponse extends MessageResponse { /// The reaction created by the api call + @ReactionV1JsonConverter() late Reaction reaction; /// Create a new instance from a json @@ -186,7 +188,7 @@ class GetMessageResponse extends MessageResponse { /// Create a new instance from a json static GetMessageResponse fromJson(Map json) { final res = _$GetMessageResponseFromJson(json); - final jsonChannel = res.message.extraData.remove('channel'); + final jsonChannel = res.message.custom.remove('channel'); if (jsonChannel != null) { res.channel = ChannelModel.fromJson(jsonChannel as Map); } @@ -434,40 +436,6 @@ class QueryDraftsResponse extends _BaseResponse { static QueryDraftsResponse fromJson(Map json) => _$QueryDraftsResponseFromJson(json); } -/// Base Model response for draft based api calls. -class MessageReminderResponse extends _BaseResponse { - /// Draft returned by the api call - late MessageReminder reminder; -} - -/// Model response for [StreamChatClient.createReminder] api call -@JsonSerializable(createToJson: false) -class CreateReminderResponse extends MessageReminderResponse { - /// Create a new instance from a json - static CreateReminderResponse fromJson(Map json) => _$CreateReminderResponseFromJson(json); -} - -/// Model response for [StreamChatClient.updateReminder] api call -@JsonSerializable(createToJson: false) -class UpdateReminderResponse extends MessageReminderResponse { - /// Create a new instance from a json - static UpdateReminderResponse fromJson(Map json) => _$UpdateReminderResponseFromJson(json); -} - -/// Model response for [StreamChatClient.queryReminders] api call -@JsonSerializable(createToJson: false) -class QueryRemindersResponse extends _BaseResponse { - /// List of reminders returned by the query - @JsonKey(defaultValue: []) - late List reminders; - - /// The next page token - late String? next; - - /// Create a new instance from a json - static QueryRemindersResponse fromJson(Map json) => _$QueryRemindersResponseFromJson(json); -} - /// Model response for [StreamChatClient.setPushPreferences] api call @JsonSerializable(createToJson: false) class UpsertPushPreferencesResponse extends _BaseResponse { @@ -499,6 +467,7 @@ Map _userPreferencesFromJson(Map? json) @JsonSerializable(createToJson: false) class GetActiveLiveLocationsResponse extends _BaseResponse { /// List of active live locations returned by the api call + @LocationV1JsonConverter() late List activeLiveLocations; /// Create a new instance from a json 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 f9cc331b51..22fe0c3256 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -65,7 +65,14 @@ QueryReactionsResponse _$QueryReactionsResponseFromJson( ) => QueryReactionsResponse() ..duration = json['duration'] as String? ..reactions = - (json['reactions'] as List?)?.map((e) => Reaction.fromJson(e as Map)).toList() ?? [] + (json['reactions'] as List?) + ?.map( + (e) => const ReactionV1JsonConverter().fromJson( + e as Map, + ), + ) + .toList() ?? + [] ..next = json['next'] as String?; QueryRepliesResponse _$QueryRepliesResponseFromJson( @@ -80,7 +87,9 @@ SendReactionResponse _$SendReactionResponseFromJson( ) => SendReactionResponse() ..duration = json['duration'] as String? ..message = Message.fromJson(json['message'] as Map) - ..reaction = Reaction.fromJson(json['reaction'] as Map); + ..reaction = const ReactionV1JsonConverter().fromJson( + json['reaction'] as Map, + ); UpdateMessageResponse _$UpdateMessageResponseFromJson( Map json, @@ -210,31 +219,6 @@ 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() - ..duration = json['duration'] as String? - ..reminder = MessageReminder.fromJson( - json['reminder'] as Map, - ); - -UpdateReminderResponse _$UpdateReminderResponseFromJson( - Map json, -) => UpdateReminderResponse() - ..duration = json['duration'] as String? - ..reminder = MessageReminder.fromJson( - json['reminder'] as Map, - ); - -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?; - UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson( Map json, ) => UpsertPushPreferencesResponse() @@ -261,5 +245,7 @@ GetActiveLiveLocationsResponse _$GetActiveLiveLocationsResponseFromJson( ) => GetActiveLiveLocationsResponse() ..duration = json['duration'] as String? ..activeLiveLocations = (json['active_live_locations'] as List) - .map((e) => Location.fromJson(e as Map)) + .map( + (e) => const LocationV1JsonConverter().fromJson(e as Map), + ) .toList(); diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index 3726a0eba0..8627561cdc 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -7,7 +7,6 @@ import 'general_api.dart'; import 'message_api.dart'; import 'moderation_api.dart'; import 'push_preferences_api.dart'; -import 'reminders_api.dart'; import 'threads_api.dart'; import 'user_api.dart'; @@ -61,10 +60,6 @@ class StreamChatApi { ModerationApi get moderation => _moderation ??= ModerationApi(_client); ModerationApi? _moderation; - /// Api dedicated to message reminders operations - RemindersApi get reminders => _reminders ??= RemindersApi(_client); - RemindersApi? _reminders; - /// Api dedicated to general operations GeneralApi get general => _general ??= GeneralApi(_client); GeneralApi? _general; diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index d5c6e4557a..a61b3bafd3 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:stream_core/stream_core.dart' show LocationCoordinate; import '../http/stream_http_client.dart'; +import '../models/converters/v1_json_converters.dart'; import '../models/location.dart'; import '../models/user.dart'; import 'requests.dart'; @@ -65,6 +66,6 @@ class UserApi { }), ); - return Location.fromJson(response.data); + return const LocationV1JsonConverter().fromJson(response.data); } } diff --git a/packages/stream_chat/lib/src/core/models/action.dart b/packages/stream_chat/lib/src/core/models/action.dart index 709fbfe0c5..02c936a9a5 100644 --- a/packages/stream_chat/lib/src/core/models/action.dart +++ b/packages/stream_chat/lib/src/core/models/action.dart @@ -1,12 +1,13 @@ -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -part 'action.g.dart'; +part 'action.freezed.dart'; -/// The class that contains the information about an action -@JsonSerializable() -class Action { - /// Constructor used for json serialization - Action({ +/// An interactive control attached to a message, such as the Send, Shuffle and +/// Cancel buttons of a giphy preview. +@freezed +class Action with _$Action { + /// Creates a new [Action]. + const Action({ required this.name, this.style = 'default', required this.text, @@ -14,24 +15,23 @@ class Action { this.value, }); - /// Create a new instance from a json - factory Action.fromJson(Map json) => _$ActionFromJson(json); - - /// The name of the action + /// The name the action is submitted under. + @override final String name; - /// The style of the action + /// The visual style of the action, such as `primary` or `default`. + @override final String style; - /// The test of the action + /// The label shown for the action. + @override final String text; - /// The type of the action + /// The kind of control, such as `button`. + @override final String type; - /// The value of the action + /// The value the action submits, or null if it submits none. + @override final String? value; - - /// Serialize to json - Map toJson() => _$ActionToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/action.freezed.dart b/packages/stream_chat/lib/src/core/models/action.freezed.dart new file mode 100644 index 0000000000..7458bc90c8 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/action.freezed.dart @@ -0,0 +1,99 @@ +// 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 'action.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$Action { + String get name; + String get style; + String get text; + String get type; + String? get value; + + /// Create a copy of Action + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $ActionCopyWith get copyWith => _$ActionCopyWithImpl(this as Action, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Action && + (identical(other.name, name) || other.name == name) && + (identical(other.style, style) || other.style == style) && + (identical(other.text, text) || other.text == text) && + (identical(other.type, type) || other.type == type) && + (identical(other.value, value) || other.value == value)); + } + + @override + int get hashCode => Object.hash(runtimeType, name, style, text, type, value); + + @override + String toString() { + return 'Action(name: $name, style: $style, text: $text, type: $type, value: $value)'; + } +} + +/// @nodoc +abstract mixin class $ActionCopyWith<$Res> { + factory $ActionCopyWith(Action value, $Res Function(Action) _then) = _$ActionCopyWithImpl; + @useResult + $Res call({String name, String style, String text, String type, String? value}); +} + +/// @nodoc +class _$ActionCopyWithImpl<$Res> implements $ActionCopyWith<$Res> { + _$ActionCopyWithImpl(this._self, this._then); + + final Action _self; + final $Res Function(Action) _then; + + /// Create a copy of Action + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? style = null, + Object? text = null, + Object? type = null, + Object? value = freezed, + }) { + return _then( + Action( + name: null == name + ? _self.name + : name // ignore: cast_nullable_to_non_nullable + as String, + style: null == style + ? _self.style + : style // ignore: cast_nullable_to_non_nullable + as String, + text: null == text + ? _self.text + : text // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as String, + value: freezed == value + ? _self.value + : value // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/action.g.dart b/packages/stream_chat/lib/src/core/models/action.g.dart deleted file mode 100644 index ec604d553f..0000000000 --- a/packages/stream_chat/lib/src/core/models/action.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'action.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Action _$ActionFromJson(Map json) => Action( - name: json['name'] as String, - style: json['style'] as String? ?? 'default', - text: json['text'] as String, - type: json['type'] as String, - value: json['value'] as String?, -); - -Map _$ActionToJson(Action instance) => { - 'name': instance.name, - 'style': instance.style, - 'text': instance.text, - 'type': instance.type, - 'value': instance.value, -}; diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index cecf94d77e..52043d67fe 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -7,6 +7,7 @@ import 'package:uuid/uuid.dart'; import '../util/serializer.dart'; import 'action.dart'; import 'attachment_file.dart'; +import 'converters/v1_json_converters.dart'; import 'response/og_attachment_response.dart'; part 'attachment.g.dart'; @@ -37,9 +38,10 @@ class Attachment extends Equatable { this.actions = const [], this.originalWidth, this.originalHeight, - Map extraData = const {}, + Map custom = const {}, this.file, this.uploadState = const UploadState.preparing(), + this.giphy, }) : id = id ?? const Uuid().v4(), _type = switch (type) { String() => AttachmentType(type), @@ -47,10 +49,9 @@ class Attachment extends Equatable { }, title = title ?? file?.name, localUri = file?.path != null ? Uri.parse(file!.path!) : null, - // For backwards compatibility, - // set 'file_size', 'mime_type' in [extraData]. - extraData = { - ...extraData, + // For backwards compatibility, set 'file_size', 'mime_type' in [custom]. + custom = { + ...custom, if (file?.size != null) 'file_size': file?.size, if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType, }; @@ -140,6 +141,7 @@ class Attachment extends Equatable { final String? assetUrl; /// Actions from a command + @ActionV1JsonConverter() final List? actions; /// The original width of the attached image. @@ -157,8 +159,9 @@ class Attachment extends Equatable { @JsonKey(defaultValue: UploadState.success) final UploadState uploadState; - /// Map of custom channel extraData - final Map extraData; + /// This attachment's custom data. + @JsonKey(name: 'extra_data') + final Map custom; /// The attachment ID. /// @@ -169,13 +172,20 @@ class Attachment extends Equatable { /// /// {@macro fileSize} @JsonKey(includeToJson: false, includeFromJson: false) - int? get fileSize => extraData['file_size'] as int?; + int? get fileSize => custom['file_size'] as int?; /// Shortcut for file mimeType. /// /// {@macro mimeType} @JsonKey(includeToJson: false, includeFromJson: false) - String? get mimeType => extraData['mime_type'] as String?; + String? get mimeType => custom['mime_type'] as String?; + + /// The renditions of this Giphy attachment, keyed by name, such as `original` + /// or `fixed_height`. + /// + /// Each rendition holds its `url`, `width`, `height`, `size` and `frames`, as + /// strings. + final Map? giphy; /// Known top level fields. /// Useful for [Serializer] methods. @@ -200,6 +210,7 @@ class Attachment extends Equatable { 'actions', 'original_width', 'original_height', + 'giphy', ]; /// Known db specific top level fields. @@ -215,13 +226,21 @@ class Attachment extends Equatable { ...dbSpecificTopLevelFields, ]; - /// Serialize to json + /// Serializes this attachment to JSON. + /// + /// Writes the entries of [custom] at the root, leaving out any named like one + /// of [topLevelFields]. Map toJson() => - Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this)) + Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this), topLevelFields: topLevelFields) ..removeWhere((key, value) => dbSpecificTopLevelFields.contains(key)); + // The fields toData leaves custom data out of. A custom `id`, the id the + // attachment was sent with, replaces the local one. + static const _storedFields = [...topLevelFields, 'file', 'upload_state']; + /// Serialize to db data - Map toData() => Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this)); + Map toData() => + Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this), topLevelFields: _storedFields); Attachment copyWith({ String? id, @@ -247,7 +266,8 @@ class Attachment extends Equatable { int? originalHeight, AttachmentFile? file, UploadState? uploadState, - Map? extraData, + Map? custom, + Map? giphy, }) => Attachment( id: id ?? this.id, type: type ?? this.type, @@ -272,7 +292,8 @@ class Attachment extends Equatable { originalHeight: originalHeight ?? this.originalHeight, file: file ?? this.file, uploadState: uploadState ?? this.uploadState, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, + giphy: giphy ?? this.giphy, ); Attachment merge(Attachment? other) { @@ -300,7 +321,8 @@ class Attachment extends Equatable { originalHeight: other.originalHeight, file: other.file, uploadState: other.uploadState, - extraData: other.extraData, + custom: other.custom, + giphy: other.giphy, ); } @@ -329,7 +351,8 @@ class Attachment extends Equatable { originalHeight, file, uploadState, - extraData, + custom, + giphy, ]; } diff --git a/packages/stream_chat/lib/src/core/models/attachment.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart index de9fd74d4a..946c4ade50 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.g.dart @@ -26,14 +26,18 @@ Attachment _$AttachmentFromJson(Map json) => Attachment( authorIcon: json['author_icon'] as String?, assetUrl: json['asset_url'] as String?, actions: - (json['actions'] as List?)?.map((e) => Action.fromJson(e as Map)).toList() ?? const [], + (json['actions'] as List?) + ?.map((e) => const ActionV1JsonConverter().fromJson(e as Map)) + .toList() ?? + const [], originalWidth: (json['original_width'] as num?)?.toInt(), originalHeight: (json['original_height'] as num?)?.toInt(), - extraData: json['extra_data'] as Map? ?? const {}, + custom: json['extra_data'] as Map? ?? const {}, file: json['file'] == null ? null : AttachmentFile.fromJson(json['file'] as Map), uploadState: json['upload_state'] == null ? const UploadState.success() : UploadState.fromJson(json['upload_state'] as Map), + giphy: json['giphy'] as Map?, ); Map _$AttachmentToJson(Attachment instance) => { @@ -54,11 +58,12 @@ Map _$AttachmentToJson(Attachment instance) => e.toJson()).toList(), + 'actions': ?instance.actions?.map(const ActionV1JsonConverter().toJson).toList(), 'original_width': ?instance.originalWidth, 'original_height': ?instance.originalHeight, 'file': ?instance.file?.toJson(), 'upload_state': instance.uploadState.toJson(), - 'extra_data': instance.extraData, + 'extra_data': instance.custom, 'id': instance.id, + 'giphy': ?instance.giphy, }; diff --git a/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart b/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart index bff31bd949..fe989239fb 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart @@ -54,7 +54,7 @@ class GiphyInfo { extension GiphyInfoX on Attachment { /// Returns the [GiphyInfo] for the given [type]. GiphyInfo? giphyInfo(GiphyInfoType type) { - final giphy = extraData['giphy'] as Map?; + final giphy = this.giphy; if (giphy == null) return null; final info = giphy[type.value] as Map?; diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 66b3aeb365..09198bf0ea 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_core/stream_core.dart' show Filter, FilterField, NullOrdering, Sort, SortDirection, SortField, Standard, normalizeStringForSort; import 'channel_model.dart'; +import 'converters/v1_json_converters.dart'; import 'draft.dart'; import 'location.dart'; import 'member.dart'; @@ -90,6 +91,7 @@ class ChannelState { final ChannelPushPreference? pushPreferences; /// The list of active live locations in the channel. + @LocationV1JsonConverter() final List? activeLiveLocations; /// Create a new instance from a json diff --git a/packages/stream_chat/lib/src/core/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart index 6d98a30b44..652760d341 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.g.dart @@ -27,7 +27,9 @@ ChannelState _$ChannelStateFromJson(Map json) => ChannelState( json['push_preferences'] as Map, ), activeLiveLocations: (json['active_live_locations'] as List?) - ?.map((e) => Location.fromJson(e as Map)) + ?.map( + (e) => const LocationV1JsonConverter().fromJson(e as Map), + ) .toList(), ); @@ -45,5 +47,5 @@ Map _$ChannelStateToJson( 'draft': instance.draft?.toJson(), 'pending_messages': instance.pendingMessages?.map((e) => e.toJson()).toList(), 'push_preferences': instance.pushPreferences?.toJson(), - 'active_live_locations': instance.activeLiveLocations?.map((e) => e.toJson()).toList(), + 'active_live_locations': instance.activeLiveLocations?.map(const LocationV1JsonConverter().toJson).toList(), }; diff --git a/packages/stream_chat/lib/src/core/models/converters/v1_json_converters.dart b/packages/stream_chat/lib/src/core/models/converters/v1_json_converters.dart index 5fce137bd7..26b00c1beb 100644 --- a/packages/stream_chat/lib/src/core/models/converters/v1_json_converters.dart +++ b/packages/stream_chat/lib/src/core/models/converters/v1_json_converters.dart @@ -3,11 +3,19 @@ import 'package:meta/meta.dart'; import 'package:stream_core/stream_core.dart' show StreamDateTimeConverter; import '../../util/serializer.dart'; +import '../action.dart'; +import '../channel_model.dart'; import '../device.dart'; +import '../location.dart'; +import '../message.dart'; +import '../message_reminder.dart'; +import '../moderation.dart'; import '../poll.dart'; import '../poll_option.dart'; import '../poll_vote.dart'; import '../push_provider.dart'; +import '../reaction.dart'; +import '../reaction_group.dart'; import '../user.dart'; import '../user_group.dart'; import '../user_group_member.dart'; @@ -16,6 +24,173 @@ import '../voting_visibility.dart'; // Converters for plain models embedded in json_serializable models that still decode v1 JSON. Each one is deleted // by the migration group that turns its parent into a plain model. +/// Converts an [Action] to and from its v1 keys. +// TODO(openapi-migration): remove in group 10 +@internal +class ActionV1JsonConverter implements JsonConverter> { + /// Creates a new [ActionV1JsonConverter]. + const ActionV1JsonConverter(); + + @override + Action fromJson(Map json) => Action( + name: json['name'] as String, + style: json['style'] as String? ?? 'default', + text: json['text'] as String, + type: json['type'] as String, + value: json['value'] as String?, + ); + + // Writes `value` even when it is null, so sent and stored attachments always + // carry the key. + @override + Map toJson(Action action) => { + 'name': action.name, + 'style': action.style, + 'text': action.text, + 'type': action.type, + 'value': action.value, + }; +} + +/// Converts a [Location] to and from its v1 keys. +/// +/// [toJson] writes the request shape: the coordinates, the device and the end +/// date, without the channel, message, user or the other dates. +// TODO(openapi-migration): remove in group 10 +@internal +class LocationV1JsonConverter implements JsonConverter> { + /// Creates a new [LocationV1JsonConverter]. + const LocationV1JsonConverter(); + + @override + Location fromJson(Map json) => Location( + channelCid: json['channel_cid'] as String?, + channel: switch (json['channel']) { + final Map channel => ChannelModel.fromJson(channel), + _ => null, + }, + messageId: json['message_id'] as String?, + message: switch (json['message']) { + final Map message => Message.fromJson(message), + _ => null, + }, + userId: json['user_id'] as String?, + latitude: (json['latitude'] as num).toDouble(), + longitude: (json['longitude'] as num).toDouble(), + createdByDeviceId: json['created_by_device_id'] as String?, + endAt: _dateTimeOrNull(json['end_at']), + createdAt: _dateTimeOrNull(json['created_at']), + updatedAt: _dateTimeOrNull(json['updated_at']), + ); + + @override + Map toJson(Location location) => { + 'latitude': location.latitude, + 'longitude': location.longitude, + if (location.createdByDeviceId case final createdByDeviceId?) 'created_by_device_id': createdByDeviceId, + if (location.endAt case final endAt?) 'end_at': endAt.toIso8601String(), + }; +} + +/// Converts a [Reaction] to and from its v1 keys. +/// +/// Custom data sits at the root of the v1 map, beside the known keys. [toJson] +/// writes the request shape: the type, score, emoji code and custom data, +/// without the message, user or dates. +// TODO(openapi-migration): remove when WebSocket payloads decode as v2 +@internal +class ReactionV1JsonConverter implements JsonConverter> { + /// Creates a new [ReactionV1JsonConverter]. + const ReactionV1JsonConverter(); + + @override + Reaction fromJson(Map json) { + final data = Serializer.moveToExtraDataFromRoot(json, _reactionV1Keys); + return Reaction( + messageId: data['message_id'] as String?, + type: data['type'] as String, + user: switch (data['user']) { + final Map user => User.fromJson(user), + _ => null, + }, + userId: data['user_id'] as String?, + score: (data['score'] as num?)?.toInt() ?? 1, + emojiCode: data['emoji_code'] as String?, + createdAt: switch (data['created_at']) { + final Object it => _dateTime.fromJson(it), + null => null, + }, + updatedAt: switch (data['updated_at']) { + final Object it => _dateTime.fromJson(it), + null => null, + }, + custom: data['extra_data'] as Map, + ); + } + + @override + Map toJson(Reaction reaction) => Serializer.moveFromExtraDataToRoot({ + 'type': reaction.type, + 'score': reaction.score, + if (reaction.emojiCode case final emojiCode?) 'emoji_code': emojiCode, + 'extra_data': reaction.custom, + }, topLevelFields: _reactionV1Keys); +} + +// The v1 keys a reaction reads into its fields; every other key is custom data. +const _reactionV1Keys = [ + 'message_id', + 'type', + 'user', + 'user_id', + 'score', + 'emoji_code', + 'created_at', + 'updated_at', +]; + +/// Converts a [MessageReminder] to and from its v1 keys. +/// +/// [toJson] writes the ids and the dates, without the channel, message or user. +// TODO(openapi-migration): remove when WebSocket payloads decode as v2 +@internal +class MessageReminderV1JsonConverter implements JsonConverter> { + /// Creates a new [MessageReminderV1JsonConverter]. + const MessageReminderV1JsonConverter(); + + @override + MessageReminder fromJson(Map json) => MessageReminder( + channelCid: json['channel_cid'] as String, + channel: switch (json['channel']) { + final Map channel => ChannelModel.fromJson(channel), + _ => null, + }, + messageId: json['message_id'] as String, + message: switch (json['message']) { + final Map message => Message.fromJson(message), + _ => null, + }, + userId: json['user_id'] as String, + user: switch (json['user']) { + final Map user => User.fromJson(user), + _ => null, + }, + remindAt: _dateTimeOrNull(json['remind_at']), + createdAt: _dateTimeOrNull(json['created_at']), + updatedAt: _dateTimeOrNull(json['updated_at']), + ); + + @override + Map toJson(MessageReminder reminder) => { + 'channel_cid': reminder.channelCid, + 'message_id': reminder.messageId, + 'user_id': reminder.userId, + 'remind_at': reminder.remindAt?.toIso8601String(), + 'created_at': reminder.createdAt.toIso8601String(), + 'updated_at': reminder.updatedAt.toIso8601String(), + }; +} + /// Converts a [Device] to and from its v1 `id` and `push_provider` keys. // TODO(openapi-migration): remove in group 09 @internal @@ -47,9 +222,51 @@ List? userGroupsFromV1Json(List? json) { return json?.map((group) => _userGroupFromV1Json(group as Map)).toList(); } +/// Reads the moderation outcome of a message from its v1 keys. +/// +/// Decode-only: [Message.moderation] is never written back to JSON. +// TODO(openapi-migration): remove in group 10 +@internal +Moderation? moderationFromV1Json(Map? json) { + if (json == null) return null; + return Moderation( + action: ModerationAction.fromJson(json['action'] as String), + originalText: json['original_text'] as String, + textHarms: (json['text_harms'] as List?)?.map((e) => e as String).toList(), + imageHarms: (json['image_harms'] as List?)?.map((e) => e as String).toList(), + blocklistMatched: json['blocklist_matched'] as String?, + semanticFilterMatched: json['semantic_filter_matched'] as String?, + platformCircumvented: json['platform_circumvented'] as bool? ?? false, + ); +} + +/// Reads the reaction groups of a message from their v1 keys. +/// +/// Decode-only: [Message.reactionGroups] is never written back to JSON. +// TODO(openapi-migration): remove in group 10 +@internal +Map? reactionGroupsFromV1Json(Map? json) { + return json?.map((type, group) => MapEntry(type, _reactionGroupFromV1Json(group as Map))); +} + +ReactionGroup _reactionGroupFromV1Json(Map json) => ReactionGroup( + count: (json['count'] as num?)?.toInt() ?? 0, + sumScores: (json['sum_scores'] as num?)?.toInt() ?? 0, + firstReactionAt: switch (json['first_reaction_at']) { + final Object it => _dateTime.fromJson(it), + null => null, + }, + lastReactionAt: switch (json['last_reaction_at']) { + final Object it => _dateTime.fromJson(it), + null => null, + }, +); + // Dates arrive as ISO-8601 strings on v1 and as epoch nanoseconds on v2; the converter reads both. const _dateTime = StreamDateTimeConverter(); +DateTime? _dateTimeOrNull(Object? json) => json == null ? null : _dateTime.fromJson(json); + // Reads the keys directly rather than through the generated group type, whose members require `app_pk`: a payload // without it should not fail the whole message. UserGroup _userGroupFromV1Json(Map json) => UserGroup( diff --git a/packages/stream_chat/lib/src/core/models/draft_message.dart b/packages/stream_chat/lib/src/core/models/draft_message.dart index f818cd3d03..10835f271c 100644 --- a/packages/stream_chat/lib/src/core/models/draft_message.dart +++ b/packages/stream_chat/lib/src/core/models/draft_message.dart @@ -29,7 +29,7 @@ class DraftMessage extends Equatable { this.command, this.poll, this._pollId, - this.extraData = const {}, + this.custom = const {}, }) : id = id ?? const Uuid().v4(), type = MessageType(type); @@ -92,12 +92,14 @@ class DraftMessage extends Equatable { String? get pollId => _pollId ?? poll?.id; final String? _pollId; - /// Message custom extraData. - final Map extraData; + /// This draft message's custom data. + @JsonKey(name: 'extra_data') + final Map custom; - /// Known top level fields. + /// The JSON keys [DraftMessage.fromJson] reads into named fields, or leaves + /// out. /// - /// Useful for [Serializer] methods. + /// Every other key is kept in [custom]. static const topLevelFields = [ 'id', 'text', @@ -114,11 +116,15 @@ class DraftMessage extends Equatable { 'extra_data', ]; - /// Serialize to json. + /// Serializes this draft message to JSON. + /// + /// Writes the entries of [custom] at the root, leaving out any named like one + /// of [topLevelFields]. Map toJson() { final message = removeMentionsIfNotIncluded(); final json = Serializer.moveFromExtraDataToRoot( _$DraftMessageToJson(message), + topLevelFields: topLevelFields, ); // If the message contains command we should append it to the text @@ -145,7 +151,7 @@ class DraftMessage extends Equatable { String? command, Poll? poll, String? pollId, - Map? extraData, + Map? custom, }) { return DraftMessage( id: id ?? this.id, @@ -161,7 +167,7 @@ class DraftMessage extends Equatable { command: command ?? this.command, poll: poll ?? this.poll, pollId: pollId ?? this.pollId, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, ); } @@ -178,7 +184,7 @@ class DraftMessage extends Equatable { silent, command, pollId, - extraData, + custom, ]; } @@ -205,7 +211,7 @@ extension MessageToDraftMessage on Message { command: command, poll: poll, pollId: pollId, - extraData: extraData, + custom: custom, ); } } @@ -234,7 +240,7 @@ extension DraftMessageToMessage on DraftMessage { command: command, poll: poll, pollId: pollId, - extraData: extraData, + custom: custom, ); } } diff --git a/packages/stream_chat/lib/src/core/models/draft_message.g.dart b/packages/stream_chat/lib/src/core/models/draft_message.g.dart index 27e1e0a332..a512e6dedc 100644 --- a/packages/stream_chat/lib/src/core/models/draft_message.g.dart +++ b/packages/stream_chat/lib/src/core/models/draft_message.g.dart @@ -26,7 +26,7 @@ DraftMessage _$DraftMessageFromJson(Map json) => DraftMessage( command: json['command'] as String?, poll: _$JsonConverterFromJson, Poll>(json['poll'], const PollV1JsonConverter().fromJson), pollId: json['poll_id'] as String?, - extraData: json['extra_data'] as Map? ?? const {}, + custom: json['extra_data'] as Map? ?? const {}, ); Map _$DraftMessageToJson(DraftMessage instance) => { @@ -40,7 +40,7 @@ Map _$DraftMessageToJson(DraftMessage instance) => (Object? json, Value? Function(Json json) fromJson) => diff --git a/packages/stream_chat/lib/src/core/models/location.dart b/packages/stream_chat/lib/src/core/models/location.dart index a4721ecc1a..0acb87540c 100644 --- a/packages/stream_chat/lib/src/core/models/location.dart +++ b/packages/stream_chat/lib/src/core/models/location.dart @@ -1,10 +1,9 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:stream_core/stream_core.dart' show LocationCoordinate; import 'channel_model.dart'; import 'message.dart'; -part 'location.g.dart'; +part 'location.freezed.dart'; /// {@template location} /// A model class representing a shared location. @@ -17,8 +16,8 @@ part 'location.g.dart'; /// 2. **Live Location**: A location that updates in real-time and has an /// end time. /// {@endtemplate} -@JsonSerializable() -class Location extends Equatable { +@freezed +class Location with _$Location { /// {@macro location} Location({ this.channelCid, @@ -36,54 +35,53 @@ class Location extends Equatable { createdAt = createdAt ?? DateTime.timestamp(), updatedAt = updatedAt ?? DateTime.timestamp(); - /// Create a new instance from a json - factory Location.fromJson(Map json) => _$LocationFromJson(json); - /// The channel CID where the message exists. /// /// Only set on a location that has been received, not on one built locally. - @JsonKey(includeToJson: false) + @override final String? channelCid; /// The channel where the message exists. - @JsonKey(includeToJson: false) + @override final ChannelModel? channel; /// The ID of the message that contains the shared location. - @JsonKey(includeToJson: false) + @override final String? messageId; /// The message that contains the shared location. - @JsonKey(includeToJson: false) + @override final Message? message; /// The ID of the user who shared the location. - @JsonKey(includeToJson: false) + @override final String? userId; /// The latitude of the shared location. + @override final double latitude; /// The longitude of the shared location. + @override final double longitude; - /// The ID of the device that created the reminder. - @JsonKey(includeIfNull: false) + /// The ID of the device that shared the location. + @override final String? createdByDeviceId; /// The date at which the shared location will end. - @JsonKey(includeIfNull: false) + @override final DateTime? endAt; - /// The date at which the reminder was created. - @JsonKey(includeToJson: false) + /// The date at which the location was shared. + @override final DateTime createdAt; - /// The date at which the reminder was last updated. - @JsonKey(includeToJson: false) + /// The date at which the location was last updated. + @override final DateTime updatedAt; - /// Returns true if the live location is still active (end_at > now) + /// Whether this is a live location whose [endAt] is still in the future. bool get isActive { final endAt = this.endAt; if (endAt == null) return false; @@ -91,62 +89,16 @@ class Location extends Equatable { return endAt.isAfter(DateTime.now()); } - /// Returns true if the live location is expired (end_at <= now) + /// Whether this location is not [isActive]: a static location, or a live one + /// whose [endAt] has passed. bool get isExpired => !isActive; - /// Returns true if this is a live location (has end_at) + /// Whether this is a live location, one with an [endAt]. bool get isLive => endAt != null; - /// Returns true if this is a static location (no end_at) + /// Whether this is a static location, one without an [endAt]. bool get isStatic => endAt == null; /// Returns the coordinates of the shared location. LocationCoordinate get coordinates => .new(latitude: latitude, longitude: longitude); - - /// Serialize to json - Map toJson() => _$LocationToJson(this); - - /// Creates a copy of [Location] with specified attributes overridden. - Location copyWith({ - String? channelCid, - ChannelModel? channel, - String? messageId, - Message? message, - String? userId, - double? latitude, - double? longitude, - String? createdByDeviceId, - DateTime? endAt, - DateTime? createdAt, - DateTime? updatedAt, - }) { - return Location( - channelCid: channelCid ?? this.channelCid, - channel: channel ?? this.channel, - messageId: messageId ?? this.messageId, - message: message ?? this.message, - userId: userId ?? this.userId, - latitude: latitude ?? this.latitude, - longitude: longitude ?? this.longitude, - createdByDeviceId: createdByDeviceId ?? this.createdByDeviceId, - endAt: endAt ?? this.endAt, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); - } - - @override - List get props => [ - channelCid, - channel, - messageId, - message, - userId, - latitude, - longitude, - createdByDeviceId, - endAt, - createdAt, - updatedAt, - ]; } diff --git a/packages/stream_chat/lib/src/core/models/location.freezed.dart b/packages/stream_chat/lib/src/core/models/location.freezed.dart new file mode 100644 index 0000000000..2fe514bcc7 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/location.freezed.dart @@ -0,0 +1,166 @@ +// 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 'location.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$Location { + String? get channelCid; + ChannelModel? get channel; + String? get messageId; + Message? get message; + String? get userId; + double get latitude; + double get longitude; + String? get createdByDeviceId; + DateTime? get endAt; + DateTime get createdAt; + DateTime get updatedAt; + + /// Create a copy of Location + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $LocationCopyWith get copyWith => _$LocationCopyWithImpl(this as Location, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Location && + (identical(other.channelCid, channelCid) || other.channelCid == channelCid) && + (identical(other.channel, channel) || other.channel == channel) && + (identical(other.messageId, messageId) || other.messageId == messageId) && + (identical(other.message, message) || other.message == message) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.latitude, latitude) || other.latitude == latitude) && + (identical(other.longitude, longitude) || other.longitude == longitude) && + (identical(other.createdByDeviceId, createdByDeviceId) || other.createdByDeviceId == createdByDeviceId) && + (identical(other.endAt, endAt) || other.endAt == endAt) && + (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + channelCid, + channel, + messageId, + message, + userId, + latitude, + longitude, + createdByDeviceId, + endAt, + createdAt, + updatedAt, + ); + + @override + String toString() { + return 'Location(channelCid: $channelCid, channel: $channel, messageId: $messageId, message: $message, userId: $userId, latitude: $latitude, longitude: $longitude, createdByDeviceId: $createdByDeviceId, endAt: $endAt, createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} + +/// @nodoc +abstract mixin class $LocationCopyWith<$Res> { + factory $LocationCopyWith(Location value, $Res Function(Location) _then) = _$LocationCopyWithImpl; + @useResult + $Res call({ + String? channelCid, + ChannelModel? channel, + String? messageId, + Message? message, + String? userId, + double latitude, + double longitude, + String? createdByDeviceId, + DateTime? endAt, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$LocationCopyWithImpl<$Res> implements $LocationCopyWith<$Res> { + _$LocationCopyWithImpl(this._self, this._then); + + final Location _self; + final $Res Function(Location) _then; + + /// Create a copy of Location + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channelCid = freezed, + Object? channel = freezed, + Object? messageId = freezed, + Object? message = freezed, + Object? userId = freezed, + Object? latitude = null, + Object? longitude = null, + Object? createdByDeviceId = freezed, + Object? endAt = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + Location( + channelCid: freezed == channelCid + ? _self.channelCid + : channelCid // ignore: cast_nullable_to_non_nullable + as String?, + channel: freezed == channel + ? _self.channel + : channel // ignore: cast_nullable_to_non_nullable + as ChannelModel?, + messageId: freezed == messageId + ? _self.messageId + : messageId // ignore: cast_nullable_to_non_nullable + as String?, + message: freezed == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as Message?, + userId: freezed == userId + ? _self.userId + : userId // ignore: cast_nullable_to_non_nullable + as String?, + latitude: null == latitude + ? _self.latitude + : latitude // ignore: cast_nullable_to_non_nullable + as double, + longitude: null == longitude + ? _self.longitude + : longitude // ignore: cast_nullable_to_non_nullable + as double, + createdByDeviceId: freezed == createdByDeviceId + ? _self.createdByDeviceId + : createdByDeviceId // ignore: cast_nullable_to_non_nullable + as String?, + endAt: freezed == endAt + ? _self.endAt + : endAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: freezed == createdAt + ? _self.createdAt! + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _self.updatedAt! + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/location.g.dart b/packages/stream_chat/lib/src/core/models/location.g.dart deleted file mode 100644 index b41b2bc802..0000000000 --- a/packages/stream_chat/lib/src/core/models/location.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'location.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Location _$LocationFromJson(Map json) => Location( - channelCid: json['channel_cid'] as String?, - channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), - messageId: json['message_id'] as String?, - message: json['message'] == null ? null : Message.fromJson(json['message'] as Map), - userId: json['user_id'] as String?, - latitude: (json['latitude'] as num).toDouble(), - longitude: (json['longitude'] as num).toDouble(), - createdByDeviceId: json['created_by_device_id'] as String?, - endAt: json['end_at'] == null ? null : DateTime.parse(json['end_at'] as String), - createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), -); - -Map _$LocationToJson(Location instance) => { - 'latitude': instance.latitude, - 'longitude': instance.longitude, - 'created_by_device_id': ?instance.createdByDeviceId, - 'end_at': ?instance.endAt?.toIso8601String(), -}; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 24706b5c8e..da8198962c 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -5,6 +5,7 @@ import 'package:stream_core/stream_core.dart' show Filter, FilterField, Standard import 'package:uuid/uuid.dart'; import '../util/extension.dart'; +import '../util/message_decoding.dart'; import '../util/serializer.dart'; import 'attachment.dart'; import 'converters/v1_json_converters.dart'; @@ -69,7 +70,7 @@ class Message extends Equatable { this.pinnedBy, this.poll, this._pollId, - this.extraData = const {}, + this.custom = const {}, this.state = const MessageState.initial(), this.i18n, this.restrictedVisibility, @@ -78,6 +79,10 @@ class Message extends Equatable { this.reminder, this.channelRole, this.sharedLocation, + this.cid, + this.html, + this.mml, + this.deletedReplyCount, }) : id = id ?? const Uuid().v4(), type = MessageType(type), pinExpires = pinExpires?.toUtc(), @@ -87,23 +92,31 @@ class Message extends Equatable { /// Create a new instance from JSON. factory Message.fromJson(Map json) { - final message = _$MessageFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); - - final isDeletedForMe = message.deletedForMe ?? false; - // TODO: Remove this override once type is properly enriched on the backend. - final type = isDeletedForMe ? MessageType.deleted : message.type; - final state = switch (message) { - _ when isDeletedForMe => MessageState.deletedForMe, - _ when message.deletedAt != null => MessageState.softDeleted, - _ when message.updatedAt.isAfter(message.createdAt) => MessageState.updated, - _ => MessageState.sent, - }; - - return message.copyWith(type: type, state: state); + final data = Serializer.moveToExtraDataFromRoot(json, topLevelFields); + final message = _$MessageFromJson(data); + + return message + .copyWith( + reactionGroups: + message.reactionGroups ?? + reactionGroupsFromCounts( + _countsFromJson(data['reaction_counts']), + _countsFromJson(data['reaction_scores']), + ), + ) + .withDerivedState(); } + // Reads a map of reaction type to count or score, whatever kind of number + // each value is. + static Map? _countsFromJson(Object? json) => switch (json) { + final Map counts => { + for (final MapEntry(:key, :value) in counts.entries) + if (value is num) key: value.toInt(), + }, + _ => null, + }; + /// The message ID. This is either created by Stream or set client side when /// the message is added. final String id; @@ -152,49 +165,18 @@ class Message extends Equatable { @JsonKey(toJson: User.toIds) final List mentionedUsers; - static Object? _reactionGroupsReadValue( - Map json, - String key, - ) { - final reactionGroups = json[key] as Map?; - if (reactionGroups != null) return reactionGroups; - - final reactionCounts = json['reaction_counts'] as Map?; - final reactionScores = json['reaction_scores'] as Map?; - if (reactionCounts == null && reactionScores == null) return null; - - final reactionTypes = {...?reactionCounts?.keys, ...?reactionScores?.keys}; - if (reactionTypes.isEmpty) return null; - - final groups = {}; - for (final type in reactionTypes) { - final count = reactionCounts?[type] ?? 0; - final sumScores = reactionScores?[type] ?? 0; - - // Keep the group while count is positive; score may be zero or negative. - if (count <= 0) continue; - final now = DateTime.timestamp(); - groups[type] = { - 'count': count, - 'sum_scores': sumScores, - 'first_reaction_at': now.toIso8601String(), - 'last_reaction_at': now.toIso8601String(), - }; - } - - return groups; - } - /// A map of reaction types and their corresponding reaction groups. - @JsonKey(includeToJson: false, readValue: _reactionGroupsReadValue) + @JsonKey(includeToJson: false, fromJson: reactionGroupsFromV1Json) final Map? reactionGroups; /// The latest reactions to the message created by any user. @JsonKey(includeToJson: false) + @ReactionV1JsonConverter() final List? latestReactions; /// The reactions added to the message by the current user. @JsonKey(includeToJson: false) + @ReactionV1JsonConverter() final List? ownReactions; /// The ID of the parent message, if the message is a thread reply. @@ -323,7 +305,7 @@ class Message extends Equatable { } /// The moderation details for this message. - @JsonKey(includeToJson: false, readValue: _moderationReadValue) + @JsonKey(includeToJson: false, readValue: _moderationReadValue, fromJson: moderationFromV1Json) final Moderation? moderation; /// Optional draft message linked to this message. @@ -336,6 +318,7 @@ class Message extends Equatable { /// /// This is present when a user has set a reminder for this message. @JsonKey(includeToJson: false) + @MessageReminderV1JsonConverter() final MessageReminder? reminder; static Object? _channelRoleReadValue(Map json, String key) { @@ -356,22 +339,41 @@ class Message extends Equatable { /// This is used to share a location in a message, allowing users to view the /// location on a map. @JsonKey(includeIfNull: false) + @LocationV1JsonConverter() final Location? sharedLocation; /// Whether the message was deleted only for the current user. @JsonKey(includeToJson: false) final bool? deletedForMe; - /// Message custom extraData. - final Map extraData; + /// This message's custom data. + @JsonKey(name: 'extra_data') + final Map custom; + + /// The id of the channel this message belongs to, such as + /// `messaging:general`. + @JsonKey(includeToJson: false) + final String? cid; + + /// The text of this message rendered as HTML. + @JsonKey(includeToJson: false) + final String? html; + + /// The message markup language (MML) source of this message. + @JsonKey(includeToJson: false) + final String? mml; + + /// The number of replies to this message that were deleted. + @JsonKey(includeToJson: false) + final int? deletedReplyCount; /// A Map of translations. @JsonKey(includeToJson: false) final Map? i18n; - /// Known top level fields. + /// The JSON keys [Message.fromJson] reads into named fields, or leaves out. /// - /// Useful for [Serializer] methods. + /// Every other key is kept in [custom]. static const topLevelFields = [ 'id', 'text', @@ -418,13 +420,21 @@ class Message extends Equatable { 'member', 'shared_location', 'deleted_for_me', + 'cid', + 'html', + 'mml', + 'deleted_reply_count', ]; - /// Serialize to json. + /// Serializes this message to JSON. + /// + /// Writes the entries of [custom] at the root, leaving out any named like one + /// of [topLevelFields]. Map toJson() { final message = removeMentionsIfNotIncluded(); final json = Serializer.moveFromExtraDataToRoot( _$MessageToJson(message), + topLevelFields: topLevelFields, ); // If the message contains command we should append it to the text @@ -474,7 +484,7 @@ class Message extends Equatable { User? pinnedBy, Poll? poll, String? pollId, - Map? extraData, + Map? custom, MessageState? state, Map? i18n, List? restrictedVisibility, @@ -484,6 +494,10 @@ class Message extends Equatable { String? channelRole, Location? sharedLocation, bool? deletedForMe, + String? cid, + String? html, + String? mml, + int? deletedReplyCount, }) { assert(() { if (pinExpires is! DateTime && pinExpires != null && pinExpires is! _NullConst) { @@ -547,7 +561,7 @@ class Message extends Equatable { pinnedBy: pinnedBy ?? this.pinnedBy, poll: poll ?? this.poll, pollId: pollId ?? _pollId, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, state: state ?? this.state, i18n: i18n ?? this.i18n, restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, @@ -557,6 +571,10 @@ class Message extends Equatable { channelRole: channelRole ?? this.channelRole, sharedLocation: sharedLocation ?? this.sharedLocation, deletedForMe: deletedForMe ?? this.deletedForMe, + cid: cid ?? this.cid, + html: html ?? this.html, + mml: mml ?? this.mml, + deletedReplyCount: deletedReplyCount ?? this.deletedReplyCount, ); } @@ -600,7 +618,7 @@ class Message extends Equatable { pinnedBy: other.pinnedBy, poll: other.poll, pollId: other.pollId, - extraData: other.extraData, + custom: other.custom, state: other.state, i18n: other.i18n, restrictedVisibility: other.restrictedVisibility, @@ -610,6 +628,10 @@ class Message extends Equatable { channelRole: other.channelRole, sharedLocation: other.sharedLocation, deletedForMe: other.deletedForMe, + cid: other.cid, + html: other.html, + mml: other.mml, + deletedReplyCount: other.deletedReplyCount, ); } @@ -713,7 +735,7 @@ class Message extends Equatable { pinnedBy, poll, pollId, - extraData, + custom, state, i18n, restrictedVisibility, @@ -723,6 +745,10 @@ class Message extends Equatable { channelRole, sharedLocation, deletedForMe, + cid, + html, + mml, + deletedReplyCount, ]; } @@ -744,11 +770,11 @@ class MessageSearchFilterField extends FilterField { /// value off an instance with [value]. MessageSearchFilterField(super.remote, super.value); - /// Creates a field the SDK does not model, read from [Message.extraData]. + /// Creates a field the SDK does not model, read from [Message.custom]. /// /// **Supported operators:** `$eq`, `$in`, `$gt`, `$gte`, `$lt`, `$lte` factory MessageSearchFilterField.custom(String remote) { - return MessageSearchFilterField(remote, (it) => it.extraData[remote]); + return MessageSearchFilterField(remote, (it) => it.custom[remote]); } /// Filters messages by their id. @@ -887,11 +913,11 @@ class MessageSearchSortField extends SortField { /// For a name the SDK has not modelled; prefer the fields declared here. MessageSearchSortField(super.remote, super.localValue); - /// Creates a field the SDK does not model, read from [Message.extraData]. + /// Creates a field the SDK does not model, read from [Message.custom]. /// /// Declared only where the API accepts a custom sort field. factory MessageSearchSortField.custom(String remote) { - return MessageSearchSortField(remote, (it) => it.extraData[remote]); + return MessageSearchSortField(remote, (it) => it.custom[remote]); } /// Sorts messages by their unique ID. diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index ad14f0dda3..d266efbb65 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -23,14 +23,12 @@ Message _$MessageFromJson(Map json) => Message( const [], silent: json['silent'] as bool? ?? false, shadowed: json['shadowed'] as bool? ?? false, - reactionGroups: (Message._reactionGroupsReadValue(json, 'reaction_groups') as Map?)?.map( - (k, e) => MapEntry(k, ReactionGroup.fromJson(e as Map)), - ), + reactionGroups: reactionGroupsFromV1Json(json['reaction_groups'] as Map?), latestReactions: (json['latest_reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) + ?.map((e) => const ReactionV1JsonConverter().fromJson(e as Map)) .toList(), ownReactions: (json['own_reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) + ?.map((e) => const ReactionV1JsonConverter().fromJson(e as Map)) .toList(), parentId: json['parent_id'] as String?, quotedMessage: json['quoted_message'] == null @@ -57,18 +55,24 @@ Message _$MessageFromJson(Map json) => Message( pinnedBy: json['pinned_by'] == null ? null : User.fromJson(json['pinned_by'] as Map), poll: _$JsonConverterFromJson, Poll>(json['poll'], const PollV1JsonConverter().fromJson), pollId: json['poll_id'] as String?, - extraData: json['extra_data'] as Map? ?? const {}, + custom: json['extra_data'] as Map? ?? const {}, i18n: (json['i18n'] as Map?)?.map((k, e) => MapEntry(k, e as String)), restrictedVisibility: (json['restricted_visibility'] as List?)?.map((e) => e as String).toList(), - moderation: Message._moderationReadValue(json, 'moderation') == null - ? null - : Moderation.fromJson(Message._moderationReadValue(json, 'moderation') as Map), + moderation: moderationFromV1Json(Message._moderationReadValue(json, 'moderation') as Map?), draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), - reminder: json['reminder'] == null ? null : MessageReminder.fromJson(json['reminder'] as Map), + reminder: _$JsonConverterFromJson, MessageReminder>( + json['reminder'], + const MessageReminderV1JsonConverter().fromJson, + ), channelRole: Message._channelRoleReadValue(json, 'channel_role') as String?, - sharedLocation: json['shared_location'] == null - ? null - : Location.fromJson(json['shared_location'] as Map), + sharedLocation: _$JsonConverterFromJson, Location>( + json['shared_location'], + const LocationV1JsonConverter().fromJson, + ), + cid: json['cid'] as String?, + html: json['html'] as String?, + mml: json['mml'] as String?, + deletedReplyCount: (json['deleted_reply_count'] as num?)?.toInt(), ); Map _$MessageToJson(Message instance) => { @@ -89,9 +93,15 @@ Map _$MessageToJson(Message instance) => { 'pin_expires': instance.pinExpires?.toIso8601String(), 'poll_id': instance.pollId, 'restricted_visibility': ?instance.restrictedVisibility, - 'shared_location': ?instance.sharedLocation?.toJson(), - 'extra_data': instance.extraData, + 'shared_location': ?_$JsonConverterToJson, Location>( + instance.sharedLocation, + const LocationV1JsonConverter().toJson, + ), + 'extra_data': instance.custom, }; Value? _$JsonConverterFromJson(Object? json, Value? Function(Json json) fromJson) => json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson(Value? value, Json? Function(Value value) toJson) => + value == null ? null : toJson(value); diff --git a/packages/stream_chat/lib/src/core/models/message_reminder.dart b/packages/stream_chat/lib/src/core/models/message_reminder.dart index 77cee97e67..c1ca0ea135 100644 --- a/packages/stream_chat/lib/src/core/models/message_reminder.dart +++ b/packages/stream_chat/lib/src/core/models/message_reminder.dart @@ -1,33 +1,21 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:stream_core/stream_core.dart' show Filter, FilterField, Sort, SortField; import 'channel_model.dart'; import 'message.dart'; import 'user.dart'; -part 'message_reminder.g.dart'; - -class _NullConst { - const _NullConst(); -} - -const _nullConst = _NullConst(); +part 'message_reminder.freezed.dart'; /// {@template messageReminder} -/// A model class representing a message reminder. -/// -/// The [MessageReminder] class represents a marked message that is important -/// to the user. +/// A reminder a user set on a message. /// -/// It can be of two types: -/// 1. **Scheduled Reminder**: (`remindAt != null`) - Used to notify the user -/// about a message after a certain time. -/// 2. **Bookmarks**: (`remindAt == null`) - Used to mark a message for later -/// reference without notification. +/// A reminder with a [remindAt] is scheduled: the user is notified about the +/// message at that time. One without it is a bookmark, which keeps the message +/// for later without a notification. /// {@endtemplate} -@JsonSerializable() -class MessageReminder extends Equatable { +@freezed +class MessageReminder with _$MessageReminder { /// {@macro messageReminder} MessageReminder({ required this.channelCid, @@ -42,98 +30,65 @@ class MessageReminder extends Equatable { }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); - /// Create a new instance from a json - factory MessageReminder.fromJson(Map json) => _$MessageReminderFromJson(json); - - /// The channel CID where the message exists. + /// The full id of the channel holding the message, in the form `type:id`. + @override final String channelCid; - /// The channel where the message exists. - @JsonKey(includeToJson: false) + /// The channel holding the message. + @override final ChannelModel? channel; - /// The ID of the message that is marked as important. + /// The id of the message the reminder is set on. + @override final String messageId; - /// The message that is marked as important. - @JsonKey(includeToJson: false) + /// The message the reminder is set on. + @override final Message? message; - /// The ID of the user who marked the message as important. + /// The id of the user who set the reminder. + @override final String userId; - /// The user who marked the message as important. - @JsonKey(includeToJson: false) + /// The user who set the reminder. + @override final User? user; /// The time at which the user wants to be reminded about the message. /// - /// If `null`, the reminder is a bookmark and no notification will be sent. + /// If `null`, the reminder is a bookmark and no notification is sent. + @override final DateTime? remindAt; /// The date at which the reminder was created. + @override final DateTime createdAt; /// The date at which the reminder was last updated. + @override final DateTime updatedAt; - /// Convert the object to JSON - Map toJson() => _$MessageReminderToJson(this); - - /// Creates a copy of this [Draft] with specified attributes overridden. - MessageReminder copyWith({ - String? channelCid, - ChannelModel? channel, - String? messageId, - Message? message, - String? userId, - User? user, - Object? remindAt = _nullConst, - DateTime? createdAt, - DateTime? updatedAt, - }) { - return MessageReminder( - channelCid: channelCid ?? this.channelCid, - channel: channel ?? this.channel, - messageId: messageId ?? this.messageId, - message: message ?? this.message, - userId: userId ?? this.userId, - user: user ?? this.user, - remindAt: remindAt == _nullConst ? this.remindAt : remindAt as DateTime?, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); - } - - /// Returns a new [MessageReminder] instance that merges the current - /// instance with another [MessageReminder] instance. + /// A copy of this reminder updated with the fields [other] carries. + /// + /// A nullable field [other] leaves null keeps its value here, except + /// [remindAt]: a null one clears it. + /// + /// Every other field takes the value of [other]. If [other] is null, this + /// reminder is returned unchanged. MessageReminder merge(MessageReminder? other) { if (other == null) return this; return copyWith( channelCid: other.channelCid, - channel: other.channel, + channel: other.channel ?? channel, messageId: other.messageId, - message: other.message, + message: other.message ?? message, userId: other.userId, - user: other.user, + user: other.user ?? user, remindAt: other.remindAt, createdAt: other.createdAt, updatedAt: other.updatedAt, ); } - - @override - List get props => [ - channelCid, - channel, - messageId, - message, - userId, - user, - remindAt, - createdAt, - updatedAt, - ]; } /// A filter for a reminder query. diff --git a/packages/stream_chat/lib/src/core/models/message_reminder.freezed.dart b/packages/stream_chat/lib/src/core/models/message_reminder.freezed.dart new file mode 100644 index 0000000000..c37b45e055 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_reminder.freezed.dart @@ -0,0 +1,140 @@ +// 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 'message_reminder.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$MessageReminder { + String get channelCid; + ChannelModel? get channel; + String get messageId; + Message? get message; + String get userId; + User? get user; + DateTime? get remindAt; + DateTime get createdAt; + DateTime get updatedAt; + + /// Create a copy of MessageReminder + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $MessageReminderCopyWith get copyWith => + _$MessageReminderCopyWithImpl(this as MessageReminder, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is MessageReminder && + (identical(other.channelCid, channelCid) || other.channelCid == channelCid) && + (identical(other.channel, channel) || other.channel == channel) && + (identical(other.messageId, messageId) || other.messageId == messageId) && + (identical(other.message, message) || other.message == message) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.user, user) || other.user == user) && + (identical(other.remindAt, remindAt) || other.remindAt == remindAt) && + (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); + } + + @override + int get hashCode => + Object.hash(runtimeType, channelCid, channel, messageId, message, userId, user, remindAt, createdAt, updatedAt); + + @override + String toString() { + return 'MessageReminder(channelCid: $channelCid, channel: $channel, messageId: $messageId, message: $message, userId: $userId, user: $user, remindAt: $remindAt, createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} + +/// @nodoc +abstract mixin class $MessageReminderCopyWith<$Res> { + factory $MessageReminderCopyWith(MessageReminder value, $Res Function(MessageReminder) _then) = + _$MessageReminderCopyWithImpl; + @useResult + $Res call({ + String channelCid, + ChannelModel? channel, + String messageId, + Message? message, + String userId, + User? user, + DateTime? remindAt, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$MessageReminderCopyWithImpl<$Res> implements $MessageReminderCopyWith<$Res> { + _$MessageReminderCopyWithImpl(this._self, this._then); + + final MessageReminder _self; + final $Res Function(MessageReminder) _then; + + /// Create a copy of MessageReminder + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channelCid = null, + Object? channel = freezed, + Object? messageId = null, + Object? message = freezed, + Object? userId = null, + Object? user = freezed, + Object? remindAt = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + MessageReminder( + channelCid: null == channelCid + ? _self.channelCid + : channelCid // ignore: cast_nullable_to_non_nullable + as String, + channel: freezed == channel + ? _self.channel + : channel // ignore: cast_nullable_to_non_nullable + as ChannelModel?, + messageId: null == messageId + ? _self.messageId + : messageId // ignore: cast_nullable_to_non_nullable + as String, + message: freezed == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as Message?, + userId: null == userId + ? _self.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + user: freezed == user + ? _self.user + : user // ignore: cast_nullable_to_non_nullable + as User?, + remindAt: freezed == remindAt + ? _self.remindAt + : remindAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: freezed == createdAt + ? _self.createdAt! + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _self.updatedAt! + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/message_reminder.g.dart b/packages/stream_chat/lib/src/core/models/message_reminder.g.dart deleted file mode 100644 index 2df37be652..0000000000 --- a/packages/stream_chat/lib/src/core/models/message_reminder.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'message_reminder.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -MessageReminder _$MessageReminderFromJson(Map json) => MessageReminder( - channelCid: json['channel_cid'] as String, - channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), - messageId: json['message_id'] as String, - message: json['message'] == null ? null : Message.fromJson(json['message'] as Map), - userId: json['user_id'] as String, - user: json['user'] == null ? null : User.fromJson(json['user'] as Map), - remindAt: json['remind_at'] == null ? null : DateTime.parse(json['remind_at'] as String), - createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), -); - -Map _$MessageReminderToJson(MessageReminder instance) => { - 'channel_cid': instance.channelCid, - 'message_id': instance.messageId, - 'user_id': instance.userId, - 'remind_at': instance.remindAt?.toIso8601String(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), -}; diff --git a/packages/stream_chat/lib/src/core/models/moderation.dart b/packages/stream_chat/lib/src/core/models/moderation.dart index 1706ccd52a..9db0477942 100644 --- a/packages/stream_chat/lib/src/core/models/moderation.dart +++ b/packages/stream_chat/lib/src/core/models/moderation.dart @@ -1,14 +1,11 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -part 'moderation.g.dart'; +part 'moderation.freezed.dart'; -/// {@template moderation} -/// Model holding data for a message moderated by Moderation V1-V2. -/// {@endtemplate} -@JsonSerializable() -class Moderation extends Equatable { - /// {@macro moderation} +/// The outcome of running a message through the moderation system. +@freezed +class Moderation with _$Moderation { + /// Creates a new [Moderation]. const Moderation({ required this.action, required this.originalText, @@ -19,47 +16,33 @@ class Moderation extends Equatable { this.platformCircumvented = false, }); - /// Create a new instance from a json - factory Moderation.fromJson(Map json) => _$ModerationFromJson(json); - /// The action taken by the moderation system. - @JsonKey( - toJson: ModerationAction.toJson, - fromJson: ModerationAction.fromJson, - ) + @override final ModerationAction action; /// The original text of the message. + @override final String originalText; /// The list of harmful text detected in the message. + @override final List? textHarms; /// The list of harmful images detected in the message. + @override final List? imageHarms; /// The blocklist matched by the message. + @override final String? blocklistMatched; /// The semantic filter matched by the message. + @override final String? semanticFilterMatched; - /// true/false if the message triggered the platform circumvention model. - final bool platformCircumvented; - - /// Serialize to json - Map toJson() => _$ModerationToJson(this); - + /// Whether the message triggered the platform circumvention model. @override - List get props => [ - action, - originalText, - textHarms, - imageHarms, - blocklistMatched, - semanticFilterMatched, - platformCircumvented, - ]; + final bool platformCircumvented; } /// The moderation action performed over the message. diff --git a/packages/stream_chat/lib/src/core/models/moderation.freezed.dart b/packages/stream_chat/lib/src/core/models/moderation.freezed.dart new file mode 100644 index 0000000000..0cfd83735f --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/moderation.freezed.dart @@ -0,0 +1,132 @@ +// 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 'moderation.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$Moderation { + ModerationAction get action; + String get originalText; + List? get textHarms; + List? get imageHarms; + String? get blocklistMatched; + String? get semanticFilterMatched; + bool get platformCircumvented; + + /// Create a copy of Moderation + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $ModerationCopyWith get copyWith => _$ModerationCopyWithImpl(this as Moderation, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Moderation && + (identical(other.action, action) || other.action == action) && + (identical(other.originalText, originalText) || other.originalText == originalText) && + const DeepCollectionEquality().equals(other.textHarms, textHarms) && + const DeepCollectionEquality().equals(other.imageHarms, imageHarms) && + (identical(other.blocklistMatched, blocklistMatched) || other.blocklistMatched == blocklistMatched) && + (identical(other.semanticFilterMatched, semanticFilterMatched) || + other.semanticFilterMatched == semanticFilterMatched) && + (identical(other.platformCircumvented, platformCircumvented) || + other.platformCircumvented == platformCircumvented)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + action, + originalText, + const DeepCollectionEquality().hash(textHarms), + const DeepCollectionEquality().hash(imageHarms), + blocklistMatched, + semanticFilterMatched, + platformCircumvented, + ); + + @override + String toString() { + return 'Moderation(action: $action, originalText: $originalText, textHarms: $textHarms, imageHarms: $imageHarms, blocklistMatched: $blocklistMatched, semanticFilterMatched: $semanticFilterMatched, platformCircumvented: $platformCircumvented)'; + } +} + +/// @nodoc +abstract mixin class $ModerationCopyWith<$Res> { + factory $ModerationCopyWith(Moderation value, $Res Function(Moderation) _then) = _$ModerationCopyWithImpl; + @useResult + $Res call({ + ModerationAction action, + String originalText, + List? textHarms, + List? imageHarms, + String? blocklistMatched, + String? semanticFilterMatched, + bool platformCircumvented, + }); +} + +/// @nodoc +class _$ModerationCopyWithImpl<$Res> implements $ModerationCopyWith<$Res> { + _$ModerationCopyWithImpl(this._self, this._then); + + final Moderation _self; + final $Res Function(Moderation) _then; + + /// Create a copy of Moderation + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? action = null, + Object? originalText = null, + Object? textHarms = freezed, + Object? imageHarms = freezed, + Object? blocklistMatched = freezed, + Object? semanticFilterMatched = freezed, + Object? platformCircumvented = null, + }) { + return _then( + Moderation( + action: null == action + ? _self.action + : action // ignore: cast_nullable_to_non_nullable + as ModerationAction, + originalText: null == originalText + ? _self.originalText + : originalText // ignore: cast_nullable_to_non_nullable + as String, + textHarms: freezed == textHarms + ? _self.textHarms + : textHarms // ignore: cast_nullable_to_non_nullable + as List?, + imageHarms: freezed == imageHarms + ? _self.imageHarms + : imageHarms // ignore: cast_nullable_to_non_nullable + as List?, + blocklistMatched: freezed == blocklistMatched + ? _self.blocklistMatched + : blocklistMatched // ignore: cast_nullable_to_non_nullable + as String?, + semanticFilterMatched: freezed == semanticFilterMatched + ? _self.semanticFilterMatched + : semanticFilterMatched // ignore: cast_nullable_to_non_nullable + as String?, + platformCircumvented: null == platformCircumvented + ? _self.platformCircumvented + : platformCircumvented // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/moderation.g.dart b/packages/stream_chat/lib/src/core/models/moderation.g.dart deleted file mode 100644 index f287d23d84..0000000000 --- a/packages/stream_chat/lib/src/core/models/moderation.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'moderation.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Moderation _$ModerationFromJson(Map json) => Moderation( - action: ModerationAction.fromJson(json['action'] as String), - originalText: json['original_text'] as String, - textHarms: (json['text_harms'] as List?)?.map((e) => e as String).toList(), - imageHarms: (json['image_harms'] as List?)?.map((e) => e as String).toList(), - blocklistMatched: json['blocklist_matched'] as String?, - semanticFilterMatched: json['semantic_filter_matched'] as String?, - platformCircumvented: json['platform_circumvented'] as bool? ?? false, -); - -Map _$ModerationToJson(Moderation instance) => { - 'action': ModerationAction.toJson(instance.action), - 'original_text': instance.originalText, - 'text_harms': instance.textHarms, - 'image_harms': instance.imageHarms, - 'blocklist_matched': instance.blocklistMatched, - 'semantic_filter_matched': instance.semanticFilterMatched, - 'platform_circumvented': instance.platformCircumvented, -}; diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index 2b0cf5f3e3..479b0d9b06 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,16 +1,17 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:stream_core/stream_core.dart' show Filter, FilterField, Sort, SortField; -import '../util/serializer.dart'; import 'user.dart'; -part 'reaction.g.dart'; +part 'reaction.freezed.dart'; -/// The class that defines a reaction -@JsonSerializable() -class Reaction extends Equatable { - /// Constructor used for json serialization +/// A reaction a user added to a message. +@freezed +class Reaction with _$Reaction { + /// Creates a new [Reaction]. + /// + /// [userId] defaults to the id of [user], and [createdAt] and [updatedAt] + /// default to now. Reaction({ this.messageId, required this.type, @@ -20,118 +21,63 @@ class Reaction extends Equatable { this.emojiCode, DateTime? createdAt, DateTime? updatedAt, - this.extraData = const {}, + this.custom = const {}, }) : userId = userId ?? user?.id, createdAt = createdAt ?? DateTime.timestamp(), updatedAt = updatedAt ?? DateTime.timestamp(); - /// Create a new instance from a json - factory Reaction.fromJson(Map json) => _$ReactionFromJson( - Serializer.moveToExtraDataFromRoot( - json, - topLevelFields, - ), - ); - - /// The messageId to which the reaction belongs - @JsonKey(includeToJson: false) + /// The id of the message the reaction is on. + @override final String? messageId; - /// The type of the reaction + /// The kind of reaction, such as `like` or `love`. + @override final String type; - /// The score of the reaction (ie. number of reactions sent) + /// How many times the user sent this reaction. + @override final int score; - /// The emoji code of the reaction (used for notifications) - @JsonKey(includeIfNull: false) + /// The emoji that stands for the reaction, such as `❤️`. + @override final String? emojiCode; - /// The user that sent the reaction - @JsonKey(includeToJson: false) + /// The user who sent the reaction. + @override final User? user; - /// The userId that sent the reaction - @JsonKey(includeToJson: false) + /// The id of the user who sent the reaction. + @override final String? userId; - /// The date of the reaction - @JsonKey(includeToJson: false) + /// The date at which the reaction was sent. + @override final DateTime createdAt; - /// The date of the reaction update - @JsonKey(includeToJson: false) + /// The date at which the reaction was last updated. + @override final DateTime updatedAt; - /// Reaction custom extraData - final Map extraData; - - /// Map of custom user extraData - static const topLevelFields = [ - 'message_id', - 'type', - 'user', - 'user_id', - 'score', - 'emoji_code', - 'created_at', - 'updated_at', - ]; - - /// Serialize to json - Map toJson() => Serializer.moveFromExtraDataToRoot( - _$ReactionToJson(this), - ); - - /// Creates a copy of [Reaction] with specified attributes overridden. - Reaction copyWith({ - String? messageId, - String? type, - User? user, - String? userId, - int? score, - String? emojiCode, - DateTime? createdAt, - DateTime? updatedAt, - Map? extraData, - }) => Reaction( - messageId: messageId ?? this.messageId, - type: type ?? this.type, - user: user ?? this.user, - userId: userId ?? this.userId, - score: score ?? this.score, - emojiCode: emojiCode ?? this.emojiCode, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - extraData: extraData ?? this.extraData, - ); + /// This reaction's custom data. + @override + final Map custom; - /// Returns a new [Reaction] that is a combination of this reaction and the - /// given [other] reaction. + /// A copy of this reaction updated with the fields [other] carries. + /// + /// A nullable field [other] leaves null keeps its value here. + /// + /// Every other field, [custom] included, takes the value of [other]. Reaction merge(Reaction other) => copyWith( - messageId: other.messageId, + messageId: other.messageId ?? messageId, type: other.type, - user: other.user, - userId: other.userId, + user: other.user ?? user, + userId: other.userId ?? userId, score: other.score, - emojiCode: other.emojiCode, + emojiCode: other.emojiCode ?? emojiCode, createdAt: other.createdAt, updatedAt: other.updatedAt, - extraData: other.extraData, + custom: other.custom, ); - - @override - List get props => [ - messageId, - type, - user, - userId, - score, - emojiCode, - createdAt, - updatedAt, - extraData, - ]; } /// A filter for a reaction query. diff --git a/packages/stream_chat/lib/src/core/models/reaction.freezed.dart b/packages/stream_chat/lib/src/core/models/reaction.freezed.dart new file mode 100644 index 0000000000..35f5daeead --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/reaction.freezed.dart @@ -0,0 +1,148 @@ +// 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 'reaction.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$Reaction { + String? get messageId; + String get type; + int get score; + String? get emojiCode; + User? get user; + String? get userId; + DateTime get createdAt; + DateTime get updatedAt; + Map get custom; + + /// Create a copy of Reaction + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $ReactionCopyWith get copyWith => _$ReactionCopyWithImpl(this as Reaction, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Reaction && + (identical(other.messageId, messageId) || other.messageId == messageId) && + (identical(other.type, type) || other.type == type) && + (identical(other.score, score) || other.score == score) && + (identical(other.emojiCode, emojiCode) || other.emojiCode == emojiCode) && + (identical(other.user, user) || other.user == user) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals(other.custom, custom)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + messageId, + type, + score, + emojiCode, + user, + userId, + createdAt, + updatedAt, + const DeepCollectionEquality().hash(custom), + ); + + @override + String toString() { + return 'Reaction(messageId: $messageId, type: $type, score: $score, emojiCode: $emojiCode, user: $user, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt, custom: $custom)'; + } +} + +/// @nodoc +abstract mixin class $ReactionCopyWith<$Res> { + factory $ReactionCopyWith(Reaction value, $Res Function(Reaction) _then) = _$ReactionCopyWithImpl; + @useResult + $Res call({ + String? messageId, + String type, + User? user, + String? userId, + int score, + String? emojiCode, + DateTime? createdAt, + DateTime? updatedAt, + Map custom, + }); +} + +/// @nodoc +class _$ReactionCopyWithImpl<$Res> implements $ReactionCopyWith<$Res> { + _$ReactionCopyWithImpl(this._self, this._then); + + final Reaction _self; + final $Res Function(Reaction) _then; + + /// Create a copy of Reaction + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? messageId = freezed, + Object? type = null, + Object? user = freezed, + Object? userId = freezed, + Object? score = null, + Object? emojiCode = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? custom = null, + }) { + return _then( + Reaction( + messageId: freezed == messageId + ? _self.messageId + : messageId // ignore: cast_nullable_to_non_nullable + as String?, + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as String, + user: freezed == user + ? _self.user + : user // ignore: cast_nullable_to_non_nullable + as User?, + userId: freezed == userId + ? _self.userId + : userId // ignore: cast_nullable_to_non_nullable + as String?, + score: null == score + ? _self.score + : score // ignore: cast_nullable_to_non_nullable + as int, + emojiCode: freezed == emojiCode + ? _self.emojiCode + : emojiCode // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _self.createdAt! + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _self.updatedAt! + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + custom: null == custom + ? _self.custom + : custom // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/reaction.g.dart b/packages/stream_chat/lib/src/core/models/reaction.g.dart deleted file mode 100644 index a3005bd298..0000000000 --- a/packages/stream_chat/lib/src/core/models/reaction.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'reaction.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Reaction _$ReactionFromJson(Map json) => Reaction( - messageId: json['message_id'] as String?, - type: json['type'] as String, - user: json['user'] == null ? null : User.fromJson(json['user'] as Map), - userId: json['user_id'] as String?, - score: (json['score'] as num?)?.toInt() ?? 1, - emojiCode: json['emoji_code'] as String?, - createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), - extraData: json['extra_data'] as Map? ?? const {}, -); - -Map _$ReactionToJson(Reaction instance) => { - 'type': instance.type, - 'score': instance.score, - 'emoji_code': ?instance.emojiCode, - 'extra_data': instance.extraData, -}; diff --git a/packages/stream_chat/lib/src/core/models/reaction_group.dart b/packages/stream_chat/lib/src/core/models/reaction_group.dart index 9be6a5150b..023011f2a9 100644 --- a/packages/stream_chat/lib/src/core/models/reaction_group.dart +++ b/packages/stream_chat/lib/src/core/models/reaction_group.dart @@ -1,12 +1,18 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../db/data_serializable.dart'; + +part 'reaction_group.freezed.dart'; part 'reaction_group.g.dart'; -/// A model class representing a reaction group. -@JsonSerializable() -class ReactionGroup extends Equatable { - /// Create a new instance of [ReactionGroup]. +/// The reactions of one type on a message, counted and scored together. +@freezed +// TODO(openapi-migration): remove in group 10 +@DataSerializable() +class ReactionGroup with _$ReactionGroup { + /// Creates a new [ReactionGroup]. + /// + /// [firstReactionAt] and [lastReactionAt] default to now. ReactionGroup({ this.count = 0, this.sumScores = 0, @@ -15,46 +21,32 @@ class ReactionGroup extends Equatable { }) : firstReactionAt = firstReactionAt ?? DateTime.timestamp(), lastReactionAt = lastReactionAt ?? DateTime.timestamp(); - /// Create a new instance from a json - factory ReactionGroup.fromJson(Map json) => _$ReactionGroupFromJson(json); + /// Creates a [ReactionGroup] from the offline-database format written by + /// [toData]. + /// + /// It is not a codec for API payloads. + factory ReactionGroup.fromData(Map json) => _$ReactionGroupFromJson(json); /// The number of users that reacted with this reaction. + @override final int count; /// The sum of scores of all reactions in this group. + @override final int sumScores; /// The date of the first reaction in this group. + @override final DateTime firstReactionAt; /// The date of the last reaction in this group. + @override final DateTime lastReactionAt; - /// Serialize to json - Map toJson() => _$ReactionGroupToJson(this); - - /// Creates a copy of [Reaction] with specified attributes overridden. - ReactionGroup copyWith({ - int? count, - int? sumScores, - DateTime? firstReactionAt, - DateTime? lastReactionAt, - }) { - return ReactionGroup( - count: count ?? this.count, - sumScores: sumScores ?? this.sumScores, - firstReactionAt: firstReactionAt ?? this.firstReactionAt, - lastReactionAt: lastReactionAt ?? this.lastReactionAt, - ); - } - - @override - List get props => [ - count, - sumScores, - firstReactionAt, - lastReactionAt, - ]; + /// Serializes this group to the format `stream_chat_persistence` stores. + /// + /// It is not a codec for API payloads. + Map toData() => _$ReactionGroupToJson(this); } /// A group of comparators for sorting [ReactionGroup]s. diff --git a/packages/stream_chat/lib/src/core/models/reaction_group.freezed.dart b/packages/stream_chat/lib/src/core/models/reaction_group.freezed.dart new file mode 100644 index 0000000000..7c5796b187 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/reaction_group.freezed.dart @@ -0,0 +1,93 @@ +// 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 'reaction_group.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$ReactionGroup { + int get count; + int get sumScores; + DateTime get firstReactionAt; + DateTime get lastReactionAt; + + /// Create a copy of ReactionGroup + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $ReactionGroupCopyWith get copyWith => + _$ReactionGroupCopyWithImpl(this as ReactionGroup, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is ReactionGroup && + (identical(other.count, count) || other.count == count) && + (identical(other.sumScores, sumScores) || other.sumScores == sumScores) && + (identical(other.firstReactionAt, firstReactionAt) || other.firstReactionAt == firstReactionAt) && + (identical(other.lastReactionAt, lastReactionAt) || other.lastReactionAt == lastReactionAt)); + } + + @override + int get hashCode => Object.hash(runtimeType, count, sumScores, firstReactionAt, lastReactionAt); + + @override + String toString() { + return 'ReactionGroup(count: $count, sumScores: $sumScores, firstReactionAt: $firstReactionAt, lastReactionAt: $lastReactionAt)'; + } +} + +/// @nodoc +abstract mixin class $ReactionGroupCopyWith<$Res> { + factory $ReactionGroupCopyWith(ReactionGroup value, $Res Function(ReactionGroup) _then) = _$ReactionGroupCopyWithImpl; + @useResult + $Res call({int count, int sumScores, DateTime? firstReactionAt, DateTime? lastReactionAt}); +} + +/// @nodoc +class _$ReactionGroupCopyWithImpl<$Res> implements $ReactionGroupCopyWith<$Res> { + _$ReactionGroupCopyWithImpl(this._self, this._then); + + final ReactionGroup _self; + final $Res Function(ReactionGroup) _then; + + /// Create a copy of ReactionGroup + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? count = null, + Object? sumScores = null, + Object? firstReactionAt = freezed, + Object? lastReactionAt = freezed, + }) { + return _then( + ReactionGroup( + count: null == count + ? _self.count + : count // ignore: cast_nullable_to_non_nullable + as int, + sumScores: null == sumScores + ? _self.sumScores + : sumScores // ignore: cast_nullable_to_non_nullable + as int, + firstReactionAt: freezed == firstReactionAt + ? _self.firstReactionAt! + : firstReactionAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastReactionAt: freezed == lastReactionAt + ? _self.lastReactionAt! + : lastReactionAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/create_reminder_response.dart b/packages/stream_chat/lib/src/core/models/response/create_reminder_response.dart new file mode 100644 index 0000000000..5101e31768 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/create_reminder_response.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../message_reminder.dart'; + +part 'create_reminder_response.freezed.dart'; + +/// The result of creating a reminder on a message. +/// +/// See also: +/// +/// * [StreamChatClient.createReminder] and [Channel.createReminder], which +/// return it. +@freezed +class CreateReminderResponse with _$CreateReminderResponse { + /// Creates a new [CreateReminderResponse]. + const CreateReminderResponse({ + required this.duration, + required this.reminder, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The reminder that was created. + @override + final MessageReminder reminder; +} diff --git a/packages/stream_chat/lib/src/core/models/response/create_reminder_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/create_reminder_response.freezed.dart new file mode 100644 index 0000000000..7daeb031d5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/create_reminder_response.freezed.dart @@ -0,0 +1,77 @@ +// 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 'create_reminder_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$CreateReminderResponse { + String get duration; + MessageReminder get reminder; + + /// Create a copy of CreateReminderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $CreateReminderResponseCopyWith get copyWith => + _$CreateReminderResponseCopyWithImpl(this as CreateReminderResponse, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is CreateReminderResponse && + (identical(other.duration, duration) || other.duration == duration) && + (identical(other.reminder, reminder) || other.reminder == reminder)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration, reminder); + + @override + String toString() { + return 'CreateReminderResponse(duration: $duration, reminder: $reminder)'; + } +} + +/// @nodoc +abstract mixin class $CreateReminderResponseCopyWith<$Res> { + factory $CreateReminderResponseCopyWith(CreateReminderResponse value, $Res Function(CreateReminderResponse) _then) = + _$CreateReminderResponseCopyWithImpl; + @useResult + $Res call({String duration, MessageReminder reminder}); +} + +/// @nodoc +class _$CreateReminderResponseCopyWithImpl<$Res> implements $CreateReminderResponseCopyWith<$Res> { + _$CreateReminderResponseCopyWithImpl(this._self, this._then); + + final CreateReminderResponse _self; + final $Res Function(CreateReminderResponse) _then; + + /// Create a copy of CreateReminderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null, Object? reminder = null}) { + return _then( + CreateReminderResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + reminder: null == reminder + ? _self.reminder + : reminder // ignore: cast_nullable_to_non_nullable + as MessageReminder, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/delete_reminder_response.dart b/packages/stream_chat/lib/src/core/models/response/delete_reminder_response.dart new file mode 100644 index 0000000000..86369778e2 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/delete_reminder_response.dart @@ -0,0 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'delete_reminder_response.freezed.dart'; + +/// The result of deleting the reminder on a message. +/// +/// See also: +/// +/// * [StreamChatClient.deleteReminder] and [Channel.deleteReminder], which +/// return it. +@freezed +class DeleteReminderResponse with _$DeleteReminderResponse { + /// Creates a new [DeleteReminderResponse]. + const DeleteReminderResponse({ + required this.duration, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; +} diff --git a/packages/stream_chat/lib/src/core/models/response/delete_reminder_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/delete_reminder_response.freezed.dart new file mode 100644 index 0000000000..17915e8658 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/delete_reminder_response.freezed.dart @@ -0,0 +1,71 @@ +// 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 'delete_reminder_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$DeleteReminderResponse { + String get duration; + + /// Create a copy of DeleteReminderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $DeleteReminderResponseCopyWith get copyWith => + _$DeleteReminderResponseCopyWithImpl(this as DeleteReminderResponse, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is DeleteReminderResponse && + (identical(other.duration, duration) || other.duration == duration)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration); + + @override + String toString() { + return 'DeleteReminderResponse(duration: $duration)'; + } +} + +/// @nodoc +abstract mixin class $DeleteReminderResponseCopyWith<$Res> { + factory $DeleteReminderResponseCopyWith(DeleteReminderResponse value, $Res Function(DeleteReminderResponse) _then) = + _$DeleteReminderResponseCopyWithImpl; + @useResult + $Res call({String duration}); +} + +/// @nodoc +class _$DeleteReminderResponseCopyWithImpl<$Res> implements $DeleteReminderResponseCopyWith<$Res> { + _$DeleteReminderResponseCopyWithImpl(this._self, this._then); + + final DeleteReminderResponse _self; + final $Res Function(DeleteReminderResponse) _then; + + /// Create a copy of DeleteReminderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null}) { + return _then( + DeleteReminderResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/query_reminders_response.dart b/packages/stream_chat/lib/src/core/models/response/query_reminders_response.dart new file mode 100644 index 0000000000..50029605a8 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/query_reminders_response.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../message_reminder.dart'; + +part 'query_reminders_response.freezed.dart'; + +/// One page of the current user's reminders matching a query. +/// +/// See also: +/// +/// * [StreamChatClient.queryReminders], which returns it. +@freezed +class QueryRemindersResponse with _$QueryRemindersResponse { + /// Creates a new [QueryRemindersResponse]. + const QueryRemindersResponse({ + required this.duration, + required this.reminders, + this.next, + this.prev, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The reminders on this page. + @override + final List reminders; + + /// The cursor for the next page, or null on the last page. + @override + final String? next; + + /// The cursor for the previous page, or null on the first page. + @override + final String? prev; +} diff --git a/packages/stream_chat/lib/src/core/models/response/query_reminders_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/query_reminders_response.freezed.dart new file mode 100644 index 0000000000..fa56d413ae --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/query_reminders_response.freezed.dart @@ -0,0 +1,89 @@ +// 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 'query_reminders_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$QueryRemindersResponse { + String get duration; + List get reminders; + String? get next; + String? get prev; + + /// Create a copy of QueryRemindersResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $QueryRemindersResponseCopyWith get copyWith => + _$QueryRemindersResponseCopyWithImpl(this as QueryRemindersResponse, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is QueryRemindersResponse && + (identical(other.duration, duration) || other.duration == duration) && + const DeepCollectionEquality().equals(other.reminders, reminders) && + (identical(other.next, next) || other.next == next) && + (identical(other.prev, prev) || other.prev == prev)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration, const DeepCollectionEquality().hash(reminders), next, prev); + + @override + String toString() { + return 'QueryRemindersResponse(duration: $duration, reminders: $reminders, next: $next, prev: $prev)'; + } +} + +/// @nodoc +abstract mixin class $QueryRemindersResponseCopyWith<$Res> { + factory $QueryRemindersResponseCopyWith(QueryRemindersResponse value, $Res Function(QueryRemindersResponse) _then) = + _$QueryRemindersResponseCopyWithImpl; + @useResult + $Res call({String duration, List reminders, String? next, String? prev}); +} + +/// @nodoc +class _$QueryRemindersResponseCopyWithImpl<$Res> implements $QueryRemindersResponseCopyWith<$Res> { + _$QueryRemindersResponseCopyWithImpl(this._self, this._then); + + final QueryRemindersResponse _self; + final $Res Function(QueryRemindersResponse) _then; + + /// Create a copy of QueryRemindersResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null, Object? reminders = null, Object? next = freezed, Object? prev = freezed}) { + return _then( + QueryRemindersResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + reminders: null == reminders + ? _self.reminders + : reminders // ignore: cast_nullable_to_non_nullable + as List, + next: freezed == next + ? _self.next + : next // ignore: cast_nullable_to_non_nullable + as String?, + prev: freezed == prev + ? _self.prev + : prev // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/models/response/update_reminder_response.dart b/packages/stream_chat/lib/src/core/models/response/update_reminder_response.dart new file mode 100644 index 0000000000..d1f49b7859 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/update_reminder_response.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../message_reminder.dart'; + +part 'update_reminder_response.freezed.dart'; + +/// The result of updating the reminder on a message. +/// +/// See also: +/// +/// * [StreamChatClient.updateReminder] and [Channel.updateReminder], which +/// return it. +@freezed +class UpdateReminderResponse with _$UpdateReminderResponse { + /// Creates a new [UpdateReminderResponse]. + const UpdateReminderResponse({ + required this.duration, + required this.reminder, + }); + + /// How long the request took to handle, such as `4.21ms`. + @override + final String duration; + + /// The reminder as it is after the update. + @override + final MessageReminder reminder; +} diff --git a/packages/stream_chat/lib/src/core/models/response/update_reminder_response.freezed.dart b/packages/stream_chat/lib/src/core/models/response/update_reminder_response.freezed.dart new file mode 100644 index 0000000000..061294877e --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/response/update_reminder_response.freezed.dart @@ -0,0 +1,77 @@ +// 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_reminder_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +/// @nodoc +mixin _$UpdateReminderResponse { + String get duration; + MessageReminder get reminder; + + /// Create a copy of UpdateReminderResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UpdateReminderResponseCopyWith get copyWith => + _$UpdateReminderResponseCopyWithImpl(this as UpdateReminderResponse, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is UpdateReminderResponse && + (identical(other.duration, duration) || other.duration == duration) && + (identical(other.reminder, reminder) || other.reminder == reminder)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration, reminder); + + @override + String toString() { + return 'UpdateReminderResponse(duration: $duration, reminder: $reminder)'; + } +} + +/// @nodoc +abstract mixin class $UpdateReminderResponseCopyWith<$Res> { + factory $UpdateReminderResponseCopyWith(UpdateReminderResponse value, $Res Function(UpdateReminderResponse) _then) = + _$UpdateReminderResponseCopyWithImpl; + @useResult + $Res call({String duration, MessageReminder reminder}); +} + +/// @nodoc +class _$UpdateReminderResponseCopyWithImpl<$Res> implements $UpdateReminderResponseCopyWith<$Res> { + _$UpdateReminderResponseCopyWithImpl(this._self, this._then); + + final UpdateReminderResponse _self; + final $Res Function(UpdateReminderResponse) _then; + + /// Create a copy of UpdateReminderResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? duration = null, Object? reminder = null}) { + return _then( + UpdateReminderResponse( + duration: null == duration + ? _self.duration + : duration // ignore: cast_nullable_to_non_nullable + as String, + reminder: null == reminder + ? _self.reminder + : reminder // ignore: cast_nullable_to_non_nullable + as MessageReminder, + ), + ); + } +} diff --git a/packages/stream_chat/lib/src/core/util/message_decoding.dart b/packages/stream_chat/lib/src/core/util/message_decoding.dart new file mode 100644 index 0000000000..7c14624567 --- /dev/null +++ b/packages/stream_chat/lib/src/core/util/message_decoding.dart @@ -0,0 +1,51 @@ +import '../models/message.dart'; +import '../models/message_state.dart'; +import '../models/reaction_group.dart'; + +/// Derivations every decoder of a received [Message] applies. +extension MessageDecoding on Message { + /// Returns this message with the type and state its flags imply. + /// + /// A message deleted only for the current user reads as a + /// [MessageType.deleted] message in the [MessageState.deletedForMe] state. + /// Otherwise the type is kept, and the state is [MessageState.softDeleted] + /// once the message has a [deletedAt], [MessageState.updated] once it was + /// updated after it was created, and [MessageState.sent] before either. + Message withDerivedState() { + final isDeletedForMe = deletedForMe ?? false; + // TODO: Remove this override once a message deleted only for the current + // user arrives with the deleted type. + final type = isDeletedForMe ? MessageType.deleted : this.type; + final state = switch (this) { + _ when isDeletedForMe => MessageState.deletedForMe, + _ when deletedAt != null => MessageState.softDeleted, + _ when updatedAt.isAfter(createdAt) => MessageState.updated, + _ => MessageState.sent, + }; + + return copyWith(type: type, state: state); + } +} + +/// Builds the reaction groups of a received message from its reaction [counts] +/// and [scores], keyed by reaction type. +/// +/// Returns null when neither has an entry. A type with no positive count is +/// left out, whatever its score. As the dates of the reactions are unknown, +/// each group's first and last reaction date is now. +Map? reactionGroupsFromCounts(Map? counts, Map? scores) { + final reactionTypes = {...?counts?.keys, ...?scores?.keys}; + if (reactionTypes.isEmpty) return null; + + final now = DateTime.timestamp(); + return { + for (final type in reactionTypes) + if ((counts?[type] ?? 0) > 0) + type: ReactionGroup( + count: counts?[type] ?? 0, + sumScores: scores?[type] ?? 0, + firstReactionAt: now, + lastReactionAt: now, + ), + }; +} diff --git a/packages/stream_chat/lib/src/repository/mapper/attachments_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/attachments_mapper.dart new file mode 100644 index 0000000000..ecd06c799c --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/attachments_mapper.dart @@ -0,0 +1,51 @@ +import '../../../open_api/api.dart' as api; +import '../../core/models/action.dart'; +import '../../core/models/attachment.dart'; +import '../../core/models/attachment_file.dart'; + +// TODO(openapi-migration): re-point these mappers in group 10. + +/// Maps a generated [api.Attachment] to an [Attachment]. +extension AttachmentMapper on api.Attachment { + /// Converts this attachment into an [Attachment]. + /// + /// The attachment gets a new local [Attachment.id]; an id it was sent with + /// stays in [Attachment.custom]. + Attachment toModel() => Attachment( + type: type, + titleLink: titleLink, + title: title, + thumbUrl: thumbUrl, + text: text, + pretext: pretext, + ogScrapeUrl: ogScrapeUrl, + imageUrl: imageUrl, + footerIcon: footerIcon, + footer: footer, + fields: fields?.map((field) => field.toJson()).toList(), + fallback: fallback, + color: color, + authorName: authorName, + authorLink: authorLink, + authorIcon: authorIcon, + assetUrl: assetUrl, + actions: actions?.map((action) => action.toModel()).toList() ?? const [], + originalWidth: originalWidth, + originalHeight: originalHeight, + custom: custom, + giphy: giphy?.toJson(), + uploadState: const UploadState.success(), + ); +} + +/// Maps a generated [api.Action] to an [Action]. +extension ActionMapper on api.Action { + /// Converts this action into an [Action]. + Action toModel() => Action( + name: name, + style: style ?? 'default', + text: text, + type: type, + value: value, + ); +} diff --git a/packages/stream_chat/lib/src/repository/mapper/drafts_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/drafts_mapper.dart new file mode 100644 index 0000000000..947f56b1d5 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/drafts_mapper.dart @@ -0,0 +1,42 @@ +import '../../../open_api/api.dart' as api; +import '../../core/models/draft.dart'; +import '../../core/models/draft_message.dart'; +import '../../core/models/message.dart'; +import 'attachments_mapper.dart'; +import 'channels_mapper.dart'; +import 'messages_mapper.dart'; +import 'users_mapper.dart'; + +// TODO(openapi-migration): re-point these mappers in group 10. + +/// Maps a generated [api.DraftResponse] to a [Draft]. +extension DraftResponseMapper on api.DraftResponse { + /// Converts this response into a [Draft]. + Draft toModel() => Draft( + channelCid: channelCid, + createdAt: createdAt, + message: message.toModel(), + channel: channel?.toModel(), + parentId: parentId, + parentMessage: parentMessage?.toModel(), + quotedMessage: quotedMessage?.toModel(), + ); +} + +/// Maps a generated [api.DraftPayloadResponse] to a [DraftMessage]. +extension DraftPayloadResponseMapper on api.DraftPayloadResponse { + /// Converts this response into a [DraftMessage]. + DraftMessage toModel() => DraftMessage( + id: id, + text: text, + type: type ?? MessageType.regular, + attachments: attachments?.map((attachment) => attachment.toModel()).toList() ?? const [], + parentId: parentId, + showInChannel: showInChannel, + mentionedUsers: mentionedUsers?.map((user) => user.toModel()).toList() ?? const [], + quotedMessageId: quotedMessageId, + silent: silent ?? false, + pollId: pollId, + custom: custom, + ); +} diff --git a/packages/stream_chat/lib/src/repository/mapper/locations_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/locations_mapper.dart new file mode 100644 index 0000000000..2cf708ad38 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/locations_mapper.dart @@ -0,0 +1,24 @@ +import '../../../open_api/api.dart' as api; +import '../../core/models/location.dart'; +import 'channels_mapper.dart'; +import 'messages_mapper.dart'; + +// TODO(openapi-migration): re-point this mapper in group 10. + +/// Maps a generated [api.SharedLocationResponseData] to a [Location]. +extension SharedLocationResponseDataMapper on api.SharedLocationResponseData { + /// Converts this response into a [Location]. + Location toModel() => Location( + channelCid: channelCid, + channel: channel?.toModel(), + messageId: messageId, + message: message?.toModel(), + userId: userId, + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); +} diff --git a/packages/stream_chat/lib/src/repository/mapper/messages_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/messages_mapper.dart new file mode 100644 index 0000000000..9d384e0a77 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/messages_mapper.dart @@ -0,0 +1,87 @@ +import '../../../open_api/api.dart' as api; +import '../../core/models/message.dart'; +import '../../core/models/moderation.dart'; +import '../../core/util/message_decoding.dart'; +import 'attachments_mapper.dart'; +import 'drafts_mapper.dart'; +import 'locations_mapper.dart'; +import 'polls_mapper.dart'; +import 'reactions_mapper.dart'; +import 'reminders_mapper.dart'; +import 'user_groups_mapper.dart'; +import 'users_mapper.dart'; + +// TODO(openapi-migration): re-point these mappers in group 10. + +/// Maps a generated [api.MessageResponse] to a [Message]. +extension MessageResponseMapper on api.MessageResponse { + /// Converts this response into a [Message]. + /// + /// The message's type and state follow from whether and when it was deleted + /// or updated. Without reaction groups, they are built from the reaction + /// counts and scores. + Message toModel() => Message( + id: id, + text: text, + type: type, + attachments: [for (final attachment in attachments) attachment.toModel()], + mentionedChannel: mentionedChannel, + mentionedGroupIds: mentionedGroupIds, + mentionedGroups: mentionedGroups?.map((group) => group.toModel()).toList(), + mentionedHere: mentionedHere, + mentionedRoles: mentionedRoles, + mentionedUsers: [for (final user in mentionedUsers) user.toModel()], + silent: silent, + shadowed: shadowed, + reactionGroups: + reactionGroups?.map((type, group) => MapEntry(type, group.toModel())) ?? + reactionGroupsFromCounts(reactionCounts, reactionScores), + latestReactions: [for (final reaction in latestReactions) reaction.toModel()], + ownReactions: [for (final reaction in ownReactions) reaction.toModel()], + parentId: parentId, + quotedMessage: quotedMessage?.toModel(), + quotedMessageId: quotedMessageId, + replyCount: replyCount, + threadParticipants: threadParticipants?.map((user) => user.toModel()).toList(), + showInChannel: showInChannel, + command: command, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + user: user.toModel(), + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedBy: pinnedBy?.toModel(), + poll: poll?.toModel(), + pollId: pollId, + custom: custom, + i18n: i18n, + restrictedVisibility: restrictedVisibility, + moderation: moderation?.toModel(), + draft: draft?.toModel(), + reminder: reminder?.toModel(), + channelRole: member?.channelRole, + sharedLocation: sharedLocation?.toModel(), + cid: cid, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, + ).withDerivedState(); +} + +/// Maps a generated [api.ModerationV2Response] to a [Moderation]. +extension ModerationV2ResponseMapper on api.ModerationV2Response { + /// Converts this response into a [Moderation]. + Moderation toModel() => Moderation( + action: ModerationAction.fromJson(action), + originalText: originalText, + textHarms: textHarms, + imageHarms: imageHarms, + blocklistMatched: blocklistMatched, + semanticFilterMatched: semanticFilterMatched, + platformCircumvented: platformCircumvented ?? false, + ); +} diff --git a/packages/stream_chat/lib/src/repository/mapper/reactions_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/reactions_mapper.dart new file mode 100644 index 0000000000..7ba22cecb2 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/reactions_mapper.dart @@ -0,0 +1,37 @@ +import '../../../open_api/api.dart' as api; +import '../../core/models/reaction.dart'; +import '../../core/models/reaction_group.dart'; +import '../../core/util/extension.dart'; +import 'users_mapper.dart'; + +// TODO(openapi-migration): re-point these mappers in group 10. + +/// Maps a generated [api.ReactionResponse] to a [Reaction]. +extension ReactionResponseMapper on api.ReactionResponse { + /// Converts this response into a [Reaction]. + /// + /// [Reaction.emojiCode] is read from [custom], and [Reaction.custom] still + /// holds it. + Reaction toModel() => Reaction( + messageId: messageId, + type: type, + user: user.toModel(), + userId: userId, + score: score, + emojiCode: custom['emoji_code'].safeCast(), + createdAt: createdAt, + updatedAt: updatedAt, + custom: custom, + ); +} + +/// Maps a generated [api.ReactionGroupResponse] to a [ReactionGroup]. +extension ReactionGroupResponseMapper on api.ReactionGroupResponse { + /// Converts this response into a [ReactionGroup]. + ReactionGroup toModel() => ReactionGroup( + count: count, + sumScores: sumScores, + firstReactionAt: firstReactionAt, + lastReactionAt: lastReactionAt, + ); +} diff --git a/packages/stream_chat/lib/src/repository/mapper/reminders_mapper.dart b/packages/stream_chat/lib/src/repository/mapper/reminders_mapper.dart new file mode 100644 index 0000000000..e37e395805 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/mapper/reminders_mapper.dart @@ -0,0 +1,55 @@ +import '../../../open_api/api.dart' as api; +import '../../core/models/message_reminder.dart'; +import '../../core/models/response/create_reminder_response.dart'; +import '../../core/models/response/delete_reminder_response.dart'; +import '../../core/models/response/query_reminders_response.dart'; +import '../../core/models/response/update_reminder_response.dart'; +import 'channels_mapper.dart'; +import 'messages_mapper.dart'; +import 'users_mapper.dart'; + +/// Maps a generated [api.CreateReminderResponse] to a [CreateReminderResponse]. +extension CreateReminderResponseMapper on api.CreateReminderResponse { + /// Converts this response into a [CreateReminderResponse]. + CreateReminderResponse toModel() => CreateReminderResponse(duration: duration, reminder: reminder.toModel()); +} + +/// Maps a generated [api.UpdateReminderResponse] to an +/// [UpdateReminderResponse]. +extension UpdateReminderResponseMapper on api.UpdateReminderResponse { + /// Converts this response into an [UpdateReminderResponse]. + UpdateReminderResponse toModel() => UpdateReminderResponse(duration: duration, reminder: reminder.toModel()); +} + +/// Maps a generated [api.DeleteReminderResponse] to a [DeleteReminderResponse]. +extension DeleteReminderResponseMapper on api.DeleteReminderResponse { + /// Converts this response into a [DeleteReminderResponse]. + DeleteReminderResponse toModel() => DeleteReminderResponse(duration: duration); +} + +/// Maps a generated [api.QueryRemindersResponse] to a [QueryRemindersResponse]. +extension QueryRemindersResponseMapper on api.QueryRemindersResponse { + /// Converts this response into a [QueryRemindersResponse]. + QueryRemindersResponse toModel() => QueryRemindersResponse( + duration: duration, + reminders: [for (final reminder in reminders) reminder.toModel()], + next: next, + prev: prev, + ); +} + +/// Maps a generated [api.ReminderResponseData] to a [MessageReminder]. +extension ReminderResponseDataMapper on api.ReminderResponseData { + /// Converts this response into a [MessageReminder]. + MessageReminder toModel() => MessageReminder( + channelCid: channelCid, + channel: channel?.toModel(), + messageId: messageId, + message: message?.toModel(), + userId: userId, + user: user?.toModel(), + remindAt: remindAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); +} diff --git a/packages/stream_chat/lib/src/repository/reminders_repository.dart b/packages/stream_chat/lib/src/repository/reminders_repository.dart new file mode 100644 index 0000000000..0b9e8d0cf2 --- /dev/null +++ b/packages/stream_chat/lib/src/repository/reminders_repository.dart @@ -0,0 +1,73 @@ +import 'package:stream_core/stream_core.dart' show PatternMatching, Result; + +import '../../open_api/api.dart' as api; +import '../core/models/message_reminder.dart'; +import '../core/models/response/create_reminder_response.dart'; +import '../core/models/response/delete_reminder_response.dart'; +import '../core/models/response/query_reminders_response.dart'; +import '../core/models/response/update_reminder_response.dart'; +import 'mapper/filter_mapper.dart'; +import 'mapper/reminders_mapper.dart'; +import 'mapper/sort_mapper.dart'; + +/// Repository dedicated to message reminder operations. +class RemindersRepository { + /// Creates a new reminders repository. + const RemindersRepository(this._api); + + final api.DefaultApi _api; + + /// Fetches one page of the current user's reminders matching [filter], + /// ordered by [sort]. + /// + /// [next] and [prev] are the cursors a previous page returned; at most one of + /// them may be given. + Future> queryReminders({ + MessageReminderFilter? filter, + List? sort, + int? limit, + String? next, + String? prev, + }) async { + final result = await _api.queryReminders( + queryRemindersRequest: api.QueryRemindersRequest( + filter: filter?.toRequest(), + sort: sort?.map((it) => it.toRequest()).toList(), + limit: limit, + next: next, + prev: prev, + ), + ); + + return result.map((response) => response.toModel()); + } + + /// Creates a reminder on the message with the id [messageId], due at + /// [remindAt], or a bookmark without it. + Future> createReminder(String messageId, {DateTime? remindAt}) async { + final result = await _api.createReminder( + messageId: messageId, + createReminderRequest: api.CreateReminderRequest(remindAt: remindAt), + ); + + return result.map((response) => response.toModel()); + } + + /// Sets the reminder on the message with the id [messageId] to be due at + /// [remindAt], or turns it into a bookmark without it. + Future> updateReminder(String messageId, {DateTime? remindAt}) async { + final result = await _api.updateReminder( + messageId: messageId, + updateReminderRequest: api.UpdateReminderRequest(remindAt: remindAt), + ); + + return result.map((response) => response.toModel()); + } + + /// Deletes the reminder on the message with the id [messageId]. + Future> deleteReminder(String messageId) async { + final result = await _api.deleteReminder(messageId: messageId); + + return result.map((response) => response.toModel()); + } +} diff --git a/packages/stream_chat/lib/src/ws/events/event.dart b/packages/stream_chat/lib/src/ws/events/event.dart index 6276a7a7d4..d7ed29a176 100644 --- a/packages/stream_chat/lib/src/ws/events/event.dart +++ b/packages/stream_chat/lib/src/ws/events/event.dart @@ -129,6 +129,7 @@ class Event extends WsEvent { final Member? member; /// The reaction sent with the event + @ReactionV1JsonConverter() final Reaction? reaction; /// The number of unread messages for current user @@ -192,6 +193,7 @@ class Event extends WsEvent { final Draft? draft; /// The message reminder sent with the event. + @MessageReminderV1JsonConverter() final MessageReminder? reminder; /// Push notification preferences for the current user. diff --git a/packages/stream_chat/lib/src/ws/events/event.g.dart b/packages/stream_chat/lib/src/ws/events/event.g.dart index 600b7d6bc3..8d489ff901 100644 --- a/packages/stream_chat/lib/src/ws/events/event.g.dart +++ b/packages/stream_chat/lib/src/ws/events/event.g.dart @@ -22,7 +22,10 @@ Event _$EventFromJson(Map json) => Event( ), totalUnreadCount: (json['total_unread_count'] as num?)?.toInt(), unreadChannels: (json['unread_channels'] as num?)?.toInt(), - reaction: json['reaction'] == null ? null : Reaction.fromJson(json['reaction'] as Map), + reaction: _$JsonConverterFromJson, Reaction>( + json['reaction'], + const ReactionV1JsonConverter().fromJson, + ), online: json['online'] as bool?, channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), member: json['member'] == null ? null : Member.fromJson(json['member'] as Map), @@ -48,7 +51,10 @@ Event _$EventFromJson(Map json) => Event( unreadMessages: (json['unread_messages'] as num?)?.toInt(), lastReadMessageId: json['last_read_message_id'] as String?, draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), - reminder: json['reminder'] == null ? null : MessageReminder.fromJson(json['reminder'] as Map), + reminder: _$JsonConverterFromJson, MessageReminder>( + json['reminder'], + const MessageReminderV1JsonConverter().fromJson, + ), pushPreference: json['push_preference'] == null ? null : PushPreference.fromJson(json['push_preference'] as Map), @@ -83,7 +89,10 @@ Map _$EventToJson(Event instance) => { ), 'channel': ?instance.channel?.toJson(), 'member': ?instance.member?.toJson(), - 'reaction': ?instance.reaction?.toJson(), + 'reaction': ?_$JsonConverterToJson, Reaction>( + instance.reaction, + const ReactionV1JsonConverter().toJson, + ), 'total_unread_count': ?instance.totalUnreadCount, 'unread_channels': ?instance.unreadChannels, 'online': ?instance.online, @@ -101,7 +110,10 @@ Map _$EventToJson(Event instance) => { 'unread_messages': ?instance.unreadMessages, 'last_read_message_id': ?instance.lastReadMessageId, 'draft': ?instance.draft?.toJson(), - 'reminder': ?instance.reminder?.toJson(), + 'reminder': ?_$JsonConverterToJson, MessageReminder>( + instance.reminder, + const MessageReminderV1JsonConverter().toJson, + ), 'push_preference': ?instance.pushPreference?.toJson(), 'channel_push_preference': ?instance.channelPushPreference?.toJson(), 'channel_member_count': ?instance.channelMemberCount, diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 22484c7594..a075308785 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -124,8 +124,10 @@ export 'src/core/models/request/update_user_partial_request.dart'; export 'src/core/models/response/add_user_group_members_response.dart'; export 'src/core/models/response/app_settings_response.dart'; export 'src/core/models/response/block_users_response.dart'; +export 'src/core/models/response/create_reminder_response.dart'; export 'src/core/models/response/create_user_group_response.dart'; export 'src/core/models/response/delete_channel_response.dart'; +export 'src/core/models/response/delete_reminder_response.dart'; export 'src/core/models/response/flag_response.dart'; export 'src/core/models/response/get_blocked_users_response.dart'; export 'src/core/models/response/get_unread_count_response.dart'; @@ -142,6 +144,7 @@ export 'src/core/models/response/poll_response.dart'; export 'src/core/models/response/poll_vote_response.dart'; export 'src/core/models/response/query_poll_votes_response.dart'; export 'src/core/models/response/query_polls_response.dart'; +export 'src/core/models/response/query_reminders_response.dart'; 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'; @@ -150,6 +153,7 @@ export 'src/core/models/response/unblock_users_response.dart'; export 'src/core/models/response/unmute_users_response.dart'; export 'src/core/models/response/update_channel_partial_response.dart'; export 'src/core/models/response/update_member_partial_response.dart'; +export 'src/core/models/response/update_reminder_response.dart'; export 'src/core/models/response/update_user_group_response.dart'; export 'src/core/models/response/update_users_response.dart'; export 'src/core/models/role.dart'; diff --git a/packages/stream_chat/test/fixtures/action.json b/packages/stream_chat/test/fixtures/action.json deleted file mode 100644 index 59da5684cd..0000000000 --- a/packages/stream_chat/test/fixtures/action.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "name", - "style": "style", - "text": "text", - "type": "type", - "value": "value" -} \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/attachment.json b/packages/stream_chat/test/fixtures/attachment.json index faf38b9081..f78c0dc429 100644 --- a/packages/stream_chat/test/fixtures/attachment.json +++ b/packages/stream_chat/test/fixtures/attachment.json @@ -3,6 +3,11 @@ "title": "awesome", "title_link": "https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti", "thumb_url": "https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif", + "giphy": { + "original": { + "url": "https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif" + } + }, "actions": [ { "name": "image_action", diff --git a/packages/stream_chat/test/fixtures/message.json b/packages/stream_chat/test/fixtures/message.json index 8dc3b15aea..9559565c97 100644 --- a/packages/stream_chat/test/fixtures/message.json +++ b/packages/stream_chat/test/fixtures/message.json @@ -4,6 +4,10 @@ "type": "regular", "silent": false, "status": "SENT", + "cid": "messaging:general", + "html": "

Hi

", + "mml": "Hi", + "deleted_reply_count": 2, "user": { "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", "role": "user", 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 f696d67594..fbfe12fe64 100644 --- a/packages/stream_chat/test/src/client/channel/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel/channel_test.dart @@ -1256,7 +1256,7 @@ void main() { ..message = Message( id: locationId, text: 'Location shared', - extraData: const {'custom': 'data'}, + custom: const {'custom': 'data'}, sharedLocation: Location( channelCid: channel.cid, messageId: locationId, @@ -1273,13 +1273,13 @@ void main() { messageText: 'Location shared', createdByDeviceId: deviceId, location: coordinates, - extraData: {'custom': 'data'}, + custom: {'custom': 'data'}, ); expect(response, isNotNull); expect(response.message.id, locationId); expect(response.message.text, 'Location shared'); - expect(response.message.extraData['custom'], 'data'); + expect(response.message.custom['custom'], 'data'); expect(response.message.sharedLocation, isNotNull); verify( @@ -1307,7 +1307,7 @@ void main() { ..message = Message( id: locationId, text: 'Location shared', - extraData: const {'custom': 'data'}, + custom: const {'custom': 'data'}, sharedLocation: Location( channelCid: channel.cid, messageId: locationId, @@ -1326,13 +1326,13 @@ void main() { createdByDeviceId: deviceId, location: coordinates, endSharingAt: endSharingAt, - extraData: {'custom': 'data'}, + custom: {'custom': 'data'}, ); expect(response, isNotNull); expect(response.message.id, locationId); expect(response.message.text, 'Location shared'); - expect(response.message.extraData['custom'], 'data'); + expect(response.message.custom['custom'], 'data'); expect(response.message.sharedLocation, isNotNull); expect(response.message.sharedLocation?.endAt, endSharingAt); @@ -1470,118 +1470,77 @@ void main() { }); }); - group('`.createReminder`', () { + test("Channel.createReminder sends the due date and returns the client's createReminder response", () async { const messageId = 'test-message-id'; + final remindAt = DateTime.utc(2024, 6, 15, 14, 30); + final response = CreateReminderResponse( + duration: '0.01ms', + reminder: MessageReminder(channelCid: channelCid, messageId: messageId, userId: 'test-user-id'), + ); + when( + () => client.createReminder(messageId, remindAt: remindAt), + ).thenAnswer((_) async => Result.success(response)); - setUp(() { - when( - () => client.createReminder( - messageId, - remindAt: any(named: 'remindAt'), - ), - ).thenAnswer( - (_) async => CreateReminderResponse() - ..reminder = MessageReminder( - messageId: messageId, - channelCid: channelCid, - userId: 'test-user-id', - remindAt: DateTime(2024, 6, 15, 14, 30), - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ), - ); - }); - - test('should call client.createReminder', () async { - final res = await channel.createReminder(messageId); - - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); + final res = await channel.createReminder(messageId, remindAt: remindAt); - verify(() => channel.client.createReminder(messageId)).called(1); - }); + expect(res.getOrNull(), response); + verify(() => client.createReminder(messageId, remindAt: remindAt)).called(1); + }); - test('with remindAt should pass remindAt to client', () async { - final remindAt = DateTime(2024, 6, 15, 14, 30); - final res = await channel.createReminder(messageId, remindAt: remindAt); + test("Channel.createReminder sends no due date and returns the client's createReminder response", () async { + const messageId = 'test-message-id'; + final response = CreateReminderResponse( + duration: '0.01ms', + reminder: MessageReminder(channelCid: channelCid, messageId: messageId, userId: 'test-user-id'), + ); + when(() => client.createReminder(messageId)).thenAnswer((_) async => Result.success(response)); - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); - expect(res.reminder.remindAt, remindAt); + final res = await channel.createReminder(messageId); - verify( - () => channel.client.createReminder( - messageId, - remindAt: remindAt, - ), - ).called(1); - }); + expect(res.getOrNull(), response); + verify(() => client.createReminder(messageId)).called(1); }); - group('`.updateReminder`', () { + test("Channel.updateReminder sends the due date and returns the client's updateReminder response", () async { const messageId = 'test-message-id'; + final remindAt = DateTime.utc(2024, 8, 20, 16, 45); + final response = UpdateReminderResponse( + duration: '0.01ms', + reminder: MessageReminder(channelCid: channelCid, messageId: messageId, userId: 'test-user-id'), + ); + when( + () => client.updateReminder(messageId, remindAt: remindAt), + ).thenAnswer((_) async => Result.success(response)); - setUp(() { - when( - () => client.updateReminder( - messageId, - remindAt: any(named: 'remindAt'), - ), - ).thenAnswer( - (_) async => UpdateReminderResponse() - ..reminder = MessageReminder( - messageId: messageId, - channelCid: channelCid, - userId: 'test-user-id', - remindAt: DateTime(2024, 8, 20, 16, 45), - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ), - ); - }); - - test('should call client.updateReminder', () async { - final res = await channel.updateReminder(messageId); - - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); + final res = await channel.updateReminder(messageId, remindAt: remindAt); - verify(() => channel.client.updateReminder(messageId)).called(1); - }); + expect(res.getOrNull(), response); + verify(() => client.updateReminder(messageId, remindAt: remindAt)).called(1); + }); - test('with remindAt should pass remindAt to client', () async { - final remindAt = DateTime(2024, 8, 20, 16, 45); - final res = await channel.updateReminder(messageId, remindAt: remindAt); + test("Channel.updateReminder sends no due date and returns the client's updateReminder response", () async { + const messageId = 'test-message-id'; + final response = UpdateReminderResponse( + duration: '0.01ms', + reminder: MessageReminder(channelCid: channelCid, messageId: messageId, userId: 'test-user-id'), + ); + when(() => client.updateReminder(messageId)).thenAnswer((_) async => Result.success(response)); - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); - expect(res.reminder.remindAt, remindAt); + final res = await channel.updateReminder(messageId); - verify( - () => channel.client.updateReminder( - messageId, - remindAt: remindAt, - ), - ).called(1); - }); + expect(res.getOrNull(), response); + verify(() => client.updateReminder(messageId)).called(1); }); - group('`.deleteReminder`', () { + test("Channel.deleteReminder returns the client's deleteReminder response", () async { const messageId = 'test-message-id'; + const response = DeleteReminderResponse(duration: '0.01ms'); + when(() => client.deleteReminder(messageId)).thenAnswer((_) async => const Result.success(response)); - setUp(() { - when(() => client.deleteReminder(messageId)).thenAnswer( - (_) async => EmptyResponse(), - ); - }); - - test('should call client.deleteReminder', () async { - final res = await channel.deleteReminder(messageId); + final res = await channel.deleteReminder(messageId); - expect(res, isNotNull); - - verify(() => channel.client.deleteReminder(messageId)).called(1); - }); + expect(res.getOrNull(), response); + verify(() => client.deleteReminder(messageId)).called(1); }); group('`.updateMessage`', () { diff --git a/packages/stream_chat/test/src/client/client_create_reminder_test.dart b/packages/stream_chat/test/src/client/client_create_reminder_test.dart new file mode 100644 index 0000000000..acce8e165d --- /dev/null +++ b/packages/stream_chat/test/src/client/client_create_reminder_test.dart @@ -0,0 +1,204 @@ +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'; +import 'message_fixtures.dart'; + +void main() { + setUpAll(() => registerFallbackValue(const api.CreateReminderRequest())); + + test('StreamChatClient.createReminder sends the due date and returns the created reminder', () async { + final request = api.CreateReminderRequest(remindAt: DateTime.utc(2026, 3)); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.createReminder(messageId: 'message-id', createReminderRequest: request), + ).thenAnswer( + (_) async => Result.success( + api.CreateReminderResponse( + duration: '0.01ms', + reminder: _generatedReminder(remindAt: DateTime.utc(2026, 3)), + ), + ), + ); + final client = _client(defaultApi); + + final res = await client.createReminder('message-id', remindAt: DateTime.utc(2026, 3)); + + final response = res.getOrNull()!; + final message = response.reminder.message!; + // A channel compares by identity, so it is checked on its own. + expect(message.sharedLocation?.channel?.cid, 'messaging:general'); + expect(message.draft?.channel?.cid, 'messaging:general'); + expect( + response, + CreateReminderResponse( + duration: '0.01ms', + reminder: _expectedReminder(expectedMessageLike(message), remindAt: DateTime.utc(2026, 3)), + ), + ); + verify(() => defaultApi.createReminder(messageId: 'message-id', createReminderRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.createReminder sends no due date and returns the bookmark', () async { + const request = api.CreateReminderRequest(); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.createReminder(messageId: 'message-id', createReminderRequest: request), + ).thenAnswer( + (_) async => Result.success( + api.CreateReminderResponse(duration: '0.01ms', reminder: _generatedReminder(remindAt: null)), + ), + ); + final client = _client(defaultApi); + + final res = await client.createReminder('message-id'); + + final response = res.getOrNull()!; + expect( + response, + CreateReminderResponse( + duration: '0.01ms', + reminder: _expectedReminder(expectedMessageLike(response.reminder.message!), remindAt: null), + ), + ); + verify(() => defaultApi.createReminder(messageId: 'message-id', createReminderRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test( + 'StreamChatClient.createReminder sends the message id and returns the reminder with reaction groups built from the counts', + () async { + const request = api.CreateReminderRequest(); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.createReminder(messageId: 'message-id', createReminderRequest: request), + ).thenAnswer( + (_) async => Result.success( + api.CreateReminderResponse( + duration: '0.01ms', + reminder: _generatedReminder(remindAt: null).copyWith(message: _generatedMessageWithCounts), + ), + ), + ); + final client = _client(defaultApi); + + final res = await client.createReminder('message-id'); + + final response = res.getOrNull()!; + // The dates of the reactions are unknown, so the groups share one. + final date = response.reminder.message!.reactionGroups!['love']!.firstReactionAt; + expect( + response, + CreateReminderResponse( + duration: '0.01ms', + reminder: _expectedReminder(_expectedMessageWithCounts(reactionDate: date), remindAt: null), + ), + ); + }, + ); + + test('StreamChatClient.createReminder returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.createReminder( + messageId: any(named: 'messageId'), + createReminderRequest: any(named: 'createReminderRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.createReminder('message-id'); + + expect(res.exceptionOrNull(), error); + }); +} + +// A reminder as a write answers it: with the message and the user, but no +// channel. +api.ReminderResponseData _generatedReminder({required DateTime? remindAt}) => api.ReminderResponseData( + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 1, 7), + expiresAt: DateTime.utc(2027), + message: generatedMessage, + messageId: 'message-id', + remindAt: remindAt, + updatedAt: DateTime.utc(2026, 1, 8), + user: fakeUserResponse('sender'), + userId: 'sender', +); + +// A message without reaction groups, only the counts and scores they are built +// from. +final _generatedMessageWithCounts = api.MessageResponse( + attachments: const [], + cid: 'messaging:general', + createdAt: DateTime.utc(2026), + custom: const {}, + deletedReplyCount: 0, + html: '', + id: 'message-id', + latestReactions: const [], + mentionedChannel: false, + mentionedHere: false, + mentionedUsers: const [], + ownReactions: const [], + pinned: false, + reactionCounts: const {'love': 3, 'wow': 1}, + reactionScores: const {'love': 7, 'wow': 2}, + replyCount: 0, + restrictedVisibility: const [], + shadowed: false, + silent: false, + text: 'Hello', + type: 'regular', + updatedAt: DateTime.utc(2026), + user: fakeUserResponse('sender'), +); + +// The message [_generatedMessageWithCounts] maps to, with the date its reaction +// groups share. +Message _expectedMessageWithCounts({required DateTime reactionDate}) => Message( + id: 'message-id', + text: 'Hello', + mentionedChannel: false, + mentionedHere: false, + reactionGroups: { + 'love': ReactionGroup(count: 3, sumScores: 7, firstReactionAt: reactionDate, lastReactionAt: reactionDate), + 'wow': ReactionGroup(count: 1, sumScores: 2, firstReactionAt: reactionDate, lastReactionAt: reactionDate), + }, + latestReactions: const [], + ownReactions: const [], + createdAt: DateTime.utc(2026), + updatedAt: DateTime.utc(2026), + user: fakeUser('sender'), + state: MessageState.sent, + restrictedVisibility: const [], + cid: 'messaging:general', + html: '', + deletedReplyCount: 0, +); + +MessageReminder _expectedReminder(Message message, {required DateTime? remindAt}) => MessageReminder( + channelCid: 'messaging:general', + messageId: 'message-id', + message: message, + userId: 'sender', + user: fakeUser('sender'), + remindAt: remindAt, + createdAt: DateTime.utc(2026, 1, 7), + updatedAt: DateTime.utc(2026, 1, 8), +); + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} diff --git a/packages/stream_chat/test/src/client/client_delete_reminder_test.dart b/packages/stream_chat/test/src/client/client_delete_reminder_test.dart new file mode 100644 index 0000000000..3db40eaacf --- /dev/null +++ b/packages/stream_chat/test/src/client/client_delete_reminder_test.dart @@ -0,0 +1,41 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/open_api/api.dart' as api; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + test('StreamChatClient.deleteReminder sends the message id and returns the response duration', () async { + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.deleteReminder(messageId: 'message-id'), + ).thenAnswer((_) async => const Result.success(api.DeleteReminderResponse(duration: '0.01ms'))); + final client = _client(defaultApi); + + final res = await client.deleteReminder('message-id'); + + expect(res.getOrNull(), const DeleteReminderResponse(duration: '0.01ms')); + verify(() => defaultApi.deleteReminder(messageId: 'message-id')).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.deleteReminder returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.deleteReminder(messageId: any(named: 'messageId')), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.deleteReminder('message-id'); + + expect(res.exceptionOrNull(), error); + }); +} + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} diff --git a/packages/stream_chat/test/src/client/client_query_reminders_test.dart b/packages/stream_chat/test/src/client/client_query_reminders_test.dart new file mode 100644 index 0000000000..a1e4d9ebb9 --- /dev/null +++ b/packages/stream_chat/test/src/client/client_query_reminders_test.dart @@ -0,0 +1,190 @@ +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'; +import 'message_fixtures.dart'; + +void main() { + setUpAll(() => registerFallbackValue(const api.QueryRemindersRequest())); + + test('StreamChatClient.queryReminders sends the filter, sort, limit and cursor and returns the page', () async { + const request = api.QueryRemindersRequest( + filter: { + 'channel_cid': {r'$eq': 'messaging:general'}, + }, + sort: [ + api.SortParamRequest(field: 'remind_at', direction: 1), + api.SortParamRequest(field: 'message_id', direction: -1), + ], + limit: 25, + next: 'next-cursor', + ); + + final defaultApi = MockDefaultApi(); + when(() => defaultApi.queryReminders(queryRemindersRequest: request)).thenAnswer( + (_) async => Result.success( + api.QueryRemindersResponse( + duration: '0.01ms', + next: 'later-cursor', + prev: 'earlier-cursor', + reminders: [_generatedReminder, _generatedBookmark], + ), + ), + ); + final client = _client(defaultApi); + + final res = await client.queryReminders( + filter: MessageReminderFilter.equal(MessageReminderFilterField.channelCid, 'messaging:general'), + sort: [ + MessageReminderSort.asc(MessageReminderSortField.remindAt), + MessageReminderSort.desc(MessageReminderSortField.messageId), + ], + limit: 25, + next: 'next-cursor', + ); + + final response = res.getOrNull()!; + final [reminder, bookmark] = response.reminders; + // A channel compares by identity, so it is checked on its own. + expect([reminder.channel?.cid, bookmark.channel?.cid], ['messaging:general', 'messaging:general']); + expect( + response, + QueryRemindersResponse( + duration: '0.01ms', + reminders: [_expectedReminder(reminder), _expectedBookmark(bookmark)], + next: 'later-cursor', + prev: 'earlier-cursor', + ), + ); + verify(() => defaultApi.queryReminders(queryRemindersRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.queryReminders sends the default limit of ten and returns the empty page', () async { + const request = api.QueryRemindersRequest(limit: 10); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.queryReminders(queryRemindersRequest: request), + ).thenAnswer((_) async => const Result.success(api.QueryRemindersResponse(duration: '0.01ms', reminders: []))); + final client = _client(defaultApi); + + final res = await client.queryReminders(); + + expect(res.getOrNull(), const QueryRemindersResponse(duration: '0.01ms', reminders: [])); + verify(() => defaultApi.queryReminders(queryRemindersRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.queryReminders sends the previous-page cursor and returns the earlier page', () async { + const request = api.QueryRemindersRequest(limit: 10, prev: 'prev-cursor'); + + final defaultApi = MockDefaultApi(); + when(() => defaultApi.queryReminders(queryRemindersRequest: request)).thenAnswer( + (_) async => Result.success( + api.QueryRemindersResponse(duration: '0.01ms', next: 'next-cursor', reminders: [_generatedBookmark]), + ), + ); + final client = _client(defaultApi); + + final res = await client.queryReminders(prev: 'prev-cursor'); + + final response = res.getOrNull()!; + final bookmark = response.reminders.single; + // A channel compares by identity, so it is checked on its own. + expect(bookmark.channel?.cid, 'messaging:general'); + expect( + response, + QueryRemindersResponse(duration: '0.01ms', reminders: [_expectedBookmark(bookmark)], next: 'next-cursor'), + ); + verify(() => defaultApi.queryReminders(queryRemindersRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.queryReminders returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.queryReminders(queryRemindersRequest: any(named: 'queryRemindersRequest')), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.queryReminders(); + + expect(res.exceptionOrNull(), error); + }); +} + +// Reminders as a query answers them: with their channel, message and user. +final _generatedReminder = api.ReminderResponseData( + channel: _generatedChannel, + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 1, 7), + expiresAt: DateTime.utc(2027), + message: generatedMessage, + messageId: 'message-id', + remindAt: DateTime.utc(2026, 3), + updatedAt: DateTime.utc(2026, 1, 8), + user: fakeUserResponse('sender'), + userId: 'sender', +); + +// A reminder with no due date. +final _generatedBookmark = api.ReminderResponseData( + channel: _generatedChannel, + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 2, 7), + message: generatedLeafMessage('other-message-id'), + messageId: 'other-message-id', + updatedAt: DateTime.utc(2026, 2, 8), + user: fakeUserResponse('sender'), + userId: 'sender', +); + +final _generatedChannel = api.ChannelResponse( + cid: 'messaging:general', + createdAt: DateTime.utc(2026), + custom: const {}, + disabled: false, + frozen: false, + id: 'general', + type: 'messaging', + updatedAt: DateTime.utc(2026), +); + +// The reminder [_generatedReminder] maps to. Its channel compares by identity, +// and its message's attachment ids are created locally, so both are read off +// [actual]. +MessageReminder _expectedReminder(MessageReminder actual) => MessageReminder( + channelCid: 'messaging:general', + channel: actual.channel, + messageId: 'message-id', + message: expectedMessageLike(actual.message!), + userId: 'sender', + user: fakeUser('sender'), + remindAt: DateTime.utc(2026, 3), + createdAt: DateTime.utc(2026, 1, 7), + updatedAt: DateTime.utc(2026, 1, 8), +); + +// The reminder [_generatedBookmark] maps to, with its channel read off +// [actual]. +MessageReminder _expectedBookmark(MessageReminder actual) => MessageReminder( + channelCid: 'messaging:general', + channel: actual.channel, + messageId: 'other-message-id', + message: expectedLeafMessage('other-message-id'), + userId: 'sender', + user: fakeUser('sender'), + createdAt: DateTime.utc(2026, 2, 7), + updatedAt: DateTime.utc(2026, 2, 8), +); + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} diff --git a/packages/stream_chat/test/src/client/client_update_reminder_test.dart b/packages/stream_chat/test/src/client/client_update_reminder_test.dart new file mode 100644 index 0000000000..dee9c69dec --- /dev/null +++ b/packages/stream_chat/test/src/client/client_update_reminder_test.dart @@ -0,0 +1,119 @@ +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'; +import 'message_fixtures.dart'; + +void main() { + setUpAll(() => registerFallbackValue(const api.UpdateReminderRequest())); + + test('StreamChatClient.updateReminder sends the new due date and returns the updated reminder', () async { + final request = api.UpdateReminderRequest(remindAt: DateTime.utc(2026, 4)); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateReminder(messageId: 'message-id', updateReminderRequest: request), + ).thenAnswer( + (_) async => Result.success( + api.UpdateReminderResponse( + duration: '0.01ms', + reminder: _generatedReminder(remindAt: DateTime.utc(2026, 4)), + ), + ), + ); + final client = _client(defaultApi); + + final res = await client.updateReminder('message-id', remindAt: DateTime.utc(2026, 4)); + + final response = res.getOrNull()!; + expect( + response, + UpdateReminderResponse( + duration: '0.01ms', + reminder: _expectedReminder( + remindAt: DateTime.utc(2026, 4), + message: expectedMessageLike(response.reminder.message!), + ), + ), + ); + verify(() => defaultApi.updateReminder(messageId: 'message-id', updateReminderRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.updateReminder sends no due date and returns the bookmark', () async { + const request = api.UpdateReminderRequest(); + + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateReminder(messageId: 'message-id', updateReminderRequest: request), + ).thenAnswer( + (_) async => Result.success( + api.UpdateReminderResponse(duration: '0.01ms', reminder: _generatedReminder(remindAt: null)), + ), + ); + final client = _client(defaultApi); + + final res = await client.updateReminder('message-id'); + + final response = res.getOrNull()!; + expect( + response, + UpdateReminderResponse( + duration: '0.01ms', + reminder: _expectedReminder(remindAt: null, message: expectedMessageLike(response.reminder.message!)), + ), + ); + verify(() => defaultApi.updateReminder(messageId: 'message-id', updateReminderRequest: request)).called(1); + verifyNoMoreInteractions(defaultApi); + }); + + test('StreamChatClient.updateReminder returns the failure without throwing', () async { + const error = StreamClientException(message: 'boom'); + final defaultApi = MockDefaultApi(); + when( + () => defaultApi.updateReminder( + messageId: any(named: 'messageId'), + updateReminderRequest: any(named: 'updateReminderRequest'), + ), + ).thenAnswer((_) async => const Result.failure(error)); + final client = _client(defaultApi); + + final res = await client.updateReminder('message-id'); + + expect(res.exceptionOrNull(), error); + }); +} + +// A reminder as an update answers it: with the message and the user, but no +// channel. +api.ReminderResponseData _generatedReminder({required DateTime? remindAt}) => api.ReminderResponseData( + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 1, 7), + expiresAt: DateTime.utc(2027), + message: generatedMessage, + messageId: 'message-id', + remindAt: remindAt, + updatedAt: DateTime.utc(2026, 1, 8), + user: fakeUserResponse('sender'), + userId: 'sender', +); + +MessageReminder _expectedReminder({required DateTime? remindAt, required Message message}) => MessageReminder( + channelCid: 'messaging:general', + messageId: 'message-id', + message: message, + userId: 'sender', + user: fakeUser('sender'), + remindAt: remindAt, + createdAt: DateTime.utc(2026, 1, 7), + updatedAt: DateTime.utc(2026, 1, 8), +); + +StreamChatClient _client(api.DefaultApi defaultApi) { + final client = StreamChatClient('test-api-key', defaultApi: defaultApi); + addTearDown(client.dispose); + return client; +} diff --git a/packages/stream_chat/test/src/client/message_fixtures.dart b/packages/stream_chat/test/src/client/message_fixtures.dart new file mode 100644 index 0000000000..e6435c4b23 --- /dev/null +++ b/packages/stream_chat/test/src/client/message_fixtures.dart @@ -0,0 +1,610 @@ +import 'package:stream_chat/open_api/api.dart' as api; +import 'package:stream_chat/stream_chat.dart'; + +import '../fakes.dart'; +import 'poll_fixtures.dart'; + +// Fixtures shared by the client tests whose responses carry messages. The +// generated message carries a value in every field, including the ones the SDK +// models leave out, so a field the mapping drops changes the compared object; +// its users are the minimal ones from fakes.dart. No two fields of a type hold +// the same value in all three messages (the message, the one it quotes and the +// leaf ones nested deeper), so a field mapped into the wrong slot changes it +// too. The quoted message carries minimal nested payloads that leave out their +// optional fields, to compare the defaults. + +/// A generated message with every field set, nesting one level of messages. +final generatedMessage = api.MessageResponse( + attachments: [_generatedAttachment, _generatedImageAttachment], + cid: 'messaging:general', + command: 'giphy', + createdAt: DateTime.utc(2026, 1, 1), + custom: const {'priority': 'high'}, + deletedAt: DateTime.utc(2026, 1, 3), + deletedForMe: false, + deletedReplyCount: 2, + draft: _generatedDraft, + html: '

Hello

', + i18n: const {'fr_text': 'Bonjour', 'language': 'en'}, + id: 'message-id', + imageLabels: const { + 'cat.png': ['cat'], + }, + latestReactions: [_generatedReaction], + member: const api.ChannelMemberPartialResponse(channelRole: 'channel_moderator', notificationsMuted: true), + mentionedChannel: true, + mentionedChannelMembers: const { + 'mo': api.ChannelMemberPartialResponse( + channelRole: 'channel_member', + custom: {'nickname': 'Mo'}, + notificationsMuted: false, + ), + }, + mentionedGroupIds: const ['group-id'], + mentionedGroups: [ + api.UserGroupResponse( + createdAt: DateTime.utc(2025, 1, 1), + createdBy: 'creator', + description: 'The group', + id: 'group-id', + members: [ + api.UserGroupMember( + appPk: 1, + createdAt: DateTime.utc(2025, 1, 2), + groupId: 'group-id', + isAdmin: true, + userId: 'member', + ), + ], + name: 'Group', + teamId: 'blue', + updatedAt: DateTime.utc(2025, 1, 3), + ), + ], + mentionedHere: false, + mentionedRoles: const ['admin'], + mentionedUsers: [fakeUserResponse('mentioned')], + messageTextUpdatedAt: DateTime.utc(2026, 1, 2, 12), + mml: 'Hello', + moderation: const api.ModerationV2Response( + action: 'remove', + blocklistMatched: 'profanity', + blocklistsMatched: ['profanity'], + imageHarms: ['nudity'], + originalText: 'Hullo', + platformCircumvented: true, + semanticFilterMatched: 'spam', + textHarms: ['insult'], + ), + ownReactions: [_generatedOwnReaction], + parentId: 'parent-id', + pinExpires: DateTime.utc(2026, 2), + pinned: true, + pinnedAt: DateTime.utc(2026, 1, 4), + pinnedBy: fakeUserResponse('pinner'), + poll: generatedPoll, + pollId: 'poll-id', + quotedMessage: _generatedQuotedMessage, + quotedMessageId: 'quoted-id', + // The counts and scores the groups were built from; the groups take + // precedence. + reactionCounts: const {'love': 1}, + reactionGroups: { + 'love': api.ReactionGroupResponse( + count: 1, + firstReactionAt: DateTime.utc(2026, 1, 5), + lastReactionAt: DateTime.utc(2026, 1, 6), + latestReactionsBy: [ + api.ReactionGroupUserResponse(createdAt: DateTime.utc(2026, 1, 6), userId: 'reactor'), + ], + sumScores: 2, + ), + }, + reactionScores: const {'love': 2}, + reminder: api.ReminderResponseData( + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 1, 7), + expiresAt: DateTime.utc(2027), + messageId: 'message-id', + remindAt: DateTime.utc(2026, 3), + updatedAt: DateTime.utc(2026, 1, 8), + userId: 'sender', + ), + replyCount: 3, + restrictedVisibility: const ['luke'], + shadowed: false, + sharedLocation: api.SharedLocationResponseData( + channel: _generatedChannel, + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 1, 9), + createdByDeviceId: 'device-id', + endAt: DateTime.utc(2026, 1, 10), + latitude: 1.5, + longitude: 2.5, + message: generatedLeafMessage('message-id'), + messageId: 'message-id', + updatedAt: DateTime.utc(2026, 1, 9, 12), + userId: 'sender', + ), + showInChannel: true, + silent: true, + text: 'Hello', + threadParticipants: [fakeUserResponse('participant')], + type: 'deleted', + updatedAt: DateTime.utc(2026, 1, 2), + user: fakeUserResponse('sender'), +); + +/// The [Message] that [generatedMessage] maps to, with the values the mapping +/// creates read off [actual]. +/// +/// Every received attachment gets a new local id, and a channel compares by +/// identity, so the attachment ids and the channels of the shared location and +/// the draft are taken from [actual]; check those channels' `cid` on their own. +Message expectedMessageLike(Message actual) => Message( + id: 'message-id', + text: 'Hello', + type: 'deleted', + attachments: [ + _expectedAttachment(actual.attachments.first.id), + _expectedImageAttachment(actual.attachments.last.id), + ], + mentionedChannel: true, + mentionedGroupIds: const ['group-id'], + mentionedGroups: [ + UserGroup( + createdAt: DateTime.utc(2025, 1, 1), + createdBy: 'creator', + description: 'The group', + id: 'group-id', + members: [ + UserGroupMember(createdAt: DateTime.utc(2025, 1, 2), groupId: 'group-id', isAdmin: true, userId: 'member'), + ], + name: 'Group', + teamId: 'blue', + updatedAt: DateTime.utc(2025, 1, 3), + ), + ], + mentionedHere: false, + mentionedRoles: const ['admin'], + mentionedUsers: [fakeUser('mentioned')], + silent: true, + shadowed: false, + reactionGroups: { + 'love': ReactionGroup( + count: 1, + sumScores: 2, + firstReactionAt: DateTime.utc(2026, 1, 5), + lastReactionAt: DateTime.utc(2026, 1, 6), + ), + }, + latestReactions: [_expectedReaction], + ownReactions: [_expectedOwnReaction], + parentId: 'parent-id', + quotedMessage: _expectedQuotedMessage(actual.quotedMessage!.attachments.single.id), + quotedMessageId: 'quoted-id', + replyCount: 3, + threadParticipants: [fakeUser('participant')], + showInChannel: true, + command: 'giphy', + createdAt: DateTime.utc(2026, 1, 1), + updatedAt: DateTime.utc(2026, 1, 2), + deletedAt: DateTime.utc(2026, 1, 3), + deletedForMe: false, + messageTextUpdatedAt: DateTime.utc(2026, 1, 2, 12), + user: fakeUser('sender'), + pinned: true, + pinnedAt: DateTime.utc(2026, 1, 4), + pinExpires: DateTime.utc(2026, 2), + pinnedBy: fakeUser('pinner'), + poll: poll, + pollId: 'poll-id', + custom: const {'priority': 'high'}, + // Deleted, but not only for the current user. + state: MessageState.softDeleted, + i18n: const {'fr_text': 'Bonjour', 'language': 'en'}, + restrictedVisibility: const ['luke'], + moderation: const Moderation( + action: ModerationAction.remove, + originalText: 'Hullo', + textHarms: ['insult'], + imageHarms: ['nudity'], + blocklistMatched: 'profanity', + semanticFilterMatched: 'spam', + platformCircumvented: true, + ), + draft: _expectedDraft( + attachmentId: actual.draft!.message.attachments.single.id, + channel: actual.draft!.channel, + ), + reminder: MessageReminder( + channelCid: 'messaging:general', + messageId: 'message-id', + userId: 'sender', + remindAt: DateTime.utc(2026, 3), + createdAt: DateTime.utc(2026, 1, 7), + updatedAt: DateTime.utc(2026, 1, 8), + ), + channelRole: 'channel_moderator', + sharedLocation: Location( + channelCid: 'messaging:general', + channel: actual.sharedLocation!.channel, + messageId: 'message-id', + message: expectedLeafMessage('message-id'), + userId: 'sender', + latitude: 1.5, + longitude: 2.5, + createdByDeviceId: 'device-id', + endAt: DateTime.utc(2026, 1, 10), + createdAt: DateTime.utc(2026, 1, 9), + updatedAt: DateTime.utc(2026, 1, 9, 12), + ), + cid: 'messaging:general', + html: '

Hello

', + mml: 'Hello', + deletedReplyCount: 2, +); + +final _generatedChannel = api.ChannelResponse( + cid: 'messaging:general', + createdAt: DateTime.utc(2026), + custom: const {}, + disabled: false, + frozen: false, + id: 'general', + type: 'messaging', + updatedAt: DateTime.utc(2026), +); + +// A link preview, read as one whatever type it was sent with. +final _generatedAttachment = api.Attachment( + actions: const [api.Action(name: 'answer', style: 'primary', text: 'Send', type: 'button', value: 'yes')], + assetUrl: 'https://example.com/asset.gif', + authorIcon: 'https://example.com/author.png', + authorLink: 'https://example.com/author', + authorName: 'Author', + color: '#ff0000', + custom: const {'id': 'attachment-id', 'caption': 'A cat'}, + fallback: 'A cat gif', + fields: const [api.Field(short: true, title: 'Size', value: 'L')], + footer: 'Footer', + footerIcon: 'https://example.com/footer.png', + giphy: api.Images( + fixedHeight: _imageData('fixed_height'), + fixedHeightDownsampled: _imageData('fixed_height_downsampled'), + fixedHeightStill: _imageData('fixed_height_still'), + fixedWidth: _imageData('fixed_width'), + fixedWidthDownsampled: _imageData('fixed_width_downsampled'), + fixedWidthStill: _imageData('fixed_width_still'), + original: _imageData('original'), + ), + imageUrl: 'https://example.com/image.gif', + ogScrapeUrl: 'https://example.com/scraped', + originalHeight: 300, + originalWidth: 400, + pretext: 'Pretext', + text: 'Attachment text', + thumbUrl: 'https://example.com/thumb.gif', + title: 'Cat', + titleLink: 'https://example.com/cat', + type: 'giphy', +); + +Attachment _expectedAttachment(String id) => Attachment( + id: id, + type: 'giphy', + titleLink: 'https://example.com/cat', + title: 'Cat', + thumbUrl: 'https://example.com/thumb.gif', + text: 'Attachment text', + pretext: 'Pretext', + ogScrapeUrl: 'https://example.com/scraped', + imageUrl: 'https://example.com/image.gif', + footerIcon: 'https://example.com/footer.png', + footer: 'Footer', + fields: const [ + {'short': true, 'title': 'Size', 'value': 'L'}, + ], + fallback: 'A cat gif', + color: '#ff0000', + authorName: 'Author', + authorLink: 'https://example.com/author', + authorIcon: 'https://example.com/author.png', + assetUrl: 'https://example.com/asset.gif', + actions: const [Action(name: 'answer', style: 'primary', text: 'Send', type: 'button', value: 'yes')], + originalWidth: 400, + originalHeight: 300, + custom: const {'id': 'attachment-id', 'caption': 'A cat'}, + giphy: { + for (final rendition in const [ + 'fixed_height', + 'fixed_height_downsampled', + 'fixed_height_still', + 'fixed_width', + 'fixed_width_downsampled', + 'fixed_width_still', + 'original', + ]) + rendition: { + 'frames': '12', + 'height': '200', + 'size': '1024', + 'url': 'https://example.com/$rendition.gif', + 'width': '300', + }, + }, + uploadState: const UploadState.success(), +); + +api.ImageData _imageData(String rendition) => api.ImageData( + frames: '12', + height: '200', + size: '1024', + url: 'https://example.com/$rendition.gif', + width: '300', +); + +const _generatedImageAttachment = api.Attachment( + custom: {}, + imageUrl: 'https://example.com/photo.png', + type: 'image', +); + +Attachment _expectedImageAttachment(String id) => Attachment( + id: id, + type: 'image', + imageUrl: 'https://example.com/photo.png', + uploadState: const UploadState.success(), +); + +final _generatedReaction = api.ReactionResponse( + createdAt: DateTime.utc(2026, 1, 5), + custom: const {'emoji_code': '❤️', 'mood': 'happy'}, + messageId: 'message-id', + score: 2, + type: 'love', + updatedAt: DateTime.utc(2026, 1, 6), + user: fakeUserResponse('reactor'), + userId: 'reactor-id', +); + +final _expectedReaction = Reaction( + messageId: 'message-id', + type: 'love', + user: fakeUser('reactor'), + userId: 'reactor-id', + score: 2, + emojiCode: '❤️', + createdAt: DateTime.utc(2026, 1, 5), + updatedAt: DateTime.utc(2026, 1, 6), + custom: const {'emoji_code': '❤️', 'mood': 'happy'}, +); + +final _generatedOwnReaction = api.ReactionResponse( + createdAt: DateTime.utc(2026, 1, 4, 12), + custom: const {}, + messageId: 'message-id', + score: 1, + type: 'wow', + updatedAt: DateTime.utc(2026, 1, 4, 13), + user: fakeUserResponse('owner'), + userId: 'owner-id', +); + +final _expectedOwnReaction = Reaction( + messageId: 'message-id', + type: 'wow', + user: fakeUser('owner'), + userId: 'owner-id', + score: 1, + createdAt: DateTime.utc(2026, 1, 4, 12), + updatedAt: DateTime.utc(2026, 1, 4, 13), +); + +final _generatedDraft = api.DraftResponse( + channel: _generatedChannel, + channelCid: 'messaging:general', + createdAt: DateTime.utc(2026, 1, 11), + message: api.DraftPayloadResponse( + attachments: const [ + api.Attachment(custom: {'caption': 'Notes'}, title: 'notes.pdf', type: 'file'), + ], + custom: const {'mood': 'busy'}, + html: '

Draft

', + id: 'draft-id', + mentionedUsers: [fakeUserResponse('mentioned')], + mml: 'Draft', + parentId: 'message-id', + pollId: 'draft-poll-id', + quotedMessageId: 'draft-quoted-id', + showInChannel: true, + silent: false, + text: 'Draft', + type: 'reply', + ), + parentId: 'message-id', + parentMessage: generatedLeafMessage('message-id'), + quotedMessage: generatedLeafMessage('draft-quoted-id'), +); + +Draft _expectedDraft({required String attachmentId, required ChannelModel? channel}) => Draft( + channelCid: 'messaging:general', + channel: channel, + createdAt: DateTime.utc(2026, 1, 11), + message: DraftMessage( + id: 'draft-id', + text: 'Draft', + type: 'reply', + attachments: [ + Attachment( + id: attachmentId, + type: 'file', + title: 'notes.pdf', + custom: const {'caption': 'Notes'}, + uploadState: const UploadState.success(), + ), + ], + parentId: 'message-id', + showInChannel: true, + mentionedUsers: [fakeUser('mentioned')], + quotedMessageId: 'draft-quoted-id', + silent: false, + pollId: 'draft-poll-id', + custom: const {'mood': 'busy'}, + ), + parentId: 'message-id', + parentMessage: expectedLeafMessage('message-id'), + quotedMessage: expectedLeafMessage('draft-quoted-id'), +); + +// A message with the fields the API always sends, a quoted message and poll id +// without the objects they point to, and minimal attachment, moderation, draft +// and shared location payloads that leave out their optional fields. +final _generatedQuotedMessage = api.MessageResponse( + attachments: const [ + api.Attachment( + actions: [api.Action(name: 'reply', text: 'Reply', type: 'button')], + custom: {'note': 'kept'}, + type: 'file', + ), + ], + cid: 'messaging:general', + createdAt: DateTime.utc(2025, 12, 2), + custom: const {'tag': 'kept'}, + deletedReplyCount: 1, + draft: api.DraftResponse( + channelCid: 'messaging:general', + createdAt: DateTime.utc(2025, 12, 3), + message: const api.DraftPayloadResponse( + custom: {'mood': 'calm'}, + id: 'quoted-draft-id', + text: 'Later', + ), + ), + html: '

Quoted

', + id: 'quoted-id', + latestReactions: const [], + mentionedChannel: false, + mentionedHere: true, + mentionedUsers: const [], + moderation: const api.ModerationV2Response(action: 'shadow', originalText: 'Quote'), + ownReactions: const [], + pinned: false, + pollId: 'quoted-poll-id', + quotedMessageId: 'earlier-id', + reactionCounts: const {}, + reactionScores: const {}, + replyCount: 0, + restrictedVisibility: const [], + shadowed: true, + sharedLocation: api.SharedLocationResponseData( + channelCid: 'messaging:general', + createdAt: DateTime.utc(2025, 12, 4), + createdByDeviceId: 'other-device-id', + latitude: 3.5, + longitude: 4.5, + messageId: 'quoted-id', + updatedAt: DateTime.utc(2025, 12, 5), + userId: 'sender', + ), + silent: true, + text: 'Quoted', + type: 'regular', + updatedAt: DateTime.utc(2025, 12, 2), + user: fakeUserResponse('sender'), +); + +Message _expectedQuotedMessage(String attachmentId) => Message( + id: 'quoted-id', + text: 'Quoted', + attachments: [ + Attachment( + id: attachmentId, + type: 'file', + actions: const [Action(name: 'reply', style: 'default', text: 'Reply', type: 'button')], + custom: const {'note': 'kept'}, + uploadState: const UploadState.success(), + ), + ], + mentionedChannel: false, + mentionedHere: true, + silent: true, + shadowed: true, + latestReactions: const [], + ownReactions: const [], + quotedMessageId: 'earlier-id', + createdAt: DateTime.utc(2025, 12, 2), + updatedAt: DateTime.utc(2025, 12, 2), + user: fakeUser('sender'), + pollId: 'quoted-poll-id', + custom: const {'tag': 'kept'}, + state: MessageState.sent, + restrictedVisibility: const [], + moderation: const Moderation(action: ModerationAction.shadow, originalText: 'Quote'), + sharedLocation: Location( + channelCid: 'messaging:general', + messageId: 'quoted-id', + userId: 'sender', + latitude: 3.5, + longitude: 4.5, + createdByDeviceId: 'other-device-id', + createdAt: DateTime.utc(2025, 12, 4), + updatedAt: DateTime.utc(2025, 12, 5), + ), + draft: Draft( + channelCid: 'messaging:general', + createdAt: DateTime.utc(2025, 12, 3), + message: DraftMessage(id: 'quoted-draft-id', text: 'Later', custom: const {'mood': 'calm'}), + ), + cid: 'messaging:general', + html: '

Quoted

', + deletedReplyCount: 1, +); + +/// A generated message with the id [id] and only the fields the API always +/// sends. +api.MessageResponse generatedLeafMessage(String id) => api.MessageResponse( + attachments: const [], + cid: 'messaging:general', + createdAt: DateTime.utc(2025, 12, 1), + custom: const {}, + deletedReplyCount: 0, + html: '

Earlier

', + id: id, + latestReactions: const [], + mentionedChannel: false, + mentionedHere: false, + mentionedUsers: const [], + ownReactions: const [], + pinned: true, + reactionCounts: const {}, + reactionScores: const {}, + replyCount: 0, + restrictedVisibility: const [], + shadowed: true, + silent: false, + text: 'Earlier', + type: 'regular', + updatedAt: DateTime.utc(2025, 12, 1), + user: fakeUserResponse('sender'), +); + +/// The [Message] that [generatedLeafMessage] maps to. +Message expectedLeafMessage(String id) => Message( + id: id, + text: 'Earlier', + mentionedChannel: false, + mentionedHere: false, + pinned: true, + shadowed: true, + latestReactions: const [], + ownReactions: const [], + createdAt: DateTime.utc(2025, 12, 1), + updatedAt: DateTime.utc(2025, 12, 1), + user: fakeUser('sender'), + state: MessageState.sent, + restrictedVisibility: const [], + cid: 'messaging:general', + html: '

Earlier

', + deletedReplyCount: 0, +); diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index 0cfeacdd83..54656dc762 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -291,7 +291,7 @@ void main() { () => client.post( path, data: jsonEncode({ - 'reaction': reaction.toJson(), + 'reaction': {'type': reactionType, 'score': 1}, 'skip_push': false, 'enforce_unique': false, }), @@ -301,7 +301,7 @@ void main() { path, data: { 'message': message.toJson(), - 'reaction': {...reaction.toJson(), 'message_id': messageId}, + 'reaction': {'type': reactionType, 'score': 1, 'message_id': messageId}, }, ), ); @@ -330,7 +330,7 @@ void main() { () => client.post( path, data: jsonEncode({ - 'reaction': reaction.toJson(), + 'reaction': {'type': reactionType, 'score': 1}, 'skip_push': false, 'enforce_unique': true, }), @@ -340,7 +340,7 @@ void main() { path, data: { 'message': message.toJson(), - 'reaction': {...reaction.toJson(), 'message_id': messageId}, + 'reaction': {'type': reactionType, 'score': 1, 'message_id': messageId}, }, ), ); @@ -403,7 +403,7 @@ void main() { data: { 'reactions': [ ...reactions.map( - (it) => {...it.toJson(), 'message_id': messageId}, + (it) => {'type': it.type, 'score': it.score, 'message_id': messageId}, ), ], }, @@ -709,7 +709,7 @@ void main() { data: { 'reactions': [ ...reactions.map( - (it) => {...it.toJson(), 'message_id': messageId}, + (it) => {'type': it.type, 'score': it.score, 'message_id': messageId}, ), ], 'next': null, @@ -749,7 +749,7 @@ void main() { data: { 'reactions': [ ...reactions.map( - (it) => {...it.toJson(), 'message_id': messageId}, + (it) => {'type': it.type, 'score': it.score, 'message_id': messageId}, ), ], 'next': nextCursor, diff --git a/packages/stream_chat/test/src/core/api/reminders_api_test.dart b/packages/stream_chat/test/src/core/api/reminders_api_test.dart deleted file mode 100644 index efe3d40050..0000000000 --- a/packages/stream_chat/test/src/core/api/reminders_api_test.dart +++ /dev/null @@ -1,272 +0,0 @@ -// ignore_for_file: avoid_redundant_argument_values - -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/api/reminders_api.dart'; -import 'package:stream_chat/src/core/api/requests.dart'; -import 'package:stream_chat/src/core/models/message_reminder.dart'; -import 'package:test/test.dart'; - -import '../../mocks.dart'; - -void main() { - Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); - - late final client = MockHttpClient(); - late RemindersApi remindersApi; - - setUp(() { - remindersApi = RemindersApi(client); - }); - - group('queryReminders', () { - test('should query reminders without parameters', () async { - const path = '/reminders/query'; - - final reminders = [ - MessageReminder( - messageId: 'test-message-id-1', - channelCid: 'test-channel-cid-1', - userId: 'test-user-id', - remindAt: DateTime(2024, 1, 1), - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 1), - ), - MessageReminder( - messageId: 'test-message-id-2', - channelCid: 'test-channel-cid-2', - userId: 'test-user-id', - remindAt: DateTime(2024, 1, 2), - createdAt: DateTime(2024, 1, 2), - updatedAt: DateTime(2024, 1, 2), - ), - ]; - - when(() => client.post(path, data: jsonEncode({}))).thenAnswer( - (_) async => successResponse( - path, - data: { - 'reminders': reminders.map((r) => r.toJson()).toList(), - }, - ), - ); - - final res = await remindersApi.queryReminders(); - - expect(res, isNotNull); - expect(res.reminders, hasLength(2)); - expect(res.reminders.first.messageId, 'test-message-id-1'); - - verify(() => client.post(path, data: jsonEncode({}))).called(1); - verifyNoMoreInteractions(client); - }); - - test('should query reminders with filter, sort, and pagination', () async { - const path = '/reminders/query'; - final filter = MessageReminderFilter.equal(MessageReminderFilterField.channelCid, 'messaging:test'); - final sort = [MessageReminderSort.desc(MessageReminderSortField.remindAt)]; - const pagination = PaginationParams(limit: 10, offset: 5); - - final expectedPayload = jsonEncode({ - 'filter': filter, - 'sort': sort, - ...pagination.toJson(), - }); - - final reminders = List.generate( - 5, - (index) => MessageReminder( - messageId: 'test-message-id-$index', - channelCid: 'test-channel-cid-$index', - userId: 'test-user-id', - remindAt: DateTime(2024, 1, index + 1), - createdAt: DateTime(2024, 1, index + 1), - updatedAt: DateTime(2024, 1, index + 1), - ), - ); - - when(() => client.post(path, data: expectedPayload)).thenAnswer( - (_) async => successResponse( - path, - data: { - 'reminders': reminders.map((r) => r.toJson()).toList(), - }, - ), - ); - - final res = await remindersApi.queryReminders( - filter: filter, - sort: sort, - pagination: pagination, - ); - - expect(res, isNotNull); - expect(res.reminders, hasLength(5)); - - verify(() => client.post(path, data: expectedPayload)).called(1); - verifyNoMoreInteractions(client); - }); - }); - - group('createReminder', () { - test('should create reminder without remindAt', () async { - const messageId = 'test-message-id'; - const path = '/messages/$messageId/reminders'; - - final reminder = MessageReminder( - messageId: messageId, - channelCid: 'test-channel-cid', - userId: 'test-user-id', - remindAt: DateTime(2024, 1, 1), - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 1), - ); - - when(() => client.post(path, data: jsonEncode({}))).thenAnswer( - (_) async => successResponse( - path, - data: { - 'reminder': reminder.toJson(), - }, - ), - ); - - final res = await remindersApi.createReminder(messageId); - - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); - - verify(() => client.post(path, data: jsonEncode({}))).called(1); - verifyNoMoreInteractions(client); - }); - - test('should create reminder with remindAt', () async { - const messageId = 'test-message-id'; - const path = '/messages/$messageId/reminders'; - final remindAt = DateTime(2024, 6, 15, 14, 30); - - final reminder = MessageReminder( - messageId: messageId, - channelCid: 'test-channel-cid', - userId: 'test-user-id', - remindAt: remindAt, - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 1), - ); - - final expectedPayload = jsonEncode({ - 'remind_at': remindAt.toUtc().toIso8601String(), - }); - - when(() => client.post(path, data: expectedPayload)).thenAnswer( - (_) async => successResponse( - path, - data: { - 'reminder': reminder.toJson(), - }, - ), - ); - - final res = await remindersApi.createReminder(messageId, remindAt: remindAt); - - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); - expect(res.reminder.remindAt, remindAt); - - verify(() => client.post(path, data: expectedPayload)).called(1); - verifyNoMoreInteractions(client); - }); - }); - - group('updateReminder', () { - test('should update reminder without remindAt', () async { - const messageId = 'test-message-id'; - const path = '/messages/$messageId/reminders'; - - final reminder = MessageReminder( - messageId: messageId, - channelCid: 'test-channel-cid', - userId: 'test-user-id', - remindAt: DateTime(2024, 1, 1), - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 2), - ); - - when(() => client.patch(path, data: jsonEncode({}))).thenAnswer( - (_) async => successResponse( - path, - data: { - 'reminder': reminder.toJson(), - }, - ), - ); - - final res = await remindersApi.updateReminder(messageId); - - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); - - verify(() => client.patch(path, data: jsonEncode({}))).called(1); - verifyNoMoreInteractions(client); - }); - - test('should update reminder with remindAt', () async { - const messageId = 'test-message-id'; - const path = '/messages/$messageId/reminders'; - final remindAt = DateTime(2024, 8, 20, 16, 45); - - final reminder = MessageReminder( - messageId: messageId, - channelCid: 'test-channel-cid', - userId: 'test-user-id', - remindAt: remindAt, - createdAt: DateTime(2024, 1, 1), - updatedAt: DateTime(2024, 1, 2), - ); - - final expectedPayload = jsonEncode({ - 'remind_at': remindAt.toUtc().toIso8601String(), - }); - - when(() => client.patch(path, data: expectedPayload)).thenAnswer( - (_) async => successResponse( - path, - data: { - 'reminder': reminder.toJson(), - }, - ), - ); - - final res = await remindersApi.updateReminder(messageId, remindAt: remindAt); - - expect(res, isNotNull); - expect(res.reminder.messageId, messageId); - expect(res.reminder.remindAt, remindAt); - - verify(() => client.patch(path, data: expectedPayload)).called(1); - verifyNoMoreInteractions(client); - }); - }); - - group('deleteReminder', () { - test('should delete reminder', () async { - const messageId = 'test-message-id'; - const path = '/messages/$messageId/reminders'; - - when(() => client.delete(path)).thenAnswer((_) async => successResponse(path, data: {})); - - final res = await remindersApi.deleteReminder(messageId); - - expect(res, isNotNull); - - verify(() => client.delete(path)).called(1); - verifyNoMoreInteractions(client); - }); - }); -} diff --git a/packages/stream_chat/test/src/core/models/action_test.dart b/packages/stream_chat/test/src/core/models/action_test.dart deleted file mode 100644 index 0cf1c996fb..0000000000 --- a/packages/stream_chat/test/src/core/models/action_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:stream_chat/src/core/models/action.dart'; -import 'package:test/test.dart'; - -import '../../utils.dart'; - -void main() { - group('src/models/action', () { - test('should parse json correctly', () { - final action = Action.fromJson(jsonFixture('action.json')); - expect(action.name, 'name'); - expect(action.style, 'style'); - expect(action.text, 'text'); - expect(action.type, 'type'); - expect(action.value, 'value'); - }); - - test('should serialize to json correctly', () { - final action = Action( - name: 'name', - style: 'style', - text: 'text', - type: 'type', - value: 'value', - ); - - expect( - action.toJson(), - { - 'name': 'name', - 'style': 'style', - 'text': 'text', - 'type': 'type', - 'value': 'value', - }, - ); - }); - }); -} diff --git a/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart b/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart index 7d044ea81e..3dbfd0f46e 100644 --- a/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart @@ -18,13 +18,11 @@ void main() { group('GiphyInfoX', () { test('giphyInfo returns valid GiphyInfo object when data is valid', () { final attachment = Attachment( - extraData: const { - 'giphy': { - 'original': { - 'url': 'https://example.com/original.gif', - 'width': '200', - 'height': '150', - }, + giphy: const { + 'original': { + 'url': 'https://example.com/original.gif', + 'width': '200', + 'height': '150', }, }, ); @@ -47,13 +45,11 @@ void main() { test('giphyInfo returns null when the specific GiphyInfoType is missing', () { final attachment = Attachment( - extraData: const { - 'giphy': { - 'fixed_height': { - 'url': 'https://example.com/fixed_height.gif', - 'width': '100', - 'height': '100', - }, + giphy: const { + 'fixed_height': { + 'url': 'https://example.com/fixed_height.gif', + 'width': '100', + 'height': '100', }, }, ); diff --git a/packages/stream_chat/test/src/core/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart index 4417ef1173..a536ae3f80 100644 --- a/packages/stream_chat/test/src/core/models/attachment_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_test.dart @@ -23,6 +23,10 @@ void main() { expect(attachment.actions, isNotEmpty); expect(attachment.actions, hasLength(3)); expect(attachment.actions![0], isA()); + expect(attachment.giphy, { + 'original': {'url': 'https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif'}, + }); + expect(attachment.custom, isEmpty); }); test('should serialize to json correctly', () { @@ -43,7 +47,7 @@ void main() { ); }); - test('fileName, mimeType property and extraData manipulation', () { + test('fileName, mimeType property and custom data manipulation', () { final file = AttachmentFile(size: 3, path: 'myfolder/myfile.txt'); final attachment = Attachment(file: file); @@ -57,16 +61,16 @@ void main() { 'mime_type': 'text/plain', }); - // Setting the size and mimeType using extraData should work fine + // Setting the size and mimeType using custom data should work fine var newAttachment = Attachment( - extraData: const { + custom: const { 'file_size': 6, 'mime_type': 'application/pdf', }, ); - expect(newAttachment.extraData['file_size'], 6); - expect(newAttachment.extraData['mime_type'], 'application/pdf'); + expect(newAttachment.custom['file_size'], 6); + expect(newAttachment.custom['mime_type'], 'application/pdf'); expect(newAttachment.fileSize, 6); expect(newAttachment.mimeType, 'application/pdf'); @@ -74,8 +78,8 @@ void main() { final fileTwo = AttachmentFile(size: 12, path: 'myfolder/fileTwo.pdf'); newAttachment = attachment.copyWith(file: fileTwo); - expect(newAttachment.extraData['file_size'], 12); - expect(newAttachment.extraData['mime_type'], 'application/pdf'); + expect(newAttachment.custom['file_size'], 12); + expect(newAttachment.custom['mime_type'], 'application/pdf'); expect(newAttachment.fileSize, 12); expect(newAttachment.mimeType, 'application/pdf'); @@ -83,14 +87,14 @@ void main() { final fileThree = AttachmentFile(size: 9, path: 'myfolder/fileThree.png'); newAttachment = attachment.copyWith( file: fileThree, - extraData: { + custom: { 'file_size': 88, 'mime_type': 'application/pdf', }, ); - expect(newAttachment.extraData['file_size'], 9); - expect(newAttachment.extraData['mime_type'], 'image/png'); + expect(newAttachment.custom['file_size'], 9); + expect(newAttachment.custom['mime_type'], 'image/png'); expect(newAttachment.fileSize, 9); expect(newAttachment.mimeType, 'image/png'); }); @@ -163,4 +167,69 @@ void main() { }); }); }); + + test('Attachment.toJson writes the Giphy renditions, so an edited message keeps them', () { + final attachment = Attachment( + type: 'giphy', + giphy: const { + 'original': {'url': 'https://example.com/original.gif'}, + }, + ); + + expect(attachment.toJson()['giphy'], { + 'original': {'url': 'https://example.com/original.gif'}, + }); + }); + + test('Attachment.toJson leaves out custom data named like a field', () { + final attachment = Attachment( + title: 'Cat', + giphy: const { + 'original': {'url': 'https://example.com/original.gif'}, + }, + custom: const {'title': 'custom', 'giphy': 'custom', 'caption': 'A cat'}, + ); + + final json = attachment.toJson(); + + expect(json['title'], 'Cat'); + expect(json['giphy'], { + 'original': {'url': 'https://example.com/original.gif'}, + }); + expect(json['caption'], 'A cat'); + }); + + test('Attachment.fromData reads back the upload state written by toData, whatever the custom data holds', () { + final attachment = Attachment( + type: 'file', + uploadState: const UploadState.success(), + custom: const {'upload_state': 'custom', 'caption': 'A file'}, + ); + + final stored = Attachment.fromData(attachment.toData()); + + expect(stored.uploadState, const UploadState.success()); + expect(stored.custom['caption'], 'A file'); + }); + + test('Attachment.copyWith keeps the Giphy renditions', () { + final attachment = Attachment( + giphy: const { + 'original': {'url': 'https://example.com/original.gif'}, + }, + ); + + expect(attachment.copyWith(title: 'Cat').giphy, attachment.giphy); + }); + + test('Attachment.fromData reads back the Giphy renditions written by toData', () { + final attachment = Attachment( + type: 'giphy', + giphy: const { + 'original': {'url': 'https://example.com/original.gif'}, + }, + ); + + expect(Attachment.fromData(attachment.toData()), attachment); + }); } diff --git a/packages/stream_chat/test/src/core/models/converters/v1_json_converters_test.dart b/packages/stream_chat/test/src/core/models/converters/v1_json_converters_test.dart index c8354353f7..c52a51eb79 100644 --- a/packages/stream_chat/test/src/core/models/converters/v1_json_converters_test.dart +++ b/packages/stream_chat/test/src/core/models/converters/v1_json_converters_test.dart @@ -1,11 +1,24 @@ +import 'package:stream_chat/src/core/models/action.dart'; +import 'package:stream_chat/src/core/models/attachment.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/converters/v1_json_converters.dart'; import 'package:stream_chat/src/core/models/device.dart'; +import 'package:stream_chat/src/core/models/location.dart'; import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/message_reminder.dart'; +import 'package:stream_chat/src/core/models/moderation.dart'; import 'package:stream_chat/src/core/models/push_provider.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; +import 'package:stream_chat/src/core/models/reaction_group.dart'; +import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/models/user_group.dart'; import 'package:stream_chat/src/core/models/user_group_member.dart'; +import 'package:stream_chat/src/ws/events/event.dart'; import 'package:test/test.dart'; +import '../../../utils.dart'; + void main() { test('DeviceV1JsonConverter.fromJson reads a v1 device, ignoring the fields a Device does not carry', () { const converter = DeviceV1JsonConverter(); @@ -112,4 +125,436 @@ void main() { test('userGroupsFromV1Json returns null when the message carries no groups', () { expect(userGroupsFromV1Json(null), isNull); }); + + test('Message.fromJson reads every moderation field from its v1 keys', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'moderation': { + 'action': 'shadow', + 'original_text': 'original message text', + 'text_harms': ['hate', 'profanity'], + 'image_harms': ['explicit'], + 'blocklist_matched': 'profanity', + 'semantic_filter_matched': 'harassment', + 'platform_circumvented': true, + }, + }); + + expect( + message.moderation, + const Moderation( + action: ModerationAction.shadow, + originalText: 'original message text', + textHarms: ['hate', 'profanity'], + imageHarms: ['explicit'], + blocklistMatched: 'profanity', + semanticFilterMatched: 'harassment', + platformCircumvented: true, + ), + ); + }); + + test('Message.fromJson reads a legacy moderation action as its current name', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'moderation': {'action': 'MESSAGE_RESPONSE_ACTION_BLOCK', 'original_text': 'original message text'}, + }); + + expect(message.moderation!.action, ModerationAction.remove); + }); + + test('Message.fromJson reads a moderation without platform_circumvented as not circumvented', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'moderation': {'action': 'flag', 'original_text': 'original message text'}, + }); + + expect( + message.moderation, + const Moderation(action: ModerationAction.flag, originalText: 'original message text'), + ); + }); + + test('Message.fromJson leaves moderation null when the message carries none', () { + final message = Message.fromJson(const {'id': 'message-id'}); + + expect(message.moderation, isNull); + }); + + test('Message.fromJson reads every reaction group field from its v1 keys', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'reaction_groups': { + 'love': { + 'count': 2, + 'sum_scores': 5, + 'first_reaction_at': '2024-01-01T00:00:00.000Z', + 'last_reaction_at': '2024-01-02T00:00:00.000Z', + }, + }, + }); + + expect(message.reactionGroups, { + 'love': ReactionGroup( + count: 2, + sumScores: 5, + firstReactionAt: DateTime.utc(2024, 1, 1), + lastReactionAt: DateTime.utc(2024, 1, 2), + ), + }); + }); + + test('Message.fromJson reads reaction group dates sent as epoch nanoseconds', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'reaction_groups': { + 'love': { + 'count': 1, + 'sum_scores': 1, + 'first_reaction_at': 1704067200123456000, + 'last_reaction_at': 1704153600000000000, + }, + }, + }); + + expect(message.reactionGroups!['love']!.firstReactionAt, DateTime.utc(2024, 1, 1, 0, 0, 0, 123, 456)); + expect(message.reactionGroups!['love']!.lastReactionAt, DateTime.utc(2024, 1, 2)); + }); + + test('Message.fromJson leaves reaction groups null when the message carries no reactions', () { + final message = Message.fromJson(const {'id': 'message-id'}); + + expect(message.reactionGroups, isNull); + }); + + test('Attachment.fromJson reads every action field from its v1 keys', () { + final attachment = Attachment.fromJson(const { + 'type': 'giphy', + 'actions': [ + {'name': 'image_action', 'style': 'primary', 'text': 'Send', 'type': 'button', 'value': 'send'}, + ], + }); + + expect(attachment.actions, [ + const Action(name: 'image_action', style: 'primary', text: 'Send', type: 'button', value: 'send'), + ]); + }); + + test('Attachment.fromJson reads an action without a style as the default style', () { + final attachment = Attachment.fromJson(const { + 'type': 'giphy', + 'actions': [ + {'name': 'image_action', 'text': 'Cancel', 'type': 'button'}, + ], + }); + + expect(attachment.actions, [const Action(name: 'image_action', text: 'Cancel', type: 'button')]); + }); + + test('Attachment.toJson writes every action key, including a null value', () { + final attachment = Attachment( + type: 'giphy', + actions: const [Action(name: 'image_action', text: 'Cancel', type: 'button')], + ); + + expect(attachment.toJson()['actions'], [ + {'name': 'image_action', 'style': 'default', 'text': 'Cancel', 'type': 'button', 'value': null}, + ]); + }); + + test('Attachment.fromJson reads back the actions written by toJson', () { + final attachment = Attachment( + type: 'giphy', + actions: const [Action(name: 'image_action', style: 'primary', text: 'Send', type: 'button', value: 'send')], + ); + + expect(Attachment.fromJson(attachment.toJson()).actions, attachment.actions); + }); + + test('Event.fromJson reads every reaction field from its v1 keys, with the rest in custom data', () { + final json = jsonFixture('reaction.json'); + + final event = Event.fromJson({ + 'type': 'reaction.new', + 'reaction': {...json, 'bananas': 'yes'}, + }); + + expect( + event.reaction, + Reaction( + messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', + type: 'wow', + user: User.fromJson(json['user'] as Map), + userId: '2de0297c-f3f2-489d-b930-ef77342edccf', + emojiCode: '😮', + createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), + updatedAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), + custom: const {'bananas': 'yes'}, + ), + ); + }); + + test('Event.fromJson reads a reaction without a score as scoring one', () { + final event = Event.fromJson(const { + 'type': 'reaction.new', + 'reaction': { + 'type': 'like', + 'created_at': '2020-01-28T22:17:31.108742Z', + 'updated_at': '2020-01-28T22:17:31.108742Z', + }, + }); + + expect(event.reaction!.score, 1); + }); + + test('Event.toJson writes the reaction in the request shape, with its custom data at the root', () { + final event = Event( + type: 'reaction.new', + reaction: Reaction( + messageId: 'message-id', + type: 'wow', + user: User(id: 'user-id'), + score: 2, + emojiCode: '😮', + custom: const {'bananas': 'yes'}, + ), + ); + + expect(event.toJson()['reaction'], {'type': 'wow', 'score': 2, 'emoji_code': '😮', 'bananas': 'yes'}); + }); + + test('Event.toJson leaves out a reaction custom entry named like a reaction key', () { + final event = Event( + type: 'reaction.new', + reaction: Reaction( + type: 'wow', + emojiCode: '😮', + custom: const {'emoji_code': 'custom', 'type': 'custom'}, + ), + ); + + expect(event.toJson()['reaction'], {'type': 'wow', 'score': 1, 'emoji_code': '😮'}); + }); + + test('Message.fromJson reads the latest and own reactions', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'latest_reactions': [ + { + 'type': 'like', + 'user_id': 'user-1', + 'created_at': '2024-01-01T00:00:00.000Z', + 'updated_at': '2024-01-01T00:00:00.000Z', + }, + ], + 'own_reactions': [ + { + 'type': 'love', + 'score': 3, + 'user_id': 'user-2', + 'created_at': '2024-01-02T00:00:00.000Z', + 'updated_at': '2024-01-02T00:00:00.000Z', + }, + ], + }); + + expect(message.latestReactions, [ + Reaction( + type: 'like', + userId: 'user-1', + createdAt: DateTime.utc(2024, 1, 1), + updatedAt: DateTime.utc(2024, 1, 1), + ), + ]); + expect(message.ownReactions, [ + Reaction( + type: 'love', + score: 3, + userId: 'user-2', + createdAt: DateTime.utc(2024, 1, 2), + updatedAt: DateTime.utc(2024, 1, 2), + ), + ]); + }); + + test('Message.fromJson reads every shared location field from its v1 keys', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'shared_location': { + 'channel_cid': 'messaging:general', + 'message_id': 'message-id', + 'user_id': 'user-id', + 'latitude': 37.7749, + 'longitude': -122.4194, + 'created_by_device_id': 'device-id', + 'end_at': '2024-12-31T23:59:59.999Z', + 'created_at': '2024-01-01T00:00:00.000Z', + 'updated_at': '2024-01-02T00:00:00.000Z', + }, + }); + + expect( + message.sharedLocation, + Location( + channelCid: 'messaging:general', + messageId: 'message-id', + userId: 'user-id', + latitude: 37.7749, + longitude: -122.4194, + createdByDeviceId: 'device-id', + endAt: DateTime.utc(2024, 12, 31, 23, 59, 59, 999), + createdAt: DateTime.utc(2024), + updatedAt: DateTime.utc(2024, 1, 2), + ), + ); + }); + + test('ChannelState.fromJson reads active live locations with their channel and message', () { + final state = ChannelState.fromJson(const { + 'active_live_locations': [ + { + 'channel_cid': 'messaging:general', + 'channel': {'id': 'general', 'type': 'messaging', 'cid': 'messaging:general'}, + 'message_id': 'message-id', + 'message': {'id': 'message-id', 'text': 'Live location'}, + 'latitude': 1, + 'longitude': 2, + 'end_at': '2024-12-31T23:59:59.999Z', + 'created_at': '2024-01-01T00:00:00.000Z', + 'updated_at': '2024-01-01T00:00:00.000Z', + }, + ], + }); + + final location = state.activeLiveLocations!.single; + expect(location.channel, isA().having((it) => it.cid, 'cid', 'messaging:general')); + expect(location.message, isA().having((it) => it.text, 'text', 'Live location')); + expect(location.coordinates.latitude, 1.0); + expect(location.coordinates.longitude, 2.0); + }); + + test('Message.toJson writes the shared location in the request shape, with the end date in UTC', () { + final message = Message( + id: 'message-id', + sharedLocation: Location( + channelCid: 'messaging:general', + messageId: 'message-id', + userId: 'user-id', + latitude: 37.7749, + longitude: -122.4194, + createdByDeviceId: 'device-id', + endAt: DateTime.utc(2024, 12, 31, 23, 59, 59, 999), + ), + ); + + expect(message.toJson()['shared_location'], { + 'latitude': 37.7749, + 'longitude': -122.4194, + 'created_by_device_id': 'device-id', + 'end_at': '2024-12-31T23:59:59.999Z', + }); + }); + + test('Message.toJson leaves the device and end date out of a static location without them', () { + final message = Message(id: 'message-id', sharedLocation: Location(latitude: 1, longitude: 2)); + + expect(message.toJson()['shared_location'], {'latitude': 1.0, 'longitude': 2.0}); + }); + + test('Message.fromJson reads every reminder field from its v1 keys', () { + const channelJson = {'id': 'general', 'type': 'messaging', 'cid': 'messaging:general'}; + const messageJson = {'id': 'message-id', 'text': 'Remember me'}; + const userJson = {'id': 'user-id'}; + + final message = Message.fromJson(const { + 'id': 'message-id', + 'reminder': { + 'channel_cid': 'messaging:general', + 'channel': channelJson, + 'message_id': 'message-id', + 'message': messageJson, + 'user_id': 'user-id', + 'user': userJson, + 'remind_at': '2024-06-15T14:30:00.000Z', + 'created_at': '2024-06-01T10:00:00.000Z', + 'updated_at': '2024-06-02T10:00:00.000Z', + }, + }); + + final reminder = message.reminder!; + // A channel compares by identity, so it is checked on its own. + expect(reminder.channel?.cid, 'messaging:general'); + expect( + reminder, + MessageReminder( + channelCid: 'messaging:general', + channel: reminder.channel, + messageId: 'message-id', + message: Message.fromJson(const {...messageJson}), + userId: 'user-id', + user: User.fromJson(const {...userJson}), + remindAt: DateTime.utc(2024, 6, 15, 14, 30), + createdAt: DateTime.utc(2024, 6, 1, 10), + updatedAt: DateTime.utc(2024, 6, 2, 10), + ), + ); + }); + + test('Event.fromJson reads reminder dates sent as epoch nanoseconds', () { + final event = Event.fromJson(const { + 'type': 'reminder.created', + 'reminder': { + 'channel_cid': 'messaging:general', + 'message_id': 'message-id', + 'user_id': 'user-id', + 'remind_at': 1718461800000000000, + 'created_at': 1717236000000000000, + 'updated_at': 1717322400000000000, + }, + }); + + expect(event.reminder?.remindAt, DateTime.utc(2024, 6, 15, 14, 30)); + expect(event.reminder?.createdAt, DateTime.utc(2024, 6, 1, 10)); + expect(event.reminder?.updatedAt, DateTime.utc(2024, 6, 2, 10)); + }); + + test("Event.toJson writes the reminder's ids and dates, without its channel, message or user", () { + final event = Event( + type: 'reminder.created', + reminder: MessageReminder( + channelCid: 'messaging:general', + channel: ChannelModel(cid: 'messaging:general'), + messageId: 'message-id', + message: Message(id: 'message-id'), + userId: 'user-id', + user: User(id: 'user-id'), + remindAt: DateTime.utc(2024, 6, 15, 14, 30), + createdAt: DateTime.utc(2024, 6, 1, 10), + updatedAt: DateTime.utc(2024, 6, 2, 10), + ), + ); + + expect(event.toJson()['reminder'], { + 'channel_cid': 'messaging:general', + 'message_id': 'message-id', + 'user_id': 'user-id', + 'remind_at': '2024-06-15T14:30:00.000Z', + 'created_at': '2024-06-01T10:00:00.000Z', + 'updated_at': '2024-06-02T10:00:00.000Z', + }); + }); + + test('Event.fromJson reads back a reminder without a due date written by toJson', () { + final reminder = MessageReminder( + channelCid: 'messaging:general', + messageId: 'message-id', + userId: 'user-id', + createdAt: DateTime.utc(2024, 6, 1, 10), + updatedAt: DateTime.utc(2024, 6, 2, 10), + ); + + final event = Event.fromJson(Event(type: 'reminder.updated', reminder: reminder).toJson()); + + expect(event.reminder, reminder); + }); } diff --git a/packages/stream_chat/test/src/core/models/draft_message_test.dart b/packages/stream_chat/test/src/core/models/draft_message_test.dart index 0557c58759..9e14e10650 100644 --- a/packages/stream_chat/test/src/core/models/draft_message_test.dart +++ b/packages/stream_chat/test/src/core/models/draft_message_test.dart @@ -32,7 +32,7 @@ void main() { expect(draftMessage.silent, false); expect(draftMessage.poll, isNull); expect(draftMessage.pollId, isNull); - expect(draftMessage.extraData, isEmpty); + expect(draftMessage.custom, isEmpty); }); test('should create a valid instance with UUID when id is not provided', () { @@ -52,7 +52,7 @@ void main() { ]; const parentId = 'parent-123'; final quotedMessage = Message(id: 'quote-123'); - const extraData = {'key1': 'value1', 'key2': 42}; + const custom = {'key1': 'value1', 'key2': 42}; final poll = Poll( id: 'poll-123', @@ -73,7 +73,7 @@ void main() { quotedMessage: quotedMessage, silent: true, poll: poll, - extraData: extraData, + custom: custom, ); expect(fullDraftMessage.id, equals(id)); @@ -88,7 +88,7 @@ void main() { expect(fullDraftMessage.silent, isTrue); expect(fullDraftMessage.poll, equals(poll)); expect(fullDraftMessage.pollId, equals(poll.id)); - expect(fullDraftMessage.extraData, equals(extraData)); + expect(fullDraftMessage.custom, equals(custom)); }); test('should handle quotedMessageId parameter', () { @@ -125,15 +125,15 @@ void main() { expect(json['silent'], equals(false)); }); - test('should serialize extraData correctly', () { - const extraData = {'custom_field': 'custom_value', 'priority': 5}; - final messageWithExtraData = DraftMessage( + test('should serialize custom data correctly', () { + const custom = {'custom_field': 'custom_value', 'priority': 5}; + final messageWithCustom = DraftMessage( id: id, text: text, - extraData: extraData, + custom: custom, ); - final json = messageWithExtraData.toJson(); + final json = messageWithCustom.toJson(); expect(json['id'], equals(id)); expect(json['text'], equals(text)); @@ -230,7 +230,7 @@ void main() { expect(deserializedMessage.silent, isFalse); }); - test('should deserialize extraData correctly', () { + test('should deserialize custom data correctly', () { final json = { 'id': id, 'text': text, @@ -242,8 +242,7 @@ void main() { expect(deserializedMessage.id, equals(id)); expect(deserializedMessage.text, equals(text)); - expect(deserializedMessage.extraData['custom_field'], equals('custom_value')); - expect(deserializedMessage.extraData['priority'], equals(5)); + expect(deserializedMessage.custom, {'custom_field': 'custom_value', 'priority': 5}); }); test('should deserialize poll_id correctly', () { @@ -310,7 +309,7 @@ void main() { ]; const parentId = 'parent-123'; final quotedMessage = Message(id: 'quote-123'); - const extraData = {'key1': 'value1', 'key2': 42}; + const custom = {'key1': 'value1', 'key2': 42}; final message = Message( id: 'message-123', @@ -331,7 +330,7 @@ void main() { PollOption(id: 'option-2', text: 'Option 2'), ], ), - extraData: extraData, + custom: custom, ); final draftMessage = message.toDraftMessage(); @@ -350,7 +349,7 @@ void main() { expect(draftMessage.command, equals(message.command)); expect(draftMessage.poll, equals(message.poll)); expect(draftMessage.pollId, equals(message.pollId)); - expect(draftMessage.extraData, equals(message.extraData)); + expect(draftMessage.custom, equals(message.custom)); }); test('should only include successfully uploaded attachments', () { @@ -405,7 +404,7 @@ void main() { ]; const parentId = 'parent-123'; final quotedMessage = Message(id: 'quote-123'); - const extraData = {'key1': 'value1', 'key2': 42}; + const custom = {'key1': 'value1', 'key2': 42}; final draftMessage = DraftMessage( id: 'draft-123', @@ -426,7 +425,7 @@ void main() { PollOption(id: 'option-2', text: 'Option 2'), ], ), - extraData: extraData, + custom: custom, ); final message = draftMessage.toMessage(); @@ -445,8 +444,17 @@ void main() { expect(message.command, equals(draftMessage.command)); expect(message.poll, equals(draftMessage.poll)); expect(message.pollId, equals(draftMessage.pollId)); - expect(message.extraData, equals(draftMessage.extraData)); + expect(message.custom, equals(draftMessage.custom)); }); }); + + test('DraftMessage.toJson leaves out custom data named like a field', () { + final draft = DraftMessage(text: 'Hi', custom: const {'text': 'custom', 'mood': 'busy'}); + + final json = draft.toJson(); + + expect(json['text'], 'Hi'); + expect(json['mood'], 'busy'); + }); }); } diff --git a/packages/stream_chat/test/src/core/models/location_test.dart b/packages/stream_chat/test/src/core/models/location_test.dart index cce5f9b53a..8ce697eb97 100644 --- a/packages/stream_chat/test/src/core/models/location_test.dart +++ b/packages/stream_chat/test/src/core/models/location_test.dart @@ -75,67 +75,19 @@ void main() { expect(fullLocation.updatedAt, equals(updatedAt)); }); - test('should correctly serialize to JSON', () { - final json = location.toJson(); - - expect(json['latitude'], equals(latitude)); - expect(json['longitude'], equals(longitude)); - expect(json['created_by_device_id'], equals(createdByDeviceId)); - expect(json['end_at'], isNull); - expect(json.containsKey('channel_cid'), isFalse); - expect(json.containsKey('channel'), isFalse); - expect(json.containsKey('message_id'), isFalse); - expect(json.containsKey('message'), isFalse); - expect(json.containsKey('user_id'), isFalse); - expect(json.containsKey('created_at'), isFalse); - expect(json.containsKey('updated_at'), isFalse); - }); - - test('should serialize live location with endAt correctly', () { - final endAt = DateTime.parse('2024-12-31T23:59:59.999Z'); + test('should store endAt in UTC regardless of input timezone', () { + final localEndAt = DateTime(2024, 10, 16, 17, 12, 30, 338, 726); final liveLocation = Location( latitude: latitude, longitude: longitude, createdByDeviceId: createdByDeviceId, - endAt: endAt, + endAt: localEndAt, ); - final json = liveLocation.toJson(); - - expect(json['latitude'], equals(latitude)); - expect(json['longitude'], equals(longitude)); - expect(json['created_by_device_id'], equals(createdByDeviceId)); - expect(json['end_at'], equals('2024-12-31T23:59:59.999Z')); + expect(liveLocation.endAt?.isUtc, isTrue); + expect(liveLocation.endAt, equals(localEndAt.toUtc())); }); - test( - 'should convert endAt to UTC in toJson regardless of input timezone', - () { - // Create a non-UTC DateTime (local time) - final localEndAt = DateTime(2024, 10, 16, 17, 12, 30, 338, 726); - final liveLocation = Location( - latitude: latitude, - longitude: longitude, - createdByDeviceId: createdByDeviceId, - endAt: localEndAt, - ); - - final json = liveLocation.toJson(); - final serializedEndAt = json['end_at'] as String?; - - // Verify the serialized date is in UTC format (ends with 'Z') - expect(serializedEndAt, isNotNull); - expect(serializedEndAt, endsWith('Z')); - - // Verify the stored endAt is in UTC - expect(liveLocation.endAt?.isUtc, isTrue); - - // Verify the date is the same instant, just in UTC - final expectedUtc = localEndAt.toUtc(); - expect(liveLocation.endAt, equals(expectedUtc)); - }, - ); - test('should return correct coordinates', () { final coordinates = location.coordinates; diff --git a/packages/stream_chat/test/src/core/models/message_reminder_test.dart b/packages/stream_chat/test/src/core/models/message_reminder_test.dart index 267239ad8f..8653fdbbce 100644 --- a/packages/stream_chat/test/src/core/models/message_reminder_test.dart +++ b/packages/stream_chat/test/src/core/models/message_reminder_test.dart @@ -81,42 +81,6 @@ void main() { expect(bookmarkReminder.userId, equals(userId)); }); - test('should correctly serialize to JSON', () { - final json = messageReminder.toJson(); - - expect(json['channel_cid'], equals(channelCid)); - expect(json['message_id'], equals(messageId)); - expect(json['user_id'], equals(userId)); - expect(json['remind_at'], isA()); - expect(json['created_at'], isA()); - expect(json['updated_at'], isA()); - - // These fields should not be included in JSON - expect(json.containsKey('channel'), isFalse); - expect(json.containsKey('message'), isFalse); - expect(json.containsKey('user'), isFalse); - }); - - test('should correctly deserialize from JSON', () { - final json = { - 'channel_cid': channelCid, - 'message_id': messageId, - 'user_id': userId, - 'remind_at': remindAt.toIso8601String(), - 'created_at': now.toIso8601String(), - 'updated_at': now.toIso8601String(), - }; - - final deserializedReminder = MessageReminder.fromJson(json); - - expect(deserializedReminder.channelCid, equals(channelCid)); - expect(deserializedReminder.messageId, equals(messageId)); - expect(deserializedReminder.userId, equals(userId)); - expect(deserializedReminder.remindAt, equals(remindAt)); - expect(deserializedReminder.createdAt, equals(now)); - expect(deserializedReminder.updatedAt, equals(now)); - }); - test('should implement equality correctly', () { final reminder1 = MessageReminder( channelCid: channelCid, @@ -259,6 +223,19 @@ void main() { expect(mergedReminder.updatedAt, equals(newUpdatedAt)); }); + test('MessageReminder.merge keeps the channel, message and user the other reminder lacks', () { + final channel = ChannelModel(cid: channelCid); + final message = Message(id: messageId, text: 'Remember me'); + final user = User(id: userId); + final listedReminder = messageReminder.copyWith(channel: channel, message: message, user: user); + // An updated reminder can lack its channel, message and user. + final eventReminder = messageReminder.copyWith(updatedAt: now.add(const Duration(hours: 1))); + + final mergedReminder = listedReminder.merge(eventReminder); + + expect((mergedReminder.channel, mergedReminder.message, mergedReminder.user), (channel, message, user)); + }); + test('should return original instance when merging with null', () { final mergedReminder = messageReminder.merge(null); diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index 9b30ca8a10..e54d9c8472 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -54,6 +54,11 @@ void main() { expect(message.i18n, null); expect(message.restrictedVisibility, isA>()); expect(message.channelRole, 'channel_member'); + expect(message.cid, 'messaging:general'); + expect(message.html, '

Hi

'); + expect(message.mml, 'Hi'); + expect(message.deletedReplyCount, 2); + expect(message.custom, {'status': 'SENT'}); }); test('should serialize to json correctly', () { @@ -82,7 +87,7 @@ void main() { showInChannel: true, parentId: 'parentId', restrictedVisibility: const ['user-id-3'], - extraData: const {'hey': 'test'}, + custom: const {'hey': 'test'}, ); expect( @@ -377,11 +382,11 @@ void main() { ); }); - test('a custom field orders by the message extra data', () { + test('a custom field orders by the message custom data', () { expectOrders( MessageSearchSortField.custom('priority'), - createTestMessage(id: 'a', extraData: const {'priority': 1}), - createTestMessage(id: 'b', extraData: const {'priority': 10}), + createTestMessage(id: 'a', custom: const {'priority': 1}), + createTestMessage(id: 'b', custom: const {'priority': 10}), ); }); @@ -458,26 +463,136 @@ void main() { ); test( - 'synthesizes groups from legacy reaction_counts/reaction_scores ' - 'even when the score total is zero or negative', + 'Message.fromJson synthesizes groups from legacy reaction_counts/reaction_scores ' + 'for the types with a positive count, whatever their score', () { final message = Message.fromJson(const { - 'reaction_counts': {'like': 2, 'dislike': 3}, - 'reaction_scores': {'like': 0, 'dislike': -3}, + 'reaction_counts': {'like': 2, 'dislike': 3, 'wow': 0}, + 'reaction_scores': {'like': 0, 'dislike': -3, 'wow': 0}, }); - // Both groups must be retained because their count is positive, even - // though the summed scores are zero and negative respectively. - expect(message.reactionGroups, isNotNull); + // 'like' and 'dislike' are retained because their count is positive, + // even though the summed scores are zero and negative respectively; + // 'wow' has no reaction left. + expect(message.reactionGroups!.keys, unorderedEquals(['like', 'dislike'])); expect(message.reactionGroups!['like']!.count, 2); expect(message.reactionGroups!['like']!.sumScores, 0); expect(message.reactionGroups!['dislike']!.count, 3); expect(message.reactionGroups!['dislike']!.sumScores, -3); }, ); + + test('Message.fromJson prefers reaction_groups over legacy reaction_counts/reaction_scores', () { + final message = Message.fromJson(const { + 'reaction_groups': { + 'like': { + 'count': 1, + 'sum_scores': 2, + 'first_reaction_at': '2026-01-05T00:00:00.000Z', + 'last_reaction_at': '2026-01-06T00:00:00.000Z', + }, + }, + // The groups' dates tell them apart from groups built from these. + 'reaction_counts': {'like': 1}, + 'reaction_scores': {'like': 2}, + }); + + expect(message.reactionGroups, { + 'like': ReactionGroup( + count: 1, + sumScores: 2, + firstReactionAt: DateTime.utc(2026, 1, 5), + lastReactionAt: DateTime.utc(2026, 1, 6), + ), + }); + }); }); }); + test('Message.toJson leaves out custom data named like a field', () { + final message = Message(text: 'Hi', custom: const {'text': 'custom', 'priority': 'high'}); + + final json = message.toJson(); + + expect(json['text'], 'Hi'); + expect(json['priority'], 'high'); + }); + + test('Message.toJson leaves out the fields only fromJson reads', () { + final message = Message(cid: 'messaging:general', html: '

Hi

', mml: 'Hi', deletedReplyCount: 2); + + expect( + message.toJson().keys, + isNot(anyOf(contains('cid'), contains('html'), contains('mml'), contains('deleted_reply_count'))), + ); + }); + + test('Message.copyWith keeps the channel id, HTML, markup and deleted reply count', () { + final message = Message(cid: 'messaging:general', html: '

Hi

', mml: 'Hi', deletedReplyCount: 2); + + expect( + message.copyWith(text: 'Hi'), + Message( + id: message.id, + text: 'Hi', + cid: 'messaging:general', + html: '

Hi

', + mml: 'Hi', + deletedReplyCount: 2, + ), + ); + }); + + test( + 'Message.fromJson reads a message deleted only for the current user as deleted, even when it is soft-deleted', + () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'type': 'regular', + 'deleted_for_me': true, + 'deleted_at': '2026-01-03T00:00:00.000Z', + 'created_at': '2026-01-01T00:00:00.000Z', + 'updated_at': '2026-01-02T00:00:00.000Z', + }); + + expect((message.type, message.state), (MessageType.deleted, MessageState.deletedForMe)); + }, + ); + + test('Message.fromJson reads a deleted message as soft-deleted', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'type': 'deleted', + 'deleted_at': '2026-01-03T00:00:00.000Z', + 'created_at': '2026-01-01T00:00:00.000Z', + 'updated_at': '2026-01-02T00:00:00.000Z', + }); + + expect((message.type, message.state), (MessageType.deleted, MessageState.softDeleted)); + }); + + test('Message.fromJson reads a message updated after it was created as updated', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'type': 'regular', + 'created_at': '2026-01-01T00:00:00.000Z', + 'updated_at': '2026-01-02T00:00:00.000Z', + }); + + expect((message.type, message.state), (MessageType.regular, MessageState.updated)); + }); + + test('Message.fromJson reads a message never updated as sent', () { + final message = Message.fromJson(const { + 'id': 'message-id', + 'type': 'regular', + 'created_at': '2026-01-01T00:00:00.000Z', + 'updated_at': '2026-01-01T00:00:00.000Z', + }); + + expect((message.type, message.state), (MessageType.regular, MessageState.sent)); + }); + group('MessageVisibility Extension Tests', () { group('hasRestrictedVisibility', () { test('should return false when restrictedVisibility is null', () { @@ -1026,7 +1141,7 @@ Message createTestMessage({ DateTime? createdAt, DateTime? updatedAt, User? user, - Map? extraData, + Map? custom, List? restrictedVisibility, Poll? poll, String? pollId, @@ -1044,7 +1159,7 @@ Message createTestMessage({ localCreatedAt: createdAt, localUpdatedAt: updatedAt, user: user, - extraData: extraData ?? {}, + custom: custom ?? {}, restrictedVisibility: restrictedVisibility, poll: poll, pollId: pollId, diff --git a/packages/stream_chat/test/src/core/models/moderation_test.dart b/packages/stream_chat/test/src/core/models/moderation_test.dart index 15ac7b2e16..58f27cccdd 100644 --- a/packages/stream_chat/test/src/core/models/moderation_test.dart +++ b/packages/stream_chat/test/src/core/models/moderation_test.dart @@ -39,50 +39,6 @@ void main() { expect(moderation.platformCircumvented, isTrue); }); - test('should create from JSON correctly', () { - final json = { - 'action': 'shadow', - 'original_text': 'original message text', - 'text_harms': ['hate', 'profanity'], - 'image_harms': ['explicit'], - 'blocklist_matched': 'profanity', - 'semantic_filter_matched': 'harassment', - 'platform_circumvented': true, - }; - - final moderation = Moderation.fromJson(json); - - expect(moderation.action, ModerationAction.shadow); - expect(moderation.originalText, 'original message text'); - expect(moderation.textHarms, ['hate', 'profanity']); - expect(moderation.imageHarms, ['explicit']); - expect(moderation.blocklistMatched, 'profanity'); - expect(moderation.semanticFilterMatched, 'harassment'); - expect(moderation.platformCircumvented, isTrue); - }); - - test('should serialize to JSON correctly', () { - const moderation = Moderation( - action: ModerationAction.bounce, - originalText: 'original message text', - textHarms: ['hate', 'profanity'], - imageHarms: ['explicit'], - blocklistMatched: 'profanity', - semanticFilterMatched: 'harassment', - platformCircumvented: true, - ); - - final json = moderation.toJson(); - - expect(json['action'], 'bounce'); - expect(json['original_text'], 'original message text'); - expect(json['text_harms'], ['hate', 'profanity']); - expect(json['image_harms'], ['explicit']); - expect(json['blocklist_matched'], 'profanity'); - expect(json['semantic_filter_matched'], 'harassment'); - expect(json['platform_circumvented'], isTrue); - }); - test('equals & hashCode should work correctly', () { const moderation1 = Moderation( action: ModerationAction.remove, diff --git a/packages/stream_chat/test/src/core/models/reaction_group_test.dart b/packages/stream_chat/test/src/core/models/reaction_group_test.dart index 602ba6ee41..7f62a3bd39 100644 --- a/packages/stream_chat/test/src/core/models/reaction_group_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_group_test.dart @@ -22,18 +22,31 @@ void main() { expect(reactionGroup.lastReactionAt, lastReactionAtDate); }); - test('fromJson creates correct object', () { - final json = { + test('toData writes snake_case keys and ISO-8601 dates', () { + final reactionGroup = ReactionGroup( + count: count, + sumScores: sumScores, + firstReactionAt: firstReactionAtDate, + lastReactionAt: lastReactionAtDate, + ); + + expect(reactionGroup.toData(), { 'count': count, 'sum_scores': sumScores, - 'first_reaction_at': firstReactionAtDate.toIso8601String(), - 'last_reaction_at': lastReactionAtDate.toIso8601String(), - }; - final reactionGroup = ReactionGroup.fromJson(json); - expect(reactionGroup.count, count); - expect(reactionGroup.sumScores, sumScores); - expect(reactionGroup.firstReactionAt, firstReactionAtDate); - expect(reactionGroup.lastReactionAt, lastReactionAtDate); + 'first_reaction_at': '2023-10-26T10:00:00.000Z', + 'last_reaction_at': '2023-10-26T12:00:00.000Z', + }); + }); + + test('fromData reads back the group written by toData', () { + final reactionGroup = ReactionGroup( + count: count, + sumScores: sumScores, + firstReactionAt: firstReactionAtDate, + lastReactionAt: lastReactionAtDate, + ); + + expect(ReactionGroup.fromData(reactionGroup.toData()), reactionGroup); }); test('copyWith creates a new object with updated values', () { @@ -82,21 +95,6 @@ void main() { expect(copiedGroup.lastReactionAt, lastReactionAtDate); }); - test('props returns correct list of properties', () { - final reactionGroup = ReactionGroup( - count: count, - sumScores: sumScores, - firstReactionAt: firstReactionAtDate, - lastReactionAt: lastReactionAtDate, - ); - expect(reactionGroup.props, [ - count, - sumScores, - firstReactionAtDate, - lastReactionAtDate, - ]); - }); - test('equality works correctly', () { final reactionGroup1 = ReactionGroup( count: count, diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index e2501d0e60..b1db8e50fc 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -4,63 +4,25 @@ import 'package:test/test.dart'; import '../../utils.dart'; +// The reaction in fixtures/reaction.json, built directly. +Reaction _fixtureReaction() { + final json = jsonFixture('reaction.json'); + return Reaction( + messageId: json['message_id'] as String, + type: json['type'] as String, + user: User.fromJson(json['user'] as Map), + userId: json['user_id'] as String, + score: json['score'] as int, + emojiCode: json['emoji_code'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); +} + void main() { group('src/models/reaction', () { - test('should parse json correctly', () { - final reaction = Reaction.fromJson(jsonFixture('reaction.json')); - expect(reaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); - expect(reaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); - expect(reaction.updatedAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); - expect(reaction.type, 'wow'); - expect( - reaction.user?.toJson(), - { - 'id': '2de0297c-f3f2-489d-b930-ef77342edccf', - 'role': 'user', - 'teams': [], - 'created_at': '2020-01-28T22:17:30.810011Z', - 'updated_at': '2020-01-28T22:17:31.077195Z', - 'online': false, - 'banned': false, - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan', - 'blocked_user_ids': [], - }, - ); - expect(reaction.score, 1); - expect(reaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); - expect(reaction.emojiCode, '😮'); - }); - - test('should serialize to json correctly', () { - final reaction = Reaction( - messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', - createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), - updatedAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), - type: 'wow', - user: User( - id: '2de0297c-f3f2-489d-b930-ef77342edccf', - image: 'https://randomuser.me/api/portraits/women/45.jpg', - name: 'Daisy Morgan', - ), - userId: '2de0297c-f3f2-489d-b930-ef77342edccf', - extraData: const {'bananas': 'yes'}, - emojiCode: '😮', - ); - - expect( - reaction.toJson(), - { - 'type': 'wow', - 'score': 1, - 'emoji_code': '😮', - 'bananas': 'yes', - }, - ); - }); - test('copyWith', () { - final reaction = Reaction.fromJson(jsonFixture('reaction.json')); + final reaction = _fixtureReaction(); var newReaction = reaction.copyWith(); expect(newReaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); expect(newReaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); @@ -92,7 +54,7 @@ void main() { emojiCode: '😂', createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), updatedAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), - extraData: {}, + custom: {}, messageId: 'test', score: 2, user: User( @@ -113,7 +75,7 @@ void main() { newReaction.updatedAt, DateTime.parse('2021-01-28T22:17:31.108742Z'), ); - expect(newReaction.extraData, {}); + expect(newReaction.custom, {}); expect(newReaction.messageId, 'test'); expect(newReaction.score, 2); expect( @@ -137,8 +99,19 @@ void main() { }); }); + test('Reaction.merge keeps the message id, user and emoji code the other reaction lacks', () { + final reaction = _fixtureReaction(); + + final merged = reaction.merge(Reaction(type: 'lol')); + + expect( + (merged.messageId, merged.user, merged.userId, merged.emojiCode), + (reaction.messageId, reaction.user, reaction.userId, reaction.emojiCode), + ); + }); + test('merge', () { - final reaction = Reaction.fromJson(jsonFixture('reaction.json')); + final reaction = _fixtureReaction(); final newUserCreateTime = DateTime.now(); final newReaction = reaction.merge( @@ -168,7 +141,7 @@ void main() { newReaction.updatedAt, DateTime.parse('2021-01-28T22:17:31.108742Z'), ); - expect(newReaction.extraData, {}); + expect(newReaction.custom, {}); expect(newReaction.messageId, 'test'); expect(newReaction.score, 2); expect( diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 832bc63686..f93f90ac97 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -177,7 +177,7 @@ class _FileAttachmentSubtitle extends StatelessWidget { final colorScheme = context.streamColorScheme; - final attachmentSize = attachment.file?.size ?? attachment.extraData['file_size']; + final attachmentSize = attachment.file?.size ?? attachment.custom['file_size']; return attachment.uploadState.when( success: () => Text(fileSize(attachmentSize)), diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart index 814216648a..eb9ad31afb 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart @@ -111,7 +111,7 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { final duration = videoController.value.duration; if (duration.inSeconds > 0) { return attachment.copyWith( - extraData: {...attachment.extraData, 'duration': duration.inSeconds}, + custom: {...attachment.custom, 'duration': duration.inSeconds}, ); } } catch (_) { diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart index bb8d865535..3e0d212948 100644 --- a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart @@ -103,7 +103,7 @@ class _DefaultStreamMessageComposerInputHeader extends StatelessWidget { final attachment = voiceRecordings.elementAtOrNull(index); if (attachment == null) return child; - final durationSecs = attachment.extraData['duration'] as num?; + final durationSecs = attachment.custom['duration'] as num?; final duration = durationSecs != null ? Duration(seconds: durationSecs.round()) : null; final a11y = context.translations.accessibility; diff --git a/packages/stream_chat_flutter/lib/src/media_gallery/stream_media_gallery_item.dart b/packages/stream_chat_flutter/lib/src/media_gallery/stream_media_gallery_item.dart index b38ce4c690..645af70bef 100644 --- a/packages/stream_chat_flutter/lib/src/media_gallery/stream_media_gallery_item.dart +++ b/packages/stream_chat_flutter/lib/src/media_gallery/stream_media_gallery_item.dart @@ -10,7 +10,7 @@ import '../../stream_chat_flutter.dart'; /// white outside-aligned border so it reads against any thumbnail. /// - A "video" [StreamMediaBadge] pinned to the bottom-leading corner when /// [attachment] is a video; the badge surfaces the video's duration in -/// `M:SS` format when present on `extraData['duration']`. +/// `M:SS` format when present on `custom['duration']`. /// /// The tile is tappable when [onTap] / [onLongPress] is set; the ripple is /// drawn over the media via a transparent [Material]. @@ -67,7 +67,7 @@ class StreamMediaGalleryItem extends StatelessWidget { Duration? videoDuration; if (isVideo) { - final secs = attachment.extraData['duration'] as num?; + final secs = attachment.custom['duration'] as num?; if (secs != null) videoDuration = Duration(seconds: secs.round()); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart index 8e27ac104b..c378e0bee8 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart @@ -297,25 +297,25 @@ extension StreamImagePickerX on StreamAttachmentPickerController { bytes: cachedFile.readAsBytesSync(), ); - final extraDataMap = {}; + final customMap = {}; final mimeType = file.mediaType?.mimeType; if (mimeType != null) { - extraDataMap['mime_type'] = mimeType; + customMap['mime_type'] = mimeType; } - extraDataMap['file_size'] = file.size!; + customMap['file_size'] = file.size!; if (type == AssetType.video) { - extraDataMap['duration'] = asset.videoDuration.inSeconds; + customMap['duration'] = asset.videoDuration.inSeconds; } final attachment = Attachment( id: asset.id, file: file, type: asset.type.toAttachmentType(), - extraData: extraDataMap, + custom: customMap, ); return addAttachment(attachment); diff --git a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart index ca23ecd0f6..d4e1081962 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart @@ -299,7 +299,7 @@ extension on RecordStateRecording { final attachment = Attachment( file: attachmentFile, type: AttachmentType.voiceRecording, - extraData: { + custom: { 'duration': duration.inMilliseconds / 1000, 'waveform_data': sampling.resampleWaveformData(waveform, 100), }, diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart index 48d7acef99..55ed2cf2d7 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart @@ -88,7 +88,7 @@ class DefaultMessageComposerAttachment extends StatelessWidget { } Widget _buildFileAttachment(BuildContext context) { - final fileSizeBytes = attachment.file?.size ?? attachment.extraData['file_size']; + final fileSizeBytes = attachment.file?.size ?? attachment.custom['file_size']; final mimeType = attachment.file?.mediaType?.mimeType; return StreamMessageComposerFileAttachment( @@ -223,7 +223,7 @@ class StreamMediaAttachmentBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - final durationSecs = attachment.extraData['duration'] as num?; + final durationSecs = attachment.custom['duration'] as num?; final videoDuration = durationSecs != null ? Duration(seconds: durationSecs.round()) : null; final a11y = context.translations.accessibility; diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index 1deaab0667..96ad12966c 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -140,20 +140,20 @@ extension XFileX on XFile { Future toAttachment({required String type}) async { final file = await toAttachmentFile; - final extraDataMap = {}; + final customMap = {}; final mimeType = this.mimeType ?? file.mediaType?.mimeType; if (mimeType != null) { - extraDataMap['mime_type'] = mimeType; + customMap['mime_type'] = mimeType; } - extraDataMap['file_size'] = file.size!; + customMap['file_size'] = file.size!; final attachment = Attachment( file: file, type: type, - extraData: extraDataMap, + custom: customMap, ); return attachment; @@ -718,7 +718,7 @@ extension VoiceRecordingAttachmentExtension on Attachment { /// Returns the duration of the voice recording attachment if available else /// returns [Duration.zero]. Duration get duration { - final duration = extraData['duration'] as num?; + final duration = custom['duration'] as num?; if (duration == null) return Duration.zero; return Duration(milliseconds: (duration * 1000).round()); @@ -727,7 +727,7 @@ extension VoiceRecordingAttachmentExtension on Attachment { /// Returns the waveform data of the voice recording attachment if available /// else returns an empty list. List get waveform { - final waveform = extraData['waveform_data'] as List?; + final waveform = custom['waveform_data'] as List?; if (waveform == null) return []; return [...waveform.map((e) => double.tryParse(e.toString())).nonNulls]; diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart index b7f449e271..7fde6ebfe4 100644 --- a/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart @@ -10,7 +10,7 @@ void main() { final attachment = Attachment( title: 'test image attachment', type: 'image', - extraData: const { + custom: const { 'mime_type': 'png', }, ); @@ -29,7 +29,7 @@ void main() { final attachment = Attachment( title: 'test giphy attachment', type: 'giphy', - extraData: const { + custom: const { 'original': 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', }, ); diff --git a/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart index 07aa6c1991..9f9a3127a3 100644 --- a/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart @@ -32,7 +32,7 @@ void main() { file: Attachment( type: 'file', title: 'example.pdf', - extraData: const { + custom: const { 'mime_type': 'pdf', }, ), diff --git a/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart index c7bde80ec0..9c855c273d 100644 --- a/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart @@ -19,7 +19,7 @@ void main() { type: 'image', title: 'example.png', imageUrl: 'https://logowik.com/content/uploads/images/flutter5786.jpg', - extraData: const { + custom: const { 'mime_type': 'png', }, ), @@ -27,7 +27,7 @@ void main() { type: 'image', title: 'example.png', imageUrl: 'https://logowik.com/content/uploads/images/flutter5786.jpg', - extraData: const { + custom: const { 'mime_type': 'png', }, ), diff --git a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart index 6035125414..525f1a28f5 100644 --- a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart @@ -33,7 +33,7 @@ void main() { type: 'giphy', title: 'example.gif', imageUrl: 'https://media.giphy.com/media/35H0pwQNaO2iLTnnBf/giphy.gif', - extraData: const { + custom: const { 'mime_type': 'gif', }, uploadState: const UploadState.success(), diff --git a/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart index 0aaca1f2e2..fd70afab5c 100644 --- a/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart @@ -33,7 +33,7 @@ void main() { type: 'image', title: 'example.png', imageUrl: 'https://logowik.com/content/uploads/images/flutter5786.jpg', - extraData: const { + custom: const { 'mime_type': 'png', }, ), diff --git a/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart b/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart index 4d9edc66dc..0389c10e7e 100644 --- a/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart @@ -15,7 +15,7 @@ void main() { size: 10000, path: 'voice_recordings/test1.m4a', ), - extraData: { + custom: { 'duration': 300, 'waveform_data': List.filled(50, 0.5), }, @@ -28,7 +28,7 @@ void main() { size: 30000, path: 'voice_recordings/test2.m4a', ), - extraData: { + custom: { 'duration': 900, 'waveform_data': List.filled(50, 0.5), }, diff --git a/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart b/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart index b431ac94d2..4510271bb5 100644 --- a/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart @@ -326,7 +326,7 @@ void main() { attachments: [ Attachment( type: AttachmentType.voiceRecording, - extraData: const {'duration': 125}, + custom: const {'duration': 125}, ), ], ); @@ -1033,7 +1033,7 @@ void main() { attachments: [ Attachment( type: 'product', - extraData: const {'title': 'iPhone'}, + custom: const {'title': 'iPhone'}, ), ], ); @@ -1354,7 +1354,7 @@ void main() { attachments: [ Attachment( type: 'voiceRecording', - extraData: const {'duration': 83.0}, + custom: const {'duration': 83.0}, ), ], ); @@ -1584,7 +1584,7 @@ void main() { attachments: [ Attachment( type: AttachmentType.voiceRecording, - extraData: const {'duration': 83.0}, + custom: const {'duration': 83.0}, ), ], ); @@ -1680,7 +1680,7 @@ void main() { attachments: [ Attachment( type: AttachmentType.voiceRecording, - extraData: const {'duration': 83.0}, + custom: const {'duration': 83.0}, ), ], ); @@ -1879,7 +1879,7 @@ class _CustomMessagePreviewFormatter extends StreamMessagePreviewFormatter { final attachment = attachments.firstOrNull; if (attachment?.type == 'product') { - final title = attachment?.extraData['title'] as String?; + final title = attachment?.custom['title'] as String?; return TextSpan(text: '🛍️ ${title ?? "Product"}'); } diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart index f4dc96144f..f622a533c6 100644 --- a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart @@ -167,8 +167,8 @@ void main() { await controller.stopRecord(); final stoppedState = controller.value as RecordStateStopped; - expect(stoppedState.audioRecording.extraData['duration'], isNotNull); - expect(stoppedState.audioRecording.extraData['waveform_data'], isNotNull); + expect(stoppedState.audioRecording.custom['duration'], isNotNull); + expect(stoppedState.audioRecording.custom['waveform_data'], isNotNull); }); }); 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 05a5defd7b..26c34ae8eb 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 @@ -126,7 +126,7 @@ void main() { expect(attachment.type, AttachmentType.image); expect(attachment.file?.name, 'cat.gif'); expect(attachment.file?.size, _gifBytes.length); - expect(attachment.extraData['mime_type'], 'image/gif'); + expect(attachment.custom['mime_type'], 'image/gif'); }); testWidgets('names the file from the MIME type when the URI has no extension', (tester) async { @@ -137,7 +137,7 @@ void main() { final attachment = controller.attachments.single; expect(attachment.file?.name, endsWith('.webp')); - expect(attachment.extraData['mime_type'], 'image/webp'); + expect(attachment.custom['mime_type'], 'image/webp'); }); testWidgets('ignores content the keyboard handed over without data', (tester) async { @@ -214,7 +214,7 @@ void main() { final attachment = controller.attachments.single; expect(attachment.file?.name, endsWith('.gif')); - expect(attachment.extraData['mime_type'], 'image/gif'); + expect(attachment.custom['mime_type'], 'image/gif'); }); }); } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart index 10db0cc0d7..e93c8e62c7 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart @@ -105,58 +105,53 @@ class StreamMessageReminderListController extends PagedValueNotifier value = PagedValue.error(_chatException(error, 'Failed to load message reminders')), + ); } @override Future loadMore(String nextPageKey) async { final previousValue = value.asSuccess; - try { - final response = await client.queryReminders( - sort: _activeSort, - filter: _activeFilter, - pagination: PaginationParams(limit: limit, next: nextPageKey), - ); - - final results = response.reminders; - final previousItems = previousValue.items; - final newItems = previousItems + results; - final next = response.next; - final nextKey = next != null && next.isNotEmpty ? next : null; - value = PagedValue( - items: newItems, - nextPageKey: nextKey, - ); - } on StreamChatException catch (error) { - value = previousValue.copyWith(error: error); - } catch (error) { - final chatError = StreamClientException(message: 'Failed to load more message reminders', cause: error); - value = previousValue.copyWith(error: chatError); - } + final result = await client.queryReminders( + sort: _activeSort, + filter: _activeFilter, + limit: limit, + next: nextPageKey, + ); + + result.fold( + onSuccess: (response) { + final next = response.next; + value = PagedValue( + items: previousValue.items + response.reminders, + nextPageKey: next != null && next.isNotEmpty ? next : null, + ); + }, + onFailure: (error, _) { + value = previousValue.copyWith(error: _chatException(error, 'Failed to load more message reminders')); + }, + ); } + StreamChatException _chatException(Object error, String message) => switch (error) { + final StreamChatException error => error, + _ => StreamClientException(message: message, cause: error), + }; + /// Event listener, which can be set in order to listen /// [client] web-socket events. /// diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index acfed462e5..ffdded80d9 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -29,7 +29,7 @@ void main() { createdAt: DateTime.now(), replyCount: i, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$i', pinned: true, pinnedAt: DateTime.now(), @@ -49,7 +49,7 @@ void main() { createdAt: DateTime.now(), replyCount: i, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$i', pinned: true, pinnedAt: DateTime.now(), diff --git a/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart index e1c4967292..68d6edef51 100644 --- a/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart @@ -66,10 +66,6 @@ List generateMessageReminders({ void main() { final client = MockClient(); - setUpAll(() { - registerFallbackValue(const PaginationParams()); - }); - setUp(() { when(client.on).thenAnswer((_) => const Stream.empty()); }); @@ -98,17 +94,18 @@ void main() { }); group('Initial loading', () { - test('successfully loads message reminders from API', () async { + test('StreamMessageReminderListController.doInitialLoad loads the first page and the cursor of the next', () async { final reminders = generateMessageReminders(); - final response = QueryRemindersResponse() - ..reminders = reminders - ..next = null; + final response = Result.success( + QueryRemindersResponse(duration: '0.01ms', reminders: reminders, next: 'next-cursor'), + ); when( () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: any(named: 'limit'), + next: any(named: 'next'), ), ).thenAnswer((_) async => response); @@ -121,12 +118,13 @@ void main() { () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: 30, ), ).called(1); expect(controller.value, isA>()); expect(controller.value.asSuccess.items, equals(reminders)); + expect(controller.value.asSuccess.nextPageKey, 'next-cursor'); }); test('an empty sort queries without a sort term', () async { @@ -136,13 +134,12 @@ void main() { () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: any(named: 'limit'), + next: any(named: 'next'), ), ).thenAnswer((invocation) async { sorts.add(invocation.namedArguments[const Symbol('sort')]); - return QueryRemindersResponse() - ..reminders = generateMessageReminders() - ..next = null; + return Result.success(QueryRemindersResponse(duration: '0.01ms', reminders: generateMessageReminders())); }); final controller = StreamMessageReminderListController( @@ -156,15 +153,16 @@ void main() { expect(sorts.single, isEmpty); }); - test('handles a Stream failure properly', () async { + test('StreamMessageReminderListController.doInitialLoad reports a failed query as an error', () async { const chatError = StreamNetworkException(message: 'Network error'); when( () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: any(named: 'limit'), + next: any(named: 'next'), ), - ).thenThrow(chatError); + ).thenAnswer((_) async => const Result.failure(chatError)); final controller = StreamMessageReminderListController(client: client); @@ -174,30 +172,54 @@ void main() { expect(controller.value, isA()); expect((controller.value as Error).error, equals(chatError)); }); + + test( + 'StreamMessageReminderListController.doInitialLoad wraps a failure that is not a chat exception', + () async { + final cause = StateError('boom'); + when( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + limit: any(named: 'limit'), + next: any(named: 'next'), + ), + ).thenAnswer((_) async => Result.failure(cause)); + + final controller = StreamMessageReminderListController(client: client); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + final error = controller.value.mapOrNull(null, error: (it) => it.error); + expect(error, isA().having((it) => it.cause, 'cause', cause)); + }, + ); }); group('Pagination', () { - test('loadMore appends new reminders to existing items', () async { + test('StreamMessageReminderListController.loadMore appends the last page and stops paging', () async { const nextKey = 'next_page_token'; final existingReminders = generateMessageReminders(); - final additionalReminders = generateMessageReminders( - count: 1, - startId: 789, - texts: ['Reminder 3'], - channelCids: ['messaging:789'], - messageIds: ['message_789'], - userIds: ['user_789'], - ); + // Due after the existing ones, which come first in the default sort. + final additionalReminders = [ + generateMessageReminder( + channelCid: 'messaging:789', + messageId: 'message_789', + userId: 'user_789', + text: 'Reminder 3', + remindAt: DateTime.now().add(const Duration(hours: 3)), + ), + ]; - final response = QueryRemindersResponse() - ..reminders = additionalReminders - ..next = null; + final response = Result.success(QueryRemindersResponse(duration: '0.01ms', reminders: additionalReminders)); when( () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: any(named: 'limit'), + next: any(named: 'next'), ), ).thenAnswer((_) async => response); @@ -212,44 +234,79 @@ void main() { await controller.loadMore(nextKey); await pumpEventQueue(); + verify( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + limit: 10, + next: nextKey, + ), + ).called(1); + final mergedReminders = [...existingReminders, ...additionalReminders]; - expect( - controller.value.asSuccess.items.length, - equals(mergedReminders.length), - ); + expect(controller.value.asSuccess.items, equals(mergedReminders)); expect(controller.value.asSuccess.nextPageKey, isNull); }); - test('loadMore handles a Stream failure properly', () async { + test('StreamMessageReminderListController.loadMore keeps the cursor of the following page', () async { const nextKey = 'next_page_token'; - final existingReminders = generateMessageReminders(); - const chatError = StreamNetworkException(message: 'Network error'); - + const page = Result.success( + QueryRemindersResponse(duration: '0.01ms', reminders: [], next: 'later-cursor'), + ); when( () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: any(named: 'limit'), + next: any(named: 'next'), ), - ).thenThrow(chatError); + ).thenAnswer((_) async => page); final controller = StreamMessageReminderListController.fromValue( - PagedValue( - items: existingReminders, - nextPageKey: nextKey, - ), + PagedValue(items: generateMessageReminders(), nextPageKey: nextKey), client: client, ); await controller.loadMore(nextKey); await pumpEventQueue(); - expect(controller.value.isSuccess, isTrue); - expect(controller.value.asSuccess.items, equals(existingReminders)); - expect(controller.value.asSuccess.error, equals(chatError)); + expect(controller.value.asSuccess.nextPageKey, 'later-cursor'); }); + + test( + 'StreamMessageReminderListController.loadMore keeps the loaded reminders and reports a failed query', + () async { + const nextKey = 'next_page_token'; + final existingReminders = generateMessageReminders(); + const chatError = StreamNetworkException(message: 'Network error'); + + when( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + limit: any(named: 'limit'), + next: any(named: 'next'), + ), + ).thenAnswer((_) async => const Result.failure(chatError)); + + final controller = StreamMessageReminderListController.fromValue( + PagedValue( + items: existingReminders, + nextPageKey: nextKey, + ), + client: client, + ); + + await controller.loadMore(nextKey); + await pumpEventQueue(); + + expect(controller.value.isSuccess, isTrue); + expect(controller.value.asSuccess.items, equals(existingReminders)); + expect(controller.value.asSuccess.error, equals(chatError)); + }, + ); }); group('Message Reminder CRUD operations', () { @@ -361,12 +418,11 @@ void main() { () => client.queryReminders( filter: any(named: 'filter'), sort: any(named: 'sort'), - pagination: any(named: 'pagination'), + limit: any(named: 'limit'), + next: any(named: 'next'), ), ).thenAnswer( - (_) async => QueryRemindersResponse() - ..reminders = initialReminders - ..next = null, + (_) async => Result.success(QueryRemindersResponse(duration: '0.01ms', reminders: initialReminders)), ); }); diff --git a/packages/stream_chat_persistence/lib/src/converter/reaction_groups_converter.dart b/packages/stream_chat_persistence/lib/src/converter/reaction_groups_converter.dart index 8964f8ae7e..eaf201893f 100644 --- a/packages/stream_chat_persistence/lib/src/converter/reaction_groups_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/reaction_groups_converter.dart @@ -4,14 +4,15 @@ import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; import 'converter.dart'; -/// A [TypeConverter] that serializes [VotingVisibility] to a [String] column. +/// A [TypeConverter] that stores a message's [ReactionGroup]s as a JSON +/// [String] column. class ReactionGroupsConverter extends MapConverter { @override Map fromSql(String fromDb) { final json = jsonDecode(fromDb) as Map; return json.map( (key, e) { - final group = ReactionGroup.fromJson(e as Map); + final group = ReactionGroup.fromData(e as Map); return MapEntry(key, group); }, ); @@ -21,7 +22,7 @@ class ReactionGroupsConverter extends MapConverter { String toSql(Map value) { return jsonEncode( value.map( - (k, e) => MapEntry(k, e.toJson()), + (k, e) => MapEntry(k, e.toData()), ), ); } 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 d98ecdfcea..00246adfd6 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 + 7; + int get schemaVersion => 1100 + 8; // 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 7bc61450e7..98b52db30d 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 @@ -1719,14 +1719,43 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, MessageEnti ).withConverter?>( $MessagesTable.$converterrestrictedVisibilityn, ); + static const VerificationMeta _htmlMeta = const VerificationMeta('html'); @override - late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn( - 'extra_data', + late final GeneratedColumn html = GeneratedColumn( + 'html', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - ).withConverter?>($MessagesTable.$converterextraDatan); + ); + static const VerificationMeta _mmlMeta = const VerificationMeta('mml'); + @override + late final GeneratedColumn mml = GeneratedColumn( + 'mml', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _deletedReplyCountMeta = const VerificationMeta( + 'deletedReplyCount', + ); + @override + late final GeneratedColumn deletedReplyCount = GeneratedColumn( + 'deleted_reply_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( + 'custom', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($MessagesTable.$convertercustomn); @override List get $columns => [ id, @@ -1765,7 +1794,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, MessageEnti channelCid, i18n, restrictedVisibility, - extraData, + html, + mml, + deletedReplyCount, + custom, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -1995,6 +2027,27 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, MessageEnti } else if (isInserting) { context.missing(_channelCidMeta); } + if (data.containsKey('html')) { + context.handle( + _htmlMeta, + html.isAcceptableOrUnknown(data['html']!, _htmlMeta), + ); + } + if (data.containsKey('mml')) { + context.handle( + _mmlMeta, + mml.isAcceptableOrUnknown(data['mml']!, _mmlMeta), + ); + } + if (data.containsKey('deleted_reply_count')) { + context.handle( + _deletedReplyCountMeta, + deletedReplyCount.isAcceptableOrUnknown( + data['deleted_reply_count']!, + _deletedReplyCountMeta, + ), + ); + } return context; } @@ -2164,10 +2217,22 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, MessageEnti data['${effectivePrefix}restricted_visibility'], ), ), - extraData: $MessagesTable.$converterextraDatan.fromSql( + html: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}html'], + ), + mml: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mml'], + ), + deletedReplyCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_reply_count'], + ), + custom: $MessagesTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), ); @@ -2201,9 +2266,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, MessageEnti static TypeConverter?, String?> $converterrestrictedVisibilityn = NullAwareTypeConverter.wrap( $converterrestrictedVisibility, ); - static TypeConverter, String> $converterextraData = MapConverter(); - static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( - $converterextraData, + static TypeConverter, String> $convertercustom = MapConverter(); + static TypeConverter?, String?> $convertercustomn = NullAwareTypeConverter.wrap( + $convertercustom, ); } @@ -2318,8 +2383,17 @@ class MessageEntity extends DataClass implements Insertable { /// The list of user ids that should be able to see the message. final List? restrictedVisibility; - /// Message custom extraData - final Map? extraData; + /// The message rendered as HTML. + final String? html; + + /// The message markup language (MML) source of the message. + final String? mml; + + /// The number of replies to the message that were deleted. + final int? deletedReplyCount; + + /// Custom data attached to the message. + final Map? custom; const MessageEntity({ required this.id, this.messageText, @@ -2357,7 +2431,10 @@ class MessageEntity extends DataClass implements Insertable { required this.channelCid, this.i18n, this.restrictedVisibility, - this.extraData, + this.html, + this.mml, + this.deletedReplyCount, + this.custom, }); @override Map toColumns(bool nullToAbsent) { @@ -2474,9 +2551,18 @@ class MessageEntity extends DataClass implements Insertable { ), ); } - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $MessagesTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || html != null) { + map['html'] = Variable(html); + } + if (!nullToAbsent || mml != null) { + map['mml'] = Variable(mml); + } + if (!nullToAbsent || deletedReplyCount != null) { + map['deleted_reply_count'] = Variable(deletedReplyCount); + } + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $MessagesTable.$convertercustomn.toSql(custom), ); } return map; @@ -2536,7 +2622,10 @@ class MessageEntity extends DataClass implements Insertable { restrictedVisibility: serializer.fromJson?>( json['restrictedVisibility'], ), - extraData: serializer.fromJson?>(json['extraData']), + html: serializer.fromJson(json['html']), + mml: serializer.fromJson(json['mml']), + deletedReplyCount: serializer.fromJson(json['deletedReplyCount']), + custom: serializer.fromJson?>(json['custom']), ); } @override @@ -2585,7 +2674,10 @@ class MessageEntity extends DataClass implements Insertable { 'restrictedVisibility': serializer.toJson?>( restrictedVisibility, ), - 'extraData': serializer.toJson?>(extraData), + 'html': serializer.toJson(html), + 'mml': serializer.toJson(mml), + 'deletedReplyCount': serializer.toJson(deletedReplyCount), + 'custom': serializer.toJson?>(custom), }; } @@ -2626,7 +2718,10 @@ class MessageEntity extends DataClass implements Insertable { String? channelCid, Value?> i18n = const Value.absent(), Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent(), + Value html = const Value.absent(), + Value mml = const Value.absent(), + Value deletedReplyCount = const Value.absent(), + Value?> custom = const Value.absent(), }) => MessageEntity( id: id ?? this.id, messageText: messageText.present ? messageText.value : this.messageText, @@ -2664,7 +2759,10 @@ class MessageEntity extends DataClass implements Insertable { channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, restrictedVisibility: restrictedVisibility.present ? restrictedVisibility.value : this.restrictedVisibility, - extraData: extraData.present ? extraData.value : this.extraData, + html: html.present ? html.value : this.html, + mml: mml.present ? mml.value : this.mml, + deletedReplyCount: deletedReplyCount.present ? deletedReplyCount.value : this.deletedReplyCount, + custom: custom.present ? custom.value : this.custom, ); MessageEntity copyWithCompanion(MessagesCompanion data) { return MessageEntity( @@ -2708,7 +2806,10 @@ class MessageEntity extends DataClass implements Insertable { restrictedVisibility: data.restrictedVisibility.present ? data.restrictedVisibility.value : this.restrictedVisibility, - extraData: data.extraData.present ? data.extraData.value : this.extraData, + html: data.html.present ? data.html.value : this.html, + mml: data.mml.present ? data.mml.value : this.mml, + deletedReplyCount: data.deletedReplyCount.present ? data.deletedReplyCount.value : this.deletedReplyCount, + custom: data.custom.present ? data.custom.value : this.custom, ); } @@ -2751,7 +2852,10 @@ class MessageEntity extends DataClass implements Insertable { ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') ..write('restrictedVisibility: $restrictedVisibility, ') - ..write('extraData: $extraData') + ..write('html: $html, ') + ..write('mml: $mml, ') + ..write('deletedReplyCount: $deletedReplyCount, ') + ..write('custom: $custom') ..write(')')) .toString(); } @@ -2794,7 +2898,10 @@ class MessageEntity extends DataClass implements Insertable { channelCid, i18n, restrictedVisibility, - extraData, + html, + mml, + deletedReplyCount, + custom, ]); @override bool operator ==(Object other) => @@ -2836,7 +2943,10 @@ class MessageEntity extends DataClass implements Insertable { other.channelCid == this.channelCid && other.i18n == this.i18n && other.restrictedVisibility == this.restrictedVisibility && - other.extraData == this.extraData); + other.html == this.html && + other.mml == this.mml && + other.deletedReplyCount == this.deletedReplyCount && + other.custom == this.custom); } class MessagesCompanion extends UpdateCompanion { @@ -2876,7 +2986,10 @@ class MessagesCompanion extends UpdateCompanion { final Value channelCid; final Value?> i18n; final Value?> restrictedVisibility; - final Value?> extraData; + final Value html; + final Value mml; + final Value deletedReplyCount; + final Value?> custom; final Value rowid; const MessagesCompanion({ this.id = const Value.absent(), @@ -2915,7 +3028,10 @@ class MessagesCompanion extends UpdateCompanion { this.channelCid = const Value.absent(), this.i18n = const Value.absent(), this.restrictedVisibility = const Value.absent(), - this.extraData = const Value.absent(), + this.html = const Value.absent(), + this.mml = const Value.absent(), + this.deletedReplyCount = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }); MessagesCompanion.insert({ @@ -2955,14 +3071,17 @@ class MessagesCompanion extends UpdateCompanion { required String channelCid, this.i18n = const Value.absent(), this.restrictedVisibility = const Value.absent(), - this.extraData = const Value.absent(), + this.html = const Value.absent(), + this.mml = const Value.absent(), + this.deletedReplyCount = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), state = Value(state), mentionedUsers = Value(mentionedUsers), channelCid = Value(channelCid); - static Insertable custom({ + static Insertable createCustom({ Expression? id, Expression? messageText, Expression? attachments, @@ -2999,7 +3118,10 @@ class MessagesCompanion extends UpdateCompanion { Expression? channelCid, Expression? i18n, Expression? restrictedVisibility, - Expression? extraData, + Expression? html, + Expression? mml, + Expression? deletedReplyCount, + Expression? custom, Expression? rowid, }) { return RawValuesInsertable({ @@ -3039,7 +3161,10 @@ class MessagesCompanion extends UpdateCompanion { if (channelCid != null) 'channel_cid': channelCid, if (i18n != null) 'i18n': i18n, if (restrictedVisibility != null) 'restricted_visibility': restrictedVisibility, - if (extraData != null) 'extra_data': extraData, + if (html != null) 'html': html, + if (mml != null) 'mml': mml, + if (deletedReplyCount != null) 'deleted_reply_count': deletedReplyCount, + if (custom != null) 'custom': custom, if (rowid != null) 'rowid': rowid, }); } @@ -3081,7 +3206,10 @@ class MessagesCompanion extends UpdateCompanion { Value? channelCid, Value?>? i18n, Value?>? restrictedVisibility, - Value?>? extraData, + Value? html, + Value? mml, + Value? deletedReplyCount, + Value?>? custom, Value? rowid, }) { return MessagesCompanion( @@ -3121,7 +3249,10 @@ class MessagesCompanion extends UpdateCompanion { channelCid: channelCid ?? this.channelCid, i18n: i18n ?? this.i18n, restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, - extraData: extraData ?? this.extraData, + html: html ?? this.html, + mml: mml ?? this.mml, + deletedReplyCount: deletedReplyCount ?? this.deletedReplyCount, + custom: custom ?? this.custom, rowid: rowid ?? this.rowid, ); } @@ -3259,9 +3390,18 @@ class MessagesCompanion extends UpdateCompanion { ), ); } - if (extraData.present) { - map['extra_data'] = Variable( - $MessagesTable.$converterextraDatan.toSql(extraData.value), + if (html.present) { + map['html'] = Variable(html.value); + } + if (mml.present) { + map['mml'] = Variable(mml.value); + } + if (deletedReplyCount.present) { + map['deleted_reply_count'] = Variable(deletedReplyCount.value); + } + if (custom.present) { + map['custom'] = Variable( + $MessagesTable.$convertercustomn.toSql(custom.value), ); } if (rowid.present) { @@ -3309,7 +3449,10 @@ class MessagesCompanion extends UpdateCompanion { ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') ..write('restrictedVisibility: $restrictedVisibility, ') - ..write('extraData: $extraData, ') + ..write('html: $html, ') + ..write('mml: $mml, ') + ..write('deletedReplyCount: $deletedReplyCount, ') + ..write('custom: $custom, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -3466,16 +3609,13 @@ class $DraftMessagesTable extends DraftMessages with TableInfo<$DraftMessagesTab ), ); @override - late final GeneratedColumnWithTypeConverter?, String> extraData = - GeneratedColumn( - 'extra_data', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ).withConverter?>( - $DraftMessagesTable.$converterextraDatan, - ); + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( + 'custom', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($DraftMessagesTable.$convertercustomn); @override List get $columns => [ id, @@ -3491,7 +3631,7 @@ class $DraftMessagesTable extends DraftMessages with TableInfo<$DraftMessagesTab silent, createdAt, channelCid, - extraData, + custom, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -3646,10 +3786,10 @@ class $DraftMessagesTable extends DraftMessages with TableInfo<$DraftMessagesTab DriftSqlType.string, data['${effectivePrefix}channel_cid'], )!, - extraData: $DraftMessagesTable.$converterextraDatan.fromSql( + custom: $DraftMessagesTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), ); @@ -3662,9 +3802,9 @@ class $DraftMessagesTable extends DraftMessages with TableInfo<$DraftMessagesTab static TypeConverter, String> $converterattachments = ListConverter(); static TypeConverter, String> $convertermentionedUsers = ListConverter(); - static TypeConverter, String> $converterextraData = MapConverter(); - static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( - $converterextraData, + static TypeConverter, String> $convertercustom = MapConverter(); + static TypeConverter?, String?> $convertercustomn = NullAwareTypeConverter.wrap( + $convertercustom, ); } @@ -3709,8 +3849,8 @@ class DraftMessageEntity extends DataClass implements Insertable? extraData; + /// Custom data attached to the draft message. + final Map? custom; const DraftMessageEntity({ required this.id, this.messageText, @@ -3725,7 +3865,7 @@ class DraftMessageEntity extends DataClass implements Insertable toColumns(bool nullToAbsent) { @@ -3763,9 +3903,9 @@ class DraftMessageEntity extends DataClass implements Insertable(silent); map['created_at'] = Variable(createdAt); map['channel_cid'] = Variable(channelCid); - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $DraftMessagesTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $DraftMessagesTable.$convertercustomn.toSql(custom), ); } return map; @@ -3790,7 +3930,7 @@ class DraftMessageEntity extends DataClass implements Insertable(json['silent']), createdAt: serializer.fromJson(json['createdAt']), channelCid: serializer.fromJson(json['channelCid']), - extraData: serializer.fromJson?>(json['extraData']), + custom: serializer.fromJson?>(json['custom']), ); } @override @@ -3810,7 +3950,7 @@ class DraftMessageEntity extends DataClass implements Insertable(silent), 'createdAt': serializer.toJson(createdAt), 'channelCid': serializer.toJson(channelCid), - 'extraData': serializer.toJson?>(extraData), + 'custom': serializer.toJson?>(custom), }; } @@ -3828,7 +3968,7 @@ class DraftMessageEntity extends DataClass implements Insertable?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), }) => DraftMessageEntity( id: id ?? this.id, messageText: messageText.present ? messageText.value : this.messageText, @@ -3843,7 +3983,7 @@ class DraftMessageEntity extends DataClass implements Insertable @@ -3919,7 +4059,7 @@ class DraftMessageEntity extends DataClass implements Insertable { @@ -3936,7 +4076,7 @@ class DraftMessagesCompanion extends UpdateCompanion { final Value silent; final Value createdAt; final Value channelCid; - final Value?> extraData; + final Value?> custom; final Value rowid; const DraftMessagesCompanion({ this.id = const Value.absent(), @@ -3952,7 +4092,7 @@ class DraftMessagesCompanion extends UpdateCompanion { this.silent = const Value.absent(), this.createdAt = const Value.absent(), this.channelCid = const Value.absent(), - this.extraData = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }); DraftMessagesCompanion.insert({ @@ -3969,13 +4109,13 @@ class DraftMessagesCompanion extends UpdateCompanion { this.silent = const Value.absent(), this.createdAt = const Value.absent(), required String channelCid, - this.extraData = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), mentionedUsers = Value(mentionedUsers), channelCid = Value(channelCid); - static Insertable custom({ + static Insertable createCustom({ Expression? id, Expression? messageText, Expression? attachments, @@ -3989,7 +4129,7 @@ class DraftMessagesCompanion extends UpdateCompanion { Expression? silent, Expression? createdAt, Expression? channelCid, - Expression? extraData, + Expression? custom, Expression? rowid, }) { return RawValuesInsertable({ @@ -4006,7 +4146,7 @@ class DraftMessagesCompanion extends UpdateCompanion { if (silent != null) 'silent': silent, if (createdAt != null) 'created_at': createdAt, if (channelCid != null) 'channel_cid': channelCid, - if (extraData != null) 'extra_data': extraData, + if (custom != null) 'custom': custom, if (rowid != null) 'rowid': rowid, }); } @@ -4025,7 +4165,7 @@ class DraftMessagesCompanion extends UpdateCompanion { Value? silent, Value? createdAt, Value? channelCid, - Value?>? extraData, + Value?>? custom, Value? rowid, }) { return DraftMessagesCompanion( @@ -4042,7 +4182,7 @@ class DraftMessagesCompanion extends UpdateCompanion { silent: silent ?? this.silent, createdAt: createdAt ?? this.createdAt, channelCid: channelCid ?? this.channelCid, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, rowid: rowid ?? this.rowid, ); } @@ -4095,9 +4235,9 @@ class DraftMessagesCompanion extends UpdateCompanion { if (channelCid.present) { map['channel_cid'] = Variable(channelCid.value); } - if (extraData.present) { - map['extra_data'] = Variable( - $DraftMessagesTable.$converterextraDatan.toSql(extraData.value), + if (custom.present) { + map['custom'] = Variable( + $DraftMessagesTable.$convertercustomn.toSql(custom.value), ); } if (rowid.present) { @@ -4122,7 +4262,7 @@ class DraftMessagesCompanion extends UpdateCompanion { ..write('silent: $silent, ') ..write('createdAt: $createdAt, ') ..write('channelCid: $channelCid, ') - ..write('extraData: $extraData, ') + ..write('custom: $custom, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -5094,16 +5234,45 @@ class $PinnedMessagesTable extends PinnedMessages with TableInfo<$PinnedMessages ).withConverter?>( $PinnedMessagesTable.$converterrestrictedVisibilityn, ); + static const VerificationMeta _htmlMeta = const VerificationMeta('html'); @override - late final GeneratedColumnWithTypeConverter?, String> extraData = + late final GeneratedColumn html = GeneratedColumn( + 'html', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _mmlMeta = const VerificationMeta('mml'); + @override + late final GeneratedColumn mml = GeneratedColumn( + 'mml', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _deletedReplyCountMeta = const VerificationMeta( + 'deletedReplyCount', + ); + @override + late final GeneratedColumn deletedReplyCount = GeneratedColumn( + 'deleted_reply_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( - 'extra_data', + 'custom', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ).withConverter?>( - $PinnedMessagesTable.$converterextraDatan, + $PinnedMessagesTable.$convertercustomn, ); @override List get $columns => [ @@ -5143,7 +5312,10 @@ class $PinnedMessagesTable extends PinnedMessages with TableInfo<$PinnedMessages channelCid, i18n, restrictedVisibility, - extraData, + html, + mml, + deletedReplyCount, + custom, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -5373,6 +5545,27 @@ class $PinnedMessagesTable extends PinnedMessages with TableInfo<$PinnedMessages } else if (isInserting) { context.missing(_channelCidMeta); } + if (data.containsKey('html')) { + context.handle( + _htmlMeta, + html.isAcceptableOrUnknown(data['html']!, _htmlMeta), + ); + } + if (data.containsKey('mml')) { + context.handle( + _mmlMeta, + mml.isAcceptableOrUnknown(data['mml']!, _mmlMeta), + ); + } + if (data.containsKey('deleted_reply_count')) { + context.handle( + _deletedReplyCountMeta, + deletedReplyCount.isAcceptableOrUnknown( + data['deleted_reply_count']!, + _deletedReplyCountMeta, + ), + ); + } return context; } @@ -5542,10 +5735,22 @@ class $PinnedMessagesTable extends PinnedMessages with TableInfo<$PinnedMessages data['${effectivePrefix}restricted_visibility'], ), ), - extraData: $PinnedMessagesTable.$converterextraDatan.fromSql( + html: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}html'], + ), + mml: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mml'], + ), + deletedReplyCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_reply_count'], + ), + custom: $PinnedMessagesTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), ); @@ -5579,9 +5784,9 @@ class $PinnedMessagesTable extends PinnedMessages with TableInfo<$PinnedMessages static TypeConverter?, String?> $converterrestrictedVisibilityn = NullAwareTypeConverter.wrap( $converterrestrictedVisibility, ); - static TypeConverter, String> $converterextraData = MapConverter(); - static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( - $converterextraData, + static TypeConverter, String> $convertercustom = MapConverter(); + static TypeConverter?, String?> $convertercustomn = NullAwareTypeConverter.wrap( + $convertercustom, ); } @@ -5696,8 +5901,17 @@ class PinnedMessageEntity extends DataClass implements Insertable? restrictedVisibility; - /// Message custom extraData - final Map? extraData; + /// The message rendered as HTML. + final String? html; + + /// The message markup language (MML) source of the message. + final String? mml; + + /// The number of replies to the message that were deleted. + final int? deletedReplyCount; + + /// Custom data attached to the message. + final Map? custom; const PinnedMessageEntity({ required this.id, this.messageText, @@ -5735,7 +5949,10 @@ class PinnedMessageEntity extends DataClass implements Insertable toColumns(bool nullToAbsent) { @@ -5856,9 +6073,18 @@ class PinnedMessageEntity extends DataClass implements Insertable( - $PinnedMessagesTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || html != null) { + map['html'] = Variable(html); + } + if (!nullToAbsent || mml != null) { + map['mml'] = Variable(mml); + } + if (!nullToAbsent || deletedReplyCount != null) { + map['deleted_reply_count'] = Variable(deletedReplyCount); + } + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $PinnedMessagesTable.$convertercustomn.toSql(custom), ); } return map; @@ -5918,7 +6144,10 @@ class PinnedMessageEntity extends DataClass implements Insertable?>( json['restrictedVisibility'], ), - extraData: serializer.fromJson?>(json['extraData']), + html: serializer.fromJson(json['html']), + mml: serializer.fromJson(json['mml']), + deletedReplyCount: serializer.fromJson(json['deletedReplyCount']), + custom: serializer.fromJson?>(json['custom']), ); } @override @@ -5967,7 +6196,10 @@ class PinnedMessageEntity extends DataClass implements Insertable?>( restrictedVisibility, ), - 'extraData': serializer.toJson?>(extraData), + 'html': serializer.toJson(html), + 'mml': serializer.toJson(mml), + 'deletedReplyCount': serializer.toJson(deletedReplyCount), + 'custom': serializer.toJson?>(custom), }; } @@ -6008,7 +6240,10 @@ class PinnedMessageEntity extends DataClass implements Insertable?> i18n = const Value.absent(), Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent(), + Value html = const Value.absent(), + Value mml = const Value.absent(), + Value deletedReplyCount = const Value.absent(), + Value?> custom = const Value.absent(), }) => PinnedMessageEntity( id: id ?? this.id, messageText: messageText.present ? messageText.value : this.messageText, @@ -6046,7 +6281,10 @@ class PinnedMessageEntity extends DataClass implements Insertable @@ -6218,7 +6465,10 @@ class PinnedMessageEntity extends DataClass implements Insertable { @@ -6258,7 +6508,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value channelCid; final Value?> i18n; final Value?> restrictedVisibility; - final Value?> extraData; + final Value html; + final Value mml; + final Value deletedReplyCount; + final Value?> custom; final Value rowid; const PinnedMessagesCompanion({ this.id = const Value.absent(), @@ -6297,7 +6550,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.channelCid = const Value.absent(), this.i18n = const Value.absent(), this.restrictedVisibility = const Value.absent(), - this.extraData = const Value.absent(), + this.html = const Value.absent(), + this.mml = const Value.absent(), + this.deletedReplyCount = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }); PinnedMessagesCompanion.insert({ @@ -6337,14 +6593,17 @@ class PinnedMessagesCompanion extends UpdateCompanion { required String channelCid, this.i18n = const Value.absent(), this.restrictedVisibility = const Value.absent(), - this.extraData = const Value.absent(), + this.html = const Value.absent(), + this.mml = const Value.absent(), + this.deletedReplyCount = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), state = Value(state), mentionedUsers = Value(mentionedUsers), channelCid = Value(channelCid); - static Insertable custom({ + static Insertable createCustom({ Expression? id, Expression? messageText, Expression? attachments, @@ -6381,7 +6640,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? channelCid, Expression? i18n, Expression? restrictedVisibility, - Expression? extraData, + Expression? html, + Expression? mml, + Expression? deletedReplyCount, + Expression? custom, Expression? rowid, }) { return RawValuesInsertable({ @@ -6421,7 +6683,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (channelCid != null) 'channel_cid': channelCid, if (i18n != null) 'i18n': i18n, if (restrictedVisibility != null) 'restricted_visibility': restrictedVisibility, - if (extraData != null) 'extra_data': extraData, + if (html != null) 'html': html, + if (mml != null) 'mml': mml, + if (deletedReplyCount != null) 'deleted_reply_count': deletedReplyCount, + if (custom != null) 'custom': custom, if (rowid != null) 'rowid': rowid, }); } @@ -6463,7 +6728,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? channelCid, Value?>? i18n, Value?>? restrictedVisibility, - Value?>? extraData, + Value? html, + Value? mml, + Value? deletedReplyCount, + Value?>? custom, Value? rowid, }) { return PinnedMessagesCompanion( @@ -6503,7 +6771,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { channelCid: channelCid ?? this.channelCid, i18n: i18n ?? this.i18n, restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, - extraData: extraData ?? this.extraData, + html: html ?? this.html, + mml: mml ?? this.mml, + deletedReplyCount: deletedReplyCount ?? this.deletedReplyCount, + custom: custom ?? this.custom, rowid: rowid ?? this.rowid, ); } @@ -6649,9 +6920,18 @@ class PinnedMessagesCompanion extends UpdateCompanion { ), ); } - if (extraData.present) { - map['extra_data'] = Variable( - $PinnedMessagesTable.$converterextraDatan.toSql(extraData.value), + if (html.present) { + map['html'] = Variable(html.value); + } + if (mml.present) { + map['mml'] = Variable(mml.value); + } + if (deletedReplyCount.present) { + map['deleted_reply_count'] = Variable(deletedReplyCount.value); + } + if (custom.present) { + map['custom'] = Variable( + $PinnedMessagesTable.$convertercustomn.toSql(custom.value), ); } if (rowid.present) { @@ -6699,7 +6979,10 @@ class PinnedMessagesCompanion extends UpdateCompanion { ..write('channelCid: $channelCid, ') ..write('i18n: $i18n, ') ..write('restrictedVisibility: $restrictedVisibility, ') - ..write('extraData: $extraData, ') + ..write('html: $html, ') + ..write('mml: $mml, ') + ..write('deletedReplyCount: $deletedReplyCount, ') + ..write('custom: $custom, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -8386,15 +8669,15 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions defaultValue: const Constant(0), ); @override - late final GeneratedColumnWithTypeConverter?, String> extraData = + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( - 'extra_data', + 'custom', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ).withConverter?>( - $PinnedMessageReactionsTable.$converterextraDatan, + $PinnedMessageReactionsTable.$convertercustomn, ); @override List get $columns => [ @@ -8405,7 +8688,7 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions createdAt, updatedAt, score, - extraData, + custom, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -8503,10 +8786,10 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions DriftSqlType.int, data['${effectivePrefix}score'], )!, - extraData: $PinnedMessageReactionsTable.$converterextraDatan.fromSql( + custom: $PinnedMessageReactionsTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), ); @@ -8517,9 +8800,9 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions return $PinnedMessageReactionsTable(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, ); } @@ -8545,8 +8828,8 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable? extraData; + /// Custom data attached to the reaction. + final Map? custom; const PinnedMessageReactionEntity({ this.userId, this.messageId, @@ -8555,7 +8838,7 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable toColumns(bool nullToAbsent) { @@ -8573,9 +8856,9 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable(createdAt); map['updated_at'] = Variable(updatedAt); map['score'] = Variable(score); - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $PinnedMessageReactionsTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $PinnedMessageReactionsTable.$convertercustomn.toSql(custom), ); } return map; @@ -8594,7 +8877,7 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), score: serializer.fromJson(json['score']), - extraData: serializer.fromJson?>(json['extraData']), + custom: serializer.fromJson?>(json['custom']), ); } @override @@ -8608,7 +8891,7 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'score': serializer.toJson(score), - 'extraData': serializer.toJson?>(extraData), + 'custom': serializer.toJson?>(custom), }; } @@ -8620,7 +8903,7 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), }) => PinnedMessageReactionEntity( userId: userId.present ? userId.value : this.userId, messageId: messageId.present ? messageId.value : this.messageId, @@ -8629,7 +8912,7 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable @@ -8683,7 +8966,7 @@ class PinnedMessageReactionEntity extends DataClass implements Insertable { @@ -8694,7 +8977,7 @@ class PinnedMessageReactionsCompanion extends UpdateCompanion createdAt; final Value updatedAt; final Value score; - final Value?> extraData; + final Value?> custom; final Value rowid; const PinnedMessageReactionsCompanion({ this.userId = const Value.absent(), @@ -8704,7 +8987,7 @@ class PinnedMessageReactionsCompanion extends UpdateCompanion custom({ + static Insertable createCustom({ Expression? userId, Expression? messageId, Expression? type, @@ -8726,7 +9009,7 @@ class PinnedMessageReactionsCompanion extends UpdateCompanion? createdAt, Expression? updatedAt, Expression? score, - Expression? extraData, + Expression? custom, Expression? rowid, }) { return RawValuesInsertable({ @@ -8737,7 +9020,7 @@ class PinnedMessageReactionsCompanion extends UpdateCompanion? createdAt, Value? updatedAt, Value? score, - Value?>? extraData, + Value?>? custom, Value? rowid, }) { return PinnedMessageReactionsCompanion( @@ -8761,7 +9044,7 @@ class PinnedMessageReactionsCompanion extends UpdateCompanion(score.value); } - if (extraData.present) { - map['extra_data'] = Variable( - $PinnedMessageReactionsTable.$converterextraDatan.toSql( - extraData.value, - ), + if (custom.present) { + map['custom'] = Variable( + $PinnedMessageReactionsTable.$convertercustomn.toSql(custom.value), ); } if (rowid.present) { @@ -8813,7 +9094,7 @@ class PinnedMessageReactionsCompanion extends UpdateCompanion?, String> extraData = GeneratedColumn( - 'extra_data', + late final GeneratedColumnWithTypeConverter?, String> custom = GeneratedColumn( + 'custom', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - ).withConverter?>($ReactionsTable.$converterextraDatan); + ).withConverter?>($ReactionsTable.$convertercustomn); @override List get $columns => [ userId, @@ -8919,7 +9200,7 @@ class $ReactionsTable extends Reactions with TableInfo<$ReactionsTable, Reaction createdAt, updatedAt, score, - extraData, + custom, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -9014,10 +9295,10 @@ class $ReactionsTable extends Reactions with TableInfo<$ReactionsTable, Reaction DriftSqlType.int, data['${effectivePrefix}score'], )!, - extraData: $ReactionsTable.$converterextraDatan.fromSql( + custom: $ReactionsTable.$convertercustomn.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}extra_data'], + data['${effectivePrefix}custom'], ), ), ); @@ -9028,9 +9309,9 @@ class $ReactionsTable extends Reactions with TableInfo<$ReactionsTable, Reaction return $ReactionsTable(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, ); } @@ -9056,8 +9337,8 @@ class ReactionEntity extends DataClass implements Insertable { /// The score of the reaction (ie. number of reactions sent) final int score; - /// Reaction custom extraData - final Map? extraData; + /// Custom data attached to the reaction. + final Map? custom; const ReactionEntity({ this.userId, this.messageId, @@ -9066,7 +9347,7 @@ class ReactionEntity extends DataClass implements Insertable { required this.createdAt, required this.updatedAt, required this.score, - this.extraData, + this.custom, }); @override Map toColumns(bool nullToAbsent) { @@ -9084,9 +9365,9 @@ class ReactionEntity extends DataClass implements Insertable { map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); map['score'] = Variable(score); - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $ReactionsTable.$converterextraDatan.toSql(extraData), + if (!nullToAbsent || custom != null) { + map['custom'] = Variable( + $ReactionsTable.$convertercustomn.toSql(custom), ); } return map; @@ -9105,7 +9386,7 @@ class ReactionEntity extends DataClass implements Insertable { createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), score: serializer.fromJson(json['score']), - extraData: serializer.fromJson?>(json['extraData']), + custom: serializer.fromJson?>(json['custom']), ); } @override @@ -9119,7 +9400,7 @@ class ReactionEntity extends DataClass implements Insertable { 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'score': serializer.toJson(score), - 'extraData': serializer.toJson?>(extraData), + 'custom': serializer.toJson?>(custom), }; } @@ -9131,7 +9412,7 @@ class ReactionEntity extends DataClass implements Insertable { DateTime? createdAt, DateTime? updatedAt, int? score, - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), }) => ReactionEntity( userId: userId.present ? userId.value : this.userId, messageId: messageId.present ? messageId.value : this.messageId, @@ -9140,7 +9421,7 @@ class ReactionEntity extends DataClass implements Insertable { createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, score: score ?? this.score, - extraData: extraData.present ? extraData.value : this.extraData, + custom: custom.present ? custom.value : this.custom, ); ReactionEntity copyWithCompanion(ReactionsCompanion data) { return ReactionEntity( @@ -9151,7 +9432,7 @@ class ReactionEntity extends DataClass implements Insertable { createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, score: data.score.present ? data.score.value : this.score, - extraData: data.extraData.present ? data.extraData.value : this.extraData, + custom: data.custom.present ? data.custom.value : this.custom, ); } @@ -9165,7 +9446,7 @@ class ReactionEntity extends DataClass implements Insertable { ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('score: $score, ') - ..write('extraData: $extraData') + ..write('custom: $custom') ..write(')')) .toString(); } @@ -9179,7 +9460,7 @@ class ReactionEntity extends DataClass implements Insertable { createdAt, updatedAt, score, - extraData, + custom, ); @override bool operator ==(Object other) => @@ -9192,7 +9473,7 @@ class ReactionEntity extends DataClass implements Insertable { other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && other.score == this.score && - other.extraData == this.extraData); + other.custom == this.custom); } class ReactionsCompanion extends UpdateCompanion { @@ -9203,7 +9484,7 @@ class ReactionsCompanion extends UpdateCompanion { final Value createdAt; final Value updatedAt; final Value score; - final Value?> extraData; + final Value?> custom; final Value rowid; const ReactionsCompanion({ this.userId = const Value.absent(), @@ -9213,7 +9494,7 @@ class ReactionsCompanion extends UpdateCompanion { this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), this.score = const Value.absent(), - this.extraData = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }); ReactionsCompanion.insert({ @@ -9224,10 +9505,10 @@ class ReactionsCompanion extends UpdateCompanion { this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), this.score = const Value.absent(), - this.extraData = const Value.absent(), + this.custom = const Value.absent(), this.rowid = const Value.absent(), }) : type = Value(type); - static Insertable custom({ + static Insertable createCustom({ Expression? userId, Expression? messageId, Expression? type, @@ -9235,7 +9516,7 @@ class ReactionsCompanion extends UpdateCompanion { Expression? createdAt, Expression? updatedAt, Expression? score, - Expression? extraData, + Expression? custom, Expression? rowid, }) { return RawValuesInsertable({ @@ -9246,7 +9527,7 @@ class ReactionsCompanion extends UpdateCompanion { if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, if (score != null) 'score': score, - if (extraData != null) 'extra_data': extraData, + if (custom != null) 'custom': custom, if (rowid != null) 'rowid': rowid, }); } @@ -9259,7 +9540,7 @@ class ReactionsCompanion extends UpdateCompanion { Value? createdAt, Value? updatedAt, Value? score, - Value?>? extraData, + Value?>? custom, Value? rowid, }) { return ReactionsCompanion( @@ -9270,7 +9551,7 @@ class ReactionsCompanion extends UpdateCompanion { createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, score: score ?? this.score, - extraData: extraData ?? this.extraData, + custom: custom ?? this.custom, rowid: rowid ?? this.rowid, ); } @@ -9299,9 +9580,9 @@ class ReactionsCompanion extends UpdateCompanion { if (score.present) { map['score'] = Variable(score.value); } - if (extraData.present) { - map['extra_data'] = Variable( - $ReactionsTable.$converterextraDatan.toSql(extraData.value), + if (custom.present) { + map['custom'] = Variable( + $ReactionsTable.$convertercustomn.toSql(custom.value), ); } if (rowid.present) { @@ -9320,7 +9601,7 @@ class ReactionsCompanion extends UpdateCompanion { ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('score: $score, ') - ..write('extraData: $extraData, ') + ..write('custom: $custom, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -14104,7 +14385,10 @@ typedef $$MessagesTableCreateCompanionBuilder = required String channelCid, Value?> i18n, Value?> restrictedVisibility, - Value?> extraData, + Value html, + Value mml, + Value deletedReplyCount, + Value?> custom, Value rowid, }); typedef $$MessagesTableUpdateCompanionBuilder = @@ -14145,7 +14429,10 @@ typedef $$MessagesTableUpdateCompanionBuilder = Value channelCid, Value?> i18n, Value?> restrictedVisibility, - Value?> extraData, + Value html, + Value mml, + Value deletedReplyCount, + Value?> custom, Value rowid, }); @@ -14409,11 +14696,25 @@ class $$MessagesTableFilterComposer extends Composer<_$DriftChatDatabase, $Messa builder: (column) => ColumnWithTypeConverterFilters(column), ); - ColumnWithTypeConverterFilters?, Map, String> get extraData => - $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column), - ); + ColumnFilters get html => $composableBuilder( + column: $table.html, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get mml => $composableBuilder( + column: $table.mml, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedReplyCount => $composableBuilder( + column: $table.deletedReplyCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters?, Map, String> get custom => $composableBuilder( + column: $table.custom, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); $$ChannelsTableFilterComposer get channelCid { final $$ChannelsTableFilterComposer composer = $composerBuilder( @@ -14693,8 +14994,23 @@ class $$MessagesTableOrderingComposer extends Composer<_$DriftChatDatabase, $Mes builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get html => $composableBuilder( + column: $table.html, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get mml => $composableBuilder( + column: $table.mml, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedReplyCount => $composableBuilder( + column: $table.deletedReplyCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, builder: (column) => ColumnOrderings(column), ); @@ -14872,8 +15188,17 @@ class $$MessagesTableAnnotationComposer extends Composer<_$DriftChatDatabase, $M builder: (column) => column, ); - GeneratedColumnWithTypeConverter?, String> get extraData => - $composableBuilder(column: $table.extraData, builder: (column) => column); + GeneratedColumn get html => $composableBuilder(column: $table.html, builder: (column) => column); + + GeneratedColumn get mml => $composableBuilder(column: $table.mml, builder: (column) => column); + + GeneratedColumn get deletedReplyCount => $composableBuilder( + column: $table.deletedReplyCount, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> get custom => + $composableBuilder(column: $table.custom, builder: (column) => column); $$ChannelsTableAnnotationComposer get channelCid { final $$ChannelsTableAnnotationComposer composer = $composerBuilder( @@ -15036,7 +15361,10 @@ class $$MessagesTableTableManager Value channelCid = const Value.absent(), Value?> i18n = const Value.absent(), Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent(), + Value html = const Value.absent(), + Value mml = const Value.absent(), + Value deletedReplyCount = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion( id: id, @@ -15075,7 +15403,10 @@ class $$MessagesTableTableManager channelCid: channelCid, i18n: i18n, restrictedVisibility: restrictedVisibility, - extraData: extraData, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, + custom: custom, rowid: rowid, ), createCompanionCallback: @@ -15116,7 +15447,10 @@ class $$MessagesTableTableManager required String channelCid, Value?> i18n = const Value.absent(), Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent(), + Value html = const Value.absent(), + Value mml = const Value.absent(), + Value deletedReplyCount = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => MessagesCompanion.insert( id: id, @@ -15155,7 +15489,10 @@ class $$MessagesTableTableManager channelCid: channelCid, i18n: i18n, restrictedVisibility: restrictedVisibility, - extraData: extraData, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, + custom: custom, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -15295,7 +15632,7 @@ typedef $$DraftMessagesTableCreateCompanionBuilder = Value silent, Value createdAt, required String channelCid, - Value?> extraData, + Value?> custom, Value rowid, }); typedef $$DraftMessagesTableUpdateCompanionBuilder = @@ -15313,7 +15650,7 @@ typedef $$DraftMessagesTableUpdateCompanionBuilder = Value silent, Value createdAt, Value channelCid, - Value?> extraData, + Value?> custom, Value rowid, }); @@ -15423,11 +15760,10 @@ class $$DraftMessagesTableFilterComposer extends Composer<_$DriftChatDatabase, $ 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), + ); $$MessagesTableFilterComposer get parentId { final $$MessagesTableFilterComposer composer = $composerBuilder( @@ -15537,8 +15873,8 @@ class $$DraftMessagesTableOrderingComposer extends Composer<_$DriftChatDatabase, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, builder: (column) => ColumnOrderings(column), ); @@ -15632,8 +15968,8 @@ class $$DraftMessagesTableAnnotationComposer extends Composer<_$DriftChatDatabas GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, 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); $$MessagesTableAnnotationComposer get parentId { final $$MessagesTableAnnotationComposer composer = $composerBuilder( @@ -15720,7 +16056,7 @@ class $$DraftMessagesTableTableManager Value silent = const Value.absent(), Value createdAt = const Value.absent(), Value channelCid = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => DraftMessagesCompanion( id: id, @@ -15736,7 +16072,7 @@ class $$DraftMessagesTableTableManager silent: silent, createdAt: createdAt, channelCid: channelCid, - extraData: extraData, + custom: custom, rowid: rowid, ), createCompanionCallback: @@ -15754,7 +16090,7 @@ class $$DraftMessagesTableTableManager Value silent = const Value.absent(), Value createdAt = const Value.absent(), required String channelCid, - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => DraftMessagesCompanion.insert( id: id, @@ -15770,7 +16106,7 @@ class $$DraftMessagesTableTableManager silent: silent, createdAt: createdAt, channelCid: channelCid, - extraData: extraData, + custom: custom, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -16341,7 +16677,10 @@ typedef $$PinnedMessagesTableCreateCompanionBuilder = required String channelCid, Value?> i18n, Value?> restrictedVisibility, - Value?> extraData, + Value html, + Value mml, + Value deletedReplyCount, + Value?> custom, Value rowid, }); typedef $$PinnedMessagesTableUpdateCompanionBuilder = @@ -16382,7 +16721,10 @@ typedef $$PinnedMessagesTableUpdateCompanionBuilder = Value channelCid, Value?> i18n, Value?> restrictedVisibility, - Value?> extraData, + Value html, + Value mml, + Value deletedReplyCount, + Value?> custom, Value rowid, }); @@ -16604,11 +16946,25 @@ class $$PinnedMessagesTableFilterComposer extends Composer<_$DriftChatDatabase, builder: (column) => ColumnWithTypeConverterFilters(column), ); - ColumnWithTypeConverterFilters?, Map, String> get extraData => - $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column), - ); + ColumnFilters get html => $composableBuilder( + column: $table.html, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get mml => $composableBuilder( + column: $table.mml, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedReplyCount => $composableBuilder( + column: $table.deletedReplyCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters?, Map, String> get custom => $composableBuilder( + column: $table.custom, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); Expression pinnedMessageReactionsRefs( Expression Function($$PinnedMessageReactionsTableFilterComposer f) f, @@ -16823,8 +17179,23 @@ class $$PinnedMessagesTableOrderingComposer extends Composer<_$DriftChatDatabase builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get html => $composableBuilder( + column: $table.html, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get mml => $composableBuilder( + column: $table.mml, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedReplyCount => $composableBuilder( + column: $table.deletedReplyCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, builder: (column) => ColumnOrderings(column), ); } @@ -16985,8 +17356,17 @@ class $$PinnedMessagesTableAnnotationComposer extends Composer<_$DriftChatDataba builder: (column) => column, ); - GeneratedColumnWithTypeConverter?, String> get extraData => - $composableBuilder(column: $table.extraData, builder: (column) => column); + GeneratedColumn get html => $composableBuilder(column: $table.html, builder: (column) => column); + + GeneratedColumn get mml => $composableBuilder(column: $table.mml, builder: (column) => column); + + GeneratedColumn get deletedReplyCount => $composableBuilder( + column: $table.deletedReplyCount, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> get custom => + $composableBuilder(column: $table.custom, builder: (column) => column); Expression pinnedMessageReactionsRefs( Expression Function($$PinnedMessageReactionsTableAnnotationComposer a) f, @@ -17076,7 +17456,10 @@ class $$PinnedMessagesTableTableManager Value channelCid = const Value.absent(), Value?> i18n = const Value.absent(), Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent(), + Value html = const Value.absent(), + Value mml = const Value.absent(), + Value deletedReplyCount = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => PinnedMessagesCompanion( id: id, @@ -17115,7 +17498,10 @@ class $$PinnedMessagesTableTableManager channelCid: channelCid, i18n: i18n, restrictedVisibility: restrictedVisibility, - extraData: extraData, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, + custom: custom, rowid: rowid, ), createCompanionCallback: @@ -17156,7 +17542,10 @@ class $$PinnedMessagesTableTableManager required String channelCid, Value?> i18n = const Value.absent(), Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent(), + Value html = const Value.absent(), + Value mml = const Value.absent(), + Value deletedReplyCount = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => PinnedMessagesCompanion.insert( id: id, @@ -17195,7 +17584,10 @@ class $$PinnedMessagesTableTableManager channelCid: channelCid, i18n: i18n, restrictedVisibility: restrictedVisibility, - extraData: extraData, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, + custom: custom, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -18183,7 +18575,7 @@ typedef $$PinnedMessageReactionsTableCreateCompanionBuilder = Value createdAt, Value updatedAt, Value score, - Value?> extraData, + Value?> custom, Value rowid, }); typedef $$PinnedMessageReactionsTableUpdateCompanionBuilder = @@ -18195,7 +18587,7 @@ typedef $$PinnedMessageReactionsTableUpdateCompanionBuilder = Value createdAt, Value updatedAt, Value score, - Value?> extraData, + Value?> custom, Value rowid, }); @@ -18263,11 +18655,10 @@ class $$PinnedMessageReactionsTableFilterComposer extends Composer<_$DriftChatDa 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), + ); $$PinnedMessagesTableFilterComposer get messageId { final $$PinnedMessagesTableFilterComposer composer = $composerBuilder( @@ -18331,8 +18722,8 @@ class $$PinnedMessageReactionsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, builder: (column) => ColumnOrderings(column), ); @@ -18380,8 +18771,8 @@ class $$PinnedMessageReactionsTableAnnotationComposer GeneratedColumn get score => $composableBuilder(column: $table.score, 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); $$PinnedMessagesTableAnnotationComposer get messageId { final $$PinnedMessagesTableAnnotationComposer composer = $composerBuilder( @@ -18452,7 +18843,7 @@ class $$PinnedMessageReactionsTableTableManager Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), Value score = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => PinnedMessageReactionsCompanion( userId: userId, @@ -18462,7 +18853,7 @@ class $$PinnedMessageReactionsTableTableManager createdAt: createdAt, updatedAt: updatedAt, score: score, - extraData: extraData, + custom: custom, rowid: rowid, ), createCompanionCallback: @@ -18474,7 +18865,7 @@ class $$PinnedMessageReactionsTableTableManager Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), Value score = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => PinnedMessageReactionsCompanion.insert( userId: userId, @@ -18484,7 +18875,7 @@ class $$PinnedMessageReactionsTableTableManager createdAt: createdAt, updatedAt: updatedAt, score: score, - extraData: extraData, + custom: custom, rowid: rowid, ), withReferenceMapper: (p0) => p0 @@ -18560,7 +18951,7 @@ typedef $$ReactionsTableCreateCompanionBuilder = Value createdAt, Value updatedAt, Value score, - Value?> extraData, + Value?> custom, Value rowid, }); typedef $$ReactionsTableUpdateCompanionBuilder = @@ -18572,7 +18963,7 @@ typedef $$ReactionsTableUpdateCompanionBuilder = Value createdAt, Value updatedAt, Value score, - Value?> extraData, + Value?> custom, Value rowid, }); @@ -18635,11 +19026,10 @@ class $$ReactionsTableFilterComposer extends Composer<_$DriftChatDatabase, $Reac 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), + ); $$MessagesTableFilterComposer get messageId { final $$MessagesTableFilterComposer composer = $composerBuilder( @@ -18702,8 +19092,8 @@ class $$ReactionsTableOrderingComposer extends Composer<_$DriftChatDatabase, $Re builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, + ColumnOrderings get custom => $composableBuilder( + column: $table.custom, builder: (column) => ColumnOrderings(column), ); @@ -18750,8 +19140,8 @@ class $$ReactionsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $ GeneratedColumn get score => $composableBuilder(column: $table.score, 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); $$MessagesTableAnnotationComposer get messageId { final $$MessagesTableAnnotationComposer composer = $composerBuilder( @@ -18808,7 +19198,7 @@ class $$ReactionsTableTableManager Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), Value score = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => ReactionsCompanion( userId: userId, @@ -18818,7 +19208,7 @@ class $$ReactionsTableTableManager createdAt: createdAt, updatedAt: updatedAt, score: score, - extraData: extraData, + custom: custom, rowid: rowid, ), createCompanionCallback: @@ -18830,7 +19220,7 @@ class $$ReactionsTableTableManager Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), Value score = const Value.absent(), - Value?> extraData = const Value.absent(), + Value?> custom = const Value.absent(), Value rowid = const Value.absent(), }) => ReactionsCompanion.insert( userId: userId, @@ -18840,7 +19230,7 @@ class $$ReactionsTableTableManager createdAt: createdAt, updatedAt: updatedAt, score: score, - extraData: extraData, + custom: custom, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart b/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart index 74a7c99f9d..16d7f671e6 100644 --- a/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart @@ -47,8 +47,8 @@ class DraftMessages extends Table { /// The channel cid of which this message is part of TextColumn get channelCid => text().references(Channels, #cid, onDelete: KeyAction.cascade)(); - /// Message custom extraData - TextColumn get extraData => text().nullable().map(MapConverter())(); + /// Custom data attached to the draft message. + TextColumn get custom => text().nullable().map(MapConverter())(); @override Set get primaryKey => {id}; diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 965825b787..d22491ab58 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -148,8 +148,17 @@ class Messages extends Table { /// The list of user ids that should be able to see the message. TextColumn get restrictedVisibility => text().nullable().map(ListConverter())(); - /// Message custom extraData - TextColumn get extraData => text().nullable().map(MapConverter())(); + /// The message rendered as HTML. + TextColumn get html => text().nullable()(); + + /// The message markup language (MML) source of the message. + TextColumn get mml => text().nullable()(); + + /// The number of replies to the message that were deleted. + IntColumn get deletedReplyCount => integer().nullable()(); + + /// Custom data attached to the message. + TextColumn get custom => text().nullable().map(MapConverter())(); @override Set get primaryKey => {id}; diff --git a/packages/stream_chat_persistence/lib/src/entity/reactions.dart b/packages/stream_chat_persistence/lib/src/entity/reactions.dart index 5be4f59838..71a32ab15e 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reactions.dart @@ -28,8 +28,8 @@ class Reactions extends Table { /// The score of the reaction (ie. number of reactions sent) IntColumn get score => integer().withDefault(const Constant(0))(); - /// Reaction custom extraData - TextColumn get extraData => text().nullable().map(MapConverter())(); + /// Custom data attached to the reaction. + TextColumn get custom => text().nullable().map(MapConverter())(); @override Set get primaryKey => { diff --git a/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart index 76f176e028..84d17c5a35 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart @@ -34,7 +34,7 @@ extension DraftMessageEntityX on DraftMessageEntity { command: command, poll: poll, pollId: pollId, - extraData: extraData ?? {}, + custom: custom ?? {}, ), parentId: parentId, parentMessage: parentMessage, @@ -64,6 +64,6 @@ extension DraftMessageX on Draft { silent: message.silent, command: message.command, pollId: message.pollId, - extraData: message.extraData, + custom: message.custom, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 5670d6f7c7..64a61905d5 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -23,7 +23,11 @@ extension MessageEntityX on MessageEntity { final json = jsonDecode(it); return Attachment.fromData(json); }).toList(), - extraData: extraData ?? {}, + custom: custom ?? {}, + cid: channelCid, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, createdAt: remoteCreatedAt, localCreatedAt: localCreatedAt, updatedAt: remoteUpdatedAt, @@ -91,7 +95,10 @@ extension MessageX on Message { state: jsonEncode(state.toJson()), remoteUpdatedAt: remoteUpdatedAt, localUpdatedAt: localUpdatedAt, - extraData: extraData, + custom: custom, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, userId: user?.id, channelRole: channelRole, remoteDeletedAt: remoteDeletedAt, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index ce2a7a836c..e8c98d9c3d 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -23,7 +23,11 @@ extension PinnedMessageEntityX on PinnedMessageEntity { final json = jsonDecode(it); return Attachment.fromData(json); }).toList(), - extraData: extraData ?? {}, + custom: custom ?? {}, + cid: channelCid, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, createdAt: remoteCreatedAt, localCreatedAt: localCreatedAt, updatedAt: remoteUpdatedAt, @@ -81,7 +85,10 @@ extension PMessageX on Message { state: jsonEncode(state.toJson()), remoteUpdatedAt: remoteUpdatedAt, localUpdatedAt: localUpdatedAt, - extraData: extraData, + custom: custom, + html: html, + mml: mml, + deletedReplyCount: deletedReplyCount, userId: user?.id, channelRole: channelRole, remoteDeletedAt: remoteDeletedAt, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart index 228abe1ea5..1a1719534d 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart @@ -13,7 +13,7 @@ extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity { emojiCode: emojiCode, createdAt: createdAt, updatedAt: updatedAt, - extraData: extraData ?? {}, + custom: custom ?? {}, ); } @@ -28,6 +28,6 @@ extension PReactionX on Reaction { emojiCode: emojiCode, createdAt: createdAt, updatedAt: updatedAt, - extraData: extraData, + custom: custom, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart index 940a2571c5..186686a557 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart @@ -13,7 +13,7 @@ extension ReactionEntityX on ReactionEntity { emojiCode: emojiCode, createdAt: createdAt, updatedAt: updatedAt, - extraData: extraData ?? {}, + custom: custom ?? {}, ); } @@ -28,6 +28,6 @@ extension ReactionX on Reaction { emojiCode: emojiCode, createdAt: createdAt, updatedAt: updatedAt, - extraData: extraData, + custom: custom, ); } diff --git a/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart index b912eb3941..1abe818c65 100644 --- a/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart @@ -78,7 +78,7 @@ void main() { quotedMessageId: withQuotedMessage ? messages[index].id : null, parentId: withParentMessage ? messages[index].id : null, pollId: withPoll && polls != null ? polls[index].id : null, - extraData: {'extra_test_field': 'extraTestData$index'}, + custom: {'extra_test_field': 'extraTestData$index'}, ); return Draft( diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index daf6c4d32f..46a6ab4ef1 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -44,7 +44,7 @@ void main() { shadowed: math.Random().nextBool(), replyCount: index, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Hello #$index', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), @@ -94,7 +94,7 @@ void main() { shadowed: math.Random().nextBool(), replyCount: index, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Hello #$index', quotedMessageId: messages[index].id, pinned: math.Random().nextBool(), @@ -119,7 +119,7 @@ void main() { shadowed: math.Random().nextBool(), replyCount: index, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Hello #$index', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), @@ -1087,7 +1087,7 @@ void main() { showInChannel: math.Random().nextBool(), replyCount: 4, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #4', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 2405eaf2e3..b0af8bcafa 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -44,7 +44,7 @@ void main() { shadowed: math.Random().nextBool(), replyCount: index, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), @@ -66,7 +66,7 @@ void main() { shadowed: math.Random().nextBool(), replyCount: index, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', quotedMessageId: messages[index].id, pinned: math.Random().nextBool(), @@ -86,7 +86,7 @@ void main() { shadowed: math.Random().nextBool(), replyCount: index, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #$index', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), @@ -618,7 +618,7 @@ void main() { showInChannel: math.Random().nextBool(), replyCount: 4, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text #4', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart index 5de47893cf..7caf029670 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart @@ -34,7 +34,7 @@ void main() { showInChannel: math.Random().nextBool(), replyCount: 3, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), @@ -54,7 +54,7 @@ void main() { messageId: message.id, score: count + 3, emojiCode: '😂$index', - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, ); }, ); @@ -90,7 +90,7 @@ void main() { messageId: messageId, score: 30, emojiCode: '🎈', - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, ); await pinnedMessageReactionDao.updateReactions([copyReaction, newReaction]); diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart index bd2205777f..11247d7d36 100644 --- a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -34,7 +34,7 @@ void main() { showInChannel: math.Random().nextBool(), replyCount: 3, updatedAt: DateTime.now(), - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, text: 'Dummy text', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), @@ -54,7 +54,7 @@ void main() { messageId: message.id, score: count + 3, emojiCode: '😂$index', - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, ); }, ); @@ -90,7 +90,7 @@ void main() { messageId: messageId, score: 30, emojiCode: '🎈', - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, ); await reactionDao.updateReactions([copyReaction, newReaction]); diff --git a/packages/stream_chat_persistence/test/src/mapper/draft_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/draft_message_mapper_test.dart index a476f59093..d2eca6de37 100644 --- a/packages/stream_chat_persistence/test/src/mapper/draft_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/draft_message_mapper_test.dart @@ -47,7 +47,7 @@ void main() { command: 'testCommand', pollId: poll.id, createdAt: createdAt, - extraData: {'extra_test_field': 'extraTestData'}, + custom: {'extra_test_field': 'extraTestData'}, ); final draft = entity.toDraft( @@ -64,7 +64,7 @@ void main() { expect(draft.message.showInChannel, entity.showInChannel); expect(draft.message.silent, entity.silent); expect(draft.message.command, entity.command); - expect(draft.message.extraData, entity.extraData); + expect(draft.message.custom, entity.custom); expect(draft.message.parentId, entity.parentId); expect(draft.message.quotedMessageId, entity.quotedMessageId); expect(draft.message.pollId, entity.pollId); @@ -124,7 +124,7 @@ void main() { command: 'testCommand', poll: poll, pollId: poll.id, - extraData: const {'extra_test_field': 'extraTestData'}, + custom: const {'extra_test_field': 'extraTestData'}, ); final draft = Draft( @@ -146,7 +146,7 @@ void main() { expect(entity.showInChannel, draft.message.showInChannel); expect(entity.silent, draft.message.silent); expect(entity.command, draft.message.command); - expect(entity.extraData, draft.message.extraData); + expect(entity.custom, draft.message.custom); expect(entity.parentId, draft.message.parentId); expect(entity.quotedMessageId, draft.message.quotedMessageId); expect(entity.pollId, draft.message.pollId); diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 94f5e8a994..79ba376de5 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -92,7 +92,7 @@ void main() { localUpdatedAt: DateTime.now(), remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)), messageTextUpdatedAt: DateTime.now().add(const Duration(minutes: 5)), - extraData: {'extra_test_data': 'extraData'}, + custom: {'extra_test_data': 'custom'}, userId: user.id, channelRole: 'channel_member', localDeletedAt: DateTime.now(), @@ -109,6 +109,9 @@ void main() { 'language': 'en', }, restrictedVisibility: const ['user-id-2'], + html: '

Hello

', + mml: 'Hello', + deletedReplyCount: 2, ); final message = entity.toMessage( user: user, @@ -159,7 +162,7 @@ void main() { message.messageTextUpdatedAt, isSameDateAs(entity.messageTextUpdatedAt), ); - expect(message.extraData, entity.extraData); + expect(message.custom, entity.custom); expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); @@ -181,6 +184,10 @@ void main() { expect(messageAttachment.assetUrl, entityAttachment.assetUrl); } expect(message.restrictedVisibility, entity.restrictedVisibility); + expect(message.cid, entity.channelCid); + expect(message.html, entity.html); + expect(message.mml, entity.mml); + expect(message.deletedReplyCount, entity.deletedReplyCount); }); test('toEntity should map message into MessageEntity', () { @@ -265,7 +272,7 @@ void main() { localUpdatedAt: DateTime.now(), updatedAt: DateTime.now().add(const Duration(seconds: 1)), messageTextUpdatedAt: DateTime.now().add(const Duration(minutes: 5)), - extraData: const {'extra_test_data': 'extraData'}, + custom: const {'extra_test_data': 'custom'}, user: user, channelRole: 'channel_member', localDeletedAt: DateTime.now(), @@ -282,6 +289,9 @@ void main() { 'language': 'en', }, restrictedVisibility: const ['user-id-2'], + html: '

Hello

', + mml: 'Hello', + deletedReplyCount: 2, ); final entity = message.toEntity(cid: cid); expect(entity, isA()); @@ -320,7 +330,7 @@ void main() { entity.messageTextUpdatedAt, isSameDateAs(message.messageTextUpdatedAt), ); - expect(entity.extraData, message.extraData); + expect(entity.custom, message.custom); expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); @@ -338,5 +348,8 @@ void main() { ); expect(entity.i18n, message.i18n); expect(entity.restrictedVisibility, message.restrictedVisibility); + expect(entity.html, message.html); + expect(entity.mml, message.mml); + expect(entity.deletedReplyCount, message.deletedReplyCount); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 170026665c..c537b729ae 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -76,7 +76,7 @@ void main() { localUpdatedAt: DateTime.now(), remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)), messageTextUpdatedAt: DateTime.now().add(const Duration(minutes: 5)), - extraData: {'extra_test_data': 'extraData'}, + custom: {'extra_test_data': 'custom'}, userId: user.id, channelRole: 'channel_member', localDeletedAt: DateTime.now(), @@ -93,6 +93,9 @@ void main() { 'language': 'en', }, restrictedVisibility: const ['user-id-2'], + html: '

Hello

', + mml: 'Hello', + deletedReplyCount: 2, ); final message = entity.toMessage( user: user, @@ -127,7 +130,7 @@ void main() { message.messageTextUpdatedAt, isSameDateAs(entity.messageTextUpdatedAt), ); - expect(message.extraData, entity.extraData); + expect(message.custom, entity.custom); expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); @@ -149,6 +152,10 @@ void main() { expect(messageAttachment.assetUrl, entityAttachment.assetUrl); } expect(message.restrictedVisibility, entity.restrictedVisibility); + expect(message.cid, entity.channelCid); + expect(message.html, entity.html); + expect(message.mml, entity.mml); + expect(message.deletedReplyCount, entity.deletedReplyCount); }); test('toEntity should map message into MessageEntity', () { @@ -217,7 +224,7 @@ void main() { localUpdatedAt: DateTime.now(), updatedAt: DateTime.now().add(const Duration(seconds: 1)), messageTextUpdatedAt: DateTime.now().add(const Duration(minutes: 5)), - extraData: const {'extra_test_data': 'extraData'}, + custom: const {'extra_test_data': 'custom'}, user: user, channelRole: 'channel_member', localDeletedAt: DateTime.now(), @@ -234,6 +241,9 @@ void main() { 'language': 'en', }, restrictedVisibility: const ['user-id-2'], + html: '

Hello

', + mml: 'Hello', + deletedReplyCount: 2, ); final entity = message.toPinnedEntity(cid: cid); expect(entity, isA()); @@ -256,7 +266,7 @@ void main() { message.messageTextUpdatedAt, isSameDateAs(entity.messageTextUpdatedAt), ); - expect(entity.extraData, message.extraData); + expect(entity.custom, message.custom); expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); @@ -274,5 +284,8 @@ void main() { ); expect(entity.i18n, message.i18n); expect(entity.restrictedVisibility, message.restrictedVisibility); + expect(entity.html, message.html); + expect(entity.mml, message.mml); + expect(entity.deletedReplyCount, message.deletedReplyCount); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart index d2b4a67094..add9882c78 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart @@ -18,7 +18,7 @@ void main() { emojiCode: '😂', createdAt: now, updatedAt: now.add(const Duration(minutes: 5)), - extraData: {'extra_test_data': 'extraData'}, + custom: {'extra_test_data': 'custom'}, ); final reaction = entity.toReaction(user: user); @@ -30,7 +30,7 @@ void main() { expect(reaction.emojiCode, entity.emojiCode); expect(reaction.createdAt, isSameDateAs(entity.createdAt)); expect(reaction.updatedAt, isSameDateAs(entity.updatedAt)); - expect(reaction.extraData, entity.extraData); + expect(reaction.custom, entity.custom); }); test('toEntity should map reaction into PinnedMessageReactionEntity', () { @@ -45,7 +45,7 @@ void main() { emojiCode: '😂', createdAt: now, updatedAt: now.add(const Duration(minutes: 5)), - extraData: const {'extra_test_data': 'extraData'}, + custom: const {'extra_test_data': 'custom'}, ); final entity = reaction.toPinnedEntity(); @@ -57,6 +57,6 @@ void main() { expect(entity.emojiCode, reaction.emojiCode); expect(entity.createdAt, isSameDateAs(reaction.createdAt)); expect(entity.updatedAt, isSameDateAs(reaction.updatedAt)); - expect(entity.extraData, reaction.extraData); + expect(entity.custom, reaction.custom); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart index 43a3b93cfb..b8694e8cf0 100644 --- a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart @@ -18,7 +18,7 @@ void main() { emojiCode: '😂', createdAt: now, updatedAt: now.add(const Duration(minutes: 5)), - extraData: {'extra_test_data': 'extraData'}, + custom: {'extra_test_data': 'custom'}, ); final reaction = entity.toReaction(user: user); @@ -30,7 +30,7 @@ void main() { expect(reaction.emojiCode, entity.emojiCode); expect(reaction.createdAt, isSameDateAs(entity.createdAt)); expect(reaction.updatedAt, isSameDateAs(entity.updatedAt)); - expect(reaction.extraData, entity.extraData); + expect(reaction.custom, entity.custom); }); test('toEntity should map reaction into ReactionEntity', () { @@ -45,7 +45,7 @@ void main() { emojiCode: '😂', createdAt: now, updatedAt: now.add(const Duration(minutes: 5)), - extraData: const {'extra_test_data': 'extraData'}, + custom: const {'extra_test_data': 'custom'}, ); final entity = reaction.toEntity(); @@ -57,6 +57,6 @@ void main() { expect(entity.emojiCode, reaction.emojiCode); expect(entity.createdAt, isSameDateAs(reaction.createdAt)); expect(entity.updatedAt, isSameDateAs(reaction.updatedAt)); - expect(entity.extraData, reaction.extraData); + expect(entity.custom, reaction.custom); }); }